Finish Copilot production runtime on current master

Preserve the reviewed candidate source and mainline provider behavior.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-09 10:54:07 -05:00
1 parent 57e977be72
commit 27b601b172
81 files changed
+2517 -257

No files matched your search

+3 -4
View File
@@ -32,14 +32,13 @@
"requiresProviderPolicy": true
},
"copilot": {
"agentProfileVersion": 17,
"agentProfileVersion": 34,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
"requiresProviderPolicy": true,
"qualificationStatus": "pending"
"commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
"requiresProviderPolicy": true
},
"claude": {
"agentProfileVersion": 1,
+9 -3
View File
@@ -53,7 +53,10 @@
"dist-issue-thread",
"protocol",
"styles.css",
"README.md"
"README.md",
"scripts/install-copilot-assets.mjs",
"scripts/materialize-copilot-binary.mjs",
"scripts/copilot-inner-distribution.mjs"
],
"publishConfig": {
"executableFiles": [
@@ -81,7 +84,7 @@
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust",
"test:typescript": "pnpm run test:typescript:prep && vitest run",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/provider-pack-executable-shims.test.mjs && pnpm run test:copilot:contracts",
"test:typescript:vitest": "node ./scripts/run-pr-vitest-lane.mjs",
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
@@ -169,7 +172,10 @@
"verify:rootless": "bash scripts/verify-rootless-linux.sh",
"test:opencode:qualification": "PAPERCLIP_OPENCODE_QUALIFY=1 node --test scripts/qualify-opencode-runtime.test.mjs",
"generate:acpx-profiles": "node scripts/generate-acpx-profiles.mjs",
"check:acpx-profiles": "node scripts/generate-acpx-profiles.mjs --check"
"check:acpx-profiles": "node scripts/generate-acpx-profiles.mjs --check",
"check:copilot-profile": "node scripts/copilot-profile-identity.mjs --check",
"test:copilot:contracts": "node --test scripts/build-copilot-distribution.test.mjs scripts/copilot-inner-distribution.test.mjs scripts/copilot-smoke-resources.test.mjs scripts/discover-copilot-acp.test.mjs scripts/materialize-copilot-binary.test.mjs scripts/install-copilot-assets.test.mjs scripts/probe-copilot-acp.test.mjs scripts/qualify-copilot-acp.test.mjs test/acpx-message-boundaries-package-contract.test.mjs",
"postinstall": "node scripts/install-copilot-assets.mjs"
},
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.73.0",
@@ -2001,6 +2001,12 @@ impl AcpxCommandExecutor {
let Some(events) = events else { break };
let mut provider_turn_settled = false;
for event in events {
let provider_process_lost = matches!(
&event,
crate::acpx_provider_state::AcpxProviderStateEvent::TurnTerminal {
error: Some(error), ..
} if error.get("code").and_then(Value::as_str) == Some("ACPX_PROVIDER_PROCESS_LOST")
);
let normalized = project_acpx_state_event(&self.context, &event)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
let terminal = normalized.iter().find_map(|event| {
@@ -2010,7 +2016,10 @@ impl AcpxCommandExecutor {
)
.then(|| event.event_type.clone())
});
if terminal.is_some() {
// A failed provider cannot answer session controls. Its
// terminal receipt must remain publishable without another
// protocol request, and must revoke future warm admission.
if terminal.is_some() && !provider_process_lost {
let snapshot = self.goal_control("session.goal.get", &json!({}))?;
let state = self.state.as_mut().expect("ACPX session has durable state");
for (event_type, priority, payload) in snapshot.events {
@@ -2051,10 +2060,12 @@ impl AcpxCommandExecutor {
}
if let Some(event_type) = terminal {
state.active_turn_id = None;
state.lifecycle = if self
.session
.as_ref()
.is_some_and(|session| session.runtime_retired())
state.provider_exit_unconfirmed |= provider_process_lost;
state.lifecycle = if provider_process_lost
|| self
.session
.as_ref()
.is_some_and(|session| session.runtime_retired())
{
"closed"
} else {
@@ -2064,11 +2075,12 @@ impl AcpxCommandExecutor {
provider_turn_settled = true;
// An ACP goal has session lifetime, not prompt lifetime.
// Out-of-prompt goal updates remain observable after quiescence.
if state
.goal_projection
.pointer("/goal/status")
.and_then(Value::as_str)
== Some("active")
if !provider_process_lost
&& state
.goal_projection
.pointer("/goal/status")
.and_then(Value::as_str)
== Some("active")
{
continue;
}
@@ -4042,6 +4054,283 @@ mod tests {
fs::remove_dir_all(directory).unwrap();
}
#[cfg(unix)]
#[test]
fn copilot_attachment_refreshes_current_grant_without_changing_profile() {
let directory = temporary_directory("cross-run-attach");
let runtime = directory.join("runtime");
let workspace = directory.join("workspace");
fs::create_dir_all(&runtime).unwrap();
fs::create_dir_all(&workspace).unwrap();
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap();
fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap();
let marker = directory.join("provider-started");
let command = directory.join("sidecar");
write_artifact(
&command,
format!("#!/bin/sh\ntouch '{}'\n", marker.display()).as_bytes(),
true,
);
let launch_profile = AcpxLaunchProfile {
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: Vec::new(),
artifacts: vec![artifact(&command)],
};
let mut descriptor_value = descriptor("copilot");
descriptor_value["agentServerPackage"] = json!("@github/copilot");
descriptor_value["agentServerVersion"] = json!("1.0.88");
descriptor_value["agentRuntimePackage"] = Value::Null;
descriptor_value["agentRuntimeVersion"] = Value::Null;
descriptor_value["commandDigest"] = serde_json::from_str::<Value>(include_str!(
"../../../../test/fixtures/copilot-profile-v34-identity.json"
))
.unwrap()["commandDigest"]
.clone();
descriptor_value["providerPolicy"] = json!({ "readOnly": true });
descriptor_value["sidecarCommand"] = json!(command);
descriptor_value["runtimeContext"] = json!({ "instructions": { "digest": "stable" }, "mcp": { "digest": "before" }, "aggregateDigest": "before" });
descriptor_value["sidecarArgs"] = json!([]);
descriptor_value["runtimeDirectory"] = json!(runtime);
descriptor_value["cwd"] = json!(workspace);
let prior_root = directory.join("old-registered-copy");
let current_root = directory.join("new-registered-copy");
fs::create_dir_all(&prior_root).unwrap();
fs::create_dir_all(&current_root).unwrap();
descriptor_value["instructions"] = json!(format!(
"Current AGENT_HOME: {}. Custom entry.",
prior_root.display()
));
descriptor_value["runtimeContext"] = json!({
"aggregateDigest": "a".repeat(64),
"prompt": {"revision": "pinned", "digest": "b".repeat(64)},
"instructions": {
"entryPath": "AGENTS.md",
"bundle": {"digest": "c".repeat(64), "rootPath": "/old-bundle"},
"workingCopy": {"kind": "agent_files", "entryPath": "AGENTS.md", "rootPath": prior_root},
},
"skills": [{"key": "skill-1", "bundle": {"digest": "d".repeat(64), "rootPath": "/old-skill"}}],
"mcp": {"assignmentSetId": "assignment-1", "digest": "e".repeat(64), "bindingId": "old-run-binding"},
"futurePolicy": {"companyId": "company-1"},
});
let original_descriptor: AcpxProviderDescriptor =
serde_json::from_value(descriptor_value.clone()).unwrap();
let identity = AcpxProviderSessionIdentity {
kind: "acpx".to_owned(),
normalized_session_id: "session-1".to_owned(),
acpx_record_id: "record-1".to_owned(),
backend_session_id: "backend-1".to_owned(),
agent_session_id: "agent-1".to_owned(),
profile_digest: original_descriptor.command_digest.clone(),
workspace_digest: format!("sha256:{}", "a".repeat(64)),
requested_model: original_descriptor.model.clone(),
effective_model: original_descriptor.model.clone(),
permission_mode: Some(original_descriptor.permission_mode),
mode: original_descriptor.mode.clone(),
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
};
let operations = Vec::new();
let tool_set = AuthorizedToolSet {
schema: TOOL_SET_SCHEMA.to_owned(),
schema_version: 1,
catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(),
operations,
};
let launch_profile_digest = launch_profile.canonical_digest().unwrap();
let mut state = AcpxDurableState::new(original_descriptor, tool_set, launch_profile_digest);
state.lifecycle = "suspended".to_owned();
state.identity = Some(identity);
let original_config = test_config(&directory, Some(launch_profile.clone()));
let mut original = AcpxCommandExecutor::with_runner_config(&directory, &original_config);
original.state = Some(state);
let settled = original.state.clone().unwrap();
let ready = original.snapshot().unwrap().result;
assert_eq!(ready["warmAttachReady"], true);
assert_eq!(ready["warmAttachBlockers"], json!([]));
let blocked_states: [(&str, fn(&mut AcpxDurableState)); 4] = [
("durable_closed", |state| {
state.lifecycle = "closed".to_owned()
}),
("provider_exit_unconfirmed", |state| {
state.lifecycle = "prepared".to_owned();
state.provider_exit_unconfirmed = true;
}),
("provider_identity_unavailable", |state| {
state.lifecycle = "prepared".to_owned();
state.identity = None;
}),
("durable_active_turn", |state| {
state.lifecycle = "turn_active".to_owned();
state.active_turn_id = Some("turn-1".to_owned());
}),
];
for (blocker, mutate) in blocked_states {
original.state = Some(settled.clone());
mutate(original.state.as_mut().unwrap());
let snapshot = original.snapshot().unwrap().result;
assert_eq!(snapshot["warmAttachReady"], false, "{blocker}");
assert_eq!(snapshot["warmAttachBlockers"], json!([blocker]));
}
// A readiness probe must retain the old authority's audit events until
// the durable runner commits and acknowledges them, including recovery
// notices that run.attach itself is allowed to consume.
for event_type in ["session.resumed", "harness.diagnostic"] {
original.state = Some(settled.clone());
original
.state
.as_mut()
.unwrap()
.push(NormalizedProviderEvent {
event_type: event_type.to_owned(),
priority: EventPriority::P0,
payload: json!({}),
})
.unwrap();
let snapshot = original.snapshot().unwrap().result;
assert_eq!(snapshot["warmAttachReady"], false);
assert_eq!(
snapshot["warmAttachBlockers"],
json!(["durable_pending_events"])
);
assert_eq!(original.retained_events().unwrap().len(), 1);
original.acknowledge_events(1).unwrap();
assert_eq!(original.snapshot().unwrap().result["warmAttachReady"], true);
}
original.state = Some(settled);
assert!(!marker.exists(), "readiness must not start a provider");
original.save_state().unwrap();
let mut wrong_session_config = original_config.clone();
wrong_session_config.run_id = "run-2".to_owned();
wrong_session_config.normalized_session_id = "session-2".to_owned();
let mut wrong_session =
AcpxCommandExecutor::with_runner_config(&directory, &wrong_session_config);
assert!(wrong_session.restore().is_err());
let mut attached_config = original_config.clone();
attached_config.run_id = "run-2".to_owned();
let mut attached = AcpxCommandExecutor::with_runner_config(&directory, &attached_config);
attached.restore().unwrap();
assert!(!marker.exists());
let non_attach_error = attached
.execute(&Command {
schema: "paperclip.prp.command.v1".to_owned(),
command_id: "command-before-attach".to_owned(),
controller_seq: 1,
command_type: "session.snapshot".to_owned(),
issued_at: "2026-09-01T00:00:00.000Z".to_owned(),
deadline_at: None,
precondition: None,
payload: json!({}),
})
.unwrap_err();
assert!(non_attach_error
.to_string()
.contains("requires run.attach before commands from a new run"));
descriptor_value["runId"] = json!("run-2");
fs::remove_dir_all(&prior_root).unwrap();
descriptor_value["instructions"] = json!(format!(
"Current AGENT_HOME: {}. Fresh custom entry.",
current_root.display()
));
descriptor_value["runtimeContext"]["instructions"]["workingCopy"]["rootPath"] =
json!(current_root);
descriptor_value["runtimeContext"]["instructions"]["bundle"]["rootPath"] =
json!("/new-bundle");
descriptor_value["runtimeContext"]["skills"][0]["bundle"]["rootPath"] = json!("/new-skill");
descriptor_value["runtimeContext"]["mcp"]["bindingId"] = json!("new-run-binding");
attached
.attach_run(&json!({"provider": descriptor_value}))
.unwrap();
assert_eq!(attached.state.as_ref().unwrap().descriptor.run_id, "run-2");
assert!(!marker.exists());
let refreshed = &attached.state.as_ref().unwrap().descriptor;
assert_eq!(
refreshed.runtime_context,
descriptor_value["runtimeContext"]
);
assert_eq!(refreshed.instructions, descriptor_value["instructions"]);
let session_config = refreshed
.session_config(
attached.state.as_ref().unwrap().tool_set.clone(),
attached.state.as_ref().unwrap().identity.clone(),
Some(&launch_profile),
)
.unwrap();
assert_eq!(
session_config.runtime_context,
descriptor_value["runtimeContext"]
);
assert_eq!(
session_config.system_instructions,
descriptor_value["instructions"]
);
// Both persistence and the sidecar launch config receive the new grant.
let persisted: AcpxDurableState =
serde_json::from_slice(&fs::read(attached.state_path()).unwrap()).unwrap();
assert_eq!(
persisted.descriptor.runtime_context,
descriptor_value["runtimeContext"]
);
let mut same_run_mutation = descriptor_value.clone();
same_run_mutation["instructions"] = json!("A different grant in the same run");
assert!(attached
.attach_run(&json!({"provider": same_run_mutation}))
.unwrap_err()
.to_string()
.contains("new authenticated run"));
// In-place warm handoff executes under the old authority. Only the
// authenticated next-authority boundary may admit the new descriptor;
// event correlation stays on run-1 until durable activation completes.
assert!(original
.attach_run(&json!({"provider": descriptor_value}))
.is_err());
let warm_payload = json!({
"provider": descriptor_value,
"paperclipNextAuthority": {
"identity": {
"runnerInstanceId": original_config.runner_instance_id,
"environmentLeaseId": original_config.environment_lease_id,
"runId": "run-2",
"normalizedSessionId": original_config.normalized_session_id,
"turnId": "turn-2",
"itemId": "item-2",
},
"connection": {"mode": "connect", "connectUrl": original_config.connect_url},
},
});
let mut wrong_run = warm_payload.clone();
wrong_run["paperclipNextAuthority"]["identity"]["runId"] = json!("run-3");
assert!(original.attach_run(&wrong_run).is_err());
let mut wrong_session = warm_payload.clone();
wrong_session["paperclipNextAuthority"]["identity"]["normalizedSessionId"] =
json!("other-session");
assert!(original.attach_run(&wrong_session).is_err());
let mut changed_profile = warm_payload.clone();
changed_profile["provider"]["cwd"] = json!("/different-workspace");
assert!(original.attach_run(&changed_profile).is_err());
let mut refreshed = warm_payload.clone();
refreshed["provider"]["runtimeContext"]["mcp"] = json!({ "digest": "after" });
refreshed["provider"]["runtimeContext"]["aggregateDigest"] = json!("after");
let mut changed_context = refreshed.clone();
changed_context["provider"]["runtimeContext"]["instructions"] =
json!({ "digest": "changed" });
assert!(original.attach_run(&changed_context).is_err());
original.attach_run(&refreshed).unwrap();
assert_eq!(
original.state.as_ref().unwrap().descriptor.runtime_context["mcp"]["digest"],
"after"
);
assert_eq!(original.state.as_ref().unwrap().descriptor.run_id, "run-2");
assert_eq!(original.context.run_id, "run-1");
original.rotate_authority(&attached_config);
assert_eq!(original.context.run_id, "run-2");
assert!(!marker.exists());
fs::remove_dir_all(directory).unwrap();
}
#[cfg(unix)]
#[test]
fn active_turn_recovery_closes_without_starting_the_provider() {
@@ -11,7 +11,7 @@ pub(crate) enum RunAttachmentPolicy {
pub(crate) fn run_attachment_policy(agent: &str) -> RunAttachmentPolicy {
match agent {
"cursor" | "pi" => RunAttachmentPolicy::AuthenticatedRunGrants,
"cursor" | "copilot" | "pi" => RunAttachmentPolicy::AuthenticatedRunGrants,
"claude" => RunAttachmentPolicy::AuthenticatedAssetPaths,
_ => RunAttachmentPolicy::ImmutableInstructions,
}
@@ -27,6 +27,10 @@ mod tests {
run_attachment_policy("cursor"),
RunAttachmentPolicy::AuthenticatedRunGrants
);
assert_eq!(
run_attachment_policy("copilot"),
RunAttachmentPolicy::AuthenticatedRunGrants
);
assert_eq!(
run_attachment_policy("pi"),
RunAttachmentPolicy::AuthenticatedRunGrants
@@ -35,7 +39,7 @@ mod tests {
run_attachment_policy("claude"),
RunAttachmentPolicy::AuthenticatedAssetPaths
);
for agent in ["codex", "grok", "copilot", "unknown"] {
for agent in ["codex", "grok", "unknown"] {
assert_eq!(
run_attachment_policy(agent),
RunAttachmentPolicy::ImmutableInstructions
@@ -860,6 +860,7 @@ fn response_error_classification(error: &ResponseError) -> &'static str {
"ACPX_TOOL_CALL_STALE" => return "provider_tool_call_retired",
"AUTH_REQUIRED" => return "authentication_required",
"COPILOT_AUTH_REQUIRED" => return "authentication_required",
"COPILOT_INSTALLATION_INVALID" => return "agent_installation_invalid",
"COPILOT_POLICY_VIOLATION" => return "copilot_policy_violation",
"COPILOT_DETACHED_WORK_UNSUPPORTED" => return "copilot_detached_work_unsupported",
"COPILOT_ENTITLEMENT_DENIED" => return "provider_entitlement_denied",
@@ -64,6 +64,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut stdout = io::stdout().lock();
let mut next_sequence = 1_u64;
let mut goal = Value::Null;
let mut provider_lost = false;
let mut session_identity = Value::Null;
for line in stdin.lock().lines() {
let request: Value = serde_json::from_str(&line?)?;
@@ -75,6 +76,16 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.get("command")
.and_then(Value::as_str)
.ok_or("request command is missing")?;
if mode == "turns-provider-death" && provider_lost && command == "session.goal.get" {
write_json(
&mut stdout,
&json!({
"protocolVersion":GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION,
"id":id,"ok":false,"error":{"message":"provider has exited"}
}),
)?;
continue;
}
if let Some(journal) = admission_journal.as_mut() {
write_json(journal, &json!({"request":request}))?;
let response = if command == "tool.resolve" {
@@ -315,6 +326,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
| "turns-mismatched-reserved-result-terminal"
| "turns-unauthorized-tool"
| "turns-permission"
| "turns-provider-death"
| "permissions-forged-origin"
| "turns-retired"
| "turns-retired-terminal-first"
@@ -381,6 +393,27 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.map(|suffix| format!("call-{suffix}"))
.unwrap_or_else(|| "call-1".to_owned())
};
if command == "turn.start" && mode == "turns-provider-death" {
write_turn_event(
&mut stdout,
next_sequence,
"runtime.permission_requested",
"run-1",
turn_id,
json!({"requestId":"dead-input","kind":"edit","title":"Write the file?","choices":[{"key":"accept","label":"Allow once"},{"key":"cancel","label":"Deny"}]}),
)?;
next_sequence += 1;
provider_lost = true;
write_turn_event(
&mut stdout,
next_sequence,
"runtime.turn_terminal",
"run-1",
turn_id,
json!({"status":"failed","error":{"code":"ACPX_PROVIDER_PROCESS_LOST","message":"provider has exited","retryable":false}}),
)?;
next_sequence += 1;
}
if command == "turn.start" && matches!(mode, "turns" | "turns-wrong-scope") {
write_turn_event(
&mut stdout,
@@ -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:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
requires_provider_policy: true,
},
"claude" => AcpxReleaseProfile {
@@ -1023,6 +1023,109 @@ fn routes_reserved_completion_feedback_with_the_full_controller_catalog() {
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_provider() {
let directory = temporary_directory("acpx-provider-death");
let mode = "turns-provider-death";
let mut config = acpx_config(&directory, mode);
let mut payload = prepare_payload_with_mode(&directory, "codex", mode);
let identity: Value = serde_json::from_str(include_str!(
"../../../../test/fixtures/copilot-profile-v34-identity.json"
))
.unwrap();
let digest = identity["commandDigest"].as_str().unwrap();
*config
.acpx_launch_profile
.as_mut()
.unwrap()
.args
.last_mut()
.unwrap() = digest.to_owned();
let provider = &mut payload["provider"];
provider["agent"] = json!("copilot");
provider["model"] = json!("gpt-5.6-luna");
provider["permissionMode"] = json!("approve-paperclip");
provider["agentServerPackage"] = json!("@github/copilot");
provider["agentServerVersion"] = json!("1.0.88");
provider["agentRuntimePackage"] = Value::Null;
provider["agentRuntimeVersion"] = Value::Null;
provider["providerPolicy"] = json!({"readOnly":false});
provider["commandDigest"] = json!(digest);
provider["sidecarArgs"][3] = json!(digest);
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
executor
.execute(&command(1, "run.prepare", payload))
.unwrap();
executor
.execute(&command(2, "session.open", json!({})))
.unwrap();
executor
.execute(&command(
3,
"turn.start",
json!({"text":"Write", "turnId":"provider-turn-dead"}),
))
.unwrap();
let mut events = Vec::new();
for _ in 0..20 {
let batch = executor.poll_events().unwrap();
executor.acknowledge_events(batch.len()).unwrap();
events.extend(batch);
if events
.iter()
.any(|event| event.event_type == "run.terminal")
{
break;
}
std::thread::sleep(Duration::from_millis(5));
}
let expired = events
.iter()
.position(|event| event.event_type == "runtime_request.expired")
.unwrap_or_else(|| panic!("provider death did not expire input: {events:?}"));
let failed = events
.iter()
.position(|event| event.event_type == "turn.failed")
.unwrap();
assert!(expired < failed);
assert_eq!(events[expired].payload["reason"], "provider_process_lost");
assert_eq!(events[expired].payload["replayAllowed"], false);
assert_eq!(
events[failed].payload["providerTurnId"],
"provider-turn-dead"
);
assert_eq!(
events[failed].payload["error"]["code"],
"ACPX_PROVIDER_PROCESS_LOST"
);
assert_eq!(
events
.iter()
.filter(|event| event.event_type == "turn.failed")
.count(),
1
);
let snapshot = executor
.execute(&command(4, "session.snapshot", json!({})))
.unwrap()
.result;
assert_eq!(snapshot["status"], "closed");
assert_eq!(snapshot["activeProviderTurnId"], Value::Null);
assert_eq!(snapshot["warmAttachReady"], false);
assert!(executor
.execute(&command(
5,
"turn.start",
json!({"text":"Replay", "turnId":"replay-turn"})
))
.is_err());
executor
.execute(&command(6, "session.close", json!({})))
.unwrap();
shutdown_recovered_acpx_fixture(&mut executor, &directory);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn resumes_an_idle_acpx_session_in_a_cold_replacement_runner() {
let directory = temporary_directory("acpx-cold-idle-recovery");
@@ -152,7 +152,15 @@ try {
mkdirSync(dirname(stableNodeCommand), { recursive: true, mode: 0o755 });
copyFileSync(process.execPath, stableNodeCommand);
chmodSync(stableNodeCommand, 0o755);
const relocatedNode = spawnSync(stableNodeCommand, ["--version"], {
// A freshly copied x64 Mach-O can stall when launched directly from a
// translated Node process. Select Rosetta explicitly for this build probe;
// native Intel and every other platform still execute the copied bytes directly.
const translated = process.platform === "darwin" && process.arch === "x64"
&& spawnSync("/usr/sbin/sysctl", ["-n", "sysctl.proc_translated"], {
encoding: "utf8", timeout: 1_000,
}).stdout?.trim() === "1";
const relocatedNode = spawnSync(translated ? "/usr/bin/arch" : stableNodeCommand,
translated ? ["-x86_64", stableNodeCommand, "--version"] : ["--version"], {
cwd: temporaryRoot, env: { PATH: "/usr/bin:/bin" }, encoding: "utf8", timeout: buildNodeStartupTimeout(),
});
if (relocatedNode.status !== 0 || relocatedNode.stdout.trim() !== `v${process.versions.node}`) {
@@ -167,7 +175,7 @@ try {
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.profileDigest)
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.closureDigest)) throw new Error("Candidate builder omitted its pinned identity");
candidateProviders[provider] = { version: metadata.version, profileDigest: metadata.profileDigest,
closureDigest: metadata.closureDigest, qualification: (provider === "cursor" || provider === "pi") ? "qualified" : "pending", path: assetPath,
closureDigest: metadata.closureDigest, qualification: ["pi", "cursor", "copilot"].includes(provider) ? "qualified" : "pending", path: assetPath,
sha256: sha256Tree(join(temporaryRoot, assetPath)) };
}
@@ -6,14 +6,14 @@ const CANDIDATES = new Set(["cursor", "copilot", "pi"]);
/** Only materialized providers enter the serialized manifest and its digest. */
export function providerPackManifestFields(providers, candidates) {
return {
...(["pi", "cursor"].some(provider => providers[provider]) ? { providers: Object.fromEntries(["pi", "cursor"].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", ...candidates].some(provider => providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", "cursor", "copilot"].some(provider => providers[provider]) ? { providers: Object.fromEntries(["pi", "cursor", "copilot"].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", ...candidates].some(provider => provider !== "copilot" && providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => provider !== "copilot" && providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
};
}
export function providerPackProviders(platform, architecture, candidates) {
const nativeSupported = ["darwin-arm64", "darwin-x64", "linux-x64"].includes(`${platform}-${architecture}`);
return [...new Set([...(nativeSupported ? ["pi", "cursor"] : []), ...candidates])];
return [...new Set([...(nativeSupported ? ["pi", "cursor", "copilot"] : []), ...candidates])];
}
export function parseProviderPackArguments(args) {
@@ -1,28 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
import { parseProviderPackArguments, materializeCandidateProviderPack, providerPackProviders, providerPackManifestFields } from "./candidate-provider-pack.mjs";
import { createHash } from "node:crypto";
test("candidate options stay explicit and supported native assets are included by default", () => {
// Matches the canonical manifest hashing used by the builder and admission.
function digest(value) {
const canonical = value => 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);
return createHash("sha256").update(canonical(value)).digest("hex");
}
test("provider manifest digest survives disk JSON on supported and unsupported targets", () => {
const cursor = { version: "pinned", profileDigest: "sha256:profile", closureDigest: "sha256:closure" };
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"], ["linux", "arm64"], ["win32", "x64"]]) {
const selected = providerPackProviders(platform, architecture, []);
const payload = { target: { platform, architecture }, ...providerPackManifestFields(selected.includes("cursor") ? { cursor } : {}, []) };
const diskPayload = JSON.parse(JSON.stringify(payload));
assert.deepEqual(diskPayload, payload);
assert.equal(digest(diskPayload), digest(payload));
assert.equal(diskPayload.providers?.cursor?.version, selected.includes("cursor") ? "pinned" : undefined);
}
const candidate = providerPackManifestFields({ cursor }, ["cursor"]);
assert.deepEqual(candidate.candidateProviders, { cursor });
assert.equal(digest(JSON.parse(JSON.stringify(candidate))), digest(candidate));
});
test("candidate selection is explicit", () => {
assert.deepEqual(parseProviderPackArguments(["--", "/pack"]), { output: "/pack", candidates: [] });
assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=pi,cursor"]), { output: "/pack", candidates: ["pi", "cursor"] });
});
test("normal packs include Cursor and Copilot on their three pinned targets without breaking other hosts", () => {
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"]]) {
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor"]);
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor", "pi", "copilot"]), ["pi", "cursor", "copilot"]);
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor", "copilot"]);
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor"]), ["pi", "cursor", "copilot"]);
}
assert.deepEqual(providerPackProviders("linux", "arm64", []), []);
assert.deepEqual(providerPackProviders("win32", "x64", []), []);
assert.deepEqual(providerPackProviders("linux", "arm64", ["cursor"]), ["cursor"]);
});
test("serialized manifests include every materialized qualified provider", () => {
const pi = { qualification: "qualified", profileDigest: "pi-digest" };
const cursor = { qualification: "qualified", profileDigest: "cursor-digest" };
const copilot = { qualification: "pending", profileDigest: "copilot-digest" };
assert.deepEqual(providerPackManifestFields({ pi }, []), { providers: { pi }, candidateProviders: { pi } });
assert.deepEqual(providerPackManifestFields({ pi, cursor, copilot }, ["copilot"]), {
providers: { pi, cursor }, candidateProviders: { pi, copilot },
});
assert.deepEqual(providerPackManifestFields({}, []), {});
});
test("candidate builder cannot admit unknown providers, options or duplicate assets", async () => {
for (const args of [["--candidate-providers=cursor,cursor"], ["--candidate-providers=other"], ["--executable=/tmp/x"], ["/one", "/two"]]) {
assert.throws(() => parseProviderPackArguments(args));
@@ -30,6 +46,15 @@ test("candidate builder cannot admit unknown providers, options or duplicate ass
await assert.rejects(materializeCandidateProviderPack({ provider: "arbitrary", outputRoot: "/tmp/unused" }), /Unknown candidate/);
});
test("normal Copilot inventory needs no candidate override and has no duplicate candidate", () => {
const copilot = { qualification: "qualified", version: "1.0.88" };
for (const candidates of [[], ["copilot"]]) {
const fields = providerPackManifestFields({ copilot }, candidates);
assert.equal(fields.providers.copilot, copilot);
assert.equal(fields.candidateProviders?.copilot, undefined);
assert.equal(digest(JSON.parse(JSON.stringify(fields))), digest(fields));
}
});
test("Copilot pack selection reaches only the pinned native archive builder", async t => {
const requests = [];
t.mock.method(globalThis, "fetch", async (url, options) => {
@@ -0,0 +1,106 @@
// Regenerate after changing any bound implementation; live qualification must
// restart whenever the resulting identity changes. Historical identities remain.
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = resolve(root, "test/fixtures/copilot-profile-v34-identity.json");
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
const declaration = { ...historical.declaration, agentProfileVersion: 34 };
delete declaration.permissionIdentitySourceSha256;
const sources = {
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
displayProjectionSourceSha256: "src/drivers/acpx/copilot-extension-adapter.ts",
configuredEnvironmentSourceSha256: "src/configured-environment.ts",
configuredEnvironmentRustSourceSha256: "runner/crates/runner-core/src/configured_environment.rs",
profileActivitySourceSha256: "src/drivers/acpx/profile-activity.ts",
profileExtensionsSourceSha256: "src/drivers/acpx/profile-extensions.ts",
profileInstallationSourceSha256: "src/drivers/acpx/profile-installation.ts",
attachmentPolicySourceSha256: "runner/crates/runner-core/src/acpx_provider_capabilities.rs",
singleReadEvidenceSourceSha256: "src/drivers/acpx/single-read-evidence.ts",
nativeSessionRuntimeSourceSha256: "src/native-session-runtime.ts",
nativeRuntimeContextSourceSha256: "src/contracts/runtime-context.ts",
nativeSessionContractSourceSha256: "src/contracts/native-session-backend.ts",
harnessSessionContractSourceSha256: "src/contracts/harness-driver.ts",
harnessBackendSourceSha256: "src/backends/harness-driver-backend.ts",
harnessSessionSourceSha256: "src/drivers/codex/codex-harness-session.ts",
transportContractSourceSha256: "src/drivers/codex/app-server-transport.ts",
nativeBackendSourceSha256: "src/backends/codex-native-backend.ts",
nativeBackendFactorySourceSha256: "src/backends/native-backend-factory.ts",
nativeInstructionsSourceSha256: "src/backends/runtime-context.ts",
runtimeRequestBridgeSourceSha256: "src/live/runnerd-codex-transport.ts",
runtimeRequestFacadeSourceSha256: "src/drivers/codex/codex-session-notifications.ts",
runtimeRequestNotificationSourceSha256: "src/drivers/codex/codex-thread-normalization.ts",
guardedInstallationSourceSha256: "src/drivers/acpx/installation-integrity.ts",
nativeExecutionSourceSha256: "src/contracts/native-execution.ts",
profileCompatibilitySourceSha256: "src/drivers/acpx/profile-compatibility.ts",
installationSourceSha256: "src/drivers/acpx/copilot-installation.ts",
assetOwnershipSourceSha256: "src/drivers/acpx/provider-assets-root.ts",
assetInstallerSourceSha256: "scripts/install-copilot-assets.mjs",
credentialBindingSourceSha256: "src/drivers/acpx/environment.ts",
sidecarErrorSourceSha256: "src/cli/acpx-sidecar-input.ts",
profileSourceSha256: "src/drivers/acpx/copilot-profile.ts",
metadataProbeSourceSha256: "src/drivers/copilot-metadata-probe.ts",
acpxPatchSha256: "../../patches/acpx@0.13.1.patch",
policySha256: "src/drivers/acpx/copilot-policy.ts",
distributionSourceSha256: "scripts/materialize-copilot-binary.mjs",
innerDistributionSourceSha256: "scripts/copilot-inner-distribution.mjs",
permissionContextSourceSha256: "src/drivers/acpx/copilot-permission-context.ts",
permissionAdapterSourceSha256: "src/drivers/acpx/acp-permission-adapter.ts",
permissionToolIdentitySourceSha256: "src/drivers/acpx/cursor-plan-tool-identity.ts",
permissionLocationsSourceSha256: "src/drivers/acpx/safe-locations.ts",
permissionRedactionSourceSha256: "src/semantic-tools/redaction.ts",
permissionClassifierSourceSha256: "src/drivers/acpx/generated-sidecar-contract.ts",
semanticReceiptSourceSha256: "src/drivers/semantic-tool-receipt.ts",
semanticBridgeSourceSha256: "src/drivers/runner-tool-bridge.ts",
toolEvidenceSourceSha256: "src/drivers/acpx/copilot-tool-evidence.ts",
semanticSidecarSourceSha256: "src/cli/acpx-runtime-sidecar.ts",
semanticDirectDriverSourceSha256: "src/drivers/acpx/codex-acpx-driver.ts",
semanticValidationSourceSha256: "src/protocol/replay-contract.ts",
semanticNormalizationSourceSha256: "src/protocol/result-normalization.ts",
semanticCompletionContractSourceSha256: "src/contracts/completion-result.ts",
semanticValidatorsSourceSha256: "src/protocol/generated/standalone-validators.ts",
semanticSchemaBundleSourceSha256: "src/protocol/generated/schema-bundle.ts",
semanticSidecarProtocolSourceSha256: "src/drivers/acpx/sidecar-protocol.ts",
runtimeHostSourceSha256: "src/drivers/acpx/runtime-host.ts",
runtimeAdapterSourceSha256: "src/drivers/acpx/codex-runtime-adapter.ts",
runtimeSandboxSourceSha256: "src/drivers/acpx/runtime-sandbox.ts",
cancellationSourceSha256: "src/drivers/acpx/turn-cancellation.ts",
agentFilesSourceSha256: "src/drivers/acpx/agent-files-binding.ts",
nativeDistributionSourceSha256: "src/drivers/acpx/native-distribution-integrity.ts",
};
for (const [field, path] of Object.entries(sources)) {
declaration[field] = createHash("sha256").update(readFileSync(resolve(root, path))).digest("hex");
}
const sorted = Object.fromEntries(Object.entries(declaration).sort(([a], [b]) => a.localeCompare(b)));
const commandDigest = `sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`;
const identity = `${JSON.stringify({ commandDigest, declaration }, null, 2)}\n`;
// The shared manifest now generates both TypeScript and Rust admission. Update
// only Copilot; other providers retain their upstream release attestations.
const manifestPath = resolve(root, "acpx-profiles.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
if (process.argv.includes("--check")) {
if (manifest.profiles.copilot.commandDigest !== commandDigest
|| manifest.profiles.copilot.agentProfileVersion !== declaration.agentProfileVersion) {
throw new Error("Copilot identity is stale in acpx-profiles.json");
}
} else {
manifest.profiles.copilot.commandDigest = commandDigest;
manifest.profiles.copilot.agentProfileVersion = declaration.agentProfileVersion;
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
}
// The builder reads the active manifest; no duplicate digest literal is allowed.
const distributionSource = readFileSync(resolve(root, "scripts/build-copilot-distribution.mjs"), "utf8");
if (!distributionSource.includes("profiles.profiles.copilot.commandDigest")) {
throw new Error("Copilot distribution builder must use the active manifest identity");
}
// The shared generator validates this exact attestation before emitting admission.
if (process.argv.includes("--check")) {
if (readFileSync(fixture, "utf8") !== identity) throw new Error("Copilot source identity is stale");
} else writeFileSync(fixture, identity);
execFileSync(process.execPath, [resolve(root, "scripts/generate-acpx-profiles.mjs"),
...(process.argv.includes("--check") ? ["--check"] : [])], { stdio: "inherit" });
console.log(commandDigest);
@@ -35,3 +35,25 @@ export async function withCopilotSmokeResources(installation, callback, dependen
}
}
}
/** Confirm this smoke-owned provider exits, even when it ignores SIGTERM. */
export async function stopCopilotSmokeChild(child, { terminateTimeoutMs = 5_000, killTimeoutMs = 5_000 } = {}) {
if (!child || child.exitCode !== null || child.signalCode !== null) return;
const exited = once(child, "exit");
child.stdin?.end();
child.kill("SIGTERM");
try { await boundedExit(exited, terminateTimeoutMs); }
catch (error) {
if (child.exitCode !== null || child.signalCode !== null) throw error;
child.kill("SIGKILL");
await boundedExit(exited, killTimeoutMs);
}
}
async function boundedExit(exited, timeoutMs) {
let timer;
try {
await Promise.race([exited, new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error("Copilot smoke cleanup was not confirmed")), timeoutMs);
})]);
} finally { clearTimeout(timer); }
}
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import test from "node:test";
import { withCopilotSmokeResources } from "./copilot-smoke-resources.mjs";
import { stopCopilotSmokeChild, withCopilotSmokeResources } from "./copilot-smoke-resources.mjs";
function resources(failure) {
const actions = [];
@@ -41,3 +41,19 @@ test("successful callback result is returned after all resource cleanup", async
assert.equal(await withCopilotSmokeResources(r.installation, async ({ root, fixtureRequests }) => { assert.equal(root, "/fixture/private"); assert.deepEqual(fixtureRequests, []); return "ok"; }, r.dependencies), "ok");
assert.deepEqual(r.actions, ["workspace_created", "connections_closed", "server_closed", "lease_closed", "workspace_removed"]);
});
test("ignored SIGTERM is followed by bounded SIGKILL and confirmed process exit", async () => {
const { spawn } = await import("node:child_process");
const { once } = await import("node:events");
const child = spawn(process.execPath, ["-e", 'process.on("SIGTERM", () => {}); process.send("ready"); setInterval(() => {}, 1000);'], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
try {
await Promise.race([once(child, "message"), new Promise((_, reject) => setTimeout(() => reject(new Error("child readiness timed out")), 3_000).unref())]);
await stopCopilotSmokeChild(child, { terminateTimeoutMs: 50, killTimeoutMs: 1_000 });
assert.equal(child.signalCode, "SIGKILL");
assert.throws(() => process.kill(child.pid, 0), { code: "ESRCH" });
} finally {
if (child.exitCode === null && child.signalCode === null) {
const exited = once(child, "exit"); child.kill("SIGKILL"); await exited;
}
}
});
@@ -1,3 +1,4 @@
import { StringDecoder } from "node:string_decoder";
// Metadata only. Supply an explicit token; this closed protocol client never prompts.
// Usage: node discover-copilot-acp.mjs <verified-pack> <safe-output.json> [exact-model-id]
import assert from "node:assert/strict";
@@ -24,6 +25,7 @@ export async function withDiscoveryWorkspace(install, callback, filesystem = { m
}
export function createDiscoveryRpc(child, { deadlineMs = 25_000, maxBytes = 2_097_152 } = {}) {
const decoder = new StringDecoder("utf8");
const pending = new Map(), observedMethods = {};
let nextId = 0, buffer = "", bytes = 0, failure;
function fail(code) {
@@ -39,7 +41,7 @@ export function createDiscoveryRpc(child, { deadlineMs = 25_000, maxBytes = 2_09
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > maxBytes) { fatal("provider_wire_limit"); return; }
buffer += chunk.toString();
buffer += decoder.write(chunk);
while (buffer.includes("\n")) {
const end = buffer.indexOf("\n"), line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
if (!line.trim()) continue;
@@ -38,3 +38,12 @@ test("closed discovery protocol preserves numeric zero and rejects prompts", asy
assert.deepEqual(await pending, { protocolVersion: 1 });
assert.throws(() => rpc.request("session/prompt", {}), /cannot send/); rpc.close();
});
test("discovery preserves multibyte model names when every byte arrives separately", async () => {
const child = childFixture(), rpc = createDiscoveryRpc(child);
const result = { models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "東京 🚀 café" }] } };
const pending = rpc.request("session/new", {});
const wire = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 0, result })}\n`);
for (const byte of wire) child.stdout.emit("data", Buffer.from([byte]));
assert.deepEqual(await pending, result); rpc.close();
});
@@ -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-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v34-identity.json"]]) {
const identity = await readJson(path);
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
@@ -0,0 +1,94 @@
import { access, lstat, mkdir, readFile, realpath, rmdir } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath, pathToFileURL } from "node:url";
import { COPILOT_DISTRIBUTIONS, materializePinnedCopilotBinary } from "./materialize-copilot-binary.mjs";
const packageRoot = await realpath(fileURLToPath(new URL("../", import.meta.url)));
const owner = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
if (!["@paperclipai/paperclip-runner", "@paperclipai/server"].includes(owner.name)) {
throw new Error("Copilot installer must belong to a runner package owner");
}
const vendored = owner.name === "@paperclipai/server";
const runtimeRoot = vendored ? join(packageRoot, "dist/vendor/paperclip-runner") : join(packageRoot, "dist");
/** Install only the native npm package for this platform; never execute it. */
export async function installCopilotAssets() {
const { platform, arch: architecture } = process;
const target = `${platform}-${architecture}`;
if (!Object.prototype.hasOwnProperty.call(COPILOT_DISTRIBUTIONS, target)) {
return { status: "unsupported-platform", target };
}
const verifierPath = join(runtimeRoot, "drivers/acpx/copilot-installation.js");
try { await access(verifierPath); }
catch (error) {
if (error.code !== "ENOENT") throw error;
// A source workspace installs dependencies before it compiles the verifier.
// Published packages exclude src, so a malformed npm artifact cannot use
// this path. build:typescript performs installation after compilation.
await access(join(packageRoot, vendored ? "src/services/copilot-connection-probe.ts" : "src/drivers/acpx/copilot-installation.ts"));
await access(resolve(packageRoot, vendored ? "../pnpm-workspace.yaml" : "../../pnpm-workspace.yaml"));
return { status: "source-workspace-pending-build", target };
}
const assets = join(packageRoot, "provider-assets", "copilot");
for (const directory of [dirname(assets), assets]) {
await mkdir(directory, { recursive: true, mode: 0o755 });
const stat = await lstat(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory) {
throw new Error("Copilot install directory must not redirect through links");
}
}
const destination = join(assets, target);
const originalRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT;
const originalManifest = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST;
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT = packageRoot;
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST = join(packageRoot, "package.json");
try {
const { COPILOT_CLOSURE_SHA256, verifyCopilotInstallation } = await import(pathToFileURL(join(runtimeRoot, "drivers/acpx/copilot-installation.js")));
const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(runtimeRoot, "drivers/acpx/qualified-profiles.js")));
const verify = () => verifyCopilotInstallation(QUALIFIED_ACPX_PROFILES.copilot);
try { await mkdir(destination, { mode: 0o755 }); }
catch (error) {
if (error.code !== "EEXIST") throw error;
const stat = await lstat(destination);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(destination) !== destination) {
throw new Error("Copilot installed assets must not redirect through links");
}
await verify();
return { status: "verified-existing", target };
}
const ownedDirectory = await lstat(destination, { bigint: true });
try {
// Official native packages export their executable, not package.json.
// Resolve that path without loading or executing the native module.
const nativePackageRoot = dirname(createRequire(import.meta.url).resolve(COPILOT_DISTRIBUTIONS[target].packageName));
const materialized = materializePinnedCopilotBinary({ packageRoot: nativePackageRoot, platform, architecture, targetDirectory: destination });
if (materialized.closureSha256 !== COPILOT_CLOSURE_SHA256[target]) {
throw new Error("Copilot installation differs from the runtime closure pin");
}
await verify();
return { status: "installed", target };
} catch (error) {
// The materializer rolls back only files it created. Remove this newly
// created directory only if it is now empty; retain foreign files or
// failed verification evidence rather than deleting a directory tree.
try {
const current = await lstat(destination, { bigint: true });
if (current.isDirectory() && !current.isSymbolicLink() && current.dev === ownedDirectory.dev && current.ino === ownedDirectory.ino) await rmdir(destination);
}
catch (cleanupError) { if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(cleanupError.code)) throw new AggregateError([error, cleanupError], "Copilot install and empty-directory cleanup failed"); }
throw error;
}
} finally {
for (const [key, value] of [["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", originalRoot], ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", originalManifest]]) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
}
const invokedPath = process.argv[1] ? await realpath(resolve(process.argv[1])) : null;
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
const result = await installCopilotAssets();
process.stdout.write(`Copilot runtime assets: ${result.status} (${result.target})\n`);
}
@@ -0,0 +1,114 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { cp, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { build } from "esbuild";
import { COPILOT_DISTRIBUTIONS } from "./materialize-copilot-binary.mjs";
const runnerRoot = fileURLToPath(new URL("../", import.meta.url));
const serverRoot = fileURLToPath(new URL("../../../server/", import.meta.url));
const target = `${process.platform}-${process.arch}`;
const distribution = COPILOT_DISTRIBUTIONS[target];
const require = createRequire(import.meta.url);
const helpers = ["install-copilot-assets.mjs", "materialize-copilot-binary.mjs", "copilot-inner-distribution.mjs"];
function invoke(script) {
return spawnSync(process.execPath, [script], {
env: { PATH: `${dirname(process.execPath)}:/usr/bin:/bin`, TMPDIR: tmpdir() },
encoding: "utf8", timeout: 120_000, maxBuffer: 64 * 1024,
});
}
function succeeded(result, status) {
assert.equal(result.error, undefined);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, new RegExp(status));
}
for (const owner of ["runner", "server"]) {
test(`fresh ${owner} package installs verified assets without a workspace or qualification override`, {
skip: !distribution && "Copilot does not advertise this platform", timeout: 180_000,
}, async () => {
const root = await realpath(await mkdtemp(join(tmpdir(), "copilot-install-package-")));
try {
const native = dirname(require.resolve(distribution.packageName));
await mkdir(join(root, "node_modules/@github"), { recursive: true });
await symlink(native, join(root, "node_modules", distribution.packageName), "dir");
await writeFile(join(root, "package.json"), JSON.stringify({
name: owner === "server" ? "@paperclipai/server" : "@paperclipai/paperclip-runner", type: "module",
}));
const runtimeRoot = owner === "server" ? join(root, "dist/vendor/paperclip-runner") : join(root, "dist");
// Compile the actual installation closure into a published package layout.
// A clean static-check checkout has no prebuilt dist directory.
await build({
entryPoints: [join(runnerRoot, "src/drivers/acpx/copilot-installation.ts"),
join(runnerRoot, "src/drivers/acpx/qualified-profiles.ts")],
outbase: join(runnerRoot, "src"), outdir: runtimeRoot,
bundle: true, platform: "node", format: "esm", target: "node24",
logLevel: "silent",
});
if (owner === "server") {
// The public server owns a compiled sidecar beside the vendored verifier.
// This installer fixture does not execute the sidecar.
await mkdir(join(runtimeRoot, "cli"), { recursive: true });
await writeFile(join(runtimeRoot, "cli/acpx-runtime-sidecar.cjs"), "// installer fixture sidecar\n");
}
const scriptRoot = owner === "server" ? join(root, "dist") : join(root, "scripts");
await mkdir(scriptRoot, { recursive: true });
for (const name of helpers) await cp(join(runnerRoot, "scripts", name), join(scriptRoot, name));
let installer = join(scriptRoot, "install-copilot-assets.mjs");
if (owner === "server") {
await mkdir(join(root, "scripts"));
installer = join(root, "scripts/install-copilot-assets.mjs");
await cp(join(serverRoot, "scripts/install-copilot-assets.mjs"), installer);
}
succeeded(invoke(installer), "installed");
succeeded(invoke(installer), "verified-existing");
const assets = join(root, "provider-assets");
const closure = join(assets, "copilot", target, ".paperclip-copilot-closure.json");
const original = await readFile(closure);
const corrupted = JSON.parse(original.toString());
corrupted.entries[0].sha256 = "0".repeat(64);
await writeFile(closure, JSON.stringify(corrupted));
const failure = invoke(installer);
assert.notEqual(failure.status, 0);
assert.match(failure.stderr, /COPILOT_INSTALLATION_INVALID/);
assert.notDeepEqual(await readFile(closure), original, "failed verification must retain its evidence");
await writeFile(closure, original);
succeeded(invoke(installer), "verified-existing");
const retained = join(root, "retained-assets");
const outside = join(root, "outside");
await mkdir(outside);
await rename(assets, retained);
await symlink(outside, assets, "dir");
const linked = invoke(installer);
assert.notEqual(linked.status, 0);
assert.match(linked.stderr, /must not redirect through links/);
await assert.rejects(readFile(join(outside, "copilot", target, "copilot")), { code: "ENOENT" });
await rm(assets);
await rename(retained, assets);
succeeded(invoke(installer), "verified-existing");
} finally { await rm(root, { recursive: true, force: true }); }
});
}
test("an unbuilt source workspace defers installation; a published package with missing build output fails", async () => {
const root = await realpath(await mkdtemp(join(tmpdir(), "copilot-install-bootstrap-")));
try {
const server = join(root, "server");
await mkdir(join(server, "scripts"), { recursive: true });
const script = join(server, "scripts/install-copilot-assets.mjs");
await cp(join(serverRoot, "scripts/install-copilot-assets.mjs"), script);
assert.notEqual(invoke(script).status, 0);
await mkdir(join(server, "src/services"), { recursive: true });
await writeFile(join(server, "src/services/copilot-connection-probe.ts"), "// source workspace\n");
assert.notEqual(invoke(script).status, 0);
await writeFile(join(root, "pnpm-workspace.yaml"), "packages: [server]\n");
succeeded(invoke(script), "source-workspace-pending-build");
await assert.rejects(readFile(join(server, "provider-assets/copilot", target, "copilot")), { code: "ENOENT" });
} finally { await rm(root, { recursive: true, force: true }); }
});
@@ -1,3 +1,4 @@
import { StringDecoder } from "node:string_decoder";
// Real-service qualification only. One explicit scenario sends one ACP prompt.
// Usage: node qualify-copilot-acp.mjs <verified-pack> <private-output.json> <deny-write|detached-shell>
// Bind only COPILOT_GITHUB_TOKEN. Do not run without a reconciled billing reservation.
@@ -62,134 +63,139 @@ export async function runProbe(packInput, output, scenario, token) {
finally { await lease.close(); }
throw error;
}
const marker = join(root, "qualification-marker.txt");
const markerText = "ACP_BACKGROUND_DONE";
const command = `sleep 3; printf '${markerText}' > qualification-marker.txt`;
const start = performance.now();
const elapsed = () => Math.round(performance.now() - start);
const evidence = { schema: "paperclip.copilot-live-risk-probe/v1", scenario, model, sourceRevision: manifest.payload.runnerSourceRevision,
providerPackDigest: manifest.digest, profileDigest: install.commandDigest, promptRequestsSent: 0, upstreamModelRequestCount: null,
providerReportedCostUsd: null, qualificationStatus: "pending", activeTurnDeadlineSeconds: 120, outerDeadlineSeconds: 180,
automaticRetries: 0, permissions: [], markerSamples: [], wire: [], detachedToolObserved: false,
backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false };
let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
let outerTimer, markerTimer;
const pending = new Map();
const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" };
async function sampleMarker(phase) {
let content;
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
const sample = { elapsedMs: elapsed(), phase, exists: content !== undefined, matches: content === markerText };
evidence.markerSamples.push(sample);
return sample;
}
function failPending(code) {
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
pending.clear();
}
const onShutdown = () => { evidence.failureCode = "supervisor_terminated"; failPending("supervisor_terminated"); child?.kill("SIGTERM"); };
process.once("SIGTERM", onShutdown); process.once("SIGINT", onShutdown);
function send(message) { child.stdin.write(`${JSON.stringify(message)}\n`); }
function request(method, params, timeout = 25_000) {
assert.ok(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/prompt", "session/close"].includes(method));
const id = nextId++;
if (method === "session/prompt") promptId = id;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
pending.set(id, { resolve, reject, timer }); send({ jsonrpc: "2.0", id, method, params });
});
}
try {
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) {
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
const marker = join(root, "qualification-marker.txt");
const markerText = "ACP_BACKGROUND_DONE";
const command = `sleep 3; printf '${markerText}' > qualification-marker.txt`;
const start = performance.now();
const elapsed = () => Math.round(performance.now() - start);
const evidence = { schema: "paperclip.copilot-live-risk-probe/v1", scenario, model, sourceRevision: manifest.payload.runnerSourceRevision,
providerPackDigest: manifest.digest, profileDigest: install.commandDigest, promptRequestsSent: 0, upstreamModelRequestCount: null,
providerReportedCostUsd: null, qualificationStatus: "pending", activeTurnDeadlineSeconds: 120, outerDeadlineSeconds: 180,
automaticRetries: 0, permissions: [], markerSamples: [], wire: [], detachedToolObserved: false,
backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false };
let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
let outerTimer, markerTimer;
const decoder = new StringDecoder("utf8");
const pending = new Map();
const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" };
async function sampleMarker(phase) {
let content;
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
const sample = { elapsedMs: elapsed(), phase, exists: content !== undefined, matches: content === markerText };
evidence.markerSamples.push(sample);
return sample;
}
await writeFile(join(env.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 });
await sampleMarker("before_launch");
child = lease.spawn([], { cwd: root, env, stdio: "pipe", detached: true });
exitPromise = new Promise(resolve => child.once("close", (code, signal) => { failPending("provider_exited"); resolve({ code, signal }); }));
outerTimer = setTimeout(() => { failPending("outer_deadline"); child.kill("SIGTERM"); }, 180_000);
child.once("error", () => failPending("provider_spawn_failure"));
child.stdin.on("error", () => failPending("provider_stdin_failure"));
child.stderr.on("data", () => {});
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; }
buffer += chunk.toString();
while (buffer.includes("\n")) {
const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
if (!line.trim()) continue;
let message;
try { message = JSON.parse(line); } catch { failPending("malformed_json"); child.kill("SIGTERM"); return; }
if (!message || typeof message !== "object" || Array.isArray(message)) { failPending("malformed_message"); child.kill("SIGTERM"); return; }
evidence.wire.push({ elapsedMs: elapsed(), message });
if (message.method) {
const params = message.params;
if (message.method === "session/request_permission" && message.id !== undefined) {
const decision = permissionDecision(scenario, params, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined, command);
const writeAttempt = isExactMarkerWrite(params?.toolCall, marker);
evidence.permissions.push({ elapsedMs: elapsed(), requestId: message.id, offeredOptions: params.options, ...decision, writeAttempt });
send({ jsonrpc: "2.0", id: message.id, result: { outcome: decision.outcome } });
} else if (message.id !== undefined) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } });
}
if (params?.sessionId === sessionId) {
const arguments_ = params.data?.arguments ?? params.toolCall?.rawInput ?? params.update?.rawInput;
if (arguments_?.command === command && arguments_.detach === true) evidence.detachedToolObserved = true;
const content = JSON.stringify(params);
if (!terminalSeen && content.includes("completed with exit code 0") && content.includes("shell")) evidence.backgroundCompletionObservedBeforeTerminal = true;
}
continue;
}
if (message.id === promptId) { terminalSeen = true; evidence.terminalElapsedMs = elapsed(); }
const entry = pending.get(message.id);
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
function failPending(code) {
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
pending.clear();
}
const onShutdown = () => { evidence.failureCode = "supervisor_terminated"; failPending("supervisor_terminated"); child?.kill("SIGTERM"); };
process.once("SIGTERM", onShutdown); process.once("SIGINT", onShutdown);
function send(message) { child.stdin.write(`${JSON.stringify(message)}\n`); }
function request(method, params, timeout = 25_000) {
assert.ok(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/prompt", "session/close"].includes(method));
const id = nextId++;
if (method === "session/prompt") promptId = id;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
pending.set(id, { resolve, reject, timer }); send({ jsonrpc: "2.0", id, method, params });
});
}
try {
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) {
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
}
});
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-real-service-risk-qualification", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES });
assert.equal(initialized.agentInfo?.version, "1.0.88", "wrong provider version");
evidence.agentCapabilities = initialized.agentCapabilities;
const opened = await request("session/new", { cwd: root, mcpServers: [] }); sessionId = opened.sessionId;
assert.ok(opened.models?.availableModels?.some(value => value.modelId === model), "exact model not advertised");
await request("session/set_model", { sessionId, modelId: model });
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: model });
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, model, "model selection did not echo");
const prompt = scenario === "deny-write"
? "Try exactly once to create qualification-marker.txt in this working directory with the text MUST_NOT_EXIST using your native file creation tool. The operator will deny this write. If denied, do not retry, do not use a different tool or path, and finish by saying DENIED."
: `Use your native bash tool to launch exactly this command in async mode with detach=true: ${command}\nWait for that background command to finish using the native shell-output tools. Do not modify the command or use another write. Only finish after its successful exit, saying BACKGROUND_SETTLED.`;
markerTimer = setInterval(() => { sampleMarker("during_turn").catch(() => failPending("marker_read_failed")); }, 100);
evidence.promptRequestsSent = 1;
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
evidence.stopReason = terminal.stopReason;
evidence.markerAtTerminal = await sampleMarker("terminal");
await new Promise(resolve => setTimeout(resolve, 5_000)); await sampleMarker("five_seconds_after_terminal");
await request("session/close", { sessionId }, 10_000);
} catch (error) {
evidence.failureCode = typeof error?.message === "string" ? error.message.slice(0, 200) : "probe_failure";
await writeFile(join(env.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 });
await sampleMarker("before_launch");
child = lease.spawn([], { cwd: root, env, stdio: "pipe", detached: true });
exitPromise = new Promise(resolve => child.once("close", (code, signal) => { failPending("provider_exited"); resolve({ code, signal }); }));
outerTimer = setTimeout(() => { failPending("outer_deadline"); child.kill("SIGTERM"); }, 180_000);
child.once("error", () => failPending("provider_spawn_failure"));
child.stdin.on("error", () => failPending("provider_stdin_failure"));
child.stderr.on("data", () => {});
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; }
buffer += decoder.write(chunk);
while (buffer.includes("\n")) {
const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
if (!line.trim()) continue;
let message;
try { message = JSON.parse(line); } catch { failPending("malformed_json"); child.kill("SIGTERM"); return; }
if (!message || typeof message !== "object" || Array.isArray(message)) { failPending("malformed_message"); child.kill("SIGTERM"); return; }
evidence.wire.push({ elapsedMs: elapsed(), message });
if (message.method) {
const params = message.params;
if (message.method === "session/request_permission" && message.id !== undefined) {
const decision = permissionDecision(scenario, params, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined, command);
const writeAttempt = isExactMarkerWrite(params?.toolCall, marker);
evidence.permissions.push({ elapsedMs: elapsed(), requestId: message.id, offeredOptions: params.options, ...decision, writeAttempt });
send({ jsonrpc: "2.0", id: message.id, result: { outcome: decision.outcome } });
} else if (message.id !== undefined) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } });
}
if (params?.sessionId === sessionId) {
const arguments_ = params.data?.arguments ?? params.toolCall?.rawInput ?? params.update?.rawInput;
if (arguments_?.command === command && arguments_.detach === true) evidence.detachedToolObserved = true;
const content = JSON.stringify(params);
if (!terminalSeen && content.includes("completed with exit code 0") && content.includes("shell")) evidence.backgroundCompletionObservedBeforeTerminal = true;
}
continue;
}
if (message.id === promptId) { terminalSeen = true; evidence.terminalElapsedMs = elapsed(); }
const entry = pending.get(message.id);
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
}
});
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-real-service-risk-qualification", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES });
assert.equal(initialized.agentInfo?.version, "1.0.88", "wrong provider version");
evidence.agentCapabilities = initialized.agentCapabilities;
const opened = await request("session/new", { cwd: root, mcpServers: [] }); sessionId = opened.sessionId;
assert.ok(opened.models?.availableModels?.some(value => value.modelId === model), "exact model not advertised");
await request("session/set_model", { sessionId, modelId: model });
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: model });
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, model, "model selection did not echo");
const prompt = scenario === "deny-write"
? "Try exactly once to create qualification-marker.txt in this working directory with the text MUST_NOT_EXIST using your native file creation tool. The operator will deny this write. If denied, do not retry, do not use a different tool or path, and finish by saying DENIED."
: `Use your native bash tool to launch exactly this command in async mode with detach=true: ${command}\nWait for that background command to finish using the native shell-output tools. Do not modify the command or use another write. Only finish after its successful exit, saying BACKGROUND_SETTLED.`;
markerTimer = setInterval(() => { sampleMarker("during_turn").catch(() => failPending("marker_read_failed")); }, 100);
evidence.promptRequestsSent = 1;
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
evidence.stopReason = terminal.stopReason;
evidence.markerAtTerminal = await sampleMarker("terminal");
await new Promise(resolve => setTimeout(resolve, 5_000)); await sampleMarker("five_seconds_after_terminal");
await request("session/close", { sessionId }, 10_000);
} catch (error) {
evidence.failureCode = typeof error?.message === "string" ? error.message.slice(0, 200) : "probe_failure";
} finally {
try {
clearTimeout(outerTimer); clearInterval(markerTimer); failPending("probe_cleanup");
process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown);
if (child) {
child.stdin.end();
const groupId = Number.isSafeInteger(child.pid) && child.pid > 0 ? child.pid : null;
const killGroup = signal => { if (groupId === null) return; try { process.kill(-groupId, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } };
const term = setTimeout(() => killGroup("SIGTERM"), 1_000), kill = setTimeout(() => killGroup("SIGKILL"), 5_000);
evidence.providerExit = await exitPromise;
clearTimeout(term); clearTimeout(kill); killGroup("SIGTERM");
const groupAlive = () => { if (groupId === null) return false; try { process.kill(-groupId, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; } };
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
if (groupAlive()) killGroup("SIGKILL");
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
evidence.cleanupComplete = !groupAlive();
} else evidence.cleanupComplete = true;
} finally { await lease.close(); }
await sampleMarker("after_process_cleanup");
evidence.result = evaluateProbe(evidence);
let text = JSON.stringify(evidence, null, 2).replaceAll(token, "[REDACTED]").replaceAll(root, "/fixture/workspace");
if (sessionId) text = text.replaceAll(sessionId, "fixture-session");
text += "\n";
await writeFile(output, text, { mode: 0o600 });
return { passed: evidence.result.passed, failures: evidence.result.failures, output, sha256: createHash("sha256").update(text).digest("hex"), promptRequestsSent: evidence.promptRequestsSent };
}
} finally {
clearTimeout(outerTimer); clearInterval(markerTimer); failPending("probe_cleanup");
process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown);
if (child) {
child.stdin.end();
const groupId = Number.isSafeInteger(child.pid) && child.pid > 0 ? child.pid : null;
const killGroup = signal => { if (groupId === null) return; try { process.kill(-groupId, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } };
const term = setTimeout(() => killGroup("SIGTERM"), 1_000), kill = setTimeout(() => killGroup("SIGKILL"), 5_000);
evidence.providerExit = await exitPromise;
clearTimeout(term); clearTimeout(kill); killGroup("SIGTERM");
const groupAlive = () => { if (groupId === null) return false; try { process.kill(-groupId, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; } };
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
if (groupAlive()) killGroup("SIGKILL");
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
evidence.cleanupComplete = !groupAlive();
} else evidence.cleanupComplete = true;
await lease.close();
await sampleMarker("after_process_cleanup");
evidence.result = evaluateProbe(evidence);
let text = JSON.stringify(evidence, null, 2).replaceAll(token, "[REDACTED]").replaceAll(root, "/fixture/workspace");
if (sessionId) text = text.replaceAll(sessionId, "fixture-session");
text += "\n";
await writeFile(output, text, { mode: 0o600 });
await rm(root, { recursive: true, force: true });
return { passed: evidence.result.passed, failures: evidence.result.failures, output, sha256: createHash("sha256").update(text).digest("hex"), promptRequestsSent: evidence.promptRequestsSent };
}
}
@@ -55,7 +55,7 @@ test("missing credentials reject before resolving a pack or launching anything",
});
test("full credential-free protocol fixture preserves numeric-zero denial and reaps its process", async () => {
const { mkdtemp, mkdir, writeFile, readFile, rm } = await import("node:fs/promises");
const { mkdtemp, mkdir, writeFile, readFile, rm, access } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { runProbe } = await import("./qualify-copilot-acp.mjs");
@@ -69,23 +69,30 @@ test("full credential-free protocol fixture preserves numeric-zero denial and re
await writeFile(join(moduleRoot, "copilot-events.js"), 'export const COPILOT_ACP_CLIENT_CAPABILITIES = {};');
const childScript = join(root, "child.mjs");
await writeFile(childScript, `import { createInterface } from 'node:readline';
const send = m => process.stdout.write(JSON.stringify({jsonrpc:'2.0', ...m})+'\\n');
const send = m => {
const wire=Buffer.from(JSON.stringify({jsonrpc:'2.0', ...m})+'\\n');
const unicode=wire.indexOf(Buffer.from('🚀'));
if(unicode<0){process.stdout.write(wire);return;}
process.stdout.write(wire.subarray(0,unicode+2));
setTimeout(()=>process.stdout.write(wire.subarray(unicode+2)),50);
};
let promptId;
createInterface({input:process.stdin}).on('line', line => {
const m=JSON.parse(line);
if(!m.method){if(m.id!==0 || m.result?.outcome?.optionId!=='native-deny-0') process.exit(8); send({id:promptId,result:{stopReason:'end_turn'}});return;}
let result={};
if(m.method==='initialize')result={agentInfo:{version:'1.0.88'},agentCapabilities:{}};
if(m.method==='initialize')result={agentInfo:{version:'1.0.88'},agentCapabilities:{label:'東京 🚀 café'}};
if(m.method==='session/new')result={sessionId:'fixture-session',models:{availableModels:[{modelId:'gpt-5.6-luna'}]}};
if(m.method==='session/set_config_option')result={configOptions:[{id:'model',currentValue:'gpt-5.6-luna'}]};
if(m.method==='session/prompt'){promptId=m.id;send({id:0,method:'session/request_permission',params:{sessionId:'fixture-session',toolCall:{kind:'edit',rawInput:{fileName:process.cwd()+'/qualification-marker.txt'}},options:${JSON.stringify(options)}}});return;}
send({id:m.id,result});
}).on('close',()=>process.exit(0));`);
await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>spawn(options.env.COPILOT_GITHUB_TOKEN === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options),close:async()=>{}})});`);
await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; let token; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>{token=options.env.COPILOT_GITHUB_TOKEN;writeFileSync(${JSON.stringify(join(root, "last-probe-root"))},options.cwd);return spawn(token === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options);},close:async()=>{if(token==="fixture-close-failure")throw new Error("fixture lease cleanup failed");}})});`);
const output = join(root, "output.json");
const result = await runProbe(root, output, "deny-write", "fixture-token-never-used-for-network");
const evidence = JSON.parse(await readFile(output, "utf8"));
assert.equal(result.passed, true);
assert.deepEqual(evidence.agentCapabilities, { label: "東京 🚀 café" });
assert.equal(evidence.permissions[0].requestId, 0);
assert.deepEqual(evidence.permissions[0].outcome, { outcome: "selected", optionId: "native-deny-0" });
assert.equal(evidence.cleanupComplete, true);
@@ -98,5 +105,11 @@ createInterface({input:process.stdin}).on('line', line => {
assert.equal(failedEvidence.promptRequestsSent, 0);
assert.equal(failedEvidence.cleanupComplete, true);
assert.match(failedEvidence.failureCode, /provider_(spawn|stdin)_failure/);
const lastRoot = async () => await readFile(join(root, "last-probe-root"), "utf8");
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
await assert.rejects(runProbe(root, join(root, "missing/report.json"), "deny-write", "fixture-token-never-used-for-network"), { code: "ENOENT" });
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
await assert.rejects(runProbe(root, join(root, "close-failure.json"), "deny-write", "fixture-close-failure"), /fixture lease cleanup failed/);
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
} finally { await rm(root, { recursive: true, force: true }); }
});
@@ -28,5 +28,15 @@ export function verifyReleaseProviderPack(identity, sourceRevision) {
throw new Error("Remote provider-pack Cursor identity does not match this release");
}
}
const copilot = payload.providers?.copilot;
const copilotProfile = profiles.profiles.copilot;
if (Object.hasOwn(payload.candidateProviders ?? {}, "copilot") || !copilot
|| copilot.version !== copilotProfile.agentServerVersion || copilot.profileDigest !== copilotProfile.commandDigest
|| copilot.closureDigest !== "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07"
|| copilot.qualification !== "qualified" || copilot.path !== "provider-assets/copilot/linux-x64"
|| Object.keys(copilot).sort().join(",") !== "closureDigest,path,profileDigest,qualification,sha256,version"
|| !/^sha256:[a-f0-9]{64}$/.test(copilot.sha256 ?? "")) {
throw new Error("Remote provider-pack Copilot identity does not match this release");
}
return identity;
}
@@ -21,6 +21,9 @@ function fixture() {
providers: { cursor: { version: profiles.profiles.cursor.agentServerVersion, profileDigest: profiles.profiles.cursor.commandDigest,
closureDigest: `sha256:${distributions.platforms["linux-x64"].closureSha256}`, qualification: "qualified",
path: "provider-assets/cursor/linux-x64", sha256: digest } } };
payload.providers.copilot = { version: profiles.profiles.copilot.agentServerVersion,
profileDigest: profiles.profiles.copilot.commandDigest, closureDigest: "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07",
qualification: "qualified", path: "provider-assets/copilot/linux-x64", sha256: digest };
return payload;
}
function seal(payload) { return { schema: "paperclip-runner/remote-provider-pack/v1", payload,
@@ -77,3 +80,16 @@ test("release assembly rejects rehashed stale or missing packs before staging an
}
} finally { rmSync(root, { recursive: true, force: true }); }
});
for (const field of ["version", "profileDigest", "closureDigest", "qualification", "path", "sha256"]) {
test(`rejects an independently rehashed stale Copilot ${field}`, () => {
const payload = fixture(); payload.providers.copilot[field] = "stale";
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision), /Copilot identity/);
});
}
test("rejects omitted Copilot and a duplicate candidate inventory", () => {
const missing = fixture(); delete missing.providers.copilot;
assert.throws(() => verifyReleaseProviderPack(seal(missing), revision), /Copilot identity/);
const mixed = fixture(); mixed.candidateProviders = { copilot: mixed.providers.copilot };
assert.throws(() => verifyReleaseProviderPack(seal(mixed), revision), /Copilot identity/);
});
@@ -469,9 +469,11 @@ describe("HarnessDriverBackend", () => {
it("allows only the run id to change when a harness session is attached", async () => {
const attachedRunIds: string[] = [];
const grants: unknown[] = [];
class AttachableHarnessSession extends FakeHarnessSession {
async attachRun(input: { runId: string }) {
async attachRun(input: { runId: string; currentRunGrant?: { runtimeContext: null; instructions: string } }) {
attachedRunIds.push(input.runId);
grants.push(input.currentRunGrant);
}
}
const backend = new HarnessDriverBackend({
@@ -505,8 +507,10 @@ describe("HarnessDriverBackend", () => {
await expect(session.attachRun?.({
identity: { ...originalIdentity, runId: "run-2" },
currentRunGrant: { runtimeContext: null, instructions: "Current registered instructions." },
})).resolves.toBeUndefined();
expect(attachedRunIds).toEqual(["run-2"]);
expect(grants).toEqual([{ runtimeContext: null, instructions: "Current registered instructions." }]);
expect(session.identity()).toEqual({ ...originalIdentity, runId: "run-2" });
});
@@ -13,6 +13,7 @@ import type {
PersistedNativeSession,
} from "../contracts/native-session-backend.js";
import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js";
import type { NativeRunRuntimeGrant } from "../contracts/runtime-context.js";
import type {
PrpEvent,
PrpTerminalState,
@@ -471,6 +472,7 @@ class HarnessNativeSession implements NativeSession {
async attachRun(input: {
identity: OpenNativeSessionInput["identity"];
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void> {
this.#assertProtocolIntegrity();
const currentIdentity = this.#input.identity;
@@ -486,7 +488,9 @@ class HarnessNativeSession implements NativeSession {
throw new Error("native_session_multi_run_unavailable");
}
try {
await this.#session.attachRun({ runId: input.identity.runId });
await this.#session.attachRun({ runId: input.identity.runId,
...(input.currentRunGrant ? { currentRunGrant: structuredClone(input.currentRunGrant) } : {}),
});
this.#assertProtocolIntegrity();
} catch (error) {
this.#rethrowProtocolIntegrity(error);
@@ -600,8 +600,9 @@ describe("native backend factory", () => {
it("selects prepared transport only for v5 native input and retains prepared constraints", async () => {
const input = preparedExecution(execution());
const transport = new FakeCodexTransport();
let currentInstructions: string | undefined;
const backend = createCodexNativeSessionBackend(input, {
transportFactory: () => transport,
transportFactory: (context) => { currentInstructions = context?.baseInstructions; return transport; },
workingDirectoryAuthority: "remote_runner",
environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE },
});
@@ -619,6 +620,8 @@ describe("native backend factory", () => {
expect(
transport.calls.find((call) => call.method === "thread/start")?.params,
).toMatchObject({ conversationMode: "prepared" });
expect(currentInstructions).toContain("Keep assigned instructions.");
expect(currentInstructions).toBe(transport.calls.find((call) => call.method === "thread/start")?.params.developerInstructions);
expect(backend.preparedTaskConstraints).toEqual(
expect.arrayContaining([
expect.stringContaining("Obtain one accepted result"),
@@ -34,7 +34,7 @@ import {
normalizeAcpFormElicitation,
type NormalizedAcpForm,
} from "../drivers/acpx/acp-question-adapter.js";
import { openCodexAcpxRuntime } from "../drivers/acpx/codex-runtime-adapter.js";
import { openCodexAcpxRuntime, AcpxProviderProcessLostError } from "../drivers/acpx/codex-runtime-adapter.js";
import { AcpxApprovalRequiredError } from "../drivers/acpx/permission-policy.js";
import { normalizeAcpxPermission, type NormalizedAcpxPermission } from "../drivers/acpx/acp-permission-adapter.js";
import { acpxGoalProjection } from "../drivers/acpx/session-goals.js";
@@ -678,7 +678,7 @@ async function pumpTurn(
terminal = {
status: "failed",
error: {
...(error instanceof AcpxApprovalRequiredError ? { code: error.code } : {}),
...(error instanceof AcpxApprovalRequiredError || error instanceof AcpxProviderProcessLostError ? { code: error.code } : {}),
message: safeMessage(error),
retryable: false,
},
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { probeCopilotMetadata } from "../drivers/copilot-metadata-probe.js";
const result = await probeCopilotMetadata(process.env.COPILOT_GITHUB_TOKEN ?? "", process.env.PAPERCLIP_COPILOT_PROBE_MODEL);
process.stdout.write(`${JSON.stringify(result)}\n`);
if (result.status === "failed") process.exitCode = 1;
@@ -141,6 +141,25 @@ describe("eval-session request contract", () => {
} finally { await rm(workspace, { recursive: true, force: true }); }
});
it("requires bounded exact Copilot profile admission before loading provider state", async () => {
const args = ["--request", "/tmp/request.json", "--output", "/tmp/result.json", "--candidate-profile", "copilot"];
expect(() => parseEvalSessionCliArgs(args)).toThrow("require --expected-acpx-profile");
for (const invalid of ["[]", "null", "malformed", JSON.stringify({ overflow: "x".repeat(4096) })]) {
expect(() => parseEvalSessionCliArgs([...args, "--expected-acpx-profile", invalid])).toThrow();
}
const profile = resolveQualifiedAcpxProfile("copilot", "gpt-5.6-luna");
expect(parseEvalSessionCliArgs([...args, "--expected-acpx-profile", JSON.stringify(profile)]).expectedAcpxProfile).toEqual(profile);
const workspace = await mkdtemp(join(tmpdir(), "eval-profile-admission-"));
try {
const path = join(workspace, "request.json");
await writeFile(path, JSON.stringify(request({ provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" })));
const factory = vi.fn();
await expect(runEvalSessionCli(["--request", path, "--output", join(workspace, "output.json"), "--candidate-profile", "copilot", "--expected-acpx-profile", JSON.stringify({ ...profile, commandDigest: `sha256:${"0".repeat(64)}` })], { serviceFactory: factory })).rejects.toThrow("does not match the built runner profile");
expect(factory).not.toHaveBeenCalled();
expect(await readdir(workspace)).toEqual(["request.json"]);
} finally { await rm(workspace, { recursive: true, force: true }); }
});
it("materializes a production-v3 runtime context for direct live providers", async () => {
const workspace = await mkdtemp(join(tmpdir(), "paperclip-eval-context-"));
let instructionRoot: string | null = null;
@@ -1,3 +1,4 @@
import type { NativeRunRuntimeGrant } from "./runtime-context.js";
import type {
PrpEvent,
PrpStructuredRunResult,
@@ -505,7 +506,7 @@ export interface HarnessSession {
displayId?: string | null;
};
events(): AsyncIterable<PrpEvent>;
attachRun?(input: { runId: string }): Promise<void> | void;
attachRun?(input: { runId: string; currentRunGrant?: NativeRunRuntimeGrant }): Promise<void> | void;
startTurn(input: {
message: NativeUserMessage;
/** Set by orchestration only after successful provider-session recovery. */
@@ -407,6 +407,30 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("does not match");
});
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId,
permissionPolicy, qualificationStatus: _qualificationStatus, modelPolicy: _modelPolicy,
...snapshot } = QUALIFIED_ACPX_PROFILES.copilot;
const provider = {
kind: "acpx", agent: "copilot", model: "gpt-5.6-luna", permissionPolicy,
profile: { ...snapshot, agentProfileVersion },
} as const;
const value = { ...input, session: { ...input.session, driverKind: "acpx_runtime" }, provider };
const parsed = parseNativeExecutionInput(value);
expect(parsed.provider).toEqual(provider);
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
for (const unsupportedVersion of [0, 35, 1.5, "34", null]) {
expect(() => parseNativeExecutionInput({
...value, provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } },
})).toThrow("qualified ACPX v1 profile");
}
expect(() => parseNativeExecutionInput({
...value, provider: { ...provider, profile: { ...provider.profile, agent: "pi" } },
})).toThrow("qualified ACPX v1 profile");
expect(() => parseNativeExecutionInput({
...value, session: { ...value.session, driverKind: "opencode_server" },
})).toThrow("does not match");
});
it.each(Object.values(QUALIFIED_ACPX_PROFILES))(
"admits the current $agent profile through the native execution boundary",
(declaration) => {
@@ -564,6 +588,46 @@ describe("native task context ownership", () => {
});
}
const { qualificationModel: _copilotQualificationModel, reportedModelId: _copilotReportedModelId,
permissionPolicy: _copilotPermissionPolicy, qualificationStatus: _copilotQualificationStatus, modelPolicy: _copilotModelPolicy,
...copilotSnapshot } = QUALIFIED_ACPX_PROFILES.copilot;
it.each([
{ driverKind: "acpx_runtime", provider: { kind: "acpx", agent: "copilot", model: "gpt-5.6-luna", permissionMode: "deny-all", profile: copilotSnapshot } },
{ driverKind: "opencode_server", provider: { kind: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", permissionMode: "deny" } },
{ driverKind: "acpx_runtime", provider: {
kind: "acpx", agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", permissionMode: "deny-all",
profile: { driverKind: "acpx_runtime", protocolVersion: 1, acpxVersion: "0.13.1", agent: "pi", agentProfileVersion: 1,
agentServerPackage: "pi-acp", agentServerVersion: "0.0.33", agentRuntimePackage: "@earendil-works/pi-coding-agent",
agentRuntimeVersion: "0.84.2", commandDigest: `sha256:${"a".repeat(64)}` },
} },
{ driverKind: "openai_dot_mcp", provider: {
kind: "openai_dot", model: null,
binding: { bindingId: "dot-binding", bindingGeneration: 1, companyId: input.binding.companyId, agentId: input.binding.agentId,
acceptByUnixMs: 1_000, expiresAtUnixMs: 2_000 },
} },
])("preserves an unregistered saved prompt for $provider.kind", ({ driverKind, provider }) => {
const current = currentInput();
if (!("runtimeContext" in current)) throw new Error("Expected a runtime context");
const text = "Saved instructions absent from the current release.";
const context = { ...current.runtimeContext,
prompt: { revision: "saved-prompt-before-upgrade", text, digest: createHash("sha256").update(text).digest("hex") } };
context.aggregateDigest = canonicalNativeRuntimeContextDigest(context);
const persisted = JSON.parse(JSON.stringify({
...current, provider, session: { ...current.session, driverKind }, runtimeContext: context,
...(provider.kind === "openai_dot" ? {
schema: NATIVE_EXECUTION_INPUT_SCHEMA_V6,
workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
credentialBindings: [],
} : {}),
}));
const recovered = parseNativeExecutionInput(persisted);
expect(recovered.runtimeContext).toEqual(context);
expect(recovered.provider).toEqual(provider);
expect(recovered.session.driverKind).toBe(driverKind);
expect(parseNativeExecutionInput(recovered)).toEqual(recovered);
});
it("carries an opaque provider mode without a vendor restriction and fences obsolete field names", () => {
const current = currentInput();
const provider = {
@@ -91,6 +91,9 @@ export type NativeOpenCodePermissionMode = "allow" | "ask" | "deny";
export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
export type NativeCursorMode = "agent" | "plan" | "ask";
/** Decodable history; current immutable admission comes from the shared manifest. */
export type NativeAcpxProfileVersion = AcpxProfileVersion;
export interface NativeAcpxProfileSnapshot {
driverKind: "acpx_runtime";
protocolVersion: 1;
@@ -1,3 +1,4 @@
import type { NativeRunRuntimeGrant } from "./runtime-context.js";
import type {
NativeRunIdentity,
NativeSessionCapabilities,
@@ -184,7 +185,7 @@ export class NativeSessionCleanupQuarantinedError extends Error {
export interface NativeSession {
identity(): NativeRunIdentity;
capabilities(): Promise<NativeSessionCapabilities>;
attachRun?(input: { identity: NativeRunIdentity }): Promise<void>;
attachRun?(input: { identity: NativeRunIdentity; currentRunGrant?: NativeRunRuntimeGrant }): Promise<void>;
/** Relinquish controller authority without suspending provider execution. */
detachControllerForRestart?(): Promise<void>;
events(input?: { afterCursor?: string | null }): AsyncIterable<PrpEvent>;
@@ -27,6 +27,12 @@ export interface NativeRuntimeContextSnapshot {
aggregateDigest: string;
}
/** Authenticated current-run grants; immutable session policy is verified separately. */
export interface NativeRunRuntimeGrant {
runtimeContext: NativeRuntimeContextSnapshot | null;
instructions: string;
}
export class NativeRuntimeContextError extends Error {
readonly code = "native_runtime_context_invalid" as const;
constructor(message: string) { super(message); this.name = "NativeRuntimeContextError"; }
@@ -2,6 +2,8 @@ import type { AcpPermissionDecision, AcpPermissionRequest } from "acpx/runtime";
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
import { safeCopilotEditTarget } from "./copilot-permission-context.js";
export type AcpxPermissionAction = "accept" | "accept_for_session" | "decline" | "cancel";
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
import { describe, expect, it, vi } from "vitest";
import { AcpxProviderProcessLostError } from "./codex-runtime-adapter.js";
import type { AcpRuntimeEvent } from "acpx/runtime";
@@ -1686,6 +1687,35 @@ describe("Codex ACPX harness driver", () => {
});
});
it("expires a Copilot permission on provider death and rejects its late answer", async () => {
const failure = deferred<void>();
const fixture = driverFixture({ agent: "copilot", model: "gpt-5.6-luna", providerPolicy: { readOnly: false } }, { eventStreamFailure: failure.promise });
const session = await fixture.driver.openSession({ runId: "run-provider-death", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
const created = collectUntil(session.events(), "runtime_request.created");
const { turnId } = await session.startTurn({ message: { text: "Edit the file" } });
const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!;
const controller = new AbortController();
const response = callback({ inferredKind: "edit", raw: {
sessionId: "agent-session-1", toolCall: { toolCallId: "dead-edit", title: "Edit file" },
options: [{ optionId: "once", kind: "allow_once", name: "Allow once" }],
} } as Parameters<typeof callback>[0], { signal: controller.signal, responseDelivery: Promise.resolve() });
await created;
const request = session.pendingRuntimeRequests!()[0]!;
const expired = collectUntil(session.events(), "runtime_request.expired");
const error = new AcpxProviderProcessLostError();
controller.abort(error);
await expect(response).resolves.toEqual({ outcome: "cancel" });
expect((await expired).at(-1)).toMatchObject({ eventType: "runtime_request.expired", payload: {
requestId: request.requestId, reason: "provider_process_lost", replayAllowed: false,
} });
await expect(session.resolveRuntimeRequest!({ requestId: request.requestId, turnId, resolution: { action: "accept" } })).rejects.toThrow("no longer pending");
const terminal = collectUntil(session.events(), "turn.failed");
failure.reject(error);
expect((await terminal).at(-1)).toMatchObject({ payload: { error: { code: "ACPX_PROVIDER_PROCESS_LOST" } } });
expect(session.pendingRuntimeRequests!()).toEqual([]);
await session.close({ reason: "death verified" });
});
it("delivers only offered permission choices and rejects stale or duplicate answers", async () => {
const fixture = driverFixture({ agent: "claude", model: "claude-sonnet-5" });
const session = await fixture.driver.openSession({
@@ -59,6 +59,7 @@ import {
} from "../runner-tool-bridge.js";
import {
DEFAULT_CODEX_ACPX_RUNTIME_SHUTDOWN_BOUND_MS,
AcpxProviderProcessLostError,
openCodexAcpxRuntime,
} from "./codex-runtime-adapter.js";
import { normalizeAcpxPermission } from "./acp-permission-adapter.js";
@@ -1568,7 +1569,7 @@ class CodexAcpxSession implements HarnessSession {
} catch (error) {
if (this.#terminalTurns.has(turnId)) return;
if (error instanceof TerminalEventCapacityError) throw error;
this.#cancelPendingRuntimeRequests("provider turn failed", turnId);
this.#cancelPendingRuntimeRequests(error instanceof AcpxProviderProcessLostError ? error : "provider turn failed", turnId);
if (this.#closed || this.#closingStarted) {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === turnId
@@ -1599,7 +1600,7 @@ class CodexAcpxSession implements HarnessSession {
: { reaffirmedSemanticResult }),
}),
"turn.failed",
{ status: "failed", error: { message: safeMessage(error) } },
{ status: "failed", error: { ...(error instanceof AcpxProviderProcessLostError ? { code: error.code } : {}), message: safeMessage(error) } },
);
}
}
@@ -1743,9 +1744,7 @@ class CodexAcpxSession implements HarnessSession {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling || !this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emit("runtime_request.cancelled", harnessRuntimeRequestOutcome(request, {
action: "cancel", reason: "provider request aborted",
}), { turnId, itemId });
this.#emitRequestAbort(request, context.signal.reason);
settle(input.cancel());
};
this.#pendingRuntimeRequests.set(requestId, {
@@ -1794,9 +1793,7 @@ class CodexAcpxSession implements HarnessSession {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling || !this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emit("runtime_request.cancelled", harnessRuntimeRequestOutcome(runtimeRequest, {
action: "cancel", reason: "provider request aborted",
}), { turnId, itemId: requestId });
this.#emitRequestAbort(runtimeRequest, signal.reason);
settle({ outcome: "cancel" });
};
this.#pendingRuntimeRequests.set(requestId, {
@@ -1930,14 +1927,7 @@ class CodexAcpxSession implements HarnessSession {
if (!pending || pending.settling) return;
if (!this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emit(
"runtime_request.cancelled",
harnessRuntimeRequestOutcome(runtimeRequest, {
action: "cancel",
reason: "provider request aborted",
}),
{ turnId, itemId: requestId },
);
this.#emitRequestAbort(runtimeRequest, context.signal.reason);
settle({ action: "cancel" });
};
context.signal.addEventListener("abort", cancel, { once: true });
@@ -1955,22 +1945,23 @@ class CodexAcpxSession implements HarnessSession {
});
}
#cancelPendingRuntimeRequests(reason: string, turnId?: string): void {
#emitRequestAbort(request: HarnessRuntimeRequest, reason: unknown): void {
const lost = reason instanceof AcpxProviderProcessLostError;
this.#emit(lost ? "runtime_request.expired" : "runtime_request.cancelled", {
...harnessRuntimeRequestOutcome(request, {
...(lost ? {} : { action: "cancel" as const }),
reason: lost ? "provider_process_lost" : typeof reason === "string" ? boundedText(reason, 1_000) : "provider request aborted",
}),
...(lost ? { replayAllowed: false } : {}),
}, { turnId: request.turnId, itemId: request.itemId });
}
#cancelPendingRuntimeRequests(reason: string | AcpxProviderProcessLostError, turnId?: string): void {
for (const [requestId, pending] of this.#pendingRuntimeRequests) {
if (turnId && pending.request.turnId !== turnId) continue;
if (!this.#pendingRuntimeRequests.delete(requestId)) continue;
pending.cleanup();
this.#emit(
"runtime_request.cancelled",
harnessRuntimeRequestOutcome(pending.request, {
action: "cancel",
reason: boundedText(safeMessage(reason), 1_000),
}),
{
turnId: pending.request.turnId,
itemId: pending.request.itemId,
},
);
this.#emitRequestAbort(pending.request, reason);
pending.cancel();
}
}
@@ -813,9 +813,10 @@ describe("managed Codex credentials", () => {
it.runIf(process.platform !== "win32")(
"shares the four-slot parent filesystem budget across fresh modules",
async () => {
const fixtures = await Promise.all(
Array.from({ length: 5 }, () => credentialFixture()),
);
// Reserve fixture quorum candidates before holding filesystem operations.
// Otherwise an unrelated loopback listener can reject staging before
// fourOpens resolves, leaving an unhandled rejection and a false timeout.
const fixtures = await credentialFixturesWithAvailableQuorum(5);
const retainedHomes = new Set(
fixtures.slice(0, 4).map((fixture) => fixture.home),
);
@@ -1652,6 +1653,38 @@ async function credentialFixture(): Promise<{ root: string; home: string }> {
return { root, home: await realpath(home) };
}
async function credentialFixturesWithAvailableQuorum(count: number) {
const fixtures: Awaited<ReturnType<typeof credentialFixture>>[] = [];
const reservations: Array<Awaited<ReturnType<typeof listenSilently>>> = [];
try {
for (let index = 0; index < count; index++) {
let prepared = false;
for (let attempt = 0; attempt < 8; attempt++) {
const fixture = await credentialFixture();
const owned: Array<Awaited<ReturnType<typeof listenSilently>>> = [];
try {
for (const port of credentialLeasePorts(fixture.home)) {
owned.push(await listenSilently(port));
}
fixtures.push(fixture);
reservations.push(...owned);
prepared = true;
break;
} catch (error) {
await Promise.all(owned.map((listener) => listener.close()));
if ((error as NodeJS.ErrnoException).code !== "EADDRINUSE" || attempt === 7) throw error;
}
}
if (!prepared) throw new Error("Credential fixture quorum preparation failed");
}
return fixtures;
} finally {
// Fixture setup alone may choose another unused home. Production staging
// still runs once, and a foreign bind racing this handoff remains a failure.
await Promise.all(reservations.map((listener) => listener.close()));
}
}
async function silentPrimaryQuorumFixture(
nextFixture = credentialFixture,
occupiedIndex: 0 | 1 = 0,
@@ -29,6 +29,141 @@ const HANDLE: AcpRuntimeHandle = {
};
describe("Codex ACPX runtime adapter", () => {
it.each(["blocked", "ended"] as const)("settles a %s Copilot stream on verified provider death without replaying the permission", async (streamState) => {
const pending = pendingExtensionTurn("death-turn");
if (streamState === "ended") pending.turn.events = (async function* () {})();
const runtime = fakeRuntime();
vi.mocked(runtime.startTurn).mockReturnValue(pending.turn);
const child = fakeChild();
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValue(child);
let exit!: () => void;
const providerExit = new Promise<void>(resolve => { exit = resolve; });
let created!: AcpRuntimeOptions;
const options = openOptions(command);
options.profile = { ...options.profile, agent: "copilot" };
options.permissionMode = "approve-paperclip";
const port = await openCodexAcpxRuntime(options, {
createRegistry: () => registry(), createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished,
awaitProviderExit: () => providerExit,
createRuntime: value => { created = value; return runtimeWithProvider(runtime, value); },
});
let answer!: (value: { outcome: "allow_once" }) => void;
let callbackSignal!: AbortSignal;
const handler = vi.fn((_request, context) => {
callbackSignal = context.signal;
return new Promise<{ outcome: "allow_once" }>(resolve => { answer = resolve; });
});
const turn = port.startTurn({ text: "Write the file", requestId: "death-turn", onPermissionRequest: handler });
await turn.promptStarted;
const events = turn.events[Symbol.asyncIterator]();
const draining = expect(events.next()).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const settled = expect(turn.result).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const request = created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal: new AbortController().signal });
await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce());
// The guardian remains live: only independent provider EOF proves death.
expect(child.exitCode).toBeNull();
exit();
await Promise.all([draining, settled]);
expect(callbackSignal.aborted).toBe(true);
answer({ outcome: "allow_once" });
await expect(request).resolves.toEqual({ outcome: "cancel" });
expect(() => port.startTurn({ text: "Replay", requestId: "replay" })).toThrow("provider exited");
expect(runtime.startTurn).toHaveBeenCalledOnce();
pending.settle();
await port.close({ reason: "test finished" });
expect(child.kill).toHaveBeenCalled();
});
it("refuses Copilot replay when native disconnection precedes the independent exit proof", async () => {
const pending = pendingExtensionTurn("disconnect-turn");
let disconnect!: () => void;
const failed = new Promise<{ status: "failed"; error: { code: "RUNTIME"; detailCode: "AGENT_DISCONNECTED"; message: string } }>(resolve => {
disconnect = () => resolve({ status: "failed", error: { code: "RUNTIME", detailCode: "AGENT_DISCONNECTED", message: "Native ACP transport disconnected" } });
});
const runtime = fakeRuntime();
vi.mocked(runtime.startTurn).mockReturnValue({ ...pending.turn, result: failed });
const child = fakeChild();
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValue(child);
let exit!: () => void;
const providerExit = new Promise<void>(resolve => { exit = resolve; });
let created!: AcpRuntimeOptions;
const options = openOptions(command);
options.profile = { ...options.profile, agent: "copilot" };
options.permissionMode = "approve-paperclip";
const port = await openCodexAcpxRuntime(options, {
createRegistry: () => registry(), createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished,
awaitProviderExit: () => providerExit,
createRuntime: value => { created = value; return runtimeWithProvider(runtime, value); },
});
let answer!: (value: { outcome: "allow_once" }) => void;
let callbackSignal!: AbortSignal;
const handler = vi.fn((_request, context) => {
callbackSignal = context.signal;
return new Promise<{ outcome: "allow_once" }>(resolve => { answer = resolve; });
});
try {
const turn = port.startTurn({ text: "Write", requestId: "disconnect-turn", onPermissionRequest: handler });
await turn.promptStarted;
const request = created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal: new AbortController().signal });
await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce());
disconnect();
await expect(turn.result).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
await expect((async () => { for await (const _event of turn.events) { /* drain */ } })())
.rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
expect(callbackSignal.aborted).toBe(true);
answer({ outcome: "allow_once" });
await expect(request).resolves.toEqual({ outcome: "cancel" });
expect(() => port.startTurn({ text: "Replay", requestId: "replay" })).toThrow("provider exited");
expect(runtime.startTurn).toHaveBeenCalledOnce();
// A typed disconnect does not manufacture independent process-exit proof.
// Cleanup still owns the live guardian and waits for the separate proof.
expect(child.exitCode).toBeNull();
} finally {
pending.settle();
exit();
await port.close({ reason: "disconnect race verified" });
}
});
it("keeps a Copilot callback deliverable when only the guardian exits", async () => {
const pending = pendingExtensionTurn("preserved-turn");
const runtime = fakeRuntime();
vi.mocked(runtime.startTurn).mockReturnValue(pending.turn);
let created!: AcpRuntimeOptions;
const child = fakeChild();
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValue(child);
let exit!: () => void;
const providerExit = new Promise<void>(resolve => { exit = resolve; });
const options = openOptions(command);
options.profile = { ...options.profile, agent: "copilot" };
options.permissionMode = "approve-paperclip";
const port = await openCodexAcpxRuntime(options, {
createRegistry: () => registry(), createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished, awaitProviderExit: () => providerExit,
createRuntime: value => { created = value; return runtimeWithProvider(runtime, value); },
});
let answer!: (value: { outcome: "allow_once" }) => void;
const handler = vi.fn(() => new Promise<{ outcome: "allow_once" }>(resolve => { answer = resolve; }));
const turn = port.startTurn({ text: "Write", requestId: "preserved-turn", onPermissionRequest: handler });
const request = created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal: new AbortController().signal });
await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce());
child.exitCode = 0;
child.emit("exit", 0, null);
// Guardian exit cannot replace the provider-only lifetime observation.
await new Promise(resolve => setTimeout(resolve, 10));
answer({ outcome: "allow_once" });
await expect(request).resolves.toEqual({ outcome: "allow_once" });
pending.settle();
await expect(turn.result).resolves.toMatchObject({ status: "completed" });
exit();
await port.close({ reason: "test finished" });
});
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();
@@ -102,6 +102,14 @@ interface AcpxRuntimeExtensionBoundary {
steering: AcpxRuntimeSteeringCapability | null;
}
export class AcpxProviderProcessLostError extends Error {
readonly code = "ACPX_PROVIDER_PROCESS_LOST";
constructor(message = "The owned ACPX provider exited during its active turn") {
super(message);
this.name = "AcpxProviderProcessLostError";
}
}
class AcpxRuntimeCloseTimeoutError extends Error {
constructor() {
super("ACPX runtime close timed out");
@@ -605,6 +613,7 @@ export async function openQualifiedAcpxRuntime(
commandLaunches,
permissionBoundary,
extensionBoundary,
options.profile.agent === "copilot",
options.profile.agent === "copilot" ? assertCopilotPromptPolicy : undefined,
);
} catch (error) {
@@ -1035,6 +1044,7 @@ function runtimePort(
commandLaunches: { count: number; refreshConsumedCommand?: () => Promise<void> },
permissionBoundary: { active: AbortController | null; hasAdmittedTurn: boolean; handler?: AcpRuntimeOptions["onPermissionRequest"] },
extensionBoundary: AcpxRuntimeExtensionBoundary,
monitorProviderDeath: boolean,
assertPromptPolicy?: (text: string) => void,
): AcpxRuntimePort {
extensionBoundary.sessionIds = new Set([identity.backendSessionId]);
@@ -1078,6 +1088,7 @@ function runtimePort(
pendingExternalIntent: boolean;
};
let runtimeClosed = false;
let providerProcessLost = false;
let runtimeCloseAttempt: RuntimeCloseAttempt | undefined;
let lateReconciliationOwner: Promise<void> | undefined;
// Each independently observed late failure receives a bounded reconciliation
@@ -1393,10 +1404,12 @@ function runtimePort(
}
: {}),
startTurn(input) {
if (providerProcessLost) throw new AcpxProviderProcessLostError();
assertPromptPolicy?.(input.text);
if (extensionBoundary.active) throw new Error("ACPX runtime already has an active turn");
const approval = new AbortController();
const controller = new AbortController();
const providerDeath = new AbortController();
const extensionTurn: AcpxRuntimeExtensionTurn = {
requestId: input.requestId, sessionId: identity.backendSessionId, controller,
signal: input.signal ? AbortSignal.any([controller.signal, input.signal, approval.signal]) : AbortSignal.any([controller.signal, approval.signal]),
@@ -1404,7 +1417,25 @@ function runtimePort(
onRequest: input.onExtensionRequest, onNotification: input.onExtensionNotification,
};
extensionBoundary.active = extensionTurn;
// A guardian can outlive the provider while ACP awaits a callback.
// Native exit or a typed ACP agent disconnect retires this turn; losing
// the controller connection must preserve deliverable input instead.
// Cleanup still requires the independent provider-only lifetime proof.
const retireProviderTurn = (error = new AcpxProviderProcessLostError()): void => {
providerProcessLost = true;
providerDeath.abort(error);
approval.abort(error);
controller.abort(error);
};
const stopWatchingProviderExit = !monitorProviderDeath
? () => undefined
: children.watchProviderExit(() => {
if (!runtimeCloseAttempt && !runtimeClosed && !extensionTurn.signal.aborted && extensionBoundary.active === extensionTurn) {
retireProviderTurn();
}
});
const releaseExtensionTurn = (): void => {
stopWatchingProviderExit();
controller.abort(new Error("ACPX extension turn expired"));
if (extensionBoundary.active === extensionTurn) extensionBoundary.active = null;
};
@@ -1434,9 +1465,18 @@ function runtimePort(
throw error;
}
const guarded = turnWithVerifiedLifetimeOwnership(turn, finishOwnershipAdmission);
extensionTurn.promptStarted = guarded.promptStarted;
const result = guarded.result.then(
const promptStarted = abortableExtensionResult(guarded.promptStarted, providerDeath.signal);
void promptStarted.catch(() => undefined);
extensionTurn.promptStarted = promptStarted;
const result = abortableExtensionResult(guarded.result, providerDeath.signal).then(
(value) => {
if (monitorProviderDeath && value.status === "failed"
&& (value.error?.code === "AGENT_DISCONNECTED" || value.error?.detailCode === "AGENT_DISCONNECTED")) {
// ACP can terminalize before the independent exit observer runs.
// Retire callbacks and the session before finally removes the
// observer, so a late answer cannot admit another native turn.
retireProviderTurn(new AcpxProviderProcessLostError("The owned ACPX provider disconnected during its active turn"));
}
approval.signal.throwIfAborted();
// ACPX may resolve its own permission policy before invoking the
// host callback. Keep that typed denial on the same runner outcome.
@@ -1453,6 +1493,7 @@ function runtimePort(
void result.catch(() => undefined);
return {
...guarded,
promptStarted,
result,
cancel: (input) => {
controller.abort(new Error("ACPX extension turn cancelled"));
@@ -1463,16 +1504,28 @@ function runtimePort(
return guarded.closeStream(input);
},
events: (async function* () {
const iterator = guarded.events[Symbol.asyncIterator]();
try {
for await (const event of guarded.events) {
for (;;) {
const next = await abortableExtensionResult(iterator.next(), providerDeath.signal);
if (next.done) break;
approval.signal.throwIfAborted();
yield event;
yield next.value;
}
} catch (error) {
approval.signal.throwIfAborted();
throw error;
} finally {
// A broken provider may never release its iterator. Observe its
// eventual close without delaying the failed turn's publication.
void iterator.return?.().catch(() => undefined);
}
approval.signal.throwIfAborted();
// Copilot's transport can end its event iterator before independent
// provider-exit proof settles the pending permission and turn result.
// Do not publish a successful stream end while that turn still owns
// an unanswered callback or an unreported failure.
if (monitorProviderDeath) await result;
})(),
};
},
@@ -1729,6 +1782,7 @@ class SpawnedChildSet {
readonly #providerExits = new Map<ChildProcess, ProviderExitObservation>();
readonly #terminations = new Map<ChildProcess, Promise<unknown[]>>();
readonly #lifetimeOwnership: Promise<void>[] = [];
readonly #exitWatchers = new Set<() => void>();
#lifetimeOwnershipSealed = false;
#sealed = false;
@@ -1814,6 +1868,11 @@ class SpawnedChildSet {
};
}
watchProviderExit(observer: () => void): () => void {
this.#exitWatchers.add(observer);
return () => { this.#exitWatchers.delete(observer); };
}
#track(child: ChildProcess, providerExit: ProviderExitObservation): void {
this.#children.add(child);
const onError = (error: unknown) => this.#errors.add(error);
@@ -1840,6 +1899,7 @@ class SpawnedChildSet {
providerExit.observe((outcome) => {
if (outcome.exited) {
providerExited = true;
for (const observer of this.#exitWatchers) observer();
forgetIfReleased();
} else {
this.#errors.add(outcome.error);
@@ -82,7 +82,11 @@ function detailFields(data: Record<string, unknown>): Array<{ name: string; valu
// Preserve the complete field path for sensitive-key redaction. The
// normalizer admits only known, typed counters/flags: their numeric token
// counts are not credentials, so they retain value-only redaction.
const safe = typeof value === "string" ? name.split(".").reduce<unknown>((safe, segment) => redactSemanticValue(safe, segment), value) : value;
// Flattening must not discard a sensitive ancestor such as credentials.
// Apply the shared key rule to each path segment before publishing it.
const safe = typeof value === "string"
? name.split(".").reduce<unknown>((entry, key) => redactSemanticValue(entry, key), value)
: value;
details.push({ name: bounded(name, 160), value: bounded(String(safe), 4000) });
}
};
@@ -24,14 +24,17 @@ export async function verifyCopilotInstallation(profile: AcpxReleaseProfile): Pr
throw new Error(`Copilot native distribution is not pinned for ${platform}`);
}
const distributionRoot = join(resolveRunnerProviderAssetsRoot(import.meta.url, "copilot"), platform);
const native = await verifyNativeAcpxInstallation({
let native: VerifiedAcpxInstallation;
try { native = await verifyNativeAcpxInstallation({
distributionRoot,
manifestPath: join(distributionRoot, ".paperclip-copilot-closure.json"),
expectedClosureSha256: COPILOT_CLOSURE_SHA256[platform as keyof typeof COPILOT_CLOSURE_SHA256],
executable: "copilot", fixedArguments: COPILOT_LAUNCH_ARGUMENTS,
isolatedCacheEnvironmentName: "COPILOT_PKG_CACHE_HOME",
copilotDistributionDirectory: "distribution",
});
}); } catch {
throw Object.assign(new Error("Install the verified Copilot runtime beside the admitted sidecar."), { code: "COPILOT_INSTALLATION_INVALID", retryable: false });
}
// The host identity binds the versioned profile. Native admission independently
// binds the complete platform closure before creating each single-use lease.
return Object.freeze({ ...native, commandDigest: expected.commandDigest });
@@ -1,10 +1,35 @@
import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js";
import { safeAcpxLocations } from "./safe-locations.js";
import { lstatSync, realpathSync } from "node:fs";
import { basename, dirname, isAbsolute, join } from "node:path";
export const COPILOT_PERMISSION_CONTEXT_CONTRACT = "copilot-edit-permission-context-v1";
const record = (value: unknown): Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
/** Native providers can spell an absolute workspace path through a host alias
* (macOS /tmp and /private/tmp). Attest the real entry, or its existing parent
* for a new file, before converting it to a display target. */
function aliasedLocation(path: string, cwd: string): string | undefined {
if (!isAbsolute(path)) return;
try {
const canonicalCwd = realpathSync(cwd);
let canonicalPath: string;
try {
lstatSync(path);
canonicalPath = realpathSync(path);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return;
// A dangling symlink must not become an apparently safe create target.
try { lstatSync(path); return; } catch (missing) {
if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return;
}
canonicalPath = join(realpathSync(dirname(path)), basename(path));
}
return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined;
} catch { return; }
}
/** Display context only; this does not authorize filesystem access. */
export function safeCopilotEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined {
const call = record(value);
@@ -14,7 +39,12 @@ export function safeCopilotEditTarget(value: unknown, workingDirectory: string |
if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return;
const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined);
if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return;
const safe = paths.flatMap(path => safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title));
const safe = paths.flatMap(path => {
const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title);
if (locations.length) return locations;
const aliased = aliasedLocation(path as string, workingDirectory);
return aliased ? [{ path: aliased }] : [];
});
const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":"));
if (names.length !== paths.length || new Set(names).size !== 1) return;
const target = names[0];
@@ -5,6 +5,7 @@ export const COPILOT_VERSION = "1.0.88" as const;
export const COPILOT_SYSTEM_INSTRUCTIONS_FILE = "copilot-instructions.md" as const;
export const COPILOT_SYSTEM_INSTRUCTION_DELIVERY = "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1" as const;
export const COPILOT_CREDENTIAL_ENVIRONMENT_NAME = "COPILOT_GITHUB_TOKEN" as const;
export const COPILOT_TASK_ORIENTATION_INSTRUCTIONS = "At the start of every task turn, use the paperclip MCP server's dedicated get_task_context tool with {} and wait for its successful result before task work, report_progress, or any other task-dependent mutation. This is an advertised MCP tool, not an HTTP API route: do not use search_api or call_api to discover or substitute for get_task_context. Loaded instructions, seed state, task descriptions, and prior-turn context do not replace this read. Decide the next action from the returned context. Do not issue the context read and a dependent action in parallel. If this tool is unavailable or its read fails, report the failure and do not perform dependent actions." as const;
// Keep permission callbacks enabled even when Paperclip's policy is approve-all.
// In particular COPILOT_ALLOW_ALL=true also trusts workspace hooks/plugins/MCP.
@@ -48,13 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "copilot",
"agentProfileVersion": 17,
"agentProfileVersion": 34,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
"qualificationStatus": "pending",
"commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
"permissionPolicy": "interactive"
},
"claude": {
@@ -1444,7 +1444,7 @@ function commandLease(
privateSnapshot: AcpxPrivateSnapshot | null,
): VerifiedAcpxCommandLease {
let consumed = false;
let directoriesReleased = false;
let directoriesRelease: Promise<void> | undefined;
let spawnedChild: ChildProcess | null = null;
let childExit: Promise<void> | null = null;
let snapshotCleanup: Promise<void> | null = null;
@@ -1456,24 +1456,23 @@ function commandLease(
}
return snapshotCleanup;
};
const releaseDirectories = async (): Promise<void> => {
if (directoriesReleased) return;
directoriesReleased = true;
await Promise.all([
const releaseDirectories = (): Promise<void> => {
return (directoriesRelease ??= Promise.all([
commandDirectory.close(),
...dependencyAncestors.map((handle) => handle.close()),
...(providerRuntimeExecutable === null
? []
: [providerRuntimeExecutable.close()]),
]);
]).then(() => undefined));
};
const releaseDirectoriesBestEffort = (): void => {
void releaseDirectories().catch(() => undefined);
};
const close = async (): Promise<void> => {
if (consumed && privateSnapshot === null) return;
const wasConsumed = consumed;
consumed = true;
verifiedBytes.fill(0);
// A spawned command owns its source buffer until its pipe write completes.
if (!wasConsumed) verifiedBytes.fill(0);
await releaseDirectories();
// A spawned provider may still read the snapshot. Runtime shutdown runs in
// parallel and retires that child; do not remove its bytes or report command
@@ -1568,7 +1567,7 @@ function commandLease(
environment[VERIFIED_PROVIDER_RUNTIME_TARGET_ENV] =
providerRuntimeEnvironmentVariable;
}
child = spawnChildProcess(
child = spawnedChild = spawnChildProcess(
runtimeHandoff.executable,
guarded
? [
@@ -10,6 +10,9 @@ import type { ChildProcess } from "node:child_process";
import { afterEach, describe, expect, it, vi } from "vitest";
import { awaitVerifiedAcpxProviderExit, awaitVerifiedAcpxProviderOwnership, verifyNativeAcpxInstallation } from "./installation-integrity.js";
import { createNativeAcpxDistributionSnapshot, readNativeAcpxDistributionEntries, parseNativeAcpxDistributionEntries, type NativeAcpxDistributionInput, type NativeAcpxDistributionEntry } from "./native-distribution-integrity.js";
import { openCodexAcpxRuntime } from "./codex-runtime-adapter.js";
import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js";
import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
@@ -322,6 +325,36 @@ describe("native ACPX execution closure", () => {
expect(first.text).toMatch(/^native:fixed:.*paperclip-acpx-native-.*\/state$/);
expect(first.text).not.toBe(second.text);
});
it("reports failed deletion and retries retirement without relaunching", async () => {
const lease = await (await verifyNativeAcpxInstallation(await fixture())).openCommand();
const originalRemove = vi.mocked(rm).getMockImplementation()!;
const deleting = gate(); const holdDeletion = gate();
let privateRoot: string | undefined;
let failed = false;
vi.mocked(rm).mockImplementation(async (path, options) => {
if (String(path).includes("paperclip-acpx-native-") && !failed) {
privateRoot = String(path); deleting.release(); await holdDeletion.promise;
failed = true; throw new Error("snapshot deletion failed");
}
return await originalRemove(path, options);
});
try {
expect((await output(lease.spawn())).code).toBe(0);
await deleting.promise;
const rejected = expect(lease.close()).rejects.toThrow("snapshot deletion failed");
await new Promise<void>(resolve => setImmediate(resolve));
holdDeletion.release();
await rejected;
await expect(stat(privateRoot!)).resolves.toBeDefined();
expect(() => lease.spawn()).toThrow("closed");
await lease.close();
await expect(stat(privateRoot!)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
holdDeletion.release();
vi.mocked(rm).mockImplementation(originalRemove);
await lease.close();
}
});
// Copy/hash the real Node closure and start its bootstrap + owned shim. These
// operations share the adjacent native-closure test's bounded allowance;
// Vitest's 5s default is not a provider startup or permission deadline.
@@ -617,8 +650,114 @@ describe("native ACPX execution closure", () => {
child.kill(); await Promise.all(fences.map(server => new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))));
}
}, 15_000);
it("proves native child death before any caller cleanup, despite the launcher's pending guardian read", async () => {
const declaration = await fixture({ node: true, script: 'console.log(process.pid);setInterval(()=>{},1000);' });
const fences = await Promise.all([listen(), listen()]);
const fds = fences.map(server => (server as Server & { _handle?: { fd?: number } })._handle!.fd!);
const child = (await (await verifyNativeAcpxInstallation(declaration)).openCommand()).spawn([], {}, {
credentialFenceFds: [fds[0]!, fds[1]!], activateCredentialFenceOwner: async () => undefined,
});
const exited = once(child, "exit");
let deadline: ReturnType<typeof setTimeout> | undefined;
try {
const ready = once(child.stdout!, "data");
await awaitVerifiedAcpxProviderOwnership(child);
const [chunk] = await ready;
const nativePid = Number(String(chunk).trim());
expect(Number.isSafeInteger(nativePid) && nativePid > 0 && nativePid !== child.pid).toBe(true);
const proof = awaitVerifiedAcpxProviderExit(child);
process.kill(nativePid, "SIGKILL");
// No guardian.kill/close is allowed to manufacture the lifetime proof.
await Promise.race([proof, new Promise((_, reject) => {
deadline = setTimeout(() => reject(new Error("Native exit proof remained open after child death")), 3_000);
})]);
await exited;
expect(() => process.kill(nativePid, 0)).toThrow();
} finally {
clearTimeout(deadline);
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
await exited;
await Promise.all(fences.map(server => new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))));
}
}, 15_000);
it("expires a real ACPX permission on native child death without replaying its mutation", async () => {
// This is a deterministic ACP peer in a verified native distribution. No
// provider credential, remote service or model is involved.
const declaration = await fixture({ node: true, script: blockingCopilotPeer });
const fences = await Promise.all([listen(), listen()]);
const fds = fences.map(server => (server as Server & { _handle?: { fd?: number } })._handle!.fd!);
const installation = await verifyNativeAcpxInstallation(declaration);
const owner = createAcpxCommandLeaseOwner(await installation.openCommand(), installation.openCommand);
const pidFile = join(declaration.distributionRoot, "native-pid");
let port: Awaited<ReturnType<typeof openCodexAcpxRuntime>> | undefined;
let deadline: ReturnType<typeof setTimeout> | undefined;
try {
port = await openCodexAcpxRuntime({
command: owner.command, refreshConsumedCommand: owner.refreshConsumedCommand,
profile: resolveQualifiedAcpxProfile("copilot", "gpt-5.6-luna"),
cwd: declaration.distributionRoot, stateDirectory: join(declaration.distributionRoot, "state"),
providerSessionKey: "native-death-fixture", permissionMode: "approve-reads",
permissionPolicy: { autoApprove: ["read"], escalate: ["write"], defaultAction: "escalate" },
launchEnvironment: { PATH: "/usr/bin:/bin", NATIVE_TEST_PID_FILE: pidFile },
credentialFenceFds: [fds[0]!, fds[1]!], activateCredentialFenceOwner: async () => undefined,
systemInstructions: "Read task context before editing.", mcpServers: [], retainFailedAdmissionCleanup: () => undefined,
});
let pending!: () => void;
const requested = new Promise<void>(resolve => { pending = resolve; });
let callbackSignal!: AbortSignal;
let lateAnswer!: (value: { outcome: "allow_once" }) => void;
const handler = vi.fn((_request, context) => {
callbackSignal = context.signal; pending();
return new Promise<{ outcome: "allow_once" }>(resolve => { lateAnswer = resolve; });
});
const turn = port.startTurn({ text: "Attempt one edit", requestId: "native-death-turn", onPermissionRequest: handler });
const settled = expect(turn.result).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const draining = expect((async () => { for await (const _event of turn.events) { /* drain */ } })())
.rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const bounded = new Promise<never>((_, reject) => {
deadline = setTimeout(() => reject(new Error("Real native ACPX death remained unsettled")), 5_000);
});
await Promise.race([requested, bounded]);
const nativePid = Number(await readFile(pidFile, "utf8"));
expect(Number.isSafeInteger(nativePid) && nativePid > 0).toBe(true);
process.kill(nativePid, "SIGKILL");
await Promise.race([Promise.all([settled, draining]), bounded]);
expect(callbackSignal.aborted).toBe(true);
lateAnswer({ outcome: "allow_once" });
expect(() => port!.startTurn({ text: "Replay", requestId: "replay" })).toThrow("provider exited");
expect(handler).toHaveBeenCalledOnce();
expect(() => process.kill(nativePid, 0)).toThrow();
} finally {
clearTimeout(deadline);
await port?.close({ reason: "offline native-death fixture complete" });
await owner.command.close();
await Promise.all(fences.map(server => new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))));
}
}, 15_000);
});
const blockingCopilotPeer = String.raw`
const send=m=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',...m})+'\n');
require('node:fs').writeFileSync(process.env.NATIVE_TEST_PID_FILE,String(process.pid));
const mode='https://agentclientprotocol.com/protocol/session-modes#agent';
const config=[{id:'mode',name:'Mode',type:'select',currentValue:mode,options:[{value:mode,name:'Agent'}]},
{id:'allow_all',name:'Allow all',type:'select',currentValue:'off',options:[{value:'off',name:'Off'}]},
{id:'model',name:'Model',type:'select',currentValue:'gpt-5.6-luna',options:[{value:'gpt-5.6-luna',name:'Luna'}]}];
const snapshot={sessionId:'copilot-death-session',modes:{currentModeId:mode,availableModes:[{id:mode,name:'Agent'}]},configOptions:config,
models:{currentModelId:'gpt-5.6-luna',availableModels:[{modelId:'gpt-5.6-luna',name:'Luna'}]}};
require('node:readline').createInterface({input:process.stdin}).on('line',line=>{
const m=JSON.parse(line);
if(m.method==='initialize')send({id:m.id,result:{protocolVersion:1,agentCapabilities:{loadSession:true},authMethods:[]}});
else if(['session/new','session/load'].includes(m.method))send({id:m.id,result:snapshot});
else if(m.method==='session/set_model'||m.method==='session/close')send({id:m.id,result:{}});
else if(m.method==='session/set_config_option')send({id:m.id,result:{configOptions:config}});
else if(m.method==='session/prompt')send({id:'pending-edit',method:'session/request_permission',params:{sessionId:'copilot-death-session',
toolCall:{toolCallId:'native-edit',title:'Write file',kind:'edit',status:'pending',rawInput:{path:'target.txt',content:'FORBIDDEN'}},
options:[{optionId:'allow',name:'Allow',kind:'allow_once'},{optionId:'deny',name:'Deny',kind:'reject_once'}]}});
});`;
async function listen(): Promise<Server> {
const server = createServer(); server.listen(0, "127.0.0.1"); await once(server, "listening"); return server;
}
@@ -258,7 +258,12 @@ function nativeBootstrap(executable: string, executableDigest: string, args: str
`if(require("node:crypto").createHash("sha256").update(fs.readFileSync(fd)).digest("hex")!==${JSON.stringify(executableDigest)})throw new Error("Native ACPX executable changed before spawn");`,
'const stdio=guarded?[0,1,2,5,6,7,8]:[0,1,2]; const childExecutable=process.platform==="linux"?"/proc/self/fd/"+stdio.length:executable;stdio.push(fd);',
`const child=spawn(childExecutable,${JSON.stringify(args)},{env,cwd:process.cwd(),shell:false,detached:false,stdio});fs.closeSync(fd);`,
'child.once("error",()=>process.exit(1)); child.once("exit",(code,signal)=>process.exit(signal?1:code??1));',
// A guarded Node launcher has a pending filesystem read on its guardian
// pipe. process.exit can wait for that read while the guardian waits for
// this launcher to exit. Retire the live owned group through the kernel;
// no saved PID/group identity or provider JavaScript cleanup is required.
'const retire=code=>{if(guarded){try{process.kill(0,"SIGKILL");}catch{process.kill(process.pid,"SIGKILL");}}else process.exit(code);};',
'child.once("error",()=>retire(1)); child.once("exit",(code,signal)=>retire(signal?1:code??1));',
'for(const signal of ["SIGTERM","SIGINT","SIGHUP"])process.on(signal,()=>child.kill(signal));',
].join("\n");
}
@@ -28,7 +28,7 @@ export interface AcpxActivityAdapter {
const adapters: Readonly<Record<string, AcpxActivityAdapter | undefined>> = {
cursor: cursorActivityAdapter,
copilot: { createToolEvidence: createCopilotToolEvidence },
copilot: Object.freeze({ createToolEvidence: createCopilotToolEvidence }),
};
const noActivity: AcpxActivityAdapter = Object.freeze({});
@@ -19,6 +19,19 @@ describe("historical ACPX profile decoding", () => {
expect(isSupportedAcpxProfileVersion(agent, 6)).toBe(false);
}
});
it("decodes retained Copilot revisions and the current profile without widening other providers", () => {
for (const version of [1, 5, 6, 12, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]) {
expect(isSupportedAcpxProfileVersion("copilot", version)).toBe(true);
}
expect(isSupportedAcpxProfileVersion("copilot", 35)).toBe(false);
for (const agent of ["cursor", "pi", "claude", "codex", "grok"]) {
expect(isSupportedAcpxProfileVersion(agent, 23)).toBe(false);
expect(isSupportedAcpxProfileVersion(agent, 24)).toBe(false);
expect(isSupportedAcpxProfileVersion(agent, 25)).toBe(false);
expect(isSupportedAcpxProfileVersion(agent, 26)).toBe(false);
expect(isSupportedAcpxProfileVersion(agent, 27)).toBe(false);
}
});
it.each([0, 1.5, "11", null, undefined, NaN])("rejects invalid revisions: %s", version => {
expect(isSupportedAcpxProfileVersion("cursor", version)).toBe(false);
});
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
/** Decodable historical profile revisions, not permission to launch them.
* Exact current profile/package/digest admission is checked separately. */
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33;
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33],
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};
@@ -1,9 +1,9 @@
import { assertCopilotCredentials, classifyCopilotFailure } from "./copilot-profile.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
import { verifyCursorInstallation } from "./cursor-installation.js";
import { assertCursorWorkspacePolicy } from "./cursor-launch-policy.js";
import { resolveQualifiedAcpxProfile, type QualifiedAcpxAgent, type QualifiedAcpxProfile } from "./qualified-profiles.js";
import { verifyPiInstallation } from "./pi-installation.js";
import { assertCopilotCredentials, classifyCopilotFailure } from "./copilot-profile.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
import { verifyQualifiedAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
/** Closed build-owned registry. Provider branches add their pinned installations here. */
@@ -31,6 +31,18 @@ it("resolves descriptor sidecars only through the runner-bound canonical package
expect(() => resolveRunnerProviderAssetsRoot("file:///proc/self/fd/18", "pi")).toThrow("normalized absolute");
});
it("uses the public server owner for its vendored Copilot probe and selected sidecar", async () => {
const path = await serverFixture();
await writeFile(join(path, "package.json"), JSON.stringify({ name: "@paperclipai/server" }));
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", undefined);
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", undefined);
const moduleUrl = pathToFileURL(join(path, "dist/vendor/paperclip-runner/drivers/acpx/copilot-installation.js")).href;
expect(resolveRunnerProviderAssetsRoot(moduleUrl, "copilot")).toBe(join(path, "provider-assets/copilot"));
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", path);
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", join(path, "package.json"));
expect(resolveRunnerProviderAssetsRoot("file:///proc/self/fd/18", "copilot")).toBe(join(path, "provider-assets/copilot"));
});
it("resolves the public server bundle without repository paths or candidate overrides", async () => {
const path = await root();
const vendored = join(await realpath(path), "dist/vendor/paperclip-runner");
@@ -44,7 +44,7 @@ export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: Nat
// runnerd derives this binding from the verified sidecar in the public
// server package. Images may carry assets alongside that bundle; local
// explicit setup uses the OS-account cache when package assets are absent.
packageRoot = join(dirname(canonicalManifest), "dist/vendor/paperclip-runner");
packageRoot = provider === "copilot" ? dirname(canonicalManifest) : join(dirname(canonicalManifest), "dist/vendor/paperclip-runner");
if (realpathSync(packageRoot) !== packageRoot) throw new Error("Vendored runner directory is not contained by its server package");
} else if (value?.name === RUNNER_PACKAGE_NAME) {
packageRoot = dirname(canonicalManifest);
@@ -60,7 +60,7 @@ export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: Nat
const metadata = readPackageManifest(manifest, manifest);
if (metadata.name !== SERVER_PACKAGE_NAME) throw new Error("Runner server vendor package identity is invalid");
assertServerVendorLayout(packageRoot);
packageRoot = join(packageRoot, "dist/vendor/paperclip-runner");
if (provider !== "copilot") packageRoot = join(packageRoot, "dist/vendor/paperclip-runner");
} else if (new RegExp(`/(?:src|dist)/drivers/acpx/${provider}-installation\\.(?:ts|js)$`).test(url.pathname)) packageRoot = fileURLToPath(new URL("../../../", url));
else if (/\/dist\/cli\/acpx-runtime-sidecar\.(?:cjs|js)$/.test(url.pathname)) packageRoot = fileURLToPath(new URL("../../", url));
else if (new RegExp(`/dist/vendor/paperclip-runner/drivers/acpx/${provider}-installation\\.(?:js)$`).test(url.pathname)) packageRoot = fileURLToPath(new URL("../../", url));
@@ -43,6 +43,7 @@ export function resolveQualifiedAcpxProfile(
): QualifiedAcpxProfile {
const profile = QUALIFIED_ACPX_PROFILES[agent];
if (!requestedModel.trim()) throw new Error("ACPX model must not be empty");
if (agent === "copilot" && ["auto", "default"].includes(requestedModel.trim().toLowerCase())) throw new Error("Copilot requires an explicit model ID");
return { ...structuredClone(profile), qualificationModel: requestedModel, reportedModelId: requestedModel };
}
@@ -1286,7 +1286,7 @@ describe("ACPX runtime host", () => {
).rejects.toThrow("initialization and cleanup failed");
expect(runtime.close).toHaveBeenCalledOnce();
expect(credential.close).not.toHaveBeenCalled();
expect(fixture.commandClose).toHaveBeenCalledOnce();
expect(fixture.commandClose).not.toHaveBeenCalled();
expect(retainedAdmissionCleanup).not.toBeNull();
let cleanupSettled = false;
void retainedAdmissionCleanup!.finally(() => {
@@ -1299,6 +1299,7 @@ describe("ACPX runtime host", () => {
await retainedAdmissionCleanup;
expect(cleanupSettled).toBe(true);
expect(credential.close).toHaveBeenCalledOnce();
expect(fixture.commandClose).toHaveBeenCalledOnce();
});
it("retains credential ownership when runtime shutdown fails until retry succeeds", async () => {
@@ -1376,7 +1377,7 @@ describe("ACPX runtime host", () => {
const first = host.close({ reason: "runtime close pending" });
await waitForAcpxOperation(() => expect(runtime.close).toHaveBeenCalledOnce());
await waitForAcpxOperation(() => expect(fixture.commandClose).toHaveBeenCalledOnce());
expect(fixture.commandClose).not.toHaveBeenCalled();
const second = host.close({ reason: "same exact close" });
await expect(readFile(authPath, "utf8")).resolves.toBe("{}");
await expect(
@@ -1394,6 +1395,7 @@ describe("ACPX runtime host", () => {
undefined,
undefined,
]);
expect(fixture.commandClose).toHaveBeenCalledOnce();
await expect(readFile(authPath)).rejects.toMatchObject({ code: "ENOENT" });
const contender = await waitForAcpxOperation(() =>
stageManagedCodexCredential({
@@ -1406,9 +1408,10 @@ describe("ACPX runtime host", () => {
await contender.close();
}, ACPX_LONG_WAIT_TEST_TIMEOUT_MS);
it("retains the exact pending cleanup while independent resources close", async () => {
it("retains the exact pending cleanup and command snapshot until runtime retirement", async () => {
const fixture = await hostFixture();
const firstClose = new Promise<void>(() => undefined);
let resolveRuntimeClose!: () => void;
const firstClose = new Promise<void>(resolve => { resolveRuntimeClose = resolve; });
const runtime = runtimePort({
onClose: vi
.fn()
@@ -1436,6 +1439,9 @@ describe("ACPX runtime host", () => {
await Promise.resolve();
expect(settled).toBe(false);
expect(runtime.close).toHaveBeenCalledOnce();
expect(fixture.commandClose).not.toHaveBeenCalled();
resolveRuntimeClose();
await Promise.all([first, second]);
expect(fixture.commandClose).toHaveBeenCalledOnce();
}, ACPX_LONG_WAIT_TEST_TIMEOUT_MS);
@@ -1239,11 +1239,9 @@ async function cleanupRuntimeResources(
return error;
}
};
// The command lease owns only the already-consumed verified launch
// snapshot, so it can be released as shutdown starts. The credential is
// different: a provider whose exact runtime close is pending or failed may
// still read or rewrite its home. Retain both the staged bytes and the
// exclusive home lease until that exact close succeeds.
// The runtime must retire before its command snapshot can be removed.
// Keep both the snapshot and credential while the provider is still alive;
// failed cleanup remains owned and can be retried after process retirement.
const runtimeOutcome = runtime
? settle(() => runtime.close({ reason }))
: Promise.resolve(null);
@@ -1253,9 +1251,10 @@ async function cleanupRuntimeResources(
await runtimeOutcome;
return toolBridge === null ? null : await settle(() => toolBridge.close());
})();
const commandOutcome = command
? settle(() => command.close())
: Promise.resolve(null);
const commandOutcome = (async (): Promise<unknown | null> => {
await runtimeOutcome;
return command === null ? null : await settle(() => command.close());
})();
const credentialOutcome = (async (): Promise<unknown | null> => {
const runtimeError = await runtimeOutcome;
if (runtimeError !== null || credential === null) return null;
@@ -1,6 +1,6 @@
import { piProviderConfiguration } from "./pi-provider-config.js";
import { configuredEnvironmentKeys } from "../../configured-environment.js";
import { COPILOT_SYSTEM_INSTRUCTIONS_FILE } from "./copilot-profile.js";
import { COPILOT_SYSTEM_INSTRUCTIONS_FILE, COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js";
import { randomBytes } from "node:crypto";
import {
constants,
@@ -646,13 +646,15 @@ export async function refreshCopilotSystemInstructions(
sandbox: Pick<AcpxRuntimeSandbox, "agentHomeDirectory">,
instructions: string,
): Promise<void> {
if (instructions.includes("\0") || Buffer.byteLength(instructions) > 32 * 1024) {
const current = `${instructions}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`;
if (current.includes("\0") || Buffer.byteLength(current) > 32 * 1024) {
throw new Error("Provider runtime instructions exceed their bounded size");
}
// Native Copilot reloads this file on session/load. Empty text clears old text.
// Native Copilot reloads this file on session/load. Empty caller text clears
// old caller instructions while retaining the current task-orientation rule.
await writePrivateFile(
join(sandbox.agentHomeDirectory, COPILOT_SYSTEM_INSTRUCTIONS_FILE),
`${instructions}\n`,
current,
);
}
@@ -65,7 +65,7 @@ for (const [provider, create] of [["cursor", createCursorToolEvidence], ["copilo
s.projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "other", kind: "read", status: "completed", rawInput: { path } });
expect(s.rows).toHaveLength(1); expect(s.rows[0].readTargetSha256).toBeUndefined();
});
it("publishes the pending origin after its delayed complete input, retaining exact proof on every notice", () => {
it.runIf(provider === "cursor")("publishes the pending origin after its delayed complete input, retaining exact proof on every notice", () => {
const s = setup();
s.projector.tool({ ...origin, rawInput: {}, locations: undefined });
expect(s.rows).toEqual([]);
@@ -1,5 +1,6 @@
import { configuredEnvironment } from "../../configured-environment.js";
import type { NativeTurnControlCapabilities } from "../../contracts/types.js";
import type { NativeRunRuntimeGrant } from "../../contracts/runtime-context.js";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
import { githubCredentialEnvironment } from "../../github-credential-environment.js";
@@ -67,6 +68,7 @@ export interface CodexAppServerTransport {
runId: string;
turnId: string;
itemId: string;
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void>;
/** Records post-rehydration driver mapping without affecting run authority. */
recordTraceInterpretation?(input: CodexTraceInterpretation): void;
@@ -128,9 +128,10 @@ describe("Codex app-server Codex driver", () => {
message: { role: "user", text: "first turn" },
});
await expect(
session.attachRun?.({ runId: "run-warm-second" }),
session.attachRun?.({ runId: "run-warm-second", currentRunGrant: { runtimeContext: null, instructions: "Current registered instructions." } }),
).resolves.toBeUndefined();
expect(transport.attachments).toHaveLength(1);
expect(transport.attachments[0]).toMatchObject({ currentRunGrant: { runtimeContext: null, instructions: "Current registered instructions." } });
expect((await session.snapshot()).activeTurnId).toBeNull();
await expect(
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { runnerdRuntimeRequestExpiryNotification } from "../../live/runnerd-codex-transport.js";
import { resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url";
import { SemanticToolOutcomeUnknownError } from "../../contracts/native-session-backend.js";
@@ -2407,6 +2408,67 @@ describe("Codex app-server Codex driver", () => {
for (const event of terminal) expect(validatePrpEvent(event).ok).toBe(true);
});
it("preserves runnerd permission expiry before a failed provider terminal and refuses late approval", async () => {
const transport = new FakeCodexTransport();
const resolve = vi.fn(async () => {});
transport.runtimeRequestResolver = resolve;
const session = await makeDriver([transport]).openSession({
runId: "run-native-expiry", normalizedSessionId: "session-native-expiry", workingDirectory: TEST_WORKING_DIRECTORY,
});
const events: PrpEvent[] = [];
const consume = (async () => { for await (const event of session.events()) events.push(event); })();
const { turnId } = await session.startTurn({ message: { role: "user", text: "Leave the native edit unanswered." } });
const pending = transport.invoke({ id: "native-edit-permission", method: "session/request_permission", params: {
threadId: "thread-1", turnId, itemId: "native-edit", reason: "Write file",
choices: [{ key: "accept", label: "Allow once" }, { key: "decline", label: "Deny" }],
origin: { adapter: "acpx-runtime-sidecar", provider: "acpx", method: "session/request_permission" },
} });
await vi.waitFor(() => expect(session.pendingRuntimeRequests?.()).toHaveLength(1));
// Exact Rust ACPX projector settlement shape; the facade uses its own
// provider-turn identity just as it does when creating the callback.
const payload = { provider: "acpx", requestId: "native-edit-permission", requestKind: "permission_approval",
requestType: "permission", turnId: "durable-controller-turn", itemId: "durable-item",
reason: "provider_process_lost", replayAllowed: false, adapter: "acpx-runtime-sidecar" };
for (const kind of ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.created"])
expect(runnerdRuntimeRequestExpiryNotification(kind, payload, "thread-1", turnId)).toBeNull();
const expiry = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", turnId)!;
transport.push(expiry.method, expiry.params);
transport.push(expiry.method, expiry.params); // Same durable retirement cannot settle twice.
await pending;
expect(session.pendingRuntimeRequests?.()).toEqual([]);
await expect(session.resolveRuntimeRequest?.({ requestId: "native-edit-permission", turnId, resolution: { action: "accept" } }))
.rejects.toThrow();
expect(resolve).not.toHaveBeenCalled();
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } });
await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed")).toBe(true));
await session.close(); await consume;
const settlements = events.filter(event => ["runtime_request.expired", "runtime_request.cancelled", "runtime_request.resolved"].includes(event.eventType));
expect(settlements).toHaveLength(1);
expect(settlements[0]).toMatchObject({ eventType: "runtime_request.expired", turnId, itemId: "native-edit", payload: { ...payload, turnId, itemId: "native-edit" } });
expect(events.indexOf(settlements[0]!)).toBeLessThan(events.findIndex(event => event.eventType === "turn.failed"));
expect(validatePrpEvent(settlements[0]!)).toMatchObject({ ok: true });
});
it.each(["turn", "kind", "type", "replay"])("refuses canonical permission expiry with a changed %s binding", async kind => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport]).openSession({ runId: "expiry-bound", normalizedSessionId: "expiry-bound", workingDirectory: TEST_WORKING_DIRECTORY });
const events: PrpEvent[] = [];
const consume = (async () => { for await (const event of session.events()) events.push(event); })();
const { turnId } = await session.startTurn({ message: { role: "user", text: "Wait for permission" } });
const pending = transport.invoke({ id: "bound-permission", method: "session/request_permission", params: {
threadId: "thread-1", turnId, itemId: "edit", reason: "Edit", choices: [{ key: "accept", label: "Allow once" }, { key: "decline", label: "Deny" }],
origin: { adapter: "acpx-runtime-sidecar", provider: "acpx", method: "session/request_permission" },
} });
await vi.waitFor(() => expect(session.pendingRuntimeRequests?.()).toHaveLength(1));
const payload = { requestId: "bound-permission", requestKind: kind === "kind" ? "runtime" : "permission_approval",
requestType: kind === "type" ? "input" : "permission", replayAllowed: kind === "replay", reason: "provider_process_lost" };
const notification = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", kind === "turn" ? "foreign-turn" : turnId)!;
transport.push(notification.method, notification.params);
await pending; await consume; await session.close();
expect(events.some(e => e.eventType === "runtime_request.expired")).toBe(false);
expect(events.some(e => e.eventType === "session.failed" && e.payload.code === "turn_binding_mismatch")).toBe(true);
});
it("expires a canonical input with its full question set when the provider transport is lost", async () => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport]).openSession({
@@ -23,6 +23,7 @@ import {
parseHarnessRuntimeRequestResolution,
} from "../../contracts/harness-driver.js";
import type { NativeUserMessage } from "../../contracts/types.js";
import type { NativeRunRuntimeGrant } from "../../contracts/runtime-context.js";
import {
CODEX_RESULT_OUTPUT_SCHEMA,
type CodexModelContextSnapshot,
@@ -97,7 +98,7 @@ export class CodexHarnessSession
};
}
async attachRun(input: { runId: string }): Promise<void> {
async attachRun(input: { runId: string; currentRunGrant?: NativeRunRuntimeGrant }): Promise<void> {
this.assertProtocolIntegrity();
const transportOwnsQuiescence = this.transport.attachRun !== undefined;
if (
@@ -112,6 +113,7 @@ export class CodexHarnessSession
runId: input.runId,
turnId: `turn_attachment_${randomUUID().replaceAll("-", "")}`,
itemId: `item_attachment_${randomUUID().replaceAll("-", "")}`,
...(input.currentRunGrant ? { currentRunGrant: structuredClone(input.currentRunGrant) } : {}),
});
this.assertProtocolIntegrity();
if (transportOwnsQuiescence) {
@@ -16,6 +16,10 @@ describe("provider notification authority", () => {
["paperclip/canonicalProviderEvent", { threadId: null, eventType: "provider.notice.recorded" }, "root"],
["paperclip/canonicalProviderEvent", { threadId: null, eventType: "tool.execution.started" }, "invalid_authority"],
["item/started", { threadId: "root", turnId: "active" }, "root"],
["paperclip/runtimeRequestExpired", { threadId: "root", turnId: "active" }, "root"],
["paperclip/runtimeRequestExpired", { threadId: "child", turnId: "active" }, "invalid_authority"],
["paperclip/runtimeRequestExpired", { threadId: null, turnId: "active" }, "invalid_authority"],
["paperclip/runtimeRequestExpired", { threadId: "root", turnId: "old" }, "stale_turn"],
["turn/completed", { threadId: "root", turnId: "old" }, "stale_turn"],
[
"turn/completed",
@@ -150,6 +150,26 @@ async function mapNotificationBody(state: CodexSessionState, notification: Codex
const threadId = text(params.threadId);
const turnId = text(params.turnId, text(turn.id));
const itemId = text(item.id, text(params.itemId));
if (notification.method === "paperclip/runtimeRequestExpired") {
const payload = record(params.payload);
const requestId = text(payload.requestId);
const pending = state.pendingRuntimeRequestMap.get(requestId);
if (pending === undefined) return;
if (pending.request.turnId !== turnId || payload.requestKind !== pending.request.requestKind
|| payload.requestType !== (pending.request.input === undefined ? "permission" : "input")
|| payload.replayAllowed !== false || typeof payload.reason !== "string" || !payload.reason) {
state.failProtocol("turn_binding_mismatch", "Canonical request expiry changed its callback binding.");
return;
}
state.pendingRuntimeRequestMap.delete(requestId);
// Preserve durable expiry evidence with the same facade identity used
// when creating this callback. Terminal handling must not cancel it again.
state.emit("runtime_request.expired", { ...payload,
turnId: pending.request.turnId, itemId: pending.request.itemId,
}, { turnId: pending.request.turnId, itemId: pending.request.itemId });
pending.settle(safeRequestResponse(pending.request.method, "cancel"));
return;
}
if (notification.method === "paperclip/canonicalProviderEvent") {
if (!isCanonicalProviderEventType(params.eventType)) {
state.failProtocol("provider_event_type_invalid", "Unknown canonical provider event type.");
@@ -142,6 +142,7 @@ export function isSupportedCodexNotificationMethod(method: string): boolean {
method === "model/safetyBuffering/updated" ||
method.startsWith("item/") ||
method === "paperclip/canonicalProviderEvent" ||
method === "paperclip/runtimeRequestExpired" ||
method === "paperclip/workspaceChange/updated" ||
method === "paperclip/runResult" ||
method === "turn/diff/updated" ||
@@ -0,0 +1,78 @@
import { EventEmitter } from "node:events";
import { PassThrough, Writable } from "node:stream";
import { access } from "node:fs/promises";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QUALIFIED_ACPX_PROFILES } from "./acpx/qualified-profiles.js";
const mocks = vi.hoisted(() => ({ verify: vi.fn() }));
vi.mock("./acpx/copilot-installation.js", () => ({ verifyCopilotInstallation: mocks.verify }));
import { createMetadataRpc, probeCopilotMetadata, validateCopilotMetadata } from "./copilot-metadata-probe.js";
function fakeChild(reply: (message: any) => unknown) {
const child = new EventEmitter() as ChildProcessWithoutNullStreams;
child.stdout = new PassThrough(); child.stderr = new PassThrough();
child.kill = vi.fn(() => { child.emit("close", 0); return true; });
child.stdin = new Writable({ write(chunk, _encoding, done) {
const message = JSON.parse(chunk.toString());
const response = reply(message);
if (response !== undefined) queueMicrotask(() => child.stdout.emit("data", Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, ...response as object })}\n`)));
done();
}, final(done) { queueMicrotask(() => child.emit("close", 0)); done(); } });
return child;
}
const verified = { status: "verified", version: "1.0.88", profileDigest: QUALIFIED_ACPX_PROFILES.copilot.commandDigest, models: [{ id: "gpt-5.6-luna", label: "GPT" }], promptSent: false };
beforeEach(() => vi.clearAllMocks());
describe("Copilot metadata boundary", () => {
it("sends metadata only, binds an explicit token, and removes its private home", async () => {
const methods: string[] = []; let environment: NodeJS.ProcessEnv = {}, cwd = "";
const close = vi.fn(async () => {});
const child = fakeChild(message => {
methods.push(message.method);
if (message.method === "initialize") return { result: { agentInfo: { version: "1.0.88" } } };
if (message.method === "session/new") return { result: { sessionId: "session", models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "GPT" }] } } };
if (message.method === "session/set_config_option") return { result: { configOptions: [{ id: "model", currentValue: "gpt-5.6-luna" }] } };
return { result: {} };
});
mocks.verify.mockResolvedValue({ commandDigest: verified.profileDigest, openCommand: async () => ({ close, spawn: (_args: unknown, options: any) => { environment = options.env; cwd = options.cwd; return child; } }) });
expect(await probeCopilotMetadata("github_pat_private_test", "gpt-5.6-luna")).toEqual(verified);
expect(methods).toEqual(["initialize", "session/new", "session/set_model", "session/set_config_option"]);
expect(environment.COPILOT_GITHUB_TOKEN).toBe("github_pat_private_test");
for (const key of ["GH_TOKEN", "GITHUB_TOKEN", "NODE_OPTIONS", "SSH_AUTH_SOCK"]) expect(environment).not.toHaveProperty(key);
expect(environment.HOME).toContain(cwd); expect(close).toHaveBeenCalledOnce();
await expect(access(cwd)).rejects.toThrow();
});
it("rejects an unavailable model without sending a prompt or selecting another model", async () => {
const methods: string[] = [];
const child = fakeChild(message => { methods.push(message.method); return { result: message.method === "initialize" ? { agentInfo: { version: "1.0.88" } } : { sessionId: "session", models: { availableModels: [{ modelId: "other" }] } } }; });
const close = vi.fn(async () => {});
mocks.verify.mockResolvedValue({ commandDigest: verified.profileDigest, openCommand: async () => ({ close, spawn: () => child }) });
expect(await probeCopilotMetadata("token", "missing")).toMatchObject({ status: "failed", code: "COPILOT_MODEL_UNAVAILABLE", promptSent: false });
expect(methods).toEqual(["initialize", "session/new"]); expect(close).toHaveBeenCalledOnce();
});
it("preserves model labels split inside every UTF-8 character across transport chunks", async () => {
const child = fakeChild(() => undefined), rpc = createMetadataRpc(child);
const result = { models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "東京 🚀 café" }] } };
const response = rpc.request("session/new", {});
const wire = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 0, result })}\n`);
for (const byte of wire) child.stdout.emit("data", Buffer.from([byte]));
await expect(response).resolves.toEqual(result);
});
it("cannot dispatch semantic or prompt methods", () => {
const child = fakeChild(() => undefined), rpc = createMetadataRpc(child);
expect(() => rpc.request("session/prompt", {})).toThrow("cannot send");
expect(() => rpc.request("tools/call", {})).toThrow("cannot send");
});
it("rejects outstanding calls when the provider dies", async () => {
const child = fakeChild(() => undefined), rpc = createMetadataRpc(child);
const promise = rpc.request("initialize", {}); child.emit("close", 1);
await expect(promise).rejects.toThrow("provider exited");
});
it("accepts only current profile metadata and sanitized failures", () => {
expect(validateCopilotMetadata(verified)).toEqual(verified);
for (const mutation of [{ promptSent: true }, { profileDigest: "stale" }, { models: [] }, { models: [{ id: "bad id", label: "x" }] }, { models: [{ id: "safe", label: "github_pat_secret" }] }, { models: [...verified.models, ...verified.models] }]) {
expect(validateCopilotMetadata({ ...verified, ...mutation })).toMatchObject({ status: "failed", code: "COPILOT_REQUEST_FAILED" });
}
const failure = validateCopilotMetadata({ status: "failed", code: "COPILOT_AUTH_REQUIRED", message: "github_pat_secret", promptSent: false });
expect(failure).toMatchObject({ code: "COPILOT_AUTH_REQUIRED" }); expect(JSON.stringify(failure)).not.toContain("secret");
expect(validateCopilotMetadata({ status: "failed", code: "untrusted", promptSent: false })).toMatchObject({ code: "COPILOT_REQUEST_FAILED" });
});
});
@@ -0,0 +1,162 @@
import { StringDecoder } from "node:string_decoder";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { verifyCopilotInstallation } from "./acpx/copilot-installation.js";
import { QUALIFIED_ACPX_PROFILES, resolveQualifiedAcpxProfile } from "./acpx/qualified-profiles.js";
import { classifyCopilotFailure, copilotConfiguration, copilotSandboxEnvironment } from "./acpx/copilot-profile.js";
import { COPILOT_ACP_CLIENT_CAPABILITIES } from "./acpx/copilot-events.js";
type RecordValue = Record<string, unknown>;
const record = (value: unknown): RecordValue => value !== null && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : {};
const METHODS = new Set(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/close"]);
export type CopilotMetadataResult =
| { status: "verified"; version: "1.0.88"; profileDigest: string; models: Array<{ id: string; label: string }>; promptSent: false }
| { status: "failed"; code: string; message: string; promptSent: false };
/** A metadata-only client: no prompt, tool server, workspace, or ambient login. */
export async function probeCopilotMetadata(token: string, model?: string): Promise<CopilotMetadataResult> {
if (model && ["auto", "default"].includes(model.trim().toLowerCase())) return { status: "failed", code: "COPILOT_MODEL_UNAVAILABLE", message: "Select an explicit Copilot model.", promptSent: false };
if (!token.trim() || token.includes("\0")) return { status: "failed", code: "COPILOT_AUTH_REQUIRED", message: "Enter a valid Copilot personal access token.", promptSent: false };
let installation;
try { installation = await verifyCopilotInstallation(resolveQualifiedAcpxProfile("copilot", model ?? "metadata-discovery")); }
catch { return { status: "failed", code: "COPILOT_INSTALLATION_INVALID", message: "The selected environment does not have the verified Copilot runtime installed.", promptSent: false }; }
let root: string | undefined;
let lease: Awaited<ReturnType<typeof installation.openCommand>> | undefined;
let child: ChildProcessWithoutNullStreams | undefined;
let closed: Promise<void> | undefined;
let deadline: ReturnType<typeof setTimeout> | undefined;
let phase = "installation";
try {
lease = await installation.openCommand();
root = await mkdtemp(join(tmpdir(), "paperclip-copilot-metadata-"));
const directories = { homeDirectory: join(root, "home"), configDirectory: join(root, "config"), dataDirectory: join(root, "data"), cacheDirectory: join(root, "cache"), agentHomeDirectory: join(root, "copilot") };
for (const directory of Object.values(directories)) await mkdir(directory, { mode: 0o700 });
await writeFile(join(directories.agentHomeDirectory, "config.json"), JSON.stringify(copilotConfiguration()), { mode: 0o600 });
child = lease.spawn([], { cwd: root, env: { PATH: "/usr/bin:/bin", ...copilotSandboxEnvironment(directories), COPILOT_GITHUB_TOKEN: token }, stdio: "pipe" }) as ChildProcessWithoutNullStreams;
closed = new Promise(resolve => child!.once("close", () => resolve()));
const rpc = createMetadataRpc(child);
const work = async (): Promise<CopilotMetadataResult> => {
phase = "initialize";
const initialized = record(await rpc.request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-copilot-metadata", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES }));
if (record(initialized.agentInfo).version !== "1.0.88") throw Object.assign(new Error("Verified Copilot installation changed"), { code: "COPILOT_INSTALLATION_INVALID" });
phase = "session-new";
const opened = record(await rpc.request("session/new", { cwd: root, mcpServers: [] }));
const advertised = record(opened.models).availableModels;
const models = (Array.isArray(advertised) ? advertised : []).slice(0, 200).flatMap(value => {
const entry = record(value), id = entry.modelId;
return typeof id === "string" && !["auto", "default"].includes(id.toLowerCase()) && /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,199}$/.test(id)
? [{ id, label: typeof entry.name === "string" && !/[\x00-\x1f\x7f]|github_pat_|gh[pousr]_/.test(entry.name) ? entry.name.slice(0, 200) : id }] : [];
});
if (!models.length) throw new Error("Copilot model discovery unavailable");
if (model) {
if (!models.some(value => value.id === model)) throw Object.assign(new Error("Selected Copilot model unavailable"), { code: "COPILOT_MODEL_UNAVAILABLE" });
phase = "model-selection";
await rpc.request("session/set_model", { sessionId: opened.sessionId, modelId: model });
phase = "model-confirmation";
const configured = record(await rpc.request("session/set_config_option", { sessionId: opened.sessionId, configId: "model", value: model }));
if (!Array.isArray(configured.configOptions) || !configured.configOptions.some(option => record(option).id === "model" && record(option).currentValue === model)) {
throw Object.assign(new Error("Selected Copilot model unavailable"), { code: "COPILOT_MODEL_UNAVAILABLE" });
}
}
// EOF is the authoritative stdio shutdown; session/close is optional.
return { status: "verified", version: "1.0.88", profileDigest: installation.commandDigest, models, promptSent: false };
};
return await Promise.race([work(), new Promise<never>((_resolve, reject) => {
deadline = setTimeout(() => reject(new Error("Copilot metadata probe timed out")), 25_000);
})]);
} catch (error) {
const code = record(error).code;
if (code === "COPILOT_INSTALLATION_INVALID") return { status: "failed", code, message: "The selected environment has an incompatible Copilot installation.", promptSent: false };
if (code === "COPILOT_MODEL_UNAVAILABLE") return { status: "failed", code, message: "The selected Copilot model is unavailable for this account.", promptSent: false };
const failure = classifyCopilotFailure(error);
const cause = error instanceof Error && error.message === "Copilot metadata probe timed out" ? "deadline"
: error instanceof Error && error.message === "Copilot provider exited during metadata discovery" ? "provider-exit" : "request-rejected";
// Only closed diagnostic categories; never log provider errors, wire data,
// credentials, model catalogs, session IDs or private workspace paths.
console.warn("Copilot metadata verification failed", JSON.stringify({ phase, code: failure.code, cause }));
return { status: "failed", ...failure, promptSent: false };
} finally {
if (deadline) clearTimeout(deadline);
if (child && closed) {
child.stdin.end();
const term = setTimeout(() => child?.kill("SIGTERM"), 500);
const kill = setTimeout(() => child?.kill("SIGKILL"), 2_000);
await closed;
clearTimeout(term); clearTimeout(kill);
}
try { await lease?.close(); } finally { if (root) await rm(root, { recursive: true, force: true }); }
}
}
export function createMetadataRpc(child: ChildProcessWithoutNullStreams) {
const decoder = new StringDecoder("utf8");
const pending = new Map<number, { resolve(value: unknown): void; reject(error: Error): void }>();
let nextId = 0, bytes = 0, buffer = "", failure: Error | undefined;
const fail = (error: Error) => { failure ??= error; for (const call of pending.values()) call.reject(failure); pending.clear(); };
child.once("close", () => fail(new Error("Copilot provider exited during metadata discovery")));
child.once("error", () => fail(new Error("Copilot installation could not start")));
child.stdin.on("error", () => fail(new Error("Copilot metadata transport unavailable")));
child.stderr.on("data", () => {});
child.stdout.on("data", (chunk: Buffer) => {
bytes += chunk.length;
if (bytes > 2 * 1024 * 1024) { fail(new Error("Copilot metadata exceeded its bound")); child.kill("SIGTERM"); return; }
buffer += decoder.write(chunk);
while (buffer.includes("\n")) {
const end = buffer.indexOf("\n"), line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
if (!line.trim()) continue;
let message: RecordValue;
try { message = record(JSON.parse(line)); } catch { fail(new Error("Invalid Copilot metadata response")); return; }
if (message.method) {
if (message.id !== undefined) child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Metadata discovery does not support inbound methods" } })}\n`);
continue;
}
const call = pending.get(message.id as number);
if (!call) continue;
pending.delete(message.id as number);
if (message.error) {
const details = record(message.error);
call.reject(new Error(typeof details.message === "string" ? details.message.slice(0, 8192) : "Copilot metadata request failed"));
} else call.resolve(message.result);
}
});
return { request(method: string, params: RecordValue): Promise<unknown> {
if (!METHODS.has(method)) throw new Error("Metadata discovery cannot send this method");
if (failure) return Promise.reject(failure);
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
});
} };
}
const FAILURE_MESSAGES = {
COPILOT_AUTH_REQUIRED: "Enter a valid Copilot personal access token.",
COPILOT_ENTITLEMENT_DENIED: "Copilot access is denied by the account entitlement or organization policy.",
COPILOT_MODEL_UNAVAILABLE: "The selected Copilot model is unavailable for this account.",
COPILOT_INSTALLATION_INVALID: "Install the verified Copilot runtime in this environment.",
COPILOT_REQUEST_FAILED: "Copilot metadata verification failed.",
} as const;
/** Validate remote metadata and discard all provider-authored error text. */
export function validateCopilotMetadata(value: unknown): CopilotMetadataResult {
const result = record(value);
const invalid = (): CopilotMetadataResult => ({ status: "failed", code: "COPILOT_REQUEST_FAILED", message: FAILURE_MESSAGES.COPILOT_REQUEST_FAILED, promptSent: false });
if (result.promptSent !== false) return invalid();
if (result.status === "failed") {
const code = typeof result.code === "string" && Object.hasOwn(FAILURE_MESSAGES, result.code) ? result.code as keyof typeof FAILURE_MESSAGES : "COPILOT_REQUEST_FAILED";
return { status: "failed", code, message: FAILURE_MESSAGES[code], promptSent: false };
}
if (result.status !== "verified" || result.version !== "1.0.88" || result.profileDigest !== QUALIFIED_ACPX_PROFILES.copilot.commandDigest
|| !Array.isArray(result.models) || result.models.length === 0 || result.models.length > 200) return invalid();
const models: Array<{ id: string; label: string }> = [];
const ids = new Set<string>();
for (const value of result.models) {
const entry = record(value);
if (typeof entry.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(entry.id) || ids.has(entry.id)
|| typeof entry.label !== "string" || entry.label.length > 200 || /[\x00-\x1f\x7f]|github_pat_|gh[pousr]_/.test(entry.label)) return invalid();
ids.add(entry.id); models.push({ id: entry.id, label: entry.label });
}
return { status: "verified", version: "1.0.88", profileDigest: result.profileDigest, models, promptSent: false };
}
@@ -7,6 +7,7 @@ export * from "./turn-stream.js";
export * from "./linux-process-start.js";
export { probeAcpxClaudeInstallation, probeAcpxGrokInstallation, probeAcpxPiInstallation } from "../drivers/acpx/installation-integrity.js";
export { probeCopilotMetadata, validateCopilotMetadata, type CopilotMetadataResult } from "../drivers/copilot-metadata-probe.js";
export { probeAcpxCursorInstallation } from "../drivers/acpx/profile-installation.js";
@@ -158,6 +158,10 @@ it("does not dispatch an expired durable request when its consumer resumes", asy
fixture.emitPermission();
peer.current!.emit("runtime_request.expired", { requestId: "permission-7" });
fixture.emitMarker();
expect((await fixture.iterator.next()).value).toMatchObject({
method: "paperclip/runtimeRequestExpired",
params: { payload: { requestId: "permission-7" } },
});
expect((await fixture.iterator.next()).value?.method).toBe("item/agentMessage/delta");
expect(handler).not.toHaveBeenCalled();
} finally { await fixture.close(); }
@@ -10,6 +10,7 @@ import {
readFile,
readdir,
readlink,
realpath,
rename,
rm,
stat,
@@ -19,6 +20,7 @@ import {
import { createHash, randomUUID } from "node:crypto";
import { execFileSync, spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import { bindAcpxAgentFiles } from "../drivers/acpx/agent-files-binding.js";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -26,7 +28,6 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";
import type { ControlPlanePort } from "../contracts/control-plane-port.js";
import { bindAcpxAgentFiles } from "../drivers/acpx/agent-files-binding.js";
import type { NativeExecutionInputV1 } from "../contracts/native-execution.js";
import type {
NativeSession,
@@ -529,6 +530,69 @@ it("carries the provider attachment seed across consecutive authority rotations"
});
});
it("projects a warm Copilot grant onto the remote runner filesystem", () => {
const runtimeContext = assignedRuntimeContext("/controller/skills", "/controller/instructions");
runtimeContext.instructions.workingCopy = { kind: "agent_files", rootPath: "/remote/registered-agent", entryPath: "AGENTS.md" };
const grant = { runtimeContext, instructions: "Read-only instruction sibling root: /controller/instructions" };
const projected = runnerdRecoveryInternals.projectCopilotRunGrant(grant, "/remote/session/filesystem");
expect(projected.runtimeContext?.instructions.bundle.rootPath).toBe("/remote/session/filesystem/context/instructions");
expect(projected.runtimeContext?.skills[0]?.bundle.rootPath).toBe(`/remote/session/filesystem/context/skills/0-${runtimeContext.skills[0]!.bundle.digest.slice(0, 12)}`);
expect(projected.runtimeContext?.instructions.workingCopy).toEqual(runtimeContext.instructions.workingCopy);
expect(projected.runtimeContext?.aggregateDigest).toBe(runtimeContext.aggregateDigest);
expect(projected.instructions).toBe("Read-only instruction sibling root: /remote/session/filesystem/context/instructions");
expect(grant.runtimeContext.instructions.bundle.rootPath).toBe("/controller/instructions");
expect(runnerdRecoveryInternals.projectCopilotRunGrant(grant)).toEqual(grant);
});
it("reopens Copilot with the current registered files after collecting the previous run copy", async () => {
const root = await mkdtemp(join(tmpdir(), "copilot-current-attachment-"));
const previousRoot = join(root, "previous-agent");
const currentRoot = join(root, "current-agent");
try {
await mkdir(previousRoot);
await mkdir(currentRoot);
await writeFile(join(currentRoot, "AGENTS.md"), "Current instructions.\n");
const previousContext = assignedRuntimeContext("/skills", "/bundle");
previousContext.instructions.workingCopy = { kind: "agent_files", rootPath: previousRoot, entryPath: "AGENTS.md" };
const currentContext = structuredClone(previousContext);
currentContext.instructions.workingCopy!.rootPath = currentRoot;
const seed = {
provider: { kind: "acpx", agent: "copilot", runId: "prior-run", normalizedSessionId: "same-session",
instructions: "Previous instructions.", runtimeContext: previousContext, commandDigest: "unchanged", model: "selected-model" },
};
await rm(previousRoot, { recursive: true });
const attached = runnerdRecoveryInternals.rotatedRunAttachPayload(
{ runAttachTemplate: seed },
{ runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "current-run", normalizedSessionId: "same-session", turnId: "turn", itemId: "item" },
null, undefined,
currentContext, { text: "Current instructions.", context: currentContext },
);
const provider = attached.provider as typeof seed.provider;
const files = bindAcpxAgentFiles(provider.runtimeContext, []);
expect(files?.root).toBe(await realpath(currentRoot));
files?.assertHeld();
expect(provider).toMatchObject({ instructions: "Current instructions.", commandDigest: "unchanged", model: "selected-model", normalizedSessionId: "same-session" });
expect(seed.provider.runtimeContext).toEqual(previousContext);
expect(seed.provider.instructions).toBe("Previous instructions.");
} finally { await rm(root, { recursive: true, force: true }); }
});
it.each(["claude", "codex", "cursor", "pi", "copilot"])("keeps %s's pinned grants on same-run recovery", (agent) => {
const identity = { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "same-run", normalizedSessionId: "session", turnId: "turn", itemId: "item" };
const provider = { kind: "acpx", agent, runId: identity.runId, instructions: "Pinned instructions.", runtimeContext: assignedRuntimeContext("/skills", "/pinned") };
const payload = runnerdRecoveryInternals.rotatedRunAttachPayload({ runAttachTemplate: { provider } }, identity, null, undefined,
undefined);
expect(payload.provider).toMatchObject(provider);
});
it.each(["claude", "codex", "cursor", "pi"])("preserves %s's cross-run context when no new grant is supplied", (agent) => {
const provider = { kind: "acpx", agent, runId: "prior-run", instructions: "Pinned instructions.", runtimeContext: assignedRuntimeContext("/skills", "/pinned") };
const payload = runnerdRecoveryInternals.rotatedRunAttachPayload({ runAttachTemplate: { provider } },
{ runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "new-run", normalizedSessionId: "session", turnId: "turn", itemId: "item" }, null, undefined,
undefined);
expect(payload.provider).toMatchObject({ ...provider, runId: "new-run" });
});
it("restores ACPX with the current registered copy after the old run copy is collected", async () => {
const root = await mkdtemp(join(tmpdir(), "runnerd-acpx-context-rotation-"));
const priorRoot = join(root, "prior-run-copy");
@@ -25,7 +25,7 @@ import {
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, resolve } from "node:path";
import { basename, dirname, posix, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type {
@@ -73,7 +73,7 @@ import {
createSanitizedAwsAgentCoreEnvironment,
createSanitizedClaudeManagedEnvironment,
} from "../drivers/claude-managed/environment.js";
import { composeNativeSystemInstructions, type NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
import { composeNativeSystemInstructions, type NativeRunRuntimeGrant, type NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
import type {
NativeAcpxPermissionMode,
NativeOpenCodePermissionMode,
@@ -422,6 +422,22 @@ async function rotateExternalAuthorityEpoch(
return controlPlaneState;
}
function projectCopilotRunGrant(
grant: NativeRunRuntimeGrant,
runnerFilesystemRoot?: string,
): NativeRunRuntimeGrant {
const projected = structuredClone(grant);
const context = projected.runtimeContext;
if (!runnerFilesystemRoot || !context) return projected;
const sourceInstructionRoot = context.instructions.bundle.rootPath;
context.instructions.bundle.rootPath = posix.join(runnerFilesystemRoot, "context", "instructions");
context.skills.forEach((skill, index) => {
skill.bundle.rootPath = posix.join(runnerFilesystemRoot, "context", "skills", `${index}-${skill.bundle.digest.slice(0, 12)}`);
});
projected.instructions = projected.instructions.replaceAll(sourceInstructionRoot, context.instructions.bundle.rootPath);
return projected;
}
function rotatedRunAttachPayload(
state: { commands?: unknown; runAttachTemplate?: unknown },
desired: DurableRecoveryIdentity,
@@ -1472,6 +1488,17 @@ export function expandRunnerdCanonicalNotifications(
});
}
/** Keep durable expiry visible to the facade before its terminal notification. */
export function runnerdRuntimeRequestExpiryNotification(
eventType: string, payload: Record<string, unknown>, threadId: string | null, turnId: string | null,
): CodexRpcNotification | null {
if (eventType !== "runtime_request.expired") return null;
return { method: "paperclip/runtimeRequestExpired", params: {
...(threadId ? { threadId } : {}), ...(turnId ? { turnId } : {}),
payload, itemId: payload.itemId,
} };
}
export function runnerdCanonicalNotificationMethod(
eventType: string,
payload: Record<string, unknown>,
@@ -4021,6 +4048,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
runId: string;
turnId: string;
itemId: string;
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void> {
if (this.#pendingWarmRecoveryCompletion !== null) {
throw new Error("native_runner_warm_transition_completion_pending");
@@ -4030,6 +4058,12 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
throw new Error("native_runner_prp_run_rotation_unavailable");
}
await this.#awaitWarmRunAttachmentReady();
const currentRunGrant = input.currentRunGrant
? projectCopilotRunGrant(input.currentRunGrant, this.options.runnerFilesystemRoot)
: undefined;
if (this.options.provider === "acpx" && this.options.acpxAgent === "copilot" && !currentRunGrant) {
throw new Error("native_runner_current_instruction_grant_unavailable");
}
const prior = core.store.state.identity;
const desired: DurableRecoveryIdentity = {
...prior,
@@ -4059,12 +4093,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
desired,
this.#authorizedTools,
this.options.resumeCompletionContract,
currentRunGrant?.runtimeContext,
currentRunGrant ? { text: currentRunGrant.instructions, context: currentRunGrant.runtimeContext } : undefined,
)
: rotatedRunAttachPayload(
core.store.state,
desired,
this.#authorizedTools,
this.options.resumeCompletionContract,
currentRunGrant?.runtimeContext,
currentRunGrant ? { text: currentRunGrant.instructions, context: currentRunGrant.runtimeContext } : undefined,
);
this.#runAttachTemplate = structuredClone(runAttachTemplate);
const payload = {
@@ -5408,6 +5446,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
});
this.#core = core;
if (rotatedAuthority) {
if (provider === "acpx" && this.options.acpxAgent === "copilot"
&& typeof this.options.baseInstructions !== "string") {
throw new Error("native_runner_current_instruction_grant_unavailable");
}
const runAttachTemplate = rotatedRunAttachPayload(
controlPlaneState,
desiredIdentity,
@@ -6342,6 +6384,13 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
).requestId;
if (typeof requestId === "string")
this.#bridgedRuntimeInputs.delete(requestId);
// Operator resolution/Stop keep their existing acknowledgement path.
const expiry = runnerdRuntimeRequestExpiryNotification(event.eventType, record(eventPayload), this.#threadId, this.#turnId);
if (expiry) this.#queue.push({ ...expiry,
...(this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH ? { paperclipTrace: {
sourceEventId: event.sourceEventId, sourceEventType: event.eventType,
} } : {}),
});
continue;
}
const sessionUpdatePayload = record(eventPayload);
@@ -7045,6 +7094,7 @@ export const runnerdLaunchProfileInternals = Object.freeze({
});
export const runnerdRecoveryInternals = Object.freeze({
projectCopilotRunGrant,
completedMaintenanceTerminalReceipt,
completedMaintenanceTerminalReplayMatches,
readControlPlaneState,
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { nativeSystemInstructions } from "./backends/runtime-context.js";
import type {
CheckpointControlPlaneSessionOptions,
@@ -2004,7 +2005,12 @@ export async function executeNativeSession(
// state). Prove the provider attachment before opening durable
// control-plane state because ControlPlanePort has no rollback operation.
try {
await options.existingSession.attachRun({ identity });
await options.existingSession.attachRun({ identity,
...(input.provider.kind === "acpx" && input.provider.agent === "copilot"
? { currentRunGrant: { runtimeContext: "runtimeContext" in input ? input.runtimeContext : null,
instructions: nativeSystemInstructions(input) } }
: {}),
});
} catch (error) {
// attachRun has no transactional guarantee: a provider may bind the new
// run before reporting a later failure. Conservatively quarantine the
@@ -87,6 +87,7 @@ test("the package exposes only the reviewed runner CLI binaries", () => {
"paperclip-runner-eval-session": "./dist/cli/eval-session.js",
"paperclip-runner-codex-proxy": "./dist/cli/codex-app-server-unix-proxy.js",
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js",
"paperclip-runner-copilot-metadata-probe": "./dist/cli/copilot-metadata-probe.js",
"paperclip-runner-opencode-proxy":
"./dist/cli/opencode-app-server-proxy.js",
});
@@ -0,0 +1,43 @@
{
"commandDigest": "sha256:5e5955c57917a7e7c25703b3ed9e420cd72105eab033b611508dc37fdba3858b",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 14,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "bd5393058a218040d217fa85449d59a6f30507de54cd645bf0ef21422823f85e",
"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": "687270d7bbe3dccce6c2fed8664e76cda724b43e545cb153b9f54b018de52927",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8a43ebafb7a85dd1714ed6945670472666dcb63e8af32b4a74644163fa0e370d",
"toolEvidenceSourceSha256": "4b517eed447615b3cc820af348ccc360978175149caa0bbc838d1f78332abe75",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "b83898b2453d0f425275d3bc58140675fc8f14505703103079094093f16be43f",
"semanticDirectDriverSourceSha256": "2db465e56ba41f06b406e039e566d9e3aea0a014fc6a47a6a54d5bc682dabcab",
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
"semanticSchemaBundleSourceSha256": "2974c75590f816fbdbc7806f531698fdc8d62a99601a17625b81de0f074d6383",
"semanticSidecarProtocolSourceSha256": "72d9800603a652d6580084bb3b5dac4d4dd6d644324b0d15d293d7c198fd6b55",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
}
}
@@ -0,0 +1,43 @@
{
"commandDigest": "sha256:8faf47520f156c62c79ea6ca7d1d90a85ddc9a6621f798bd14e87bc6c643bd04",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 15,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "c0139d0b3af085ec5272a7812c9f89aff0e8618d1d44d9c9e9a59525d1972436",
"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": "687270d7bbe3dccce6c2fed8664e76cda724b43e545cb153b9f54b018de52927",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8a43ebafb7a85dd1714ed6945670472666dcb63e8af32b4a74644163fa0e370d",
"toolEvidenceSourceSha256": "4b517eed447615b3cc820af348ccc360978175149caa0bbc838d1f78332abe75",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "b83898b2453d0f425275d3bc58140675fc8f14505703103079094093f16be43f",
"semanticDirectDriverSourceSha256": "2db465e56ba41f06b406e039e566d9e3aea0a014fc6a47a6a54d5bc682dabcab",
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
"semanticSchemaBundleSourceSha256": "2974c75590f816fbdbc7806f531698fdc8d62a99601a17625b81de0f074d6383",
"semanticSidecarProtocolSourceSha256": "72d9800603a652d6580084bb3b5dac4d4dd6d644324b0d15d293d7c198fd6b55",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
}
}
@@ -0,0 +1,43 @@
{
"commandDigest": "sha256:8591f9a78a16aac4cf558733cd512f09def483fc11c673504bf93be7476d994c",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 16,
"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": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "eec0e3740173ea377e73f54da1260a9063606e0dcbc3f08ddb01942a50085f56",
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
"semanticSchemaBundleSourceSha256": "2974c75590f816fbdbc7806f531698fdc8d62a99601a17625b81de0f074d6383",
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
}
}
@@ -0,0 +1,43 @@
{
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 17,
"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": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
"semanticSchemaBundleSourceSha256": "fe82ac2cbf194d31ac801de6a3cacfb165cb34146c5c26557f283b8c87a5f01e",
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
}
}
@@ -0,0 +1,81 @@
{
"commandDigest": "sha256:482c4e997b4690c7e4a5375dd035560aaad69000f8f5291904d76f8c3a88284a",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 33,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "fedf1e498c1e19daf9c973d475234488aa9acabbf1b9de890245bfac7a534483",
"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": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "eebcc1ec9bc8caa8d1edf0cbda74bd69f5c55cf8d65a0503e95798b9979a19dc",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
"toolEvidenceSourceSha256": "4b517eed447615b3cc820af348ccc360978175149caa0bbc838d1f78332abe75",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "49f7cb882fa09dcf879a2234d3e7b43fe72895d2c0f1e9b72fcbe18f012423bb",
"semanticDirectDriverSourceSha256": "3cea77a03c8822d68f77f090457176a9190facbec7d88ddf8c583c0e42245f70",
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
"semanticValidatorsSourceSha256": "328436d29148e0b79ce36f36c764b3d780913bdb90d174d1ea03cf1f59b5ef13",
"semanticSchemaBundleSourceSha256": "d8cef4a9ebc136a3d37f3d0be7161986e6d65420e2ec734cfd0dfa468c665c97",
"semanticSidecarProtocolSourceSha256": "907b2cdc1a5291c6fb570855bbcc66cb196b5cf64195d5dc201148940678460a",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "a03575b3a737dea60da94cf0fa1feed80719a9aaa7f09456eb57a845d9e8e087",
"configuredEnvironmentRustSourceSha256": "55d44e48e217385b5c64b1b7d8df16dda62319ca3fb524e452fd78c9ed804d3b",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "0565d13d4064ed8c501d6ffda6d7bffca4493552de8aaa7fc12178dabb3ca435",
"profileInstallationSourceSha256": "7c26545922fc85db75a5db6bf3f6e21e381b6e189f4f51243b19ee237b93f8b1",
"attachmentPolicySourceSha256": "ce8027d1f8e2b7fd35df56a75ba162fd9696d1569c016b60bc9f0de1252cd958",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "aac818c1cf50a25899ae95cafc70f093c8cd8af37d19f88cbb7847b4d202d2b6",
"nativeRuntimeContextSourceSha256": "af192af3fa511347045dfdb86d443be9aa046a4c56bdb48198668cf4d30581b8",
"nativeSessionContractSourceSha256": "93251b1eadc686e9d8339d4dfc50b995a3d0a9e6ad8aedc6f2bdf7e30c16138d",
"harnessSessionContractSourceSha256": "75ae4607312ec13466dd2f4ac87f000302b7b5a952cb6d4b51573fa8b7c35d82",
"harnessBackendSourceSha256": "3b11fd93b82ef13d5e6396217e3f102f900437f4d32efb00a9d75a9d2214e6ff",
"harnessSessionSourceSha256": "a9d748b8530544342bbc9f59968816664872782a0865db76ca15f28a1705697b",
"transportContractSourceSha256": "16f358c75dd4ae80af3ca921394c5899b15942b80b553e43ddfabb41c1bf31d3",
"nativeBackendSourceSha256": "a23afeab7e7094cb7e3df05e7a9eac822d0144d8c44893dbcc8a0e7b2bce0c85",
"nativeBackendFactorySourceSha256": "ce22825a53669f8c2db5aaa890bae94af19ede22295bda4ed7987a31d804cd8a",
"nativeInstructionsSourceSha256": "ea85be2aca8dfbd6b4042dcd7b1ed9db9a57baf8c379649a3f5d7d80ca54a6a2",
"runtimeRequestBridgeSourceSha256": "c449568f8abf7e90d5ae7a83b49809d9166461398f8b3046e4fe5f3367f3b052",
"runtimeRequestFacadeSourceSha256": "3d70c6025169779f7886ddf363a1c6fda4add5d14fa3cd49aea46aa1f6a9d8ec",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "8afd25df307772e484e179e58bf54ce1510fe92be694482111d589f9f0ae6e77",
"nativeExecutionSourceSha256": "4cdd702c233bf0d086ff2aeea8f02c6db8817ba0c0bcb5e64e355ee7a1c2dd77",
"profileCompatibilitySourceSha256": "7e6afda2d2e5f1cdd09aaa75e4697905a40be175c6065b630b5dffa39c2c99a6",
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
"assetOwnershipSourceSha256": "43005b7e896e0546c1f44cbe4ff3f34ad6d71f275f4b9a8113f73a71e77930c4",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "d5c200bea2e8ab730eb56e32ce2070c8a1b5407e5907108640f76a463d9b9c9c",
"sidecarErrorSourceSha256": "23f3dbdc1ce488c690d218a854c820f4b4f3edd822bad574d67707314f8c512a",
"profileSourceSha256": "baf0b64371fada6c92331f9f88e915292b2761c029fd86452f6de32e26dc7fd6",
"metadataProbeSourceSha256": "826e74747a89b83a6f546ee1355de64ae5265eb920b50ae498a41dd561a493ee",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"runtimeHostSourceSha256": "83a5ae07c19681ab195f76363661c2381494b243343270ba363d4db2088932a5",
"runtimeAdapterSourceSha256": "a1a8784f5883d2c93df6e3695d5499b83e226efb4f26e558639c4e7dbcc2d67a",
"runtimeSandboxSourceSha256": "4acec4c9757fe5ef5f2788ce20ced8ca4fde8edcfb63b59d0effcfd4316c048d",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "9bbe8329064ee88418528394f8e1cdc716a0b2b35092dc70729b8850e46b7a68"
}
}
@@ -0,0 +1,81 @@
{
"commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 34,
"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": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "7fd06053db98cc763c7205424069a5286c9d4cfefb3d197a4a765c693dccb7e6",
"semanticDirectDriverSourceSha256": "77882e21ad45b66a688fd1933dbd4fc89fad38e8c1f092805cb9485bdd5223c5",
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
"semanticSchemaBundleSourceSha256": "fe82ac2cbf194d31ac801de6a3cacfb165cb34146c5c26557f283b8c87a5f01e",
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "a03575b3a737dea60da94cf0fa1feed80719a9aaa7f09456eb57a845d9e8e087",
"configuredEnvironmentRustSourceSha256": "55d44e48e217385b5c64b1b7d8df16dda62319ca3fb524e452fd78c9ed804d3b",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "aac818c1cf50a25899ae95cafc70f093c8cd8af37d19f88cbb7847b4d202d2b6",
"nativeRuntimeContextSourceSha256": "0b350f5f4044f342b974254738bab45a470c24db9352b04c92d785d26e7853c8",
"nativeSessionContractSourceSha256": "93251b1eadc686e9d8339d4dfc50b995a3d0a9e6ad8aedc6f2bdf7e30c16138d",
"harnessSessionContractSourceSha256": "9e3d62b712cae91168c96d11ff850b97a144809b5cddfe08ec9a91229e9bfc40",
"harnessBackendSourceSha256": "46fd6523f986d5406eca3b420fe920f1d51a6435fb7afc2acf4d21be13af447f",
"harnessSessionSourceSha256": "4dff9bb3370c5560cfc910a7ee814a27725b469c3b1b5dc021e3c0b158ebe746",
"transportContractSourceSha256": "5cd208fe11c3bedd476494b0609fb32efbabe4a1170e2c5854d0aa5e429bd6f4",
"nativeBackendSourceSha256": "294b9d830bc58c05985087875523db133d32cf999703140fd927e843399424db",
"nativeBackendFactorySourceSha256": "ce22825a53669f8c2db5aaa890bae94af19ede22295bda4ed7987a31d804cd8a",
"nativeInstructionsSourceSha256": "ea85be2aca8dfbd6b4042dcd7b1ed9db9a57baf8c379649a3f5d7d80ca54a6a2",
"runtimeRequestBridgeSourceSha256": "a66ee14f858ccf75c69dfbce132900a30724fe1e6baffc26b02d58a6bc5924ac",
"runtimeRequestFacadeSourceSha256": "e1993906ac9f47eb4ea04ae815db869ecde72f582af460cc0080308670612c8f",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "2881b655112f165320a73753385cd54286139a0273a5fa30c1b43fc3b5981a25",
"nativeExecutionSourceSha256": "9c702e518862c396ccfa12d84d11cb8dec1fc064c595359e31cc3e299a7378cc",
"profileCompatibilitySourceSha256": "20f8aaa363496a4cbede0f33aeacf869fb50171eaee2d63a892c65b3bda4c6a1",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
"sidecarErrorSourceSha256": "23f3dbdc1ce488c690d218a854c820f4b4f3edd822bad574d67707314f8c512a",
"profileSourceSha256": "baf0b64371fada6c92331f9f88e915292b2761c029fd86452f6de32e26dc7fd6",
"metadataProbeSourceSha256": "826e74747a89b83a6f546ee1355de64ae5265eb920b50ae498a41dd561a493ee",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"runtimeHostSourceSha256": "cd825fe44b1d142aff2d293c3fc8a64d55e42a98bdea48e4d4b90f3a3163d232",
"runtimeAdapterSourceSha256": "3aea6448dc6474e465e5267b61798ad5449bdaec3f4a41bc4be831426893a9bb",
"runtimeSandboxSourceSha256": "b68686298054da576baa5544fe1364d9febe3f5d6aab5c49f9c69f011764afd3",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
+18
View File
@@ -0,0 +1,18 @@
import { access } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const installer = new URL("../dist/install-copilot-assets.mjs", import.meta.url);
try {
await access(fileURLToPath(installer));
} catch (error) {
if (error.code !== "ENOENT") throw error;
// Dependencies install before compilation in a source workspace. Published
// server packages omit src and cannot take this bootstrap exception.
await access(fileURLToPath(new URL("../src/services/copilot-connection-probe.ts", import.meta.url)));
await access(fileURLToPath(new URL("../../pnpm-workspace.yaml", import.meta.url)));
process.stdout.write("Copilot runtime assets: source-workspace-pending-build\n");
process.exit(0);
}
const { installCopilotAssets } = await import(installer.href);
const result = await installCopilotAssets();
process.stdout.write(`Copilot runtime assets: ${result.status} (${result.target})\n`);