feat(runner): add Phase 7 semantic tool authorization

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 12:43:29 +00:00
1 parent 4d97afc2ed
commit 3f20bf4626
8 files changed
+1741

No files matched your search

+1
View File
@@ -27,3 +27,4 @@ export * from "./reducer/session-reducer.js";
export * from "./tracer/phase0-runner.js";
export * from "./tracer/phase1-replay.js";
export * from "./tracer/phase2-live.js";
export * from "./tools/index.js";
@@ -0,0 +1,5 @@
export * from "./phase7-semantic-tool-types.js";
export * from "./phase7-semantic-tool-catalog.js";
export * from "./phase7-tool-authorization.js";
export * from "./phase7-semantic-tool-runtime.js";
export * from "./phase7-tool-bindings.js";
@@ -0,0 +1,374 @@
import type {
Phase7JsonSchema,
Phase7OptionalCatalogGroup,
Phase7SemanticToolDescriptor,
Phase7ToolTaskMode,
} from "./phase7-semantic-tool-types.js";
const ALL_MODES: Phase7ToolTaskMode[] = ["standard", "ask", "planning", "skill_test"];
const WORK_MODES: Phase7ToolTaskMode[] = ["standard", "planning", "skill_test"];
const STANDARD_MODES: Phase7ToolTaskMode[] = ["standard", "skill_test"];
const RESULT_SCHEMA = objectSchema({
schema: stringSchema({ enum: ["paperclip.phase7.tool-result.v1"] }),
ok: { type: "boolean" },
operationId: stringSchema({ minLength: 1 }),
operationResultId: stringSchema({ minLength: 1 }),
value: {},
commandResult: {},
authorization: {},
}, ["schema", "ok", "operationId", "operationResultId", "value", "commandResult", "authorization"]);
const SECRET_REDACTION = [
{
path: "$.value",
replacement: "[SECRET_VALUE]" as const,
appliesTo: ["output", "error", "authorization_record"] as const,
},
];
const AUTH_REDACTION = [
{
path: "$.headers.authorization",
replacement: "[REDACTED]" as const,
appliesTo: ["input", "output", "error", "authorization_record"] as const,
},
{
path: "$.headers.cookie",
replacement: "[REDACTED]" as const,
appliesTo: ["input", "output", "error", "authorization_record"] as const,
},
];
function stringSchema(options: Partial<Phase7JsonSchema> = {}): Phase7JsonSchema {
return { type: "string", ...options };
}
function arraySchema(items: Phase7JsonSchema): Phase7JsonSchema {
return { type: "array", items };
}
function objectSchema(
properties: Record<string, Phase7JsonSchema>,
required: string[] = [],
): Phase7JsonSchema {
return { type: "object", properties, required, additionalProperties: false };
}
function descriptor(
value: Omit<Phase7SemanticToolDescriptor, "version" | "redaction"> & {
redaction?: Phase7SemanticToolDescriptor["redaction"];
},
): Phase7SemanticToolDescriptor {
const { redaction, ...rest } = value;
return { version: 1, ...rest, redaction: redaction ?? [] };
}
const ALWAYS_TOOLS: Phase7SemanticToolDescriptor[] = [
descriptor({
operationId: "get_task_context",
title: "Get task context",
description: "Read the active task, ancestors, wake context, linked results, and budget summary.",
inputSchema: objectSchema({}),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "read",
idempotency: "none",
mockCommandMapping: { kind: "context_read", projection: "active_task" },
}),
descriptor({
operationId: "get_task_history",
title: "Get task history",
description: "Read comments and audit-visible history for the active task only.",
inputSchema: objectSchema({ limit: { type: "integer", minimum: 1 } }),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "read",
idempotency: "none",
mockCommandMapping: { kind: "snapshot_read", projection: "active_task_history" },
}),
descriptor({
operationId: "list_documents",
title: "List task documents",
description: "List document metadata attached to the active task.",
inputSchema: objectSchema({}),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "read",
idempotency: "none",
mockCommandMapping: { kind: "snapshot_read", projection: "active_task_documents" },
}),
descriptor({
operationId: "read_document",
title: "Read task document",
description: "Read one document on the active task by stable key.",
inputSchema: objectSchema({ key: stringSchema({ minLength: 1 }) }, ["key"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "read",
idempotency: "none",
mockCommandMapping: { kind: "snapshot_read", projection: "active_task_document" },
}),
descriptor({
operationId: "list_document_revisions",
title: "List document revisions",
description: "Inspect revision history for one active-task document.",
inputSchema: objectSchema({ key: stringSchema({ minLength: 1 }) }, ["key"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "read",
idempotency: "none",
mockCommandMapping: { kind: "snapshot_read", projection: "active_task_document_revisions" },
}),
descriptor({
operationId: "report_progress",
title: "Report progress",
description: "Append a durable progress update to the active task.",
inputSchema: objectSchema({ body: stringSchema({ minLength: 1 }) }, ["body"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: WORK_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "report_progress" },
}),
descriptor({
operationId: "answer_status_question",
title: "Answer status question",
description: "Post an answer to a status question without changing task state.",
inputSchema: objectSchema({ body: stringSchema({ minLength: 1 }) }, ["body"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "report_progress" },
}),
descriptor({
operationId: "finish_task",
title: "Finish task",
description: "Finish the active task with a durable completion summary.",
inputSchema: objectSchema({ summary: stringSchema({ minLength: 1 }) }, ["summary"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: STANDARD_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "finish_task" },
}),
descriptor({
operationId: "block_task",
title: "Block task",
description: "Block the active task with a reason and optional first-class dependencies.",
inputSchema: objectSchema({
reason: stringSchema({ minLength: 1 }),
blockedByTaskIds: arraySchema(stringSchema({ minLength: 1 })),
}, ["reason"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: WORK_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "block_task" },
}),
descriptor({
operationId: "request_review",
title: "Request task review",
description: "Move the active task to review with a durable summary.",
inputSchema: objectSchema({ summary: stringSchema({ minLength: 1 }) }, ["summary"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: WORK_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "request_review" },
}),
descriptor({
operationId: "write_document",
title: "Write task document",
description: "Create or revision-safely update an active-task document.",
inputSchema: objectSchema({
key: stringSchema({ minLength: 1 }),
title: stringSchema({ minLength: 1 }),
body: stringSchema(),
baseRevisionId: { oneOf: [stringSchema({ minLength: 1 }), { type: "null" }] },
changeSummary: stringSchema(),
}, ["key", "title", "body", "baseRevisionId"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: WORK_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "write_document" },
}),
descriptor({
operationId: "request_human_input",
title: "Request human input",
description: "Create a typed confirmation, checkbox, question, task suggestion, or item-verdict request.",
inputSchema: objectSchema({
interactionKind: stringSchema({ enum: ["confirmation", "checkbox", "questions", "suggest_tasks", "item_verdicts"] }),
title: stringSchema({ minLength: 1 }),
prompt: stringSchema({ minLength: 1 }),
payload: {},
targetRevisionId: { oneOf: [stringSchema({ minLength: 1 }), { type: "null" }] },
continuationPolicy: stringSchema({ enum: ["none", "wake_assignee", "wake_assignee_on_accept"] }),
}, ["interactionKind", "title", "prompt", "continuationPolicy"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: WORK_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "request_human_input" },
}),
descriptor({
operationId: "register_deliverable",
title: "Register deliverable",
description: "Register an inspectable artifact and its work product on the active task.",
inputSchema: objectSchema({
filename: stringSchema({ minLength: 1 }),
contentType: stringSchema({ minLength: 1 }),
byteSize: { type: "integer", minimum: 0 },
sha256: stringSchema({ minLength: 1 }),
contentRef: stringSchema({ minLength: 1 }),
title: stringSchema({ minLength: 1 }),
}, ["filename", "contentType", "byteSize", "sha256", "contentRef", "title"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: STANDARD_MODES,
sideEffectClass: "task_write",
idempotency: "required",
mockCommandMapping: { kind: "semantic_command", commandKind: "register_deliverable" },
}),
descriptor({
operationId: "inspect_operation_result",
title: "Inspect operation result",
description: "Read a prior semantic operation result from this run.",
inputSchema: objectSchema({ operationResultId: stringSchema({ minLength: 1 }) }, ["operationResultId"]),
outputSchema: RESULT_SCHEMA,
disposition: "always_agent_tool",
optionalGroup: null,
requiredClaims: [],
taskModes: ALL_MODES,
sideEffectClass: "read",
idempotency: "none",
mockCommandMapping: { kind: "operation_result" },
}),
];
function optional(
operationId: string,
group: Phase7OptionalCatalogGroup,
requiredClaims: string[],
sideEffectClass: Phase7SemanticToolDescriptor["sideEffectClass"],
mapping: Phase7SemanticToolDescriptor["mockCommandMapping"],
inputSchema: Phase7JsonSchema = objectSchema({}),
options: Partial<Pick<Phase7SemanticToolDescriptor, "allowedRoles" | "redaction" | "taskModes" | "idempotency">> = {},
): Phase7SemanticToolDescriptor {
const title = operationId.split("_").map((part) => part[0]!.toUpperCase() + part.slice(1)).join(" ");
return descriptor({
operationId,
title,
description: `${title} through the Phase 7 ${group.replaceAll("_", " ")} capability set.`,
inputSchema,
outputSchema: RESULT_SCHEMA,
disposition: "optional_agent_tool",
optionalGroup: group,
requiredClaims,
taskModes: options.taskModes ?? STANDARD_MODES,
sideEffectClass,
idempotency: options.idempotency ?? (sideEffectClass === "read" ? "none" : "required"),
redaction: options.redaction,
allowedRoles: options.allowedRoles,
mockCommandMapping: mapping,
});
}
const OPTIONAL_TOOLS: Phase7SemanticToolDescriptor[] = [
optional("search_tasks", "discovery", ["discovery:tasks:read"], "read", { kind: "snapshot_read", projection: "company_tasks" }, objectSchema({ query: stringSchema(), status: stringSchema() })),
optional("list_agents", "discovery", ["discovery:agents:read"], "read", { kind: "snapshot_read", projection: "company_actors" }),
optional("list_projects", "discovery", ["discovery:projects:read"], "read", { kind: "mock_extension", extension: "discovery.projects" }),
optional("list_goals", "discovery", ["discovery:goals:read"], "read", { kind: "mock_extension", extension: "discovery.goals" }),
optional("create_task", "delegation_dependencies", ["delegation:tasks:create"], "company_write", { kind: "semantic_command", commandKind: "create_task" }, objectSchema({ title: stringSchema({ minLength: 1 }), description: stringSchema(), assigneeActorId: stringSchema(), priority: stringSchema(), blockedByTaskIds: arraySchema(stringSchema()) }, ["title"])),
optional("set_dependencies", "delegation_dependencies", ["dependencies:write"], "company_write", { kind: "semantic_command", commandKind: "set_dependencies" }, objectSchema({ blockedByTaskIds: arraySchema(stringSchema({ minLength: 1 })) }, ["blockedByTaskIds"])),
optional("list_approvals", "governance", ["governance:approvals:read"], "read", { kind: "snapshot_read", projection: "company_approvals" }),
optional("request_approval", "governance", ["governance:approvals:request"], "governance", { kind: "semantic_command", commandKind: "request_approval" }, objectSchema({ approvalType: stringSchema({ minLength: 1 }), payload: {} }, ["approvalType", "payload"])),
optional("decide_approval", "governance", ["governance:approvals:decide"], "governance", { kind: "semantic_command", commandKind: "decide_approval" }, objectSchema({ approvalId: stringSchema({ minLength: 1 }), decision: stringSchema({ enum: ["approved", "rejected", "cancelled"] }), note: stringSchema({ minLength: 1 }) }, ["approvalId", "decision", "note"]), { allowedRoles: ["board", "approver", "security"] }),
optional("comment_on_approval", "governance", ["governance:approvals:comment"], "governance", { kind: "semantic_command", commandKind: "comment_on_approval" }, objectSchema({ approvalId: stringSchema({ minLength: 1 }), body: stringSchema({ minLength: 1 }) }, ["approvalId", "body"])),
optional("list_cases", "cases", ["cases:read"], "read", { kind: "mock_extension", extension: "cases.list" }),
optional("upsert_case", "cases", ["cases:write"], "company_write", { kind: "mock_extension", extension: "cases.upsert" }, objectSchema({ key: stringSchema({ minLength: 1 }), body: stringSchema() }, ["key", "body"])),
optional("get_workspace_runtime", "workspace_runtime", ["workspace:read"], "read", { kind: "snapshot_read", projection: "active_task_workspace" }),
optional("control_workspace_service", "workspace_runtime", ["workspace:control"], "workspace_control", { kind: "semantic_command", commandKind: "control_workspace_service" }, objectSchema({ serviceId: stringSchema({ minLength: 1 }), action: stringSchema({ enum: ["start", "stop", "fail"] }), url: stringSchema() }, ["serviceId", "action"])),
optional("list_routines", "routines", ["routines:read"], "read", { kind: "mock_extension", extension: "routines.list" }),
optional("manage_routine", "routines", ["routines:write"], "admin", { kind: "mock_extension", extension: "routines.manage" }),
optional("list_company_skills", "company_skills", ["company_skills:read"], "read", { kind: "mock_extension", extension: "company_skills.list" }),
optional("sync_company_skills", "company_skills", ["company_skills:write"], "admin", { kind: "mock_extension", extension: "company_skills.sync" }),
optional("list_secret_metadata", "secrets", ["secrets:metadata:read"], "read", { kind: "mock_extension", extension: "secrets.metadata" }),
optional("read_secret_value", "secrets", ["secrets:values:read"], "secret_read", { kind: "mock_extension", extension: "secrets.value" }, objectSchema({ name: stringSchema({ minLength: 1 }) }, ["name"]), { redaction: [...SECRET_REDACTION], idempotency: "none" }),
optional("export_company", "portability_admin", ["portability:export"], "admin", { kind: "mock_extension", extension: "portability.export" }),
optional("administer_company", "portability_admin", ["company:admin"], "admin", { kind: "mock_extension", extension: "company.admin" }, objectSchema({ action: stringSchema({ minLength: 1 }), payload: {} }, ["action"]), { allowedRoles: ["board", "ceo", "admin"] }),
optional("generic_api_request", "test_escape_hatch", ["test:generic_api"], "test_escape_hatch", { kind: "mock_extension", extension: "test.generic_api" }, objectSchema({ method: stringSchema({ minLength: 1 }), path: stringSchema({ minLength: 1 }), headers: { type: "object", additionalProperties: stringSchema() }, body: {} }, ["method", "path"]), { taskModes: ["skill_test"], redaction: [...AUTH_REDACTION] }),
];
export const PHASE7_SEMANTIC_TOOL_CATALOG: readonly Phase7SemanticToolDescriptor[] = Object.freeze(
[...ALWAYS_TOOLS, ...OPTIONAL_TOOLS].map((tool) => Object.freeze(tool)),
);
export const PHASE7_OPTIONAL_TOOL_CATALOGS: Readonly<
Record<Phase7OptionalCatalogGroup, readonly Phase7SemanticToolDescriptor[]>
> = Object.freeze({
discovery: toolsInGroup("discovery"),
delegation_dependencies: toolsInGroup("delegation_dependencies"),
governance: toolsInGroup("governance"),
cases: toolsInGroup("cases"),
workspace_runtime: toolsInGroup("workspace_runtime"),
routines: toolsInGroup("routines"),
company_skills: toolsInGroup("company_skills"),
secrets: toolsInGroup("secrets"),
portability_admin: toolsInGroup("portability_admin"),
test_escape_hatch: toolsInGroup("test_escape_hatch"),
});
export function phase7SemanticTool(operationId: string): Phase7SemanticToolDescriptor | undefined {
return PHASE7_SEMANTIC_TOOL_CATALOG.find((tool) => tool.operationId === operationId);
}
function toolsInGroup(group: Phase7OptionalCatalogGroup): readonly Phase7SemanticToolDescriptor[] {
return Object.freeze(OPTIONAL_TOOLS.filter((tool) => tool.optionalGroup === group));
}
@@ -0,0 +1,482 @@
import type {
Phase7CommandEnvelope,
Phase7JsonValue,
Phase7MockControlPlanePort,
Phase7SemanticCommand,
} from "../mock-core/phase7-control-plane-types.js";
import { phase7SemanticTool } from "./phase7-semantic-tool-catalog.js";
import { Phase7ToolAuthorizationEngine } from "./phase7-tool-authorization.js";
import type {
Phase7AuthorizationRecord,
Phase7JsonSchema,
Phase7PolicyDenial,
Phase7ScenarioToolPolicy,
Phase7SemanticToolDescriptor,
Phase7ToolAuthorizationContext,
Phase7ToolInvocation,
Phase7ToolInvocationResult,
Phase7ToolSuccess,
Phase7VisibleToolSet,
} from "./phase7-semantic-tool-types.js";
export interface Phase7SemanticToolRuntimeOptions {
adapter: Phase7MockControlPlanePort;
runId: string;
scenarioGrants?: string[];
policy?: Phase7ScenarioToolPolicy;
resolveSecretValue?: (name: string) => Promise<string | null> | string | null;
}
export class Phase7SemanticToolRuntime {
readonly #adapter: Phase7MockControlPlanePort;
readonly #runId: string;
readonly #scenarioGrants: string[];
readonly #policy: Phase7ScenarioToolPolicy | undefined;
readonly #resolveSecretValue: Phase7SemanticToolRuntimeOptions["resolveSecretValue"];
readonly #authorization = new Phase7ToolAuthorizationEngine();
readonly #operationResults = new Map<string, Phase7JsonValue>();
#resultSequence = 0;
constructor(options: Phase7SemanticToolRuntimeOptions) {
this.#adapter = options.adapter;
this.#runId = options.runId;
this.#scenarioGrants = [...new Set(options.scenarioGrants ?? [])].sort();
this.#policy = options.policy === undefined ? undefined : structuredClone(options.policy);
this.#resolveSecretValue = options.resolveSecretValue;
}
visibleTools(): Phase7VisibleToolSet {
return this.#authorization.computeVisibleTools(this.#context());
}
authorizationRecords(): readonly Phase7AuthorizationRecord[] {
return this.#authorization.records();
}
async invoke(invocation: Phase7ToolInvocation): Promise<Phase7ToolInvocationResult> {
const context = this.#context();
const authorization = this.#authorization.authorizeInvocation(
invocation.operationId,
invocation.input,
context,
);
if (authorization.outcome !== "allowed") {
return this.#denial(
invocation.operationId,
authorization,
authorization.outcome === "absent" ? "operation_absent" : "policy_denied",
);
}
const descriptor = phase7SemanticTool(invocation.operationId)!;
const validationIssues = validateJsonSchema(descriptor.inputSchema, invocation.input);
if (validationIssues.length > 0) {
const denied = this.#authorization.denyInvocation(
invocation.operationId,
context,
"input_schema_invalid",
);
return this.#denial(invocation.operationId, denied, "input_invalid");
}
if (descriptor.idempotency === "required" && !invocation.idempotencyKey?.trim()) {
const denied = this.#authorization.denyInvocation(
invocation.operationId,
context,
"idempotency_key_required",
);
return this.#denial(invocation.operationId, denied, "input_invalid");
}
if (invocation.operationId === "decide_approval" && this.#isSelfApproval(invocation.input)) {
const denied = this.#authorization.denyInvocation(
invocation.operationId,
context,
"self_approval_conflict",
);
return this.#denial(invocation.operationId, denied, "policy_denied");
}
const beforeRevision = this.#adapter.snapshot().revision;
try {
const execution = await this.#execute(descriptor, invocation);
const finalAuthorization = this.#authorization.attachStateChange(
authorization.sequence,
beforeRevision,
this.#adapter.snapshot().revision,
execution.entityRefs,
);
const resultId = execution.commandResult?.commandId ?? `tool-result-${++this.#resultSequence}`;
const success: Phase7ToolSuccess = {
schema: "paperclip.phase7.tool-result.v1",
ok: true,
operationId: invocation.operationId,
operationResultId: resultId,
value: execution.value,
commandResult: execution.commandResult,
authorization: finalAuthorization,
};
this.#operationResults.set(resultId, redactStoredResult(descriptor, execution.value));
return deepFreeze(success);
} catch {
const denied = this.#authorization.denyInvocation(
invocation.operationId,
context,
"mock_operation_rejected",
);
return this.#denial(invocation.operationId, denied, "operation_unsupported");
}
}
async #execute(
descriptor: Phase7SemanticToolDescriptor,
invocation: Phase7ToolInvocation,
): Promise<{ value: Phase7JsonValue; commandResult: Phase7ToolSuccess["commandResult"]; entityRefs: string[] }> {
const input = asObject(invocation.input);
switch (descriptor.mockCommandMapping.kind) {
case "context_read":
return { value: toJsonValue(this.#adapter.context(this.#runId)), commandResult: null, entityRefs: [] };
case "snapshot_read":
return {
value: this.#snapshotProjection(descriptor.mockCommandMapping.projection, input),
commandResult: null,
entityRefs: [],
};
case "operation_result": {
const id = requireString(input.operationResultId);
const result = this.#operationResults.get(id);
if (result === undefined) throw new Error("operation result is not available");
return { value: structuredClone(result), commandResult: null, entityRefs: [] };
}
case "semantic_command": {
const command = commandForOperation(descriptor.operationId, input);
const envelope: Phase7CommandEnvelope = {
runId: this.#runId,
idempotencyKey: invocation.idempotencyKey!,
command,
};
const commandResult = await this.#adapter.applyCommand(envelope);
return {
value: toJsonValue(commandResult),
commandResult,
entityRefs: commandResult.entityRefs,
};
}
case "mock_extension":
return this.#executeExtension(descriptor, input);
}
}
async #executeExtension(
descriptor: Phase7SemanticToolDescriptor,
input: Record<string, Phase7JsonValue>,
): Promise<{ value: Phase7JsonValue; commandResult: null; entityRefs: string[] }> {
switch (descriptor.mockCommandMapping.kind === "mock_extension"
? descriptor.mockCommandMapping.extension
: "") {
case "discovery.projects":
case "discovery.goals":
case "cases.list":
case "routines.list":
case "company_skills.list":
return { value: [], commandResult: null, entityRefs: [] };
case "secrets.metadata":
return { value: [], commandResult: null, entityRefs: [] };
case "secrets.value": {
const name = requireString(input.name);
const value = await this.#resolveSecretValue?.(name);
if (value === null || value === undefined) throw new Error("secret is not available");
return { value: { name, value }, commandResult: null, entityRefs: [] };
}
case "portability.export": {
const snapshot = this.#adapter.snapshot();
return {
value: {
schema: "paperclip.phase7.mock-export.v1",
company: { id: snapshot.company.id, name: snapshot.company.name },
taskCount: snapshot.tasks.length,
actorCount: snapshot.actors.length,
},
commandResult: null,
entityRefs: [],
};
}
case "test.generic_api":
return {
value: {
status: 200,
body: null,
warning: "TEST-ONLY mock request; no control plane or network was contacted.",
},
commandResult: null,
entityRefs: [],
};
default:
throw new Error("mock extension is not implemented");
}
}
#snapshotProjection(
projection: string,
input: Record<string, Phase7JsonValue>,
): Phase7JsonValue {
const snapshot = this.#adapter.snapshot();
const context = this.#adapter.context(this.#runId);
switch (projection) {
case "active_task_history":
return toJsonValue(snapshot.comments.filter((comment) => comment.taskId === context.activeTask.id));
case "active_task_documents":
return toJsonValue(snapshot.documents
.filter((document) => document.taskId === context.activeTask.id)
.map(({ id, key, title, format, latestRevisionId, revisions }) => ({
id, key, title, format, latestRevisionId, revisionCount: revisions.length,
})));
case "active_task_document": {
const key = requireString(input.key);
const document = snapshot.documents.find(
(candidate) => candidate.taskId === context.activeTask.id && candidate.key === key,
);
if (document === undefined) throw new Error("document is not available");
return toJsonValue(document);
}
case "active_task_document_revisions": {
const key = requireString(input.key);
const document = snapshot.documents.find(
(candidate) => candidate.taskId === context.activeTask.id && candidate.key === key,
);
if (document === undefined) throw new Error("document is not available");
return toJsonValue(document.revisions);
}
case "company_tasks": {
const query = typeof input.query === "string" ? input.query.toLowerCase() : "";
const status = typeof input.status === "string" ? input.status : null;
return toJsonValue(snapshot.tasks.filter(
(task) =>
(query === "" || `${task.identifier} ${task.title} ${task.description ?? ""}`.toLowerCase().includes(query)) &&
(status === null || task.status === status),
));
}
case "company_actors":
return toJsonValue(snapshot.actors.map(({ id, companyId, name, role, status }) => ({ id, companyId, name, role, status })));
case "company_approvals":
return toJsonValue(snapshot.approvals);
case "active_task_workspace":
return toJsonValue(snapshot.workspaceServices.filter((service) => service.taskId === context.activeTask.id));
default:
throw new Error("snapshot projection is not implemented");
}
}
#context(): Phase7ToolAuthorizationContext {
const runContext = this.#adapter.context(this.#runId);
return {
runId: this.#runId,
actor: {
id: runContext.actor.id,
role: runContext.actor.role,
capabilityGrants: [...runContext.actor.capabilityGrants],
},
task: {
id: runContext.activeTask.id,
assigneeActorId: runContext.activeTask.assigneeActorId,
workMode: runContext.activeTask.workMode,
},
scenarioGrants: [...this.#scenarioGrants],
policy: this.#policy,
};
}
#isSelfApproval(input: Phase7JsonValue): boolean {
const approvalId = asObject(input).approvalId;
if (typeof approvalId !== "string") return false;
const actorId = this.#adapter.context(this.#runId).actor.id;
const approval = this.#adapter.snapshot().approvals.find((candidate) => candidate.id === approvalId);
return approval?.requestedByActorId === actorId;
}
#denial(
operationId: string,
authorization: Phase7AuthorizationRecord,
code: Phase7PolicyDenial["error"]["code"],
): Phase7PolicyDenial {
return deepFreeze({
schema: "paperclip.phase7.tool-result.v1",
ok: false,
error: {
code,
message: code === "operation_absent"
? "The requested semantic operation is not available."
: "The requested semantic operation was not executed.",
operationId,
reason: authorization.reason,
},
authorization,
});
}
}
function commandForOperation(
operationId: string,
input: Record<string, Phase7JsonValue>,
): Phase7SemanticCommand {
switch (operationId) {
case "report_progress":
case "answer_status_question":
return { kind: "report_progress", body: requireString(input.body) };
case "finish_task":
return { kind: "finish_task", summary: requireString(input.summary) };
case "block_task":
return {
kind: "block_task",
reason: requireString(input.reason),
blockedByTaskIds: optionalStringArray(input.blockedByTaskIds),
};
case "request_review":
return { kind: "request_review", summary: requireString(input.summary) };
case "write_document":
return {
kind: "write_document",
key: requireString(input.key),
title: requireString(input.title),
body: requireString(input.body, true),
baseRevisionId: input.baseRevisionId === null ? null : requireString(input.baseRevisionId),
changeSummary: typeof input.changeSummary === "string" ? input.changeSummary : undefined,
};
case "request_human_input":
return {
kind: "request_human_input",
interactionKind: requireString(input.interactionKind) as never,
title: requireString(input.title),
prompt: requireString(input.prompt),
payload: input.payload,
targetRevisionId: input.targetRevisionId === null || input.targetRevisionId === undefined
? input.targetRevisionId
: requireString(input.targetRevisionId),
continuationPolicy: requireString(input.continuationPolicy) as never,
};
case "register_deliverable":
return {
kind: "register_deliverable",
filename: requireString(input.filename),
contentType: requireString(input.contentType),
byteSize: input.byteSize as number,
sha256: requireString(input.sha256),
contentRef: requireString(input.contentRef),
title: requireString(input.title),
};
case "create_task":
return {
kind: "create_task",
title: requireString(input.title),
description: typeof input.description === "string" ? input.description : undefined,
assigneeActorId: typeof input.assigneeActorId === "string" ? input.assigneeActorId : undefined,
priority: typeof input.priority === "string" ? input.priority as never : undefined,
blockedByTaskIds: optionalStringArray(input.blockedByTaskIds),
};
case "set_dependencies":
return { kind: "set_dependencies", blockedByTaskIds: optionalStringArray(input.blockedByTaskIds) ?? [] };
case "request_approval":
return { kind: "request_approval", approvalType: requireString(input.approvalType), payload: input.payload ?? {} };
case "decide_approval":
return {
kind: "decide_approval",
approvalId: requireString(input.approvalId),
decision: requireString(input.decision) as never,
note: requireString(input.note),
};
case "comment_on_approval":
return { kind: "comment_on_approval", approvalId: requireString(input.approvalId), body: requireString(input.body) };
case "control_workspace_service":
return {
kind: "control_workspace_service",
serviceId: requireString(input.serviceId),
action: requireString(input.action) as never,
url: typeof input.url === "string" ? input.url : undefined,
};
default:
throw new Error("semantic command mapping is not implemented");
}
}
export function validateJsonSchema(schema: Phase7JsonSchema, value: Phase7JsonValue, path = "$" ): string[] {
if (schema.oneOf !== undefined) {
return schema.oneOf.some((candidate) => validateJsonSchema(candidate, value, path).length === 0)
? []
: [`${path} does not match any allowed shape`];
}
if (schema.type !== undefined && !matchesType(schema.type, value)) return [`${path} must be ${schema.type}`];
if (schema.enum !== undefined && !schema.enum.some((candidate) => JSON.stringify(candidate) === JSON.stringify(value))) {
return [`${path} must be an allowed value`];
}
if (typeof value === "string" && schema.minLength !== undefined && value.length < schema.minLength) {
return [`${path} is too short`];
}
if (typeof value === "number" && schema.minimum !== undefined && value < schema.minimum) {
return [`${path} is below the minimum`];
}
if (Array.isArray(value) && schema.items !== undefined) {
return value.flatMap((item, index) => validateJsonSchema(schema.items!, item, `${path}[${index}]`));
}
if (schema.type === "object" && typeof value === "object" && value !== null && !Array.isArray(value)) {
const object = value as Record<string, Phase7JsonValue>;
const issues = (schema.required ?? [])
.filter((key) => !(key in object))
.map((key) => `${path}.${key} is required`);
for (const [key, child] of Object.entries(object)) {
const childSchema = schema.properties?.[key];
if (childSchema !== undefined) issues.push(...validateJsonSchema(childSchema, child, `${path}.${key}`));
else if (schema.additionalProperties === false) issues.push(`${path}.${key} is not allowed`);
else if (typeof schema.additionalProperties === "object") {
issues.push(...validateJsonSchema(schema.additionalProperties, child, `${path}.${key}`));
}
}
return issues;
}
return [];
}
function matchesType(type: NonNullable<Phase7JsonSchema["type"]>, value: Phase7JsonValue): boolean {
switch (type) {
case "null": return value === null;
case "array": return Array.isArray(value);
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
case "integer": return typeof value === "number" && Number.isSafeInteger(value);
default: return typeof value === type;
}
}
function redactStoredResult(
descriptor: Phase7SemanticToolDescriptor,
value: Phase7JsonValue,
): Phase7JsonValue {
if (!descriptor.redaction.some((rule) => rule.appliesTo.includes("output"))) return structuredClone(value);
if (descriptor.operationId === "read_secret_value" && typeof value === "object" && value !== null && !Array.isArray(value)) {
return { ...value, value: "[SECRET_VALUE]" };
}
return structuredClone(value);
}
function optionalStringArray(value: Phase7JsonValue | undefined): string[] | undefined {
if (value === undefined) return undefined;
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error("expected string array");
return value as string[];
}
function requireString(value: Phase7JsonValue | undefined, allowEmpty = false): string {
if (typeof value !== "string" || (!allowEmpty && value.length === 0)) throw new Error("expected string");
return value;
}
function asObject(value: Phase7JsonValue): Record<string, Phase7JsonValue> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("expected object input");
return value;
}
function toJsonValue(value: unknown): Phase7JsonValue {
return structuredClone(value) as Phase7JsonValue;
}
function deepFreeze<T>(value: T): T {
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const child of Object.values(value)) deepFreeze(child);
return value;
}
@@ -0,0 +1,179 @@
import type {
Phase7CommandResult,
Phase7InteractionKind,
Phase7JsonValue,
Phase7SemanticCommand,
} from "../mock-core/phase7-control-plane-types.js";
export type { Phase7JsonValue } from "../mock-core/phase7-control-plane-types.js";
export type Phase7ToolDisposition = "always_agent_tool" | "optional_agent_tool";
export type Phase7OptionalCatalogGroup =
| "discovery"
| "delegation_dependencies"
| "governance"
| "cases"
| "workspace_runtime"
| "routines"
| "company_skills"
| "secrets"
| "portability_admin"
| "test_escape_hatch";
export type Phase7SideEffectClass =
| "read"
| "task_write"
| "company_write"
| "governance"
| "workspace_control"
| "secret_read"
| "admin"
| "test_escape_hatch";
export type Phase7IdempotencyBehavior = "none" | "required" | "recommended";
export type Phase7ToolTaskMode = "standard" | "ask" | "planning" | "skill_test";
export interface Phase7JsonSchema {
$schema?: string;
type?: "object" | "array" | "string" | "number" | "integer" | "boolean" | "null";
title?: string;
description?: string;
properties?: Record<string, Phase7JsonSchema>;
required?: string[];
additionalProperties?: boolean | Phase7JsonSchema;
items?: Phase7JsonSchema;
enum?: Phase7JsonValue[];
minLength?: number;
minimum?: number;
oneOf?: Phase7JsonSchema[];
}
export interface Phase7RedactionRule {
path: string;
replacement: "[REDACTED]" | "[SECRET_VALUE]";
appliesTo: ReadonlyArray<"input" | "output" | "error" | "authorization_record">;
}
export type Phase7MockCommandMapping =
| { kind: "context_read"; projection: string }
| { kind: "snapshot_read"; projection: string }
| { kind: "semantic_command"; commandKind: Phase7SemanticCommand["kind"] }
| { kind: "operation_result" }
| { kind: "mock_extension"; extension: string };
export interface Phase7SemanticToolDescriptor {
operationId: string;
version: 1;
title: string;
description: string;
inputSchema: Phase7JsonSchema;
outputSchema: Phase7JsonSchema;
disposition: Phase7ToolDisposition;
optionalGroup: Phase7OptionalCatalogGroup | null;
requiredClaims: string[];
allowedRoles?: string[];
taskModes: Phase7ToolTaskMode[];
sideEffectClass: Phase7SideEffectClass;
idempotency: Phase7IdempotencyBehavior;
redaction: Phase7RedactionRule[];
mockCommandMapping: Phase7MockCommandMapping;
}
export interface Phase7ScenarioToolPolicy {
deniedOperationIds?: string[];
deniedClaims?: string[];
allowedInteractionKinds?: Phase7InteractionKind[];
allowSecretValueAccess?: boolean;
allowGenericEscapeHatch?: boolean;
genericEscapeHatchAllowlist?: Array<{ method: string; path: string }>;
}
export interface Phase7ToolAuthorizationContext {
runId: string;
actor: {
id: string;
role: string;
capabilityGrants: string[];
};
task: {
id: string;
assigneeActorId: string | null;
workMode: Phase7ToolTaskMode;
};
scenarioGrants: string[];
policy?: Phase7ScenarioToolPolicy;
}
export type Phase7AuthorizationOutcome = "exposed" | "allowed" | "denied" | "absent";
export interface Phase7AuthorizationRecord {
schema: "paperclip.phase7.authorization-record.v1";
sequence: number;
runId: string;
actorId: string;
taskId: string;
operationId: string;
phase: "exposure" | "invocation";
outcome: Phase7AuthorizationOutcome;
consideredClaims: string[];
grantedClaims: string[];
missingClaims: string[];
reason: string;
redactions: Array<{ path: string; replacement: string }>;
resultingStateChange: {
beforeRevision: number | null;
afterRevision: number | null;
entityRefs: string[];
} | null;
}
export interface Phase7VisibleToolSet {
schema: "paperclip.phase7.visible-tools.v1";
tools: Phase7SemanticToolDescriptor[];
authorizationRecords: Phase7AuthorizationRecord[];
}
export interface Phase7PolicyDenial {
schema: "paperclip.phase7.tool-result.v1";
ok: false;
error: {
code: "policy_denied" | "operation_absent" | "input_invalid" | "operation_unsupported";
message: string;
operationId: string;
reason: string;
};
authorization: Phase7AuthorizationRecord;
}
export interface Phase7ToolSuccess {
schema: "paperclip.phase7.tool-result.v1";
ok: true;
operationId: string;
operationResultId: string;
value: Phase7JsonValue;
commandResult: Phase7CommandResult | null;
authorization: Phase7AuthorizationRecord;
}
export type Phase7ToolInvocationResult = Phase7ToolSuccess | Phase7PolicyDenial;
export interface Phase7ToolInvocation {
operationId: string;
input: Phase7JsonValue;
idempotencyKey?: string;
}
export const PHASE7_CONTROL_PLANE_OWNED_OPERATION_IDS = [
"checkout_task",
"release_task",
"select_work",
"route_wake",
"enforce_budget",
"append_audit_record",
"persist_run",
"replay_run",
"schedule_blocker_wake",
"reconcile_run",
] as const;
@@ -0,0 +1,390 @@
import { describe, expect, it } from "vitest";
import type {
Phase7FixtureApproval,
Phase7FixtureSeed,
Phase7JsonValue,
} from "../mock-core/phase7-control-plane-types.js";
import { Phase7MockControlPlaneAdapter } from "../mock-core/phase7-mock-control-plane-adapter.js";
import {
PHASE7_OPTIONAL_TOOL_CATALOGS,
PHASE7_SEMANTIC_TOOL_CATALOG,
phase7SemanticTool,
} from "./phase7-semantic-tool-catalog.js";
import { Phase7SemanticToolRuntime, validateJsonSchema } from "./phase7-semantic-tool-runtime.js";
import { Phase7CodexToolBinding, Phase7FakeAgentToolBinding } from "./phase7-tool-bindings.js";
import { PHASE7_CONTROL_PLANE_OWNED_OPERATION_IDS } from "./phase7-semantic-tool-types.js";
const OPEN = {
identity: {
runId: "run-tools",
sessionId: "session-tools",
companyId: "company-1",
issueId: "task-1",
agentId: "actor-1",
},
backendKind: "mock" as const,
sourceInstanceId: "phase7-tools-test",
};
async function runtimeFor(options: {
workMode?: "standard" | "ask" | "planning" | "skill_test";
role?: string;
actorGrants?: string[];
scenarioGrants?: string[];
policy?: ConstructorParameters<typeof Phase7SemanticToolRuntime>[0]["policy"];
seed?: Phase7FixtureSeed;
resolveSecretValue?: (name: string) => string | null;
} = {}) {
const actor = {
id: "actor-1",
companyId: "company-1",
name: "Fixture Actor",
role: options.role ?? "engineer",
status: "active" as const,
budgetId: "budget-actor-1",
capabilityGrants: options.actorGrants ?? [],
};
const adapter = new Phase7MockControlPlaneAdapter({
...options.seed,
actors: [actor],
tasks: options.seed?.tasks ?? [{
id: "task-1",
companyId: "company-1",
identifier: "MCK-1",
title: "Tool boundary fixture",
description: "Protected task details",
status: "todo",
priority: "high",
workMode: options.workMode ?? "standard",
parentId: null,
assigneeActorId: "actor-1",
checkoutRunId: null,
executionRunId: null,
startedAt: null,
completedAt: null,
}],
});
await adapter.start();
await adapter.openFixtureRun(OPEN);
return {
adapter,
runtime: new Phase7SemanticToolRuntime({
adapter,
runId: OPEN.identity.runId,
scenarioGrants: options.scenarioGrants,
policy: options.policy,
resolveSecretValue: options.resolveSecretValue,
}),
};
}
describe("Phase 7 semantic tool catalog", () => {
it("defines a unique versioned provider-neutral catalog and every optional group", () => {
const operationIds = PHASE7_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId);
expect(new Set(operationIds).size).toBe(operationIds.length);
expect(PHASE7_SEMANTIC_TOOL_CATALOG.every((tool) => tool.version === 1)).toBe(true);
expect(PHASE7_SEMANTIC_TOOL_CATALOG.every((tool) => tool.inputSchema.type === "object")).toBe(true);
expect(PHASE7_SEMANTIC_TOOL_CATALOG.every((tool) => tool.outputSchema.type === "object")).toBe(true);
expect(PHASE7_SEMANTIC_TOOL_CATALOG.every((tool) => tool.mockCommandMapping !== undefined)).toBe(true);
expect(Object.keys(PHASE7_OPTIONAL_TOOL_CATALOGS).sort()).toEqual([
"cases",
"company_skills",
"delegation_dependencies",
"discovery",
"governance",
"portability_admin",
"routines",
"secrets",
"test_escape_hatch",
"workspace_runtime",
]);
expect(Object.values(PHASE7_OPTIONAL_TOOL_CATALOGS).every((tools) => tools.length > 0)).toBe(true);
expect([
"get_task_context",
"get_task_history",
"list_documents",
"read_document",
"list_document_revisions",
"report_progress",
"answer_status_question",
"finish_task",
"block_task",
"request_review",
"write_document",
"request_human_input",
"register_deliverable",
"inspect_operation_result",
].every((id) => phase7SemanticTool(id)?.disposition === "always_agent_tool")).toBe(true);
});
it("keeps control-plane operations absent and binds identical fake-agent and Codex surfaces", async () => {
const { runtime } = await runtimeFor();
const visible = runtime.visibleTools();
const fake = new Phase7FakeAgentToolBinding().bind(visible);
const codex = new Phase7CodexToolBinding().bind(visible);
expect(fake.map((tool) => tool.operationId)).toEqual(codex.map((tool) => tool.name));
expect(codex.every((tool) => tool.type === "function" && tool.strict)).toBe(true);
for (const operationId of PHASE7_CONTROL_PLANE_OWNED_OPERATION_IDS) {
expect(visible.tools.some((tool) => tool.operationId === operationId)).toBe(false);
}
expect(JSON.stringify({ fake, codex })).not.toContain("Bearer ");
});
});
describe("Phase 7 exposure and authorization", () => {
it("computes exposure before invocation from task mode, claims, role, and policy", async () => {
const baseline = await runtimeFor();
const baselineIds = baseline.runtime.visibleTools().tools.map((tool) => tool.operationId);
expect(baselineIds).toContain("finish_task");
expect(baselineIds).not.toContain("search_tasks");
const ask = await runtimeFor({ workMode: "ask" });
const askIds = ask.runtime.visibleTools().tools.map((tool) => tool.operationId);
expect(askIds).toContain("answer_status_question");
expect(askIds).not.toContain("finish_task");
expect(askIds).not.toContain("write_document");
const granted = await runtimeFor({
scenarioGrants: ["discovery:tasks:read", "governance:approvals:decide"],
role: "approver",
policy: { deniedOperationIds: ["search_tasks"] },
});
const grantedIds = granted.runtime.visibleTools().tools.map((tool) => tool.operationId);
expect(grantedIds).not.toContain("search_tasks");
expect(grantedIds).toContain("decide_approval");
expect(granted.runtime.authorizationRecords().some(
(record) => record.operationId === "search_tasks" && record.reason === "operation_denied_by_policy",
)).toBe(true);
});
it("keeps always tools active-task scoped and returns typed denials without protected state", async () => {
const protectedValue = "protected-approval-payload";
const approval: Phase7FixtureApproval = {
id: "approval-protected",
companyId: "company-1",
taskIds: ["task-1"],
type: "request_board_approval",
status: "pending",
requestedByActorId: null,
payload: { private: protectedValue },
decisionNote: null,
comments: [],
createdAt: "2026-08-09T00:00:00.000Z",
decidedAt: null,
};
const { adapter, runtime } = await runtimeFor({ seed: { approvals: [approval] } });
const revision = adapter.snapshot().revision;
const scoped = await runtime.invoke({
operationId: "get_task_context",
input: { taskId: "task-other" },
});
expect(scoped).toMatchObject({ ok: false, error: { code: "input_invalid" } });
expect(adapter.snapshot().revision).toBe(revision);
const denied = await runtime.invoke({
operationId: "decide_approval",
input: { approvalId: "approval-protected", decision: "approved", note: "approve" },
idempotencyKey: "decision-1",
});
expect(denied).toMatchObject({ ok: false, error: { code: "policy_denied" } });
expect(JSON.stringify(denied)).not.toContain(protectedValue);
const absent = await runtime.invoke({ operationId: "checkout_task", input: {} });
expect(absent).toMatchObject({
ok: false,
error: { code: "operation_absent", reason: "control_plane_owned_operation" },
});
expect(JSON.stringify(absent)).not.toContain("Protected task details");
});
it("emits authorization records with decision reasons and resulting state changes", async () => {
const { adapter, runtime } = await runtimeFor();
const beforeRevision = adapter.snapshot().revision;
const result = await runtime.invoke({
operationId: "report_progress",
input: { body: "Semantic progress." },
idempotencyKey: "progress-1",
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected success");
expect(result.authorization).toMatchObject({
outcome: "allowed",
reason: "task_scoped_always_tool",
resultingStateChange: {
beforeRevision,
afterRevision: adapter.snapshot().revision,
},
});
expect(result.authorization.resultingStateChange?.entityRefs).toEqual(
expect.arrayContaining(["task:task-1"]),
);
expect(adapter.snapshot().comments.at(-1)?.body).toBe("Semantic progress.");
expect(validateJsonSchema(
phase7SemanticTool("report_progress")!.outputSchema,
structuredClone(result) as unknown as Phase7JsonValue,
)).toEqual([]);
});
it("executes the minimum always-tool workflow through the mock command contract", async () => {
const { adapter, runtime } = await runtimeFor();
const written = await runtime.invoke({
operationId: "write_document",
input: {
key: "plan",
title: "Plan",
body: "# Semantic plan",
baseRevisionId: null,
},
idempotencyKey: "write-plan-1",
});
expect(written.ok).toBe(true);
const revisionId = written.ok
? written.commandResult?.entityRefs.find((ref) => ref.startsWith("revision:"))?.slice(9)
: undefined;
expect(revisionId).toBeTruthy();
const read = await runtime.invoke({ operationId: "read_document", input: { key: "plan" } });
expect(read).toMatchObject({ ok: true, value: { key: "plan", latestRevisionId: revisionId } });
const revisions = await runtime.invoke({
operationId: "list_document_revisions",
input: { key: "plan" },
});
expect(revisions).toMatchObject({ ok: true, value: [{ id: revisionId, revision: 1 }] });
expect(await runtime.invoke({
operationId: "request_human_input",
input: {
interactionKind: "confirmation",
title: "Confirm plan",
prompt: "Accept this plan?",
targetRevisionId: revisionId!,
continuationPolicy: "wake_assignee",
},
idempotencyKey: "confirm-plan-1",
})).toMatchObject({ ok: true });
expect(await runtime.invoke({
operationId: "register_deliverable",
input: {
filename: "report.json",
contentType: "application/json",
byteSize: 42,
sha256: "fixture-sha256",
contentRef: "fixture://report.json",
title: "Semantic report",
},
idempotencyKey: "deliverable-1",
})).toMatchObject({ ok: true });
expect(await runtime.invoke({
operationId: "request_review",
input: { summary: "Ready for semantic review." },
idempotencyKey: "review-1",
})).toMatchObject({ ok: true });
expect(adapter.snapshot()).toMatchObject({
tasks: [{ status: "in_review" }],
documents: [{ key: "plan", revisions: [{ body: "# Semantic plan" }] }],
interactions: [{ kind: "confirmation", status: "pending" }],
artifacts: [{ filename: "report.json" }],
workProducts: [{ type: "artifact" }],
});
});
});
describe("Phase 7 security policy", () => {
it("redacts secret values from records and stored operation results", async () => {
const secret = "fixture-secret-value";
const { runtime } = await runtimeFor({
scenarioGrants: ["secrets:values:read"],
policy: { allowSecretValueAccess: true },
resolveSecretValue: (name) => name === "DEPLOY_TOKEN" ? secret : null,
});
expect(runtime.visibleTools().tools.map((tool) => tool.operationId)).toContain("read_secret_value");
const result = await runtime.invoke({
operationId: "read_secret_value",
input: { name: "DEPLOY_TOKEN" },
});
expect(result).toMatchObject({ ok: true, value: { name: "DEPLOY_TOKEN", value: secret } });
if (!result.ok) throw new Error("expected secret read success");
expect(JSON.stringify(runtime.authorizationRecords())).not.toContain(secret);
expect(result.authorization.redactions).toEqual([
{ path: "$.value", replacement: "[SECRET_VALUE]" },
]);
const inspected = await runtime.invoke({
operationId: "inspect_operation_result",
input: { operationResultId: result.operationResultId },
});
expect(inspected).toMatchObject({ ok: true, value: { value: "[SECRET_VALUE]" } });
const ungranted = await runtimeFor({
scenarioGrants: ["secrets:values:read"],
resolveSecretValue: () => secret,
});
expect(ungranted.runtime.visibleTools().tools.map((tool) => tool.operationId)).not.toContain("read_secret_value");
});
it("prohibits self approval even with the role and explicit decision claim", async () => {
const approval: Phase7FixtureApproval = {
id: "approval-self",
companyId: "company-1",
taskIds: ["task-1"],
type: "request_board_approval",
status: "pending",
requestedByActorId: "actor-1",
payload: { summary: "self requested" },
decisionNote: null,
comments: [],
createdAt: "2026-08-09T00:00:00.000Z",
decidedAt: null,
};
const { adapter, runtime } = await runtimeFor({
role: "approver",
scenarioGrants: ["governance:approvals:decide"],
seed: { approvals: [approval] },
});
const result = await runtime.invoke({
operationId: "decide_approval",
input: { approvalId: "approval-self", decision: "approved", note: "self approve" },
idempotencyKey: "self-decision",
});
expect(result).toMatchObject({
ok: false,
error: { code: "policy_denied", reason: "self_approval_conflict" },
});
expect(adapter.snapshot().approvals[0]?.status).toBe("pending");
});
it("keeps the generic escape hatch test-only, explicitly granted, and allowlisted", async () => {
const denied = await runtimeFor({ workMode: "skill_test", scenarioGrants: ["test:generic_api"] });
expect(denied.runtime.visibleTools().tools.map((tool) => tool.operationId)).not.toContain("generic_api_request");
const { runtime } = await runtimeFor({
workMode: "skill_test",
scenarioGrants: ["test:generic_api"],
policy: {
allowGenericEscapeHatch: true,
genericEscapeHatchAllowlist: [{ method: "GET", path: "/mock/health" }],
},
});
expect(runtime.visibleTools().tools.map((tool) => tool.operationId)).toContain("generic_api_request");
const allowed = await runtime.invoke({
operationId: "generic_api_request",
input: { method: "GET", path: "/mock/health", headers: { authorization: "Bearer not-forwarded" } },
idempotencyKey: "escape-1",
});
expect(allowed).toMatchObject({ ok: true, value: { status: 200, body: null } });
expect(JSON.stringify(allowed)).not.toContain("not-forwarded");
const blocked = await runtime.invoke({
operationId: "generic_api_request",
input: { method: "POST", path: "/api/companies" },
idempotencyKey: "escape-2",
});
expect(blocked).toMatchObject({
ok: false,
error: { code: "policy_denied", reason: "generic_escape_hatch_target_denied" },
});
});
});
@@ -0,0 +1,233 @@
import { phase7SemanticTool, PHASE7_SEMANTIC_TOOL_CATALOG } from "./phase7-semantic-tool-catalog.js";
import {
PHASE7_CONTROL_PLANE_OWNED_OPERATION_IDS,
type Phase7AuthorizationRecord,
type Phase7JsonValue,
type Phase7SemanticToolDescriptor,
type Phase7ToolAuthorizationContext,
type Phase7VisibleToolSet,
} from "./phase7-semantic-tool-types.js";
export class Phase7ToolAuthorizationEngine {
#sequence = 0;
readonly #records: Phase7AuthorizationRecord[] = [];
computeVisibleTools(context: Phase7ToolAuthorizationContext): Phase7VisibleToolSet {
const tools: Phase7SemanticToolDescriptor[] = [];
const authorizationRecords = PHASE7_SEMANTIC_TOOL_CATALOG.map((tool) => {
const decision = this.#evaluate(tool, context, undefined);
const record = this.#record(context, tool, "exposure", decision.allowed ? "exposed" : "absent", decision.reason);
if (decision.allowed) tools.push(tool);
return record;
});
return deepFreeze({
schema: "paperclip.phase7.visible-tools.v1",
tools: clone(tools),
authorizationRecords,
});
}
authorizeInvocation(
operationId: string,
input: Phase7JsonValue,
context: Phase7ToolAuthorizationContext,
): Phase7AuthorizationRecord {
if ((PHASE7_CONTROL_PLANE_OWNED_OPERATION_IDS as readonly string[]).includes(operationId)) {
return this.#recordUnknown(context, operationId, "absent", "control_plane_owned_operation");
}
const tool = phase7SemanticTool(operationId);
if (tool === undefined) {
return this.#recordUnknown(context, operationId, "absent", "unknown_operation");
}
const decision = this.#evaluate(tool, context, input);
return this.#record(
context,
tool,
"invocation",
decision.allowed ? "allowed" : "denied",
decision.reason,
);
}
denyInvocation(
operationId: string,
context: Phase7ToolAuthorizationContext,
reason: string,
): Phase7AuthorizationRecord {
const tool = phase7SemanticTool(operationId);
return tool === undefined
? this.#recordUnknown(context, operationId, "absent", reason)
: this.#record(context, tool, "invocation", "denied", reason);
}
attachStateChange(
sequence: number,
beforeRevision: number,
afterRevision: number,
entityRefs: string[],
): Phase7AuthorizationRecord {
const record = this.#records.find((candidate) => candidate.sequence === sequence);
if (record === undefined || record.outcome !== "allowed") {
throw new Error(`cannot attach state change to authorization record ${sequence}`);
}
record.resultingStateChange = {
beforeRevision,
afterRevision,
entityRefs: [...entityRefs].sort(),
};
return deepFreeze(clone(record));
}
records(): readonly Phase7AuthorizationRecord[] {
return deepFreeze(clone(this.#records));
}
#evaluate(
tool: Phase7SemanticToolDescriptor,
context: Phase7ToolAuthorizationContext,
input: Phase7JsonValue | undefined,
): { allowed: boolean; reason: string } {
const policy = context.policy ?? {};
if (policy.deniedOperationIds?.includes(tool.operationId)) {
return { allowed: false, reason: "operation_denied_by_policy" };
}
if (!tool.taskModes.includes(context.task.workMode)) {
return { allowed: false, reason: "operation_not_available_in_task_mode" };
}
if (
tool.allowedRoles !== undefined &&
!tool.allowedRoles.map(normalize).includes(normalize(context.actor.role))
) {
return { allowed: false, reason: "actor_role_not_authorized" };
}
const deniedClaims = new Set(policy.deniedClaims ?? []);
if (tool.requiredClaims.some((claim) => deniedClaims.has(claim))) {
return { allowed: false, reason: "required_claim_denied_by_policy" };
}
const grants = new Set([...context.actor.capabilityGrants, ...context.scenarioGrants]);
if (tool.requiredClaims.some((claim) => !grants.has(claim))) {
return { allowed: false, reason: "required_claim_missing" };
}
if (tool.operationId === "read_secret_value" && policy.allowSecretValueAccess !== true) {
return { allowed: false, reason: "secret_value_access_disabled" };
}
if (tool.operationId === "generic_api_request") {
if (policy.allowGenericEscapeHatch !== true) {
return { allowed: false, reason: "generic_escape_hatch_disabled" };
}
if (input !== undefined && !genericRequestAllowed(input, policy.genericEscapeHatchAllowlist ?? [])) {
return { allowed: false, reason: "generic_escape_hatch_target_denied" };
}
}
if (tool.operationId === "request_human_input" && input !== undefined) {
const interactionKind = objectValue(input)?.interactionKind;
if (
typeof interactionKind === "string" &&
policy.allowedInteractionKinds !== undefined &&
!policy.allowedInteractionKinds.includes(interactionKind as never)
) {
return { allowed: false, reason: "interaction_kind_denied_by_policy" };
}
}
return {
allowed: true,
reason: tool.disposition === "always_agent_tool" ? "task_scoped_always_tool" : "required_claims_granted",
};
}
#record(
context: Phase7ToolAuthorizationContext,
tool: Phase7SemanticToolDescriptor,
phase: Phase7AuthorizationRecord["phase"],
outcome: Phase7AuthorizationRecord["outcome"],
reason: string,
): Phase7AuthorizationRecord {
const grants = [...new Set([...context.actor.capabilityGrants, ...context.scenarioGrants])].sort();
const consideredClaims = [...tool.requiredClaims].sort();
return this.#store({
schema: "paperclip.phase7.authorization-record.v1",
sequence: ++this.#sequence,
runId: context.runId,
actorId: context.actor.id,
taskId: context.task.id,
operationId: tool.operationId,
phase,
outcome,
consideredClaims,
grantedClaims: consideredClaims.filter((claim) => grants.includes(claim)),
missingClaims: consideredClaims.filter((claim) => !grants.includes(claim)),
reason,
redactions: tool.redaction.map(({ path, replacement }) => ({ path, replacement })),
resultingStateChange: null,
});
}
#recordUnknown(
context: Phase7ToolAuthorizationContext,
operationId: string,
outcome: "absent" | "denied",
reason: string,
): Phase7AuthorizationRecord {
return this.#store({
schema: "paperclip.phase7.authorization-record.v1",
sequence: ++this.#sequence,
runId: context.runId,
actorId: context.actor.id,
taskId: context.task.id,
operationId,
phase: "invocation",
outcome,
consideredClaims: [],
grantedClaims: [],
missingClaims: [],
reason,
redactions: [],
resultingStateChange: null,
});
}
#store(record: Phase7AuthorizationRecord): Phase7AuthorizationRecord {
this.#records.push(record);
return deepFreeze(clone(record));
}
}
function genericRequestAllowed(
input: Phase7JsonValue,
allowlist: Array<{ method: string; path: string }>,
): boolean {
const object = objectValue(input);
if (object === undefined || typeof object.method !== "string" || typeof object.path !== "string") {
return false;
}
const method = object.method.toUpperCase();
return allowlist.some(
(entry) => entry.method.toUpperCase() === method && pathMatches(entry.path, object.path as string),
);
}
function pathMatches(pattern: string, path: string): boolean {
if (!pattern.endsWith("*")) return pattern === path;
return path.startsWith(pattern.slice(0, -1));
}
function objectValue(value: Phase7JsonValue): Record<string, Phase7JsonValue> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value
: undefined;
}
function normalize(value: string): string {
return value.trim().toLowerCase();
}
function clone<T>(value: T): T {
return structuredClone(value);
}
function deepFreeze<T>(value: T): T {
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const child of Object.values(value)) deepFreeze(child);
return value;
}
@@ -0,0 +1,77 @@
import type {
Phase7JsonSchema,
Phase7SemanticToolDescriptor,
Phase7VisibleToolSet,
} from "./phase7-semantic-tool-types.js";
export interface Phase7ToolBinding<TDefinition> {
readonly kind: "fake_agent" | "codex";
bind(visibleTools: Phase7VisibleToolSet): readonly TDefinition[];
operationId(definition: TDefinition): string;
}
export interface Phase7FakeAgentToolDefinition {
operationId: string;
description: string;
inputSchema: Phase7JsonSchema;
outputSchema: Phase7JsonSchema;
}
export interface Phase7CodexToolDefinition {
type: "function";
name: string;
description: string;
strict: true;
parameters: Phase7JsonSchema;
}
export class Phase7FakeAgentToolBinding implements Phase7ToolBinding<Phase7FakeAgentToolDefinition> {
readonly kind = "fake_agent" as const;
bind(visibleTools: Phase7VisibleToolSet): readonly Phase7FakeAgentToolDefinition[] {
return freezeDefinitions(visibleTools.tools.map((tool) => ({
operationId: tool.operationId,
description: tool.description,
inputSchema: structuredClone(tool.inputSchema),
outputSchema: structuredClone(tool.outputSchema),
})));
}
operationId(definition: Phase7FakeAgentToolDefinition): string {
return definition.operationId;
}
}
export class Phase7CodexToolBinding implements Phase7ToolBinding<Phase7CodexToolDefinition> {
readonly kind = "codex" as const;
bind(visibleTools: Phase7VisibleToolSet): readonly Phase7CodexToolDefinition[] {
return freezeDefinitions(visibleTools.tools.map(toCodexDefinition));
}
operationId(definition: Phase7CodexToolDefinition): string {
return definition.name;
}
}
function toCodexDefinition(tool: Phase7SemanticToolDescriptor): Phase7CodexToolDefinition {
return {
type: "function",
name: tool.operationId,
description: tool.description,
strict: true,
parameters: structuredClone(tool.inputSchema),
};
}
function freezeDefinitions<T>(definitions: T[]): readonly T[] {
for (const definition of definitions) deepFreeze(definition);
return Object.freeze(definitions);
}
function deepFreeze<T>(value: T): T {
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const child of Object.values(value)) deepFreeze(child);
return value;
}