summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-06-02 19:51:26 -0400
committerDax Raad <[email protected]>2025-06-02 19:51:37 -0400
commit786db364d26f5fe4b723ad528d90da47ba7c7157 (patch)
tree5c4a3cde1ecf18d2d261314cb4d2662afacb3e48
parent863e7a093ec5d81c79672aa01813b7ec3864e8d8 (diff)
downloadopencode-786db364d26f5fe4b723ad528d90da47ba7c7157.tar.gz
opencode-786db364d26f5fe4b723ad528d90da47ba7c7157.zip
add permission system
-rw-r--r--packages/opencode/src/permission/index.ts135
-rw-r--r--packages/opencode/src/session/session.ts4
-rw-r--r--packages/opencode/src/tool/edit.ts14
-rw-r--r--packages/opencode/src/tool/tool.ts8
4 files changed, 158 insertions, 3 deletions
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
new file mode 100644
index 000000000..24074160a
--- /dev/null
+++ b/packages/opencode/src/permission/index.ts
@@ -0,0 +1,135 @@
+import { App } from "../app/app"
+import { z } from "zod"
+import { Bus } from "../bus"
+import { Log } from "../util/log"
+
+export namespace Permission {
+ const log = Log.create({ service: "permission" })
+
+ export const Info = z
+ .object({
+ id: z.string(),
+ sessionID: z.string(),
+ title: z.string(),
+ metadata: z.record(z.any()),
+ time: z.object({
+ created: z.number(),
+ }),
+ })
+ .openapi({
+ ref: "permission.info",
+ })
+ export type Info = z.infer<typeof Info>
+
+ export const Event = {
+ Updated: Bus.event("permission.updated", Info),
+ }
+
+ const state = App.state(
+ "permission",
+ () => {
+ const pending: {
+ [sessionID: string]: {
+ [permissionID: string]: {
+ info: Info
+ resolve: () => void
+ reject: (e: any) => void
+ }
+ }
+ } = {}
+
+ const approved: {
+ [sessionID: string]: {
+ [permissionID: string]: Info
+ }
+ } = {}
+
+ return {
+ pending,
+ approved,
+ }
+ },
+ async (state) => {
+ for (const pending of Object.values(state.pending)) {
+ for (const item of Object.values(pending)) {
+ item.reject(new RejectedError(item.info.sessionID, item.info.id))
+ }
+ }
+ },
+ )
+
+ export function ask(input: {
+ id: Info["id"]
+ sessionID: Info["sessionID"]
+ title: Info["title"]
+ metadata: Info["metadata"]
+ }) {
+ const { pending, approved } = state()
+ log.info("asking", {
+ sessionID: input.sessionID,
+ permissionID: input.id,
+ })
+ if (approved[input.sessionID]?.[input.id]) {
+ log.info("previously approved", {
+ sessionID: input.sessionID,
+ permissionID: input.id,
+ })
+ return
+ }
+ const info: Info = {
+ id: input.id,
+ sessionID: input.sessionID,
+ title: input.title,
+ metadata: input.metadata,
+ time: {
+ created: Date.now(),
+ },
+ }
+ pending[input.sessionID] = pending[input.sessionID] || {}
+ return new Promise<void>((resolve, reject) => {
+ pending[input.sessionID][input.id] = {
+ info,
+ resolve,
+ reject,
+ }
+ setTimeout(() => {
+ respond({
+ sessionID: input.sessionID,
+ permissionID: input.id,
+ response: "always",
+ })
+ }, 1000)
+ Bus.publish(Event.Updated, info)
+ })
+ }
+
+ export function respond(input: {
+ sessionID: Info["sessionID"]
+ permissionID: Info["id"]
+ response: "once" | "always" | "reject"
+ }) {
+ log.info("response", input)
+ const { pending, approved } = state()
+ const match = pending[input.sessionID]?.[input.permissionID]
+ if (!match) return
+ delete pending[input.sessionID][input.permissionID]
+ if (input.response === "reject") {
+ match.reject(new RejectedError(input.sessionID, input.permissionID))
+ return
+ }
+ match.resolve()
+ if (input.response === "always") {
+ approved[input.sessionID] = approved[input.sessionID] || {}
+ approved[input.sessionID][input.permissionID] = match.info
+ }
+ }
+
+ export class RejectedError extends Error {
+ constructor(
+ public readonly sessionID: string,
+ public readonly permissionID: string,
+ ) {
+ super(`The user rejected permission to use this functionality`)
+ }
+ }
+}
diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts
index 65e9c4515..d9ae3b6e6 100644
--- a/packages/opencode/src/session/session.ts
+++ b/packages/opencode/src/session/session.ts
@@ -300,7 +300,9 @@ export namespace Session {
async execute(args, opts) {
const start = Date.now()
try {
- const result = await item.execute(args)
+ const result = await item.execute(args, {
+ sessionID: input.sessionID,
+ })
next.metadata!.tool![opts.toolCallId] = {
...result.metadata,
time: {
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
index 7f06df9e3..3dd332aee 100644
--- a/packages/opencode/src/tool/edit.ts
+++ b/packages/opencode/src/tool/edit.ts
@@ -4,6 +4,7 @@ import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import { LSP } from "../lsp"
import { diffLines } from "diff"
+import { Permission } from "../permission"
const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files.
@@ -61,7 +62,7 @@ export const EditTool = Tool.define({
oldString: z.string().describe("The text to replace"),
newString: z.string().describe("The text to replace it with"),
}),
- async execute(params) {
+ async execute(params, ctx) {
if (!params.filePath) {
throw new Error("filePath is required")
}
@@ -71,6 +72,17 @@ export const EditTool = Tool.define({
filePath = path.join(process.cwd(), filePath)
}
+ await Permission.ask({
+ id: "opencode.edit",
+ sessionID: ctx.sessionID,
+ title: "Edit this file: " + filePath,
+ metadata: {
+ filePath,
+ oldString: params.oldString,
+ newString: params.newString,
+ },
+ })
+
let contentOld = ""
let contentNew = ""
await (async () => {
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index 75ff1cfb8..68bdc254f 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/packages/opencode/src/tool/tool.ts
@@ -1,6 +1,9 @@
import type { StandardSchemaV1 } from "@standard-schema/spec"
export namespace Tool {
+ export type Context = {
+ sessionID: string
+ }
export interface Info<
Parameters extends StandardSchemaV1 = StandardSchemaV1,
Metadata extends Record<string, any> = Record<string, any>,
@@ -8,7 +11,10 @@ export namespace Tool {
id: string
description: string
parameters: Parameters
- execute(args: StandardSchemaV1.InferOutput<Parameters>): Promise<{
+ execute(
+ args: StandardSchemaV1.InferOutput<Parameters>,
+ ctx: Context,
+ ): Promise<{
metadata: Metadata
output: string
}>