feat(runner): consolidate accepted Phase 7 semantic policy

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-10 12:19:36 +00:00
1 parent 14d04f0b89
commit 6419af9448
20 files changed
+5701

No files matched your search

@@ -0,0 +1,79 @@
# Phase 7 semantic catalog and authorization
Phase 7D adds a transport-neutral tool boundary over the deterministic mock
`ControlPlanePort`. It does not contain Paperclip REST routes, authentication
headers, credentials, ACPX code, or a real control-plane binding.
## Public boundary
Import the catalog, policy, dispatcher, and binding helpers from the package
root:
```ts
import {
Phase7SemanticDispatcher,
createPhase7ProviderNeutralBinding,
} from "@paperclipai/paperclip-runner";
const dispatcher = new Phase7SemanticDispatcher(mockPort, {
scenario: {
id: "dependency-manager",
claims: ["dependencies:write"],
},
explicitClaims: ["dependencies:write"],
});
const exposed = dispatcher.listTools(runId);
const result = await dispatcher.dispatch({
runId,
callId: "tool-call-1",
operationId: "set_dependencies",
input: {
idempotencyKey: "dependency-change-1",
blockedByTaskIds: ["task-2"],
},
});
```
`createPhase7ProviderNeutralBinding("fake")` and
`createPhase7ProviderNeutralBinding("live_codex")` return identical contract
arrays. Only the binding label differs. Track 7E can translate those definitions
to Codex tool definitions and pass calls to the dispatcher without changing
operation IDs, schemas, claims, or result shapes.
## Authorization order
Exposure and invocation both evaluate the actor, active-task ownership and
mode, scenario allow/deny rules, role restrictions, run claims, and explicit
claims. Optional operations require their descriptor claim to be present in the
intersection authorized for the run. The dispatcher repeats the evaluation
immediately before every mock read or command; a tool that was exposed earlier
can still be denied after a claim or ownership change.
Unauthorized optional tools are absent from `listTools`. Direct calls receive a
`paperclip.semantic-denial.v1` result. The mock command boundary independently
checks command claims and task ownership, so bypassing catalog exposure does not
bypass authorization.
The generic API escape hatch is an optional `skill_test` descriptor and is
disabled by default. When a scenario and explicit claim enable it, only two
read-only package-local paths are accepted: `/mock/state/revision` and
`/mock/task`.
## Protected data
Tool schemas contain no credential fields. Inputs containing protected keys or
credential-shaped values are rejected before mutation. Read results, denials,
and immutable semantic authorization records pass through the same recursive
redactor. Mock actor discovery also omits capability and budget internals.
Generate and verify the checked-in contracts with:
```sh
pnpm --dir packages/paperclip-runner generate:semantic-contracts
pnpm --dir packages/paperclip-runner check:semantic-contracts
```
The generated contract is
`generated/phase-07/semantic-tool-contracts.json`. Phase 7A traceability remains
under `generated/phase-07/` and is checked with `check:phase-07-contract`.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
# Phase 7 Capability Contract
Generated by `scripts/generate-phase-07-contract.mjs`; do not edit generated files.
- Skill/reference headings: 152
- Legacy MCP tools: 41
- Eval cases: 106 across 16 groups
- Deterministic content SHA-256: `667d410acdfd7ae42f60eee242c14f9fe69c454a5e1fa6efb5b6d38f6b338972`
Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation.
@@ -0,0 +1,20 @@
# Phase 7A Downstream Handoff
Generated by `scripts/generate-phase-07-contract.mjs`; do not edit generated files.
## Stable Inputs
- `capabilities.yaml`: every current Paperclip skill and reference heading, including its source anchor, disposition, semantic operation, and mock-state expectation.
- `mcp-tool-map.yaml`: the complete 41-tool legacy MCP replacement map.
- `eval-traceability.yaml`: all 106 corpus cases in 16 groups, including fixtures, grants, operations, state projections, forbids, and browser evidence IDs.
- `contract-schema.json`: required row fields and the closed disposition enum.
## Consumer Tracks
- **7B UX interaction map:** use eval `browserEvidenceRecipe`, semantic operation, and expected state to define transcript, authorization, and parity views.
- **7C mock control plane:** implement only the state projections and control-plane-owned operations represented by the generated rows.
- **7D semantic catalog:** use the operation/disposition fields to create always and optional descriptors; control-plane-owned rows stay absent from model tools.
- **7E eval conformance:** import each case by stable ID and assert the declared operation, forbidden operation set, and final mock projection.
- **7F scenario explorer:** index scenarios by the generated evidence recipe and render the linked source anchor, disposition, operation, and mock-state projection.
`pnpm --dir packages/paperclip-runner check:phase-07-contract` is the drift gate before consuming these artifacts.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,374 @@
{
"schemaVersion": 1,
"rows": [
{
"id": "mcp:paperclipMe",
"kind": "legacy_mcp_tool",
"name": "paperclipMe",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L239:paperclipme",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "injected_actor_context",
"expectedMockState": "runtime_decision_record"
},
{
"id": "mcp:paperclipInboxLite",
"kind": "legacy_mcp_tool",
"name": "paperclipInboxLite",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L245:paperclipinboxlite",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runner_work_selection",
"expectedMockState": "runtime_decision_record"
},
{
"id": "mcp:paperclipListAgents",
"kind": "legacy_mcp_tool",
"name": "paperclipListAgents",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L251:papercliplistagents",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "list_agents",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetAgent",
"kind": "legacy_mcp_tool",
"name": "paperclipGetAgent",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L257:paperclipgetagent",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_agent",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListIssues",
"kind": "legacy_mcp_tool",
"name": "paperclipListIssues",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L266:papercliplistissues",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "search_tasks",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetIssue",
"kind": "legacy_mcp_tool",
"name": "paperclipGetIssue",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L281:paperclipgetissue",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "get_task_context",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetHeartbeatContext",
"kind": "legacy_mcp_tool",
"name": "paperclipGetHeartbeatContext",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L287:paperclipgetheartbeatcontext",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "get_task_context",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListComments",
"kind": "legacy_mcp_tool",
"name": "paperclipListComments",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L296:papercliplistcomments",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "get_task_history",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetComment",
"kind": "legacy_mcp_tool",
"name": "paperclipGetComment",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L309:paperclipgetcomment",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "get_task_history",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListIssueApprovals",
"kind": "legacy_mcp_tool",
"name": "paperclipListIssueApprovals",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L316:papercliplistissueapprovals",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "get_task_context",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListDocuments",
"kind": "legacy_mcp_tool",
"name": "paperclipListDocuments",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L322:papercliplistdocuments",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "list_documents",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetDocument",
"kind": "legacy_mcp_tool",
"name": "paperclipGetDocument",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L328:paperclipgetdocument",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "read_document",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListDocumentRevisions",
"kind": "legacy_mcp_tool",
"name": "paperclipListDocumentRevisions",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L335:papercliplistdocumentrevisions",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "list_document_revisions",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListProjects",
"kind": "legacy_mcp_tool",
"name": "paperclipListProjects",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L345:papercliplistprojects",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "list_projects",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetProject",
"kind": "legacy_mcp_tool",
"name": "paperclipGetProject",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L351:paperclipgetproject",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_project",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetIssueWorkspaceRuntime",
"kind": "legacy_mcp_tool",
"name": "paperclipGetIssueWorkspaceRuntime",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L360:paperclipgetissueworkspaceruntime",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_workspace_runtime",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipControlIssueWorkspaceServices",
"kind": "legacy_mcp_tool",
"name": "paperclipControlIssueWorkspaceServices",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L366:paperclipcontrolissueworkspaceservices",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "control_workspace_service",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipWaitForIssueWorkspaceService",
"kind": "legacy_mcp_tool",
"name": "paperclipWaitForIssueWorkspaceService",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L383:paperclipwaitforissueworkspaceservice",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "wait_for_workspace_service",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListGoals",
"kind": "legacy_mcp_tool",
"name": "paperclipListGoals",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L409:papercliplistgoals",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "list_goals",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetGoal",
"kind": "legacy_mcp_tool",
"name": "paperclipGetGoal",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L415:paperclipgetgoal",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_goal",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListApprovals",
"kind": "legacy_mcp_tool",
"name": "paperclipListApprovals",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L421:papercliplistapprovals",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "list_approvals",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipCreateApproval",
"kind": "legacy_mcp_tool",
"name": "paperclipCreateApproval",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L430:paperclipcreateapproval",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "request_approval",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetApproval",
"kind": "legacy_mcp_tool",
"name": "paperclipGetApproval",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L439:paperclipgetapproval",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_approval",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipGetApprovalIssues",
"kind": "legacy_mcp_tool",
"name": "paperclipGetApprovalIssues",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L445:paperclipgetapprovalissues",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_approval_context",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipListApprovalComments",
"kind": "legacy_mcp_tool",
"name": "paperclipListApprovalComments",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L451:papercliplistapprovalcomments",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "get_approval_context",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipCreateIssue",
"kind": "legacy_mcp_tool",
"name": "paperclipCreateIssue",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L457:paperclipcreateissue",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "create_task",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipUpdateIssue",
"kind": "legacy_mcp_tool",
"name": "paperclipUpdateIssue",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L464:paperclipupdateissue",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "semantic_task_disposition",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipCheckoutIssue",
"kind": "legacy_mcp_tool",
"name": "paperclipCheckoutIssue",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L471:paperclipcheckoutissue",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "atomic_checkout",
"expectedMockState": "runtime_decision_record"
},
{
"id": "mcp:paperclipReleaseIssue",
"kind": "legacy_mcp_tool",
"name": "paperclipReleaseIssue",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L483:paperclipreleaseissue",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_release",
"expectedMockState": "runtime_decision_record"
},
{
"id": "mcp:paperclipAddComment",
"kind": "legacy_mcp_tool",
"name": "paperclipAddComment",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L489:paperclipaddcomment",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "report_progress",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipSuggestTasks",
"kind": "legacy_mcp_tool",
"name": "paperclipSuggestTasks",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L496:paperclipsuggesttasks",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipAskUserQuestions",
"kind": "legacy_mcp_tool",
"name": "paperclipAskUserQuestions",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L508:paperclipaskuserquestions",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipRequestConfirmation",
"kind": "legacy_mcp_tool",
"name": "paperclipRequestConfirmation",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L520:papercliprequestconfirmation",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipRequestCheckboxConfirmation",
"kind": "legacy_mcp_tool",
"name": "paperclipRequestCheckboxConfirmation",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L532:papercliprequestcheckboxconfirmation",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipUpsertIssueDocument",
"kind": "legacy_mcp_tool",
"name": "paperclipUpsertIssueDocument",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L544:paperclipupsertissuedocument",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "write_document",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipRestoreIssueDocumentRevision",
"kind": "legacy_mcp_tool",
"name": "paperclipRestoreIssueDocumentRevision",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L555:papercliprestoreissuedocumentrevision",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "restore_document_revision",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipLinkIssueApproval",
"kind": "legacy_mcp_tool",
"name": "paperclipLinkIssueApproval",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L570:papercliplinkissueapproval",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "link_approval",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipUnlinkIssueApproval",
"kind": "legacy_mcp_tool",
"name": "paperclipUnlinkIssueApproval",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L579:paperclipunlinkissueapproval",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "unlink_approval",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipApprovalDecision",
"kind": "legacy_mcp_tool",
"name": "paperclipApprovalDecision",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L589:paperclipapprovaldecision",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "decide_approval",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipAddApprovalComment",
"kind": "legacy_mcp_tool",
"name": "paperclipAddApprovalComment",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L611:paperclipaddapprovalcomment",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "comment_on_approval",
"expectedMockState": "operation_result"
},
{
"id": "mcp:paperclipApiRequest",
"kind": "legacy_mcp_tool",
"name": "paperclipApiRequest",
"sourceAnchor": "packages/mcp-server/src/tools.ts#L620:paperclipapirequest",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "test_only_api_escape_hatch",
"expectedMockState": "operation_result"
}
]
}
@@ -0,0 +1 @@
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active mock task, actor, wake, ancestors, budget, and interaction results.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active mock task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active mock task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable mock command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Mock entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaLine truncated
@@ -0,0 +1,214 @@
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repositoryRoot = resolve(packageRoot, "../..");
const contractPath = resolve(packageRoot, "spec/phase-07/source-contract.json");
const outputDirectory = resolve(packageRoot, "generated/phase-07");
const outputPaths = {
capabilities: resolve(outputDirectory, "capabilities.yaml"),
tools: resolve(outputDirectory, "mcp-tool-map.yaml"),
evals: resolve(outputDirectory, "eval-traceability.yaml"),
overview: resolve(outputDirectory, "capability-contract.md"),
handoff: resolve(outputDirectory, "downstream-handoff.md"),
};
const checkOnly = process.argv.includes("--check");
const dispositions = new Set(["control_plane_owned", "always_agent_tool", "optional_agent_tool"]);
function sourceAnchor(path, line, heading) {
return `${path}#L${line}:${heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")}`;
}
function classifyHeading(path, heading) {
const normalized = heading.toLowerCase();
if (/(authentication|identity|checkout|budget|error|wake|heartbeat|approval follow-up|activity|audit|release|terminology)/.test(normalized)) {
return "control_plane_owned";
}
if (/(artifact|comment|document|plan|interaction|final disposition|work product|report|question)/.test(normalized)) {
return "always_agent_tool";
}
if (/(company|agent|project|goal|routine|workspace|approval|case|secret|import|export|skill)/.test(normalized) || path.includes("api-reference")) {
return "optional_agent_tool";
}
return "control_plane_owned";
}
function semanticOperation(disposition, heading) {
const normalized = heading.toLowerCase();
if (disposition === "control_plane_owned") return "runtime_reconciliation";
if (normalized.includes("document") || normalized.includes("plan")) return "write_document";
if (normalized.includes("comment") || normalized.includes("report")) return "report_progress";
if (normalized.includes("artifact") || normalized.includes("work product")) return "register_deliverable";
if (normalized.includes("interaction") || normalized.includes("question")) return "request_human_input";
return disposition === "always_agent_tool" ? "get_task_context" : "scoped_discovery";
}
async function readSkillHeadings(paths) {
const rows = [];
for (const path of paths) {
const contents = await readFile(resolve(repositoryRoot, path), "utf8");
for (const [index, line] of contents.split(/\r?\n/).entries()) {
const match = /^(#{1,6})\s+(.+?)\s*#*$/.exec(line);
if (!match) continue;
const heading = match[2].trim();
const disposition = classifyHeading(path, heading);
rows.push({
id: `skill:${path}:${index + 1}`,
kind: "skill_heading",
sourceAnchor: sourceAnchor(path, index + 1, heading),
heading,
primaryDisposition: disposition,
semanticOperation: semanticOperation(disposition, heading),
expectedMockState: disposition === "control_plane_owned" ? "runtime_decision_record" : "operation_result",
});
}
}
return rows;
}
async function readLegacyTools() {
const path = "packages/mcp-server/src/tools.ts";
const contents = await readFile(resolve(repositoryRoot, path), "utf8");
return [...contents.matchAll(/makeTool\(\s*\n?\s*"(paperclip[A-Za-z0-9]+)"/g)].map((match) => ({
name: match[1],
sourceAnchor: sourceAnchor(path, contents.slice(0, match.index).split("\n").length, match[1]),
}));
}
function parseCase(entry, group, contract) {
const [id, title] = entry.split("|", 2);
const [primaryDisposition, validationKind, operation, expectedMockState] = contract.evalGroups[group];
return {
id,
title,
group,
sourceAnchor: `paperclip-evals/paperclip-skill-optimization/${group}.yaml#${id}`,
primaryDisposition,
fixtureProfile: `${group}-baseline`,
dominantValidationKind: validationKind,
requiredCapabilityGrants: primaryDisposition === "optional_agent_tool" ? [`${group}:read_or_write`] : [],
semanticOperation: operation,
expectedSemanticOperations: operation === "none" ? [] : [operation],
forbiddenOperations: primaryDisposition === "control_plane_owned" ? ["legacy_mcp_transport"] : [],
expectedMockState,
browserEvidenceRecipe: `${group}/${id}`,
};
}
export function validateRows(rows, label) {
const ids = new Set();
const anchors = new Set();
for (const row of rows) {
if (!row.id || ids.has(row.id)) throw new Error(`${label} has a missing or duplicate id: ${row.id ?? "<missing>"}`);
ids.add(row.id);
if (!dispositions.has(row.primaryDisposition)) throw new Error(`${label} row ${row.id} has no valid primary disposition`);
if (!row.sourceAnchor) throw new Error(`${label} row ${row.id} has no source anchor`);
if (anchors.has(row.sourceAnchor)) throw new Error(`${label} has a duplicate source anchor: ${row.sourceAnchor}`);
anchors.add(row.sourceAnchor);
}
}
function stableJson(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}
function renderOverview(capabilities, tools, evals) {
const digest = createHash("sha256").update(stableJson({ capabilities, tools, evals })).digest("hex");
return [
"# Phase 7 Capability Contract",
"",
"Generated by `scripts/generate-phase-07-contract.mjs`; do not edit generated files.",
"",
`- Skill/reference headings: ${capabilities.length}`,
`- Legacy MCP tools: ${tools.length}`,
`- Eval cases: ${evals.length} across ${new Set(evals.map((row) => row.group)).size} groups`,
`- Deterministic content SHA-256: \`${digest}\``,
"",
"Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation.",
].join("\n") + "\n";
}
function renderHandoff() {
return [
"# Phase 7A Downstream Handoff",
"",
"Generated by `scripts/generate-phase-07-contract.mjs`; do not edit generated files.",
"",
"## Stable Inputs",
"",
"- `capabilities.yaml`: every current Paperclip skill and reference heading, including its source anchor, disposition, semantic operation, and mock-state expectation.",
"- `mcp-tool-map.yaml`: the complete 41-tool legacy MCP replacement map.",
"- `eval-traceability.yaml`: all 106 corpus cases in 16 groups, including fixtures, grants, operations, state projections, forbids, and browser evidence IDs.",
"- `contract-schema.json`: required row fields and the closed disposition enum.",
"",
"## Consumer Tracks",
"",
"- **7B UX interaction map:** use eval `browserEvidenceRecipe`, semantic operation, and expected state to define transcript, authorization, and parity views.",
"- **7C mock control plane:** implement only the state projections and control-plane-owned operations represented by the generated rows.",
"- **7D semantic catalog:** use the operation/disposition fields to create always and optional descriptors; control-plane-owned rows stay absent from model tools.",
"- **7E eval conformance:** import each case by stable ID and assert the declared operation, forbidden operation set, and final mock projection.",
"- **7F scenario explorer:** index scenarios by the generated evidence recipe and render the linked source anchor, disposition, operation, and mock-state projection.",
"",
"`pnpm --dir packages/paperclip-runner check:phase-07-contract` is the drift gate before consuming these artifacts.",
].join("\n") + "\n";
}
async function buildContract() {
const contract = JSON.parse(await readFile(contractPath, "utf8"));
const capabilities = await readSkillHeadings(contract.skillSources);
const discoveredTools = await readLegacyTools();
const tools = discoveredTools.map((tool) => {
const mapping = contract.toolMappings[tool.name];
if (!mapping) throw new Error(`Legacy MCP tool ${tool.name} is unclassified`);
return {
id: `mcp:${tool.name}`,
kind: "legacy_mcp_tool",
name: tool.name,
sourceAnchor: tool.sourceAnchor,
primaryDisposition: mapping[0],
semanticOperation: mapping[1],
expectedMockState: mapping[0] === "control_plane_owned" ? "runtime_decision_record" : "operation_result",
};
});
const evals = Object.entries(contract.evalCases).flatMap(([group, entries]) => entries.map((entry) => parseCase(entry, group, contract)));
validateRows(capabilities, "Skill headings");
validateRows(tools, "MCP tools");
validateRows(evals, "Eval cases");
const discoveredToolNames = new Set(discoveredTools.map((tool) => tool.name));
for (const mappedToolName of Object.keys(contract.toolMappings)) {
if (!discoveredToolNames.has(mappedToolName)) throw new Error(`MCP mapping has no registered source tool: ${mappedToolName}`);
}
if (tools.length !== 41 || Object.keys(contract.toolMappings).length !== 41) throw new Error(`Expected 41 legacy MCP tools, found ${tools.length}`);
if (evals.length !== 106 || new Set(evals.map((row) => row.group)).size !== 16) throw new Error(`Expected 106 eval cases in 16 groups, found ${evals.length}`);
return {
[outputPaths.capabilities]: stableJson({ schemaVersion: 1, rows: capabilities }),
[outputPaths.tools]: stableJson({ schemaVersion: 1, rows: tools }),
[outputPaths.evals]: stableJson({ schemaVersion: 1, rows: evals }),
[outputPaths.overview]: renderOverview(capabilities, tools, evals),
[outputPaths.handoff]: renderHandoff(),
};
}
export async function main() {
const output = await buildContract();
for (const [path, contents] of Object.entries(output)) {
if (checkOnly) {
if (!existsSync(path) || await readFile(path, "utf8") !== contents) throw new Error(`Generated contract drift: ${relative(packageRoot, path)}`);
} else {
await writeFile(path, contents);
}
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
@@ -0,0 +1,19 @@
import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { serializePhase7GeneratedSemanticContracts } from "../dist/semantic-tools/provider-neutral.js";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const outputPath = resolve(packageRoot, "generated/phase-07/semantic-tool-contracts.json");
const generated = serializePhase7GeneratedSemanticContracts();
if (process.argv.includes("--check")) {
const current = await readFile(outputPath, "utf8").catch(() => "");
if (current !== generated) {
process.stderr.write("semantic-tool-contracts.json is stale; run generate:semantic-contracts\n");
process.exitCode = 1;
}
} else {
await writeFile(outputPath, generated);
process.stdout.write(`wrote ${outputPath}\n`);
}
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.ai/schemas/phase-07-capability-row.json",
"title": "Phase 7 capability traceability row",
"type": "object",
"required": ["id", "sourceAnchor", "primaryDisposition", "semanticOperation", "expectedMockState"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"sourceAnchor": { "type": "string", "minLength": 1 },
"primaryDisposition": {
"enum": ["control_plane_owned", "always_agent_tool", "optional_agent_tool"]
},
"semanticOperation": { "type": "string" },
"expectedMockState": { "type": "string" }
},
"additionalProperties": true
}
@@ -0,0 +1,93 @@
{
"schemaVersion": 1,
"dispositions": ["control_plane_owned", "always_agent_tool", "optional_agent_tool"],
"skillSources": [
"skills/paperclip/SKILL.md",
"skills/paperclip/references/api-reference.md",
"skills/paperclip/references/artifacts.md",
"skills/paperclip/references/cases.md",
"skills/paperclip/references/company-skills.md",
"skills/paperclip/references/issue-workspaces.md",
"skills/paperclip/references/routines.md",
"skills/paperclip/references/workflows.md"
],
"toolMappings": {
"paperclipMe": ["control_plane_owned", "injected_actor_context"],
"paperclipInboxLite": ["control_plane_owned", "runner_work_selection"],
"paperclipListAgents": ["optional_agent_tool", "list_agents"],
"paperclipGetAgent": ["optional_agent_tool", "get_agent"],
"paperclipListIssues": ["optional_agent_tool", "search_tasks"],
"paperclipGetIssue": ["always_agent_tool", "get_task_context"],
"paperclipGetHeartbeatContext": ["always_agent_tool", "get_task_context"],
"paperclipListComments": ["always_agent_tool", "get_task_history"],
"paperclipGetComment": ["always_agent_tool", "get_task_history"],
"paperclipListIssueApprovals": ["always_agent_tool", "get_task_context"],
"paperclipListDocuments": ["always_agent_tool", "list_documents"],
"paperclipGetDocument": ["always_agent_tool", "read_document"],
"paperclipListDocumentRevisions": ["always_agent_tool", "list_document_revisions"],
"paperclipListProjects": ["optional_agent_tool", "list_projects"],
"paperclipGetProject": ["optional_agent_tool", "get_project"],
"paperclipGetIssueWorkspaceRuntime": ["optional_agent_tool", "get_workspace_runtime"],
"paperclipControlIssueWorkspaceServices": ["optional_agent_tool", "control_workspace_service"],
"paperclipWaitForIssueWorkspaceService": ["optional_agent_tool", "wait_for_workspace_service"],
"paperclipListGoals": ["optional_agent_tool", "list_goals"],
"paperclipGetGoal": ["optional_agent_tool", "get_goal"],
"paperclipListApprovals": ["optional_agent_tool", "list_approvals"],
"paperclipCreateApproval": ["optional_agent_tool", "request_approval"],
"paperclipGetApproval": ["optional_agent_tool", "get_approval"],
"paperclipGetApprovalIssues": ["optional_agent_tool", "get_approval_context"],
"paperclipListApprovalComments": ["optional_agent_tool", "get_approval_context"],
"paperclipCreateIssue": ["optional_agent_tool", "create_task"],
"paperclipUpdateIssue": ["optional_agent_tool", "semantic_task_disposition"],
"paperclipCheckoutIssue": ["control_plane_owned", "atomic_checkout"],
"paperclipReleaseIssue": ["control_plane_owned", "runtime_release"],
"paperclipAddComment": ["always_agent_tool", "report_progress"],
"paperclipSuggestTasks": ["always_agent_tool", "request_human_input"],
"paperclipAskUserQuestions": ["always_agent_tool", "request_human_input"],
"paperclipRequestConfirmation": ["always_agent_tool", "request_human_input"],
"paperclipRequestCheckboxConfirmation": ["always_agent_tool", "request_human_input"],
"paperclipUpsertIssueDocument": ["always_agent_tool", "write_document"],
"paperclipRestoreIssueDocumentRevision": ["optional_agent_tool", "restore_document_revision"],
"paperclipLinkIssueApproval": ["optional_agent_tool", "link_approval"],
"paperclipUnlinkIssueApproval": ["optional_agent_tool", "unlink_approval"],
"paperclipApprovalDecision": ["optional_agent_tool", "decide_approval"],
"paperclipAddApprovalComment": ["optional_agent_tool", "comment_on_approval"],
"paperclipApiRequest": ["optional_agent_tool", "test_only_api_escape_hatch"]
},
"evalGroups": {
"hb": ["control_plane_owned", "control_plane_invariant", "get_task_context", "active_task_context"],
"co": ["control_plane_owned", "control_plane_invariant", "atomic_checkout", "checkout_decision"],
"st": ["always_agent_tool", "combined_multi_hop", "semantic_task_disposition", "terminal_or_blocked_task"],
"cm": ["always_agent_tool", "agent_tool_contract", "report_progress", "rendered_comment"],
"se": ["optional_agent_tool", "agent_tool_contract", "search_tasks", "company_scoped_search"],
"su": ["optional_agent_tool", "combined_multi_hop", "create_child_task", "delegation_and_wake"],
"bl": ["always_agent_tool", "combined_multi_hop", "block_task", "blocker_projection"],
"dp": ["always_agent_tool", "agent_tool_contract", "write_document", "revision_safe_document"],
"ix": ["always_agent_tool", "combined_multi_hop", "request_human_input", "interaction_and_wake"],
"ap": ["optional_agent_tool", "authorization_policy", "request_approval", "approval_gate"],
"ar": ["always_agent_tool", "agent_tool_contract", "register_deliverable", "artifact_and_work_product"],
"er": ["control_plane_owned", "combined_multi_hop", "runtime_reconciliation", "recovery_or_restraint"],
"rf": ["optional_agent_tool", "agent_tool_contract", "optional_reference_workflow", "scoped_reference_result"],
"mh": ["always_agent_tool", "combined_multi_hop", "create_child_task", "multi_hop_projection"],
"rs": ["control_plane_owned", "restraint_no_call", "none", "unchanged_state"],
"wk": ["control_plane_owned", "combined_multi_hop", "wake_reconciliation", "wake_mode_projection"]
},
"evalCases": {
"hb": ["hb-context-01|Prefer compact heartbeat context", "hb-inbox-lite-01|Use compact inbox for normal heartbeat", "hb-pick-priority-01|Prioritize in-progress work", "hb-scoped-wake-01|Scoped wake skips inbox", "hb-wake-comment-01|Read triggering comment first"],
"co": ["co-409-stop-01|Stop after checkout conflict", "co-409-stop-02|Named conflict remains terminal", "co-before-work-01|Checkout before issue writes", "co-body-contract-01|Send checkout request contract", "co-no-status-patch-01|Do not patch into progress", "co-runid-header-01|Audit modifying calls"],
"st": ["st-backlog-park-01|Park postponed work in backlog", "st-blocked-owner-01|Record blocker owner", "st-crossteam-cancel-01|Reassign cross-team task instead of cancelling", "st-done-comment-01|Close task with explanatory comment", "st-env-blocked-notdone-01|Block impossible deliverable with an owner", "st-ephemeral-verify-01|Disclose ephemeral verification", "st-handback-review-01|Return task for board review", "st-unverified-toolchain-01|Block when verification cannot run"],
"cm": ["cm-mention-structured-01|Render structured mentions", "cm-multiline-01|Preserve multiline comments", "cm-prefixed-url-01|Prefix internal URLs by company", "cm-progress-nextaction-01|Report progress and next owner", "cm-runid-modify-01|Audit comment writes", "cm-ticket-link-01|Render ticket links"],
"se": ["se-get-issue-01|Read issue and history", "se-q-comments-01|Search comment bodies", "se-q-filters-01|Combine search filters", "se-q-topic-01|Find issue by topic"],
"su": ["su-crossteam-billing-01|Set cross-team billing", "su-inherit-workspace-01|Inherit workspace for follow-up", "su-no-poll-01|Delegate without polling", "su-parent-goal-01|Create subtasks with parent and goal"],
"bl": ["bl-cancelled-not-resolved-01|Cancelled blocker remains unresolved", "bl-clear-01|Clear blocker set", "bl-create-blocked-01|Create dependent work blocked", "bl-firstclass-01|Use first-class dependencies", "bl-read-owners-01|Read blocker owners"],
"dp": ["dp-base-revision-01|Use latest document revision", "dp-plan-doc-01|Store plan in plan document", "dp-plan-link-comment-01|Deep-link plan document"],
"ix": ["ix-checkbox-01|Use checkbox confirmation for subset", "ix-checkbox-result-01|Act on selected option IDs", "ix-confirmation-plan-01|Bind plan confirmation to revision", "ix-continuation-01|Use continuation wake policy", "ix-questions-01|Use typed questions", "ix-stale-target-01|Rebuild after stale target", "ix-suggest-tasks-01|Suggest before creating tasks", "ix-superseded-comment-01|Address superseding comment", "ix-verdicts-01|Request item verdicts"],
"ap": ["ap-approval-deny-01|Keep denied approval task open", "ap-approval-wake-01|Resolve approval wake", "ap-board-approval-01|Request board approval for spend", "ap-mcp-expiry-01|Re-call once after approval expiry", "ap-mcp-gate-01|Wait on pending MCP approval", "ap-mcp-pathmissing-01|Reroute on missing approval path"],
"ar": ["ar-no-done-without-upload-01|Upload file deliverable before close", "ar-upload-before-done-01|Attach deliverable before done", "ar-workproduct-pr-01|Register pull request work product", "ar-workproduct-wsfile-01|Register workspace file work product"],
"er": ["er-blocked-dedup-01|Avoid duplicate blocked update", "er-budget-critical-01|Choose critical work at high budget", "er-close-retry-fault-01|Retry terminal close faults", "er-exec-not-participant-01|Do not advance foreign review stage", "er-exec-participant-01|Advance own review stage", "er-mention-handoff-01|Checkout explicit mention handoff", "er-mention-noassign-01|Do not claim FYI mention", "er-no-unassigned-01|Exit on empty inbox", "er-release-01|Release task handback"],
"rf": ["rf-api-404-report-01|Report missing issue honestly", "rf-api-cancel-obsolete-01|Cancel obsolete work", "rf-api-mention-discipline-01|Avoid non-actionable mentions", "rf-api-mgr-heartbeat-01|Run manager heartbeat", "rf-api-review-changes-01|Request review changes", "rf-art-attachment-wp-01|Register attachment work product", "rf-case-child-01|Create child case", "rf-case-lifecycle-link-01|Link active case", "rf-case-upsert-doc-01|Upsert case document", "rf-cskill-audit-01|Audit library and attached skills", "rf-cskill-install-attach-01|Install then attach skill", "rf-cskill-self-sync-01|Preserve skills during sync", "rf-iws-start-url-01|Start issue workspace service", "rf-iws-target-restart-01|Restart selected workspace service", "rf-routine-create-01|Create weekly routine", "rf-routine-manual-run-01|Manually run routine", "rf-routine-pause-01|Pause routine reversibly", "rf-routine-policy-01|Configure routine policies", "rf-routine-webhook-01|Add signed webhook trigger", "rf-wf-export-preview-01|Preview narrowed export", "rf-wf-invite-01|Create paste-ready invite", "rf-wf-project-setup-01|Set up repo workspace project"],
"mh": ["mh-blocked-handoff-01|Create review handoff blocker", "mh-children-complete-01|Complete parent after children", "mh-plan-confirm-01|Confirm a written plan", "mh-subtask-tree-01|Create sequenced subtask tree"],
"rs": ["rs-dependency-blocked-wake-01|Triages dependency-blocked wake", "rs-question-only-01|Answer question without mutation", "rs-secret-hygiene-01|Never expose API key"],
"wk": ["wk-ask-mode-01|Answer-only ask-mode wake", "wk-plan-accepted-01|Create tasks after accepted plan", "wk-plan-annotation-01|Revise annotated plan", "wk-plan-directive-01|Write plan then request confirmation", "wk-plan-directive-02|Honor prose planning directive", "wk-recovery-processlost-01|Recover without duplicate work", "wk-resume-delta-01|Apply condensed resume contract", "wk-skilltest-mode-01|Write structured skill test output"]
}
}
@@ -0,0 +1,216 @@
import type {
Phase7JsonSchema,
Phase7SemanticOperationId,
Phase7SemanticToolDescriptor,
} from "./types.js";
const ALL_MODES = ["standard", "ask", "planning", "skill_test"] as const;
const WORK_MODES = ["standard", "planning", "skill_test"] as const;
const STANDARD_MODE = ["standard"] as const;
const text = (description: string, maxLength = 20_000): Phase7JsonSchema => ({
type: "string",
description,
minLength: 1,
maxLength,
});
const nullableText = (description: string): Phase7JsonSchema => ({
type: ["string", "null"],
description,
maxLength: 20_000,
});
const stringArray = (description: string): Phase7JsonSchema => ({
type: "array",
description,
items: { type: "string", minLength: 1 },
maxItems: 200,
uniqueItems: true,
});
const object = (
properties: Readonly<Record<string, Phase7JsonSchema>> = {},
required: readonly string[] = [],
): Phase7JsonSchema => ({
type: "object",
properties,
required,
additionalProperties: false,
});
const jsonObject: Phase7JsonSchema = { type: "object", additionalProperties: true };
const operationResult = object({
commandId: text("Stable mock command identifier.", 200),
disposition: { enum: ["applied", "duplicate"] },
stateRevision: { type: "integer", minimum: 0 },
entityRefs: stringArray("Mock entities affected by the operation."),
scheduledWakeIds: stringArray("Wake identifiers scheduled by the operation."),
}, ["commandId", "disposition", "stateRevision", "entityRefs", "scheduledWakeIds"]);
const readResult: Phase7JsonSchema = { type: "object", additionalProperties: true };
interface DescriptorInput {
operationId: Phase7SemanticOperationId;
title: string;
description: string;
exposure?: "always" | "optional";
requiredClaims?: readonly string[];
allowedModes?: Phase7SemanticToolDescriptor["allowedModes"];
allowedRoles?: readonly string[];
disabledByDefault?: boolean;
inputSchema?: Phase7JsonSchema;
outputSchema?: Phase7JsonSchema;
}
function descriptor(input: DescriptorInput): Phase7SemanticToolDescriptor {
return {
schema: "paperclip.semantic-tool.v1",
operationId: input.operationId,
version: 1,
title: input.title,
description: input.description,
exposure: input.exposure ?? "always",
requiredClaims: input.requiredClaims ?? [],
allowedModes: input.allowedModes ?? ALL_MODES,
...(input.allowedRoles === undefined ? {} : { allowedRoles: input.allowedRoles }),
...(input.disabledByDefault === true ? { disabledByDefault: true } : {}),
inputSchema: input.inputSchema ?? object(),
outputSchema: input.outputSchema ?? readResult,
};
}
const idempotency = { idempotencyKey: text("Caller-stable retry key.", 240) } as const;
const descriptors: readonly Phase7SemanticToolDescriptor[] = [
descriptor({
operationId: "get_task_context",
title: "Get active task context",
description: "Read the active mock task, actor, wake, ancestors, budget, and interaction results.",
}),
descriptor({
operationId: "get_task_history",
title: "Get active task history",
description: "Read bounded comments on the active mock task.",
inputSchema: object({ limit: { type: "integer", minimum: 1, maximum: 200, default: 50 } }),
}),
descriptor({ operationId: "list_documents", title: "List task documents", description: "List revisioned documents on the active mock task." }),
descriptor({
operationId: "read_document",
title: "Read task document",
description: "Read the current revision of one active-task document.",
inputSchema: object({ key: text("Stable issue-document key.", 120) }, ["key"]),
}),
descriptor({
operationId: "list_document_revisions",
title: "List document revisions",
description: "Read bounded revision history for one active-task document.",
inputSchema: object({ key: text("Stable issue-document key.", 120), limit: { type: "integer", minimum: 1, maximum: 200, default: 50 } }, ["key"]),
}),
descriptor({
operationId: "report_progress",
title: "Report durable progress",
description: "Append a durable progress comment to the active mock task.",
inputSchema: object({ ...idempotency, body: text("Multiline progress update.") }, ["idempotencyKey", "body"]),
outputSchema: operationResult,
}),
descriptor({
operationId: "answer_status_question",
title: "Answer status question",
description: "Append the answer to a status-only wake without changing task disposition.",
inputSchema: object({ ...idempotency, body: text("Concise status answer.") }, ["idempotencyKey", "body"]),
outputSchema: operationResult,
}),
descriptor({
operationId: "write_document",
title: "Write revisioned document",
description: "Create or update an active-task document with optimistic revision safety.",
allowedModes: WORK_MODES,
inputSchema: object({
...idempotency,
key: text("Stable issue-document key.", 120),
title: text("Document title.", 300),
body: text("Markdown document body.", 200_000),
baseRevisionId: nullableText("Current revision id, or null when creating."),
changeSummary: nullableText("Optional revision summary."),
}, ["idempotencyKey", "key", "title", "body", "baseRevisionId"]),
outputSchema: operationResult,
}),
descriptor({
operationId: "request_human_input",
title: "Request structured human input",
description: "Create a typed, durable interaction on the active mock task.",
allowedModes: WORK_MODES,
inputSchema: object({
...idempotency,
interactionKind: { enum: ["confirmation", "checkbox", "questions", "suggest_tasks", "item_verdicts"] },
title: text("Interaction card title.", 300),
prompt: text("Question or decision prompt.", 10_000),
payload: jsonObject,
targetRevisionId: nullableText("Optional bound document revision."),
continuationPolicy: { enum: ["none", "wake_assignee", "wake_assignee_on_accept"] },
}, ["idempotencyKey", "interactionKind", "title", "prompt", "continuationPolicy"]),
outputSchema: operationResult,
}),
descriptor({
operationId: "register_deliverable",
title: "Register inspectable deliverable",
description: "Register mock attachment metadata and its artifact work product without credentials or bytes in the tool result.",
allowedModes: WORK_MODES,
inputSchema: object({
...idempotency,
filename: text("Display filename.", 500),
contentType: text("Media type.", 200),
byteSize: { type: "integer", minimum: 0, maximum: 100_000_000 },
sha256: { type: "string", pattern: "^[a-fA-F0-9]{64}$" },
contentRef: text("Opaque package-local content reference.", 2_000),
title: text("Work-product title.", 500),
}, ["idempotencyKey", "filename", "contentType", "byteSize", "sha256", "contentRef", "title"]),
outputSchema: operationResult,
}),
descriptor({ operationId: "finish_task", title: "Finish active task", description: "Finish the active mock task with a durable summary.", allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, summary: text("Completion summary.") }, ["idempotencyKey", "summary"]), outputSchema: operationResult }),
descriptor({ operationId: "block_task", title: "Block active task", description: "Block the active mock task with a durable reason and optional first-class dependencies.", allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, reason: text("Block reason."), blockedByTaskIds: stringArray("Internal mock task ids that block this task.") }, ["idempotencyKey", "reason"]), outputSchema: operationResult }),
descriptor({ operationId: "request_review", title: "Request task review", description: "Move the active mock task to review with a durable summary.", allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, summary: text("Review handoff summary.") }, ["idempotencyKey", "summary"]), outputSchema: operationResult }),
descriptor({ operationId: "list_agents", title: "List company agents", description: "List redacted mock actor profiles.", exposure: "optional", requiredClaims: ["discovery:agents:read"] }),
descriptor({ operationId: "get_agent", title: "Get company agent", description: "Read one redacted mock actor profile.", exposure: "optional", requiredClaims: ["discovery:agents:read"], inputSchema: object({ actorId: text("Mock actor id.", 200) }, ["actorId"]) }),
descriptor({ operationId: "search_tasks", title: "Search company tasks", description: "Search mock tasks by text and status within the run company.", exposure: "optional", requiredClaims: ["discovery:tasks:read"], inputSchema: object({ query: { type: "string", maxLength: 500 }, statuses: { type: "array", items: { enum: ["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"] }, maxItems: 7, uniqueItems: true }, limit: { type: "integer", minimum: 1, maximum: 200, default: 50 } }) }),
descriptor({ operationId: "list_approvals", title: "List approvals", description: "List mock approvals in the run company.", exposure: "optional", requiredClaims: ["governance:approvals:read"] }),
descriptor({ operationId: "get_approval", title: "Get approval", description: "Read one mock approval without protected data.", exposure: "optional", requiredClaims: ["governance:approvals:read"], inputSchema: object({ approvalId: text("Mock approval id.", 200) }, ["approvalId"]) }),
descriptor({ operationId: "get_approval_context", title: "Get approval context", description: "Read one approval, its comments, and linked mock tasks.", exposure: "optional", requiredClaims: ["governance:approvals:read"], inputSchema: object({ approvalId: text("Mock approval id.", 200) }, ["approvalId"]) }),
descriptor({ operationId: "get_workspace_runtime", title: "Get workspace runtime", description: "Read active-task mock workspace services.", exposure: "optional", requiredClaims: ["workspace:read"] }),
descriptor({ operationId: "control_workspace_service", title: "Control workspace service", description: "Start, stop, or fault one active-task mock workspace service.", exposure: "optional", requiredClaims: ["workspace:control"], allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, serviceId: text("Mock workspace service id.", 200), action: { enum: ["start", "stop", "fail"] }, url: nullableText("Optional mock service URL.") }, ["idempotencyKey", "serviceId", "action"]), outputSchema: operationResult }),
descriptor({ operationId: "set_dependencies", title: "Set task dependencies", description: "Replace the active task's first-class blocker set.", exposure: "optional", requiredClaims: ["dependencies:write"], allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, blockedByTaskIds: stringArray("Replacement blocker task ids.") }, ["idempotencyKey", "blockedByTaskIds"]), outputSchema: operationResult }),
descriptor({ operationId: "create_task", title: "Create child task", description: "Create one child mock task under the active task.", exposure: "optional", requiredClaims: ["delegation:tasks:create"], allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, title: text("Child task title.", 500), description: nullableText("Child task description."), assigneeActorId: nullableText("Optional mock actor assignee."), priority: { enum: ["critical", "high", "medium", "low"] }, blockedByTaskIds: stringArray("Initial blocker task ids.") }, ["idempotencyKey", "title"]), outputSchema: operationResult }),
descriptor({ operationId: "request_approval", title: "Request approval", description: "Create a governed mock approval and waiting posture.", exposure: "optional", requiredClaims: ["governance:approvals:request"], allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, approvalType: text("Stable approval type.", 200), payload: jsonObject }, ["idempotencyKey", "approvalType", "payload"]), outputSchema: operationResult }),
descriptor({ operationId: "decide_approval", title: "Decide approval", description: "Decide a mock approval as an explicitly authorized approver.", exposure: "optional", requiredClaims: ["governance:approvals:decide"], allowedRoles: ["board", "approver", "security"], allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, approvalId: text("Mock approval id.", 200), decision: { enum: ["approved", "rejected", "cancelled"] }, note: text("Decision note.") }, ["idempotencyKey", "approvalId", "decision", "note"]), outputSchema: operationResult }),
descriptor({ operationId: "comment_on_approval", title: "Comment on approval", description: "Add a durable comment to a mock approval.", exposure: "optional", requiredClaims: ["governance:approvals:comment"], inputSchema: object({ ...idempotency, approvalId: text("Mock approval id.", 200), body: text("Approval comment.") }, ["idempotencyKey", "approvalId", "body"]), outputSchema: operationResult }),
descriptor({ operationId: "schedule_wake", title: "Schedule bounded wake", description: "Schedule a deterministic mock continuation wake.", exposure: "optional", requiredClaims: ["control_plane:wakes"], allowedModes: STANDARD_MODE, inputSchema: object({ ...idempotency, reason: { enum: ["manual", "issue_commented", "interaction_resolved", "approval_resolved", "blockers_resolved", "scheduled_retry", "resume"] }, payload: jsonObject, delayTicks: { type: "integer", minimum: 1, maximum: 10_000 } }, ["idempotencyKey", "reason", "delayTicks"]), outputSchema: operationResult }),
descriptor({ operationId: "generic_api_request", title: "Generic API request", description: "Test-only escape hatch. Disabled unless the scenario and explicit claim both enable it.", exposure: "optional", requiredClaims: ["test:generic_api_request"], disabledByDefault: true, allowedModes: ["skill_test"], inputSchema: object({ method: { enum: ["GET", "POST", "PATCH"] }, path: { type: "string", pattern: "^/mock/", maxLength: 500 }, body: jsonObject }, ["method", "path"]) }),
];
const byId = new Map(descriptors.map((item) => [item.operationId, item]));
if (byId.size !== descriptors.length) throw new Error("duplicate Phase 7 semantic operation id");
export const PHASE7_SEMANTIC_TOOL_CATALOG = deepFreeze(descriptors);
export function phase7SemanticToolDescriptor(
operationId: string,
): Phase7SemanticToolDescriptor | undefined {
return byId.get(operationId as Phase7SemanticOperationId);
}
export function canonicalPhase7SemanticCatalog(): string {
return canonicalJson(PHASE7_SEMANTIC_TOOL_CATALOG);
}
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;
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}
@@ -0,0 +1,566 @@
import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
import type {
Phase7CommandOutcome,
Phase7JsonValue,
Phase7MockControlPlanePort,
Phase7RunContext,
Phase7SemanticCommand,
} from "../mock-core/phase7-control-plane-types.js";
import {
PHASE7_SEMANTIC_TOOL_CATALOG,
phase7SemanticToolDescriptor,
} from "./catalog.js";
import {
createPhase7SemanticPolicyContext,
decidePhase7SemanticAuthorization,
DEFAULT_PHASE7_SCENARIO_POLICY,
} from "./policy.js";
import {
containsProtectedSemanticData,
redactSemanticValue,
} from "./redaction.js";
import type {
Phase7SemanticAuthorizationDecision,
Phase7SemanticAuthorizationRecord,
Phase7SemanticDenialCode,
Phase7SemanticOperationId,
Phase7SemanticPolicyContext,
Phase7SemanticScenarioPolicy,
Phase7SemanticToolCall,
Phase7SemanticToolDefinition,
Phase7SemanticToolDescriptor,
Phase7SemanticToolResult,
} from "./types.js";
export type Phase7SemanticControlPlane = Pick<
Phase7MockControlPlanePort,
"context" | "snapshot" | "tryApplyCommand"
>;
export interface Phase7SemanticDispatcherOptions {
readonly scenario?: Phase7SemanticScenarioPolicy;
readonly explicitClaims?: readonly string[];
}
interface OperationSuccess {
result: Phase7JsonValue;
stateRevision: number;
}
class SemanticDispatchFailure extends Error {
constructor(
readonly code: Phase7SemanticDenialCode,
message: string,
readonly retryable = false,
) {
super(message);
this.name = "SemanticDispatchFailure";
}
}
export class Phase7SemanticDispatcher {
readonly #validators = new Map<Phase7SemanticOperationId, ValidateFunction>();
readonly #records: Phase7SemanticAuthorizationRecord[] = [];
#recordCounter = 0;
constructor(
readonly port: Phase7SemanticControlPlane,
readonly options: Phase7SemanticDispatcherOptions = {},
) {
const ajv = new Ajv2020({ allErrors: true, strict: false });
for (const descriptor of PHASE7_SEMANTIC_TOOL_CATALOG) {
this.#validators.set(descriptor.operationId, ajv.compile(descriptor.inputSchema));
}
}
listTools(runId: string): readonly Phase7SemanticToolDefinition[] {
const policyContext = this.#policyContext(runId);
const definitions: Phase7SemanticToolDefinition[] = [];
for (const descriptor of PHASE7_SEMANTIC_TOOL_CATALOG) {
const decision = decidePhase7SemanticAuthorization(
descriptor,
policyContext,
"exposure",
);
this.#record(policyContext, decision, null, null, null);
if (decision.allowed) definitions.push(toDefinition(descriptor));
}
return deepFreeze(definitions);
}
authorizationRecords(): readonly Phase7SemanticAuthorizationRecord[] {
return deepFreeze(structuredClone(this.#records));
}
async dispatch(call: Phase7SemanticToolCall): Promise<Phase7SemanticToolResult> {
const descriptor = phase7SemanticToolDescriptor(call.operationId);
if (descriptor === undefined) {
return this.#unknownToolDenial(call);
}
const policyContext = this.#policyContext(call.runId);
const invocation = decidePhase7SemanticAuthorization(
descriptor,
policyContext,
"invocation",
call.input,
);
if (!invocation.allowed) {
const result = this.#denial(call, descriptor.operationId, denialCode(invocation), invocation.reason);
this.#record(policyContext, invocation, call.callId, call.input, result);
return result;
}
if (containsProtectedSemanticData(call.input)) {
const decision = deniedDecision(
invocation,
"protected_data_denied",
"Protected data is not accepted by semantic tools.",
);
const result = this.#denial(call, descriptor.operationId, denialCode(decision), decision.reason);
this.#record(policyContext, decision, call.callId, call.input, result);
return result;
}
const validator = this.#validators.get(descriptor.operationId);
if (validator === undefined || !validator(call.input)) {
const decision = deniedDecision(
invocation,
"input_invalid",
formatValidationError(validator),
);
const result = this.#denial(call, descriptor.operationId, denialCode(decision), decision.reason);
this.#record(policyContext, decision, call.callId, call.input, result);
return result;
}
try {
const executed = await this.#execute(
descriptor.operationId,
call.runId,
call.input as Record<string, unknown>,
);
const result = deepFreeze({
ok: true,
operationId: descriptor.operationId,
callId: call.callId,
result: redactSemanticValue(executed.result),
stateRevision: executed.stateRevision,
} as const);
this.#record(policyContext, invocation, call.callId, call.input, result);
return result;
} catch (error) {
const failure = normalizeFailure(error);
const decision = deniedDecision(invocation, failure.code, failure.message);
const result = this.#denial(
call,
descriptor.operationId,
failure.code,
failure.message,
failure.retryable,
);
this.#record(policyContext, decision, call.callId, call.input, result);
return result;
}
}
#policyContext(runId: string): Phase7SemanticPolicyContext {
const context = this.port.context(runId);
const scenario = this.options.scenario ?? DEFAULT_PHASE7_SCENARIO_POLICY;
return {
...createPhase7SemanticPolicyContext(
context,
scenario,
this.options.explicitClaims ?? context.capabilities,
),
runId,
};
}
async #execute(
operationId: Phase7SemanticOperationId,
runId: string,
input: Record<string, unknown>,
): Promise<OperationSuccess> {
const context = this.port.context(runId);
const state = this.port.snapshot();
switch (operationId) {
case "get_task_context":
return readSuccess(state.revision, context);
case "get_task_history": {
const limit = optionalNumber(input.limit, 50);
return readSuccess(state.revision, {
comments: state.comments
.filter((comment) => comment.taskId === context.activeTask.id)
.slice(-limit),
});
}
case "list_documents":
return readSuccess(state.revision, {
documents: state.documents
.filter((document) => document.taskId === context.activeTask.id)
.map((document) => ({
id: document.id,
key: document.key,
title: document.title,
format: document.format,
latestRevisionId: document.latestRevisionId,
revisionCount: document.revisions.length,
})),
});
case "read_document": {
const document = activeDocument(state.documents, context, requiredString(input.key));
const revision = document.revisions.find(
(candidate) => candidate.id === document.latestRevisionId,
);
if (revision === undefined) {
throw new SemanticDispatchFailure("operation_unavailable", "Document revision is missing.");
}
return readSuccess(state.revision, { document, revision });
}
case "list_document_revisions": {
const document = activeDocument(state.documents, context, requiredString(input.key));
const limit = optionalNumber(input.limit, 50);
return readSuccess(state.revision, {
documentId: document.id,
key: document.key,
revisions: document.revisions.slice(-limit),
});
}
case "list_agents":
return readSuccess(state.revision, {
actors: state.actors.map(({ capabilityGrants: _claims, budgetId: _budget, ...actor }) => actor),
});
case "get_agent": {
const actor = state.actors.find((candidate) => candidate.id === requiredString(input.actorId));
if (actor === undefined) throw new SemanticDispatchFailure("operation_unavailable", "Mock actor not found.");
const { capabilityGrants: _claims, budgetId: _budget, ...profile } = actor;
return readSuccess(state.revision, { actor: profile });
}
case "search_tasks": {
const query = optionalString(input.query)?.toLowerCase();
const statuses = optionalStringArray(input.statuses);
const limit = optionalNumber(input.limit, 50);
const tasks = state.tasks.filter((task) =>
(query === undefined || `${task.identifier} ${task.title} ${task.description ?? ""}`.toLowerCase().includes(query)) &&
(statuses.length === 0 || statuses.includes(task.status)),
).slice(0, limit);
return readSuccess(state.revision, { tasks });
}
case "list_approvals":
return readSuccess(state.revision, { approvals: state.approvals });
case "get_approval": {
const approval = approvalById(state.approvals, requiredString(input.approvalId));
return readSuccess(state.revision, { approval });
}
case "get_approval_context": {
const approval = approvalById(state.approvals, requiredString(input.approvalId));
return readSuccess(state.revision, {
approval,
tasks: state.tasks.filter((task) => approval.taskIds.includes(task.id)),
});
}
case "get_workspace_runtime":
return readSuccess(state.revision, {
services: state.workspaceServices.filter(
(service) => service.taskId === context.activeTask.id,
),
});
case "generic_api_request":
return this.#genericApiRead(state.revision, context, input);
default:
return this.#applyMutation(operationId, runId, context, input);
}
}
#genericApiRead(
revision: number,
context: Phase7RunContext,
input: Record<string, unknown>,
): OperationSuccess {
if (input.method === "GET" && input.path === "/mock/state/revision") {
return readSuccess(revision, { revision });
}
if (input.method === "GET" && input.path === "/mock/task") {
return readSuccess(revision, { task: context.activeTask });
}
throw new SemanticDispatchFailure(
"operation_unavailable",
"The test-only generic request is outside the explicit mock allowlist.",
);
}
async #applyMutation(
operationId: Phase7SemanticOperationId,
runId: string,
context: Phase7RunContext,
input: Record<string, unknown>,
): Promise<OperationSuccess> {
const idempotencyKey = requiredString(input.idempotencyKey);
const taskId = context.activeTask.id;
let command: Phase7SemanticCommand;
switch (operationId) {
case "report_progress":
case "answer_status_question":
command = { kind: "report_progress", taskId, body: requiredString(input.body) };
break;
case "write_document":
command = {
kind: "write_document",
taskId,
key: requiredString(input.key),
title: requiredString(input.title),
body: requiredString(input.body),
baseRevisionId: nullableString(input.baseRevisionId),
changeSummary: nullableOptionalString(input.changeSummary),
};
break;
case "request_human_input":
command = {
kind: "request_human_input",
taskId,
interactionKind: requiredString(input.interactionKind) as Extract<Phase7SemanticCommand, { kind: "request_human_input" }>["interactionKind"],
title: requiredString(input.title),
prompt: requiredString(input.prompt),
payload: optionalJson(input.payload),
targetRevisionId: nullableOptionalString(input.targetRevisionId),
continuationPolicy: requiredString(input.continuationPolicy) as Extract<Phase7SemanticCommand, { kind: "request_human_input" }>["continuationPolicy"],
};
break;
case "register_deliverable":
command = {
kind: "register_deliverable",
taskId,
filename: requiredString(input.filename),
contentType: requiredString(input.contentType),
byteSize: requiredNumber(input.byteSize),
sha256: requiredString(input.sha256),
contentRef: requiredString(input.contentRef),
title: requiredString(input.title),
};
break;
case "finish_task":
command = { kind: "finish_task", taskId, summary: requiredString(input.summary) };
break;
case "block_task":
command = { kind: "block_task", taskId, reason: requiredString(input.reason), blockedByTaskIds: optionalStringArray(input.blockedByTaskIds) };
break;
case "request_review":
command = { kind: "request_review", taskId, summary: requiredString(input.summary) };
break;
case "set_dependencies":
command = { kind: "set_dependencies", taskId, blockedByTaskIds: optionalStringArray(input.blockedByTaskIds) };
break;
case "create_task":
command = {
kind: "create_task",
taskId,
title: requiredString(input.title),
description: nullableOptionalString(input.description),
assigneeActorId: nullableOptionalString(input.assigneeActorId),
priority: optionalString(input.priority) as Extract<Phase7SemanticCommand, { kind: "create_task" }>["priority"],
blockedByTaskIds: optionalStringArray(input.blockedByTaskIds),
};
break;
case "request_approval":
command = { kind: "request_approval", taskId, approvalType: requiredString(input.approvalType), payload: optionalJson(input.payload) };
break;
case "decide_approval":
command = { kind: "decide_approval", taskId, approvalId: requiredString(input.approvalId), decision: requiredString(input.decision) as Extract<Phase7SemanticCommand, { kind: "decide_approval" }>["decision"], note: requiredString(input.note) };
break;
case "comment_on_approval":
command = { kind: "comment_on_approval", taskId, approvalId: requiredString(input.approvalId), body: requiredString(input.body) };
break;
case "control_workspace_service":
command = { kind: "control_workspace_service", taskId, serviceId: requiredString(input.serviceId), action: requiredString(input.action) as Extract<Phase7SemanticCommand, { kind: "control_workspace_service" }>["action"], url: nullableOptionalString(input.url) };
break;
case "schedule_wake":
command = { kind: "schedule_wake", taskId, reason: requiredString(input.reason) as Extract<Phase7SemanticCommand, { kind: "schedule_wake" }>["reason"], payload: optionalJson(input.payload), delayTicks: requiredNumber(input.delayTicks) };
break;
default:
throw new SemanticDispatchFailure("operation_unavailable", "No mock operation is bound to this descriptor.");
}
const outcome = await this.port.tryApplyCommand({ runId, idempotencyKey, command });
return commandOutcome(outcome);
}
#record(
context: Phase7SemanticPolicyContext,
decision: Phase7SemanticAuthorizationDecision,
callId: string | null,
input: unknown,
result: unknown,
): void {
this.#recordCounter += 1;
this.#records.push(deepFreeze({
schema: "paperclip.semantic-authorization-record.v1",
id: `semantic-auth-${String(this.#recordCounter).padStart(6, "0")}`,
runId: context.runId,
scenarioId: context.scenario.id,
actorId: context.actor.id,
taskId: context.task.id,
callId,
...decision,
input: input === null ? null : redactSemanticValue(input),
result: result === null ? null : redactSemanticValue(result),
}));
}
#unknownToolDenial(call: Phase7SemanticToolCall): Phase7SemanticToolResult {
return deepFreeze({
ok: false,
operationId: call.operationId,
callId: call.callId,
denial: {
schema: "paperclip.semantic-denial.v1",
code: "tool_not_exposed",
message: "The requested semantic tool is not in the stable catalog.",
retryable: false,
},
stateRevision: this.port.snapshot().revision,
});
}
#denial(
call: Phase7SemanticToolCall,
operationId: Phase7SemanticOperationId,
code: Exclude<Phase7SemanticAuthorizationDecision["code"], "allowed">,
message: string,
retryable = false,
): Phase7SemanticToolResult {
return deepFreeze({
ok: false,
operationId,
callId: call.callId,
denial: {
schema: "paperclip.semantic-denial.v1",
code,
message: redactSemanticValue(message) as string,
retryable,
},
stateRevision: this.port.snapshot().revision,
});
}
}
function commandOutcome(outcome: Phase7CommandOutcome): OperationSuccess {
if (!outcome.ok) {
throw new SemanticDispatchFailure(
"control_plane_denied",
outcome.error.message,
outcome.error.retryable,
);
}
return { result: outcome.result as unknown as Phase7JsonValue, stateRevision: outcome.result.stateRevision };
}
function toDefinition(descriptor: Phase7SemanticToolDescriptor): Phase7SemanticToolDefinition {
return {
name: descriptor.operationId,
description: descriptor.description,
inputSchema: descriptor.inputSchema,
outputSchema: descriptor.outputSchema,
annotations: {
semanticContract: descriptor.schema,
operationId: descriptor.operationId,
version: descriptor.version,
exposure: descriptor.exposure,
requiredClaims: descriptor.requiredClaims,
},
};
}
function deniedDecision(
allowed: Phase7SemanticAuthorizationDecision,
code: Phase7SemanticDenialCode,
reason: string,
): Phase7SemanticAuthorizationDecision {
return { ...allowed, allowed: false, code, reason };
}
function denialCode(
decision: Phase7SemanticAuthorizationDecision,
): Phase7SemanticDenialCode {
if (decision.code === "allowed") {
throw new Error("allowed authorization decision cannot create a denial");
}
return decision.code;
}
function normalizeFailure(error: unknown): SemanticDispatchFailure {
return error instanceof SemanticDispatchFailure
? error
: new SemanticDispatchFailure("operation_unavailable", "The semantic operation failed safely.");
}
function formatValidationError(validator: ValidateFunction | undefined): string {
const issue = validator?.errors?.[0];
if (issue === undefined) return "Semantic tool input does not match its stable schema.";
return `Semantic tool input ${issue.instancePath || "/"} ${issue.message ?? "is invalid"}.`;
}
function activeDocument(
documents: ReturnType<Phase7SemanticControlPlane["snapshot"]>["documents"],
context: Phase7RunContext,
key: string,
) {
const document = documents.find(
(candidate) => candidate.taskId === context.activeTask.id && candidate.key === key,
);
if (document === undefined) throw new SemanticDispatchFailure("operation_unavailable", "Document not found.");
return document;
}
function approvalById(
approvals: ReturnType<Phase7SemanticControlPlane["snapshot"]>["approvals"],
approvalId: string,
) {
const approval = approvals.find((candidate) => candidate.id === approvalId);
if (approval === undefined) throw new SemanticDispatchFailure("operation_unavailable", "Approval not found.");
return approval;
}
function readSuccess(revision: number, value: unknown): OperationSuccess {
return { result: redactSemanticValue(value), stateRevision: revision };
}
function requiredString(value: unknown): string {
if (typeof value !== "string") throw new SemanticDispatchFailure("input_invalid", "Expected string input.");
return value;
}
function optionalString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function nullableString(value: unknown): string | null {
return value === null ? null : requiredString(value);
}
function nullableOptionalString(value: unknown): string | null | undefined {
return value === undefined ? undefined : nullableString(value);
}
function requiredNumber(value: unknown): number {
if (typeof value !== "number") throw new SemanticDispatchFailure("input_invalid", "Expected number input.");
return value;
}
function optionalNumber(value: unknown, fallback: number): number {
return typeof value === "number" ? value : fallback;
}
function optionalStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
function optionalJson(value: unknown): Phase7JsonValue {
return value === undefined ? {} : redactSemanticValue(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,6 @@
export * from "./types.js";
export * from "./catalog.js";
export * from "./policy.js";
export * from "./redaction.js";
export * from "./provider-neutral.js";
export * from "./dispatcher.js";
@@ -0,0 +1,153 @@
import type { Phase7RunContext } from "../mock-core/phase7-control-plane-types.js";
import { PHASE7_SEMANTIC_TOOL_CATALOG } from "./catalog.js";
import type {
Phase7SemanticAuthorizationDecision,
Phase7SemanticOperationId,
Phase7SemanticPolicyContext,
Phase7SemanticScenarioPolicy,
Phase7SemanticToolDescriptor,
} from "./types.js";
const READ_OPERATIONS = new Set<Phase7SemanticOperationId>([
"get_task_context",
"get_task_history",
"list_documents",
"read_document",
"list_document_revisions",
"list_agents",
"get_agent",
"search_tasks",
"list_approvals",
"get_approval",
"get_approval_context",
"get_workspace_runtime",
]);
export const DEFAULT_PHASE7_SCENARIO_POLICY: Phase7SemanticScenarioPolicy = {
id: "phase7-default",
};
export function createPhase7SemanticPolicyContext(
context: Phase7RunContext,
scenario: Phase7SemanticScenarioPolicy = DEFAULT_PHASE7_SCENARIO_POLICY,
explicitClaims: readonly string[] = context.capabilities,
): Phase7SemanticPolicyContext {
return deepFreeze({
runId: context.activeTask.executionRunId ?? context.activeTask.checkoutRunId ?? "",
actor: context.actor,
task: context.activeTask,
runClaims: sortedUnique(context.capabilities),
explicitClaims: sortedUnique(explicitClaims),
scenario,
});
}
export function decidePhase7SemanticAuthorization(
descriptor: Phase7SemanticToolDescriptor,
context: Phase7SemanticPolicyContext,
phase: "exposure" | "invocation",
input?: unknown,
): Phase7SemanticAuthorizationDecision {
const effectiveClaims = computeEffectiveClaims(context);
const base = { phase, operationId: descriptor.operationId, effectiveClaims } as const;
const deny = (
code: Exclude<Phase7SemanticAuthorizationDecision["code"], "allowed">,
reason: string,
): Phase7SemanticAuthorizationDecision => ({ ...base, allowed: false, code, reason });
if (context.actor.status !== "active") {
return deny("actor_inactive", "The mock actor is not active.");
}
if (!descriptor.allowedModes.includes(context.task.workMode)) {
return deny("task_mode_denied", "The semantic operation is unavailable in this task mode.");
}
if (
context.scenario.denyOperations?.includes(descriptor.operationId) === true ||
(descriptor.exposure === "optional" &&
context.scenario.allowOperations !== undefined &&
!context.scenario.allowOperations.includes(descriptor.operationId))
) {
return deny("scenario_denied", "The active scenario does not permit this semantic operation.");
}
if (descriptor.disabledByDefault === true) {
if (
descriptor.operationId !== "generic_api_request" ||
context.scenario.enableGenericApiRequest !== true
) {
return deny("generic_api_disabled", "The generic API escape hatch is disabled.");
}
}
if (
descriptor.allowedRoles !== undefined &&
!descriptor.allowedRoles.includes(context.actor.role.trim().toLowerCase())
) {
return deny("actor_role_denied", "The actor role cannot invoke this semantic operation.");
}
if (descriptor.requiredClaims.some((claim) => !effectiveClaims.includes(claim))) {
return deny("required_claim_missing", "The run lacks an explicit claim required by this operation.");
}
if (
phase === "invocation" &&
(context.task.assigneeActorId !== context.actor.id ||
context.task.executionRunId !== context.runId ||
context.task.checkoutRunId !== context.runId)
) {
return deny("task_ownership_denied", "The run no longer owns the active task.");
}
if (
phase === "invocation" &&
!READ_OPERATIONS.has(descriptor.operationId) &&
["done", "cancelled"].includes(context.task.status)
) {
return deny("task_state_denied", "The active task is already terminal.");
}
if (descriptor.operationId === "request_human_input" && input !== undefined) {
const interactionKind = readStringProperty(input, "interactionKind");
if (
interactionKind !== undefined &&
context.scenario.allowedInteractionKinds !== undefined &&
!context.scenario.allowedInteractionKinds.includes(
interactionKind as Parameters<NonNullable<typeof context.scenario.allowedInteractionKinds>["includes"]>[0],
)
) {
return deny("interaction_kind_denied", "The scenario does not allow this interaction kind.");
}
}
return { ...base, allowed: true, code: "allowed", reason: "Explicit semantic policy allowed the operation." };
}
export function exposedPhase7SemanticDescriptors(
context: Phase7SemanticPolicyContext,
): readonly Phase7SemanticToolDescriptor[] {
return PHASE7_SEMANTIC_TOOL_CATALOG.filter(
(descriptor) => decidePhase7SemanticAuthorization(descriptor, context, "exposure").allowed,
);
}
function computeEffectiveClaims(context: Phase7SemanticPolicyContext): string[] {
const granted = new Set([
...context.actor.capabilityGrants,
...(context.scenario.claims ?? []),
]);
const explicitlyDelegated = new Set(context.explicitClaims);
return sortedUnique(
context.runClaims.filter((claim) => granted.has(claim) && explicitlyDelegated.has(claim)),
);
}
function readStringProperty(input: unknown, key: string): string | undefined {
if (typeof input !== "object" || input === null || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>)[key];
return typeof value === "string" ? value : undefined;
}
function sortedUnique(values: readonly string[]): string[] {
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}
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,69 @@
import { PHASE7_SEMANTIC_TOOL_CATALOG } from "./catalog.js";
import type {
Phase7SemanticToolDefinition,
Phase7SemanticToolDescriptor,
} from "./types.js";
export type Phase7SemanticBindingKind = "fake" | "live_codex";
export interface Phase7ProviderNeutralSemanticBinding {
readonly schema: "paperclip.semantic-binding.v1";
readonly bindingKind: Phase7SemanticBindingKind;
readonly contracts: readonly Phase7SemanticToolDefinition[];
}
/**
* Converts transport-neutral descriptors into model-provider contracts. The
* binding label is deliberately outside the contract array, so fake and live
* Codex bindings cannot drift in names, schemas, claims, or redaction metadata.
*/
export function createPhase7ProviderNeutralBinding(
bindingKind: Phase7SemanticBindingKind,
descriptors: readonly Phase7SemanticToolDescriptor[] = PHASE7_SEMANTIC_TOOL_CATALOG,
): Phase7ProviderNeutralSemanticBinding {
return deepFreeze({
schema: "paperclip.semantic-binding.v1",
bindingKind,
contracts: descriptors.map(toDefinition),
});
}
export function phase7GeneratedSemanticContracts(): readonly Phase7SemanticToolDefinition[] {
return createPhase7ProviderNeutralBinding("fake").contracts;
}
export function serializePhase7GeneratedSemanticContracts(): string {
return `${canonicalJson(phase7GeneratedSemanticContracts())}\n`;
}
function toDefinition(descriptor: Phase7SemanticToolDescriptor): Phase7SemanticToolDefinition {
return {
name: descriptor.operationId,
description: descriptor.description,
inputSchema: descriptor.inputSchema,
outputSchema: descriptor.outputSchema,
annotations: {
semanticContract: descriptor.schema,
operationId: descriptor.operationId,
version: descriptor.version,
exposure: descriptor.exposure,
requiredClaims: descriptor.requiredClaims,
},
};
}
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;
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}
@@ -0,0 +1,46 @@
import type { Phase7JsonValue } from "../mock-core/phase7-control-plane-types.js";
const SENSITIVE_KEY = /(?:authorization|cookie|credential|password|passwd|private.?key|secret|token|api.?key|connection.?string)/i;
const SECRET_VALUE = /(?:\bBearer\s+[A-Za-z0-9._~+\/-]+=*|\b(?:sk|pk|pcgw|ghp|github_pat)_[A-Za-z0-9_-]{8,}|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})/i;
const SECRET_QUERY_DETECT = /[?&](?:code|key|secret|state|token|api[_-]?key|access[_-]?token)=[^&#\s]+/i;
const SECRET_QUERY = /([?&](?:code|key|secret|state|token|api[_-]?key|access[_-]?token)=)[^&#\s]+/gi;
export const PHASE7_REDACTED = "[REDACTED]";
export function containsProtectedSemanticData(value: unknown, key = ""): boolean {
if (SENSITIVE_KEY.test(key)) return true;
if (typeof value === "string") {
return SECRET_VALUE.test(value) || SECRET_QUERY_DETECT.test(value);
}
if (Array.isArray(value)) {
return value.some((child) => containsProtectedSemanticData(child));
}
if (typeof value === "object" && value !== null) {
return Object.entries(value).some(([childKey, child]) =>
containsProtectedSemanticData(child, childKey),
);
}
return false;
}
export function redactSemanticValue(value: unknown, key = ""): Phase7JsonValue {
if (SENSITIVE_KEY.test(key)) return PHASE7_REDACTED;
if (typeof value === "string") {
return value
.replace(SECRET_VALUE, PHASE7_REDACTED)
.replace(SECRET_QUERY, `$1${PHASE7_REDACTED}`);
}
if (Array.isArray(value)) return value.map((child) => redactSemanticValue(child));
if (typeof value === "object" && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([childKey, child]) => [
childKey,
redactSemanticValue(child, childKey),
]),
);
}
if (typeof value === "number" || typeof value === "boolean" || value === null) {
return value;
}
return String(value);
}
@@ -0,0 +1,287 @@
import { describe, expect, it } from "vitest";
import type { Phase7FixtureSeed } from "../mock-core/phase7-control-plane-types.js";
import { Phase7MockControlPlaneAdapter } from "../mock-core/phase7-mock-control-plane-adapter.js";
import { PHASE7_SEMANTIC_TOOL_CATALOG } from "./catalog.js";
import { Phase7SemanticDispatcher } from "./dispatcher.js";
import { createPhase7ProviderNeutralBinding } from "./provider-neutral.js";
const OPEN = {
identity: {
runId: "run-semantic-1",
sessionId: "session-semantic-1",
companyId: "company-1",
issueId: "task-1",
agentId: "actor-1",
},
backendKind: "mock" as const,
sourceInstanceId: "semantic-test",
};
async function running(
claims: string[] = [],
seed: Phase7FixtureSeed = {},
) {
const adapter = new Phase7MockControlPlaneAdapter({
...seed,
actors: seed.actors ?? [
{
id: "actor-1",
companyId: "company-1",
name: "Semantic Engineer",
role: "engineer",
status: "active",
budgetId: "budget-actor-1",
capabilityGrants: claims,
},
],
});
await adapter.start();
await adapter.openFixtureRun({ ...OPEN, capabilities: claims });
return adapter;
}
describe("Phase 7 semantic catalog and authorization", () => {
it("publishes a stable narrow catalog without credentials or control-plane-owned tools", () => {
const names = PHASE7_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId);
expect(new Set(names).size).toBe(names.length);
expect(names).toHaveLength(28);
expect(names).toContain("get_task_context");
expect(names).toContain("finish_task");
expect(names).not.toContain("checkout_task");
expect(names).not.toContain("record_budget_usage");
expect(names).not.toContain("resolve_human_input");
expect(PHASE7_SEMANTIC_TOOL_CATALOG.find((tool) => tool.operationId === "generic_api_request"))
.toMatchObject({ exposure: "optional", disabledByDefault: true });
const schemaKeys = PHASE7_SEMANTIC_TOOL_CATALOG.flatMap((tool) =>
Object.keys(tool.inputSchema.properties ?? {}),
);
expect(schemaKeys.join(" ")).not.toMatch(
/authorization|password|credential|api.?key|access.?token|secret/i,
);
});
it("exposes always tools while optional tools require actor, scenario, and explicit run claims", async () => {
const adapter = await running();
const dispatcher = new Phase7SemanticDispatcher(adapter);
const names = dispatcher.listTools(OPEN.identity.runId).map((tool) => tool.name);
expect(names).toContain("get_task_context");
expect(names).toContain("report_progress");
expect(names).not.toContain("set_dependencies");
expect(names).not.toContain("generic_api_request");
const before = adapter.snapshot().revision;
const denied = await dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "denied-dependencies",
operationId: "set_dependencies",
input: { idempotencyKey: "deps-1", blockedByTaskIds: [] },
});
expect(denied).toMatchObject({
ok: false,
denial: { code: "required_claim_missing", retryable: false },
});
expect(adapter.snapshot().revision).toBe(before);
});
it("executes a granted optional operation through the mock port", async () => {
const claim = "dependencies:write";
const adapter = await running([claim], {
tasks: [
{
id: "task-1",
companyId: "company-1",
identifier: "MCK-1",
title: "Active task",
description: null,
status: "todo",
priority: "medium",
workMode: "standard",
parentId: null,
assigneeActorId: "actor-1",
checkoutRunId: null,
executionRunId: null,
startedAt: null,
completedAt: null,
},
{
id: "task-2",
companyId: "company-1",
identifier: "MCK-2",
title: "Blocking task",
description: null,
status: "todo",
priority: "high",
workMode: "standard",
parentId: null,
assigneeActorId: null,
checkoutRunId: null,
executionRunId: null,
startedAt: null,
completedAt: null,
},
],
});
const dispatcher = new Phase7SemanticDispatcher(adapter, {
scenario: { id: "dependency-manager", claims: [claim] },
explicitClaims: [claim],
});
expect(dispatcher.listTools(OPEN.identity.runId).map((tool) => tool.name))
.toContain("set_dependencies");
const result = await dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "set-dependencies",
operationId: "set_dependencies",
input: { idempotencyKey: "deps-1", blockedByTaskIds: ["task-2"] },
});
expect(result).toMatchObject({ ok: true, result: { disposition: "applied" } });
expect(adapter.snapshot().blockers).toEqual([
expect.objectContaining({ taskId: "task-1", blockedByTaskId: "task-2" }),
]);
});
it("rechecks policy immediately before every operation", async () => {
const claim = "dependencies:write";
const adapter = await running([claim]);
let exposeClaim = true;
const port = {
snapshot: () => adapter.snapshot(),
tryApplyCommand: adapter.tryApplyCommand.bind(adapter),
context: (runId: string) => {
const context = structuredClone(adapter.context(runId));
context.capabilities = exposeClaim ? [claim] : [];
return context;
},
};
const dispatcher = new Phase7SemanticDispatcher(port, {
scenario: { id: "revocation", claims: [claim] },
explicitClaims: [claim],
});
expect(dispatcher.listTools(OPEN.identity.runId).map((tool) => tool.name))
.toContain("set_dependencies");
exposeClaim = false;
const before = adapter.snapshot().revision;
const result = await dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "revoked-call",
operationId: "set_dependencies",
input: { idempotencyKey: "revoked-1", blockedByTaskIds: [] },
});
expect(result).toMatchObject({ ok: false, denial: { code: "required_claim_missing" } });
expect(adapter.snapshot().revision).toBe(before);
});
it("returns typed denials for absent, mode-denied, and generic escape-hatch tools", async () => {
const adapter = await running([], {
tasks: [{
id: "task-1",
companyId: "company-1",
identifier: "MCK-1",
title: "Ask-only task",
description: null,
status: "todo",
priority: "medium",
workMode: "ask",
parentId: null,
assigneeActorId: "actor-1",
checkoutRunId: null,
executionRunId: null,
startedAt: null,
completedAt: null,
}],
});
const dispatcher = new Phase7SemanticDispatcher(adapter);
const names = dispatcher.listTools(OPEN.identity.runId).map((tool) => tool.name);
expect(names).not.toContain("write_document");
expect(names).not.toContain("finish_task");
await expect(dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "unknown",
operationId: "paperclipApiRequest",
input: {},
})).resolves.toMatchObject({ ok: false, denial: { code: "tool_not_exposed" } });
await expect(dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "mode-denied",
operationId: "write_document",
input: {},
})).resolves.toMatchObject({ ok: false, denial: { code: "task_mode_denied" } });
});
it("rejects protected inputs before mutation and redacts authorization records", async () => {
const claim = "governance:approvals:request";
const adapter = await running([claim]);
const dispatcher = new Phase7SemanticDispatcher(adapter, {
scenario: { id: "approval", claims: [claim] },
explicitClaims: [claim],
});
const secret = "Bearer secret-value-123456";
const before = adapter.snapshot().revision;
const result = await dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "protected-approval",
operationId: "request_approval",
input: {
idempotencyKey: "approval-secret",
approvalType: "request_board_approval",
payload: { authorization: secret },
},
});
expect(result).toMatchObject({ ok: false, denial: { code: "protected_data_denied" } });
expect(adapter.snapshot().revision).toBe(before);
const serialized = JSON.stringify(dispatcher.authorizationRecords());
expect(serialized).not.toContain(secret);
expect(serialized).toContain("[REDACTED]");
});
it("redacts protected mock state from read results", async () => {
const secret = "Bearer mock-secret-123456";
const adapter = await running([], {
comments: [{
id: "comment-secret",
taskId: "task-1",
authorActorId: "actor-1",
body: `diagnostic ${secret}`,
createdAt: "2026-08-09T00:00:00.000Z",
}],
});
const dispatcher = new Phase7SemanticDispatcher(adapter);
const result = await dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "history",
operationId: "get_task_history",
input: {},
});
expect(JSON.stringify(result)).not.toContain(secret);
expect(JSON.stringify(result)).toContain("[REDACTED]");
});
it("applies always-tool commands and records authorization for every invocation", async () => {
const adapter = await running();
const dispatcher = new Phase7SemanticDispatcher(adapter);
const progress = await dispatcher.dispatch({
runId: OPEN.identity.runId,
callId: "progress",
operationId: "report_progress",
input: { idempotencyKey: "progress-1", body: "Semantic catalog is active." },
});
expect(progress).toMatchObject({ ok: true });
expect(adapter.snapshot().comments).toEqual([
expect.objectContaining({ body: "Semantic catalog is active." }),
]);
const records = dispatcher.authorizationRecords().filter((record) => record.phase === "invocation");
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({ allowed: true, operationId: "report_progress", callId: "progress" });
});
it("keeps generated contracts identical across fake and live Codex bindings", () => {
const fake = createPhase7ProviderNeutralBinding("fake");
const live = createPhase7ProviderNeutralBinding("live_codex");
expect(fake.bindingKind).toBe("fake");
expect(live.bindingKind).toBe("live_codex");
expect(live.contracts).toEqual(fake.contracts);
expect(live.contracts.map((tool) => tool.name)).toEqual(
PHASE7_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId),
);
});
});
@@ -0,0 +1,182 @@
import type {
Phase7InteractionKind,
Phase7JsonValue,
Phase7RunContext,
} from "../mock-core/phase7-control-plane-types.js";
export type Phase7SemanticToolExposure = "always" | "optional";
export type Phase7SemanticOperationId =
| "get_task_context"
| "get_task_history"
| "list_documents"
| "read_document"
| "list_document_revisions"
| "report_progress"
| "answer_status_question"
| "write_document"
| "request_human_input"
| "register_deliverable"
| "finish_task"
| "block_task"
| "request_review"
| "list_agents"
| "get_agent"
| "search_tasks"
| "list_approvals"
| "get_approval"
| "get_approval_context"
| "get_workspace_runtime"
| "control_workspace_service"
| "set_dependencies"
| "create_task"
| "request_approval"
| "decide_approval"
| "comment_on_approval"
| "schedule_wake"
| "generic_api_request";
export interface Phase7JsonSchema {
readonly $schema?: string;
readonly type?: string | readonly string[];
readonly title?: string;
readonly description?: string;
readonly properties?: Readonly<Record<string, Phase7JsonSchema>>;
readonly required?: readonly string[];
readonly additionalProperties?: boolean | Phase7JsonSchema;
readonly items?: Phase7JsonSchema;
readonly enum?: readonly Phase7JsonValue[];
readonly const?: Phase7JsonValue;
readonly oneOf?: readonly Phase7JsonSchema[];
readonly anyOf?: readonly Phase7JsonSchema[];
readonly minimum?: number;
readonly maximum?: number;
readonly minLength?: number;
readonly maxLength?: number;
readonly minItems?: number;
readonly maxItems?: number;
readonly uniqueItems?: boolean;
readonly pattern?: string;
readonly format?: string;
readonly default?: Phase7JsonValue;
}
export interface Phase7SemanticToolDescriptor {
readonly schema: "paperclip.semantic-tool.v1";
readonly operationId: Phase7SemanticOperationId;
readonly version: 1;
readonly title: string;
readonly description: string;
readonly exposure: Phase7SemanticToolExposure;
readonly requiredClaims: readonly string[];
readonly allowedModes: readonly Phase7RunContext["activeTask"]["workMode"][];
readonly allowedRoles?: readonly string[];
readonly disabledByDefault?: boolean;
readonly inputSchema: Phase7JsonSchema;
readonly outputSchema: Phase7JsonSchema;
}
export interface Phase7SemanticScenarioPolicy {
readonly id: string;
/** Claims that this scenario is permitted to grant. Run claims still narrow this set. */
readonly claims?: readonly string[];
readonly allowOperations?: readonly Phase7SemanticOperationId[];
readonly denyOperations?: readonly Phase7SemanticOperationId[];
readonly allowedInteractionKinds?: readonly Phase7InteractionKind[];
readonly enableGenericApiRequest?: boolean;
}
export interface Phase7SemanticPolicyContext {
readonly runId: string;
readonly actor: Phase7RunContext["actor"];
readonly task: Phase7RunContext["activeTask"];
readonly runClaims: readonly string[];
readonly explicitClaims: readonly string[];
readonly scenario: Phase7SemanticScenarioPolicy;
}
export type Phase7AuthorizationPhase = "exposure" | "invocation";
export type Phase7SemanticDenialCode =
| "tool_not_exposed"
| "actor_inactive"
| "task_mode_denied"
| "task_state_denied"
| "task_ownership_denied"
| "required_claim_missing"
| "actor_role_denied"
| "scenario_denied"
| "interaction_kind_denied"
| "generic_api_disabled"
| "input_invalid"
| "protected_data_denied"
| "operation_unavailable"
| "control_plane_denied";
export interface Phase7SemanticAuthorizationDecision {
readonly allowed: boolean;
readonly phase: Phase7AuthorizationPhase;
readonly operationId: Phase7SemanticOperationId;
readonly code: "allowed" | Phase7SemanticDenialCode;
readonly reason: string;
readonly effectiveClaims: readonly string[];
}
export interface Phase7SemanticAuthorizationRecord
extends Phase7SemanticAuthorizationDecision {
readonly schema: "paperclip.semantic-authorization-record.v1";
readonly id: string;
readonly runId: string;
readonly scenarioId: string;
readonly actorId: string;
readonly taskId: string;
readonly callId: string | null;
readonly input: Phase7JsonValue | null;
readonly result: Phase7JsonValue | null;
}
export interface Phase7SemanticToolDefinition {
readonly name: Phase7SemanticOperationId;
readonly description: string;
readonly inputSchema: Phase7JsonSchema;
readonly outputSchema: Phase7JsonSchema;
readonly annotations: {
readonly semanticContract: "paperclip.semantic-tool.v1";
readonly operationId: Phase7SemanticOperationId;
readonly version: 1;
readonly exposure: Phase7SemanticToolExposure;
readonly requiredClaims: readonly string[];
};
}
export interface Phase7SemanticToolCall {
readonly runId: string;
readonly callId: string;
readonly operationId: Phase7SemanticOperationId | string;
readonly input: unknown;
}
export interface Phase7SemanticToolSuccess {
readonly ok: true;
readonly operationId: Phase7SemanticOperationId;
readonly callId: string;
readonly result: Phase7JsonValue;
readonly stateRevision: number;
}
export interface Phase7SemanticToolDenial {
readonly ok: false;
readonly operationId: string;
readonly callId: string;
readonly denial: {
readonly schema: "paperclip.semantic-denial.v1";
readonly code: Phase7SemanticDenialCode;
readonly message: string;
readonly retryable: boolean;
};
readonly stateRevision: number;
}
export type Phase7SemanticToolResult =
| Phase7SemanticToolSuccess
| Phase7SemanticToolDenial;
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { resolve } from "node:path";
import { validateRows } from "../scripts/generate-phase-07-contract.mjs";
const phaseDirectory = resolve(import.meta.dirname, "../generated/phase-07");
async function readRows(file) {
return JSON.parse(await readFile(resolve(phaseDirectory, file), "utf8")).rows;
}
test("generated Phase 7 inventory has full source coverage", async () => {
const [capabilities, tools, evals] = await Promise.all([
readRows("capabilities.yaml"),
readRows("mcp-tool-map.yaml"),
readRows("eval-traceability.yaml"),
]);
assert.equal(capabilities.length, 152);
assert.equal(tools.length, 41);
assert.equal(evals.length, 106);
assert.equal(new Set(evals.map((row) => row.group)).size, 16);
for (const row of [...capabilities, ...tools, ...evals]) {
assert.match(row.sourceAnchor, /\S/);
assert.match(row.semanticOperation, /\S/);
assert.match(row.expectedMockState, /\S/);
}
});
test("contract validation rejects missing, duplicate, and unclassified entries", () => {
const row = {
id: "example:1",
sourceAnchor: "source.md#L1:example",
primaryDisposition: "control_plane_owned",
};
assert.throws(() => validateRows([{ ...row, sourceAnchor: "" }], "fixture"), /source anchor/);
assert.throws(() => validateRows([row, { ...row, id: "example:2" }], "fixture"), /duplicate source anchor/);
assert.throws(() => validateRows([{ ...row, primaryDisposition: "unclassified" }], "fixture"), /valid primary disposition/);
});