mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
fix(runner): preserve hire runtime safety and first-activity timing (#13852)
## Thinking Path > - Paperclip is the open source control plane for companies of AI agents. > - Native runner agents need governed tools, durable runtime state, and useful execution evidence. > - A first activity trace waited 53.467 seconds even though tool activity took 6.274 seconds; provider input arrived before the server API call executed. > - Native agents also need a safe way to hire teammates without asking the model to rebuild runtime configuration. > - This pull request separates the observed ACP input-stream window from the actual server `tool.execute` span and adds a server-owned native hire contract. > - The benefit is clearer latency evidence and safer native teammates with existing approval, auth, and company boundaries preserved. ## Linked Issues or Issue Description Related Daytona provenance work is in [#13814](https://github.com/paperclipai/paperclip/pull/13814). No duplicate public PR was found for this combined timing and native-hire change. **What existing behavior does this improve?** Native runner agents can use governed tools and request hires. The server did not expose a safe native hire operation that reused the caller's validated runtime settings. First-activity traces also mixed provider input timing with server tool execution timing. **Current behavior** A native hire must construct a separate runner configuration. Full configuration copying could expose paths, instructions, secrets, or sessions. Timing evidence could make a provider or MCP identity join appear proven when the trace did not contain that join. **Proposed behavior** The native `hire_agent` operation accepts identity and persona inputs. The server sends `adapterType: "paperclip_runner"` with `inheritRuntimeFrom: "caller"`, then copies only validated provider, model, permission, lifecycle, and bounded execution settings. It inherits and validates the default environment, derives the managed AI binding through existing normalization, preserves approval and permissions, and creates fresh child instructions. Caller secrets, paths, prompts, and sessions are excluded. Provider events now include the optional boolean `inputUpdated`, with Rust forwarding support. Timing evidence separately records the ACP input-stream window and the actual server `tool.execute` activity. It does not claim a provider or MCP join without matching evidence. **Reason and benefit** Native agents can hire teammates that start with the caller's approved execution policy. Operators retain company boundaries, auth rules, approval gates, and requalification. Reviewers can distinguish provider streaming time from server API execution time when diagnosing first-activity delays. **Breaking changes** None for existing hires or tool calls. `inheritRuntimeFrom` is optional and only applies to same-company native agent callers. Conflicting explicit runtime settings are rejected. The provider event field is optional for existing producers. ## What Changed - Added the native `hire_agent` protocol action, catalog entry, API contract, and runner authority checks. - Added `inheritRuntimeFrom: "caller"` validation and a closed native runtime inheritance allowlist. - Preserved managed AI binding normalization, default-environment validation, approval snapshots, permissions, requalification, and fresh child instructions. - Added provider `inputUpdated` schema support and Rust forwarding. - Added first-activity and server tool timing evidence with conservative identity-join handling. - Added route, authority, provider-event, sidecar, API, catalog, and Rust-focused tests. - Kept private Honeycomb links, raw traces, and local result paths out of this description. ## Verification Focused checks passed: - 458 timing/session checks. - 61 native hire inheritance checks. - 20 hire authority checks. - 1,741 API checks. - 106 catalog checks. - 54 provider sidecar checks. - 12 Rust provider checks. Live R2 and R3 each passed 45 checks across 6 runs (361,135 ms for R2). R1 stopped at missing Docker image setup. The final trace is available at https://ui.honeycomb.io/paperclip/environments/test/datasets/paperclip/result/BiMypLNvmiB?tab=traces. Latest-head CI passed all required build, typecheck, Rust, static, Vitest, serialized-server, workspace, chat, and E2E jobs. The focused local checks listed above passed; the broad local suite was not run before the live evaluation, while CI provides the full repository verification. ## Risks - Timing fields describe separate observed windows. They do not prove a provider or MCP owner without a valid trace join. - The inheritance allowlist must stay synchronized with native runner configuration fields. - Approval snapshots include resolved safe inherited settings and should be reviewed when native configuration fields change. - The focused local suite is narrower than the full repository suite; latest-head CI covers the broader repository checks. > Roadmap review: `ROADMAP.md` places this work within Paperclip's bring-your-own-agent direction. It extends existing native runner hiring and observability behavior. ## Model Used OpenAI GPT-6 (exact serving model ID is not exposed), with extended reasoning and repository tool use; GPT-5.6 Luna assisted with focused implementation and verification work. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
64faf0ae90
commit
7944ed3d97
47 files changed
+1204
-202
No files matched your search
+25
-1
@@ -168,7 +168,9 @@ task.run
|
||||
│ │ └── runner.turn.submit
|
||||
│ └── agent.turn
|
||||
│ ├── provider.turn.queue
|
||||
│ └── provider.time_to_first_agent_event
|
||||
│ ├── provider.time_to_first_agent_event
|
||||
│ ├── tool.request.input_stream
|
||||
│ └── tool.execute
|
||||
└── task.settle
|
||||
├── native.result.finalize
|
||||
└── session.checkpoint.persist
|
||||
@@ -189,6 +191,28 @@ session records `runner.session.resume`. `agent.turn` begins at
|
||||
at that terminal event and remains open through finalization and checkpoint
|
||||
persistence, so settlement work does not appear to outlive its parent.
|
||||
|
||||
`provider.time_to_first_agent_event` ends at the first meaningful provider
|
||||
activity: a nonempty assistant/reasoning delta, a tool announcement with an
|
||||
execution identity, or an existing assistant/reasoning/tool item start or
|
||||
completion. Usage updates and empty deltas do not count. This is a first
|
||||
activity metric, not time to a finished answer.
|
||||
|
||||
`tool.request.input_stream` measures the observed request-input window: from
|
||||
an ACP tool announcement to its last input update before the provider reports
|
||||
the tool complete. It can include provider buffering and transport. It is not
|
||||
a claim about the exact end of model generation or the start of API execution.
|
||||
The span is omitted when no subsequent input update is observed.
|
||||
|
||||
For native control-plane tools, `tool.execute` measures authorization and
|
||||
execution inside the server authority, including time spent in HTTP requests.
|
||||
Provider tool IDs and MCP request IDs are separate namespaces; the trace does
|
||||
not guess a pairing by tool name or arrival order. Both spans belong to the
|
||||
current run's `agent.turn`, including when a warm provider session is reused.
|
||||
Their `operation` attribute is a known semantic tool name or `other`. Only a
|
||||
boolean input-update marker crosses the provider-event boundary; no arguments,
|
||||
argument hashes, or raw call IDs are exported by these spans. Concurrent tools
|
||||
have separate durations; overlapping spans must not be summed as wall time.
|
||||
|
||||
The active native scope is also published through the existing asynchronous
|
||||
runtime-parent seam. Provider execution, plugin, websocket/duplex, daemon, and
|
||||
sandbox spans therefore inherit the correct branch even when their callbacks
|
||||
|
||||
@@ -177,25 +177,25 @@ The skill/reference inventory and eval cases are the only normative behavior sou
|
||||
| skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:828 | optional_agent_tool | skills/paperclip/references/api-reference.md:828 |
|
||||
| skill:skills/paperclip/references/api-reference.md:governance-and-approvals:857 | optional_agent_tool | skills/paperclip/references/api-reference.md:857 |
|
||||
| skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:861 | optional_agent_tool | skills/paperclip/references/api-reference.md:861 |
|
||||
| skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:893 | optional_agent_tool | skills/paperclip/references/api-reference.md:893 |
|
||||
| skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:902 | always_agent_tool | skills/paperclip/references/api-reference.md:902 |
|
||||
| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:999 | always_agent_tool | skills/paperclip/references/api-reference.md:999 |
|
||||
| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1057 | always_agent_tool | skills/paperclip/references/api-reference.md:1057 |
|
||||
| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1172 | optional_agent_tool | skills/paperclip/references/api-reference.md:1172 |
|
||||
| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1282 | optional_agent_tool | skills/paperclip/references/api-reference.md:1282 |
|
||||
| skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1288 | always_agent_tool | skills/paperclip/references/api-reference.md:1288 |
|
||||
| skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1306 | always_agent_tool | skills/paperclip/references/api-reference.md:1306 |
|
||||
| skill:skills/paperclip/references/api-reference.md:error-handling:1336 | control_plane_owned | skills/paperclip/references/api-reference.md:1336 |
|
||||
| skill:skills/paperclip/references/api-reference.md:full-api-reference:1350 | optional_agent_tool | skills/paperclip/references/api-reference.md:1350 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agents:1352 | optional_agent_tool | skills/paperclip/references/api-reference.md:1352 |
|
||||
| skill:skills/paperclip/references/api-reference.md:issues-tasks:1373 | optional_agent_tool | skills/paperclip/references/api-reference.md:1373 |
|
||||
| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1413 | optional_agent_tool | skills/paperclip/references/api-reference.md:1413 |
|
||||
| skill:skills/paperclip/references/api-reference.md:routines:1437 | optional_agent_tool | skills/paperclip/references/api-reference.md:1437 |
|
||||
| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1453 | optional_agent_tool | skills/paperclip/references/api-reference.md:1453 |
|
||||
| skill:skills/paperclip/references/api-reference.md:secrets:1475 | optional_agent_tool | skills/paperclip/references/api-reference.md:1475 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1488 | optional_agent_tool | skills/paperclip/references/api-reference.md:1488 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1588 | optional_agent_tool | skills/paperclip/references/api-reference.md:1588 |
|
||||
| skill:skills/paperclip/references/api-reference.md:common-mistakes:1628 | optional_agent_tool | skills/paperclip/references/api-reference.md:1628 |
|
||||
| skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:926 | optional_agent_tool | skills/paperclip/references/api-reference.md:926 |
|
||||
| skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:935 | always_agent_tool | skills/paperclip/references/api-reference.md:935 |
|
||||
| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1032 | always_agent_tool | skills/paperclip/references/api-reference.md:1032 |
|
||||
| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1090 | always_agent_tool | skills/paperclip/references/api-reference.md:1090 |
|
||||
| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1205 | optional_agent_tool | skills/paperclip/references/api-reference.md:1205 |
|
||||
| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1315 | optional_agent_tool | skills/paperclip/references/api-reference.md:1315 |
|
||||
| skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1321 | always_agent_tool | skills/paperclip/references/api-reference.md:1321 |
|
||||
| skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1339 | always_agent_tool | skills/paperclip/references/api-reference.md:1339 |
|
||||
| skill:skills/paperclip/references/api-reference.md:error-handling:1369 | control_plane_owned | skills/paperclip/references/api-reference.md:1369 |
|
||||
| skill:skills/paperclip/references/api-reference.md:full-api-reference:1383 | optional_agent_tool | skills/paperclip/references/api-reference.md:1383 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agents:1385 | optional_agent_tool | skills/paperclip/references/api-reference.md:1385 |
|
||||
| skill:skills/paperclip/references/api-reference.md:issues-tasks:1406 | optional_agent_tool | skills/paperclip/references/api-reference.md:1406 |
|
||||
| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1446 | optional_agent_tool | skills/paperclip/references/api-reference.md:1446 |
|
||||
| skill:skills/paperclip/references/api-reference.md:routines:1470 | optional_agent_tool | skills/paperclip/references/api-reference.md:1470 |
|
||||
| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1486 | optional_agent_tool | skills/paperclip/references/api-reference.md:1486 |
|
||||
| skill:skills/paperclip/references/api-reference.md:secrets:1508 | optional_agent_tool | skills/paperclip/references/api-reference.md:1508 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1521 | optional_agent_tool | skills/paperclip/references/api-reference.md:1521 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1621 | optional_agent_tool | skills/paperclip/references/api-reference.md:1621 |
|
||||
| skill:skills/paperclip/references/api-reference.md:common-mistakes:1661 | optional_agent_tool | skills/paperclip/references/api-reference.md:1661 |
|
||||
|
||||
## Legacy MCP Alias Index
|
||||
|
||||
|
||||
@@ -722,180 +722,180 @@
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:893",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:926",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L893:ceo-strategy-approval",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L926:ceo-strategy-approval",
|
||||
"heading": "CEO strategy approval",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:902",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:935",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L902:questions-and-waiting-for-human-input",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L935:questions-and-waiting-for-human-input",
|
||||
"heading": "Questions and waiting for human input",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "request_human_input",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:999",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1032",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L999:issue-thread-confirmations",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1032:issue-thread-confirmations",
|
||||
"heading": "Issue-thread confirmations",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1057",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1090",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1057:checkbox-confirmations",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1090:checkbox-confirmations",
|
||||
"heading": "Checkbox confirmations",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1172",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1205",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1172:item-verdict-requests",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1205:item-verdict-requests",
|
||||
"heading": "Item verdict requests",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1282",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1315",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1282:checking-approval-status",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1315:checking-approval-status",
|
||||
"heading": "Checking approval status",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1288",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1321",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1288:approval-follow-up-requesting-agent",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1321:approval-follow-up-requesting-agent",
|
||||
"heading": "Approval follow-up (requesting agent)",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1306",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1339",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1306:issue-lifecycle",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1339:issue-lifecycle",
|
||||
"heading": "Issue Lifecycle",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1336",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1369",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1336:error-handling",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1369:error-handling",
|
||||
"heading": "Error Handling",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1350",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1383",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1350:full-api-reference",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1383:full-api-reference",
|
||||
"heading": "Full API Reference",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1352",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1385",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1352:agents",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1385:agents",
|
||||
"heading": "Agents",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1373",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1406",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1373:issues-tasks",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1406:issues-tasks",
|
||||
"heading": "Issues (Tasks)",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1413",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1446",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1413:companies-projects-goals",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1446:companies-projects-goals",
|
||||
"heading": "Companies, Projects, Goals",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1437",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1470",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1437:routines",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1470:routines",
|
||||
"heading": "Routines",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1453",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1486",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1453:approvals-costs-activity-dashboard",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1486:approvals-costs-activity-dashboard",
|
||||
"heading": "Approvals, Costs, Activity, Dashboard",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1475",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1508",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1475:secrets",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1508:secrets",
|
||||
"heading": "Secrets",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1488",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1521",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1488:agent-secret-proposals",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1521:agent-secret-proposals",
|
||||
"heading": "Agent secret proposals",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1541",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1574",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1541:re-bind-an-existing-secret-under-a-new-path-no-secret-id",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1574:re-bind-an-existing-secret-under-a-new-path-no-secret-id",
|
||||
"heading": "Re-bind an existing secret under a new path (no secret ID)",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1588",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1621",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1588:agent-secret-access",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1621:agent-secret-access",
|
||||
"heading": "Agent secret access",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1628",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:1661",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1628:common-mistakes",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1661:common-mistakes",
|
||||
"heading": "Common Mistakes",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
|
||||
@@ -5,6 +5,6 @@ Generated by `scripts/generate-capability-contract.mjs`; do not edit generated f
|
||||
- Skill/reference headings: 156
|
||||
- Legacy MCP tools: 42
|
||||
- Eval cases: 106 across 16 groups
|
||||
- Deterministic content SHA-256: `b42cfa2dbf4d314914ca18c77829f54e06b585892e38b1c4857b583989367a03`
|
||||
- Deterministic content SHA-256: `5a87f905c451fb79ec5d5fc9a961d3cbc4ba96c8486265088b5da2fda30e4831`
|
||||
|
||||
Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation.
|
||||
@@ -1 +1 @@
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","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 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 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 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 command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"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 command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"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","requiredClaimLine truncated
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","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 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 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 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 command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"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 command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"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","requiredClaimLine truncated
|
||||
@@ -1206,6 +1206,82 @@
|
||||
"title": "List company agents",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
"skill_test"
|
||||
],
|
||||
"description": "Create one native Paperclip Runner teammate for the current company and task. The new agent reports to you, inherits your native runtime, and receives no provider, adapter, environment, or credential configuration from the tool. Reuse an existing teammate when appropriate and follow any approval returned by the API.",
|
||||
"effect": "write",
|
||||
"inputSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"description": "Optional concise capability summary.",
|
||||
"maxLength": 2000,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"instructions": {
|
||||
"description": "Optional persona or task instructions.",
|
||||
"maxLength": 20000,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name for the new teammate.",
|
||||
"maxLength": 200,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"default": "general",
|
||||
"enum": [
|
||||
"ceo",
|
||||
"cto",
|
||||
"cmo",
|
||||
"cfo",
|
||||
"security",
|
||||
"engineer",
|
||||
"designer",
|
||||
"pm",
|
||||
"qa",
|
||||
"devops",
|
||||
"researcher",
|
||||
"general"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"description": "Optional teammate title.",
|
||||
"maxLength": 300,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"operationId": "hire_agent",
|
||||
"outputSchema": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"placement": "optional",
|
||||
"requiredClaims": [
|
||||
"delegation:agents:create"
|
||||
],
|
||||
"schema": "paperclip.semantic-action.v1",
|
||||
"title": "Hire a native agent",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"prpVersion": 1,
|
||||
"nativeExecutionVersion": 1,
|
||||
"catalogVersion": 1,
|
||||
"catalogSha256": "sha256:73321806bf90df701a9677a7a9527c5b3d39485078e4d3fa4bb3a244a74a0c3b",
|
||||
"catalogSha256": "sha256:1fa8ecf5d3c28e62a32e818de95633ff3efe5d4af1a100310516ff9b5ab2358d",
|
||||
"driverContractVersion": 1,
|
||||
"driverKind": "paperclip-deterministic",
|
||||
"driverVersion": "1.0.0"
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{
|
||||
"path": "schemas/provider-event.schema.json",
|
||||
"id": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json",
|
||||
"sha256": "e0ffb95cbde4afb65783106f471c5154cbcc59055461f96838cd8a5239e5ec56"
|
||||
"sha256": "0b138a41bd3bd0b1a115f5536a2d7349537e79e36ede3b51d003f9f21ce73b50"
|
||||
},
|
||||
{
|
||||
"path": "schemas/question-adapter-fixture.schema.json",
|
||||
@@ -160,7 +160,7 @@
|
||||
},
|
||||
{
|
||||
"path": "fixtures/evals/native-execution-seeded.json",
|
||||
"sha256": "ab9247489ae2427fec5a86c54b55698ac3e211bd65d93cf4e973e363050455fa",
|
||||
"sha256": "5f3144ea856ad27778f47de73eb542703b1d9df2abad2c427838fbaf69c3d59f",
|
||||
"expectation": "accept",
|
||||
"compatibilityCase": "canonical"
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"target": { "$ref": "#/$defs/safePath" },
|
||||
"namespace": { "type": ["string", "null"], "maxLength": 240 },
|
||||
"readOnly": { "type": ["boolean", "null"] },
|
||||
"inputUpdated": { "type": "boolean" },
|
||||
"status": { "enum": ["running", "completed", "failed", "cancelled", "interrupted"] },
|
||||
"durationMs": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"exitCode": { "type": ["integer", "null"] },
|
||||
|
||||
@@ -1168,6 +1168,9 @@ fn normalize_acpx_tool_call(
|
||||
.unwrap_or(Value::Null)
|
||||
},
|
||||
});
|
||||
if let Some(input_updated) = payload.get("inputUpdated").and_then(Value::as_bool) {
|
||||
normalized["inputUpdated"] = Value::Bool(input_updated);
|
||||
}
|
||||
if let (Some(object), Value::Object(output)) =
|
||||
(normalized.as_object_mut(), bounded_output(&output))
|
||||
{
|
||||
@@ -1424,6 +1427,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_only_typed_acpx_input_update_marker() {
|
||||
for input_updated in [
|
||||
Value::Bool(true),
|
||||
Value::Bool(false),
|
||||
json!("secret"),
|
||||
Value::Null,
|
||||
] {
|
||||
let events = normalize_acpx_tool_call(
|
||||
&json!({
|
||||
"type": "tool_call", "tag": "tool_call_update", "status": "pending",
|
||||
"title": "mcp__paperclip__hire_agent", "inputUpdated": input_updated,
|
||||
"rawInput": {"private": "secret-input"},
|
||||
}),
|
||||
"provider-tool-id",
|
||||
"execute",
|
||||
);
|
||||
assert_eq!(events[0].event_type, "tool.execution.progressed");
|
||||
if input_updated.is_boolean() {
|
||||
assert_eq!(events[0].payload["inputUpdated"], input_updated);
|
||||
} else {
|
||||
assert!(events[0].payload.get("inputUpdated").is_none());
|
||||
}
|
||||
assert!(!events[0].payload.to_string().contains("secret"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_codex_plan_without_retaining_native_envelope() {
|
||||
let events = normalize_codex_notification(
|
||||
|
||||
@@ -2054,9 +2054,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:893",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:926",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:893",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:926",
|
||||
"title": "CEO strategy approval",
|
||||
"expectedSemantics": "Skill guidance headed “CEO strategy approval”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2065,13 +2065,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:893"
|
||||
"skill:skills/paperclip/references/api-reference.md:926"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:902",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:935",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:902",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:935",
|
||||
"title": "Questions and waiting for human input",
|
||||
"expectedSemantics": "Skill guidance headed “Questions and waiting for human input”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -2080,13 +2080,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:902"
|
||||
"skill:skills/paperclip/references/api-reference.md:935"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:999",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1032",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:999",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1032",
|
||||
"title": "Issue-thread confirmations",
|
||||
"expectedSemantics": "Skill guidance headed “Issue-thread confirmations”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -2095,13 +2095,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:999"
|
||||
"skill:skills/paperclip/references/api-reference.md:1032"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1057",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1090",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1057",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1090",
|
||||
"title": "Checkbox confirmations",
|
||||
"expectedSemantics": "Skill guidance headed “Checkbox confirmations”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -2110,13 +2110,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1057"
|
||||
"skill:skills/paperclip/references/api-reference.md:1090"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1172",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1205",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1172",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1205",
|
||||
"title": "Item verdict requests",
|
||||
"expectedSemantics": "Skill guidance headed “Item verdict requests”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2125,13 +2125,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1172"
|
||||
"skill:skills/paperclip/references/api-reference.md:1205"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:checking-approval-status:1282",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:checking-approval-status:1315",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1282",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1315",
|
||||
"title": "Checking approval status",
|
||||
"expectedSemantics": "Skill guidance headed “Checking approval status”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2140,13 +2140,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1282"
|
||||
"skill:skills/paperclip/references/api-reference.md:1315"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1288",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:approval-follow-up-requesting-agent:1321",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1288",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1321",
|
||||
"title": "Approval follow-up (requesting agent)",
|
||||
"expectedSemantics": "Skill guidance headed “Approval follow-up (requesting agent)”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -2155,13 +2155,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1288"
|
||||
"skill:skills/paperclip/references/api-reference.md:1321"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1306",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:issue-lifecycle:1339",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1306",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1339",
|
||||
"title": "Issue Lifecycle",
|
||||
"expectedSemantics": "Skill guidance headed “Issue Lifecycle”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -2170,13 +2170,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1306"
|
||||
"skill:skills/paperclip/references/api-reference.md:1339"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:error-handling:1336",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:error-handling:1369",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1336",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1369",
|
||||
"title": "Error Handling",
|
||||
"expectedSemantics": "Skill guidance headed “Error Handling”.",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
@@ -2185,13 +2185,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1336"
|
||||
"skill:skills/paperclip/references/api-reference.md:1369"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:full-api-reference:1350",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:full-api-reference:1383",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1350",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1383",
|
||||
"title": "Full API Reference",
|
||||
"expectedSemantics": "Skill guidance headed “Full API Reference”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2200,13 +2200,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1350"
|
||||
"skill:skills/paperclip/references/api-reference.md:1383"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:agents:1352",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:agents:1385",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1352",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1385",
|
||||
"title": "Agents",
|
||||
"expectedSemantics": "Skill guidance headed “Agents”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2215,13 +2215,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1352"
|
||||
"skill:skills/paperclip/references/api-reference.md:1385"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:issues-tasks:1373",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:issues-tasks:1406",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1373",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1406",
|
||||
"title": "Issues (Tasks)",
|
||||
"expectedSemantics": "Skill guidance headed “Issues (Tasks)”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2230,13 +2230,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1373"
|
||||
"skill:skills/paperclip/references/api-reference.md:1406"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1413",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1446",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1413",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1446",
|
||||
"title": "Companies, Projects, Goals",
|
||||
"expectedSemantics": "Skill guidance headed “Companies, Projects, Goals”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2245,13 +2245,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1413"
|
||||
"skill:skills/paperclip/references/api-reference.md:1446"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:routines:1437",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:routines:1470",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1437",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1470",
|
||||
"title": "Routines",
|
||||
"expectedSemantics": "Skill guidance headed “Routines”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2260,13 +2260,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1437"
|
||||
"skill:skills/paperclip/references/api-reference.md:1470"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1453",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1486",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1453",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1486",
|
||||
"title": "Approvals, Costs, Activity, Dashboard",
|
||||
"expectedSemantics": "Skill guidance headed “Approvals, Costs, Activity, Dashboard”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2275,13 +2275,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1453"
|
||||
"skill:skills/paperclip/references/api-reference.md:1486"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:secrets:1475",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:secrets:1508",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1475",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1508",
|
||||
"title": "Secrets",
|
||||
"expectedSemantics": "Skill guidance headed “Secrets”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2290,13 +2290,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1475"
|
||||
"skill:skills/paperclip/references/api-reference.md:1508"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1488",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1521",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1488",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1521",
|
||||
"title": "Agent secret proposals",
|
||||
"expectedSemantics": "Skill guidance headed “Agent secret proposals”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2305,13 +2305,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1488"
|
||||
"skill:skills/paperclip/references/api-reference.md:1521"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:agent-secret-access:1588",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:agent-secret-access:1621",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1588",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1621",
|
||||
"title": "Agent secret access",
|
||||
"expectedSemantics": "Skill guidance headed “Agent secret access”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2320,13 +2320,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1588"
|
||||
"skill:skills/paperclip/references/api-reference.md:1621"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:common-mistakes:1628",
|
||||
"id": "skill:skills/paperclip/references/api-reference.md:common-mistakes:1661",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1628",
|
||||
"sourceAnchor": "skills/paperclip/references/api-reference.md:1661",
|
||||
"title": "Common Mistakes",
|
||||
"expectedSemantics": "Skill guidance headed “Common Mistakes”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -2335,7 +2335,7 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/references/api-reference.md:1628"
|
||||
"skill:skills/paperclip/references/api-reference.md:1661"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"src/scenarios/scenario-plan.ts"
|
||||
],
|
||||
"counts": {
|
||||
"actions": 47,
|
||||
"actions": 48,
|
||||
"legacyRequirements": 106
|
||||
},
|
||||
"actions": [
|
||||
@@ -802,6 +802,25 @@
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hire_agent",
|
||||
"ownership": "optional_agent_tool",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"legacyAliases": [],
|
||||
"contractCase": "protocol-action:hire_agent",
|
||||
"contractOwner": "src/catalog/protocol-action-contracts.test.ts::hire_agent has a schema-valid canonical example and every declared projection",
|
||||
"legacyBehavioralCases": [],
|
||||
"deterministicCases": [
|
||||
"protocol-action:hire_agent"
|
||||
],
|
||||
"legacyRequirementCases": [],
|
||||
"deterministicOwners": [
|
||||
"src/catalog/protocol-action-contracts.test.ts::hire_agent has a schema-valid canonical example and every declared projection",
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "list_approvals",
|
||||
"ownership": "optional_agent_tool",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{
|
||||
"id": "delegation_dependencies",
|
||||
"description": "Create or reassign delegated work and maintain dependency edges.",
|
||||
"operationIds": ["create_task", "reassign_task", "set_dependencies"]
|
||||
"operationIds": ["create_task", "reassign_task", "set_dependencies", "hire_agent"]
|
||||
},
|
||||
{
|
||||
"id": "governance",
|
||||
@@ -180,7 +180,7 @@
|
||||
"legacyGroup": 6,
|
||||
"name": "Subtasks",
|
||||
"owner": "optional delegation tools",
|
||||
"operationIds": ["create_task", "reassign_task"],
|
||||
"operationIds": ["create_task", "reassign_task", "hire_agent"],
|
||||
"controlPlaneOperationIds": ["route_wake"],
|
||||
"realSurface": "company issue create, child issue, guarded reassignment, and wake services",
|
||||
"mockStateDomains": ["company", "task", "actor", "blockers", "wake", "audit"],
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: canonical explanatory contract for the Paperclip runner V1 surface.
|
||||
|
||||
This document keeps three independent meanings of **group** separate. PRP families describe wire evidence and controller commands; capability placement decides who owns an operation; behavioral eval groups organize the 106 scenario corpus. None of the three axes can be used as a substitute for another.
|
||||
|
||||
The generated totals are **105 PRP events in 31 event families**, **18 controller commands in 7 command families**, **10 control-plane operations**, **47 reconciled semantic operations** (14 always, 33 optional), and **106 scenarios in 16 behavior groups**.
|
||||
The generated totals are **105 PRP events in 31 event families**, **18 controller commands in 7 command families**, **10 control-plane operations**, **48 reconciled semantic operations** (14 always, 34 optional), and **106 scenarios in 16 behavior groups**.
|
||||
|
||||
## Axis 1: PRP v1 event and command families
|
||||
|
||||
@@ -87,7 +87,7 @@ Placement has exactly three outcomes:
|
||||
|
||||
`answer_status_question`, `block_task`, `finish_task`, `get_task_context`, `get_task_history`, `inspect_operation_result`, `list_document_revisions`, `list_documents`, `read_document`, `register_deliverable`, `report_progress`, `request_human_input`, `request_review`, `write_document`.
|
||||
|
||||
### Optional operations (33) and grant groups (13)
|
||||
### Optional operations (34) and grant groups (13)
|
||||
|
||||
Grant groups are documentation/exposure bundles, not additional authority. The operation descriptor's exact `requiredClaims` remains decisive.
|
||||
|
||||
@@ -95,7 +95,7 @@ Grant groups are documentation/exposure bundles, not additional authority. The o
|
||||
| --- | --- | --- | --- |
|
||||
| `discovery` | `search_tasks`<br>`list_agents`<br>`get_agent`<br>`list_projects`<br>`list_goals` | `discovery:agents:read`<br>`discovery:goals:read`<br>`discovery:projects:read`<br>`discovery:tasks:read` | Company-visible task, agent, project, and goal discovery. |
|
||||
| `projects` | `create_project`<br>`list_project_repositories` | none | Project creation and authorized repository discovery through the live company/run authority. |
|
||||
| `delegation_dependencies` | `create_task`<br>`reassign_task`<br>`set_dependencies` | `delegation:tasks:assign`<br>`delegation:tasks:create`<br>`dependencies:write` | Create or reassign delegated work and maintain dependency edges. |
|
||||
| `delegation_dependencies` | `create_task`<br>`reassign_task`<br>`set_dependencies`<br>`hire_agent` | `delegation:agents:create`<br>`delegation:tasks:assign`<br>`delegation:tasks:create`<br>`dependencies:write` | Create or reassign delegated work and maintain dependency edges. |
|
||||
| `governance` | `list_approvals`<br>`get_approval`<br>`get_approval_context`<br>`request_approval`<br>`decide_approval`<br>`comment_on_approval` | `governance:approvals:comment`<br>`governance:approvals:decide`<br>`governance:approvals:read`<br>`governance:approvals:request` | Read, request, comment on, and decide approvals under governed-action checks. |
|
||||
| `cases` | `list_cases`<br>`upsert_case` | `cases:read`<br>`cases:write` | Read and update case summaries without reusing issue-document authority. |
|
||||
| `workspace_runtime` | `get_workspace_runtime`<br>`control_workspace_service` | `workspace:control`<br>`workspace:read` | Inspect and control the active issue workspace runtime. |
|
||||
@@ -132,6 +132,7 @@ Grant groups are documentation/exposure bundles, not additional authority. The o
|
||||
| `get_task_context` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `context_read:active_task` | `scenario` + `live`<br>`live_codex` | `PaperclipRunnerToolAuthority active issue/run + accepted plan revision`<br>bound company/assignment query plus exact accepted document revision projection<br>catalog PRP status: `bound` |
|
||||
| `get_task_history` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_history` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `get_workspace_runtime` | `optional_agent_tool` | `workspace:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:active_task_workspace` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `hire_agent` | `optional_agent_tool` | `delegation:agents:create` | `standard`<br>`skill_test` | `company_write` | `required` | no | inline/no mapping | `live`<br>`live_codex` | `PaperclipRunnerToolAuthority`<br>Authenticated PRP tool input/result and agent-hire state diff with activity record.<br>catalog PRP status: `bound` |
|
||||
| `inspect_operation_result` | `always_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `operation_result` | `scenario`<br>`scenario_mock` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_agents` | `optional_agent_tool` | `discovery:agents:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:company_actors` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `list_approvals` | `optional_agent_tool` | `governance:approvals:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:company_approvals` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
@@ -174,7 +175,7 @@ Behavior groups describe expected outcomes and trajectories. They do not grant t
|
||||
| [`st` — Status](#behavior-group-st-status) | always tools + control-plane arbitration | `answer_status_question`<br>`finish_task`<br>`block_task`<br>`request_review` | `reconcile_run`<br>`append_audit_record` | issue PATCH, review/liveness policy, and native finalization arbitration | `task`<br>`comments`<br>`interactions`<br>`blockers`<br>`audit`<br>`run` | 8 | semantic operation receipt, work assessment, issue-status decision, and terminal causality | Production semantic binding and additive typed operation/conflict receipts remain unimplemented. |
|
||||
| [`cm` — Comments](#behavior-group-cm-comments) | always tools | `get_task_history`<br>`report_progress` | `append_audit_record` | issue comment list/get/create routes | `task`<br>`comments`<br>`actor`<br>`idempotency`<br>`audit` | 6 | bounded read result or idempotent comment-write receipt plus audit reference | Active-task binding is unbound; cross-task comment mutation is deliberately outside V1. |
|
||||
| [`se` — Search](#behavior-group-se-search) | optional discovery tools | `search_tasks`<br>`list_agents`<br>`get_agent`<br>`list_projects`<br>`list_goals`<br>`list_project_repositories` | none | company issue search and agent/project/goal list/get routes | `company`<br>`task`<br>`actor`<br>`project`<br>`goal` | 4 | bounded redacted read projections through tool-result item events | Goal operations remain scenario-only. Project and repository discovery contracts are delivered by the live server authority; they are not implemented by the mock command dispatcher. |
|
||||
| [`su` — Subtasks](#behavior-group-su-subtasks) | optional delegation tools | `create_task`<br>`reassign_task` | `route_wake` | company issue create, child issue, guarded reassignment, and wake services | `company`<br>`task`<br>`actor`<br>`blockers`<br>`wake`<br>`audit` | 4 | company/task state diff, audit reference, and continuation wake evidence | create_task is production-bound to ordinary active-issue child creation with assignment, dependency-ready wake, company checks, child limits, and durable source-scoped idempotency. |
|
||||
| [`su` — Subtasks](#behavior-group-su-subtasks) | optional delegation tools | `create_task`<br>`reassign_task`<br>`hire_agent` | `route_wake` | company issue create, child issue, guarded reassignment, and wake services | `company`<br>`task`<br>`actor`<br>`blockers`<br>`wake`<br>`audit` | 4 | company/task state diff, audit reference, and continuation wake evidence | create_task is production-bound to ordinary active-issue child creation with assignment, dependency-ready wake, company checks, child limits, and durable source-scoped idempotency. |
|
||||
| [`bl` — Blockers](#behavior-group-bl-blockers) | always/optional tools + control plane | `block_task`<br>`set_dependencies` | `schedule_blocker_wake`<br>`route_wake` | issue relations, blocker projection, liveness validation, and blocker wake services | `task`<br>`blockers`<br>`wake`<br>`actor`<br>`audit`<br>`fault` | 5 | dependency diff, block receipt, attention routing, and issue-status decision | set_dependencies is production-bound for the active issue; block_task remains unbound, and cancelled-blocker receipts still need typed additive evidence. |
|
||||
| [`dp` — Documents and plans](#behavior-group-dp-documents-and-plans) | always tools; restore optional; destructive lifecycle control-plane-only | `list_documents`<br>`read_document`<br>`list_document_revisions`<br>`write_document` | `append_audit_record` | issue document list/read/upsert/revision/restore/lock/unlock/delete routes | `task`<br>`documents`<br>`interactions`<br>`idempotency`<br>`audit`<br>`fault` | 3 | bounded reads and revision-safe write/conflict/denial receipts with revision lineage | restore_document_revision is an approved optional-tool gap; lock/unlock/delete are intentionally control-plane-only. |
|
||||
| [`ix` — Interactions](#behavior-group-ix-interactions) | always tools + addressed resolver | `request_human_input` | `route_wake` | issue-thread interaction create/respond/accept/reject/withdraw services | `task`<br>`documents`<br>`interactions`<br>`wake`<br>`actor`<br>`idempotency` | 9 | interaction proposal/materialization/response/delivery and attention events | Production binding and semantic resolution receipts are unbound. |
|
||||
@@ -458,10 +459,10 @@ Current responsibility-based paths are normative. Numbered `phase-*` or mileston
|
||||
### Catalog split and deliberate replacement
|
||||
|
||||
- Scenario/eval catalog: **39** operations.
|
||||
- Live dispatcher catalog: **35** operations.
|
||||
- Shared: **27**; union/canonical authority: **47**.
|
||||
- Live dispatcher catalog: **36** operations.
|
||||
- Shared: **27**; union/canonical authority: **48**.
|
||||
- Scenario-only: `administer_company`, `export_company`, `inspect_operation_result`, `list_cases`, `list_company_skills`, `list_goals`, `list_routines`, `list_secret_metadata`, `manage_routine`, `read_secret_value`, `sync_company_skills`, `upsert_case`.
|
||||
- Live-only: `call_api`, `create_project`, `get_agent`, `get_approval`, `get_approval_context`, `list_project_repositories`, `schedule_wake`, `search_api`.
|
||||
- Live-only: `call_api`, `create_project`, `get_agent`, `get_approval`, `get_approval_context`, `hire_agent`, `list_project_repositories`, `schedule_wake`, `search_api`.
|
||||
- The generated provider contract contains exactly the live catalog; the canonical union remains the migration authority until all scenario-only operations are either implemented, deferred, or removed by an explicit reconciliation decision.
|
||||
- `generic_api_request` stays exported only for controlled tests and cannot be cited as real-surface, mock-parity, or PRP product coverage.
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
it("pins the reconciled op-set relationship between the two catalogs", () => {
|
||||
const summary = capabilityCatalogReconciliation();
|
||||
expect(summary.scenarioCount).toBe(39);
|
||||
expect(summary.liveCount).toBe(35);
|
||||
expect(summary.liveCount).toBe(36);
|
||||
expect(summary.sharedCount).toBe(27);
|
||||
expect(summary.unionCount).toBe(47);
|
||||
expect(summary.unionCount).toBe(48);
|
||||
// Any operation added to or removed from either catalog without a
|
||||
// reconciliation decision changes these exact sets and fails the gate.
|
||||
expect(summary.liveOnly).toEqual([
|
||||
@@ -31,6 +31,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
"get_agent",
|
||||
"get_approval",
|
||||
"get_approval_context",
|
||||
"hire_agent",
|
||||
"list_project_repositories",
|
||||
"schedule_wake",
|
||||
"search_api",
|
||||
@@ -52,7 +53,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
});
|
||||
|
||||
it("is the single source both catalogs derive their operation set from", () => {
|
||||
expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(47);
|
||||
expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(48);
|
||||
const canonicalIds = new Set(CAPABILITY_CANONICAL_OPERATIONS.map((operation) => operation.operationId));
|
||||
// Neither catalog may contain an operation absent from the canonical source.
|
||||
for (const tool of SCENARIO_CATALOG) expect(canonicalIds.has(tool.operationId)).toBe(true);
|
||||
@@ -86,7 +87,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
});
|
||||
|
||||
it("names placement, claims, task modes, side-effect class, idempotency, redaction, mock mapping, real binding status, and PRP evidence for every operation", () => {
|
||||
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(47);
|
||||
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(48);
|
||||
for (const operation of CAPABILITY_CANONICAL_CATALOG) {
|
||||
expect(operation.placement).toMatch(/^(always|optional)_agent_tool$/);
|
||||
expect(Array.isArray(operation.requiredClaims)).toBe(true);
|
||||
@@ -112,7 +113,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
it("classifies real binding status so generic_api_request is never product coverage", () => {
|
||||
const summary = capabilityCatalogReconciliation();
|
||||
expect(summary.byRealBindingStatus).toEqual({
|
||||
live_codex: 34,
|
||||
live_codex: 35,
|
||||
scenario_mock: 12,
|
||||
test_only: 1,
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("semantic action catalog", () => {
|
||||
(action) => action.operationId,
|
||||
);
|
||||
|
||||
expect(operationIds).toHaveLength(34);
|
||||
expect(operationIds).toHaveLength(35);
|
||||
expect(new Set(operationIds).size).toBe(operationIds.length);
|
||||
expect(operationIds).not.toContain("generic_api_request");
|
||||
expect(Object.isFrozen(PAPERCLIP_SEMANTIC_ACTION_CATALOG)).toBe(true);
|
||||
@@ -78,6 +78,17 @@ describe("semantic action catalog", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("declares hire_agent as a native identity-only mutation", () => {
|
||||
const hire = paperclipSemanticAction("hire_agent");
|
||||
expect(hire).toMatchObject({
|
||||
effect: "write",
|
||||
requiredClaims: ["delegation:agents:create"],
|
||||
allowedModes: ["standard", "skill_test"],
|
||||
});
|
||||
expect(hire?.inputSchema.properties).not.toHaveProperty("adapterConfig");
|
||||
expect(hire?.inputSchema.properties).not.toHaveProperty("env");
|
||||
});
|
||||
|
||||
it("compiles every operation input and output schema", () => {
|
||||
const ajv = new Ajv2020({
|
||||
allErrors: true,
|
||||
|
||||
@@ -323,6 +323,30 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
|
||||
placement: "optional",
|
||||
requiredClaims: ["discovery:agents:read"],
|
||||
}),
|
||||
descriptor({
|
||||
operationId: "hire_agent",
|
||||
title: "Hire a native agent",
|
||||
description:
|
||||
"Create one native Paperclip Runner teammate for the current company and task. The new agent reports to you, inherits your native runtime, and receives no provider, adapter, environment, or credential configuration from the tool. Reuse an existing teammate when appropriate and follow any approval returned by the API.",
|
||||
placement: "optional",
|
||||
effect: "write",
|
||||
requiredClaims: ["delegation:agents:create"],
|
||||
allowedModes: STANDARD_MODE,
|
||||
inputSchema: object(
|
||||
{
|
||||
name: text("Name for the new teammate.", 200),
|
||||
role: {
|
||||
enum: ["ceo", "cto", "cmo", "cfo", "security", "engineer", "designer", "pm", "qa", "devops", "researcher", "general"],
|
||||
default: "general",
|
||||
},
|
||||
title: nullableText("Optional teammate title.", 300),
|
||||
capabilities: nullableText("Optional concise capability summary.", 2_000),
|
||||
instructions: nullableText("Optional persona or task instructions.", 20_000),
|
||||
},
|
||||
["name"],
|
||||
),
|
||||
outputSchema: openObject,
|
||||
}),
|
||||
descriptor({
|
||||
operationId: "get_agent",
|
||||
title: "Get company agent",
|
||||
|
||||
@@ -15,6 +15,7 @@ export type PaperclipSemanticActionId =
|
||||
| "block_task"
|
||||
| "request_review"
|
||||
| "list_agents"
|
||||
| "hire_agent"
|
||||
| "get_agent"
|
||||
| "search_tasks"
|
||||
| "list_approvals"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ACPX_SIDECAR_PROTOCOL_VERSION } from "../drivers/acpx/sidecar-protocol.js";
|
||||
import { canonicalProviderEventsFromAcpxRuntimeEvent } from "../provider-events.js";
|
||||
import {
|
||||
awaitSidecarCleanupWithin,
|
||||
closeActiveSidecarHostWithin,
|
||||
@@ -30,6 +31,43 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("qualified ACPX runtime sidecar", () => {
|
||||
it("preserves ACP input presence through the bounded sidecar handoff", () => {
|
||||
const source = readFileSync(
|
||||
fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)),
|
||||
"utf8",
|
||||
);
|
||||
const start = source.indexOf("function boundRuntimeEventForNormalization");
|
||||
const end = source.indexOf("\nfunction sanitizeRuntimeEvent", start);
|
||||
if (start < 0 || end < 0) throw new Error("sidecar normalization source not found");
|
||||
const functionSource = source
|
||||
.slice(start, end)
|
||||
.replace(
|
||||
/function boundRuntimeEventForNormalization\(\n event: AcpRuntimeEvent,\n\): AcpRuntimeEvent \{/,
|
||||
"function boundRuntimeEventForNormalization(event) {",
|
||||
)
|
||||
.replace(" } as BoundedRuntimeToolEvent;", " };");
|
||||
const bound = new Function(
|
||||
"boundedOptionalText", "stableProviderIdentity", "safeAcpxLocations", "openParams", "safeOutput",
|
||||
`return (${functionSource});`,
|
||||
)(
|
||||
(value: unknown, fallback: string, max: number) => typeof value === "string" ? value.slice(0, max) : fallback,
|
||||
(value: string) => value,
|
||||
() => [],
|
||||
null,
|
||||
() => ({ output: null, outputBytes: 0, outputTruncated: false, outputDigest: null }),
|
||||
) as (event: Record<string, unknown>) => Record<string, unknown>;
|
||||
const bounded = bound({
|
||||
type: "tool_call", toolCallId: "provider-tool", title: "search", kind: "other",
|
||||
status: "pending", rawInput: { secret: "must not cross" }, rawOutput: null,
|
||||
});
|
||||
expect(bounded.inputUpdated).toBe(true);
|
||||
expect(bounded).not.toHaveProperty("rawInput");
|
||||
const canonical = canonicalProviderEventsFromAcpxRuntimeEvent(bounded as never, "fallback")[0]!;
|
||||
expect(canonical.payload).toMatchObject({ inputUpdated: true });
|
||||
expect(JSON.stringify(canonical.payload)).not.toContain("must not cross");
|
||||
expect(source).toContain("? boundedTool.inputUpdated");
|
||||
});
|
||||
|
||||
it.each(["paperclip_finish", "paperclip_block"])(
|
||||
"bounds pending %s calls before reserved handling and resumes admission",
|
||||
async (operationId) => {
|
||||
|
||||
@@ -776,6 +776,7 @@ function rejectTurnWaiters(terminalTurnId: string, message: string): void {
|
||||
|
||||
type BoundedRuntimeToolEvent = AcpRuntimeEvent & {
|
||||
paperclipBoundedTool: true;
|
||||
inputUpdated: boolean;
|
||||
paperclipOutput: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@@ -802,6 +803,8 @@ function boundRuntimeEventForNormalization(
|
||||
text: boundedOptionalText(event.text, "", 4_000),
|
||||
status: boundedOptionalText(event.status, "", 100),
|
||||
tag: boundedOptionalText(event.tag, "", 160),
|
||||
// Keep only whether this update carried input; rawInput is intentionally dropped.
|
||||
inputUpdated: Object.prototype.hasOwnProperty.call(event, "rawInput") && event.rawInput !== undefined,
|
||||
paperclipBoundedTool: true,
|
||||
paperclipOutput: safeOutput(event.rawOutput),
|
||||
} as BoundedRuntimeToolEvent;
|
||||
@@ -880,6 +883,10 @@ function sanitizeRuntimeEvent(event: AcpRuntimeEvent): Record<string, unknown> {
|
||||
: null,
|
||||
title: toolTitle,
|
||||
text: boundedOptionalText(event.text, "", 4_000) || null,
|
||||
// Preserve only the presence bit; rawInput itself never crosses the sidecar boundary.
|
||||
inputUpdated: boundedTool.paperclipBoundedTool === true
|
||||
? boundedTool.inputUpdated
|
||||
: Object.prototype.hasOwnProperty.call(event, "rawInput") && event.rawInput !== undefined,
|
||||
...toolClassification,
|
||||
};
|
||||
return boundedSidecarValue(
|
||||
|
||||
@@ -466,13 +466,13 @@ describe("workflow reports and stress traceability", () => {
|
||||
candidateFailures: 36,
|
||||
});
|
||||
expect(report.coverage).toMatchObject({
|
||||
canonicalOperations: 47,
|
||||
canonicalOperations: 48,
|
||||
capabilityCases: 106,
|
||||
workflows: 12,
|
||||
stressFindings: 44,
|
||||
stressExclusions: 1,
|
||||
});
|
||||
expect(report.coverage.operations).toHaveLength(47);
|
||||
expect(report.coverage.operations).toHaveLength(48);
|
||||
expect(report.coverage.composedWorkflows).toHaveLength(12);
|
||||
expect(
|
||||
report.coverage.operations.find(
|
||||
@@ -480,7 +480,7 @@ describe("workflow reports and stress traceability", () => {
|
||||
)?.workflowIds.length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(renderRunnerWorkflowMarkdown(report)).toContain(
|
||||
"47 operations · 106 capability cases · 12 workflows",
|
||||
"48 operations · 106 capability cases · 12 workflows",
|
||||
);
|
||||
expect(renderRunnerWorkflowJUnit(report)).toContain(
|
||||
'tests="36" failures="36" skipped="0"',
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/** Canonical definition and documentation for `hire_agent`. */
|
||||
export const hireAgentAction = {
|
||||
id: "hire_agent",
|
||||
canonical: {
|
||||
operationId: "hire_agent",
|
||||
surfaces: ["live"],
|
||||
placement: "optional_agent_tool",
|
||||
optionalGroup: "delegation_dependencies",
|
||||
requiredClaims: ["delegation:agents:create"],
|
||||
taskModes: ["standard", "skill_test"],
|
||||
sideEffectClass: "company_write",
|
||||
idempotency: "required",
|
||||
disabledByDefault: false,
|
||||
realBindingStatus: "live_codex",
|
||||
realServiceBinding: "PaperclipRunnerToolAuthority",
|
||||
prpEvidence: "Authenticated PRP tool input/result and agent-hire state diff with activity record.",
|
||||
prpBindingStatus: "bound",
|
||||
legacyAliases: [],
|
||||
},
|
||||
documentation: {
|
||||
title: "Hire a native agent",
|
||||
description: "Create a persistent native Runner teammate with an identity and persona. The teammate reports to the caller and inherits the caller's native runtime; provider, adapter, environment, and credential settings are selected by Paperclip and are never caller-supplied here.",
|
||||
note: "Use list_agents first when a suitable teammate may already exist.",
|
||||
},
|
||||
examples: {
|
||||
call: {
|
||||
operationId: "hire_agent",
|
||||
input: { name: "Reviewer", role: "qa" },
|
||||
},
|
||||
success: {
|
||||
ok: true,
|
||||
operationId: "hire_agent",
|
||||
result: {},
|
||||
},
|
||||
},
|
||||
live: {
|
||||
order: 15,
|
||||
descriptor: {
|
||||
schema: "paperclip.semantic-tool.v1",
|
||||
operationId: "hire_agent",
|
||||
version: 1,
|
||||
title: "Hire a native agent",
|
||||
description: "Create a persistent native Runner teammate with an identity and persona. The teammate reports to the caller and inherits the caller's native runtime; provider, adapter, environment, and credential settings are selected by Paperclip and are never caller-supplied here. Reuse a suitable teammate from list_agents when possible.",
|
||||
exposure: "optional",
|
||||
requiredClaims: ["delegation:agents:create"],
|
||||
allowedModes: ["standard", "skill_test"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1, maxLength: 200 },
|
||||
role: { type: "string", enum: ["ceo", "cto", "cmo", "cfo", "security", "engineer", "designer", "pm", "qa", "devops", "researcher", "general"] },
|
||||
title: { type: ["string", "null"], maxLength: 300 },
|
||||
capabilities: { type: ["string", "null"], maxLength: 2000 },
|
||||
instructions: { type: ["string", "null"], maxLength: 20000 },
|
||||
},
|
||||
required: ["name"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: { type: "object", additionalProperties: true },
|
||||
},
|
||||
},
|
||||
scenario: null,
|
||||
} as const;
|
||||
@@ -22,6 +22,7 @@ import { getTaskHistoryAction } from "./get-task-history.js";
|
||||
import { getWorkspaceRuntimeAction } from "./get-workspace-runtime.js";
|
||||
import { inspectOperationResultAction } from "./inspect-operation-result.js";
|
||||
import { listAgentsAction } from "./list-agents.js";
|
||||
import { hireAgentAction } from "./hire-agent.js";
|
||||
import { listApprovalsAction } from "./list-approvals.js";
|
||||
import { listCasesAction } from "./list-cases.js";
|
||||
import { listCompanySkillsAction } from "./list-company-skills.js";
|
||||
@@ -71,6 +72,7 @@ export const PAPERCLIP_PROTOCOL_ACTIONS = deepFreezeProtocolAction([
|
||||
getWorkspaceRuntimeAction,
|
||||
inspectOperationResultAction,
|
||||
listAgentsAction,
|
||||
hireAgentAction,
|
||||
listApprovalsAction,
|
||||
listCasesAction,
|
||||
listCompanySkillsAction,
|
||||
|
||||
@@ -1094,6 +1094,9 @@ export const providerEventSchema = {
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"inputUpdated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"running",
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
import equalModule from "ajv/dist/runtime/equal.js";
|
||||
import ucs2LengthModule from "ajv/dist/runtime/ucs2length.js";
|
||||
|
||||
"use strict";export const fixtureValidator: unknown = validate20;const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/fixture.schema.json","title":"PRP scripted replay fixture","type":"object","required":["schema","fixtureVersion","protocolVersion","name","description","identity","capabilities","commands","events","result"],"properties":{"schema":{"const":"paperclip.prp.fixture.v1"},"fixtureVersion":{"const":1},"protocolVersion":{"enum":[1,2]},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","minLength":1,"maxLength":1000},"identity":{"$ref":"https://paperclip.dev/schemas/prp/v1/identity.schema.json"},"capabilities":{"oneOf":[{"$ref":"https://paperclip.dev/schemas/prp/v1/capabilities.schema.json"},{"$ref":"https://paperclip.dev/schemas/prp/v2/capabilities.schema.json"}]},"commands":{"type":"array","items":{"oneOf":[{"$ref":"https://paperclip.dev/schemas/prp/v1/command.schema.json"},{"$ref":"https://paperclip.dev/schemas/prp/v2/command.schema.json"}]}},"events":{"type":"array","minItems":1,"items":{"oneOf":[{"$ref":"https://paperclip.dev/schemas/prp/v1/event.schema.json"},{"$ref":"https://paperclip.dev/schemas/prp/v2/event.schema.json"}]}},"requests":{"type":"array","items":{"$ref":"https://paperclip.dev/schemas/prp/v1/request.schema.json"}},"result":{"$ref":"https://paperclip.dev/schemas/prp/v1/result.schema.json"}},"additionalProperties":true};const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/capabilities.schema.json","title":"PRP negotiated capabilities","type":"object","required":["schema","sessionReusePolicy","driver","steer","interrupt","resume","runtimeRequests","structuredResult","typedEvents"],"properties":{"schema":{"const":"paperclip.prp.capabilities.v1"},"sessionReusePolicy":{"enum":["new_per_run","reuse_per_issue","reuse_per_workspace"]},"driver":{"type":"object","required":["kind","version"],"properties":{"kind":{"type":"string","minLength":1,"maxLength":80},"version":{"type":"string","minLength":1,"maxLength":80}},"additionalProperties":true},"steer":{"type":"boolean"},"interrupt":{"type":"boolean"},"resume":{"type":"boolean"},"runtimeRequests":{"type":"boolean"},"structuredResult":{"type":"boolean"},"typedEvents":{"type":"boolean"},"typedEventFamilies":{"type":"array","maxItems":64,"items":{"type":"object","required":["family","version","availability","detailLevel"],"properties":{"family":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z][a-z0-9_.-]*$"},"version":{"const":1},"availability":{"enum":["available","unsupported","policy_disabled"]},"detailLevel":{"enum":["summary","structured"]}},"additionalProperties":false}},"semanticTools":{"type":"object","required":["schema","schemaVersion","operations"],"properties":{"schema":{"const":"paperclip.prp.semantic_tools.v1"},"schemaVersion":{"const":1},"operations":{"type":"array","items":{"type":"object","required":["operationId","version","availability","redactionDisposition"],"properties":{"operationId":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]*$"},"version":{"const":1},"availability":{"enum":["available","denied","unsupported"]},"redactionDisposition":{"const":"digest_only"},"requiredClaims":{"type":"array","items":{"type":"string","minLength":1,"maxLength":160},"uniqueItems":true},"reasonCode":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z][a-z0-9_.:-]*$"}},"additionalProperties":true}}},"additionalProperties":true},"unsupported":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}},"additionalProperties":true};const schema45 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/command.schema.json","title":"PRP runner command","type":"object","required":["schema","commandId","controllerSeq","type","issuedAt","payload"],"properties":{"schema":{"const":"paperclip.prp.command.v1"},"commandId":{"type":"string","minLength":1,"maxLength":160},"controllerSeq":{"type":"integer","minimum":1},"type":{"enum":["run.prepare","run.attach","session.open","turn.start","turn.steer","turn.interrupt","turn.stop","request.resolve","interaction.receipt","semantic_tool.result","session.snapshot","session.close","session.budget.increase","session.destroy","run.cancel","runner.drain","runner.suspend","runner.shutdown"]},"issuedAt":{"type":"string","format":"date-time"},"deadlineAt":{"type":"string","format":"date-time"},"precondition":{"type":"object","properties":{"runnerState":{"type":"array","items":{"type":"string"}},"runState":{"type":"array","items":{"type":"string"}},"sessionState":{"type":"array","items":{"type":"string"}},"activeTurnId":{"type":["string","null"]}},"additionalProperties":true},"payload":{"type":"object","additionalProperties":true}},"additionalProperties":true};const schema46 = {"$schema":"https://json-schema.org/draft/2020-1
Line truncated
|
||||
"use strict";export const fixtureValidator: unknown = validate20;const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/fixture.schema.json","title":"PRP scripted replay fixture","type":"object","required":["schema","fixtureVersion","protocolVersion","name","description","identity","capabilities","commands","events","result"],"properties":{"schema":{"const":"paperclip.prp.fixture.v1"},"fixtureVersion":{"const":1},"protocolVersion":{"enum":[1,2]},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","minLength":1,"maxLength":1000},"identity":{"$ref":"https://paperclip.dev/schemas/prp/v1/identity.schema.json"},"capabilities":{"oneOf":[{"$ref":"https://paperclip.dev/schemas/prp/v1/capabilities.schema.json"},{"$ref":"https://paperclip.dev/schemas/prp/v2/capabilities.schema.json"}]},"commands":{"type":"array","items":{"oneOf":[{"$ref":"https://paperclip.dev/schemas/prp/v1/command.schema.json"},{"$ref":"https://paperclip.dev/schemas/prp/v2/command.schema.json"}]}},"events":{"type":"array","minItems":1,"items":{"oneOf":[{"$ref":"https://paperclip.dev/schemas/prp/v1/event.schema.json"},{"$ref":"https://paperclip.dev/schemas/prp/v2/event.schema.json"}]}},"requests":{"type":"array","items":{"$ref":"https://paperclip.dev/schemas/prp/v1/request.schema.json"}},"result":{"$ref":"https://paperclip.dev/schemas/prp/v1/result.schema.json"}},"additionalProperties":true};const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/capabilities.schema.json","title":"PRP negotiated capabilities","type":"object","required":["schema","sessionReusePolicy","driver","steer","interrupt","resume","runtimeRequests","structuredResult","typedEvents"],"properties":{"schema":{"const":"paperclip.prp.capabilities.v1"},"sessionReusePolicy":{"enum":["new_per_run","reuse_per_issue","reuse_per_workspace"]},"driver":{"type":"object","required":["kind","version"],"properties":{"kind":{"type":"string","minLength":1,"maxLength":80},"version":{"type":"string","minLength":1,"maxLength":80}},"additionalProperties":true},"steer":{"type":"boolean"},"interrupt":{"type":"boolean"},"resume":{"type":"boolean"},"runtimeRequests":{"type":"boolean"},"structuredResult":{"type":"boolean"},"typedEvents":{"type":"boolean"},"typedEventFamilies":{"type":"array","maxItems":64,"items":{"type":"object","required":["family","version","availability","detailLevel"],"properties":{"family":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z][a-z0-9_.-]*$"},"version":{"const":1},"availability":{"enum":["available","unsupported","policy_disabled"]},"detailLevel":{"enum":["summary","structured"]}},"additionalProperties":false}},"semanticTools":{"type":"object","required":["schema","schemaVersion","operations"],"properties":{"schema":{"const":"paperclip.prp.semantic_tools.v1"},"schemaVersion":{"const":1},"operations":{"type":"array","items":{"type":"object","required":["operationId","version","availability","redactionDisposition"],"properties":{"operationId":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]*$"},"version":{"const":1},"availability":{"enum":["available","denied","unsupported"]},"redactionDisposition":{"const":"digest_only"},"requiredClaims":{"type":"array","items":{"type":"string","minLength":1,"maxLength":160},"uniqueItems":true},"reasonCode":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z][a-z0-9_.:-]*$"}},"additionalProperties":true}}},"additionalProperties":true},"unsupported":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}},"additionalProperties":true};const schema45 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/command.schema.json","title":"PRP runner command","type":"object","required":["schema","commandId","controllerSeq","type","issuedAt","payload"],"properties":{"schema":{"const":"paperclip.prp.command.v1"},"commandId":{"type":"string","minLength":1,"maxLength":160},"controllerSeq":{"type":"integer","minimum":1},"type":{"enum":["run.prepare","run.attach","session.open","turn.start","turn.steer","turn.interrupt","turn.stop","request.resolve","interaction.receipt","semantic_tool.result","session.snapshot","session.close","session.budget.increase","session.destroy","run.cancel","runner.drain","runner.suspend","runner.shutdown"]},"issuedAt":{"type":"string","format":"date-time"},"deadlineAt":{"type":"string","format":"date-time"},"precondition":{"type":"object","properties":{"runnerState":{"type":"array","items":{"type":"string"}},"runState":{"type":"array","items":{"type":"string"}},"sessionState":{"type":"array","items":{"type":"string"}},"activeTurnId":{"type":["string","null"]}},"additionalProperties":true},"payload":{"type":"object","additionalProperties":true}},"additionalProperties":true};const schema46 = {"$schema":"https://json-schema.org/draft/2020-1
Line truncated
|
||||
@@ -684,6 +684,42 @@ describe("provider-neutral events", () => {
|
||||
expect(unnamed.payload).toMatchObject({ name: null, operation: "unknown" });
|
||||
});
|
||||
|
||||
it("records only ACP tool-input presence without exporting input values", () => {
|
||||
const update = canonicalProviderEventsFromAcpxRuntimeEvent(
|
||||
{
|
||||
type: "tool_call",
|
||||
tag: "tool_call_update",
|
||||
toolCallId: "acpx-input-1",
|
||||
title: "paperclip.search_tasks",
|
||||
kind: "other",
|
||||
status: "pending",
|
||||
rawInput: { query: "private value" },
|
||||
},
|
||||
"fallback",
|
||||
)[0]!;
|
||||
const statusOnly = canonicalProviderEventsFromAcpxRuntimeEvent(
|
||||
{
|
||||
type: "tool_call",
|
||||
tag: "tool_call_update",
|
||||
toolCallId: "acpx-input-1",
|
||||
title: "paperclip.search_tasks",
|
||||
kind: "other",
|
||||
status: "pending",
|
||||
},
|
||||
"fallback",
|
||||
)[0]!;
|
||||
expect(update.payload).toMatchObject({ inputUpdated: true });
|
||||
expect(update.payload).not.toHaveProperty("rawInput");
|
||||
expect(JSON.stringify(update.payload)).not.toContain("private value");
|
||||
expect(statusOnly.payload).not.toHaveProperty("inputUpdated");
|
||||
expect(
|
||||
canonicalProviderEventsFromAcpxRuntimeEvent(
|
||||
{ ...statusOnly.payload, type: "tool_call", inputUpdated: false } as never,
|
||||
"fallback",
|
||||
)[0]!.payload,
|
||||
).toMatchObject({ inputUpdated: false });
|
||||
});
|
||||
|
||||
it("presents OpenCode's server-qualified semantic tool with its canonical name", () => {
|
||||
const event = canonicalProviderEventsFromOpenCodePart({
|
||||
id: "finish-1",
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AcpRuntimeEventShape {
|
||||
text?: string;
|
||||
status?: string;
|
||||
rawOutput?: unknown;
|
||||
inputUpdated?: boolean;
|
||||
tag?: string;
|
||||
entries?: Array<{ content: string; status?: string }>;
|
||||
[key: string]: unknown;
|
||||
@@ -1133,6 +1134,11 @@ export function canonicalProviderEventsFromAcpxRuntimeEvent(
|
||||
target: safeAcpLocation(event.locations?.[0]),
|
||||
namespace: mcp?.namespace ?? null,
|
||||
readOnly: ["read", "search", "list"].includes(operation),
|
||||
...(Object.prototype.hasOwnProperty.call(event, "rawInput")
|
||||
? { inputUpdated: event.rawInput !== undefined }
|
||||
: typeof event.inputUpdated === "boolean"
|
||||
? { inputUpdated: event.inputUpdated }
|
||||
: {}),
|
||||
status,
|
||||
durationMs: null,
|
||||
exitCode: null,
|
||||
|
||||
@@ -28,6 +28,7 @@ const NAMESPACE: Readonly<Record<PaperclipSemanticActionId, string>> =
|
||||
block_task: "active_task",
|
||||
request_review: "active_task",
|
||||
list_agents: "discovery",
|
||||
hire_agent: "delegation",
|
||||
get_agent: "discovery",
|
||||
search_tasks: "discovery",
|
||||
list_approvals: "governance",
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("Capability semantic catalog and authorization", () => {
|
||||
it("publishes a stable narrow catalog without credentials or control-plane-owned tools", () => {
|
||||
const names = CAPABILITY_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
expect(names).toHaveLength(35);
|
||||
expect(names).toHaveLength(36);
|
||||
expect(names).toContain("get_task_context");
|
||||
expect(names).toContain("finish_task");
|
||||
expect(names).not.toContain("checkout_task");
|
||||
|
||||
@@ -133,6 +133,9 @@ export type BuiltInAgentReset = z.infer<typeof builtInAgentResetSchema>;
|
||||
export const createAgentHireSchema = createAgentSchema.extend({
|
||||
sourceIssueId: z.string().guid().optional().nullable(),
|
||||
sourceIssueIds: z.array(z.string().guid()).optional(),
|
||||
// Agent-authored hires may explicitly request the caller's native runner
|
||||
// settings. The server consumes this intent; it is never an agent column.
|
||||
inheritRuntimeFrom: z.literal("caller").optional(),
|
||||
});
|
||||
|
||||
export type CreateAgentHire = z.infer<typeof createAgentHireSchema>;
|
||||
|
||||
@@ -175,6 +175,46 @@ describe("agent-created hires use managed AI connections", () => {
|
||||
try { expect(runtime.attribution.connectionId).toBe(f.account.connectionId); } finally { await runtime.cleanup(); }
|
||||
});
|
||||
|
||||
it.each([
|
||||
["codex", { provider: "codex", model: "gpt-5.6-sol", codexPermissionMode: "never", lifecycleMode: "per_turn" }],
|
||||
["claude", { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" }],
|
||||
] as const)("caller runtime inheritance preserves safe %s settings only", async (_name, parentConfig) => {
|
||||
const f = await fixture("anthropic", "subscription");
|
||||
await db.update(agents).set({
|
||||
adapterType: "paperclip_runner",
|
||||
adapterConfig: {
|
||||
...parentConfig,
|
||||
cwd: "/private/parent-workspace",
|
||||
env: { ANTHROPIC_API_KEY: { type: "secret_ref", secretId: "parent-secret" } },
|
||||
instructionsFilePath: "/private/parent-instructions.md",
|
||||
runtimeSessionId: "parent-session",
|
||||
},
|
||||
}).where(eq(agents.id, f.agentId));
|
||||
const agent = hired(await request(f.app).post(`/api/companies/${f.companyId}/agent-hires`).send({
|
||||
name: "Inherited teammate", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
}));
|
||||
expect(agent.adapterConfig).toMatchObject(parentConfig);
|
||||
expect(agent.adapterConfig).not.toHaveProperty("cwd");
|
||||
expect(agent.adapterConfig).not.toHaveProperty("env");
|
||||
expect(agent.adapterConfig.instructionsFilePath).not.toBe("/private/parent-instructions.md");
|
||||
expect(agent.adapterConfig).not.toHaveProperty("runtimeSessionId");
|
||||
expect(agent.runtimeConfig.aiConnection).toMatchObject({
|
||||
provider: parentConfig.provider === "codex" ? "openai" : "anthropic",
|
||||
mode: "responsible_user",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects caller inheritance when the request supplies competing runtime settings", async () => {
|
||||
const f = await fixture("anthropic", "subscription");
|
||||
await db.update(agents).set({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude" } }).where(eq(agents.id, f.agentId));
|
||||
const response = await request(f.app).post(`/api/companies/${f.companyId}/agent-hires`).send({
|
||||
name: "Conflicting teammate", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
adapterConfig: { model: "caller.override" },
|
||||
});
|
||||
expect(response.status).toBe(422);
|
||||
expect(response.body.error).toContain("cannot be combined");
|
||||
});
|
||||
|
||||
it.each([true, false])("preserves shared connection access boundaries (company access: %s)", async (allAgents) => {
|
||||
const f = await fixture("anthropic");
|
||||
const account = await aiConnectionService(db).save(f.companyId, f.userId, { provider: "anthropic", method: "api_key", name: "Shared Claude", ownership: "shared", apiKey: "fixture", agentIds: [f.agentId], allAgents }, "fixture");
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
companySecretVersions,
|
||||
companySecrets,
|
||||
createDb,
|
||||
environments,
|
||||
approvals,
|
||||
principalPermissionGrants,
|
||||
userSecretDeclarations,
|
||||
userSecretDefinitions,
|
||||
@@ -78,6 +80,7 @@ describeEmbeddedPostgres("hired agent provider credential inheritance", () => {
|
||||
}, 60_000);
|
||||
|
||||
afterEach(async () => {
|
||||
await db.delete(approvals);
|
||||
await db.delete(activityLog);
|
||||
await db.delete(userSecretDeclarations);
|
||||
await db.delete(userSecretDefinitions);
|
||||
@@ -88,6 +91,7 @@ describeEmbeddedPostgres("hired agent provider credential inheritance", () => {
|
||||
await db.delete(principalPermissionGrants);
|
||||
await db.delete(companyMemberships);
|
||||
await db.delete(agents);
|
||||
await db.delete(environments);
|
||||
await db.delete(companies);
|
||||
});
|
||||
|
||||
@@ -458,6 +462,88 @@ describeEmbeddedPostgres("hired agent provider credential inheritance", () => {
|
||||
expect(Object.keys(childEnvOf(res))).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects native runtime inheritance for a board actor", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const parent = await seedParentAgent(companyId, "paperclip_runner", {});
|
||||
const res = await hire(userActor(), companyId, {
|
||||
name: "Board Inheritance Attempt", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(parent.adapterType).toBe("paperclip_runner");
|
||||
});
|
||||
|
||||
it("rejects inheritance from a legacy caller", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const parent = await seedParentAgent(companyId, "codex_local", {});
|
||||
const res = await hire(agentActor(companyId, parent.id), companyId, {
|
||||
name: "Legacy Inheritance Attempt", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status).toBe(422);
|
||||
expect(res.body.error).toContain("paperclip_runner adapter");
|
||||
});
|
||||
|
||||
it("rejects native runtime inheritance across companies", async () => {
|
||||
const parentCompanyId = await seedCompany();
|
||||
const targetCompanyId = await seedCompany();
|
||||
const parent = await seedParentAgent(parentCompanyId, "paperclip_runner", { provider: "codex", model: "gpt-5.6-sol" });
|
||||
const res = await hire(agentActor(targetCompanyId, parent.id), targetCompanyId, {
|
||||
name: "Cross Company Inheritance Attempt", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(await db.select().from(agents).where(eq(agents.companyId, targetCompanyId))).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("records only inherited safe settings in an approval snapshot", async () => {
|
||||
const companyId = await seedCompany();
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: true }).where(eq(companies.id, companyId));
|
||||
const parent = await seedParentAgent(companyId, "paperclip_runner", {});
|
||||
await db.update(agents).set({ adapterConfig: {
|
||||
provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5", acpxPermissionMode: "approve-all",
|
||||
env: { ANTHROPIC_API_KEY: secretRef("parent-secret") }, cwd: "/private/parent",
|
||||
invocationLimits: { maxIterations: 3, credentials: "never-copy" },
|
||||
} }).where(eq(agents.id, parent.id));
|
||||
const res = await hire(agentActor(companyId, parent.id), companyId, {
|
||||
name: "Approval Snapshot Child", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.agent.status).toBe("pending_approval");
|
||||
expect(res.body.approval.payload.inheritRuntimeFrom).toBeUndefined();
|
||||
expect(res.body.approval.payload.adapterConfig).toMatchObject({ provider: "acpx", acpxAgent: "claude", acpxPermissionMode: "approve-all" });
|
||||
expect(res.body.approval.payload.adapterConfig.env).toBeUndefined();
|
||||
expect(res.body.approval.payload.adapterConfig.cwd).toBeUndefined();
|
||||
expect(res.body.approval.payload.adapterConfig.invocationLimits).toEqual({ maxIterations: 3 });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude_managed", "managedProfileId", "managedAgentsRetentionAcknowledged", "Managed Agent"],
|
||||
["aws_agentcore", "agentCoreProfileId", "agentCoreRetentionAcknowledged", "Remote Agent"],
|
||||
])("revalidates the inherited %s company profile before creating a hire", async (provider, profileKey, retentionKey, label) => {
|
||||
const companyId = await seedCompany();
|
||||
const parent = await seedParentAgent(companyId, "paperclip_runner", {});
|
||||
await db.update(agents).set({ adapterConfig: {
|
||||
provider, [profileKey]: randomUUID(), [retentionKey]: true,
|
||||
} }).where(eq(agents.id, parent.id));
|
||||
const res = await hire(agentActor(companyId, parent.id), companyId, {
|
||||
name: "Missing Profile Child", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(404);
|
||||
expect(res.body.error).toBe(`${label} profile not found`);
|
||||
expect(await db.select().from(agents).where(eq(agents.companyId, companyId))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("validates and preserves an inherited caller default environment", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const parent = await seedParentAgent(companyId, "paperclip_runner", {});
|
||||
const environmentId = randomUUID();
|
||||
await db.insert(environments).values({ id: environmentId, name: `Local ${environmentId}`, driver: "local" });
|
||||
await db.update(agents).set({ defaultEnvironmentId: environmentId }).where(eq(agents.id, parent.id));
|
||||
const res = await hire(agentActor(companyId, parent.id), companyId, {
|
||||
name: "Invalid Environment Child", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(res.body.agent.defaultEnvironmentId).toBe(environmentId);
|
||||
});
|
||||
|
||||
it("inherits the fixed Claude OAuth binding, keeps its version, and leaks no token value", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const parent = await seedParentAgent(companyId, "claude_local", {
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { trackAgentCreated } from "@paperclipai/shared/telemetry";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { inheritNativeRunnerAdapterConfig } from "../services/native-runtime/native-agent-runtime-inheritance.js";
|
||||
import { agentInstructionsBundleMode } from "../services/agent-instructions.js";
|
||||
import {
|
||||
agentService,
|
||||
@@ -4450,8 +4451,33 @@ export function agentRoutes(
|
||||
// The onboarding marker is not an agent column. The server consumes it to
|
||||
// seed the chief-of-staff persona; it never reaches the insert values.
|
||||
onboardingFirstAgent: hireOnboardingFirstAgent,
|
||||
// This intent flag is consumed below and must never reach agent create.
|
||||
inheritRuntimeFrom,
|
||||
...hireInput
|
||||
} = req.body;
|
||||
|
||||
if (inheritRuntimeFrom === "caller") {
|
||||
if (req.actor.type !== "agent" || !req.actor.agentId) {
|
||||
throw forbidden("Only an agent can inherit native runtime settings from the caller");
|
||||
}
|
||||
if (hireInput.adapterType !== "paperclip_runner") {
|
||||
throw unprocessable("inheritRuntimeFrom=caller requires adapterType=paperclip_runner");
|
||||
}
|
||||
const requestedConfig = (hireInput.adapterConfig ?? {}) as Record<string, unknown>;
|
||||
const requestedRuntime = (hireInput.runtimeConfig ?? {}) as Record<string, unknown>;
|
||||
if (Object.keys(requestedConfig).length > 0 || Object.keys(requestedRuntime).length > 0 || hireInput.defaultEnvironmentId !== undefined) {
|
||||
throw unprocessable("inheritRuntimeFrom=caller cannot be combined with adapterConfig, runtimeConfig, or defaultEnvironmentId");
|
||||
}
|
||||
const caller = await svc.getById(req.actor.agentId);
|
||||
if (!caller || caller.companyId !== companyId) {
|
||||
throw forbidden("The caller agent is not in this company");
|
||||
}
|
||||
if (caller.adapterType !== "paperclip_runner") {
|
||||
throw unprocessable("The caller must use the paperclip_runner adapter");
|
||||
}
|
||||
hireInput.adapterConfig = inheritNativeRunnerAdapterConfig(caller.adapterConfig);
|
||||
hireInput.defaultEnvironmentId = caller.defaultEnvironmentId ?? null;
|
||||
}
|
||||
hireInput.adapterType = await assertSelectableAdapterType(hireInput.adapterType);
|
||||
const rawHireAdapterConfig = (hireInput.adapterConfig ?? {}) as Record<string, unknown>;
|
||||
assertProviderTraceSettingTransition(req, hireInput.runtimeConfig);
|
||||
@@ -4513,6 +4539,11 @@ export function agentRoutes(
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
runtimeConfig: normalizedRuntimeConfig,
|
||||
};
|
||||
await assertAgentEnvironmentSelection(companyId, hireInput.adapterType, hireInput.defaultEnvironmentId);
|
||||
await assertAgentDefaultEnvironmentSelection(companyId, hireInput.defaultEnvironmentId, {
|
||||
allowedDrivers: allowedEnvironmentDriversForAgent(hireInput.adapterType),
|
||||
allowedSandboxProviders: allowedSandboxProvidersForAgent(hireInput.adapterType),
|
||||
});
|
||||
|
||||
const company = await db
|
||||
.select()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inheritNativeRunnerAdapterConfig } from "./native-agent-runtime-inheritance.js";
|
||||
import { resolvePaperclipRunnerProviderProfile } from "./provider-profile.js";
|
||||
|
||||
describe("native hire runtime inheritance", () => {
|
||||
it.each([
|
||||
["claude_managed", "managedProfileId", "managedAgentsRetentionAcknowledged"],
|
||||
["aws_agentcore", "agentCoreProfileId", "agentCoreRetentionAcknowledged"],
|
||||
])("preserves the company profile reference for %s", (provider, profileKey, retentionKey) => {
|
||||
const inherited = inheritNativeRunnerAdapterConfig({
|
||||
provider,
|
||||
[profileKey]: "qualified-company-profile",
|
||||
[retentionKey]: true,
|
||||
runtimeSessionId: "parent-session",
|
||||
env: { API_KEY: "parent-secret" },
|
||||
});
|
||||
expect(resolvePaperclipRunnerProviderProfile(inherited)).toMatchObject({
|
||||
provider,
|
||||
[profileKey]: "qualified-company-profile",
|
||||
});
|
||||
expect(inherited).not.toHaveProperty("runtimeSessionId");
|
||||
expect(inherited).not.toHaveProperty("env");
|
||||
});
|
||||
|
||||
it.each(["codex", "acpx", "opencode"])("does not copy unrelated profile references for %s", (provider) => {
|
||||
const inherited = inheritNativeRunnerAdapterConfig({
|
||||
provider, managedProfileId: "unrelated-managed", agentCoreProfileId: "unrelated-remote",
|
||||
});
|
||||
expect(inherited).not.toHaveProperty("managedProfileId");
|
||||
expect(inherited).not.toHaveProperty("agentCoreProfileId");
|
||||
});
|
||||
|
||||
it.each([null, true, {}, "", " "])("does not copy an invalid profile reference %j", (profileId) => {
|
||||
expect(inheritNativeRunnerAdapterConfig({ provider: "claude_managed", managedProfileId: profileId }))
|
||||
.not.toHaveProperty("managedProfileId");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* The small, non-sensitive portion of a native runner configuration that may
|
||||
* follow an agent when it hires a teammate. Keep this list closed: adapter
|
||||
* configuration also contains instructions paths, environment bindings, and
|
||||
* provider session identity that must remain owned by the new agent. A company
|
||||
* profile reference may be reused; the hire route revalidates its qualification.
|
||||
*/
|
||||
export const INHERITABLE_NATIVE_RUNNER_CONFIG_KEYS = [
|
||||
"provider",
|
||||
"acpxAgent",
|
||||
"model",
|
||||
"codexPermissionMode",
|
||||
"opencodePermissionMode",
|
||||
"acpxPermissionMode",
|
||||
"lifecycleMode",
|
||||
"modelReasoningEffort",
|
||||
"maxIterations",
|
||||
"maxOutputTokens",
|
||||
"timeoutSeconds",
|
||||
"idleTimeoutMs",
|
||||
"maxSessionListCostUsd",
|
||||
"maxEstimatedSessionCostUsd",
|
||||
"invocationLimits",
|
||||
"managedAgentsRetentionAcknowledged",
|
||||
"agentCoreRetentionAcknowledged",
|
||||
] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
const INHERITABLE_INVOCATION_LIMIT_KEYS = [
|
||||
"maxIterations",
|
||||
"maxOutputTokens",
|
||||
"timeoutSeconds",
|
||||
] as const;
|
||||
|
||||
function inheritedScalar(key: string, value: unknown): unknown {
|
||||
if (key === "maxIterations" || key === "maxOutputTokens" || key === "timeoutSeconds" || key === "idleTimeoutMs") {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
||||
const bounds = key === "maxIterations" ? [1, 8] : key === "maxOutputTokens" ? [1, 4096] : key === "timeoutSeconds" ? [1, 300] : [1, 86_400_000];
|
||||
return value >= bounds[0] && value <= bounds[1] ? value : undefined;
|
||||
}
|
||||
if (key === "maxSessionListCostUsd" || key === "maxEstimatedSessionCostUsd") {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
return typeof value === "string" || typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
export function inheritNativeRunnerAdapterConfig(adapterConfig: unknown): Record<string, unknown> {
|
||||
const source = isRecord(adapterConfig) ? adapterConfig : {};
|
||||
const inherited = Object.fromEntries(
|
||||
INHERITABLE_NATIVE_RUNNER_CONFIG_KEYS
|
||||
.filter((key) => key !== "invocationLimits")
|
||||
.filter((key) => source[key] !== undefined)
|
||||
.map((key) => [key, inheritedScalar(key, source[key])])
|
||||
.filter((entry): entry is [string, unknown] => entry[1] !== undefined),
|
||||
);
|
||||
const invocationLimits = isRecord(source.invocationLimits) ? source.invocationLimits : null;
|
||||
const limits = invocationLimits
|
||||
? Object.fromEntries(
|
||||
INHERITABLE_INVOCATION_LIMIT_KEYS
|
||||
.filter((key) => invocationLimits[key] !== undefined)
|
||||
.map((key) => [key, inheritedScalar(key, invocationLimits[key])])
|
||||
.filter((entry): entry is [string, unknown] => entry[1] !== undefined),
|
||||
)
|
||||
: undefined;
|
||||
if (limits && Object.keys(limits).length > 0) inherited.invocationLimits = limits;
|
||||
// Copy only the selected provider's company profile reference, never a live
|
||||
// provider session or the profile contents. The route still checks company
|
||||
// ownership, enablement, retention, and current qualification before hiring.
|
||||
const profileKey = source.provider === "claude_managed"
|
||||
? "managedProfileId"
|
||||
: source.provider === "aws_agentcore" ? "agentCoreProfileId" : null;
|
||||
if (profileKey) {
|
||||
const profileId = source[profileKey];
|
||||
if (typeof profileId === "string" && profileId.trim()) inherited[profileKey] = profileId;
|
||||
}
|
||||
return inherited;
|
||||
}
|
||||
@@ -156,6 +156,7 @@ const SAFE_NATIVE_RUN_SPAN_ATTRIBUTE_KEYS = new Set([
|
||||
"lifecycleMode",
|
||||
"mode",
|
||||
"outcome",
|
||||
"operation",
|
||||
"provider",
|
||||
"reason",
|
||||
"runtime",
|
||||
|
||||
@@ -289,6 +289,7 @@ import {
|
||||
resolveRemoteRunnerTransportMode,
|
||||
renewNativeSessionExecutionLease,
|
||||
runtimeInputLifecycleMetric,
|
||||
firstMeaningfulAgentEventKind,
|
||||
runtimeQuestionFallbackFromEvent,
|
||||
resolveNativeRuntimeRequest,
|
||||
resolveNativeHarnessPersistenceProfile,
|
||||
@@ -308,6 +309,59 @@ beforeEach(() => {
|
||||
state.assertCurrentWakeCommentsRead.mockReset().mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("first meaningful native agent event", () => {
|
||||
const event = (eventType: string, payload: Record<string, unknown>) =>
|
||||
({ eventType, payload }) as PrpEvent;
|
||||
|
||||
it("accepts nonempty assistant and reasoning deltas", () => {
|
||||
expect(
|
||||
firstMeaningfulAgentEventKind(
|
||||
event("item.delta", { kind: "agentMessage", text: "first token" }),
|
||||
),
|
||||
).toBe("agentMessage");
|
||||
expect(
|
||||
firstMeaningfulAgentEventKind(
|
||||
event("item.delta", { kind: "reasoning", delta: "thinking" }),
|
||||
),
|
||||
).toBe("reasoning");
|
||||
});
|
||||
|
||||
it("accepts real tool starts and preserves qualified item events", () => {
|
||||
expect(
|
||||
firstMeaningfulAgentEventKind(
|
||||
event("tool.execution.started", {
|
||||
executionId: "tool-1",
|
||||
name: "Terminal",
|
||||
}),
|
||||
),
|
||||
).toBe("toolCall");
|
||||
expect(
|
||||
firstMeaningfulAgentEventKind(
|
||||
event("item.started", { kind: "dynamicToolCall" }),
|
||||
),
|
||||
).toBe("dynamicToolCall");
|
||||
expect(
|
||||
firstMeaningfulAgentEventKind(
|
||||
event("item.completed", { kind: "agentMessage" }),
|
||||
),
|
||||
).toBe("agentMessage");
|
||||
});
|
||||
|
||||
it("counts a generic tool announcement without a display name", () => {
|
||||
expect(firstMeaningfulAgentEventKind(event("tool.execution.started", { executionId: "tool-1", name: null }))).toBe("toolCall");
|
||||
});
|
||||
|
||||
it("ignores empty deltas and usage, status, or metadata events", () => {
|
||||
for (const candidate of [
|
||||
event("item.delta", { kind: "agentMessage", text: " " }),
|
||||
event("item.delta", { kind: "reasoning", delta: "" }),
|
||||
event("item.delta", { kind: "usage", text: "tokens" }),
|
||||
event("turn.started", { kind: "agentMessage", text: "message" }),
|
||||
event("tool.execution.started", { name: "Terminal" }),
|
||||
]) expect(firstMeaningfulAgentEventKind(candidate)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote controller restart adoption", () => {
|
||||
const identity = {
|
||||
runId: "run",
|
||||
@@ -9982,6 +10036,7 @@ describe("runnerd provider runtime wiring", () => {
|
||||
.mockImplementation((binding: Record<string, unknown>) =>
|
||||
Promise.resolve({ runId: binding.runId }),
|
||||
);
|
||||
const tracedRuns: string[] = [];
|
||||
let firstScopedRoot: string | undefined;
|
||||
for (const candidate of [
|
||||
first,
|
||||
@@ -10015,6 +10070,13 @@ describe("runnerd provider runtime wiring", () => {
|
||||
db: candidateDb,
|
||||
execution: candidate,
|
||||
runnerInstanceId: `runner-${candidate.binding.runId}`,
|
||||
toolTrace: {
|
||||
observe() {},
|
||||
async execute(_call, work) {
|
||||
tracedRuns.push(candidate.binding.runId);
|
||||
return await work();
|
||||
},
|
||||
},
|
||||
});
|
||||
state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!();
|
||||
if (candidate === first) {
|
||||
@@ -10067,6 +10129,7 @@ describe("runnerd provider runtime wiring", () => {
|
||||
await expect(
|
||||
continuationOptions.dynamicToolHandler!({}),
|
||||
).resolves.toEqual({ runId: continuation.binding.runId });
|
||||
expect(tracedRuns).toEqual([continuation.binding.runId, continuation.binding.runId]);
|
||||
} finally {
|
||||
if (previousStateDirectory === undefined) {
|
||||
delete process.env.PAPERCLIP_RUNNER_STATE_DIR;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createNativeToolTrace, type NativeToolTrace } from "./native-tool-trace.js";
|
||||
import { createNativeGitHubAccess, type NativeGitHubAccess } from "./native-github-access.js";
|
||||
import { resolveGitHubOperationCredentials } from "../github-operation-credentials.js";
|
||||
import { bindManagedNativeCredentialTurn, completeManagedNativeCredentialTurn } from "./managed-native-credentials.js";
|
||||
@@ -1410,7 +1411,7 @@ class SessionToolAuthorityEpoch {
|
||||
#authority: PaperclipRunnerToolAuthority;
|
||||
#revoked = false;
|
||||
|
||||
constructor(runId: string, authority: PaperclipRunnerToolAuthority) {
|
||||
constructor(runId: string, authority: PaperclipRunnerToolAuthority, private readonly toolTrace?: NativeToolTrace) {
|
||||
this.runId = runId;
|
||||
this.#authority = authority;
|
||||
}
|
||||
@@ -1432,7 +1433,9 @@ class SessionToolAuthorityEpoch {
|
||||
|
||||
async execute(call: Parameters<PaperclipRunnerToolAuthority["execute"]>[0]) {
|
||||
this.#assertCurrent();
|
||||
return await this.#authority.execute(call);
|
||||
return this.toolTrace
|
||||
? await this.toolTrace.execute(call, () => this.#authority.execute(call))
|
||||
: await this.#authority.execute(call);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6131,6 +6134,42 @@ function record(value: unknown): Record<string, unknown> {
|
||||
: {};
|
||||
}
|
||||
|
||||
type FirstAgentEventKind =
|
||||
| "reasoning"
|
||||
| "agentMessage"
|
||||
| "toolCall"
|
||||
| "dynamicToolCall";
|
||||
|
||||
/** Returns the first provider activity worth measuring after turn.started. */
|
||||
export function firstMeaningfulAgentEventKind(
|
||||
event: Pick<PrpEvent, "eventType" | "payload">,
|
||||
): FirstAgentEventKind | null {
|
||||
const payload = record(event.payload);
|
||||
if (event.eventType === "tool.execution.started") {
|
||||
return typeof payload.executionId === "string" &&
|
||||
payload.executionId.trim()
|
||||
? "toolCall"
|
||||
: null;
|
||||
}
|
||||
if (
|
||||
event.eventType !== "item.started" &&
|
||||
event.eventType !== "item.delta" &&
|
||||
event.eventType !== "item.completed"
|
||||
) return null;
|
||||
const kind = payload.kind;
|
||||
if (
|
||||
kind !== "reasoning" &&
|
||||
kind !== "agentMessage" &&
|
||||
kind !== "toolCall" &&
|
||||
kind !== "dynamicToolCall"
|
||||
) return null;
|
||||
if (event.eventType !== "item.delta") return kind;
|
||||
const text = [payload.text, payload.delta, payload.content].find(
|
||||
(value): value is string => typeof value === "string" && value.trim().length > 0,
|
||||
);
|
||||
return text ? kind : null;
|
||||
}
|
||||
|
||||
export type NativeSessionSteeringState = {
|
||||
disposition: "available" | "unsupported" | "temporarily_unavailable";
|
||||
activeTurnId: string | null;
|
||||
@@ -7254,6 +7293,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
startedAtMs: preparationStarts.runStartedAtMs,
|
||||
onEvent: input.onEvent,
|
||||
});
|
||||
const toolTrace = createNativeToolTrace(trace);
|
||||
const taskPrepareScope = trace.start("task.prepare", {
|
||||
parentName: "task.run",
|
||||
startedAtMs: preparationStarts.preparationStartedAtMs,
|
||||
@@ -7643,6 +7683,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
},
|
||||
{
|
||||
onCommittedEvent: async (event) => {
|
||||
await toolTrace.observe(event);
|
||||
if (event.eventType === "item.completed" &&
|
||||
record(event.payload).kind === "agentMessage" &&
|
||||
record(event.payload).channel === "final") {
|
||||
@@ -7722,23 +7763,9 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
endedAtMs: milestoneAtMs,
|
||||
});
|
||||
}
|
||||
if (
|
||||
!firstAgentEventRecorded &&
|
||||
turnStartedAtMs !== null &&
|
||||
(event.eventType === "item.started" ||
|
||||
event.eventType === "item.completed")
|
||||
) {
|
||||
const payload = record(event.payload);
|
||||
const kind =
|
||||
typeof payload.kind === "string" ? payload.kind : "unknown";
|
||||
if (
|
||||
[
|
||||
"reasoning",
|
||||
"agentMessage",
|
||||
"toolCall",
|
||||
"dynamicToolCall",
|
||||
].includes(kind)
|
||||
) {
|
||||
if (!firstAgentEventRecorded && turnStartedAtMs !== null) {
|
||||
const kind = firstMeaningfulAgentEventKind(event);
|
||||
if (kind) {
|
||||
firstAgentEventRecorded = true;
|
||||
await trace.record({
|
||||
name: "provider.time_to_first_agent_event",
|
||||
@@ -8108,6 +8135,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
runnerInstanceId: effectiveRunnerInstanceId,
|
||||
durableEnvironmentLeaseId: durableRunnerBinding?.environmentLeaseId,
|
||||
trace,
|
||||
toolTrace,
|
||||
})
|
||||
: null;
|
||||
const remoteCleanupLease = input.runnerExecutionTarget?.kind === "remote" &&
|
||||
@@ -10332,6 +10360,7 @@ export async function createRunnerdBackend(input: {
|
||||
runnerRemoteCodexNpmSpec?: string | null;
|
||||
runnerRemoteProviderPackPath?: string | null;
|
||||
trace?: NativeRunTrace;
|
||||
toolTrace?: NativeToolTrace;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
stopTaskForReassignment?: (target: { companyId: string; issueId: string; agentId: string; runId: string | null }) => Promise<void>;
|
||||
enqueueWakeup?: (
|
||||
@@ -10455,6 +10484,7 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
const authorityEpoch = new SessionToolAuthorityEpoch(
|
||||
input.execution.binding.runId,
|
||||
authority,
|
||||
input.toolTrace,
|
||||
);
|
||||
let dynamicTools: Awaited<
|
||||
ReturnType<SessionToolAuthorityEpoch["definitions"]>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createNativeRunTrace } from "./native-run-trace.js";
|
||||
import { createNativeToolTrace } from "./native-tool-trace.js";
|
||||
|
||||
function fixture() {
|
||||
let now = 1_000;
|
||||
const spans: Record<string, unknown>[] = [];
|
||||
const trace = createNativeRunTrace({ runId: "test", startedAtMs: 1_000, onEvent: async (event) => { spans.push(event.payload as Record<string, unknown>); } });
|
||||
trace.start("agent.turn");
|
||||
return { spans, timing: createNativeToolTrace(trace, () => now), at: (time: number) => { now = time; } };
|
||||
}
|
||||
const event = (kind: string, id: string, time: number, inputUpdated = false) => ({ eventType: `tool.execution.${kind}`, emittedAt: new Date(time).toISOString(), payload: { executionId: id, name: "mcp__paperclip__call_api", inputUpdated } });
|
||||
|
||||
describe("native tool timing", () => {
|
||||
it("measures streamed input and server execution in their distinct ID namespaces", async () => {
|
||||
const { spans, timing, at } = fixture();
|
||||
await timing.observe(event("started", "toolu_hire", 1_000));
|
||||
await timing.observe(event("started", "toolu_hire", 2_000));
|
||||
await timing.observe(event("progressed", "toolu_hire", 2_100, true));
|
||||
await timing.observe(event("progressed", "toolu_hire", 11_568, true));
|
||||
at(11_596);
|
||||
expect(await timing.execute({ callId: "3", tool: "call_api" }, async () => { at(11_631); return "created"; })).toBe("created");
|
||||
await timing.observe(event("completed", "toolu_hire", 12_003));
|
||||
await timing.observe(event("completed", "toolu_hire", 12_004));
|
||||
expect(spans).toEqual([
|
||||
expect.objectContaining({ span: "tool.execute", durationMs: 35, operation: "call_api", outcome: "ok" }),
|
||||
expect.objectContaining({ span: "tool.request.input_stream", durationMs: 10_568, parentSpan: "agent.turn" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not invent input-stream duration without input updates or a valid announcement", async () => {
|
||||
const { spans, timing } = fixture();
|
||||
await timing.observe({ ...event("started", "bad", 1_000), emittedAt: "invalid" });
|
||||
await timing.observe(event("started", "future", 5_000));
|
||||
await timing.observe(event("progressed", "future", 2_000, true));
|
||||
await timing.observe(event("started", "status-only", 1_000));
|
||||
await timing.observe(event("progressed", "status-only", 2_000));
|
||||
for (const id of ["missing", "bad", "future", "status-only"]) await timing.observe(event("completed", id, 3_000));
|
||||
expect(spans).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("preserves failures, does not export private data, and discards old turns", async () => {
|
||||
const { spans, timing, at } = fixture();
|
||||
await timing.observe(event("started", "secret-call-id", 1_000));
|
||||
await timing.observe(event("progressed", "secret-call-id", 1_001, true));
|
||||
await timing.observe({ eventType: "turn.completed", emittedAt: new Date(1_001).toISOString(), payload: {} });
|
||||
await timing.observe(event("completed", "secret-call-id", 1_002));
|
||||
at(2_000);
|
||||
const failure = new Error("private error");
|
||||
await expect(timing.execute({ callId: "secret-call-id", tool: "private tool" }, async () => { at(2_020); throw failure; })).rejects.toBe(failure);
|
||||
expect(spans).toEqual([expect.objectContaining({ span: "tool.execute", durationMs: 20, outcome: "failed", operation: "other" })]);
|
||||
expect(JSON.stringify(spans)).not.toMatch(/secret-call-id|private/);
|
||||
});
|
||||
|
||||
it("keeps concurrent same-name provider tools separate without FIFO matching", async () => {
|
||||
const { spans, timing, at } = fixture();
|
||||
await timing.observe(event("started", "provider-a", 1_000));
|
||||
await timing.observe(event("started", "provider-b", 1_005));
|
||||
await timing.observe(event("progressed", "provider-b", 1_015, true));
|
||||
at(1_020);
|
||||
let complete!: () => void;
|
||||
const first = timing.execute({ callId: "mcp-a", tool: "call_api" }, () => new Promise<void>((resolve) => { complete = resolve; }));
|
||||
at(1_025);
|
||||
await timing.execute({ callId: "mcp-b", tool: "call_api" }, async () => { at(1_030); });
|
||||
await timing.observe(event("progressed", "provider-a", 1_031, true));
|
||||
await timing.observe(event("completed", "provider-b", 1_032));
|
||||
at(1_040);
|
||||
complete();
|
||||
await first;
|
||||
await timing.observe(event("completed", "provider-a", 1_045));
|
||||
expect(spans.filter((span) => span.span === "tool.execute").map((span) => span.durationMs)).toEqual([5, 20]);
|
||||
expect(spans.filter((span) => span.span === "tool.request.input_stream").map((span) => span.durationMs)).toEqual([10, 31]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { CAPABILITY_SEMANTIC_TOOL_CATALOG } from "../../vendor/paperclip-runner/index.js";
|
||||
import type { NativeRunTrace } from "./native-run-trace.js";
|
||||
|
||||
const SAFE_OPERATIONS = new Set<string>(CAPABILITY_SEMANTIC_TOOL_CATALOG.map((tool) => tool.operationId));
|
||||
function operation(value: unknown): string {
|
||||
const name = typeof value === "string" ? value.replace(/^mcp__paperclip__/, "") : "";
|
||||
return SAFE_OPERATIONS.has(name) ? name : "other";
|
||||
}
|
||||
|
||||
/** Provider stream IDs and MCP request IDs are separate namespaces. Never join by name or order. */
|
||||
export function createNativeToolTrace(trace: NativeRunTrace, now = Date.now) {
|
||||
const announced = new Map<string, { startedAtMs: number; lastInputAtMs?: number; operation: string }>();
|
||||
return {
|
||||
observe(event: { eventType: string; emittedAt: string; payload: unknown }): void | Promise<void> {
|
||||
if (["turn.started", "turn.completed", "turn.failed", "turn.cancelled"].includes(event.eventType)) {
|
||||
announced.clear();
|
||||
return;
|
||||
}
|
||||
if (!event.eventType.startsWith("tool.execution.")) return;
|
||||
const payload = event.payload as { executionId?: unknown; name?: unknown; inputUpdated?: unknown } | null;
|
||||
const id = payload?.executionId;
|
||||
const time = Date.parse(event.emittedAt);
|
||||
if (typeof id !== "string" || !id || !Number.isFinite(time)) return;
|
||||
if (event.eventType === "tool.execution.started") {
|
||||
if (announced.has(id)) return;
|
||||
if (announced.size >= 512) announced.delete(announced.keys().next().value!);
|
||||
announced.set(id, { startedAtMs: time, operation: operation(payload?.name) });
|
||||
return;
|
||||
}
|
||||
const pending = announced.get(id);
|
||||
if (!pending) return;
|
||||
if (payload?.inputUpdated === true && time >= pending.startedAtMs) {
|
||||
pending.lastInputAtMs = Math.max(pending.lastInputAtMs ?? pending.startedAtMs, time);
|
||||
}
|
||||
if (event.eventType !== "tool.execution.completed") return;
|
||||
announced.delete(id);
|
||||
if (pending.lastInputAtMs === undefined) return;
|
||||
// Measures observed argument streaming, not an inferred execution-start boundary.
|
||||
return trace.record({
|
||||
name: "tool.request.input_stream", parentName: "agent.turn",
|
||||
startedAtMs: pending.startedAtMs, endedAtMs: pending.lastInputAtMs,
|
||||
attributes: { operation: pending.operation },
|
||||
});
|
||||
},
|
||||
async execute<T>(call: { callId: string; tool: string }, work: () => Promise<T>): Promise<T> {
|
||||
const scope = trace.start("tool.execute", {
|
||||
parentName: "agent.turn", startedAtMs: now(), attributes: { operation: operation(call.tool) },
|
||||
});
|
||||
let outcome: "ok" | "failed" = "ok";
|
||||
try {
|
||||
return await trace.run(scope, work);
|
||||
} catch (error) {
|
||||
outcome = "failed";
|
||||
throw error;
|
||||
} finally {
|
||||
await trace.end(scope, { endedAtMs: now(), outcome });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
export type NativeToolTrace = ReturnType<typeof createNativeToolTrace>;
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as cloudIdentity from "../cloud-runtime-identity.js";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
activityLog,
|
||||
agents,
|
||||
@@ -248,6 +248,7 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
);
|
||||
expect(disabledNames).not.toContain("search_api");
|
||||
expect(disabledNames).not.toContain("call_api");
|
||||
expect(disabledNames).not.toContain("hire_agent");
|
||||
|
||||
process.env.PAPERCLIP_RUNNER_API_TOOLS_ENABLED = "true";
|
||||
process.env.PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS = companyId;
|
||||
@@ -259,8 +260,14 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
...requiredChatFileTools,
|
||||
"search_api",
|
||||
"call_api",
|
||||
"hire_agent",
|
||||
]),
|
||||
);
|
||||
const hire = createAuthority().definitions().find((tool) => tool.name === "hire_agent")!;
|
||||
const hireSchema = hire.inputSchema as { properties: Record<string, unknown> };
|
||||
expect(hireSchema.properties).toEqual(expect.objectContaining({ name: expect.any(Object), role: expect.any(Object) }));
|
||||
expect(hireSchema.properties).not.toHaveProperty("adapterConfig");
|
||||
expect(hireSchema.properties).not.toHaveProperty("env");
|
||||
} finally {
|
||||
if (previousEnabled === undefined) {
|
||||
delete process.env.PAPERCLIP_RUNNER_API_TOOLS_ENABLED;
|
||||
@@ -275,6 +282,71 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("dispatches hire_agent with fixed caller context and replays its API receipt", async () => {
|
||||
const previousEnabled = process.env.PAPERCLIP_RUNNER_API_TOOLS_ENABLED;
|
||||
const previousCompanies = process.env.PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS;
|
||||
const previousSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
process.env.PAPERCLIP_AGENT_JWT_SECRET = "hire-agent-test-secret";
|
||||
process.env.PAPERCLIP_RUNNER_API_TOOLS_ENABLED = "true";
|
||||
process.env.PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS = companyId;
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ id: randomUUID(), status: "active" }), {
|
||||
status: 201,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const authority = new PaperclipRunnerToolAuthority(db, {
|
||||
companyId,
|
||||
agentId,
|
||||
issueId,
|
||||
runId,
|
||||
apiUrl: "http://runner-test.invalid",
|
||||
});
|
||||
const call = {
|
||||
tool: "hire_agent",
|
||||
callId: "hire-agent-receipt",
|
||||
arguments: {
|
||||
name: "QA teammate",
|
||||
role: "qa",
|
||||
title: "Quality lead",
|
||||
capabilities: "Test native workflows",
|
||||
instructions: "Use the assigned workspace and report findings.",
|
||||
},
|
||||
} as const;
|
||||
const first = await authority.execute(call);
|
||||
expect(first).toMatchObject({ ok: true, status: 201, apiOperationId: "POST /api/companies/{companyId}/agent-hires" });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [url, request] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(String(url)).toBe(`http://runner-test.invalid/api/companies/${companyId}/agent-hires`);
|
||||
const body = JSON.parse(String(request.body));
|
||||
expect(body).toMatchObject({
|
||||
name: "QA teammate",
|
||||
role: "qa",
|
||||
title: "Quality lead",
|
||||
capabilities: "Test native workflows",
|
||||
adapterType: "paperclip_runner",
|
||||
inheritRuntimeFrom: "caller",
|
||||
reportsTo: agentId,
|
||||
sourceIssueId: issueId,
|
||||
instructionsBundle: { entryFile: "AGENTS.md", files: { "AGENTS.md": "Use the assigned workspace and report findings." } },
|
||||
});
|
||||
expect(body).not.toHaveProperty("adapterConfig");
|
||||
expect(body).not.toHaveProperty("runtimeConfig");
|
||||
expect(body).not.toHaveProperty("env");
|
||||
await expect(authority.execute(call)).resolves.toEqual(first);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
fetchMock.mockRestore();
|
||||
if (previousEnabled === undefined) delete process.env.PAPERCLIP_RUNNER_API_TOOLS_ENABLED;
|
||||
else process.env.PAPERCLIP_RUNNER_API_TOOLS_ENABLED = previousEnabled;
|
||||
if (previousCompanies === undefined) delete process.env.PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS;
|
||||
else process.env.PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS = previousCompanies;
|
||||
if (previousSecret === undefined) delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
else process.env.PAPERCLIP_AGENT_JWT_SECRET = previousSecret;
|
||||
}
|
||||
});
|
||||
|
||||
it("advertises structured human input in ask mode", () => {
|
||||
const authority = new PaperclipRunnerToolAuthority(db, {
|
||||
companyId,
|
||||
@@ -567,7 +639,7 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
const progressActivity = await db
|
||||
.select()
|
||||
.from(activityLog)
|
||||
.where(eq(activityLog.entityId, issueId));
|
||||
.where(and(eq(activityLog.entityId, issueId), eq(activityLog.action, "issue.comment_added")));
|
||||
expect(progressActivity).toHaveLength(1);
|
||||
expect(progressActivity[0]).toMatchObject({
|
||||
action: "issue.comment_added",
|
||||
|
||||
@@ -78,7 +78,7 @@ import {
|
||||
} from "./chat-attachment-read.js";
|
||||
|
||||
const IMPLEMENTED_OPERATIONS = new Set([
|
||||
"search_api", "call_api",
|
||||
"search_api", "call_api", "hire_agent",
|
||||
"get_task_context", "get_task_history", "search_tasks", "report_progress",
|
||||
"request_human_input",
|
||||
"create_skill", "create_task", "reassign_task", "set_dependencies", "create_project", "list_project_repositories", "list_projects", "register_deliverable",
|
||||
@@ -180,7 +180,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
this.binding.companyId,
|
||||
this.binding.apiToolsEnabled,
|
||||
) ||
|
||||
!["search_api", "call_api"].includes(descriptor.operationId)) &&
|
||||
!["search_api", "call_api", "hire_agent"].includes(descriptor.operationId)) &&
|
||||
descriptor.allowedModes.includes(workMode) &&
|
||||
(descriptor.operationId !== "register_deliverable" ||
|
||||
(Boolean(this.binding.workspaceRoot) &&
|
||||
@@ -290,7 +290,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
this.binding.companyId,
|
||||
this.binding.apiToolsEnabled,
|
||||
) &&
|
||||
["search_api", "call_api"].includes(call.tool)
|
||||
["search_api", "call_api", "hire_agent"].includes(call.tool)
|
||||
) {
|
||||
throw new Error("paperclip_runner_tool_not_advertised");
|
||||
}
|
||||
@@ -367,6 +367,30 @@ export class PaperclipRunnerToolAuthority {
|
||||
conversation: Boolean(context.issue.conversationAgentId) });
|
||||
}
|
||||
case "search_api": return searchRunnerApi(call.arguments);
|
||||
case "hire_agent": {
|
||||
const body: Record<string, unknown> = {
|
||||
name: requiredString(input.name),
|
||||
adapterType: "paperclip_runner",
|
||||
inheritRuntimeFrom: "caller",
|
||||
reportsTo: this.binding.agentId,
|
||||
sourceIssueId: this.binding.issueId,
|
||||
};
|
||||
if (input.role !== undefined) body.role = input.role;
|
||||
if (input.title !== undefined) body.title = input.title;
|
||||
if (input.capabilities !== undefined) body.capabilities = input.capabilities;
|
||||
if (typeof input.instructions === "string" && input.instructions.trim()) {
|
||||
body.instructionsBundle = {
|
||||
entryFile: "AGENTS.md",
|
||||
files: { "AGENTS.md": input.instructions },
|
||||
};
|
||||
}
|
||||
const { operationId, ...response } = record(await this.#callApi(call.callId, {
|
||||
operationId: "POST /api/companies/{companyId}/agent-hires",
|
||||
pathParams: { companyId: this.binding.companyId },
|
||||
body,
|
||||
}));
|
||||
return { ...response, apiOperationId: operationId };
|
||||
}
|
||||
case "call_api": {
|
||||
// PRP reserves operationId/callId for semantic result identity. The
|
||||
// HTTP operation is metadata, including in previously saved receipts;
|
||||
|
||||
@@ -43,6 +43,7 @@ function words(text: string): string[] {
|
||||
|
||||
function dedicatedTools(method: string, path: string): string[] {
|
||||
if (/\/companies\/\{[^}]+\}\/skills$/.test(path) && method === "POST") return ["create_skill"];
|
||||
if (/\/companies\/\{[^}]+\}\/agent-hires$/.test(path) && method === "POST") return ["hire_agent"];
|
||||
if (/\/projects$/.test(path)) return method === "GET" ? ["list_projects"] : method === "POST" ? ["create_project"] : [];
|
||||
if (/\/project-repositories$/.test(path) && method === "GET") return ["list_project_repositories"];
|
||||
if (/\/issues\/\{[^}]+\}\/comments$/.test(path)) return method === "GET" ? ["get_task_history"] : ["report_progress"];
|
||||
@@ -96,7 +97,9 @@ export function buildRunnerApiCatalog(document: Json = buildOpenApiDocument()):
|
||||
return descriptor ? [{ name, description: descriptor.description, supportedParameters: Object.keys((descriptor.inputSchema as Json).properties ?? {}) }] : [];
|
||||
}),
|
||||
runnerRestrictions: [...(restriction ? [restriction] : []), "Active run and assignment must remain authorized.", "Cannot replace checkout, completion, task status/ownership changes, approval decisions, or runner execution control. Dedicated tools retain their existing permissions."],
|
||||
dedicatedToolGuidance: "Use an available dedicated tool for its supported fields. Inspect that tool's advertised schema; call_api may be used for additional API fields, subject to lifecycle restrictions.",
|
||||
dedicatedToolGuidance: dedicatedTools(method, path).includes("hire_agent")
|
||||
? "Use hire_agent for native teammate identity and persona fields. It fixes the reportsTo and source task context and inherits the caller's native runtime; never use call_api to supply adapter, environment, or credential configuration."
|
||||
: "Use an available dedicated tool for its supported fields. Inspect that tool's advertised schema; call_api may be used for additional API fields, subject to lifecycle restrictions.",
|
||||
allowedModes: method === "GET" || method === "HEAD" ? ["standard", "ask", "planning", "skill_test"] : ["standard", "skill_test"],
|
||||
...(skillReference ? { skillReference } : {}),
|
||||
});
|
||||
|
||||
@@ -189,39 +189,6 @@ export const runnerApiReference: Record<string, { section: string; description?:
|
||||
"section": "Issues (Tasks)",
|
||||
"description": "Create issue-thread interaction (`suggest_tasks`, `ask_user_questions`, `request_confirmation`, `request_checkbox_confirmation`, `request_item_verdicts`)",
|
||||
"examples": [
|
||||
{
|
||||
"body": {
|
||||
"kind": "ask_user_questions",
|
||||
"idempotencyKey": "questions:{issueId}:responsibility:v1",
|
||||
"title": "Hire responsibility",
|
||||
"resolverPolicy": "human_only",
|
||||
"continuationPolicy": "wake_assignee",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"questions": [
|
||||
{
|
||||
"id": "responsibility",
|
||||
"prompt": "What should the new agent be responsible for?",
|
||||
"selectionMode": "single",
|
||||
"required": true,
|
||||
"allowOther": true,
|
||||
"options": [
|
||||
{
|
||||
"id": "research",
|
||||
"label": "Research",
|
||||
"description": "Find and summarize information."
|
||||
},
|
||||
{
|
||||
"id": "writing",
|
||||
"label": "Writing",
|
||||
"description": "Draft and edit content."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"kind": "ask_user_questions",
|
||||
@@ -260,6 +227,39 @@ export const runnerApiReference: Record<string, { section: string; description?:
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"kind": "ask_user_questions",
|
||||
"idempotencyKey": "questions:{issueId}:responsibility:v1",
|
||||
"title": "Hire responsibility",
|
||||
"resolverPolicy": "human_only",
|
||||
"continuationPolicy": "wake_assignee",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"questions": [
|
||||
{
|
||||
"id": "responsibility",
|
||||
"prompt": "What should the new agent be responsible for?",
|
||||
"selectionMode": "single",
|
||||
"required": true,
|
||||
"allowOther": true,
|
||||
"options": [
|
||||
{
|
||||
"id": "research",
|
||||
"label": "Research",
|
||||
"description": "Find and summarize information."
|
||||
},
|
||||
{
|
||||
"id": "writing",
|
||||
"label": "Writing",
|
||||
"description": "Draft and edit content."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"kind": "request_confirmation",
|
||||
@@ -671,6 +671,22 @@ export const runnerApiReference: Record<string, { section: string; description?:
|
||||
"section": "Approvals, Costs, Activity, Dashboard",
|
||||
"description": "Create hire request/agent draft",
|
||||
"examples": [
|
||||
{
|
||||
"body": {
|
||||
"name": "Marketing Analyst",
|
||||
"role": "researcher",
|
||||
"reportsTo": "{manager-agent-id}",
|
||||
"capabilities": "Market research, competitor analysis",
|
||||
"adapterType": "paperclip_runner",
|
||||
"inheritRuntimeFrom": "caller",
|
||||
"instructionsBundle": {
|
||||
"entryFile": "AGENTS.md",
|
||||
"files": {
|
||||
"AGENTS.md": "# Marketing Analyst\nResearch markets and competitors. Report findings with sources to your manager.\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"name": "Marketing Analyst",
|
||||
|
||||
@@ -286,6 +286,8 @@ else if(m.id!==undefined) send({id:m.id,result:{}});
|
||||
expect(treatment.issueId).toBe(baseline.issueId);
|
||||
expect((await treatment.snapshot()).issues[0].billingCode).toBeNull();
|
||||
const originalTools = (await baseline.authority.definitions()).map(tool => tool.name);
|
||||
expect((await treatment.authority.definitions()).filter(tool => !["call_api", "search_api"].includes(String(tool.name))).map(tool => tool.name)).toEqual(originalTools);
|
||||
const treatmentTools = (await treatment.authority.definitions()).map(tool => tool.name);
|
||||
expect(treatmentTools.filter(name => !originalTools.includes(name))).toEqual(["hire_agent", "search_api", "call_api"]);
|
||||
expect(treatmentTools.filter(name => !["hire_agent", "search_api", "call_api"].includes(String(name)))).toEqual(originalTools);
|
||||
});
|
||||
});
|
||||
@@ -52,6 +52,8 @@ describe("runner API catalog", () => {
|
||||
expect(runnerApiOperation(createProject).dedicatedTools).toEqual(["create_project"]);
|
||||
expect(runnerApiOperation(projects).dedicatedTools).toEqual(["list_projects"]);
|
||||
expect(runnerApiOperation("GET /api/companies/{companyId}/project-repositories").dedicatedTools).toEqual(["list_project_repositories"]);
|
||||
expect(runnerApiOperation("POST /api/companies/{companyId}/agent-hires").dedicatedTools).toEqual(["hire_agent"]);
|
||||
expect(runnerApiOperation("POST /api/companies/{companyId}/agent-hires").dedicatedToolGuidance).toContain("inherits the caller's native runtime");
|
||||
});
|
||||
it.each(runnerApiCatalog().filter(operation => operation.transport === "rest"))("resolves the catalog route $operationId inside the bound origin", operation => {
|
||||
const pathParams = Object.fromEntries(operation.parameters.filter(parameter => parameter.in === "path").map(parameter => [parameter.name, parameter.name === "companyId" ? context.companyId : "fixture-id"]));
|
||||
|
||||
@@ -860,6 +860,39 @@ Some actions require board approval. You cannot bypass these gates.
|
||||
|
||||
### Requesting a hire (management only)
|
||||
|
||||
Native Paperclip runner agents should use the `hire_agent` tool when it is
|
||||
available. Supply the new teammate's identity and responsibilities. Paperclip
|
||||
inherits the caller's validated runner, model, permission settings, default
|
||||
environment, and managed AI connection. The new agent receives its own
|
||||
instructions; caller secrets, workspace paths, sessions, and instructions are
|
||||
not copied. Existing hiring permissions and company approval policy still apply.
|
||||
|
||||
The equivalent native API request is:
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/agent-hires
|
||||
{
|
||||
"name": "Marketing Analyst",
|
||||
"role": "researcher",
|
||||
"reportsTo": "{manager-agent-id}",
|
||||
"capabilities": "Market research, competitor analysis",
|
||||
"adapterType": "paperclip_runner",
|
||||
"inheritRuntimeFrom": "caller",
|
||||
"instructionsBundle": {
|
||||
"entryFile": "AGENTS.md",
|
||||
"files": {
|
||||
"AGENTS.md": "# Marketing Analyst\nResearch markets and competitors. Report findings with sources to your manager.\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`inheritRuntimeFrom` is available only to a native runner agent in the same
|
||||
company. Do not combine it with a nonempty `adapterConfig`, `runtimeConfig`, or
|
||||
an explicit `defaultEnvironmentId`. Paperclip selects and validates those fields.
|
||||
For other adapters or a deliberately different runner configuration, use an
|
||||
explicit configuration, for example:
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/agent-hires
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user