mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
feat(runner): carry acknowledged Claude effort through native resume (#15994)
## Thinking Path > - Paperclip manages AI agents and their work. > - The native runner must carry each supported agent setting. > - Claude effort must reach its ACP session before a prompt and after resume. > - Installed Claude setup must find the owning server package without a qualification override. > - This PR adds those shared contracts and preserves historical readers. > - The Claude default change can then use the same runner path. ## Linked Issues or Issue Description Refs #15683. This prerequisite follows the actual Codex squash `adb69e8f09852c772d9901ae270e5b2d66e39c77`. Consumers: #15993 and #15997. Related scope: #15422, #13978 and #15224. ## What Changed - Add native-input v9 for Claude effort. Preserve v1–v8 readers, including v7 explicit home modes, v8 Muse, Dot and Grok Bot. - Preserve prepared task input and constraints through the newer contracts, including completion-only continuation without wrapping compact model input again. - Carry effort through daemon/sidecar admission; require provider acknowledgment before prompting and after resume. - Find installed server package authority through the verified canonical layout. Retain package-integrity, digest, architecture and no-follow checks. - Return Claude startup cancellation promptly while retaining unfinished settings writes and the lifetime lease through cleanup. Preserve merged Codex close/cleanup behavior. - Retain both independently authored historical Pi profile-24 declarations. Combined master/Claude source attestations use Pi profile 27; pending Copilot remains profile 22. This does not expand either provider's qualification boundary. - Avoid a spare verified executable snapshot when Claude effort control reuses its initialized provider. Preserve cold-control snapshot renewal and acknowledged effort. ## Verification - Reconciled with master `9179d8dcc3e908c217fde4f63c9d08ccf8132eeb`, retaining its task-loading, progress, skill-retention, permission and OpenRouter changes. - Final focused protocol/provider checks passed 155 tests (one skipped), the updated Pi attestation check passed 18 (one skipped), and 91 evaluation/catalog checks passed. Generated TypeScript/Rust profile checks and Rust formatting pass. The integrated consumer's TypeScript-only runtime build also passes. - Earlier branch checks passed 369 protocol tests (one skipped), 72 catalog checks and TypeScript checks/build; their source identities remain historical. No Rust compilation or Docker build is needed locally for this proof. - Current-head cloud CI is green. All three stack PRs have fresh Greptile 5/5 reviews and zero unresolved review threads. Required typecheck, test and build gates ran in cloud CI; Docker work stayed remote. Earlier failed checks remain in their original runs. - [Earlier four-target package qualification](https://github.com/paperclipai/paperclip/actions/runs/39267321383) passed on source `04e0202ca81387b17cbe7fd094a8338648356fe9`; it is not relabeled as proof for the final merge. Existing failures and unknown-cost holds remain preserved. - Current-head cloud CI is green. All three stack PRs have fresh Greptile 5/5 reviews and zero unresolved review threads. Required typecheck, test and build gates ran in cloud CI; Docker work stayed remote. Earlier failed checks remain in their original runs. - Latest-application manual continuation passed in 37.942 seconds on `695e5c005a85bddc8711d3fcddcaf6b7e1fb64d0`, with High effort, native Opus 5.5, Anthropic API attribution, the same resumed Claude session, an independently checked 635-byte file and persisted Done status. It uses the earlier verified Mac daemon; the preceding-source package qualification passed at `dca62c7aeb4b45f0dd91a8e18d386a16a2658ef8`. That package run predates the small TypeScript startup fix and is not relabeled as qualification for it. - Upgrade limitation: changed compiled sidecar identity rejected old durable launch state and recovery hit session-open bounds before eventually resuming. That attempt was operator-cancelled and retained; the successful manual continuation does not qualify seamless upgrades. - Local latency profile, Opus 5.5/High unchanged: three alternating real-ACP loopback samples per variant reduce median provider-ready time from 5.081s to 4.034s. The matched real API task startup improves from 5.533s to 4.348s, but total reply time changes from 17.933s to 20.221s because agent work varies. This establishes a startup saving of about one second, not a consistent total-reply speedup. A real candidate follow-up passed in 20.146s and resumed the same saved session. The loopback fixture makes no external model requests; failed initial probes remain retained. - The startup change passes 107 focused runtime/effort/lease tests, 53 profile checks (one skipped), TypeScript checking and the TypeScript-only runtime build. Native hire fixture checks pass 70 tests; their first local run retained a socket-hangup failure and stale OpenCode expectation, both accounted for before the passing rerun. Broader required checks run in cloud CI. ## Risks - Explicit Claude effort needs a daemon and sidecar that support input v9. Historical readers stay available; downstream builders and permission-pin transport checks must also use v9. - Source-bound profile changes may require reopening older Pi/Copilot sessions. Copilot remains pending and executable package closures remain unchanged. - This prerequisite changes shared runtime behavior; Claude default selection remains in #15993/#15997. No database migration or Codex default change is introduced. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, repository tools, code execution and browser testing. The exact serving model ID and context-window size are not exposed in this session. ## 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 (e.g. `docs/...`, `fix/...`) 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
c5226daf7a
commit
95672307f4
66 files changed
+1379
-181
No files matched your search
+12
-1
@@ -45,8 +45,9 @@ packages/paperclip-runner/test/fixtures/**
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v16-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v17-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v18-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v18-muse-historical-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v18-claude-qualification-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v19-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v18-muse-historical-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v19-muse-historical-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v20-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v21-identity.json
|
||||
@@ -61,6 +62,7 @@ packages/paperclip-runner/test-fixtures/pi-acp/**
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v23-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v24-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v25-identity.json
|
||||
!packages/paperclip-runner/docs/capability-contract.md
|
||||
# check:runner-workflow-traceability access()es every regression test the
|
||||
# stress-traceability spec names — those are src/**/*.test.ts files, so
|
||||
@@ -69,3 +71,12 @@ packages/paperclip-runner/test-fixtures/pi-acp/**
|
||||
!packages/paperclip-runner/src/**/*.test.tsx
|
||||
|
||||
packages/grok-acp/bin
|
||||
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v23-claude-qualification-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v26-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v19-claude-qualification-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v22-identity.json
|
||||
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v24-claude-historical-identity.json
|
||||
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v27-identity.json
|
||||
@@ -1,6 +1,29 @@
|
||||
# Pi rich ACP runtime
|
||||
|
||||
## Current integration — 2026-10-08
|
||||
## Current source integration — 2026-10-19
|
||||
|
||||
The composed Claude source declares **Pi profile 27**
|
||||
(`sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba`)
|
||||
and **Copilot profile 22**, still pending qualification
|
||||
(`sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2`).
|
||||
These identities bind the shared Claude effort and lifecycle changes plus
|
||||
master's OpenRouter attribution. Provider executable closures, model/effort
|
||||
options and qualification boundaries are unchanged. Current package and source
|
||||
checks remain separate from retained provider execution results, which keep
|
||||
their literal profile/source identities. Earlier immutable fixtures remain
|
||||
historical; they are not accepted as the current source identity.
|
||||
|
||||
## Historical source integration — 2026-10-12
|
||||
|
||||
Pi profile 25 (`sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a`)
|
||||
advances the shared sandbox source attestation after Claude settings initialization
|
||||
moved behind provider lifetime ownership. Profile 23 remains an immutable
|
||||
historical identity. Pi's provider executable closures, model/effort options and
|
||||
platform qualification are unchanged; retained live evidence keeps its literal
|
||||
profile/source identity. Copilot remains pending at profile 18. Current package
|
||||
and source controls are separate from those retained provider execution results.
|
||||
|
||||
## Historical integration — 2026-10-08
|
||||
|
||||
The experimental Runner admits **Pi profile 22** (`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`) and
|
||||
**Cursor profile 15**. Copilot remains pending at **profile 17**. Legacy
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
# Rich ACP integration and qualification report
|
||||
|
||||
## Current integration — 2026-10-08
|
||||
## Current source integration — 2026-10-19
|
||||
|
||||
The composed Claude source declares **Pi profile 27**
|
||||
(`sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba`)
|
||||
and **Copilot profile 22**, still pending qualification
|
||||
(`sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2`).
|
||||
These identities bind the shared Claude effort and lifecycle changes plus
|
||||
master's OpenRouter attribution. Provider executable closures, model/effort
|
||||
options and qualification boundaries are unchanged. Current package and source
|
||||
checks remain separate from retained provider execution results, which keep
|
||||
their literal profile/source identities. Earlier immutable fixtures remain
|
||||
historical; they are not accepted as the current source identity.
|
||||
|
||||
## Historical source integration — 2026-10-14
|
||||
|
||||
Pi profile 25 (`sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a`)
|
||||
advances the shared sandbox source attestation after Claude settings initialization
|
||||
moved behind provider lifetime ownership. Profile 23 remains an immutable
|
||||
historical identity. Pi's provider executable closures, model/effort options and
|
||||
platform qualification are unchanged; retained live evidence keeps its literal
|
||||
profile/source identity. Copilot remains pending at profile 19. Current package
|
||||
and source controls are separate from those retained provider execution results.
|
||||
|
||||
The actual Codex squash `adb69e8f09852c772d9901ae270e5b2d66e39c77`
|
||||
already owns Copilot profile 18 for the Grok descriptor schema. Its immutable
|
||||
fixture is retained. The combined Claude effort sources use profile 19
|
||||
(`sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321`),
|
||||
with executable, permission policy and pending qualification unchanged. The
|
||||
prior unmerged Claude profile-18 attestation is retained separately as
|
||||
`copilot-profile-v18-claude-qualification-identity.json`; earlier evidence keeps
|
||||
its literal source. Pi profile 25 and its source digest remain unchanged by
|
||||
this reconciliation. Existing settings and effort/session admission still need
|
||||
final packaged and real-provider qualification on the composed Claude source.
|
||||
|
||||
## Historical integration — 2026-10-08
|
||||
|
||||
The experimental Runner admits **Pi profile 22** (`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`) and
|
||||
**Cursor profile 15**. Copilot remains pending at **profile 17**. Legacy
|
||||
|
||||
@@ -240,6 +240,13 @@ ownership checks still apply. No agent gains session or recovery control.
|
||||
PostgreSQL regressions cover valid native/Legacy callbacks and rejected bindings;
|
||||
revised live execution and actor authorization remain `not-run`.
|
||||
|
||||
Scoped source-only evidence: [configuration changes](../../server/src/services/agent-configuration.ts)
|
||||
and [native execution/recovery](../../server/src/services/native-runtime/native-session-executor.ts)
|
||||
retain existing execution on unrelated edits and use recorded run configuration
|
||||
for active/recovered runs. Explicit runner changes use revisions and session
|
||||
invalidation. Verify saved configuration, current versus historical run identity,
|
||||
restart and follow-up without granting session-reset authority to an agent.
|
||||
|
||||
Scoped Evidence: `source-only` — the
|
||||
[native transport](../../packages/paperclip-runner/src/live/runnerd-codex-transport.ts)
|
||||
joins owned startup before certifying cleanup. Rejected duplicates cannot replace
|
||||
|
||||
@@ -148,6 +148,7 @@ state. Readiness observes shutdown; resumed startup cannot reactivate ingress
|
||||
after close. These are lifecycle controls, separate from installed provider proof.
|
||||
The real daemon readiness and recovery controls require their existing native
|
||||
fixtures in hosted CI; passing credential-free controls does not qualify them.
|
||||
A primary setup timeout remains a failure even when teardown also fails.
|
||||
|
||||
For remote setup shutdown, verify that each durable-state read respects the
|
||||
remaining suspension deadline. A late result cannot establish suspension or
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"pi": {
|
||||
"agentProfileVersion": 24,
|
||||
"agentProfileVersion": 27,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:347feab6eed4bc0f4ab099aae3292694e73d08e05148483ac612b0db958b0e39",
|
||||
"commandDigest": "sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"cursor": {
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 21,
|
||||
"agentProfileVersion": 22,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:1dad3d26d91e66c6f911dbfa43e7963f36c0bbf0d7709f4bbb568029a8b4b46a",
|
||||
"commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2",
|
||||
"requiresProviderPolicy": true,
|
||||
"qualificationStatus": "pending"
|
||||
},
|
||||
|
||||
@@ -154,7 +154,16 @@ policy in the closed provider configuration: `approvalPolicy` for Codex and
|
||||
provider-session identity, so an incompatible idle or recovered session is
|
||||
replaced on the next execution. An active turn is never mutated in place.
|
||||
|
||||
`paperclip.native-execution-input.v5` is the current input format. It retains
|
||||
`paperclip.native-execution-input.v5` remains the ordinary input format.
|
||||
Claude inputs with explicitly configured effort use v9 with the optional
|
||||
`provider.claudeEffort` field. V9 retains the compact v3 model envelope; effort
|
||||
is host configuration, never model-visible task text. V6 remains the separate
|
||||
OpenAI Dot format. Readers retain persisted v1-v8 contracts, including v7 home modes and v8 Muse. Claude effort is
|
||||
acknowledged on each ACP connection before prompting, bound into the session
|
||||
identity and recovery checkpoint, and cross-checked against the public provider
|
||||
descriptor. Missing effort retains the provider default; values are never clamped.
|
||||
|
||||
The v5 input format retains
|
||||
the v4 permission pinning and adds optional completion-source references;
|
||||
`paperclip.native-model-envelope.v3` is the corresponding explicit model
|
||||
projection. Readers continue to accept persisted v1-v4 inputs, and v5 readers
|
||||
@@ -295,3 +304,16 @@ not part of the contract. Native attempt output is
|
||||
fields but rejects unknown required versions and inconsistent terminal,
|
||||
semantic-denial, usage, or transcript facts. Full fields and the deterministic
|
||||
gate are exercised by the package-local deterministic conformance suite.
|
||||
|
||||
|
||||
## Claude effort after the Codex default merge
|
||||
|
||||
Native input v9 adds configured Claude effort while retaining v1–v8 readers.
|
||||
It carries the explicit home mode from v7; v8 Muse remains separate. The shared
|
||||
executor retains master's provider and cleanup behavior. Copilot's shared source
|
||||
attestation advances from pending profile 21 to 22 without replacing historical
|
||||
fixtures. Pi advances from master's profile 24 to 27 to bind the combined source,
|
||||
including master's OpenRouter attribution. The prior branch profile 24 is
|
||||
preserved separately from master's published profile 24.
|
||||
Provider executable bytes remain unchanged. These new source identities require
|
||||
fresh compatibility checks; earlier provider runs retain their original identities.
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"path": "schemas/provider-descriptor.schema.json",
|
||||
"id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json",
|
||||
"sha256": "8383e8e73b3f7a9bd2e863eab9bd524591d10f4f92c8014e63e5c2d25e2dc597"
|
||||
"sha256": "862c0c13f23564c3530cefcf01579b0535a565b919cc3dcff7bc1bee9a668baf"
|
||||
},
|
||||
{
|
||||
"path": "schemas/provider-event.schema.json",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "command": { "const": "session.open" } } },
|
||||
"then": { "properties": { "params": { "$ref": "#/$defs/sessionOpenThinkingLevel" } } }
|
||||
"then": { "properties": { "params": { "allOf": [{ "$ref": "#/$defs/sessionOpenThinkingLevel" }, { "$ref": "#/$defs/sessionOpenClaudeEffort" }] } } }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -89,6 +89,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"claudeEffort": { "enum": ["low", "medium", "high", "xhigh", "max"] },
|
||||
"sessionOpenClaudeEffort": {
|
||||
"type": "object",
|
||||
"properties": { "claudeEffort": { "$ref": "#/$defs/claudeEffort" } },
|
||||
"if": { "properties": { "agent": { "const": "claude" } }, "required": ["agent"] },
|
||||
"then": { "properties": { "expectedIdentity": {
|
||||
"type": ["object", "null"], "properties": { "claudeEffort": { "$ref": "#/$defs/claudeEffort" } }
|
||||
} } },
|
||||
"else": { "properties": {
|
||||
"claudeEffort": false,
|
||||
"expectedIdentity": { "type": ["object", "null"], "properties": { "claudeEffort": false } }
|
||||
} }
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"required": ["protocolVersion", "id", "ok"],
|
||||
|
||||
@@ -159,6 +159,15 @@
|
||||
"high",
|
||||
"max"
|
||||
]
|
||||
},
|
||||
"claudeEffort": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
@@ -182,6 +191,26 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"required": [
|
||||
"claudeEffort"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "acpx"
|
||||
},
|
||||
"agent": {
|
||||
"const": "claude"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -143,6 +143,8 @@ struct AcpxProviderDescriptor {
|
||||
mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pi_thinking_level: Option<PiThinkingLevel>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
claude_effort: Option<String>,
|
||||
permission_mode_pinned: bool,
|
||||
#[serde(default)]
|
||||
provider_policy: Option<AcpxProviderRuntimePolicy>,
|
||||
@@ -267,6 +269,10 @@ impl AcpxProviderDescriptor {
|
||||
|| self.model.len() > 240
|
||||
|| self.model.contains('\0')
|
||||
|| ((self.agent == "pi") != self.pi_thinking_level.is_some())
|
||||
|| self.claude_effort.as_ref().is_some_and(|effort| {
|
||||
self.agent != "claude"
|
||||
|| !matches!(effort.as_str(), "low" | "medium" | "high" | "xhigh" | "max")
|
||||
})
|
||||
|| self.mode.as_ref().is_some_and(|mode| {
|
||||
mode.trim().is_empty()
|
||||
|| mode.chars().count() > 240
|
||||
@@ -368,6 +374,7 @@ impl AcpxProviderDescriptor {
|
||||
permission_mode: self.permission_mode,
|
||||
mode: self.mode.clone(),
|
||||
pi_thinking_level: self.pi_thinking_level,
|
||||
claude_effort: self.claude_effort.clone(),
|
||||
permission_mode_pinned: self.permission_mode_pinned,
|
||||
provider_policy: self.provider_policy.clone(),
|
||||
system_instructions: self.instructions.clone(),
|
||||
@@ -472,6 +479,9 @@ impl AcpxProviderDescriptor {
|
||||
"acpxRecordId": identity.map(|value| value.acpx_record_id.as_str()),
|
||||
"permissionMode": self.permission_mode,
|
||||
});
|
||||
if let Some(effort) = &self.claude_effort {
|
||||
descriptor["claudeEffort"] = json!(effort);
|
||||
}
|
||||
if let Some(level) = self.pi_thinking_level {
|
||||
descriptor["piThinkingLevel"] = json!(level);
|
||||
}
|
||||
@@ -611,6 +621,7 @@ impl AcpxDurableState {
|
||||
if identity.profile_digest != self.descriptor.command_digest
|
||||
|| identity.mode != self.descriptor.mode
|
||||
|| identity.pi_thinking_level != self.descriptor.pi_thinking_level
|
||||
|| identity.claude_effort != self.descriptor.claude_effort
|
||||
{
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"ACPX durable identity no longer matches its qualified profile or provider mode",
|
||||
@@ -1295,6 +1306,9 @@ impl AcpxCommandExecutor {
|
||||
"sessionId": identity.agent_session_id,
|
||||
"processId": process_id,
|
||||
});
|
||||
if let Some(effort) = &identity.claude_effort {
|
||||
result["claudeEffort"] = json!(effort);
|
||||
}
|
||||
if let Some(level) = identity.pi_thinking_level {
|
||||
// This identity is admitted only after the sidecar's effective-mode ACK.
|
||||
result["piThinkingLevel"] = json!(level);
|
||||
@@ -2754,6 +2768,7 @@ mod tests {
|
||||
permission_mode: Some(descriptor.permission_mode),
|
||||
mode: descriptor.mode.clone(),
|
||||
pi_thinking_level: descriptor.pi_thinking_level,
|
||||
claude_effort: descriptor.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
let operations = Vec::new();
|
||||
@@ -3030,6 +3045,7 @@ mod tests {
|
||||
permission_mode: Some(AcpxPermissionMode::ApproveReads),
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
|
||||
@@ -3184,7 +3200,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pi_profile_matches_published_identity_and_rejects_prior_profiles() {
|
||||
let published: Value = serde_json::from_str(include_str!(
|
||||
"../../../../test-fixtures/pi-acp/profile-v24-identity.json"
|
||||
"../../../../test-fixtures/pi-acp/profile-v27-identity.json"
|
||||
))
|
||||
.unwrap();
|
||||
let mut value = descriptor("codex");
|
||||
@@ -3201,6 +3217,8 @@ mod tests {
|
||||
current.validate(&context()).unwrap();
|
||||
|
||||
for prior in [
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v24-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v23-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v15-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v16-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v17-identity.json"),
|
||||
@@ -3210,6 +3228,12 @@ mod tests {
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v21-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v22-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v23-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v24-identity.json"),
|
||||
include_str!(
|
||||
"../../../../test-fixtures/pi-acp/profile-v24-claude-historical-identity.json"
|
||||
),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v25-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v26-identity.json"),
|
||||
] {
|
||||
let prior: Value = serde_json::from_str(prior).unwrap();
|
||||
value["commandDigest"] = prior["commandDigest"].clone();
|
||||
@@ -3509,6 +3533,7 @@ mod tests {
|
||||
permission_mode: Some(original_descriptor.permission_mode),
|
||||
mode: original_descriptor.mode.clone(),
|
||||
pi_thinking_level: original_descriptor.pi_thinking_level,
|
||||
claude_effort: original_descriptor.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
let operations = Vec::new();
|
||||
@@ -3794,6 +3819,7 @@ mod tests {
|
||||
permission_mode: Some(original_descriptor.permission_mode),
|
||||
mode: original_descriptor.mode.clone(),
|
||||
pi_thinking_level: original_descriptor.pi_thinking_level,
|
||||
claude_effort: original_descriptor.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
let operations = Vec::new();
|
||||
@@ -4088,6 +4114,7 @@ mod tests {
|
||||
permission_mode: Some(descriptor.permission_mode),
|
||||
mode: descriptor.mode.clone(),
|
||||
pi_thinking_level: descriptor.pi_thinking_level,
|
||||
claude_effort: descriptor.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates,
|
||||
};
|
||||
let operations = Vec::new();
|
||||
@@ -4261,6 +4288,7 @@ mod tests {
|
||||
permission_mode: Some(provider_descriptor.permission_mode),
|
||||
mode: provider_descriptor.mode.clone(),
|
||||
pi_thinking_level: provider_descriptor.pi_thinking_level,
|
||||
claude_effort: provider_descriptor.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates,
|
||||
});
|
||||
state.provider_exit_unconfirmed = true;
|
||||
|
||||
@@ -53,6 +53,8 @@ struct PersistedAcpxProviderSessionIdentity {
|
||||
mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pi_thinking_level: Option<PiThinkingLevel>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
claude_effort: Option<String>,
|
||||
provider_lifetime_fence_candidates: [u16; 3],
|
||||
}
|
||||
|
||||
@@ -77,6 +79,7 @@ impl PersistedAcpxProviderSessionIdentity {
|
||||
permission_mode,
|
||||
mode: identity.mode,
|
||||
pi_thinking_level: identity.pi_thinking_level,
|
||||
claude_effort: identity.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates: identity.provider_lifetime_fence_candidates,
|
||||
})
|
||||
}
|
||||
@@ -95,6 +98,7 @@ impl PersistedAcpxProviderSessionIdentity {
|
||||
permission_mode: Some(self.permission_mode),
|
||||
mode: self.mode.clone(),
|
||||
pi_thinking_level: self.pi_thinking_level,
|
||||
claude_effort: self.claude_effort.clone(),
|
||||
provider_lifetime_fence_candidates: self.provider_lifetime_fence_candidates,
|
||||
}
|
||||
}
|
||||
@@ -117,6 +121,7 @@ impl AcpxSuspensionCheckpoint {
|
||||
|| identity.permission_mode != Some(config.permission_mode)
|
||||
|| identity.mode != config.mode
|
||||
|| identity.pi_thinking_level != config.pi_thinking_level
|
||||
|| identity.claude_effort != config.claude_effort
|
||||
|| config
|
||||
.expected_identity
|
||||
.as_ref()
|
||||
|
||||
@@ -72,6 +72,8 @@ pub struct AcpxProviderSessionIdentity {
|
||||
pub mode: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pi_thinking_level: Option<PiThinkingLevel>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub claude_effort: Option<String>,
|
||||
pub provider_lifetime_fence_candidates: [u16; 3],
|
||||
}
|
||||
|
||||
@@ -94,6 +96,7 @@ pub struct AcpxProviderSessionConfig {
|
||||
pub permission_mode: AcpxPermissionMode,
|
||||
pub mode: Option<String>,
|
||||
pub pi_thinking_level: Option<PiThinkingLevel>,
|
||||
pub claude_effort: Option<String>,
|
||||
pub permission_mode_pinned: bool,
|
||||
pub provider_policy: Option<AcpxProviderRuntimePolicy>,
|
||||
pub system_instructions: String,
|
||||
@@ -109,6 +112,14 @@ impl AcpxProviderSessionConfig {
|
||||
LocalRunnerError::invalid("ACPX agent must name a known immutable profile")
|
||||
})?;
|
||||
validate_text(&self.model, MAX_MODEL_CHARS, "ACPX model")?;
|
||||
if self.claude_effort.as_ref().is_some_and(|effort| {
|
||||
self.agent != "claude"
|
||||
|| !matches!(effort.as_str(), "low" | "medium" | "high" | "xhigh" | "max")
|
||||
}) {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Claude effort must be a supported Claude-only value",
|
||||
));
|
||||
}
|
||||
if (self.agent == "pi") != self.pi_thinking_level.is_some() {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"ACPX Pi thinking level must be explicit for Pi and absent for other agents",
|
||||
@@ -188,6 +199,7 @@ impl AcpxProviderSessionConfig {
|
||||
|| expected_identity.permission_mode != Some(self.permission_mode)
|
||||
|| expected_identity.mode != self.mode
|
||||
|| expected_identity.pi_thinking_level != self.pi_thinking_level
|
||||
|| expected_identity.claude_effort != self.claude_effort
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"ACPX expected identity conflicts with the requested session",
|
||||
@@ -208,6 +220,13 @@ impl AcpxProviderSessionIdentity {
|
||||
if let Some(mode) = self.mode.as_deref() {
|
||||
validate_text(mode, MAX_ID_CHARS, "ACPX provider mode")?;
|
||||
}
|
||||
if self.claude_effort.as_ref().is_some_and(|effort| {
|
||||
!matches!(effort.as_str(), "low" | "medium" | "high" | "xhigh" | "max")
|
||||
}) {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"ACPX identity contains invalid Claude effort",
|
||||
));
|
||||
}
|
||||
for (value, label) in [
|
||||
(&self.normalized_session_id, "normalized session"),
|
||||
(&self.acpx_record_id, "record"),
|
||||
@@ -1313,6 +1332,9 @@ fn session_open_params(config: &AcpxProviderSessionConfig, sidecar_tools: &[Valu
|
||||
"tools": &sidecar_tools,
|
||||
"expectedIdentity": config.expected_identity,
|
||||
});
|
||||
if let Some(effort) = &config.claude_effort {
|
||||
params["claudeEffort"] = json!(effort);
|
||||
}
|
||||
if let Some(level) = config.pi_thinking_level {
|
||||
params["piThinkingLevel"] = json!(level);
|
||||
}
|
||||
@@ -1430,6 +1452,7 @@ fn verify_open_response(
|
||||
|| identity.permission_mode != Some(config.permission_mode)
|
||||
|| identity.mode != config.mode
|
||||
|| identity.pi_thinking_level != config.pi_thinking_level
|
||||
|| identity.claude_effort != config.claude_effort
|
||||
|| config
|
||||
.expected_identity
|
||||
.as_ref()
|
||||
@@ -1755,6 +1778,7 @@ mod mode_tests {
|
||||
permission_mode: AcpxPermissionMode::ApproveReads,
|
||||
mode: Some("plan".to_owned()),
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
permission_mode_pinned: true,
|
||||
provider_policy: Some(AcpxProviderRuntimePolicy { read_only: false }),
|
||||
system_instructions: String::new(),
|
||||
@@ -1782,6 +1806,7 @@ mod mode_tests {
|
||||
permission_mode: Some(AcpxPermissionMode::ApproveReads),
|
||||
mode: Some("plan".to_owned()),
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
}
|
||||
}
|
||||
@@ -1921,6 +1946,27 @@ mod mode_tests {
|
||||
identity
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_effort_is_explicit_claude_only_and_forwarded_without_clamping() {
|
||||
let mut cfg = config();
|
||||
cfg.agent = "claude".to_owned();
|
||||
for effort in ["low", "medium", "high", "xhigh", "max"] {
|
||||
cfg.claude_effort = Some(effort.to_owned());
|
||||
cfg.validate().unwrap();
|
||||
assert_eq!(
|
||||
session_open_params(&cfg, &[])["claudeEffort"],
|
||||
json!(effort)
|
||||
);
|
||||
}
|
||||
cfg.claude_effort = Some("ultra".to_owned());
|
||||
assert!(cfg.validate().is_err());
|
||||
cfg.claude_effort = Some("high".to_owned());
|
||||
cfg.agent = "codex".to_owned();
|
||||
assert!(cfg.validate().is_err());
|
||||
cfg.claude_effort = None;
|
||||
cfg.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_thinking_level_is_closed_explicit_and_pi_only() {
|
||||
for (name, level) in [
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
|
||||
agent_runtime_version: Some("1.0.0"),
|
||||
command_digest:
|
||||
"sha256:347feab6eed4bc0f4ab099aae3292694e73d08e05148483ac612b0db958b0e39",
|
||||
"sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"cursor" => AcpxReleaseProfile {
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:1dad3d26d91e66c6f911dbfa43e7963f36c0bbf0d7709f4bbb568029a8b4b46a",
|
||||
"sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
@@ -52,6 +52,7 @@ fn config(directory: &std::path::Path) -> AcpxProviderSessionConfig {
|
||||
permission_mode: AcpxPermissionMode::ApproveReads,
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
permission_mode_pinned: true,
|
||||
provider_policy: None,
|
||||
system_instructions: "Complete the supplied task.".to_owned(),
|
||||
@@ -80,6 +81,7 @@ fn identity() -> AcpxProviderSessionIdentity {
|
||||
permission_mode: Some(AcpxPermissionMode::ApproveReads),
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ fn config(mode: &str) -> AcpxProviderSessionConfig {
|
||||
permission_mode: AcpxPermissionMode::ApproveReads,
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
permission_mode_pinned: true,
|
||||
provider_policy: None,
|
||||
system_instructions: "Complete the supplied task.".to_owned(),
|
||||
|
||||
@@ -47,6 +47,7 @@ fn config(mode: &str) -> AcpxProviderSessionConfig {
|
||||
permission_mode: AcpxPermissionMode::ApproveReads,
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
permission_mode_pinned: true,
|
||||
provider_policy: None,
|
||||
system_instructions: "Complete the supplied task.".to_owned(),
|
||||
@@ -70,6 +71,7 @@ fn expected_identity() -> AcpxProviderSessionIdentity {
|
||||
permission_mode: Some(AcpxPermissionMode::ApproveReads),
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ fn config(mode: &str) -> AcpxProviderSessionConfig {
|
||||
permission_mode: AcpxPermissionMode::ApproveReads,
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
permission_mode_pinned: true,
|
||||
provider_policy: None,
|
||||
system_instructions: "Complete the supplied task.".to_owned(),
|
||||
|
||||
@@ -50,6 +50,7 @@ fn config(mode: &str) -> AcpxProviderSessionConfig {
|
||||
working_directory: std::env::temp_dir(),
|
||||
permission_mode: AcpxPermissionMode::ApproveReads,
|
||||
mode: None,
|
||||
claude_effort: None,
|
||||
pi_thinking_level: if mode.starts_with("controls") {
|
||||
Some(paperclip_runner_core::acpx_provider_session::PiThinkingLevel::Low)
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
|
||||
const CODEX_ACPX_DIGEST: &str =
|
||||
"sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc";
|
||||
const PI_ACPX_DIGEST: &str =
|
||||
"sha256:347feab6eed4bc0f4ab099aae3292694e73d08e05148483ac612b0db958b0e39";
|
||||
"sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba";
|
||||
|
||||
fn temporary_directory(label: &str) -> PathBuf {
|
||||
let nonce = SystemTime::now()
|
||||
|
||||
@@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract;
|
||||
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
|
||||
assert.equal(contract.acpxPatchSha256, createHash("sha256")
|
||||
.update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex"));
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v24-identity.json"], ["copilot", "test/fixtures/copilot-profile-v21-identity.json"]]) {
|
||||
for (const [agent, path] of [["pi", `test-fixtures/pi-acp/profile-v${manifest.profiles.pi.agentProfileVersion}-identity.json`], ["copilot", `test/fixtures/copilot-profile-v${manifest.profiles.copilot.agentProfileVersion}-identity.json`]]) {
|
||||
const identity = await readJson(path);
|
||||
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
|
||||
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
export interface CodexAcpxNativeSessionBackendOptions extends Omit<
|
||||
CodexAcpxDriverOptions,
|
||||
"model" | "permissionMode" | "mode" | "piThinkingLevel" | "systemInstructions" | "providerPolicy" | "runtimeContext"
|
||||
"model" | "permissionMode" | "mode" | "piThinkingLevel" | "claudeEffort" | "systemInstructions" | "providerPolicy" | "runtimeContext"
|
||||
> {}
|
||||
|
||||
export type AcpxNativeSessionBackendOptions =
|
||||
@@ -76,6 +76,7 @@ export function createAcpxNativeSessionBackend(
|
||||
permissionMode: input.provider.permissionMode ?? "approve-reads",
|
||||
mode: input.provider.mode,
|
||||
piThinkingLevel: input.provider.piThinkingLevel,
|
||||
claudeEffort: "claudeEffort" in input.provider ? input.provider.claudeEffort : undefined,
|
||||
systemInstructions,
|
||||
runtimeContext: "runtimeContext" in input ? input.runtimeContext : null,
|
||||
providerPolicy: { readOnly: "executionMode" in input && input.executionMode === "plan" },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describeRunnerdMuseDriver } from "../drivers/muse/runnerd-muse-driver.js";
|
||||
import { createCodexTaskEnvelope } from "../contracts/codex.js";
|
||||
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA_V9 } from "../contracts/native-execution.js";
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import type { PersistedHarnessSession } from "../contracts/harness-driver.js";
|
||||
import type {
|
||||
@@ -116,7 +116,9 @@ function createTransportBackedNativeSessionBackend(
|
||||
}
|
||||
const driverIdentity = transportDriverIdentity(input);
|
||||
const isCodex = input.provider.kind === "codex";
|
||||
const preparedContext = (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7);
|
||||
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V9;
|
||||
const supportsCollaborativePlanning =
|
||||
isCodex ||
|
||||
input.provider.kind === "opencode" ||
|
||||
|
||||
@@ -4,13 +4,13 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions } from "../contracts/runtime-context.js";
|
||||
|
||||
const contextRoots: string[] = [];
|
||||
afterEach(() => { for (const root of contextRoots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
|
||||
function preparedExecution(base: NativeExecutionInput): NativeExecutionInput {
|
||||
function preparedExecution(base: NativeExecutionInput, version: "v5" | "v7" | "v9" = "v5"): NativeExecutionInput {
|
||||
const root = mkdtempSync(join(tmpdir(), "paperclip-prepared-backend-"));
|
||||
contextRoots.push(root);
|
||||
writeFileSync(join(root, "AGENTS.md"), "Keep assigned instructions.");
|
||||
@@ -21,7 +21,7 @@ function preparedExecution(base: NativeExecutionInput): NativeExecutionInput {
|
||||
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
};
|
||||
return parseNativeExecutionInput({
|
||||
...base, schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null,
|
||||
...base, schema: `paperclip.native-execution-input.${version}`, ...(version === "v5" ? {} : { homeMode: "isolated" }), executionMode: "default", planningContext: null,
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
completionContract: { ...base.completionContract, contract: { ...base.completionContract.contract, criteria: [{ id: "objective", requirement: "Complete the task." }] } },
|
||||
});
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
WORKSPACE,
|
||||
} from "../drivers/codex/codex-app-server-driver.test-support.js";
|
||||
import { createNativeSessionBackend } from "../index.js";
|
||||
import { createCodexNativeSessionBackend } from "./codex-native-backend.js";
|
||||
import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } from "./codex-native-backend.js";
|
||||
import { createOpenCodeNativeSessionBackend } from "./opencode-native-backend.js";
|
||||
|
||||
function execution(
|
||||
@@ -597,8 +597,8 @@ describe("native backend factory", () => {
|
||||
).toThrow("Codex native backend requires provider kind codex");
|
||||
});
|
||||
|
||||
it("selects prepared transport only for v5 native input and retains prepared constraints", async () => {
|
||||
const input = preparedExecution(execution());
|
||||
it.each(["v5", "v7", "v9"] as const)("selects prepared transport for %s native input and retains prepared constraints", async version => {
|
||||
const input = preparedExecution(execution(), version);
|
||||
const transport = new FakeCodexTransport();
|
||||
const backend = createCodexNativeSessionBackend(input, {
|
||||
transportFactory: () => transport,
|
||||
@@ -626,8 +626,49 @@ describe("native backend factory", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("projects v5 OpenCode constraints to the prepared runtime boundary", () => {
|
||||
const input = preparedExecution(opencodeExecution());
|
||||
it("allows a completion-only turn after v9 Claude work without wrapping prepared input", async () => {
|
||||
const { qualificationModel: _model, reportedModelId: _reported, permissionPolicy: _policy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-opus-5-5");
|
||||
const input = preparedExecution({ ...acpxExecution("claude"), provider: {
|
||||
kind: "acpx", agent: "claude", model: "claude-opus-5-5", permissionMode: "approve-all", profile,
|
||||
} }, "v9");
|
||||
if (input.schema !== "paperclip.native-execution-input.v9" || input.provider.kind !== "acpx") throw new Error("invalid fixture");
|
||||
input.provider.claudeEffort = "high";
|
||||
const transport = new FakeCodexTransport("thread-1", "provider-session-1", {
|
||||
kind: "acpx", normalizedSessionId: "session", acpxRecordId: "fixture-record",
|
||||
backendSessionId: "fixture-backend", agentSessionId: "fixture-agent",
|
||||
profileDigest: `sha256:${"a".repeat(64)}`, workspaceDigest: `sha256:${"b".repeat(64)}`,
|
||||
requestedModel: input.provider.model, effectiveModel: input.provider.model, permissionMode: "approve-all",
|
||||
providerLifetimeFenceCandidates: [60001, 60002, 60003],
|
||||
});
|
||||
const backend = createRunnerdNativeSessionBackend(input, {
|
||||
transportFactory: () => transport,
|
||||
environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE },
|
||||
});
|
||||
const session = await backend.openSession({
|
||||
identity: { companyId: "company", agentId: "agent", runId: "run", sessionId: "session" },
|
||||
workingDirectory: WORKSPACE,
|
||||
});
|
||||
const first = JSON.stringify(buildNativeModelEnvelope(input));
|
||||
const completion = "Call paperclip_finish with the result of the work already completed.";
|
||||
try {
|
||||
for (const [index, text] of [first, completion].entries()) {
|
||||
transport.turnStartResponse = Promise.resolve({ turn: { id: `turn-${index + 1}`, status: "inProgress", items: [] } });
|
||||
const turn = await session.startTurn({ message: { role: "user", text } });
|
||||
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] } });
|
||||
for await (const event of session.events()) {
|
||||
if (["turn.completed", "turn.failed", "turn.cancelled"].includes(event.eventType)) break;
|
||||
}
|
||||
}
|
||||
const turns = transport.calls.filter(call => call.method === "turn/start");
|
||||
expect(turns.map(call => call.params.input)).toEqual([first, completion].map(text => [{ type: "text", text, text_elements: [] }]));
|
||||
expect(backend.preparedTaskConstraints).toEqual(expect.arrayContaining([expect.stringContaining("Obtain one accepted result")]));
|
||||
} finally {
|
||||
await session.close({ reason: "test-completed" });
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["v5", "v7", "v9"] as const)("projects %s OpenCode constraints to the prepared runtime boundary", version => {
|
||||
const input = preparedExecution(opencodeExecution(), version);
|
||||
const backend = createOpenCodeNativeSessionBackend(input, {
|
||||
runtimeDirectory: "/tmp/paperclip-opencode-prepared-factory",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createCodexTaskEnvelope } from "../contracts/codex.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA_V9 } from "../contracts/native-execution.js";
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import type { NativeSessionBackend } from "../contracts/native-session-backend.js";
|
||||
import { OpenCodeServerDriver, type OpenCodeServerDriverOptions } from "../drivers/opencode/opencode-server-driver.js";
|
||||
@@ -13,7 +13,9 @@ export function createOpenCodeNativeSessionBackend(
|
||||
if (input.provider.kind !== "opencode" || !input.provider.model) {
|
||||
throw new Error("OpenCode native backend requires a persisted OpenCode provider/model selection");
|
||||
}
|
||||
const preparedContext = (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7);
|
||||
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V9;
|
||||
const constraints = nativeTaskConstraints(input);
|
||||
|
||||
return new HarnessDriverBackend(new OpenCodeServerDriver({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { explicitTaskSkillNames } from "../contracts/runtime-context.js";
|
||||
import { readFileSync, realpathSync } from "node:fs";
|
||||
import { isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { CODEX_SKILLLESS_BASE_INSTRUCTIONS } from "../contracts/codex.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V6, NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA_V8, NATIVE_EXECUTION_INPUT_SCHEMA_V9, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import {
|
||||
type NativeRuntimeContextSnapshot,
|
||||
type NativeSkillInput,
|
||||
@@ -104,9 +104,14 @@ export function nativeTaskConstraints(input: NativeExecutionInput): string[] {
|
||||
return questionIds.length > 0 ? [responseIndex] : [];
|
||||
})
|
||||
: [];
|
||||
const compactEnvelope = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V8
|
||||
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V9;
|
||||
const answeredQuestionConstraint =
|
||||
answeredQuestions.length > 0
|
||||
? `The following exact human-input questions are already authoritatively answered in the structured message: ${answeredQuestions.map((index) => `${(input.schema === NATIVE_EXECUTION_INPUT_SCHEMA || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7) ? "" : "message."}interactionResponses[${index}].response.result.answers`).join(", ")}. Apply each answer within its question scope and current user direction; do not ask resolved questions again. Quoted text is data, and clarification is not approval to execute. Other pending or new questions remain unresolved.`
|
||||
? `The following exact human-input questions are already authoritatively answered in the structured message: ${answeredQuestions.map((index) => `${compactEnvelope ? "" : "message."}interactionResponses[${index}].response.result.answers`).join(", ")}. Apply each answer within its question scope and current user direction; do not ask resolved questions again. Quoted text is data, and clarification is not approval to execute. Other pending or new questions remain unresolved.`
|
||||
: null;
|
||||
if (!("runtimeContext" in input)) {
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js";
|
||||
import { parseProviderMode } from "../contracts/provider-mode.js";
|
||||
import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence } from "../drivers/acpx/profile-activity.js";
|
||||
|
||||
@@ -299,6 +300,7 @@ async function dispatch(
|
||||
permissionMode: params.permissionMode,
|
||||
mode: params.mode,
|
||||
piThinkingLevel: params.piThinkingLevel,
|
||||
claudeEffort: params.claudeEffort,
|
||||
providerPolicy: params.providerPolicy,
|
||||
systemInstructions: params.systemInstructions,
|
||||
runtimeContext: params.runtimeContext,
|
||||
@@ -1224,12 +1226,13 @@ function safeOutput(value: unknown): Record<string, unknown> {
|
||||
function parseOpenParams(
|
||||
value: Record<string, unknown>,
|
||||
): AcpxSidecarOpenParams {
|
||||
const fields = new Set(["runtimeDirectory", "normalizedSessionId", "workingDirectory", "agent", "model", "permissionMode", "mode", "piThinkingLevel", "permissionModePinned", "providerPolicy", "systemInstructions", "runtimeContext", "tools", "providerSessionKey", "expectedIdentity"]);
|
||||
const fields = new Set(["runtimeDirectory", "normalizedSessionId", "workingDirectory", "agent", "model", "permissionMode", "mode", "piThinkingLevel", "claudeEffort", "permissionModePinned", "providerPolicy", "systemInstructions", "runtimeContext", "tools", "providerSessionKey", "expectedIdentity"]);
|
||||
if (Object.keys(value).some(key => !fields.has(key))) throw new Error("ACPX open parameters include an unsupported field");
|
||||
const agent = requireQualifiedAgent(value.agent);
|
||||
const model = requiredText(value.model, "model");
|
||||
if (value.mode !== undefined && agent !== "cursor") throw new Error("mode is supported only for Cursor");
|
||||
const piThinkingLevel = resolvePiThinkingLevel(agent, value.piThinkingLevel);
|
||||
const claudeEffort = resolveClaudeEffort(agent, value.claudeEffort);
|
||||
resolveQualifiedAcpxProfile(agent, model);
|
||||
if (
|
||||
value.providerSessionKey !== undefined &&
|
||||
@@ -1251,6 +1254,7 @@ function parseOpenParams(
|
||||
permissionMode: requiredPermissionMode(value.permissionMode),
|
||||
...(value.mode === undefined ? {} : { mode: parseProviderMode(value.mode) }),
|
||||
...(piThinkingLevel ? { piThinkingLevel } : {}),
|
||||
...(claudeEffort ? { claudeEffort } : {}),
|
||||
permissionModePinned: value.permissionModePinned === true,
|
||||
...(value.providerPolicy == null ? {} : { providerPolicy: parseProviderPolicy(value.providerPolicy) }),
|
||||
systemInstructions: boundedText(
|
||||
@@ -1322,6 +1326,7 @@ function parseExpectedIdentity(value: unknown): AcpxExpectedSessionIdentity {
|
||||
: { permissionMode: requiredPermissionMode(input.permissionMode) }),
|
||||
...(input.mode === undefined ? {} : { mode: parseProviderMode(input.mode) }),
|
||||
...(input.piThinkingLevel === undefined ? {} : { piThinkingLevel: resolvePiThinkingLevel("pi", input.piThinkingLevel) }),
|
||||
...(input.claudeEffort === undefined ? {} : { claudeEffort: resolveClaudeEffort("claude", input.claudeEffort) }),
|
||||
providerLifetimeFenceCandidates: requiredFenceCandidates(
|
||||
input.providerLifetimeFenceCandidates,
|
||||
),
|
||||
|
||||
@@ -458,6 +458,7 @@ export interface AcpxSessionIdentity {
|
||||
/** Effective provider mode identifier, bound to the session identity. */
|
||||
mode?: string;
|
||||
piThinkingLevel?: "off" | "low" | "high" | "max";
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerLifetimeFenceCandidates: readonly [number, number, number];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { nativeTaskConstraints } from "../backends/runtime-context.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "../drivers/acpx/qualified-profiles.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildNativeModelEnvelope, parseNativeExecutionInput, NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V6, type NativeExecutionInputV1 } from "./native-execution.js";
|
||||
import { buildNativeModelEnvelope, parseNativeExecutionInput, NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V6, NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA_V9, type NativeExecutionInputV1 } from "./native-execution.js";
|
||||
import {
|
||||
NATIVE_RUNTIME_ASSET_SCHEMA,
|
||||
PAPERCLIP_EXECUTION_PROMPT,
|
||||
@@ -710,3 +711,45 @@ describe("native task context ownership", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Claude effort input v9", () => {
|
||||
const { permissionPolicy: _permission, qualificationModel: _model, reportedModelId: _reported, ...profile } = QUALIFIED_ACPX_PROFILES.claude;
|
||||
const digest = "0".repeat(64);
|
||||
const context = { prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null } };
|
||||
const current = { ...input, schema: NATIVE_EXECUTION_INPUT_SCHEMA_V9, homeMode: "isolated", executionMode: "default", planningContext: null,
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
session: { ...input.session, driverKind: "acpx_runtime" },
|
||||
provider: { kind: "acpx", agent: "claude", model: "claude-opus-5-5", permissionMode: "approve-all", profile } };
|
||||
it.each(["low", "medium", "high", "xhigh", "max"])("round trips explicit %s and retains the compact model envelope", claudeEffort => {
|
||||
const parsed = parseNativeExecutionInput({ ...current, provider: { ...current.provider, claudeEffort } });
|
||||
expect(parsed).toMatchObject({ schema: NATIVE_EXECUTION_INPUT_SCHEMA_V9, provider: { claudeEffort } });
|
||||
expect(buildNativeModelEnvelope(parsed).schema).toBe("paperclip.native-model-envelope.v3");
|
||||
expect(JSON.stringify(buildNativeModelEnvelope(parsed))).not.toContain("claudeEffort");
|
||||
});
|
||||
it("directs answered Claude v9 questions to the actual compact envelope on fresh and resumed turns", () => {
|
||||
const response = { interactionId: "answer-1", kind: "ask_user_questions", response: {
|
||||
status: "answered", result: { version: 1, answers: [{ questionId: "destination", optionIds: ["workspace"], otherText: "Use the task workspace." }] },
|
||||
} };
|
||||
const parsed = parseNativeExecutionInput({ ...current, provider: { ...current.provider, claudeEffort: "high" }, interactionResponses: [response] });
|
||||
const constraint = nativeTaskConstraints(parsed).find(value => value.includes("already authoritatively answered"));
|
||||
expect(constraint).toContain("interactionResponses[0].response.result.answers");
|
||||
expect(constraint).not.toContain("message.interactionResponses");
|
||||
expect(constraint).not.toContain("Use the task workspace.");
|
||||
for (const envelope of [buildNativeModelEnvelope(parsed), buildNativeModelEnvelope(parsed, { resumedSession: true })]) {
|
||||
expect(envelope).toHaveProperty("interactionResponses.0.response.result.answers", response.response.result.answers);
|
||||
expect(envelope).not.toHaveProperty("message");
|
||||
}
|
||||
});
|
||||
it("keeps historical readers closed and rejects aliases and foreign effort", () => {
|
||||
const { homeMode: _homeMode, ...historical } = current;
|
||||
for (const schema of ["paperclip.native-execution-input.v4", NATIVE_EXECUTION_INPUT_SCHEMA]) {
|
||||
expect(() => parseNativeExecutionInput({ ...historical, schema, provider: { ...current.provider, claudeEffort: "high" } })).toThrow("claudeEffort");
|
||||
expect(parseNativeExecutionInput({ ...historical, schema }).schema).toBe(schema);
|
||||
}
|
||||
expect(() => parseNativeExecutionInput({ ...current, provider: { ...current.provider, claudeEffort: "ultra" } })).toThrow("Claude effort");
|
||||
expect(() => parseNativeExecutionInput({ ...current, provider: { ...current.provider, agent: "codex", profile: QUALIFIED_ACPX_PROFILES.codex, claudeEffort: "high" } })).toThrow("Claude effort");
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js";
|
||||
import type { DotBindingSnapshot, MuseBindingSnapshot, ExternalBotProviderKind } from "./external-provider.js";
|
||||
import { QUALIFIED_ACPX_VERSION } from "../drivers/acpx/generated-profiles.js";
|
||||
import { isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js";
|
||||
@@ -9,6 +10,7 @@ import { explicitTaskSkillNames, parseNativeRuntimeContext, type NativeRuntimeCo
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V1 = "paperclip.native-execution-input.v1" as const;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V2 = "paperclip.native-execution-input.v2" as const;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V3 = "paperclip.native-execution-input.v3" as const;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V9 = "paperclip.native-execution-input.v9" as const;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V8 = "paperclip.native-execution-input.v8" as const;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V6 = "paperclip.native-execution-input.v6" as const;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA_V4 = "paperclip.native-execution-input.v4" as const;
|
||||
@@ -253,13 +255,20 @@ export interface NativeExecutionInputV7 extends Omit<NativeExecutionInputV5, "sc
|
||||
homeMode: NativeHomeMode;
|
||||
}
|
||||
|
||||
export type NativeProviderConfigV9 = Exclude<NativeProviderConfigV5, { kind: "acpx" }>
|
||||
| (Extract<NativeProviderConfigV5, { kind: "acpx" }> & { claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max" });
|
||||
export interface NativeExecutionInputV9 extends Omit<NativeExecutionInputV7, "schema" | "provider"> {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V9;
|
||||
provider: NativeProviderConfigV9;
|
||||
}
|
||||
|
||||
export interface NativeExecutionInputV8 extends Omit<NativeExecutionInputV6, "schema" | "provider" | "session"> {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V8;
|
||||
provider: { kind: "muse"; model: null; binding: MuseBindingSnapshot; bridgeRevision: "muse-v1" };
|
||||
session: Omit<NativeExecutionInputV6["session"], "driverKind"> & { driverKind: "muse_external" };
|
||||
}
|
||||
|
||||
export type NativeExecutionInput = NativeExecutionInputV1 | NativeExecutionInputV2 | NativeExecutionInputV3 | NativeExecutionInputV4 | NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 | NativeExecutionInputV8;
|
||||
export type NativeExecutionInput = NativeExecutionInputV1 | NativeExecutionInputV2 | NativeExecutionInputV3 | NativeExecutionInputV4 | NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 | NativeExecutionInputV8 | NativeExecutionInputV9;
|
||||
|
||||
/** The only task data that may enter provider-visible model input. */
|
||||
export interface NativeModelEnvelopeV1 {
|
||||
@@ -403,7 +412,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
const input = record(value, "input");
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V8) return parseMuseNativeExecutionInput(input);
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6) return parseExternalNativeExecutionInput(input);
|
||||
const isV7 = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
|
||||
const isV9 = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V9;
|
||||
const isV7 = isV9 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
|
||||
const isV5 = isV7 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
|
||||
const isV4 = isV5 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V4;
|
||||
const isV3 = isV4 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V3;
|
||||
@@ -556,7 +566,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
: provider.kind === "aws_agentcore"
|
||||
? ["kind", "model", "agentCoreProfile", "maxEstimatedSessionCostUsd", "invocationLimits"]
|
||||
: provider.kind === "acpx"
|
||||
? ["kind", "agent", "model", isV4 ? "permissionMode" : "permissionPolicy", "profile", "piThinkingLevel", ...(isV4 ? ["mode"] : [])]
|
||||
? ["kind", "agent", "model", isV4 ? "permissionMode" : "permissionPolicy", "profile", "piThinkingLevel", ...(isV4 ? ["mode"] : []), ...(isV9 ? ["claudeEffort"] : [])]
|
||||
: provider.kind === "codex" && isV4
|
||||
? ["kind", "model", "approvalPolicy", ...(isV5 ? ["reasoningEffort"] : [])]
|
||||
: provider.kind === "opencode" && isV4
|
||||
@@ -579,7 +589,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
if (provider.kind === "opencode" && (providerModel === null || !providerModel.includes("/"))) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for opencode in provider/model form");
|
||||
}
|
||||
let parsedProvider: NativeProviderConfig | NativeProviderConfigV5;
|
||||
let parsedProvider: NativeProviderConfigV9 | NativeProviderConfig | NativeProviderConfigV5;
|
||||
if (provider.kind === "claude_managed") {
|
||||
if (providerModel === null) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for claude_managed");
|
||||
@@ -695,6 +705,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
} else if (provider.permissionPolicy !== "interactive") {
|
||||
throw new NativeExecutionInputError("input.provider.permissionPolicy must be interactive");
|
||||
}
|
||||
const claudeEffort = resolveClaudeEffort(provider.agent, provider.claudeEffort);
|
||||
const profile = record(provider.profile, "input.provider.profile");
|
||||
exactKeys(profile, [
|
||||
"driverKind",
|
||||
@@ -733,6 +744,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
? { permissionMode: provider.permissionMode as NativeAcpxPermissionMode }
|
||||
: { permissionPolicy: "interactive" as const }),
|
||||
...(provider.mode === undefined ? {} : { mode: provider.mode as string }),
|
||||
...(claudeEffort === undefined ? {} : { claudeEffort }),
|
||||
...(provider.piThinkingLevel === undefined ? {} : { piThinkingLevel: provider.piThinkingLevel as "off" | "low" | "high" | "max" }),
|
||||
profile: {
|
||||
driverKind: "acpx_runtime",
|
||||
@@ -887,6 +899,11 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
provider: parsedProvider as NativeProviderConfigV4,
|
||||
};
|
||||
if (!isV5) return { ...withPermissions, schema: NATIVE_EXECUTION_INPUT_SCHEMA_V4 };
|
||||
if (isV9) return {
|
||||
...withPermissions, schema: NATIVE_EXECUTION_INPUT_SCHEMA_V9, homeMode: input.homeMode as NativeHomeMode,
|
||||
provider: parsedProvider as NativeProviderConfigV9,
|
||||
...(input.completionSources !== undefined ? { completionSources: parseCompletionSources(input.completionSources) } : {}),
|
||||
};
|
||||
return {
|
||||
...withPermissions,
|
||||
...(isV7 ? { schema: NATIVE_EXECUTION_INPUT_SCHEMA_V7, homeMode: input.homeMode as NativeHomeMode } : { schema: NATIVE_EXECUTION_INPUT_SCHEMA }),
|
||||
@@ -953,7 +970,7 @@ export function buildNativeModelEnvelope(input: NativeExecutionInput, options?:
|
||||
interactionResponses: structuredClone(input.interactionResponses),
|
||||
};
|
||||
}
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V8) {
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V8 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V9) {
|
||||
const sourceBinding = input.completionSources;
|
||||
const references = sourceBinding?.contractRevision === input.completionContract.contract.revision
|
||||
&& sourceBinding.promptSha256 === createHash("sha256").update(input.task.prompt).digest("hex")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { admittedClaudeEffort, createClaudeEffortAdmission, resolveClaudeEffort } from "./claude-effort.js";
|
||||
|
||||
const response = (value: string, sessionId?: string) => ({ ...(sessionId ? { sessionId } : {}), configOptions: [{ id: "effort", currentValue: value }] });
|
||||
function loaded(method = "session/new") {
|
||||
const admission = createClaudeEffortAdmission("high");
|
||||
const guard = admission.createGuard();
|
||||
guard("outbound", { id: 1, method, params: method === "session/new" ? {} : { sessionId: "s" } });
|
||||
guard("inbound", { id: 1, result: response("medium", method === "session/new" ? "s" : undefined) });
|
||||
return { admission, guard };
|
||||
}
|
||||
function select(guard: ReturnType<ReturnType<typeof createClaudeEffortAdmission>["createGuard"]>, acknowledged = "high") {
|
||||
guard("outbound", { id: 2, method: "session/set_config_option", params: { sessionId: "s", configId: "effort", value: "high" } });
|
||||
guard("inbound", { id: 2, result: response(acknowledged) });
|
||||
}
|
||||
describe("Claude effort admission", () => {
|
||||
it.each(["low", "medium", "high", "xhigh", "max"] as const)("accepts exact %s without aliases", value => {
|
||||
expect(resolveClaudeEffort("claude", value)).toBe(value);
|
||||
expect(admittedClaudeEffort("claude", value, { claudeEffort: value }, { claudeEffort: value })).toBe(value);
|
||||
});
|
||||
it("rejects unsupported, foreign and conflicting effort", () => {
|
||||
for (const value of ["ultra", "off", null, 2]) expect(() => resolveClaudeEffort("claude", value)).toThrow();
|
||||
expect(() => resolveClaudeEffort("pi", "high")).toThrow("only supported");
|
||||
expect(() => admittedClaudeEffort("claude", "high", { claudeEffort: "high" }, { claudeEffort: "medium" })).toThrow("differs");
|
||||
});
|
||||
it.each(["session/new", "session/load", "session/resume"])("requires acknowledged effort before prompting after %s", method => {
|
||||
const { admission, guard } = loaded(method);
|
||||
expect(admission.isReady()).toBe(false);
|
||||
select(guard);
|
||||
expect(admission.isReady()).toBe(true);
|
||||
expect(() => guard("outbound", { id: 3, method: "session/prompt", params: { sessionId: "s" } })).not.toThrow();
|
||||
});
|
||||
|
||||
it("rejects a resumed session with missing effort configuration", () => {
|
||||
const admission = createClaudeEffortAdmission("high");
|
||||
const guard = admission.createGuard();
|
||||
guard("outbound", { id: 1, method: "session/resume", params: { sessionId: "s" } });
|
||||
expect(() => guard("inbound", { id: 1, result: {} })).toThrow("missing or ambiguous");
|
||||
});
|
||||
it("blocks a prompt based only on a restored default", () => {
|
||||
const { guard } = loaded("session/load");
|
||||
expect(() => guard("outbound", { id: 3, method: "session/prompt", params: { sessionId: "s" } })).toThrow("does not acknowledge");
|
||||
});
|
||||
|
||||
it("requires the bound effort acknowledgement on each cold connection", () => {
|
||||
const { admission, guard } = loaded();
|
||||
select(guard);
|
||||
const resumed = admission.createGuard();
|
||||
resumed("outbound", { id: 4, method: "session/load", params: { sessionId: "s" } });
|
||||
expect(() => resumed("outbound", { id: 5, method: "session/prompt", params: { sessionId: "s" } })).toThrow("does not acknowledge");
|
||||
const fresh = loaded(); select(fresh.guard);
|
||||
const acknowledged = fresh.admission.createGuard();
|
||||
acknowledged("outbound", { id: 4, method: "session/load", params: { sessionId: "s" } });
|
||||
acknowledged("inbound", { id: 4, result: response("high") });
|
||||
expect(() => acknowledged("outbound", { id: 5, method: "session/prompt", params: { sessionId: "s" } })).not.toThrow();
|
||||
});
|
||||
it("rejects a provider that ignores the requested effort", () => {
|
||||
const { guard } = loaded();
|
||||
expect(() => select(guard, "medium")).toThrow("did not apply");
|
||||
});
|
||||
it("rejects drift and wrong-session prompts after admission", () => {
|
||||
const { guard } = loaded(); select(guard);
|
||||
expect(() => guard("inbound", { method: "session/update", params: { sessionId: "s", update: { sessionUpdate: "config_option_update", ...response("low") } } })).toThrow("drifted");
|
||||
const fresh = loaded(); select(fresh.guard);
|
||||
expect(() => fresh.guard("outbound", { id: 3, method: "session/prompt", params: { sessionId: "foreign" } })).toThrow("different session");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createAcpxConfigAdmission } from "./config-admission.js";
|
||||
|
||||
export type ClaudeEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export function resolveClaudeEffort(agent: string, value: unknown): ClaudeEffort | undefined {
|
||||
if (value === undefined || value === "") return undefined;
|
||||
if (agent !== "claude") throw new Error("Claude effort is only supported by Claude");
|
||||
if (value !== "low" && value !== "medium" && value !== "high" && value !== "xhigh" && value !== "max") throw new Error("Claude effort must be low, medium, high, xhigh, or max");
|
||||
return value;
|
||||
}
|
||||
export function createClaudeEffortAdmission(expected: ClaudeEffort) {
|
||||
return createAcpxConfigAdmission(expected, { label: "Claude effort", configId: "effort",
|
||||
supportedValues: ["default", "low", "medium", "high", "xhigh", "max"] });
|
||||
}
|
||||
export function admittedClaudeEffort(agent: string, requested: unknown, descriptor: Record<string, unknown>, identity: Record<string, unknown>): ClaudeEffort | undefined {
|
||||
const expected = resolveClaudeEffort(agent, requested);
|
||||
const effective = resolveClaudeEffort(agent, identity.claudeEffort);
|
||||
if (effective !== expected || descriptor.claudeEffort !== expected) throw new Error("Claude effort differs from the admitted configuration");
|
||||
return effective;
|
||||
}
|
||||
@@ -149,6 +149,7 @@ export interface CodexAcpxDriverOptions {
|
||||
permissionMode?: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: "off" | "low" | "high" | "max";
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerPolicy?: { readOnly: boolean };
|
||||
runtimeContext?: OpenAcpxRuntimeHostOptions["runtimeContext"];
|
||||
systemInstructions?: string;
|
||||
@@ -464,6 +465,7 @@ export class CodexAcpxDriver implements HarnessDriver {
|
||||
permissionMode: this.#options.permissionMode ?? "approve-all",
|
||||
mode: this.#options.mode,
|
||||
piThinkingLevel: this.#options.piThinkingLevel,
|
||||
claudeEffort: this.#options.claudeEffort,
|
||||
providerPolicy: this.#options.providerPolicy,
|
||||
runtimeContext: this.#options.runtimeContext,
|
||||
systemInstructions: this.#options.systemInstructions,
|
||||
@@ -1300,6 +1302,7 @@ class CodexAcpxSession implements HarnessSession {
|
||||
permissionMode: identity.permissionMode,
|
||||
...(identity.mode === undefined ? {} : { mode: identity.mode }),
|
||||
...(identity.piThinkingLevel === undefined ? {} : { piThinkingLevel: identity.piThinkingLevel }),
|
||||
...(identity.claudeEffort === undefined ? {} : { claudeEffort: identity.claudeEffort }),
|
||||
providerLifetimeFenceCandidates:
|
||||
identity.providerLifetimeFenceCandidates,
|
||||
},
|
||||
@@ -2163,6 +2166,7 @@ function validateRecoverySnapshot(snapshot: PersistedHarnessSession): void {
|
||||
identity.permissionMode,
|
||||
)) ||
|
||||
(identity.mode !== undefined && !isProviderMode(identity.mode)) ||
|
||||
(identity.claudeEffort !== undefined && !["low", "medium", "high", "xhigh", "max"].includes(identity.claudeEffort)) ||
|
||||
(identity.piThinkingLevel !== undefined && !["off", "low", "high", "max"].includes(identity.piThinkingLevel)) ||
|
||||
!validProviderLifetimeFenceCandidates(
|
||||
identity.providerLifetimeFenceCandidates,
|
||||
|
||||
@@ -34,6 +34,41 @@ const HANDLE: AcpRuntimeHandle = {
|
||||
};
|
||||
|
||||
describe("Codex ACPX runtime adapter", () => {
|
||||
it.each([false, true])("acknowledges configured Claude effort before admission (restoring=%s)", async restoring => {
|
||||
const runtime = fakeRuntime();
|
||||
const child = fakeChild();
|
||||
const options = openOptions({ spawn: () => child, close: async () => undefined });
|
||||
options.profile = { ...options.profile, agent: "claude" };
|
||||
options.claudeEffort = "high";
|
||||
options.restoringSession = restoring;
|
||||
options.refreshConsumedCommand = vi.fn(async () => undefined);
|
||||
let created!: AcpRuntimeOptions;
|
||||
const spawnProvider = () => created.spawnAgent!({ command: "ignored", args: [], options: {} });
|
||||
vi.mocked(runtime.ensureSession).mockImplementation(async () => {
|
||||
if (!restoring) spawnProvider();
|
||||
return HANDLE;
|
||||
});
|
||||
vi.mocked(runtime.setConfigOption).mockImplementation(async input => {
|
||||
if (restoring) spawnProvider();
|
||||
const guard = created.protocolGuardFactory!();
|
||||
guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } });
|
||||
guard("inbound", { id: 0, result: { configOptions: [{ id: "effort", currentValue: "default" }] } });
|
||||
guard("outbound", { id: 1, method: "session/set_config_option", params: { sessionId: "backend-1", configId: input.key, value: input.value } });
|
||||
guard("inbound", { id: 1, result: { configOptions: [{ id: "effort", currentValue: input.value }] } });
|
||||
});
|
||||
const port = await openCodexAcpxRuntime(options, {
|
||||
createRegistry: () => registry(), createStore: () => store(), createRuntime: value => { created = value; return runtime; },
|
||||
awaitProviderOwnership: providerOwnershipEstablished,
|
||||
awaitProviderExit: providerOwnershipEstablished,
|
||||
});
|
||||
expect(runtime.setConfigOption).toHaveBeenCalledExactlyOnceWith({ handle: HANDLE, key: "effort", value: "high" });
|
||||
expect(await port.identity()).toMatchObject({ claudeEffort: "high" });
|
||||
if (restoring) expect(options.refreshConsumedCommand).toHaveBeenCalledOnce();
|
||||
else expect(options.refreshConsumedCommand).not.toHaveBeenCalled();
|
||||
expect(runtime.startTurn).not.toHaveBeenCalled();
|
||||
await port.close({ reason: "test complete" });
|
||||
});
|
||||
|
||||
it("admits an exact Claude model through the real ACP without a user model allowlist", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "preconfigured-claude-model-"));
|
||||
const home = join(root, "home"), cwd = join(root, "workspace"), config = join(home, ".claude");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveClaudeEffort, createClaudeEffortAdmission } from "./claude-effort.js";
|
||||
import { createPiThinkingAdmission, resolvePiThinkingLevel } from "./pi-thinking.js";
|
||||
import { usesPreconfiguredHome } from "../preconfigured-home.js";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
@@ -308,6 +309,8 @@ export async function openQualifiedAcpxRuntime(
|
||||
const modeBinding = createAcpxModeBinding(options.profile.agent, options.mode);
|
||||
const selectedPiThinkingLevel = resolvePiThinkingLevel(options.profile.agent, options.piThinkingLevel);
|
||||
const piThinking = selectedPiThinkingLevel ? createPiThinkingAdmission(selectedPiThinkingLevel, { restoring: options.restoringSession }) : undefined;
|
||||
const selectedClaudeEffort = resolveClaudeEffort(options.profile.agent, options.claudeEffort);
|
||||
const claudeEffort = selectedClaudeEffort ? createClaudeEffortAdmission(selectedClaudeEffort) : undefined;
|
||||
const cursorInstructions = options.profile.agent === "cursor"
|
||||
? createCursorInstructionAdmission(options.systemInstructions)
|
||||
: null;
|
||||
@@ -319,6 +322,7 @@ export async function openQualifiedAcpxRuntime(
|
||||
return (direction: "inbound" | "outbound", message: unknown) => { instructions?.(direction, message); mode?.(direction, message); };
|
||||
} } : {}),
|
||||
...(piThinking ? { protocolGuardFactory: () => piThinking.createGuard() } : {}),
|
||||
...(claudeEffort ? { protocolGuardFactory: () => claudeEffort.createGuard() } : {}),
|
||||
sessionStore,
|
||||
agentRegistry: createRegistry({
|
||||
// Preserve Claude's ACP capability identity. This is metadata only: the
|
||||
@@ -544,6 +548,23 @@ export async function openQualifiedAcpxRuntime(
|
||||
await commandLaunches.refreshConsumedCommand?.();
|
||||
}
|
||||
return ensuredHandle;
|
||||
}) : claudeEffort ? ensuredSession.then(async (ensuredHandle) => {
|
||||
handle = ensuredHandle;
|
||||
options.signal?.throwIfAborted();
|
||||
if (!claudeEffort.isReady()) {
|
||||
if (!runtime.setConfigOption) throw new Error("Claude effort admission requires native configuration");
|
||||
const spawnsBeforeControl = commandLaunches.count;
|
||||
await runtime.setConfigOption({ handle: ensuredHandle, key: "effort", value: selectedClaudeEffort! });
|
||||
claudeEffort.assertReady();
|
||||
await children.verifyLifetimeOwnership();
|
||||
options.signal?.throwIfAborted();
|
||||
// Fresh sessions retain their initialized provider. Only a cold
|
||||
// control connection consumes another single-use launch snapshot.
|
||||
if (commandLaunches.count > spawnsBeforeControl) {
|
||||
await commandLaunches.refreshConsumedCommand?.();
|
||||
}
|
||||
}
|
||||
return ensuredHandle;
|
||||
}) : ensuredSession)
|
||||
.catch((error: unknown) => {
|
||||
throw classifySessionEnsureFailure(error);
|
||||
@@ -560,6 +581,7 @@ export async function openQualifiedAcpxRuntime(
|
||||
cursorInstructions?.assertReady();
|
||||
modeBinding?.assertReady();
|
||||
piThinking?.assertReady();
|
||||
claudeEffort?.assertReady();
|
||||
// A provider can answer only after the verified sentinel is armed, but do
|
||||
// not admit the session until the owner has observed that exact handoff.
|
||||
await children.verifyLifetimeOwnership();
|
||||
@@ -614,7 +636,7 @@ export async function openQualifiedAcpxRuntime(
|
||||
return runtimePort(
|
||||
runtime,
|
||||
handle,
|
||||
{ ...requireIdentity(handle), ...(modeBinding ? { mode: modeBinding.selectedMode } : {}), ...(selectedPiThinkingLevel ? { piThinkingLevel: selectedPiThinkingLevel } : {}) },
|
||||
{ ...requireIdentity(handle), ...(modeBinding ? { mode: modeBinding.selectedMode } : {}), ...(selectedPiThinkingLevel ? { piThinkingLevel: selectedPiThinkingLevel } : {}), ...(selectedClaudeEffort ? { claudeEffort: selectedClaudeEffort } : {}) },
|
||||
baseStore,
|
||||
children,
|
||||
runtimeCloseTimeoutMs,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
export function createAcpxConfigAdmission(expected: string, options: { label: string; configId: string; supportedValues: readonly string[]; restoring?: boolean; sessionMode?: boolean }) {
|
||||
type State = { sessionId: string | null; mode: string | null; error: Error | null };
|
||||
let current: State | null = null;
|
||||
let selectedOnce = options.restoring === true;
|
||||
const failure = (detail: string) => new Error(`${options.label} admission failed: ${detail}`);
|
||||
const assertReady = () => {
|
||||
if (current?.error) throw current.error;
|
||||
if (!selectedOnce || !current?.sessionId || current.mode !== expected) throw failure("native mode does not acknowledge the selected mode");
|
||||
};
|
||||
const readMode = (result: Record<string, unknown>, session: boolean): string => {
|
||||
const configs = Array.isArray(result.configOptions) ? result.configOptions.filter(value => object(value).id === options.configId) : [];
|
||||
if (configs.length !== 1) throw failure("native mode configuration is missing or ambiguous");
|
||||
const mode = object(configs[0]).currentValue;
|
||||
if (typeof mode !== "string" || !options.supportedValues.includes(mode)) throw failure("native mode is unsupported");
|
||||
if (session && options.sessionMode && object(result.modes).currentModeId !== mode) throw failure("native mode acknowledgements conflict");
|
||||
return mode;
|
||||
};
|
||||
return {
|
||||
assertReady,
|
||||
isReady() { assertNoError(); return selectedOnce && current?.sessionId != null && current.mode === expected; },
|
||||
createGuard() {
|
||||
const state: State = { sessionId: null, mode: null, error: null };
|
||||
current = state;
|
||||
const pending = new Map<string | number, { method: string; sessionId: unknown; selectsMode: boolean }>();
|
||||
return (direction: "inbound" | "outbound", value: unknown): void => {
|
||||
if (current !== state) throw failure("connection authority was replaced");
|
||||
assertNoError();
|
||||
const message = object(value);
|
||||
const params = object(message.params);
|
||||
try {
|
||||
if (direction === "outbound") {
|
||||
const method = message.method;
|
||||
if (method === "session/prompt") {
|
||||
assertReady();
|
||||
if (params.sessionId !== state.sessionId) throw failure("prompt belongs to a different session");
|
||||
} else if (method === "session/new" || method === "session/load" || method === "session/resume" || method === "session/set_config_option") {
|
||||
if (typeof message.id !== "string" && typeof message.id !== "number") throw failure("uncorrelated mode request");
|
||||
if (pending.has(message.id)) throw failure("duplicate mode request identity");
|
||||
if (pending.size) throw failure("overlapping native admission controls");
|
||||
if (method === "session/new" || method === "session/load" || method === "session/resume") {
|
||||
if (pending.size) throw failure("overlapping session admission");
|
||||
state.sessionId = null; state.mode = null;
|
||||
} else {
|
||||
if (!state.sessionId || params.sessionId !== state.sessionId) throw failure("configuration belongs to a different session");
|
||||
if (params.configId === options.configId && params.value !== expected) throw failure("configuration changes the selected mode");
|
||||
state.mode = null; // No prompt may race unacknowledged native configuration.
|
||||
}
|
||||
pending.set(message.id, { method, sessionId: params.sessionId, selectsMode: params.configId === options.configId });
|
||||
} else if (options.sessionMode && method === "session/set_mode") {
|
||||
if (!state.sessionId || params.sessionId !== state.sessionId || params.modeId !== expected) throw failure("mode control changes the selected mode");
|
||||
state.mode = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "session/update" && object(params.update).sessionUpdate === "config_option_update") {
|
||||
const update = object(params.update);
|
||||
if (Array.isArray(update.configOptions) && update.configOptions.some(value => object(value).id === options.configId)) {
|
||||
if (state.sessionId && params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session");
|
||||
const observed = readMode(update, false);
|
||||
if (selectedOnce && observed !== expected) throw failure("native mode drifted from the selected mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (options.sessionMode && message.method === "session/update" && object(params.update).sessionUpdate === "current_mode_update") {
|
||||
if (state.sessionId && params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session");
|
||||
if (selectedOnce && object(params.update).currentModeId !== expected) throw failure("native mode drifted from the selected mode");
|
||||
return; // Notifications alone never admit a prompt.
|
||||
}
|
||||
if (message.method !== undefined || (typeof message.id !== "string" && typeof message.id !== "number")) return;
|
||||
const request = pending.get(message.id);
|
||||
if (!request) return;
|
||||
pending.delete(message.id);
|
||||
if (message.error !== undefined) { state.mode = null; return; }
|
||||
const result = object(message.result);
|
||||
const session = request.method !== "session/set_config_option";
|
||||
const sessionId = request.method === "session/new" ? result.sessionId : request.sessionId;
|
||||
if (typeof sessionId !== "string" || !sessionId.trim()) throw failure("mode acknowledgement has no session identity");
|
||||
const mode = readMode(result, session);
|
||||
if (session && selectedOnce && mode !== expected) throw failure("restored native mode drifted from the selected mode");
|
||||
if (!session && (request.selectsMode || selectedOnce) && mode !== expected) throw failure("configuration did not apply the selected mode");
|
||||
if (!session && request.selectsMode) selectedOnce = true;
|
||||
state.sessionId = sessionId; state.mode = mode;
|
||||
} catch (error) {
|
||||
state.error = error instanceof Error ? error : failure("invalid mode acknowledgement");
|
||||
throw state.error;
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
function assertNoError() { if (current?.error) throw current.error; }
|
||||
}
|
||||
|
||||
@@ -211,8 +211,8 @@ describe("Copilot build-owned installation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v21 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v21-identity.json", import.meta.url), "utf8"));
|
||||
it("admits the current v22 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v22-identity.json", import.meta.url), "utf8"));
|
||||
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
|
||||
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
|
||||
@@ -243,7 +243,7 @@ describe("Copilot build-owned installation", () => {
|
||||
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
|
||||
});
|
||||
it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v21-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v22-identity.json", import.meta.url), "utf8"));
|
||||
identity.declaration[field] = "0".repeat(64);
|
||||
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
|
||||
|
||||
@@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 24,
|
||||
"agentProfileVersion": 27,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:347feab6eed4bc0f4ab099aae3292694e73d08e05148483ac612b0db958b0e39",
|
||||
"commandDigest": "sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"cursor": {
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 21,
|
||||
"agentProfileVersion": 22,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:1dad3d26d91e66c6f911dbfa43e7963f36c0bbf0d7709f4bbb568029a8b4b46a",
|
||||
"commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2",
|
||||
"qualificationStatus": "pending",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import type { Readable, Writable } from "node:stream";
|
||||
|
||||
import { resolveQualifiedAcpxProfile, type QualifiedAcpxProfile } from "./qualified-profiles.js";
|
||||
import { resolveInstalledProviderAuthority } from "./installed-provider-authority.js";
|
||||
import {
|
||||
VERIFIED_RUNTIME_EXECUTABLE_ENV,
|
||||
verifiedRuntimeExecutableHandoff,
|
||||
@@ -652,11 +653,13 @@ export async function verifyQualifiedAcpxInstallation(
|
||||
): Promise<VerifiedAcpxInstallation> {
|
||||
// Keep the selected package authority available when its bundled provider
|
||||
// wrapper delegates platform installation to the published server manifest.
|
||||
if ((profile.agent === "codex" || profile.agent === "claude") && resolvePackageJson === defaultPackageJsonResolver && process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT !== undefined) {
|
||||
resolvePackageJson = createAcpxPackageJsonResolver(
|
||||
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT,
|
||||
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST,
|
||||
);
|
||||
if ((profile.agent === "codex" || profile.agent === "claude") && resolvePackageJson === defaultPackageJsonResolver) {
|
||||
const boundRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT;
|
||||
const authority = boundRoot === undefined ? resolveInstalledProviderAuthority(import.meta.url) : {
|
||||
root: boundRoot,
|
||||
manifest: process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST,
|
||||
};
|
||||
if (authority) resolvePackageJson = createAcpxPackageJsonResolver(authority.root, authority.manifest);
|
||||
}
|
||||
const builtin = profile.agent === "grok";
|
||||
if (builtin && (profile.agentServerPackage !== "builtin:grok-acp" || profile.agentServerVersion !== "1" || profile.agentRuntimePackage !== "native:grok" || profile.agentRuntimeVersion !== "1.0.13")) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { mkdtemp, mkdir, writeFile, symlink, rm, realpath } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resolveInstalledProviderAuthority } from "./installed-provider-authority.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
|
||||
async function fixture(name = "@paperclipai/server") {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "installed-acpx-authority-")));
|
||||
roots.push(root);
|
||||
const directory = join(root, "dist/vendor/paperclip-runner/drivers/acpx");
|
||||
await mkdir(directory, { recursive: true });
|
||||
const module = join(directory, "installation-integrity.js");
|
||||
await writeFile(module, "export {};\n");
|
||||
const manifest = join(root, "package.json");
|
||||
await writeFile(manifest, JSON.stringify({ name }));
|
||||
return { root, module, manifest };
|
||||
}
|
||||
|
||||
describe("installed provider authority", () => {
|
||||
it("derives the owning server only from the normal installed verifier layout", async () => {
|
||||
const f = await fixture();
|
||||
expect(resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toEqual({ root: f.root, manifest: f.manifest });
|
||||
expect(resolveInstalledProviderAuthority(pathToFileURL(join(f.root, "src/drivers/acpx/installation-integrity.ts")).href)).toBeUndefined();
|
||||
expect(resolveInstalledProviderAuthority(pathToFileURL(join(f.root, "dist/drivers/acpx/installation-integrity.js")).href)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a substituted package owner", async () => {
|
||||
const f = await fixture("unrelated-package");
|
||||
expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow("owning server package");
|
||||
});
|
||||
|
||||
it("rejects a symlinked manifest before adopting its declarations", async () => {
|
||||
const f = await fixture();
|
||||
const other = join(f.root, "other.json");
|
||||
await writeFile(other, JSON.stringify({ name: "@paperclipai/server" }));
|
||||
await rm(f.manifest);
|
||||
await symlink(other, f.manifest);
|
||||
expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow();
|
||||
});
|
||||
|
||||
it("rejects a redirected verifier directory", async () => {
|
||||
const f = await fixture();
|
||||
const old = join(f.root, "dist/vendor/paperclip-runner/drivers/acpx");
|
||||
const other = join(f.root, "other");
|
||||
await mkdir(other);
|
||||
await writeFile(join(other, "installation-integrity.js"), "export {};\n");
|
||||
await rm(old, { recursive: true });
|
||||
await symlink(other, old);
|
||||
expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow("package layout");
|
||||
});
|
||||
|
||||
it.each(["", " ".repeat(256 * 1024 + 1)])("rejects unbounded or empty owner metadata", async contents => {
|
||||
const f = await fixture();
|
||||
await writeFile(f.manifest, contents);
|
||||
expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow("bounded regular file");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync } from "node:fs";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export function resolveInstalledProviderAuthority(moduleUrl: string): { root: string; manifest: string } | undefined {
|
||||
const url = new URL(moduleUrl);
|
||||
if (url.protocol !== "file:") return undefined;
|
||||
const modulePath = fileURLToPath(url);
|
||||
const suffix = "/dist/vendor/paperclip-runner/drivers/acpx/installation-integrity.js";
|
||||
if (!modulePath.endsWith(suffix)) return undefined;
|
||||
const root = modulePath.slice(0, -suffix.length);
|
||||
if (!isAbsolute(root) || resolve(root) !== root || realpathSync(root) !== root) {
|
||||
throw new Error("Installed ACPX server authority must be a canonical package directory");
|
||||
}
|
||||
for (const relative of ["", "dist", "dist/vendor", "dist/vendor/paperclip-runner", "dist/vendor/paperclip-runner/drivers", "dist/vendor/paperclip-runner/drivers/acpx"]) {
|
||||
const path = join(root, relative);
|
||||
const metadata = lstatSync(path);
|
||||
if (!metadata.isDirectory() || metadata.isSymbolicLink() || realpathSync(path) !== path) {
|
||||
throw new Error("Installed ACPX server authority escapes its package layout");
|
||||
}
|
||||
}
|
||||
const moduleMetadata = lstatSync(modulePath);
|
||||
if (!moduleMetadata.isFile() || moduleMetadata.isSymbolicLink() || realpathSync(modulePath) !== modulePath) {
|
||||
throw new Error("Installed ACPX verifier must remain in its package layout");
|
||||
}
|
||||
const manifest = join(root, "package.json");
|
||||
const fd = openSync(manifest, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
try {
|
||||
const before = fstatSync(fd, { bigint: true });
|
||||
if (!before.isFile() || before.size < 1n || before.size > 256n * 1024n) {
|
||||
throw new Error("Installed ACPX server manifest must be a bounded regular file");
|
||||
}
|
||||
const bytes = readFileSync(fd);
|
||||
const after = fstatSync(fd, { bigint: true });
|
||||
const named = lstatSync(manifest, { bigint: true });
|
||||
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
||||
|| before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs
|
||||
|| named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()
|
||||
|| bytes.length !== Number(before.size) || realpathSync(manifest) !== manifest) {
|
||||
throw new Error("Installed ACPX server manifest changed during admission");
|
||||
}
|
||||
const value = JSON.parse(bytes.toString("utf8"));
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)
|
||||
|| !Object.hasOwn(value, "name") || value.name !== "@paperclipai/server") {
|
||||
throw new Error("Installed ACPX verifier requires the owning server package");
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
return { root, manifest };
|
||||
}
|
||||
@@ -118,11 +118,40 @@ describe("Pi installation factory", () => {
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("retains master profile 23 and binds the combined Claude effort sources in profile 26", async () => {
|
||||
const read = async (version: number) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/profile-v${version}-identity.json`, import.meta.url), "utf8"));
|
||||
const prior = await read(23), current = await read(26);
|
||||
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort())
|
||||
.toEqual(["agentProfileVersion", "recoveryIdentitySourceSha256", "runtimeSandboxSourceSha256"]);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("retains both independently authored profile 24 declarations", async () => {
|
||||
const read = async (name: string) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/${name}`, import.meta.url), "utf8"));
|
||||
const published = await read("profile-v24-identity.json"), historical = await read("profile-v24-claude-historical-identity.json");
|
||||
expect(published.declaration.agentProfileVersion).toBe(24);
|
||||
expect(historical.declaration.agentProfileVersion).toBe(24);
|
||||
expect(published.commandDigest).not.toBe(historical.commandDigest);
|
||||
});
|
||||
|
||||
it("combines master OpenRouter attribution with the retained Claude source attestations in profile 27", async () => {
|
||||
const read = async (version: number) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/profile-v${version}-identity.json`, import.meta.url), "utf8"));
|
||||
const current = await read(27);
|
||||
for (const [version, expected] of [
|
||||
[24, ["agentProfileVersion", "recoveryIdentitySourceSha256", "runtimeSandboxSourceSha256"]],
|
||||
[26, ["agentProfileVersion", "credentialEnvironmentSourceSha256", "openRouterAttributionSourceSha256", "providerConfigurationSourceSha256"]],
|
||||
] as const) {
|
||||
const prior = await read(version);
|
||||
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort()).toEqual(expected);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
}
|
||||
});
|
||||
|
||||
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
|
||||
const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
||||
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value);
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v24-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v27-identity.json", import.meta.url), "utf8"));
|
||||
const declaration = identity.declaration;
|
||||
expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`);
|
||||
@@ -153,21 +182,21 @@ describe("Pi installation factory", () => {
|
||||
|
||||
it("rejects legacy profiles and tampered executable identities", () => {
|
||||
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 24");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 27");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 27");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 27");
|
||||
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createAcpxConfigAdmission } from "./config-admission.js";
|
||||
|
||||
export type PiThinkingLevel = "off" | "low" | "high" | "max";
|
||||
|
||||
export function resolvePiThinkingLevel(agent: string, mode: unknown): PiThinkingLevel | undefined {
|
||||
@@ -10,108 +12,13 @@ export function resolvePiThinkingLevel(agent: string, mode: unknown): PiThinking
|
||||
return mode;
|
||||
}
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
export function createPiThinkingAdmission(expected: PiThinkingLevel, options: { restoring?: boolean } = {}) {
|
||||
return createAcpxConfigAdmission(expected, { ...options, label: "Pi thinking mode", configId: "thought_level",
|
||||
supportedValues: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], sessionMode: true });
|
||||
}
|
||||
|
||||
/** Native mode is independent of permissions. Every connection must prove its
|
||||
* current mode before a prompt; a persisted ACPX preference is not proof. */
|
||||
export function createPiThinkingAdmission(expected: PiThinkingLevel, options: { restoring?: boolean } = {}) {
|
||||
type State = { sessionId: string | null; mode: string | null; error: Error | null };
|
||||
let current: State | null = null;
|
||||
let selectedOnce = options.restoring === true;
|
||||
const failure = (detail: string) => new Error(`Pi thinking mode admission failed: ${detail}`);
|
||||
const assertReady = () => {
|
||||
if (current?.error) throw current.error;
|
||||
if (!selectedOnce || !current?.sessionId || current.mode !== expected) throw failure("native mode does not acknowledge the selected mode");
|
||||
};
|
||||
const readMode = (result: Record<string, unknown>, session: boolean): string => {
|
||||
const configs = Array.isArray(result.configOptions) ? result.configOptions.filter(value => object(value).id === "thought_level") : [];
|
||||
if (configs.length !== 1) throw failure("native mode configuration is missing or ambiguous");
|
||||
const mode = object(configs[0]).currentValue;
|
||||
// Before exact model selection, Pi can legitimately report a different
|
||||
// native model's level. Observation alone never admits the first prompt.
|
||||
if (typeof mode !== "string" || !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(mode)) throw failure("native mode is unsupported");
|
||||
if (session && object(result.modes).currentModeId !== mode) throw failure("native mode acknowledgements conflict");
|
||||
return mode;
|
||||
};
|
||||
return {
|
||||
assertReady,
|
||||
isReady() { assertNoError(); return selectedOnce && current?.sessionId != null && current.mode === expected; },
|
||||
createGuard() {
|
||||
const state: State = { sessionId: null, mode: null, error: null };
|
||||
current = state;
|
||||
const pending = new Map<string | number, { method: string; sessionId: unknown; selectsMode: boolean }>();
|
||||
return (direction: "inbound" | "outbound", value: unknown): void => {
|
||||
if (current !== state) throw failure("connection authority was replaced");
|
||||
assertNoError();
|
||||
const message = object(value);
|
||||
const params = object(message.params);
|
||||
try {
|
||||
if (direction === "outbound") {
|
||||
const method = message.method;
|
||||
if (method === "session/prompt") {
|
||||
assertReady();
|
||||
if (params.sessionId !== state.sessionId) throw failure("prompt belongs to a different session");
|
||||
} else if (method === "session/new" || method === "session/load" || method === "session/set_config_option") {
|
||||
if (typeof message.id !== "string" && typeof message.id !== "number") throw failure("uncorrelated mode request");
|
||||
if (pending.has(message.id)) throw failure("duplicate mode request identity");
|
||||
if (pending.size) throw failure("overlapping native admission controls");
|
||||
if (method === "session/new" || method === "session/load") {
|
||||
if (pending.size) throw failure("overlapping session admission");
|
||||
state.sessionId = null; state.mode = null;
|
||||
} else {
|
||||
if (!state.sessionId || params.sessionId !== state.sessionId) throw failure("configuration belongs to a different session");
|
||||
if (params.configId === "thought_level" && params.value !== expected) throw failure("configuration changes the selected mode");
|
||||
state.mode = null; // No prompt may race unacknowledged native configuration.
|
||||
}
|
||||
pending.set(message.id, { method, sessionId: params.sessionId, selectsMode: params.configId === "thought_level" });
|
||||
} else if (method === "session/set_mode") {
|
||||
if (!state.sessionId || params.sessionId !== state.sessionId || params.modeId !== expected) throw failure("mode control changes the selected mode");
|
||||
// This method only returns {}; require a later config/session
|
||||
// acknowledgement rather than treating that empty result as proof.
|
||||
state.mode = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "session/update" && object(params.update).sessionUpdate === "config_option_update") {
|
||||
const update = object(params.update);
|
||||
// Config updates may contain only model options. A mode entry is
|
||||
// authoritative observation, but never an admission acknowledgement.
|
||||
if (Array.isArray(update.configOptions) && update.configOptions.some(value => object(value).id === "thought_level")) {
|
||||
if (state.sessionId && params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session");
|
||||
const observed = readMode(update, false);
|
||||
if (selectedOnce && observed !== expected) throw failure("native mode drifted from the selected mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "session/update" && object(params.update).sessionUpdate === "current_mode_update") {
|
||||
if (state.sessionId && params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session");
|
||||
if (selectedOnce && object(params.update).currentModeId !== expected) throw failure("native mode drifted from the selected mode");
|
||||
return; // Notifications alone never admit a prompt.
|
||||
}
|
||||
if (message.method !== undefined || (typeof message.id !== "string" && typeof message.id !== "number")) return;
|
||||
const request = pending.get(message.id);
|
||||
if (!request) return;
|
||||
pending.delete(message.id);
|
||||
if (message.error !== undefined) { state.mode = null; return; }
|
||||
const result = object(message.result);
|
||||
const session = request.method !== "session/set_config_option";
|
||||
const sessionId = request.method === "session/new" ? result.sessionId : request.sessionId;
|
||||
if (typeof sessionId !== "string" || !sessionId.trim()) throw failure("mode acknowledgement has no session identity");
|
||||
const mode = readMode(result, session);
|
||||
if (session && selectedOnce && mode !== expected) throw failure("restored native mode drifted from the selected mode");
|
||||
if (!session && (request.selectsMode || selectedOnce) && mode !== expected) throw failure("configuration did not apply the selected mode");
|
||||
if (!session && request.selectsMode) selectedOnce = true;
|
||||
state.sessionId = sessionId; state.mode = mode;
|
||||
} catch (error) {
|
||||
state.error = error instanceof Error ? error : failure("invalid mode acknowledgement");
|
||||
throw state.error;
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
function assertNoError() { if (current?.error) throw current.error; }
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
/** Cross-check independently returned durable identity and public descriptor. */
|
||||
|
||||
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
|
||||
/** Decodable historical profile revisions, not permission to launch them.
|
||||
* Exact current profile/package/digest admission is checked separately. */
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23;
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26;
|
||||
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
|
||||
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
|
||||
@@ -77,6 +77,21 @@ describe("ACPX recovery identity", () => {
|
||||
expect(customMode.mode).toBe("provider-custom-mode");
|
||||
});
|
||||
|
||||
it("binds Claude effort to recovery and rejects changed or omitted effort", async () => {
|
||||
const fixture = await recoveryFixture();
|
||||
const profile = resolveQualifiedAcpxProfile("claude", "claude-opus-5-5");
|
||||
const input = { ...fixture.input, profile, requestedModel: profile.qualificationModel, claudeEffort: "high" as const };
|
||||
const high = await createAcpxRecoveryBinding(input);
|
||||
const low = await createAcpxRecoveryBinding({ ...input, claudeEffort: "low" });
|
||||
expect(low.profileSessionKey).not.toBe(high.profileSessionKey);
|
||||
const expected = { ...fixture.expected, profileDigest: high.commandDigest, requestedModel: high.requestedModel, effectiveModel: high.effectiveModel, claudeEffort: "high" as const };
|
||||
const saved = createAcpxIdentityRecord(expected, high);
|
||||
expect(acpxProviderSessionIdentity(saved, high).claudeEffort).toBe("high");
|
||||
expect(() => verifyExpectedAcpxIdentity({ ...expected, claudeEffort: undefined }, high, saved)).toThrow("immutable");
|
||||
expect(() => verifyExpectedAcpxIdentity(expected, high, { ...saved, claudeEffort: undefined })).toThrow("persisted");
|
||||
expect(() => verifyExpectedAcpxIdentity(expected, low, saved)).toThrow("immutable");
|
||||
});
|
||||
|
||||
it("binds Pi thinking mode and rejects old or mismatched warm identities", async () => {
|
||||
const fixture = await recoveryFixture();
|
||||
const profile = resolveQualifiedAcpxProfile("pi", "openrouter/deepseek/deepseek-v4-flash-0731");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveClaudeEffort } from "./claude-effort.js";
|
||||
import { parseProviderMode } from "../../contracts/provider-mode.js";
|
||||
import { resolvePiThinkingLevel, type PiThinkingLevel } from "./pi-thinking.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -23,6 +24,7 @@ export interface AcpxRecoveryBinding {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
profileSessionKey: string;
|
||||
}
|
||||
|
||||
@@ -39,6 +41,7 @@ export interface AcpxIdentityRecord {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerLifetimeFenceCandidates: readonly [number, number, number];
|
||||
}
|
||||
|
||||
@@ -51,6 +54,7 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerConfigurationDigest?: string;
|
||||
homeBinding?: Readonly<Record<string, string>>;
|
||||
providerPolicy?: { readOnly: boolean; readRoots?: readonly string[]; protectedPaths?: readonly string[] };
|
||||
@@ -58,6 +62,7 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
validateIdentity(input.normalizedSessionId, "normalized session");
|
||||
const mode = parseProviderMode(input.mode);
|
||||
const piThinkingLevel = resolvePiThinkingLevel(input.profile.agent, input.piThinkingLevel);
|
||||
const claudeEffort = resolveClaudeEffort(input.profile.agent, input.claudeEffort);
|
||||
if (input.providerPolicy !== undefined && typeof input.providerPolicy.readOnly !== "boolean") throw new Error("ACPX recovery requires a valid task execution policy");
|
||||
if (input.requestedModel !== input.profile.qualificationModel) {
|
||||
throw new Error("ACPX recovery requested model does not match its admitted profile");
|
||||
@@ -106,6 +111,7 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
permissionMode: input.permissionMode,
|
||||
...(mode ? { mode } : {}),
|
||||
...(piThinkingLevel ? { piThinkingLevel } : {}),
|
||||
...(claudeEffort ? { claudeEffort } : {}),
|
||||
}),
|
||||
).replace("sha256:", "paperclip-");
|
||||
return {
|
||||
@@ -120,6 +126,7 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
permissionMode: input.permissionMode,
|
||||
...(mode ? { mode } : {}),
|
||||
...(piThinkingLevel ? { piThinkingLevel } : {}),
|
||||
...(claudeEffort ? { claudeEffort } : {}),
|
||||
profileSessionKey,
|
||||
};
|
||||
}
|
||||
@@ -142,6 +149,7 @@ export function createAcpxIdentityRecord(
|
||||
permissionMode: binding.permissionMode,
|
||||
...(binding.mode ? { mode: binding.mode } : {}),
|
||||
...(binding.piThinkingLevel ? { piThinkingLevel: binding.piThinkingLevel } : {}),
|
||||
...(binding.claudeEffort ? { claudeEffort: binding.claudeEffort } : {}),
|
||||
providerLifetimeFenceCandidates: Object.freeze([
|
||||
...expected.providerLifetimeFenceCandidates,
|
||||
]) as readonly [number, number, number],
|
||||
@@ -169,6 +177,7 @@ export function acpxProviderSessionIdentity(
|
||||
permissionMode: record.permissionMode,
|
||||
...(record.mode ? { mode: record.mode } : {}),
|
||||
...(record.piThinkingLevel ? { piThinkingLevel: record.piThinkingLevel } : {}),
|
||||
...(record.claudeEffort ? { claudeEffort: record.claudeEffort } : {}),
|
||||
providerLifetimeFenceCandidates: record.providerLifetimeFenceCandidates,
|
||||
};
|
||||
verifyExpectedAcpxIdentity(identity, binding, record);
|
||||
@@ -195,7 +204,8 @@ export function verifyExpectedAcpxIdentity(
|
||||
expected.effectiveModel !== binding.effectiveModel ||
|
||||
expected.permissionMode !== binding.permissionMode ||
|
||||
expected.mode !== binding.mode ||
|
||||
expected.piThinkingLevel !== binding.piThinkingLevel
|
||||
expected.piThinkingLevel !== binding.piThinkingLevel ||
|
||||
expected.claudeEffort !== binding.claudeEffort
|
||||
) {
|
||||
throw new Error(
|
||||
"ACPX recovery identity conflicts with the immutable session configuration",
|
||||
@@ -216,6 +226,7 @@ export function verifyExpectedAcpxIdentity(
|
||||
record.permissionMode !== binding.permissionMode ||
|
||||
record.mode !== binding.mode ||
|
||||
record.piThinkingLevel !== binding.piThinkingLevel ||
|
||||
record.claudeEffort !== binding.claudeEffort ||
|
||||
!sameFenceCandidates(
|
||||
record.providerLifetimeFenceCandidates,
|
||||
expected.providerLifetimeFenceCandidates,
|
||||
@@ -242,6 +253,7 @@ function parsePersistedRecord(value: unknown): AcpxIdentityRecord {
|
||||
"permissionMode",
|
||||
"mode",
|
||||
"piThinkingLevel",
|
||||
"claudeEffort",
|
||||
"providerLifetimeFenceCandidates",
|
||||
]);
|
||||
return validatedRecord(record);
|
||||
@@ -269,6 +281,7 @@ function validatedRecord(value: Record<string, unknown>): AcpxIdentityRecord {
|
||||
throw new Error("ACPX identity permission mode is invalid");
|
||||
}
|
||||
if (value.mode !== undefined) parseProviderMode(value.mode);
|
||||
resolveClaudeEffort("claude", value.claudeEffort);
|
||||
if (value.piThinkingLevel !== undefined) resolvePiThinkingLevel("pi", value.piThinkingLevel);
|
||||
validateFenceCandidates(value.providerLifetimeFenceCandidates);
|
||||
return value as unknown as AcpxIdentityRecord;
|
||||
@@ -299,6 +312,7 @@ function validateExpected(expected: AcpxExpectedSessionIdentity): void {
|
||||
throw new Error("Expected ACPX permission mode is invalid");
|
||||
}
|
||||
if (expected.mode !== undefined) parseProviderMode(expected.mode);
|
||||
resolveClaudeEffort("claude", expected.claudeEffort);
|
||||
if (expected.piThinkingLevel !== undefined) resolvePiThinkingLevel("pi", expected.piThinkingLevel);
|
||||
validateFenceCandidates(expected.providerLifetimeFenceCandidates);
|
||||
}
|
||||
|
||||
@@ -316,6 +316,69 @@ describe("ACPX runtime host", () => {
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("leaves an active Claude owner's effort settings unchanged when a second opener loses the lifetime lease", async () => {
|
||||
const fixture = await hostFixture();
|
||||
let settingsPath = "";
|
||||
const openRuntime = vi.fn(async () => runtimePort({ getStatus: async () => ({ models: { currentModelId: "claude-sonnet-5" } }) }));
|
||||
const dependencies = fixture.dependencies({ openRuntime });
|
||||
const prepare = dependencies.prepareSandbox!;
|
||||
dependencies.prepareSandbox = async input => {
|
||||
const sandbox = await prepare(input);
|
||||
settingsPath = join(sandbox.agentHomeDirectory, "settings.json");
|
||||
return sandbox;
|
||||
};
|
||||
const options = { ...fixture.options, agent: "claude" as const, model: "claude-sonnet-5", permissionMode: "approve-all" as const };
|
||||
const host = await AcpxRuntimeHost.open(options, dependencies);
|
||||
try {
|
||||
const ownedSettings = `${JSON.stringify({ ...JSON.parse(await readFile(settingsPath, "utf8")), effortLevel: "high", activeOwnerMarker: true })}\n`;
|
||||
await writeFile(settingsPath, ownedSettings, { mode: 0o600 });
|
||||
await expect(AcpxRuntimeHost.open(options, dependencies)).rejects.toThrow("already has an active lease");
|
||||
expect(await readFile(settingsPath, "utf8")).toBe(ownedSettings);
|
||||
expect(openRuntime).toHaveBeenCalledTimes(1);
|
||||
} finally { await host.close({ reason: "ownership regression complete" }); }
|
||||
const nextOwner = await waitForAcpxOperation(() => AcpxRuntimeHost.open(options, dependencies));
|
||||
try {
|
||||
expect(JSON.parse(await readFile(settingsPath, "utf8"))).not.toHaveProperty("activeOwnerMarker");
|
||||
expect(openRuntime).toHaveBeenCalledTimes(2);
|
||||
} finally { await nextOwner.close({ reason: "next owner complete" }); }
|
||||
});
|
||||
|
||||
it.each([false, true])("cancels Claude startup promptly while retaining its settings write and lease (late failure: %s)", async failWrite => {
|
||||
const fixture = await hostFixture();
|
||||
const controller = new AbortController();
|
||||
let announceWrite!: () => void;
|
||||
let finishWrite!: () => void;
|
||||
const writeStarted = new Promise<void>(resolve => { announceWrite = resolve; });
|
||||
const writePending = new Promise<void>(resolve => { finishWrite = resolve; });
|
||||
const openRuntime = vi.fn(async () => runtimePort({ getStatus: async () => ({ models: { currentModelId: "claude-sonnet-5" } }) }));
|
||||
const dependencies = fixture.dependencies({ openRuntime });
|
||||
const prepare = dependencies.prepareSandbox!;
|
||||
dependencies.prepareSandbox = async input => {
|
||||
const sandbox = await prepare(input);
|
||||
const initialize = sandbox.initializeOwnedConfiguration;
|
||||
return { ...sandbox, initializeOwnedConfiguration: async () => {
|
||||
announceWrite();
|
||||
await writePending;
|
||||
if (failWrite) throw new Error("settings write failed after cancellation");
|
||||
await initialize?.();
|
||||
} };
|
||||
};
|
||||
const options = { ...fixture.options, agent: "claude" as const, model: "claude-sonnet-5", permissionMode: "approve-all" as const };
|
||||
const opening = AcpxRuntimeHost.open({ ...options, signal: controller.signal }, dependencies);
|
||||
const rejected = expect(opening).rejects.toThrow("settings admission cancelled");
|
||||
try {
|
||||
await writeStarted;
|
||||
controller.abort(new Error("settings admission cancelled"));
|
||||
await runAcpxOperationAttempt(() => rejected, Date.now() + 1_000);
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
await expect(AcpxRuntimeHost.open(options, fixture.dependencies({ openRuntime }))).rejects.toThrow("already has an active lease");
|
||||
} finally { finishWrite(); }
|
||||
await rejected;
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
const nextOwner = await waitForAcpxOperation(() => AcpxRuntimeHost.open(options, fixture.dependencies({ openRuntime })));
|
||||
await nextOwner.close({ reason: "cancelled initializer ownership released" });
|
||||
});
|
||||
|
||||
it("registers the assigned connection gateway alongside the Claude task bridge", async () => {
|
||||
const fixture = await hostFixture();
|
||||
let servers: AcpxRuntimePortOpenOptions["mcpServers"] = [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveClaudeEffort } from "./claude-effort.js";
|
||||
import { piProviderConfiguration } from "./pi-provider-config.js";
|
||||
import { withAcpxTurnCancellation } from "./turn-cancellation.js";
|
||||
import { resolveAcpxProviderMode } from "./provider-mode.js";
|
||||
@@ -85,6 +86,7 @@ const ACPX_ADMISSION_CLEANUP_RESCHEDULE_MS = 1_000;
|
||||
export interface AcpxRuntimePortIdentity {
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
acpxRecordId: string;
|
||||
backendSessionId: string;
|
||||
agentSessionId: string;
|
||||
@@ -165,6 +167,7 @@ export interface AcpxRuntimePortOpenOptions {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
permissionPolicy: ReturnType<typeof acpxRuntimePermissionPolicy>;
|
||||
launchEnvironment: Readonly<NodeJS.ProcessEnv>;
|
||||
/** Kernel credential-home quorum inherited by the provider sentinel. */
|
||||
@@ -239,6 +242,7 @@ export interface OpenAcpxRuntimeHostOptions {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
systemInstructions?: string;
|
||||
runtimeContext?: NativeRuntimeContextSnapshot | null;
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
@@ -361,6 +365,7 @@ export class AcpxRuntimeHost {
|
||||
permissionMode: options.permissionMode,
|
||||
mode: resolveAcpxProviderMode(options.agent, options.mode),
|
||||
piThinkingLevel: resolvePiThinkingLevel(options.agent, options.piThinkingLevel),
|
||||
claudeEffort: resolveClaudeEffort(options.agent, options.claudeEffort),
|
||||
...(preconfigured ? { homeBinding: preconfiguredHomeEnvironment(options.environment) } : {}),
|
||||
...(["cursor", "copilot", "pi"].includes(options.agent) && options.providerPolicy !== undefined
|
||||
? { providerPolicy: options.providerPolicy } : {}),
|
||||
@@ -489,6 +494,21 @@ export class AcpxRuntimeHost {
|
||||
dependencies.reportRetainedCleanupFailure(failure),
|
||||
});
|
||||
}
|
||||
options.signal?.throwIfAborted();
|
||||
const configurationLifetime = credential;
|
||||
await runAbortableAdmissionStage(
|
||||
options.signal,
|
||||
async () => { await sandbox.initializeOwnedConfiguration?.(); },
|
||||
cleanup => {
|
||||
pendingRuntimeOwnsCredential = true;
|
||||
const retained = cleanup.then(() => cleanupAbortedRuntimeAdmission(
|
||||
null, configurationLifetime, "ACPX owned configuration cancelled",
|
||||
));
|
||||
retainRuntimeHostCleanup(retained);
|
||||
dependencies.retainAdmissionCleanup?.(retained);
|
||||
},
|
||||
);
|
||||
options.signal?.throwIfAborted();
|
||||
assertAcpxProfileEnvironment(options.agent, sandbox.launchEnvironment);
|
||||
let launchEnvironment = sandbox.launchEnvironment;
|
||||
// This copy is registered by the authenticated controller for its company,
|
||||
@@ -593,6 +613,7 @@ export class AcpxRuntimeHost {
|
||||
permissionMode: binding.permissionMode,
|
||||
mode: binding.mode,
|
||||
piThinkingLevel: binding.piThinkingLevel,
|
||||
claudeEffort: binding.claudeEffort,
|
||||
permissionPolicy: acpxRuntimePermissionPolicy(
|
||||
binding.permissionMode,
|
||||
),
|
||||
@@ -657,6 +678,7 @@ export class AcpxRuntimeHost {
|
||||
if (runtimeIdentity.piThinkingLevel !== binding.piThinkingLevel) {
|
||||
throw new Error("ACPX runtime Pi thinking level conflicts with session binding");
|
||||
}
|
||||
if (runtimeIdentity.claudeEffort !== binding.claudeEffort) throw new Error("ACPX runtime Claude effort conflicts with session binding");
|
||||
const observedIdentity: AcpxExpectedSessionIdentity = {
|
||||
kind: "acpx",
|
||||
normalizedSessionId: binding.normalizedSessionId,
|
||||
|
||||
@@ -121,6 +121,7 @@ describe("ACPX runtime sandbox", () => {
|
||||
binding: { ...fixture.binding, requestedModel: model },
|
||||
agent: "claude",
|
||||
});
|
||||
await sandbox.initializeOwnedConfiguration?.();
|
||||
const settingsPath = join(sandbox.agentHomeDirectory, "settings.json");
|
||||
expect(JSON.parse(await readFile(settingsPath, "utf8"))).toMatchObject({
|
||||
model,
|
||||
@@ -136,12 +137,29 @@ describe("ACPX runtime sandbox", () => {
|
||||
const binding = { ...fixture.binding, requestedModel: model, effectiveModel: model };
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const sandbox = await prepareAcpxRuntimeSandbox({ binding, agent: "claude" });
|
||||
await sandbox.initializeOwnedConfiguration?.();
|
||||
const settings = JSON.parse(await readFile(join(sandbox.agentHomeDirectory, "settings.json"), "utf8"));
|
||||
expect(settings).toMatchObject({ model, availableModels: [model] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("restores bound Claude effort in private settings across cold connections", async () => {
|
||||
const fixture = await sandboxFixture("claude");
|
||||
for (const claudeEffort of ["high", "xhigh", "max"] as const) {
|
||||
const binding = { ...fixture.binding, claudeEffort };
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const sandbox = await prepareAcpxRuntimeSandbox({ binding, agent: "claude" });
|
||||
await sandbox.initializeOwnedConfiguration?.();
|
||||
const settings = JSON.parse(await readFile(join(sandbox.agentHomeDirectory, "settings.json"), "utf8"));
|
||||
expect(settings.effortLevel).toBe(claudeEffort);
|
||||
}
|
||||
}
|
||||
const sandbox = await prepareAcpxRuntimeSandbox({ binding: fixture.binding, agent: "claude" });
|
||||
await sandbox.initializeOwnedConfiguration?.();
|
||||
expect(JSON.parse(await readFile(join(sandbox.agentHomeDirectory, "settings.json"), "utf8"))).not.toHaveProperty("effortLevel");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pi", "OPENROUTER_API_KEY", "pi-home"],
|
||||
["claude", "ANTHROPIC_API_KEY", "claude-home"],
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface AcpxRuntimeSandbox {
|
||||
cacheDirectory: string;
|
||||
agentHomeDirectory: string;
|
||||
workspaceRecordPath: string;
|
||||
initializeOwnedConfiguration?: () => Promise<void>;
|
||||
launchEnvironment: Readonly<NodeJS.ProcessEnv>;
|
||||
persistedEnvironment: Readonly<NodeJS.ProcessEnv>;
|
||||
/** The host sandbox must protect these runtime/config paths from tools. */
|
||||
@@ -421,7 +422,7 @@ export async function prepareAcpxRuntimeSandbox(input: {
|
||||
const configuration = piProviderConfiguration(input.environment);
|
||||
if (configuration) await writePrivateFile(join(agentHomeDirectory, "models.json"), configuration.json);
|
||||
}
|
||||
if (input.agent === "claude") {
|
||||
const initializeOwnedConfiguration = input.agent === "claude" ? async () => {
|
||||
// ACP otherwise rewrites exact IDs (including user-entered model IDs) to
|
||||
// picker aliases such as "sonnet". Its supported availableModels setting
|
||||
// preserves our requested ID through selection and model verification.
|
||||
@@ -430,12 +431,13 @@ export async function prepareAcpxRuntimeSandbox(input: {
|
||||
`${JSON.stringify({
|
||||
model: input.binding.requestedModel,
|
||||
availableModels: [input.binding.requestedModel],
|
||||
...(input.binding.claudeEffort ? { effortLevel: input.binding.claudeEffort } : {}),
|
||||
...((input.binding.permissionMode === "approve-reads" || input.binding.permissionMode === "approve-paperclip")
|
||||
? { permissions: { allow: claudePaperclipPermissionRules(input.tools ?? [], input.binding.permissionMode) } }
|
||||
: {}),
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
} : undefined;
|
||||
if (input.agent === "grok") {
|
||||
// Native project rules merge with global rules. Ask rules win over project
|
||||
// allows, keeping operation approval at the runner's authenticated boundary.
|
||||
@@ -586,6 +588,7 @@ export async function prepareAcpxRuntimeSandbox(input: {
|
||||
cacheDirectory,
|
||||
agentHomeDirectory,
|
||||
workspaceRecordPath,
|
||||
...(initializeOwnedConfiguration ? { initializeOwnedConfiguration } : {}),
|
||||
protectedPaths,
|
||||
launchEnvironment: Object.freeze({ ...launchEnvironment }),
|
||||
persistedEnvironment: Object.freeze(persistedEnvironment),
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface AcpxSidecarOpenParams {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: "off" | "low" | "high" | "max";
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
permissionModePinned: boolean;
|
||||
providerPolicy?: { readOnly: boolean };
|
||||
systemInstructions: string;
|
||||
@@ -69,6 +70,7 @@ export interface AcpxExpectedSessionIdentity {
|
||||
permissionMode?: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: "off" | "low" | "high" | "max";
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerLifetimeFenceCandidates: readonly [number, number, number];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveClaudeEffort } from "../acpx/claude-effort.js";
|
||||
import { resolvePiThinkingLevel } from "../acpx/pi-thinking.js";
|
||||
import { isProviderMode } from "../../contracts/provider-mode.js";
|
||||
import type { PersistedHarnessProviderIdentity } from "../../contracts/harness-driver.js";
|
||||
@@ -81,6 +82,7 @@ export function parseProviderIdentity(
|
||||
);
|
||||
}
|
||||
const piThinkingLevel = identity.piThinkingLevel === undefined ? undefined : resolvePiThinkingLevel("pi", identity.piThinkingLevel);
|
||||
const claudeEffort = resolveClaudeEffort("claude", identity.claudeEffort);
|
||||
const mode = identity.mode;
|
||||
if (mode !== undefined && !isProviderMode(mode)) {
|
||||
throw new Error("ACPX provider identity contains an invalid provider mode");
|
||||
@@ -112,6 +114,7 @@ export function parseProviderIdentity(
|
||||
...(permissionMode === undefined ? {} : { permissionMode }),
|
||||
...(mode === undefined ? {} : { mode }),
|
||||
...(piThinkingLevel === undefined ? {} : { piThinkingLevel }),
|
||||
...(claudeEffort === undefined ? {} : { claudeEffort }),
|
||||
providerLifetimeFenceCandidates: fenceCandidates as [
|
||||
number,
|
||||
number,
|
||||
|
||||
@@ -57,6 +57,7 @@ export {
|
||||
type QualifiedAcpxEnvironmentProbe,
|
||||
} from "./drivers/acpx/codex-acpx-driver.js";
|
||||
export * from "./drivers/acpx/sidecar-protocol.js";
|
||||
export { resolveClaudeEffort } from "./drivers/acpx/claude-effort.js";
|
||||
export * from "./drivers/runner-tool-bridge.js";
|
||||
export {
|
||||
createRunnerdCodexTransport,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RunnerExecutionDriver } from "../control-plane/runner-execution-driver.js";
|
||||
import { resolvePinnedOpenCodeCommand } from "../drivers/opencode/opencode-server-driver.js";
|
||||
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
|
||||
import { resolveClaudeEffort, admittedClaudeEffort } from "../drivers/acpx/claude-effort.js";
|
||||
import { admittedPiThinkingLevel, resolvePiThinkingLevel } from "../drivers/acpx/pi-thinking.js";
|
||||
import { configuredEnvironment } from "../configured-environment.js";
|
||||
import { githubCredentialEnvironment } from "../github-credential-environment.js";
|
||||
@@ -1220,6 +1221,7 @@ export interface CapabilityRunnerdProcessEvidence {
|
||||
providerDriver: string | null;
|
||||
/** Effective native mode, populated only from the admitted Pi identity. */
|
||||
piThinkingLevel?: "off" | "low" | "high" | "max";
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerVersion: string | null;
|
||||
acpxAgent: QualifiedAcpxAgent | null;
|
||||
agentServerVersion: string | null;
|
||||
@@ -1247,6 +1249,7 @@ export interface CapabilityRunnerdCodexTransportOptions extends RunnerExecutionD
|
||||
acpxPermissionMode?: NativeAcpxPermissionMode;
|
||||
acpxMode?: string;
|
||||
piThinkingLevel?: "off" | "low" | "high" | "max";
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
acpxPermissionModePinned?: boolean;
|
||||
acpxSidecarPath?: string;
|
||||
/** SHA-256 verified by the provider-pack authority before runner startup. */
|
||||
@@ -3623,6 +3626,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
readonly #bridgedRuntimeInputs = new Map<string, { durableTurnId: string; permission?: boolean }>();
|
||||
|
||||
constructor(readonly options: CapabilityRunnerdCodexTransportOptions) {
|
||||
resolveClaudeEffort(options.provider === "acpx" ? options.acpxAgent ?? "codex" : "", options.claudeEffort);
|
||||
resolvePiThinkingLevel(options.provider === "acpx" ? options.acpxAgent ?? "codex" : "", options.piThinkingLevel);
|
||||
if (options.acpxMode !== undefined && options.provider !== "acpx") {
|
||||
throw new Error("acpxMode requires the ACPX provider");
|
||||
@@ -4987,6 +4991,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
providerPolicy: { readOnly: params.permissions === "paperclip-runner-workspace-read-only" },
|
||||
...(selectedAcpxMode === undefined ? {} : { mode: selectedAcpxMode }),
|
||||
...(acpxProfile!.agent === "pi" ? { piThinkingLevel: this.options.piThinkingLevel } : {}),
|
||||
...(acpxProfile!.agent === "claude" && this.options.claudeEffort ? { claudeEffort: this.options.claudeEffort } : {}),
|
||||
permissionMode: resolveRunnerdAcpxPermissionMode(
|
||||
this.options.acpxPermissionMode,
|
||||
),
|
||||
@@ -6721,6 +6726,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
if (descriptor.turnControls !== undefined && descriptor.agent !== (this.options.acpxAgent ?? "codex")) {
|
||||
throw new Error("ACPX capability identity differs from the admitted profile");
|
||||
}
|
||||
const claudeEffort = admittedClaudeEffort(this.options.acpxAgent ?? "codex", this.options.claudeEffort, descriptor, providerIdentity);
|
||||
if (claudeEffort !== undefined) this.#evidence.claudeEffort = claudeEffort;
|
||||
const piThinkingLevel = admittedPiThinkingLevel(this.options.acpxAgent ?? "codex", this.options.piThinkingLevel, descriptor, providerIdentity);
|
||||
if (piThinkingLevel !== undefined) this.#evidence.piThinkingLevel = piThinkingLevel;
|
||||
this.#turnControls = parseAcpxTurnControlCapabilities(descriptor.turnControls, descriptor.agent);
|
||||
|
||||
@@ -1365,6 +1365,15 @@ export const providerDescriptorSchema = {
|
||||
"high",
|
||||
"max"
|
||||
]
|
||||
},
|
||||
"claudeEffort": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
@@ -1388,6 +1397,26 @@ export const providerDescriptorSchema = {
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"required": [
|
||||
"claudeEffort"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "acpx"
|
||||
},
|
||||
"agent": {
|
||||
"const": "claude"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 23,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865",
|
||||
"credentialEnvironmentSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
|
||||
"runtimeSandboxSourceSha256": "8767456c125b53b1e85fbfce84245c9b213f9ef27a58970cf73dbc2ff82005d8",
|
||||
"recoveryIdentitySourceSha256": "8479e04da1ae2f7742e34f045b0270b99e9c3ae39a5b8fb9f164e5776d83224c",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 24,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865",
|
||||
"credentialEnvironmentSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
|
||||
"runtimeSandboxSourceSha256": "1119cd01f131d814d5bdb77bcb38af359b9328876ff8537894d19ec2eac35e7f",
|
||||
"recoveryIdentitySourceSha256": "8479e04da1ae2f7742e34f045b0270b99e9c3ae39a5b8fb9f164e5776d83224c",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 25,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865",
|
||||
"credentialEnvironmentSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
|
||||
"runtimeSandboxSourceSha256": "144d62b3415cfedf272b5b052e14af1e41fe50a644af84247fc9c20c7ef7edde",
|
||||
"recoveryIdentitySourceSha256": "8479e04da1ae2f7742e34f045b0270b99e9c3ae39a5b8fb9f164e5776d83224c",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:cbf43cd25d8e8523507ce4af82b042eddc8641a7c24627556263940ffccf8a5b",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 26,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865",
|
||||
"credentialEnvironmentSourceSha256": "c9c3650b6415430b045533eea2d7cddfe87302e03817f40ae83f7447ef0ae957",
|
||||
"runtimeSandboxSourceSha256": "e5a034b2e4008b56b33e654a8c299bd3b329a9d8940bdb1eafdbffe3574e9372",
|
||||
"recoveryIdentitySourceSha256": "d2c9fc161fcc135fb9760110bfa3d984ee2bb38d87fa5ce7cb7418a49dd89e66",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"commandDigest": "sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 27,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "75a9a13f13f224dc092bd8ebf49e8747162172943761e44476c8eb18d80c52d4",
|
||||
"credentialEnvironmentSourceSha256": "b29464dd2fcbd1bff3a9cdb4ba97a00d9c4a07a6cdd878f1baaa8a4948d020ca",
|
||||
"runtimeSandboxSourceSha256": "e5a034b2e4008b56b33e654a8c299bd3b329a9d8940bdb1eafdbffe3574e9372",
|
||||
"recoveryIdentitySourceSha256": "d2c9fc161fcc135fb9760110bfa3d984ee2bb38d87fa5ce7cb7418a49dd89e66",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814",
|
||||
"openRouterAttributionSourceSha256": "1211191529d6073b733b311c3d69cada4973eede0f3e540c46e261f78e0b90f9"
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fb4a232460dc9e6c7e7221cb4ddd66d7cb82fa0b35e1b3eb04f2d1ee5ee18c86",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 19,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "e9ca8d1a7449fe28b63dcd0f858bfc233f1ec791e159cdd8436d6ea34a3089fe",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 22,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "ddd98aabe20f4d40b04bb58126bda548a9d85b8234a3f9ea84b6db414e9b4814",
|
||||
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
|
||||
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
|
||||
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,8 @@ assert.equal(JSON.parse(await readFile(join(server, 'package.json'), 'utf8')).na
|
||||
const { resolvePaperclipRunnerBinary } = await import(pathToFileURL(join(server, 'dist/services/native-runtime/native-codex-runner.js')));
|
||||
const { createCapabilityRunnerdCodexTransport } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/live/runnerd-codex-transport.js')));
|
||||
const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js')));
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 24);
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:347feab6eed4bc0f4ab099aae3292694e73d08e05148483ac612b0db958b0e39');
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 27);
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba');
|
||||
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'qualificationModel'), false);
|
||||
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'reportedModelId'), false);
|
||||
const daemon = resolvePaperclipRunnerBinary();
|
||||
@@ -97,7 +97,7 @@ try {
|
||||
assert.equal(state.activeTurnId, null);
|
||||
assert.equal(state.identity, null);
|
||||
assert.equal(state.providerExitUnconfirmed, false);
|
||||
console.log(JSON.stringify({ schema: 'paperclip.pi.public-npm-install.v1', target: `${process.platform}-${process.arch}`, normalPackagedDaemon: true, exactPiProfile: 24, runtime: '1.0.0', credentials: 'none', promptCalls: 0, settledMs, cleanRunnerExit: true }));
|
||||
console.log(JSON.stringify({ schema: 'paperclip.pi.public-npm-install.v1', target: `${process.platform}-${process.arch}`, normalPackagedDaemon: true, exactPiProfile: QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, runtime: '1.0.0', credentials: 'none', promptCalls: 0, settledMs, cleanRunnerExit: true }));
|
||||
} finally {
|
||||
try { await bundle?.transport.close(); }
|
||||
finally { clearTimeout(watchdog); await rm(root, { recursive: true, force: true }); }
|
||||
|
||||
@@ -2,12 +2,13 @@ import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runnerSuites, buildRunnerMatrix } from "./catalog.js";
|
||||
import { gradeNativeDefault, NATIVE_MASTER_DEFAULT_SHA256 } from "./native-completion-defaults.js";
|
||||
import { nativeCompletionTasks } from "./native-completion-cases.js";
|
||||
import { nativeCompletionPreflightEnvironment } from "./native-completion-admission.js";
|
||||
import { assertNativeInstructionLineage, assertNativeInstructionSelection, nativeInstructionVariant, validateNativeInstructionMeasurement, NATIVE_INSTRUCTION_BASE_SHA, NATIVE_INSTRUCTION_DEFAULT_SHA256, NATIVE_INSTRUCTION_SUITE, NATIVE_INSTRUCTION_VARIANTS } from "./native-instruction-consolidation.js";
|
||||
|
||||
describe("native instruction comparison admission", () => {
|
||||
@@ -67,12 +68,15 @@ describe("native instruction comparison admission", () => {
|
||||
});
|
||||
it("accepts each complete source variant and rejects a mixed variant", () => {
|
||||
const files = Object.keys(NATIVE_INSTRUCTION_VARIANTS.baseline);
|
||||
const baseline = new Map(files.map(file => [file, execFileSync("git", ["show", `${NATIVE_INSTRUCTION_BASE_SHA}:${file}`])]));
|
||||
const historicalFile = (sha: string, file: string) => execFileSync("git", ["--no-replace-objects", "show", `${sha}:${file}`], {
|
||||
cwd: resolve(import.meta.dirname, "../.."), env: nativeCompletionPreflightEnvironment(process.env), timeout: 30_000,
|
||||
});
|
||||
const baseline = new Map(files.map(file => [file, historicalFile(NATIVE_INSTRUCTION_BASE_SHA, file)]));
|
||||
expect(nativeInstructionVariant(file => baseline.get(file)!)).toBe("baseline");
|
||||
// This historical admission intentionally rejects later production changes.
|
||||
// Exercise the retained qualified tree rather than requiring HEAD to remain
|
||||
// one of this completed comparison's frozen variants forever.
|
||||
const candidate = new Map(files.map(file => [file, execFileSync("git", ["show", `16ef1a5744b23bb043a8b286e6288921da1de98f:${file}`])]));
|
||||
const candidate = new Map(files.map(file => [file, historicalFile("16ef1a5744b23bb043a8b286e6288921da1de98f", file)]));
|
||||
const currentVariant = nativeInstructionVariant(file => candidate.get(file)!);
|
||||
expect(["baseline", "candidate", "corrected", "feedback", "opencodeFeedback", "opencodeFeedbackSettlement", "opencodeFeedbackResponses"]).toContain(currentVariant);
|
||||
candidate.set(files[0]!, currentVariant === "candidate" ? baseline.get(files[0]!)! : Buffer.from("unknown source"));
|
||||
|
||||
@@ -189,8 +189,8 @@ describe("Pi controls catalog admission", () => {
|
||||
// Extended v3 states the strict file oracle's task-wide Bash limit.
|
||||
const pi = runnerMatrix.find(c => c.profile.qualificationCandidate === "pi")!.profile;
|
||||
expect(pi.modelQualification?.qualificationId).toBe("pi:0.0.33:1.0.0:openrouter");
|
||||
expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 23, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 23, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "single-json-write-and-content-bound-native-read-both-runs", agentMemoryReadAuthority: "local-withheld-or-exact-remote-agent-run-file", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" });
|
||||
expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 10, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", profileVersion: 23,
|
||||
expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 23, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 27, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "single-json-write-and-content-bound-native-read-both-runs", agentMemoryReadAuthority: "local-withheld-or-exact-remote-agent-run-file", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" });
|
||||
expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 10, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", profileVersion: 27,
|
||||
remoteProcessIdentity: "observer-pid-startTicks-bootId",
|
||||
controlPlaneSettlement: "required-scoped-result-and-terminal-after-runner" });
|
||||
expect(runnerSuites.find(s => s.id === "extended-harnesses")!.definitionMetadata).toMatchObject({ version: 5, piFileArtifactTitle: "exact-filename", piFileCommandTransport: "fenced-bash-markdown-paste" });
|
||||
@@ -200,7 +200,7 @@ describe("Pi controls catalog admission", () => {
|
||||
}
|
||||
const hashes = Object.fromEntries(runnerSuites.filter(s => ["pi-native", "native-active-stop", "extended-harnesses", "rich-acp-warm-continuity"].includes(s.id)).map(s => [s.id, suiteDefinitionHash(s)]));
|
||||
expect(hashes).toEqual({
|
||||
"pi-native": "3c77ced7edfb99aa12052f451398da7580466a54abadcaf287d31ee12c16382d",
|
||||
"pi-native": "7bf1477d5ca92569aad29d31278fad220ff59baa9f6622c8c9b1302ecaa47b83",
|
||||
"native-active-stop": "2d4fdeeb75bd531b309bd1b35cfa5680ceefdeee6b7155b068dd011ce9901980",
|
||||
"rich-acp-warm-continuity": "331b88d9538a132670789fb7864df7df5f4bf294ae19b62f53d8a02f901774f0",
|
||||
"extended-harnesses": "5de4fac78d4d581bc0954f07b5cf2df2862d37f8b0205325eede6d9ef6d9f5e6",
|
||||
|
||||
Reference in new issue
Block a user