@@ -11,121 +11,128 @@ import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
1111import z from "zod"
1212import { PermissionID } from "./schema"
1313
14- const log = Log . create ( { service : "permission" } )
15-
16- export const Action = z . enum ( [ "allow" , "deny" , "ask" ] ) . meta ( {
17- ref : "PermissionAction" ,
18- } )
19- export type Action = z . infer < typeof Action >
20-
21- export const Rule = z
22- . object ( {
23- permission : z . string ( ) ,
24- pattern : z . string ( ) ,
25- action : Action ,
26- } )
27- . meta ( {
28- ref : "PermissionRule" ,
29- } )
30- export type Rule = z . infer < typeof Rule >
31-
32- export const Ruleset = Rule . array ( ) . meta ( {
33- ref : "PermissionRuleset" ,
34- } )
35- export type Ruleset = z . infer < typeof Ruleset >
36-
37- export const Request = z
38- . object ( {
39- id : PermissionID . zod ,
40- sessionID : SessionID . zod ,
41- permission : z . string ( ) ,
42- patterns : z . string ( ) . array ( ) ,
43- metadata : z . record ( z . string ( ) , z . any ( ) ) ,
44- always : z . string ( ) . array ( ) ,
45- tool : z
46- . object ( {
47- messageID : MessageID . zod ,
48- callID : z . string ( ) ,
49- } )
50- . optional ( ) ,
14+ export namespace PermissionEffect {
15+ const log = Log . create ( { service : "permission" } )
16+
17+ export const Action = z . enum ( [ "allow" , "deny" , "ask" ] ) . meta ( {
18+ ref : "PermissionAction" ,
5119 } )
52- . meta ( {
53- ref : "PermissionRequest" ,
20+ export type Action = z . infer < typeof Action >
21+
22+ export const Rule = z
23+ . object ( {
24+ permission : z . string ( ) ,
25+ pattern : z . string ( ) ,
26+ action : Action ,
27+ } )
28+ . meta ( {
29+ ref : "PermissionRule" ,
30+ } )
31+ export type Rule = z . infer < typeof Rule >
32+
33+ export const Ruleset = Rule . array ( ) . meta ( {
34+ ref : "PermissionRuleset" ,
5435 } )
55- export type Request = z . infer < typeof Request >
56-
57- export const Reply = z . enum ( [ "once" , "always" , "reject" ] )
58- export type Reply = z . infer < typeof Reply >
36+ export type Ruleset = z . infer < typeof Ruleset >
5937
60- export const Approval = z . object ( {
61- projectID : ProjectID . zod ,
62- patterns : z . string ( ) . array ( ) ,
63- } )
64-
65- export const Event = {
66- Asked : BusEvent . define ( "permission.asked" , Request ) ,
67- Replied : BusEvent . define (
68- "permission.replied" ,
69- z . object ( {
38+ export const Request = z
39+ . object ( {
40+ id : PermissionID . zod ,
7041 sessionID : SessionID . zod ,
71- requestID : PermissionID . zod ,
72- reply : Reply ,
73- } ) ,
74- ) ,
75- }
42+ permission : z . string ( ) ,
43+ patterns : z . string ( ) . array ( ) ,
44+ metadata : z . record ( z . string ( ) , z . any ( ) ) ,
45+ always : z . string ( ) . array ( ) ,
46+ tool : z
47+ . object ( {
48+ messageID : MessageID . zod ,
49+ callID : z . string ( ) ,
50+ } )
51+ . optional ( ) ,
52+ } )
53+ . meta ( {
54+ ref : "PermissionRequest" ,
55+ } )
56+ export type Request = z . infer < typeof Request >
57+
58+ export const Reply = z . enum ( [ "once" , "always" , "reject" ] )
59+ export type Reply = z . infer < typeof Reply >
60+
61+ export const Approval = z . object ( {
62+ projectID : ProjectID . zod ,
63+ patterns : z . string ( ) . array ( ) ,
64+ } )
7665
77- export class RejectedError extends Schema . TaggedErrorClass < RejectedError > ( ) ( "PermissionRejectedError" , { } ) {
78- override get message ( ) {
79- return "The user rejected permission to use this specific tool call."
66+ export const Event = {
67+ Asked : BusEvent . define ( "permission.asked" , Request ) ,
68+ Replied : BusEvent . define (
69+ "permission.replied" ,
70+ z . object ( {
71+ sessionID : SessionID . zod ,
72+ requestID : PermissionID . zod ,
73+ reply : Reply ,
74+ } ) ,
75+ ) ,
8076 }
81- }
8277
83- export class CorrectedError extends Schema . TaggedErrorClass < CorrectedError > ( ) ( "PermissionCorrectedError" , {
84- feedback : Schema . String ,
85- } ) {
86- override get message ( ) {
87- return `The user rejected permission to use this specific tool call with the following feedback: ${ this . feedback } `
78+ export class RejectedError extends Schema . TaggedErrorClass < RejectedError > ( ) ( "PermissionRejectedError" , { } ) {
79+ override get message ( ) {
80+ return "The user rejected permission to use this specific tool call."
81+ }
8882 }
89- }
9083
91- export class DeniedError extends Schema . TaggedErrorClass < DeniedError > ( ) ( "PermissionDeniedError" , {
92- ruleset : Schema . Any ,
93- } ) {
94- override get message ( ) {
95- 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 ) } `
84+ export class CorrectedError extends Schema . TaggedErrorClass < CorrectedError > ( ) ( "PermissionCorrectedError" , {
85+ feedback : Schema . String ,
86+ } ) {
87+ override get message ( ) {
88+ return `The user rejected permission to use this specific tool call with the following feedback: ${ this . feedback } `
89+ }
9690 }
97- }
9891
99- export type PermissionError = DeniedError | RejectedError | CorrectedError
92+ export class DeniedError extends Schema . TaggedErrorClass < DeniedError > ( ) ( "PermissionDeniedError" , {
93+ ruleset : Schema . Any ,
94+ } ) {
95+ override get message ( ) {
96+ 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 ) } `
97+ }
98+ }
10099
101- interface PendingEntry {
102- info : Request
103- deferred : Deferred . Deferred < void , RejectedError | CorrectedError >
104- }
100+ export type Error = DeniedError | RejectedError | CorrectedError
105101
106- export const AskInput = Request . partial ( { id : true } ) . extend ( {
107- ruleset : Ruleset ,
108- } )
102+ export const AskInput = Request . partial ( { id : true } ) . extend ( {
103+ ruleset : Ruleset ,
104+ } )
109105
110- export const ReplyInput = z . object ( {
111- requestID : PermissionID . zod ,
112- reply : Reply ,
113- message : z . string ( ) . optional ( ) ,
114- } )
106+ export const ReplyInput = z . object ( {
107+ requestID : PermissionID . zod ,
108+ reply : Reply ,
109+ message : z . string ( ) . optional ( ) ,
110+ } )
115111
116- export declare namespace PermissionService {
117112 export interface Api {
118- readonly ask : ( input : z . infer < typeof AskInput > ) => Effect . Effect < void , PermissionError >
113+ readonly ask : ( input : z . infer < typeof AskInput > ) => Effect . Effect < void , Error >
119114 readonly reply : ( input : z . infer < typeof ReplyInput > ) => Effect . Effect < void >
120115 readonly list : ( ) => Effect . Effect < Request [ ] >
121116 }
122- }
123117
124- export class PermissionService extends ServiceMap . Service < PermissionService , PermissionService . Api > ( ) (
125- "@opencode/PermissionNext" ,
126- ) {
127- static readonly layer = Layer . effect (
128- PermissionService ,
118+ interface PendingEntry {
119+ info : Request
120+ deferred : Deferred . Deferred < void , RejectedError | CorrectedError >
121+ }
122+
123+ export function evaluate ( permission : string , pattern : string , ...rulesets : Ruleset [ ] ) : Rule {
124+ const rules = rulesets . flat ( )
125+ log . info ( "evaluate" , { permission, pattern, ruleset : rules } )
126+ const match = rules . findLast (
127+ ( rule ) => Wildcard . match ( permission , rule . permission ) && Wildcard . match ( pattern , rule . pattern ) ,
128+ )
129+ return match ?? { action : "ask" , permission, pattern : "*" }
130+ }
131+
132+ export class Service extends ServiceMap . Service < Service , Api > ( ) ( "@opencode/PermissionNext" ) { }
133+
134+ export const layer = Layer . effect (
135+ Service ,
129136 Effect . gen ( function * ( ) {
130137 const { project } = yield * InstanceContext
131138 const row = Database . use ( ( db ) =>
@@ -225,27 +232,13 @@ export class PermissionService extends ServiceMap.Service<PermissionService, Per
225232 } )
226233 yield * Deferred . succeed ( item . deferred , undefined )
227234 }
228-
229- // TODO: we don't save the permission ruleset to disk yet until there's
230- // UI to manage it
231- // db().insert(PermissionTable).values({ projectID: Instance.project.id, data: s.approved })
232- // .onConflictDoUpdate({ target: PermissionTable.projectID, set: { data: s.approved } }).run()
233235 } )
234236
235237 const list = Effect . fn ( "PermissionService.list" ) ( function * ( ) {
236238 return Array . from ( pending . values ( ) , ( item ) => item . info )
237239 } )
238240
239- return PermissionService . of ( { ask, reply, list } )
241+ return Service . of ( { ask, reply, list } )
240242 } ) ,
241243 )
242244}
243-
244- export function evaluate ( permission : string , pattern : string , ...rulesets : Ruleset [ ] ) : Rule {
245- const merged = rulesets . flat ( )
246- log . info ( "evaluate" , { permission, pattern, ruleset : merged } )
247- const match = merged . findLast (
248- ( rule ) => Wildcard . match ( permission , rule . permission ) && Wildcard . match ( pattern , rule . pattern ) ,
249- )
250- return match ?? { action : "ask" , permission, pattern : "*" }
251- }
0 commit comments