From 52df7b892730f564064447e89e40649a30973c09 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 17 Mar 2026 20:21:15 -0400 Subject: [PATCH] refactor(permission): move service under namespace --- packages/opencode/src/effect/instances.ts | 2 +- packages/opencode/src/permission/evaluate.ts | 12 -- packages/opencode/src/permission/next.ts | 13 +- packages/opencode/src/permission/service.ts | 203 +++++++++--------- .../opencode/src/tool/truncate-service.ts | 4 +- .../opencode/test/permission/next.test.ts | 2 +- 6 files changed, 112 insertions(+), 124 deletions(-) delete mode 100644 packages/opencode/src/permission/evaluate.ts diff --git a/packages/opencode/src/effect/instances.ts b/packages/opencode/src/effect/instances.ts index 382fe4c0d0..dbbff5fb68 100644 --- a/packages/opencode/src/effect/instances.ts +++ b/packages/opencode/src/effect/instances.ts @@ -17,7 +17,7 @@ export { InstanceContext } from "./instance-context" export type InstanceServices = | QuestionService - | PermissionService + | PermissionService.Service | ProviderAuthService | FileWatcherService | VcsService diff --git a/packages/opencode/src/permission/evaluate.ts b/packages/opencode/src/permission/evaluate.ts deleted file mode 100644 index f891a14c60..0000000000 --- a/packages/opencode/src/permission/evaluate.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Log } from "@/util/log" -import { Wildcard } from "@/util/wildcard" -import type { Rule, Ruleset } from "./service" - -const log = Log.create({ service: "permission" }) - -export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { - const rules = rulesets.flat() - log.info("evaluate", { permission, pattern, ruleset: rules }) - const match = rules.findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) - return match ?? { action: "ask", permission, pattern: "*" } -} diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 426682ebca..69150eb1b2 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -3,8 +3,7 @@ import { Config } from "@/config/config" import { fn } from "@/util/fn" import { Wildcard } from "@/util/wildcard" import os from "os" -import { evaluate as run } from "./evaluate" -import * as S from "./service" +import { PermissionService as S } from "./service" export namespace PermissionNext { function expand(pattern: string): string { @@ -27,7 +26,7 @@ export namespace PermissionNext { export type Reply = S.Reply export const Approval = S.Approval export const Event = S.Event - export const Service = S.PermissionService + export const Service = S.Service export const RejectedError = S.RejectedError export const CorrectedError = S.CorrectedError export const DeniedError = S.DeniedError @@ -55,19 +54,19 @@ export namespace PermissionNext { } export const ask = fn(S.AskInput, async (input) => - runPromiseInstance(S.PermissionService.use((service) => service.ask(input))), + runPromiseInstance(S.Service.use((service) => service.ask(input))), ) export const reply = fn(S.ReplyInput, async (input) => - runPromiseInstance(S.PermissionService.use((service) => service.reply(input))), + runPromiseInstance(S.Service.use((service) => service.reply(input))), ) export async function list() { - return runPromiseInstance(S.PermissionService.use((service) => service.list())) + return runPromiseInstance(S.Service.use((service) => service.list())) } export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { - return run(permission, pattern, ...rulesets) + return S.evaluate(permission, pattern, ...rulesets) } const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"] diff --git a/packages/opencode/src/permission/service.ts b/packages/opencode/src/permission/service.ts index e8f98771d3..38411e9c1d 100644 --- a/packages/opencode/src/permission/service.ts +++ b/packages/opencode/src/permission/service.ts @@ -9,124 +9,130 @@ import { Log } from "@/util/log" import { Wildcard } from "@/util/wildcard" import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect" import z from "zod" -import { evaluate } from "./evaluate" import { PermissionID } from "./schema" -const log = Log.create({ service: "permission" }) +export namespace PermissionService { + const log = Log.create({ service: "permission" }) -export const Action = z.enum(["allow", "deny", "ask"]).meta({ - ref: "PermissionAction", -}) -export type Action = z.infer - -export const Rule = z - .object({ - permission: z.string(), - pattern: z.string(), - action: Action, + export const Action = z.enum(["allow", "deny", "ask"]).meta({ + ref: "PermissionAction", }) - .meta({ - ref: "PermissionRule", + export type Action = z.infer + + export const Rule = z + .object({ + permission: z.string(), + pattern: z.string(), + action: Action, + }) + .meta({ + ref: "PermissionRule", + }) + export type Rule = z.infer + + export const Ruleset = Rule.array().meta({ + ref: "PermissionRuleset", }) -export type Rule = z.infer + export type Ruleset = z.infer -export const Ruleset = Rule.array().meta({ - ref: "PermissionRuleset", -}) -export type Ruleset = z.infer - -export const Request = z - .object({ - id: PermissionID.zod, - sessionID: SessionID.zod, - permission: z.string(), - patterns: z.string().array(), - metadata: z.record(z.string(), z.any()), - always: z.string().array(), - tool: z - .object({ - messageID: MessageID.zod, - callID: z.string(), - }) - .optional(), - }) - .meta({ - ref: "PermissionRequest", - }) -export type Request = z.infer - -export const Reply = z.enum(["once", "always", "reject"]) -export type Reply = z.infer - -export const Approval = z.object({ - projectID: ProjectID.zod, - patterns: z.string().array(), -}) - -export const Event = { - Asked: BusEvent.define("permission.asked", Request), - Replied: BusEvent.define( - "permission.replied", - z.object({ + export const Request = z + .object({ + id: PermissionID.zod, sessionID: SessionID.zod, - requestID: PermissionID.zod, - reply: Reply, - }), - ), -} + permission: z.string(), + patterns: z.string().array(), + metadata: z.record(z.string(), z.any()), + always: z.string().array(), + tool: z + .object({ + messageID: MessageID.zod, + callID: z.string(), + }) + .optional(), + }) + .meta({ + ref: "PermissionRequest", + }) + export type Request = z.infer -export class RejectedError extends Schema.TaggedErrorClass()("PermissionRejectedError", {}) { - override get message() { - return "The user rejected permission to use this specific tool call." + export const Reply = z.enum(["once", "always", "reject"]) + export type Reply = z.infer + + export const Approval = z.object({ + projectID: ProjectID.zod, + patterns: z.string().array(), + }) + + export const Event = { + Asked: BusEvent.define("permission.asked", Request), + Replied: BusEvent.define( + "permission.replied", + z.object({ + sessionID: SessionID.zod, + requestID: PermissionID.zod, + reply: Reply, + }), + ), } -} -export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { - feedback: Schema.String, -}) { - override get message() { - return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` + export class RejectedError extends Schema.TaggedErrorClass()("PermissionRejectedError", {}) { + override get message() { + return "The user rejected permission to use this specific tool call." + } } -} -export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { - ruleset: Schema.Any, -}) { - override get message() { - return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` + export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { + feedback: Schema.String, + }) { + override get message() { + return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` + } } -} -export type PermissionError = DeniedError | RejectedError | CorrectedError + export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { + ruleset: Schema.Any, + }) { + override get message() { + return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` + } + } -interface PendingEntry { - info: Request - deferred: Deferred.Deferred -} + export type Error = DeniedError | RejectedError | CorrectedError -export const AskInput = Request.partial({ id: true }).extend({ - ruleset: Ruleset, -}) + export const AskInput = Request.partial({ id: true }).extend({ + ruleset: Ruleset, + }) -export const ReplyInput = z.object({ - requestID: PermissionID.zod, - reply: Reply, - message: z.string().optional(), -}) + export const ReplyInput = z.object({ + requestID: PermissionID.zod, + reply: Reply, + message: z.string().optional(), + }) -export declare namespace PermissionService { export interface Api { - readonly ask: (input: z.infer) => Effect.Effect + readonly ask: (input: z.infer) => Effect.Effect readonly reply: (input: z.infer) => Effect.Effect readonly list: () => Effect.Effect } -} -export class PermissionService extends ServiceMap.Service()( - "@opencode/PermissionNext", -) { - static readonly layer = Layer.effect( - PermissionService, + interface PendingEntry { + info: Request + deferred: Deferred.Deferred + } + + export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { + const rules = rulesets.flat() + log.info("evaluate", { permission, pattern, ruleset: rules }) + const match = rules.findLast( + (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), + ) + return match ?? { action: "ask", permission, pattern: "*" } + } + + export class Service extends ServiceMap.Service()("@opencode/PermissionNext") {} + + export const layer = Layer.effect( + Service, Effect.gen(function* () { const { project } = yield* InstanceContext const row = Database.use((db) => @@ -226,18 +232,13 @@ export class PermissionService extends ServiceMap.Service item.info) }) - return PermissionService.of({ ask, reply, list }) + return Service.of({ ask, reply, list }) }), ) } diff --git a/packages/opencode/src/tool/truncate-service.ts b/packages/opencode/src/tool/truncate-service.ts index eee9172a08..8aaf07fbee 100644 --- a/packages/opencode/src/tool/truncate-service.ts +++ b/packages/opencode/src/tool/truncate-service.ts @@ -3,7 +3,7 @@ import { Log } from "../util/log" import { TRUNCATION_DIR } from "./truncation-dir" import { Identifier } from "../id/id" import type { Agent } from "../agent/agent" -import { evaluate } from "../permission/evaluate" +import { PermissionService } from "../permission/service" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Cause, Duration, Effect, FileSystem, Layer, Schedule, ServiceMap } from "effect" import { ToolID } from "./schema" @@ -24,7 +24,7 @@ export interface Options { function hasTaskTool(agent?: Agent.Info) { if (!agent?.permission) return false - return evaluate("task", "*", agent.permission).action !== "deny" + return PermissionService.evaluate("task", "*", agent.permission).action !== "deny" } export namespace TruncateService { diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 7f7e5e1f1f..94cf6bc011 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -1005,7 +1005,7 @@ test("ask - abort should clear pending request", async () => { fn: async () => { const ctl = new AbortController() const ask = runtime.runPromise( - S.PermissionService.use((svc) => + S.PermissionService.Service.use((svc) => svc.ask({ sessionID: SessionID.make("session_test"), permission: "bash",