@@ -5,31 +5,25 @@ import { eq } from "drizzle-orm"
55import { Database } from "./database/database"
66import { EventV2 } from "./event"
77import { Location } from "./location"
8+ import { AgentV2 } from "./agent"
89import { SessionV2 } from "./session"
910import { withStatics } from "./schema"
1011import { Identifier } from "./util/identifier"
1112import { Wildcard } from "./util/wildcard"
1213import { PermissionTable } from "./permission/sql"
14+ import { PermissionSchema } from "./permission/schema"
15+
16+ export { Effect , Rule , Ruleset } from "./permission/schema"
17+ type Effect = PermissionSchema . Effect
18+ type Rule = PermissionSchema . Rule
19+ type Ruleset = PermissionSchema . Ruleset
1320
1421export const ID = Schema . String . check ( Schema . isStartsWith ( "per" ) ) . pipe (
1522 Schema . brand ( "PermissionV2.ID" ) ,
1623 withStatics ( ( schema ) => ( { create : ( id ?: string ) => schema . make ( id ?? "per_" + Identifier . ascending ( ) ) } ) ) ,
1724)
1825export type ID = typeof ID . Type
1926
20- export const Effect = Schema . Literals ( [ "allow" , "deny" , "ask" ] ) . annotate ( { identifier : "PermissionV2.Effect" } )
21- export type Effect = typeof Effect . Type
22-
23- export const Rule = Schema . Struct ( {
24- action : Schema . String ,
25- resource : Schema . String ,
26- effect : Effect ,
27- } ) . annotate ( { identifier : "PermissionV2.Rule" } )
28- export type Rule = typeof Rule . Type
29-
30- export const Ruleset = Schema . Array ( Rule ) . annotate ( { identifier : "PermissionV2.Ruleset" } )
31- export type Ruleset = typeof Ruleset . Type
32-
3327export const Source = Schema . Union ( [
3428 Schema . Struct ( {
3529 type : Schema . Literal ( "tool" ) ,
@@ -61,7 +55,6 @@ export const AssertInput = Schema.Struct({
6155 remember : Schema . Array ( Schema . String ) . pipe ( Schema . optional ) ,
6256 metadata : Schema . Record ( Schema . String , Schema . Unknown ) . pipe ( Schema . optional ) ,
6357 source : Source . pipe ( Schema . optional ) ,
64- rules : Ruleset ,
6558} ) . annotate ( { identifier : "PermissionV2.AssertInput" } )
6659export type AssertInput = typeof AssertInput . Type
6760
@@ -72,6 +65,12 @@ export const ReplyInput = Schema.Struct({
7265} ) . annotate ( { identifier : "PermissionV2.ReplyInput" } )
7366export type ReplyInput = typeof ReplyInput . Type
7467
68+ export const AskResult = Schema . Struct ( {
69+ id : ID ,
70+ effect : PermissionSchema . Effect ,
71+ } ) . annotate ( { identifier : "PermissionV2.AskResult" } )
72+ export type AskResult = typeof AskResult . Type
73+
7574export const Event = {
7675 Asked : EventV2 . define ( { type : "permission.v2.asked" , schema : Request . fields } ) ,
7776 Replied : EventV2 . define ( {
@@ -91,7 +90,7 @@ export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("P
9190} ) { }
9291
9392export class DeniedError extends Schema . TaggedErrorClass < DeniedError > ( ) ( "PermissionV2.DeniedError" , {
94- rules : Ruleset ,
93+ rules : PermissionSchema . Ruleset ,
9594} ) { }
9695
9796export class NotFoundError extends Schema . TaggedErrorClass < NotFoundError > ( ) ( "PermissionV2.NotFoundError" , {
@@ -117,8 +116,8 @@ export function merge(...rulesets: Ruleset[]): Ruleset {
117116}
118117
119118export interface Interface {
120- readonly ask : ( input : AssertInput ) => EffectRuntime . Effect < Request >
121- readonly assert : ( input : AssertInput ) => EffectRuntime . Effect < void , Error >
119+ readonly ask : ( input : AssertInput ) => EffectRuntime . Effect < AskResult , SessionV2 . NotFoundError >
120+ readonly assert : ( input : AssertInput ) => EffectRuntime . Effect < void , Error | SessionV2 . NotFoundError >
122121 readonly reply : ( input : ReplyInput ) => EffectRuntime . Effect < void , NotFoundError >
123122 readonly get : ( id : ID ) => EffectRuntime . Effect < Request | undefined >
124123 readonly forSession : ( sessionID : SessionV2 . ID ) => EffectRuntime . Effect < ReadonlyArray < Request > >
@@ -129,7 +128,6 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
129128
130129interface Pending {
131130 readonly request : Request
132- readonly rules : Ruleset
133131 readonly deferred : Deferred . Deferred < void , RejectedError | CorrectedError >
134132}
135133
@@ -139,6 +137,8 @@ export const layer = Layer.effect(
139137 const { db } = yield * Database . Service
140138 const events = yield * EventV2 . Service
141139 const location = yield * Location . Service
140+ const agents = yield * AgentV2 . Service
141+ const sessions = yield * SessionV2 . Service
142142 const pending = new Map < ID , Pending > ( )
143143
144144 yield * EffectRuntime . addFinalizer ( ( ) =>
@@ -163,15 +163,31 @@ export const layer = Layer.effect(
163163 return rows . map ( ( row ) : Rule => ( { action : row . action , resource : row . resource , effect : "allow" } ) )
164164 } )
165165
166+ const configured = EffectRuntime . fn ( "PermissionV2.configured" ) ( function * ( sessionID : SessionV2 . ID ) {
167+ const session = yield * sessions . get ( sessionID )
168+ if ( ! session . agent ) return [ ]
169+ return ( yield * agents . get ( AgentV2 . ID . make ( session . agent ) ) ) ?. permissions ?? [ ]
170+ } )
171+
172+ function denied ( input : AssertInput , rules : Ruleset ) {
173+ return input . resources . some ( ( resource ) => evaluate ( input . action , resource , rules ) . effect === "deny" )
174+ }
175+
176+ function relevant ( input : AssertInput , rules : Ruleset ) {
177+ return rules . filter ( ( rule ) => Wildcard . match ( input . action , rule . action ) )
178+ }
179+
166180 const evaluateInput = EffectRuntime . fnUntraced ( function * ( input : AssertInput ) {
167- const rules = [ ...input . rules , ...( yield * remembered ( ) ) ]
168- const effects = input . resources . map ( ( resource ) => evaluate ( input . action , resource , rules ) . effect )
181+ const rules = yield * configured ( input . sessionID )
182+ if ( denied ( input , rules ) ) return { effect : "deny" as const , rules }
183+ const all = [ ...rules , ...( yield * remembered ( ) ) ]
184+ const effects = input . resources . map ( ( resource ) => evaluate ( input . action , resource , all ) . effect )
169185 const effect : Effect = effects . includes ( "deny" ) ? "deny" : effects . includes ( "ask" ) ? "ask" : "allow"
170- return { effect, rules }
186+ return { effect, rules : all }
171187 } )
172188
173- const create = EffectRuntime . fnUntraced ( function * ( input : AssertInput ) {
174- const request : Request = {
189+ function request ( input : AssertInput ) : Request {
190+ return {
175191 id : input . id ?? ID . create ( ) ,
176192 sessionID : input . sessionID ,
177193 action : input . action ,
@@ -180,27 +196,32 @@ export const layer = Layer.effect(
180196 metadata : input . metadata ,
181197 source : input . source ,
182198 }
199+ }
200+
201+ const create = EffectRuntime . fnUntraced ( function * ( request : Request ) {
183202 const deferred = yield * Deferred . make < void , RejectedError | CorrectedError > ( )
184- const item = { request, rules : input . rules , deferred }
203+ const item = { request, deferred }
185204 pending . set ( request . id , item )
186205 yield * events . publish ( Event . Asked , request )
187206 return item
188207 } )
189208
190209 const ask = EffectRuntime . fn ( "PermissionV2.ask" ) ( function * ( input : AssertInput ) {
191- const pending = yield * create ( input )
192- return pending . request
210+ const result = yield * evaluateInput ( input )
211+ const value = request ( input )
212+ if ( result . effect === "ask" ) yield * create ( value )
213+ return { id : value . id , effect : result . effect }
193214 } )
194215
195216 const assert = EffectRuntime . fn ( "PermissionV2.assert" ) ( function * ( input : AssertInput ) {
196217 const result = yield * evaluateInput ( input )
197218 if ( result . effect === "deny" ) {
198219 return yield * new DeniedError ( {
199- rules : result . rules . filter ( ( candidate ) => Wildcard . match ( input . action , candidate . action ) ) ,
220+ rules : relevant ( input , result . rules ) ,
200221 } )
201222 }
202223 if ( result . effect === "allow" ) return
203- const item = yield * create ( input )
224+ const item = yield * create ( request ( input ) )
204225 return yield * Deferred . await ( item . deferred ) . pipe (
205226 EffectRuntime . ensuring (
206227 EffectRuntime . sync ( ( ) => {
@@ -257,9 +278,15 @@ export const layer = Layer.effect(
257278
258279 const rememberedRules = yield * remembered ( )
259280 for ( const [ id , item ] of pending ) {
260- const rules = [ ...item . rules , ...rememberedRules ]
281+ const input = { ...item . request }
282+ const rules = yield * configured ( item . request . sessionID ) . pipe (
283+ EffectRuntime . catchTag ( "Session.NotFoundError" , ( ) => EffectRuntime . succeed ( undefined ) ) ,
284+ )
285+ if ( ! rules ) continue
286+ if ( denied ( input , rules ) ) continue
287+ const effective = [ ...rules , ...rememberedRules ]
261288 if (
262- ! item . request . resources . every ( ( resource ) => evaluate ( item . request . action , resource , rules ) . effect === "allow" )
289+ ! item . request . resources . every ( ( resource ) => evaluate ( item . request . action , resource , effective ) . effect === "allow" )
263290 )
264291 continue
265292 pending . delete ( id )
@@ -288,4 +315,4 @@ export const layer = Layer.effect(
288315 } ) ,
289316)
290317
291- export const locationLayer = layer
318+ export const locationLayer = layer . pipe ( Layer . provideMerge ( AgentV2 . locationLayer ) )
0 commit comments