import { createHash } from "node:crypto"; import * as Effect from "effect/Effect"; import { effectiveCapabilityPolicy, type CapabilityMetadata, type CapabilityReference, } from "../shared/capability-policy"; import { skillIdentity, type SkillIdentity } from "../shared/skill-management"; import { requiredSkillPermissions, type SkillPermissionKind, } from "../shared/skill-permissions"; import { skillCapability } from "./skill-activity"; import { CapabilityDenied, runCapability, type CapabilityPolicyStore, } from "./capability-policy"; import type { DiagnosticStore } from "./diagnostics"; import type { SkillPermissionStore, SkillPermissionSubject, } from "./skill-permissions"; import { agentValidation, type AgentFailure } from "./agent-io"; export interface SkillExecutionRequest { skill: SkillIdentity; path: string; capability: CapabilityReference; } // Definition fingerprints identify package-declared capabilities. This trusted // revision instead identifies the owner's current, non-revivable authority. export interface SkillExternalAuthority { metadata: CapabilityMetadata; // Advance on every authority/lifecycle change; never reuse after restoration. revision: string; } // This object stays inside the trusted host. It is never serialized into a // script or browser. Adapters use it at each host-operation boundary. export interface SkillExecutionGrant { check: ( permission: SkillPermissionKind, capability?: CapabilityReference, ) => void; use: ( permission: SkillPermissionKind, operation: () => Effect.Effect, capability?: CapabilityReference, ) => Effect.Effect; } export class SkillExecutionPermissions { constructor( private readonly context: { current: (identity: SkillIdentity) => SkillPermissionSubject; permissions: SkillPermissionStore; policy: CapabilityPolicyStore; diagnostics: DiagnosticStore; resolveExternal?: ( reference: CapabilityReference, ) => SkillExternalAuthority | undefined; }, ) {} describe(value: unknown, path: string): CapabilityMetadata | undefined { const parsed = skillIdentity.safeParse(value); if (!parsed.success) return undefined; try { const subject = this.context.current(parsed.data); const { skill } = subject; if (!skill.enabled || !skill.scripts.includes(path)) return undefined; const permission = this.context.permissions.read(parsed.data); const policy = this.context.policy.read(); const activity = skillCapability(skill, "execute", path); const required = requiredSkillPermissions( skill.permissions, skill.scripts, ); const decisions = required.map( (kind) => effectiveCapabilityPolicy( { ...activity, source: { kind: "skill", id: activity.source.id }, sourceName: skill.name, actionSummary: path, enabled: true, approval: permission.choices[kind], }, policy, ).policy, ); const approval = decisions.includes("never") ? "never" : decisions.includes("ask") ? "ask" : "allow"; return { id: activity.id, fingerprint: createHash("sha256") .update( JSON.stringify([ activity.fingerprint, subject.installationId, parsed.data.revision, permission.revision, policy.revision, ]), ) .digest("hex"), source: { kind: "skill", id: activity.source.id }, name: activity.name, sourceName: skill.name, actionSummary: `Run ${path}; permissions: ${required.join(", ")}`, enabled: true, approval, }; } catch { return undefined; } } run( request: SkillExecutionRequest, invocation: { signal: AbortSignal; approvedOnce?: boolean; conversationId?: string; toolCallId?: string; }, operation: (grant: SkillExecutionGrant) => Effect.Effect, ) { return Effect.gen({ self: this }, function* () { yield* agentValidation(() => invocation.signal.throwIfAborted()); let active = true; return yield* runCapability( { ...this.context, ...invocation, resolve: () => this.describe(request.skill, request.path), }, request.capability, () => { const downstream = this.context .current(request.skill) .skill.permissions.externalCapabilities.map((reference) => ({ reference, revision: this.context.resolveExternal?.(reference)?.revision, })); const validate: SkillExecutionGrant["check"] = ( permission, capability, ) => { if (!active || invocation.signal.aborted) throw new CapabilityDenied("unavailable"); for (const { reference, revision } of downstream) { if ( this.context.resolveExternal?.(reference)?.revision !== revision ) throw new CapabilityDenied("unavailable"); } const current = this.describe(request.skill, request.path); if ( !current || current.fingerprint !== request.capability.fingerprint || current.id !== request.capability.id ) throw new CapabilityDenied("unavailable"); const { skill } = this.context.current(request.skill); if ( !requiredSkillPermissions( skill.permissions, skill.scripts, ).includes(permission) ) throw new CapabilityDenied("never"); if ( permission === "externalCapabilities" && (!capability || !skill.permissions.externalCapabilities.some( (entry) => entry.id === capability.id && entry.fingerprint === capability.fingerprint && entry.source.kind === capability.source.kind && entry.source.id === capability.source.id, )) ) throw new CapabilityDenied("never"); if (permission === "externalCapabilities" && capability) { const external = this.context.resolveExternal?.(capability)?.metadata; if ( !external?.enabled || external.id !== capability.id || external.fingerprint !== capability.fingerprint || external.source.kind !== capability.source.kind || external.source.id !== capability.source.id ) throw new CapabilityDenied("unavailable"); const decision = effectiveCapabilityPolicy( external, this.context.policy.read(), ).policy; if (decision !== "allow") throw new CapabilityDenied(decision); } }; const check: SkillExecutionGrant["check"] = ( permission, capability, ) => { try { validate(permission, capability); } catch (error) { // A caller may catch a rejected host operation. It must never be // able to reuse the same grant after the failed authority check. active = false; throw error instanceof CapabilityDenied ? error : new CapabilityDenied("unavailable"); } }; const grant: SkillExecutionGrant = { check, use: (permission, hostOperation, capability) => Effect.gen({ self: this }, function* () { yield* Effect.try({ try: () => check(permission, capability), catch: (error) => error instanceof CapabilityDenied ? error : new CapabilityDenied("unavailable"), }); const result = yield* permission === "externalCapabilities" ? runCapability( { policy: this.context.policy, diagnostics: this.context.diagnostics, conversationId: invocation.conversationId, toolCallId: invocation.toolCallId, resolve: (reference) => this.context.resolveExternal?.(reference)?.metadata, }, capability, () => hostOperation(), ) : hostOperation(); yield* Effect.try({ try: () => check(permission, capability), catch: (error) => error instanceof CapabilityDenied ? error : new CapabilityDenied("unavailable"), }); return result; }), }; return Effect.gen(function* () { yield* agentValidation(() => check("execute")); const result = yield* operation(grant); yield* agentValidation(() => check("execute")); return result; }); }, ).pipe( Effect.ensuring( Effect.sync(() => { active = false; }), ), ); }); } }