mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
feat(runner): admit Claude effort before native prompts and resume
Version the shared Pi and pending Copilot source attestations without changing their executable closures or platform qualification. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a200495cc7
commit
2e8d5e906e
44 files changed
+681
-149
No files matched your search
@@ -14,12 +14,12 @@
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"pi": {
|
||||
"agentProfileVersion": 22,
|
||||
"agentProfileVersion": 23,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9",
|
||||
"commandDigest": "sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"cursor": {
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 17,
|
||||
"agentProfileVersion": 18,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"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 v7 with the optional
|
||||
`provider.claudeEffort` field. V7 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-v6 contracts. 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 It 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
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
{
|
||||
"path": "schemas/provider-descriptor.schema.json",
|
||||
"id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json",
|
||||
"sha256": "bf691bda4a58490dba680b74559bfe1c9254b19e3265a603d9cbf3e169094519"
|
||||
"sha256": "ef37ebba071ac239e0b4ff680a469eca6948b8eace451c5c88089c81b7acdae1"
|
||||
},
|
||||
{
|
||||
"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"],
|
||||
|
||||
@@ -153,6 +153,15 @@
|
||||
"high",
|
||||
"max"
|
||||
]
|
||||
},
|
||||
"claudeEffort": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
@@ -176,6 +185,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-v22-identity.json"
|
||||
"../../../../test-fixtures/pi-acp/profile-v23-identity.json"
|
||||
))
|
||||
.unwrap();
|
||||
let mut value = descriptor("codex");
|
||||
@@ -3208,6 +3224,7 @@ mod tests {
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v19-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v20-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v21-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v22-identity.json"),
|
||||
] {
|
||||
let prior: Value = serde_json::from_str(prior).unwrap();
|
||||
value["commandDigest"] = prior["commandDigest"].clone();
|
||||
@@ -3507,6 +3524,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();
|
||||
@@ -3792,6 +3810,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();
|
||||
@@ -4086,6 +4105,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();
|
||||
@@ -4259,6 +4279,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:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9",
|
||||
"sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
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:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
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:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3";
|
||||
const PI_ACPX_DIGEST: &str =
|
||||
"sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9";
|
||||
"sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6";
|
||||
|
||||
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-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v17-identity.json"]]) {
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v23-identity.json"], ["copilot", "test/fixtures/copilot-profile-v18-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,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";
|
||||
|
||||
@@ -297,6 +298,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,
|
||||
@@ -1209,12 +1211,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 &&
|
||||
@@ -1236,6 +1239,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(
|
||||
@@ -1307,6 +1311,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,
|
||||
),
|
||||
|
||||
@@ -457,6 +457,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];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ 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, type NativeExecutionInputV1 } from "./native-execution.js";
|
||||
import {
|
||||
NATIVE_RUNTIME_ASSET_SCHEMA,
|
||||
PAPERCLIP_EXECUTION_PROMPT,
|
||||
@@ -680,3 +680,30 @@ describe("native task context ownership", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Claude effort input v7", () => {
|
||||
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_V7, 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_V7, provider: { claudeEffort } });
|
||||
expect(buildNativeModelEnvelope(parsed).schema).toBe("paperclip.native-model-envelope.v3");
|
||||
expect(JSON.stringify(buildNativeModelEnvelope(parsed))).not.toContain("claudeEffort");
|
||||
});
|
||||
it("keeps historical readers closed and rejects aliases and foreign effort", () => {
|
||||
for (const schema of ["paperclip.native-execution-input.v4", NATIVE_EXECUTION_INPUT_SCHEMA]) {
|
||||
expect(() => parseNativeExecutionInput({ ...current, schema, provider: { ...current.provider, claudeEffort: "high" } })).toThrow("claudeEffort");
|
||||
expect(parseNativeExecutionInput({ ...current, 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 } 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_V7 = "paperclip.native-execution-input.v7" 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;
|
||||
export const NATIVE_EXECUTION_INPUT_SCHEMA = "paperclip.native-execution-input.v5" as const;
|
||||
@@ -238,6 +240,13 @@ export interface NativeExecutionInputV5 extends Omit<NativeExecutionInputV4, "sc
|
||||
completionSources?: NativeCompletionSources;
|
||||
}
|
||||
|
||||
export type NativeProviderConfigV7 = Exclude<NativeProviderConfigV5, { kind: "acpx" }>
|
||||
| (Extract<NativeProviderConfigV5, { kind: "acpx" }> & { claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max" });
|
||||
export interface NativeExecutionInputV7 extends Omit<NativeExecutionInputV5, "schema" | "provider"> {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V7;
|
||||
provider: NativeProviderConfigV7;
|
||||
}
|
||||
|
||||
export interface NativeExecutionInputV6 extends Omit<NativeExecutionInputV5, "schema" | "provider" | "workspace" | "session"> {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V6;
|
||||
provider: { kind: "openai_dot"; model: null; binding: DotBindingSnapshot };
|
||||
@@ -245,7 +254,7 @@ export interface NativeExecutionInputV6 extends Omit<NativeExecutionInputV5, "sc
|
||||
session: Omit<NativeExecutionInputV5["session"], "driverKind"> & { driverKind: "openai_dot_mcp" };
|
||||
}
|
||||
|
||||
export type NativeExecutionInput = NativeExecutionInputV1 | NativeExecutionInputV2 | NativeExecutionInputV3 | NativeExecutionInputV4 | NativeExecutionInputV5 | NativeExecutionInputV6;
|
||||
export type NativeExecutionInput = NativeExecutionInputV1 | NativeExecutionInputV2 | NativeExecutionInputV3 | NativeExecutionInputV4 | NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7;
|
||||
|
||||
/** The only task data that may enter provider-visible model input. */
|
||||
export interface NativeModelEnvelopeV1 {
|
||||
@@ -372,7 +381,8 @@ function parseDotNativeExecutionInput(input: Record<string, unknown>): NativeExe
|
||||
export function parseNativeExecutionInput(value: unknown): NativeExecutionInput {
|
||||
const input = record(value, "input");
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6) return parseDotNativeExecutionInput(input);
|
||||
const isV5 = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
|
||||
const isV7 = 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;
|
||||
const isV2 = isV3 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V2;
|
||||
@@ -504,7 +514,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"] : []), ...(isV7 ? ["claudeEffort"] : [])]
|
||||
: provider.kind === "codex" && isV4
|
||||
? ["kind", "model", "approvalPolicy", ...(isV5 ? ["reasoningEffort"] : [])]
|
||||
: provider.kind === "opencode" && isV4
|
||||
@@ -527,7 +537,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: NativeProviderConfigV7 | NativeProviderConfig | NativeProviderConfigV5;
|
||||
if (provider.kind === "claude_managed") {
|
||||
if (providerModel === null) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for claude_managed");
|
||||
@@ -643,6 +653,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",
|
||||
@@ -681,6 +692,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",
|
||||
@@ -835,6 +847,12 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
provider: parsedProvider as NativeProviderConfigV4,
|
||||
};
|
||||
if (!isV5) return { ...withPermissions, schema: NATIVE_EXECUTION_INPUT_SCHEMA_V4 };
|
||||
if (isV7) return {
|
||||
...withPermissions,
|
||||
schema: NATIVE_EXECUTION_INPUT_SCHEMA_V7,
|
||||
provider: parsedProvider as NativeProviderConfigV7,
|
||||
...(input.completionSources !== undefined ? { completionSources: parseCompletionSources(input.completionSources) } : {}),
|
||||
};
|
||||
return {
|
||||
...withPermissions,
|
||||
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
|
||||
@@ -901,7 +919,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) {
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7) {
|
||||
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,21 @@
|
||||
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) {
|
||||
// Every connection, including session/load, must select and acknowledge the
|
||||
// configured effort. A restored preference alone never admits a prompt.
|
||||
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;
|
||||
}
|
||||
@@ -148,6 +148,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;
|
||||
@@ -463,6 +464,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,
|
||||
@@ -1290,6 +1292,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,
|
||||
},
|
||||
@@ -2153,6 +2156,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,
|
||||
|
||||
@@ -29,6 +29,31 @@ 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 options = openOptions(fakeCommand());
|
||||
options.profile = { ...options.profile, agent: "claude" };
|
||||
options.claudeEffort = "high";
|
||||
options.restoringSession = restoring;
|
||||
options.refreshConsumedCommand = vi.fn(async () => undefined);
|
||||
let created!: AcpRuntimeOptions;
|
||||
vi.mocked(runtime.setConfigOption).mockImplementation(async input => {
|
||||
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; },
|
||||
});
|
||||
expect(runtime.setConfigOption).toHaveBeenCalledExactlyOnceWith({ handle: HANDLE, key: "effort", value: "high" });
|
||||
expect(await port.identity()).toMatchObject({ claudeEffort: "high" });
|
||||
expect(options.refreshConsumedCommand).toHaveBeenCalledOnce();
|
||||
expect(runtime.startTurn).not.toHaveBeenCalled();
|
||||
await port.close({ reason: "test complete" });
|
||||
});
|
||||
|
||||
it("installs per-connection policy authority only for Copilot and rejects native commands before a turn starts", async () => {
|
||||
for (const agent of ["copilot", "codex", "claude", "grok"] as const) {
|
||||
const runtime = fakeRuntime();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveClaudeEffort, createClaudeEffortAdmission } from "./claude-effort.js";
|
||||
import { createPiThinkingAdmission, resolvePiThinkingLevel } from "./pi-thinking.js";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
|
||||
@@ -305,6 +306,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;
|
||||
@@ -316,6 +319,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
|
||||
@@ -527,6 +531,18 @@ 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");
|
||||
await runtime.setConfigOption({ handle: ensuredHandle, key: "effort", value: selectedClaudeEffort! });
|
||||
claudeEffort.assertReady();
|
||||
await children.verifyLifetimeOwnership();
|
||||
options.signal?.throwIfAborted();
|
||||
await commandLaunches.refreshConsumedCommand?.();
|
||||
}
|
||||
return ensuredHandle;
|
||||
}) : ensuredSession)
|
||||
.catch((error: unknown) => {
|
||||
throw classifySessionEnsureFailure(error);
|
||||
@@ -543,6 +559,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();
|
||||
@@ -597,7 +614,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,104 @@
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
/** 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 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;
|
||||
// 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" || !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");
|
||||
// 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 === 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; }
|
||||
}
|
||||
|
||||
@@ -87,8 +87,16 @@ describe("Copilot build-owned installation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v17 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
it("preserves v17 executable and policy identity while binding the shared Claude effort sources", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v18 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-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);
|
||||
@@ -119,7 +127,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-v17-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-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": 22,
|
||||
"agentProfileVersion": 23,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9",
|
||||
"commandDigest": "sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"cursor": {
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 17,
|
||||
"agentProfileVersion": 18,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"qualificationStatus": "pending",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
|
||||
@@ -88,11 +88,19 @@ describe("Pi installation factory", () => {
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves profile 22 runtime bytes and revises only shared Claude effort source attestations", 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(22), current = await read(23);
|
||||
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("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-v22-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v23-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))}`);
|
||||
@@ -123,21 +131,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 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 22");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 23");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 23");
|
||||
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. */
|
||||
|
||||
@@ -75,6 +75,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,12 +54,14 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
providerConfigurationDigest?: string;
|
||||
providerPolicy?: { readOnly: boolean; readRoots?: readonly string[]; protectedPaths?: readonly string[] };
|
||||
}): Promise<AcpxRecoveryBinding> {
|
||||
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");
|
||||
@@ -104,6 +109,7 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
permissionMode: input.permissionMode,
|
||||
...(mode ? { mode } : {}),
|
||||
...(piThinkingLevel ? { piThinkingLevel } : {}),
|
||||
...(claudeEffort ? { claudeEffort } : {}),
|
||||
}),
|
||||
).replace("sha256:", "paperclip-");
|
||||
return {
|
||||
@@ -118,6 +124,7 @@ export async function createAcpxRecoveryBinding(input: {
|
||||
permissionMode: input.permissionMode,
|
||||
...(mode ? { mode } : {}),
|
||||
...(piThinkingLevel ? { piThinkingLevel } : {}),
|
||||
...(claudeEffort ? { claudeEffort } : {}),
|
||||
profileSessionKey,
|
||||
};
|
||||
}
|
||||
@@ -140,6 +147,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],
|
||||
@@ -167,6 +175,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);
|
||||
@@ -193,7 +202,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",
|
||||
@@ -214,6 +224,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,
|
||||
@@ -240,6 +251,7 @@ function parsePersistedRecord(value: unknown): AcpxIdentityRecord {
|
||||
"permissionMode",
|
||||
"mode",
|
||||
"piThinkingLevel",
|
||||
"claudeEffort",
|
||||
"providerLifetimeFenceCandidates",
|
||||
]);
|
||||
return validatedRecord(record);
|
||||
@@ -267,6 +279,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;
|
||||
@@ -297,6 +310,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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -83,6 +84,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;
|
||||
@@ -163,6 +165,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. */
|
||||
@@ -238,6 +241,7 @@ export interface OpenAcpxRuntimeHostOptions {
|
||||
permissionMode: NativeAcpxPermissionMode;
|
||||
mode?: string;
|
||||
piThinkingLevel?: PiThinkingLevel;
|
||||
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
systemInstructions?: string;
|
||||
runtimeContext?: NativeRuntimeContextSnapshot | null;
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
@@ -350,6 +354,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),
|
||||
...(["cursor", "copilot", "pi"].includes(options.agent) && options.providerPolicy !== undefined
|
||||
? { providerPolicy: options.providerPolicy } : {}),
|
||||
}),
|
||||
@@ -581,6 +586,7 @@ export class AcpxRuntimeHost {
|
||||
permissionMode: binding.permissionMode,
|
||||
mode: binding.mode,
|
||||
piThinkingLevel: binding.piThinkingLevel,
|
||||
claudeEffort: binding.claudeEffort,
|
||||
permissionPolicy: acpxRuntimePermissionPolicy(
|
||||
binding.permissionMode,
|
||||
),
|
||||
@@ -644,6 +650,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,
|
||||
|
||||
@@ -119,6 +119,20 @@ describe("ACPX runtime sandbox", () => {
|
||||
}
|
||||
});
|
||||
|
||||
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" });
|
||||
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" });
|
||||
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"],
|
||||
|
||||
@@ -403,6 +403,10 @@ export async function prepareAcpxRuntimeSandbox(input: {
|
||||
`${JSON.stringify({
|
||||
model: input.binding.requestedModel,
|
||||
availableModels: [input.binding.requestedModel],
|
||||
// Claude reloads effort from settings on a new ACP connection. Keep
|
||||
// the bound choice available during model verification and resume;
|
||||
// the protocol guard still requires a provider acknowledgement.
|
||||
...(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) } }
|
||||
: {}),
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
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 { resolveAcpxProviderMode } from "../drivers/acpx/provider-mode.js";
|
||||
@@ -1191,6 +1192,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;
|
||||
@@ -1215,6 +1217,7 @@ export interface CapabilityRunnerdCodexTransportOptions {
|
||||
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. */
|
||||
@@ -3617,6 +3620,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");
|
||||
@@ -4912,6 +4916,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,
|
||||
),
|
||||
@@ -6597,6 +6602,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);
|
||||
|
||||
@@ -1178,6 +1178,15 @@ export const providerDescriptorSchema = {
|
||||
"high",
|
||||
"max"
|
||||
]
|
||||
},
|
||||
"claudeEffort": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
@@ -1201,6 +1210,26 @@ export const providerDescriptorSchema = {
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"required": [
|
||||
"claudeEffort"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "acpx"
|
||||
},
|
||||
"agent": {
|
||||
"const": "claude"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ describe("Pi controls catalog admission", () => {
|
||||
expect(cells).toHaveLength(4); expect(cells.every(c => c.profile.qualificationCandidate === "pi" && c.task.expectedRunCount === 1)).toBe(true);
|
||||
expect(cells.map(c => `${c.environment.id}/${c.task.id}`).sort()).toEqual(["daytona/pending-permission-stop", "daytona/same-turn-steering", "local/pending-permission-stop", "local/same-turn-steering"]);
|
||||
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(c => c.suite.id === "pi-controls")).toBe(false);
|
||||
expect(validateRunnerCatalog()).toHaveLength(722);
|
||||
expect(validateRunnerCatalog()).toHaveLength(724);
|
||||
for (const cell of cells) expect(buildRunnerE2EProcessEnvironment({}, [cell]).PAPERCLIP_RUNNER_ACPX_QUALIFICATION).toBeUndefined();
|
||||
expect(() => assertRemoteNativeEvidencePrerequisites(cells, {})).toThrow();
|
||||
});
|
||||
@@ -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: 19, 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: 19,
|
||||
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,
|
||||
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": "90f2e901d7f39bfb6bbadcf63bc78e410a3b071af980f3a23979747e3d5f2021",
|
||||
"pi-native": "3c77ced7edfb99aa12052f451398da7580466a54abadcaf287d31ee12c16382d",
|
||||
"native-active-stop": "2d4fdeeb75bd531b309bd1b35cfa5680ceefdeee6b7155b068dd011ce9901980",
|
||||
"rich-acp-warm-continuity": "331b88d9538a132670789fb7864df7df5f4bf294ae19b62f53d8a02f901774f0",
|
||||
"extended-harnesses": "5de4fac78d4d581bc0954f07b5cf2df2862d37f8b0205325eede6d9ef6d9f5e6",
|
||||
|
||||
Reference in new issue
Block a user