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;
}),
),
);
});
}
}