feat(core): admit v2 skill guidance (#30843)

This commit is contained in:
Kit Langton 2026-06-05 11:19:55 -04:00 committed by GitHub
commit 3f64b5e621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3119 additions and 174 deletions

View file

@ -33,14 +33,18 @@ export const Source = Schema.Union([
]).annotate({ identifier: "PermissionV2.Source" })
export type Source = typeof Source.Type
export const Request = Schema.Struct({
id: ID,
const RequestFields = {
sessionID: SessionV2.ID,
action: Schema.String,
resources: Schema.Array(Schema.String),
save: Schema.Array(Schema.String).pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
source: Source.pipe(Schema.optional),
}
export const Request = Schema.Struct({
id: ID,
...RequestFields,
}).annotate({ identifier: "PermissionV2.Request" })
export type Request = typeof Request.Type
@ -49,12 +53,8 @@ export type Reply = typeof Reply.Type
export const AssertInput = Schema.Struct({
id: ID.pipe(Schema.optional),
sessionID: SessionV2.ID,
action: Schema.String,
resources: Schema.Array(Schema.String),
save: Schema.Array(Schema.String).pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
source: Source.pipe(Schema.optional),
...RequestFields,
agent: AgentV2.ID.pipe(Schema.optional),
}).annotate({ identifier: "PermissionV2.AssertInput" })
export type AssertInput = typeof AssertInput.Type
@ -128,6 +128,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
interface Pending {
readonly request: Request
readonly agent?: AgentV2.ID
readonly deferred: Deferred.Deferred<void, RejectedError | CorrectedError>
}
@ -159,10 +160,14 @@ export const layer = Layer.effect(
)
})
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID) {
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (
sessionID: SessionV2.ID,
agentID?: AgentV2.ID,
) {
const session = yield* sessions.get(sessionID)
if (!session) return yield* new SessionV2.NotFoundError({ sessionID })
return (yield* agents.resolve(session.agent))?.permissions ?? missingAgentPermissions
const agent = yield* agents.resolve(agentID ?? session.agent)
return agent?.permissions ?? missingAgentPermissions
})
function denied(input: AssertInput, rules: Ruleset) {
@ -174,7 +179,7 @@ export const layer = Layer.effect(
}
const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) {
const rules = yield* configured(input.sessionID)
const rules = yield* configured(input.sessionID, input.agent)
if (denied(input, rules)) return { effect: "deny" as const, rules }
const all = [...rules, ...(yield* savedRules())]
const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect)
@ -194,11 +199,11 @@ export const layer = Layer.effect(
}
}
const create = (request: Request) =>
const create = (request: Request, agent?: AgentV2.ID) =>
EffectRuntime.uninterruptible(
EffectRuntime.gen(function* () {
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
const item = { request, deferred }
const item = { request, agent, deferred }
if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
pending.set(request.id, item)
yield* events
@ -211,7 +216,7 @@ export const layer = Layer.effect(
const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) {
const result = yield* evaluateInput(input)
const value = request(input)
if (result.effect === "ask") yield* create(value)
if (result.effect === "ask") yield* create(value, input.agent)
return { id: value.id, effect: result.effect }
})
@ -225,7 +230,7 @@ export const layer = Layer.effect(
})
}
if (result.effect === "allow") return
const item = yield* create(request(input))
const item = yield* create(request(input), input.agent)
return yield* restore(Deferred.await(item.deferred)).pipe(
EffectRuntime.ensuring(
EffectRuntime.sync(() => {
@ -281,7 +286,7 @@ export const layer = Layer.effect(
const rememberedRules = yield* savedRules()
for (const [id, item] of pending) {
const input = { ...item.request }
const rules = yield* configured(item.request.sessionID).pipe(
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)),
)
if (!rules) continue