mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
feat(runner): add experimental Cursor Cloud provider (#18068)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Runner gives each provider durable execution, tool, and completion contracts. > - Cursor Cloud currently has a direct adapter outside that native contract. > - Cloud work needs exact ownership across HTTP, SSE, Stop, and recovery. > - This PR adds a qualified Cloud API driver and verified proxy to Runner. > - Product setup and hosted placement remain separate dependent changes. ## Linked Issues or Issue Description Refs #5664, #11803, #17299, and #17471. Product integration is #18069. Managed-only API placement is #18310. The related direct-adapter model/transcript work is not replaced. No duplicate native Cursor Cloud integration was found. ## What Changed - Add native `cursor_cloud` / `cursor_cloud_api` with explicit model/repository validation, bounded HTTP/SSE, durable per-turn checkpoints and a qualified Node proxy launched through the Rust facade. - Commit terminal custody before publication/release; coalesce consumers, preserve ambiguous create/Stop/recovery fences, reject late tools, and distinguish definitely-unsent oversized requests from ambiguous responses. - Support scoped cold attachment, current authority, reconnect and verified artifact staging. Keep unsupported warm/external and Windows paths closed. - Preserve early source cursors and verify immutable legacy prefixes before suffix replay, including supported compaction/masking. Optional proof publication failure falls back to complete cold verification. - Preserve canonical records at the durable native producer with JSON-safe privacy and the existing 8MiB bound; drain admitted successor flushes. Reconcile upstream semantic/pack/schema/lifecycle changes and retain S3 and legacy CLI checks. ## Verification Core head `37c442ec8dd4`: all 66 check contexts are terminal successful/neutral/skipped, including required `ci / verify` and `ci / e2e`; fresh exact-head Greptile is 5/5 with zero unresolved threads. Dependent Product `81e828d20a77` and Top `dd2959689627` include the later client handoff correction and are running their own fresh gates. Core: 47/47 Rust, 4/4 compiled-native retirement, 19/19 settlement and normal Runner build passed. Runtime Top `9af1a5de42a2`: build and recursive typecheck passed. Earlier source-scoped evidence includes 398 focused tests, five relay tests and token/type gates. The 251-assertion, 7/8-suite run failed physical PostgreSQL teardown; the unchanged stable WSP suite passed 128 through the documented wrapper. The downstream fixture corrections passed 7/7 browser and 121/121 startup tests without timeout/oracle relaxation; a broken-unsubscribe control fails. Hosted runtime `9af1a5de42a2` has a passing normal build and recursive typecheck. Latest dependent heads additionally contain a pure client fix for stale live/history overlap, with 48 focused tests, UI typecheck and token gates passing. Its new client is not deployed in the frozen hosted image; server/native/API/provider source is unchanged by these four downstream files. Fresh hosted Linux admission on `9af1a5de42a2` retired its owned runner/proxy/socket with zero prompts. The saved configuration survived reopening, and Test authenticated the bound key/catalog/selected model without a prompt. Artifact with `claude-sonnet-4-5` completed, downloaded the exact 27 bytes, matched 121 canonical identities against complete logs and independently reached FINISHED. The active Stop retry was clicked within its immutable boundary, produced one canonical interrupted turn and acknowledged run-only cancellation, retained a valid 35.076s quiet window (maximum gap 4.584s), and independently remained CANCELLED/artifact-absent beyond the original 180-second command deadline. All provider-observer credential callbacks, DB clients and loaders closed; accounting is settled and explicitly unpriced. The first fresh Stop retains its failed 10.373s sampling gap and every sample. On the upgraded9af server, the historical F226 artifact was reopened and downloaded with its exact original hash after its provider agent had been deleted/GET404 earlier today. Historical F226 full Artifact/Stop cleanup, Question, corrected Plan and reconnect retain their original identities. Deleting the three newly created disposable provider agents and the new-case post-delete grade remain blocked on explicit user approval after automatic review rejected expanding the two original approved IDs. No new-case cleanup or complete fresh matrix pass is claimed; this is separate from the directly observed task/Stop behavior and retained exact cleanup proof. Retained failures: stale fixture 132/133; E8 48/135 with 87 reset-hook failures; Mac 17 SQL failures/skips; earlier startup-title, title-save, custody/Stop and deletion failures with unknown causes and bounded recoveries. `dea023` Artifact passed 17 checks but Stop failed its false warning and 50 duplicate log identities. Pre-repair ACK failure and original Core CI failure remain recorded; retry alone establishes no cause or full local-suite pass. Provider dollar cost stays UNKNOWN. ## Risks Experimental: Cursor independently acknowledges neither the effective model nor instruction digest before the first prompt. The caller’s explicit values are submitted and behavior checked live. Unpriced provider dollar cost stays UNKNOWN. Only qualified per-turn controllers are admitted. Uncertain work and incomplete cleanup stay fenced. Existing damaged/duplicate archives remain failed evidence. Prefix proof adds bounded object-store I/O; the 8MiB producer bound does not guarantee future providers’ output fits. ## Model Used OpenAI Codex, GPT-6 family, assisted with reasoning, repository tools, code execution, browser verification, and delegated code review. The serving model’s exact API ID and context window are not exposed. No value is inferred from the model catalog. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used and the limits of available model metadata - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have linked public issues or described the issue in this PR - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch describes the change and contains no internal ticket ID - [x] I have run the scoped local tests reported above; pending gates are stated separately - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation - [x] I have considered and documented the risks - [x] All required CI gates pass on the current head - [x] Fresh Greptile is 5/5 with no unresolved findings on the current head - [x] I will address all reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2b14ec03a4
commit
3910352c92
222 files changed
+20082
-274
No files matched your search
@@ -82,13 +82,13 @@ jobs:
|
||||
node-version: 24
|
||||
package-manager-cache: false
|
||||
- name: Install isolated qualification tools
|
||||
run: npm install --prefix "$RUNNER_TEMP/ci-tools" --no-audit --no-fund turbo@2.11.10
|
||||
run: npm install --prefix "$RUNNER_TEMP/ci-tools" --no-audit --no-fund turbo@2.11.10 pnpm@9.15.4
|
||||
- name: Prove planning and required checks
|
||||
run: node --test .github/scripts/tests/ci-plan.test.mjs .github/scripts/tests/ci-routing.test.mjs
|
||||
- name: Prove real cache reuse and invalidation
|
||||
env:
|
||||
PAPERCLIP_CI_TEST_TURBO: ${{ runner.temp }}/ci-tools/node_modules/.bin/turbo
|
||||
run: node --test scripts/ci/cache.integration.test.mjs
|
||||
run: PATH="$RUNNER_TEMP/ci-tools/node_modules/.bin:$PATH" node --test scripts/ci/cache.integration.test.mjs
|
||||
- name: Check the real repository task graph
|
||||
env:
|
||||
CI_TURBO_BIN: ${{ runner.temp }}/ci-tools/node_modules/.bin/turbo
|
||||
|
||||
@@ -64,10 +64,13 @@ describe("prepareOpenCodeRuntimeConfig", () => {
|
||||
expect(command.status).toBe(7);
|
||||
if (process.platform === "linux") {
|
||||
const parentScore = await fs.readFile("/proc/self/oom_score_adj", "utf8");
|
||||
expect(execFileSync("bash", ["-c", "cat /proc/self/oom_score_adj"], {
|
||||
env: { ...process.env, ...hookEnv.child }, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
||||
}).trim()).toBe("1000");
|
||||
expect(await fs.readFile("/proc/self/oom_score_adj", "utf8")).toBe(parentScore);
|
||||
for (const shellLevel of ["0", "1"]) {
|
||||
expect(execFileSync("bash", ["-c", "cat /proc/self/oom_score_adj"], {
|
||||
env: { ...process.env, ...hookEnv.child, SHLVL: shellLevel }, encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).trim()).toBe("1000");
|
||||
expect(await fs.readFile("/proc/self/oom_score_adj", "utf8")).toBe(parentScore);
|
||||
}
|
||||
}
|
||||
const config = JSON.parse(await fs.readFile(path.join(relocated, "opencode", "opencode.json"), "utf8"));
|
||||
expect(config).toMatchObject({ plugin: ["operator-plugin"], permission: { bash: "ask" } });
|
||||
|
||||
@@ -214,6 +214,60 @@ are unavailable. Accepting a native plan ends the planning run successfully whil
|
||||
the task waits for the next instruction. See the
|
||||
[Cursor release report](../../doc/plans/2026-10-03-cursor-production-readiness.md).
|
||||
|
||||
Cursor Cloud is a separate experimental native provider (`cursor_cloud`, driver
|
||||
`cursor_cloud_api`). Configure `NativeProviderConfig` with `kind: "cursor_cloud"`,
|
||||
an explicit `model`, and a `cursorCloud` object using the
|
||||
`paperclip.cursor-cloud.config.v1` schema. That object requires either a repository
|
||||
list or a named Cursor Cloud environment. The launch environment supplies
|
||||
`CURSOR_API_KEY` and the scoped tool relay configuration. Native Cloud supports
|
||||
per-turn Runner lifecycle only; Cursor executes tools in its hosted VM. Configure
|
||||
hosted shell secrets in the Cursor Cloud environment.
|
||||
Paperclip native tools use the injected `paperclip` MCP server with
|
||||
controller-managed authentication; they need no Paperclip REST credentials in the
|
||||
Cloud VM.
|
||||
|
||||
Cloud v1 provides no independent acknowledgment of the effective model or exact
|
||||
instruction digest. Runner records submitted inputs and treats this support as
|
||||
experimental. Cursor manages its own shell permissions; native permission,
|
||||
question and plan callbacks and fixed per-run dollar limits are unavailable.
|
||||
Paperclip planning tasks retain their pinned planning context and existing tool
|
||||
restrictions while using Cloud's default harness. Plan documents and
|
||||
revision-bound Paperclip confirmations use the normal semantic tools; this does
|
||||
not enable Cursor's native Plan API or native plan-decision callbacks.
|
||||
Production usage and dollar cost remain unknown; the Cloud usage API is not yet
|
||||
projected through the Runner facade. Stop cancels the exact Cloud run and requires
|
||||
a terminal provider receipt. Unknown cancellation remains unconfirmed. Recovery
|
||||
reuses the saved Agent/Run and stream cursor, keeps the original deadline, and does
|
||||
not replay an ambiguous paid mutation. Tool callbacks require the capability of
|
||||
the current turn, including fresh MCP initialization. Recovery retains that
|
||||
capability in the private checkpoint. Older active checkpoints without it permit
|
||||
cleanup, but cannot resume tool access. Read-only observation retries transient
|
||||
stream failures within the original deadline, with bounded backoff.
|
||||
|
||||
Checkpoint acquisition and dead-owner takeover share a private, bounded local
|
||||
acquisition guard; no network work runs while it is held. Ordinary dead main-owner
|
||||
recovery remains automatic when that guard is absent. An abandoned or
|
||||
ownership-changed acquisition guard blocks reuse and archival as ambiguous state.
|
||||
It is retained for reconciliation, never reclaimed from its age or PID alone.
|
||||
|
||||
Cloud deliverables use `cursor-artifact:<listed relative path>` with the exact
|
||||
byte count and SHA-256. Runner downloads the provider artifact to its private
|
||||
artifact root before normal deliverable registration. Fully verified bytes publish
|
||||
atomically without replacing an existing final file; existing files must match the
|
||||
claim. A crashed writer can leave an unreferenced private temporary file, which is
|
||||
retained rather than mistaken for a completed artifact. A completed publication
|
||||
alias is removed only for a proven inactive publisher and the exact verified
|
||||
private final inode. Active or unknown aliases remain fenced; successful handoff
|
||||
requires a single final link. A remote path never grants access to the controller's
|
||||
project files. Staging is experimental and bounded to 256 MiB and 256 new
|
||||
allocations/import attempts per artifact root, including zero-byte attempts.
|
||||
A durable reservation precedes each new download. Rejected, aborted or uncertain
|
||||
registration retains the charge and verified file for recovery. The bound survives
|
||||
session changes and seeds existing retained usage. Exact existing-file recovery
|
||||
still verifies current provider bytes without another body/temp allocation at
|
||||
capacity. An uncertain ledger, retained pending reservation or ownership lock
|
||||
blocks new imports rather than resetting the budget.
|
||||
|
||||
Google Antigravity is an **unqualified, pending** native ACPX integration. The
|
||||
explicit `paperclipai runtime setup antigravity` command installs the verified
|
||||
Antigravity ACP 1.4.3 distribution and its owned admission patch on macOS ARM64.
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"path": "schemas/provider-descriptor.schema.json",
|
||||
"id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json",
|
||||
"sha256": "81d525335245dd4cb8a9bf41e3390dc60c6dd3e32474aad48b4f18a94d5f9711"
|
||||
"sha256": "811e6a345fd21fa18a135dee082a5249db1f37b31a3ae098e7e25a5f9294495b"
|
||||
},
|
||||
{
|
||||
"path": "schemas/provider-event.schema.json",
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"acpx",
|
||||
"openai_dot",
|
||||
"grok_bot",
|
||||
"muse"
|
||||
"muse",
|
||||
"cursor_cloud"
|
||||
]
|
||||
},
|
||||
"driver": {
|
||||
@@ -32,7 +33,8 @@
|
||||
"acpx_runtime",
|
||||
"openai_dot_mcp",
|
||||
"grok_bot_mcp",
|
||||
"muse_external"
|
||||
"muse_external",
|
||||
"cursor_cloud_api"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
@@ -328,6 +330,19 @@
|
||||
"const": "remote_service"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "cursor_cloud"
|
||||
},
|
||||
"driver": {
|
||||
"const": "cursor_cloud_api"
|
||||
},
|
||||
"executionKind": {
|
||||
"const": "local_process"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -243,6 +243,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
runner_digest: value("--runner-digest")?,
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: value("--max-outbox-bytes")?.parse()?,
|
||||
p0_reserve_bytes: value("--p0-reserve-bytes")?.parse()?,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
|
||||
@@ -2713,6 +2713,7 @@ mod tests {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: launch_profile,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
|
||||
@@ -1219,6 +1219,55 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}))?;
|
||||
}
|
||||
"initialized" => {}
|
||||
"paperclip/ackTerminal" => {
|
||||
let path = argument(&args, "--cursor-cloud-terminal-file")
|
||||
.ok_or("terminal fixture missing")?;
|
||||
let delivery: Value = serde_json::from_slice(&fs::read(&path)?)?;
|
||||
if message.pointer("/params/turnId") != delivery.get("turnId")
|
||||
|| message.pointer("/params/receiptId") != delivery.get("receiptId")
|
||||
{
|
||||
return Err("terminal receipt changed".into());
|
||||
}
|
||||
if Path::new(&format!("{path}.hold-ack")).exists() {
|
||||
send(
|
||||
json!({"id": id, "error": {"code": -32000, "message": "controlled lost receipt"}}),
|
||||
)?;
|
||||
} else {
|
||||
fs::remove_file(path)?;
|
||||
send(
|
||||
json!({"id": id, "result": {"acknowledged": true, "turnId": delivery["turnId"], "receiptId": delivery["receiptId"]}}),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"paperclip/shutdown" if args.iter().any(|arg| arg == "--cursor-cloud-shutdown") => {
|
||||
let unconfirmed =
|
||||
argument(&args, "--cursor-cloud-shutdown-control").is_some_and(|path| {
|
||||
fs::read_to_string(path).is_ok_and(|value| value.trim() == "unconfirmed")
|
||||
});
|
||||
let params = &message["params"];
|
||||
let has_remote_run = state.next_turn > 0;
|
||||
if !unconfirmed {
|
||||
state.active_turn_id = None;
|
||||
save_state(&state_path, &state)?;
|
||||
}
|
||||
let mut receipt = json!({
|
||||
"threadId": params["threadId"], "turnId": params["turnId"],
|
||||
"remoteStopped": !unconfirmed,
|
||||
"remoteAgentId": if has_remote_run { json!("cursor-agent-fixture") } else { Value::Null },
|
||||
"remoteRunId": if has_remote_run { json!("cursor-run-fixture") } else { Value::Null },
|
||||
"status": if unconfirmed { "RUNNING" } else { "CANCELLED" },
|
||||
});
|
||||
if let Some(path) = argument(&args, "--cursor-cloud-terminal-file") {
|
||||
if !unconfirmed && Path::new(&path).exists() {
|
||||
let delivery: Value = serde_json::from_slice(&fs::read(path)?)?;
|
||||
receipt["remoteRunId"] = delivery["providerRunId"].clone();
|
||||
receipt["status"] = delivery["status"].clone();
|
||||
receipt["cursorCloudTerminalDelivery"] = delivery;
|
||||
}
|
||||
}
|
||||
send(json!({"id":id,"result":receipt}))?;
|
||||
}
|
||||
|
||||
"thread/start" => {
|
||||
if let Some(delay) = argument(&args, "--thread-start-delay-ms") {
|
||||
thread::sleep(Duration::from_millis(delay.parse()?));
|
||||
@@ -1372,6 +1421,26 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
send(json!({"id": id, "result": {"data": turns, "nextCursor": null}}))?;
|
||||
}
|
||||
"thread/read" => {
|
||||
if let Some(path) = argument(&args, "--cursor-cloud-terminal-file") {
|
||||
if Path::new(&path).exists() {
|
||||
let first_read = format!("{path}.first-read");
|
||||
let finishes_between_reads = args
|
||||
.iter()
|
||||
.any(|arg| arg == "--cursor-cloud-finish-between-reads")
|
||||
&& !Path::new(&first_read).exists();
|
||||
if finishes_between_reads {
|
||||
fs::write(first_read, b"observed active")?;
|
||||
} else {
|
||||
let delivery: Value = serde_json::from_slice(&fs::read(&path)?)?;
|
||||
state.active_turn_id = None;
|
||||
save_state(&state_path, &state)?;
|
||||
send(
|
||||
json!({"id": id, "result": {"thread": {"id": state.thread_id, "turns": []}, "cursorCloudTerminalDelivery": delivery}}),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if descendant_notifications
|
||||
&& message.pointer("/params/threadId").and_then(Value::as_str)
|
||||
== Some("descendant-1")
|
||||
@@ -1896,6 +1965,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
finish_turn(&state_path, &mut state, "completed")?;
|
||||
} else if emit_split_event_burst {
|
||||
send_split_event_burst(&state, split_event_prefix_count)?;
|
||||
} else if hold_turn && args.iter().any(|arg| arg == "--held-final-message") {
|
||||
send(json!({"method": "item/completed", "params": {
|
||||
"threadId": state.thread_id, "turnId": provider_turn_id,
|
||||
"item": {"id": "final", "type": "agentMessage", "text": "Durable Cloud final"}
|
||||
}}))?;
|
||||
} else if hold_turn && args.iter().any(|arg| arg == "--held-output") {
|
||||
if let Some(release_file) = argument(&args, "--held-output-release-file") {
|
||||
let deadline = Instant::now() + Duration::from_secs(60);
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
|
||||
use paperclip_runner_core::acpx_candidate_admission::AcpxCandidateAdmission;
|
||||
use paperclip_runner_core::durable::{
|
||||
capture_bootstrap_ticket, redact_diagnostic_text, run_durable_runner, AcpxLaunchProfile,
|
||||
DurableRunnerConfig, OpenCodeLaunchProfile, QualifiedLaunchArtifact,
|
||||
CursorCloudLaunchProfile, DurableRunnerConfig, OpenCodeLaunchProfile, QualifiedLaunchArtifact,
|
||||
};
|
||||
use paperclip_runner_core::local_runner::{run_local_runner, LocalRunnerError, RunnerConfig};
|
||||
use paperclip_runner_core::native_provider_backend::NativeProviderCommandExecutor;
|
||||
@@ -344,6 +344,33 @@ fn opencode_launch_profile(
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_cloud_launch_profile(
|
||||
args: &[String],
|
||||
) -> Result<Option<CursorCloudLaunchProfile>, LocalRunnerError> {
|
||||
let command = optional_value(args, "--cursor-cloud-proxy-command")?;
|
||||
let command_sha256 = optional_value(args, "--cursor-cloud-proxy-command-sha256")?;
|
||||
let proxy_script = optional_value(args, "--cursor-cloud-proxy-script")?;
|
||||
let proxy_script_sha256 = optional_value(args, "--cursor-cloud-proxy-script-sha256")?;
|
||||
match (command, command_sha256, proxy_script, proxy_script_sha256) {
|
||||
(None, None, None, None) => Ok(None),
|
||||
(Some(command), Some(command_sha256), Some(proxy_script), Some(proxy_script_sha256)) => {
|
||||
Ok(Some(CursorCloudLaunchProfile {
|
||||
command: QualifiedLaunchArtifact {
|
||||
path: PathBuf::from(command),
|
||||
sha256: command_sha256,
|
||||
},
|
||||
proxy_script: QualifiedLaunchArtifact {
|
||||
path: PathBuf::from(proxy_script),
|
||||
sha256: proxy_script_sha256,
|
||||
},
|
||||
}))
|
||||
}
|
||||
_ => Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud launch profile requires command, proxy, and both SHA-256 digests",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn usize_value(args: &[String], name: &str, default: usize) -> Result<usize, LocalRunnerError> {
|
||||
optional_u64(args, name)?.map_or(Ok(default), |value| {
|
||||
usize::try_from(value)
|
||||
@@ -425,6 +452,7 @@ fn run_durable(args: &[String]) -> Result<(), LocalRunnerError> {
|
||||
runner_digest: value(args, "--runner-digest")?,
|
||||
acpx_launch_profile: acpx_launch_profile(args)?,
|
||||
opencode_launch_profile: opencode_launch_profile(args)?,
|
||||
cursor_cloud_launch_profile: cursor_cloud_launch_profile(args)?,
|
||||
max_outbox_bytes: usize_value(args, "--max-outbox-bytes", 16 * 1024 * 1024)?,
|
||||
p0_reserve_bytes: usize_value(args, "--p0-reserve-bytes", 1024 * 1024)?,
|
||||
max_frame_bytes: usize_value(args, "--max-frame-bytes", 1024 * 1024)?,
|
||||
@@ -546,6 +574,39 @@ fn run_main(args: Vec<String>) -> ExitCode {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cursor_cloud_launch_flags_are_optional_as_a_whole_and_required_as_an_exact_pin_set() {
|
||||
assert!(cursor_cloud_launch_profile(&[]).unwrap().is_none());
|
||||
let complete = vec![
|
||||
"--cursor-cloud-proxy-command",
|
||||
"/runtime/node",
|
||||
"--cursor-cloud-proxy-command-sha256",
|
||||
"sha256:node",
|
||||
"--cursor-cloud-proxy-script",
|
||||
"/runtime/cursor-cloud-proxy.cjs",
|
||||
"--cursor-cloud-proxy-script-sha256",
|
||||
"sha256:proxy",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
let profile = cursor_cloud_launch_profile(&complete).unwrap().unwrap();
|
||||
assert_eq!(profile.command.path, PathBuf::from("/runtime/node"));
|
||||
assert_eq!(
|
||||
profile.proxy_script.path,
|
||||
PathBuf::from("/runtime/cursor-cloud-proxy.cjs")
|
||||
);
|
||||
for missing in (0..complete.len()).step_by(2) {
|
||||
let partial = complete
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| *index < missing || *index > missing + 1)
|
||||
.map(|(_, value)| value.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(cursor_cloud_launch_profile(&partial).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_port_is_configurable_and_defaults_to_43127() {
|
||||
let base: Vec<String> = [
|
||||
|
||||
@@ -12,7 +12,7 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::durable::QualifiedLaunchArtifact;
|
||||
use crate::durable::{redact_text, OpenCodeLaunchProfile};
|
||||
use crate::durable::{redact_text, CursorCloudLaunchProfile, OpenCodeLaunchProfile};
|
||||
use crate::local_runner::LocalRunnerError;
|
||||
use crate::process_supervisor::{
|
||||
is_node_interpreter, BoundedLogBuffer, ProcessExitFact, ProcessOutput, SupervisedProcess,
|
||||
@@ -71,6 +71,7 @@ pub(crate) fn runtime_notification_codex_args(
|
||||
|
||||
pub const CODEX_APP_SERVER_MAX_FRAME_BYTES: usize = 4 * 1024 * 1024;
|
||||
const QUALIFIED_OPENCODE_VERSION: &str = "1.18.34";
|
||||
const CURSOR_CLOUD_PROXY_VERSION: &str = "cursor-cloud-v1";
|
||||
const DEFAULT_PROVIDER_TRACE_MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
const MAX_BUFFERED_MESSAGES: usize = 1_024;
|
||||
const MAX_BUFFERED_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
|
||||
@@ -103,6 +104,19 @@ const OPENCODE_PROVIDER_ENVIRONMENT_KEYS: &[&str] = &[
|
||||
"PAPERCLIP_NORMALIZED_SESSION_ID",
|
||||
"PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH",
|
||||
];
|
||||
const CURSOR_CLOUD_PROVIDER_ENVIRONMENT_KEYS: &[&str] = &[
|
||||
"CURSOR_API_KEY",
|
||||
"PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH",
|
||||
"PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_URL",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT",
|
||||
"PAPERCLIP_RUNNER_INSTANCE_ID",
|
||||
"PAPERCLIP_RUN_ID",
|
||||
"PAPERCLIP_NORMALIZED_SESSION_ID",
|
||||
"PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH",
|
||||
];
|
||||
const TRUSTED_OPENCODE_EXECUTABLE_ARG: &str = "--paperclip-trusted-opencode-executable";
|
||||
const MAX_PROVIDER_STDERR_LINES: usize = 32;
|
||||
const MAX_PROVIDER_STDERR_BYTES: usize = 8 * 1024;
|
||||
@@ -490,10 +504,12 @@ impl CodexProviderConfig {
|
||||
}
|
||||
if !matches!(
|
||||
(self.provider.as_str(), self.driver.as_str()),
|
||||
("codex", "codex_app_server") | ("opencode", "opencode_server")
|
||||
("codex", "codex_app_server")
|
||||
| ("opencode", "opencode_server")
|
||||
| ("cursor_cloud", "cursor_cloud_api")
|
||||
) {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"local runner provider must be codex through codex_app_server or opencode through opencode_server",
|
||||
"runner facade provider/driver pair must be codex/codex_app_server, opencode/opencode_server, or cursor_cloud/cursor_cloud_api",
|
||||
));
|
||||
}
|
||||
if self.provider_version.trim().is_empty() || self.provider_version.len() > 120 {
|
||||
@@ -506,6 +522,18 @@ impl CodexProviderConfig {
|
||||
"OpenCode providerVersion must equal the qualified {QUALIFIED_OPENCODE_VERSION} release",
|
||||
)));
|
||||
}
|
||||
if self.provider == "cursor_cloud"
|
||||
&& (self.provider_version != CURSOR_CLOUD_PROXY_VERSION
|
||||
|| self.model.as_ref().is_none_or(|model| {
|
||||
model.trim().is_empty() || model.chars().any(char::is_control)
|
||||
})
|
||||
|| self.approval_policy != "never"
|
||||
|| self.read_only)
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud requires cursor-cloud-v1, an exact model, provider-managed permissions, and no local read-only claim",
|
||||
));
|
||||
}
|
||||
if self.externally_sandboxed && self.provider != "codex" {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"external sandbox delegation is only supported by the Codex provider",
|
||||
@@ -760,6 +788,8 @@ pub struct CodexProvider {
|
||||
trace: Option<ProviderTraceSink>,
|
||||
last_trace_frame_id: Option<u64>,
|
||||
opencode_launch_profile: Option<OpenCodeLaunchProfile>,
|
||||
cursor_cloud_launch_profile: Option<CursorCloudLaunchProfile>,
|
||||
provider_termination_receipt: Option<Value>,
|
||||
completion_contract: Option<ProviderCompletionContract>,
|
||||
permission_profile: &'static str,
|
||||
exit_drain: Option<ProviderExitDrain>,
|
||||
@@ -895,7 +925,9 @@ const CODEX_PROVIDER_ENVIRONMENT_KEYS: &[&str] = &[
|
||||
];
|
||||
|
||||
fn codex_permission_profile(provider: &str, external_sandbox: bool) -> &'static str {
|
||||
if provider == "codex" && external_sandbox {
|
||||
if provider == "cursor_cloud" {
|
||||
"paperclip-runner-provider-managed"
|
||||
} else if provider == "codex" && external_sandbox {
|
||||
"paperclip-runner-external-sandbox"
|
||||
} else {
|
||||
"paperclip-runner-workspace-only"
|
||||
@@ -918,6 +950,7 @@ impl CodexProvider {
|
||||
1,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -933,6 +966,7 @@ impl CodexProvider {
|
||||
1,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -942,6 +976,7 @@ impl CodexProvider {
|
||||
resume_thread_id: Option<&str>,
|
||||
process_generation: u64,
|
||||
opencode_launch_profile: Option<&OpenCodeLaunchProfile>,
|
||||
cursor_cloud_launch_profile: Option<&CursorCloudLaunchProfile>,
|
||||
completion_contract: Option<(&str, &[String])>,
|
||||
) -> Result<Self, LocalRunnerError> {
|
||||
Self::start_with_tools_observed(
|
||||
@@ -950,6 +985,7 @@ impl CodexProvider {
|
||||
resume_thread_id,
|
||||
process_generation,
|
||||
opencode_launch_profile,
|
||||
cursor_cloud_launch_profile,
|
||||
completion_contract,
|
||||
&mut |_| Ok(()),
|
||||
)
|
||||
@@ -961,6 +997,7 @@ impl CodexProvider {
|
||||
resume_thread_id: Option<&str>,
|
||||
process_generation: u64,
|
||||
opencode_launch_profile: Option<&OpenCodeLaunchProfile>,
|
||||
cursor_cloud_launch_profile: Option<&CursorCloudLaunchProfile>,
|
||||
completion_contract: Option<(&str, &[String])>,
|
||||
observe: &mut dyn FnMut(ProviderStartupObservation) -> Result<(), LocalRunnerError>,
|
||||
) -> Result<Self, LocalRunnerError> {
|
||||
@@ -1007,16 +1044,21 @@ impl CodexProvider {
|
||||
"all_proxy",
|
||||
"RUST_BACKTRACE",
|
||||
];
|
||||
let provider_environment_keys = if config.provider == "opencode" {
|
||||
OPENCODE_PROVIDER_ENVIRONMENT_KEYS
|
||||
let provider_environment_keys = match config.provider.as_str() {
|
||||
"opencode" => OPENCODE_PROVIDER_ENVIRONMENT_KEYS,
|
||||
"cursor_cloud" => CURSOR_CLOUD_PROVIDER_ENVIRONMENT_KEYS,
|
||||
_ => CODEX_PROVIDER_ENVIRONMENT_KEYS,
|
||||
};
|
||||
let github_environment_keys = if config.provider == "cursor_cloud" {
|
||||
&[][..]
|
||||
} else {
|
||||
CODEX_PROVIDER_ENVIRONMENT_KEYS
|
||||
GITHUB_CREDENTIAL_ENVIRONMENT_KEYS
|
||||
};
|
||||
let environment_keys = common_environment_keys
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(provider_environment_keys.iter().copied())
|
||||
.chain(GITHUB_CREDENTIAL_ENVIRONMENT_KEYS.iter().copied())
|
||||
.chain(github_environment_keys.iter().copied())
|
||||
.collect::<Vec<_>>();
|
||||
let runtime_request_scope = new_runtime_request_scope()?;
|
||||
let stdio_mcp = if config.provider == "codex" && !config.read_only {
|
||||
@@ -1051,7 +1093,28 @@ impl CodexProvider {
|
||||
}
|
||||
}
|
||||
let spawn = (|| {
|
||||
if config.provider == "opencode" {
|
||||
if config.provider == "cursor_cloud" {
|
||||
let profile = cursor_cloud_launch_profile.ok_or_else(|| {
|
||||
LocalRunnerError::invalid(
|
||||
"Cursor Cloud runner startup omitted its qualified launch profile",
|
||||
)
|
||||
})?;
|
||||
let proxy_script = profile.proxy_script.path.to_string_lossy();
|
||||
if config.command != profile.command.path
|
||||
|| config.args.as_slice() != [proxy_script.as_ref()]
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud launch does not match the runner-owned qualified profile",
|
||||
));
|
||||
}
|
||||
let launch = verified_cursor_cloud_launch(profile)?;
|
||||
SupervisedProcess::spawn_verified_with_environment_keys(
|
||||
&launch,
|
||||
Duration::from_secs(2),
|
||||
CODEX_APP_SERVER_MAX_FRAME_BYTES,
|
||||
&environment_keys,
|
||||
)
|
||||
} else if config.provider == "opencode" {
|
||||
let profile = opencode_launch_profile.ok_or_else(|| {
|
||||
LocalRunnerError::invalid(
|
||||
"OpenCode runner startup omitted its qualified launch profile",
|
||||
@@ -1145,6 +1208,8 @@ impl CodexProvider {
|
||||
trace: ProviderTraceSink::from_environment(),
|
||||
last_trace_frame_id: None,
|
||||
opencode_launch_profile: opencode_launch_profile.cloned(),
|
||||
cursor_cloud_launch_profile: cursor_cloud_launch_profile.cloned(),
|
||||
provider_termination_receipt: None,
|
||||
completion_contract: completion_contract.map(|(revision, criterion_ids)| {
|
||||
ProviderCompletionContract {
|
||||
revision: revision.to_owned(),
|
||||
@@ -1198,7 +1263,7 @@ impl CodexProvider {
|
||||
} else {
|
||||
params_object.insert("permissions".to_owned(), json!(provider.permission_profile));
|
||||
}
|
||||
if config.provider == "opencode" {
|
||||
if matches!(config.provider.as_str(), "opencode" | "cursor_cloud") {
|
||||
if let Some(mode) = &config.conversation_mode {
|
||||
params_object.insert("conversationMode".to_owned(), json!(mode));
|
||||
}
|
||||
@@ -1344,6 +1409,9 @@ impl CodexProvider {
|
||||
completion_contract: Option<(&str, &[String])>,
|
||||
accounting: &mut Vec<(String, Value)>,
|
||||
) -> Result<bool, LocalRunnerError> {
|
||||
if self.config.provider == "cursor_cloud" {
|
||||
return Ok(false);
|
||||
}
|
||||
let authorized_tools = authorized_tools.into_iter().collect::<Vec<_>>();
|
||||
let completion_contract =
|
||||
completion_contract.map(|(revision, criterion_ids)| ProviderCompletionContract {
|
||||
@@ -1675,6 +1743,7 @@ impl CodexProvider {
|
||||
Some(&thread_id),
|
||||
next_generation,
|
||||
self.opencode_launch_profile.as_ref(),
|
||||
self.cursor_cloud_launch_profile.as_ref(),
|
||||
completion_contract.as_ref().map(|contract| {
|
||||
(
|
||||
contract.revision.as_str(),
|
||||
@@ -2048,6 +2117,7 @@ impl CodexProvider {
|
||||
// Retain the prior settled identity while the next turn runs. Besides
|
||||
// recognizing delayed prior-turn requests, this fails closed if a
|
||||
// provider ambiguously reuses the same turn id for fresh work.
|
||||
self.provider_termination_receipt = None;
|
||||
self.active_provider_turn_id = Some(provider_turn_id);
|
||||
self.runtime_request_scope = runtime_request_scope;
|
||||
}
|
||||
@@ -2102,6 +2172,59 @@ impl CodexProvider {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_cloud_terminal_delivery(
|
||||
&mut self,
|
||||
) -> Result<Option<crate::cursor_cloud_proxy::TerminalDelivery>, LocalRunnerError> {
|
||||
if self.config.provider != "cursor_cloud" {
|
||||
return Ok(None);
|
||||
}
|
||||
let snapshot = self.read_thread()?;
|
||||
if snapshot.pointer("/thread/id").and_then(Value::as_str) != Some(self.thread_id.as_str()) {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud terminal snapshot changed its thread",
|
||||
));
|
||||
}
|
||||
let Some(value) = snapshot
|
||||
.get("cursorCloudTerminalDelivery")
|
||||
.filter(|value| !value.is_null())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if latest_active_turn_id(&snapshot).is_some() {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud terminal snapshot also reports active work",
|
||||
));
|
||||
}
|
||||
let delivery = crate::cursor_cloud_proxy::TerminalDelivery::parse(value, &self.thread_id)?;
|
||||
self.active_provider_turn_id = None;
|
||||
Ok(Some(delivery))
|
||||
}
|
||||
|
||||
pub(crate) fn acknowledge_cursor_cloud_terminal(
|
||||
&mut self,
|
||||
delivery: &crate::cursor_cloud_proxy::TerminalDelivery,
|
||||
) -> Result<(), LocalRunnerError> {
|
||||
if self.config.provider != "cursor_cloud" || delivery.thread_id != self.thread_id {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud terminal receipt changed its provider",
|
||||
));
|
||||
}
|
||||
let response = self.request(
|
||||
"paperclip/ackTerminal",
|
||||
json!({"turnId":delivery.turn_id,"receiptId":delivery.receipt_id}),
|
||||
)?;
|
||||
if response.get("acknowledged") != Some(&Value::Bool(true))
|
||||
|| response.get("turnId").and_then(Value::as_str) != Some(delivery.turn_id.as_str())
|
||||
|| response.get("receiptId").and_then(Value::as_str)
|
||||
!= Some(delivery.receipt_id.as_str())
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud terminal receipt was not acknowledged",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_thread(&mut self) -> Result<Value, LocalRunnerError> {
|
||||
// Probing provider state does not supersede an authoritative terminal.
|
||||
// A later replacement turn must still establish its own identity.
|
||||
@@ -3120,16 +3243,78 @@ impl CodexProvider {
|
||||
}
|
||||
|
||||
pub fn shutdown(&mut self) -> Result<(), LocalRunnerError> {
|
||||
self.shutdown_with_cursor_cloud_delivery(|_| Ok(false))
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown_with_cursor_cloud_delivery(
|
||||
&mut self,
|
||||
mut retain: impl FnMut(
|
||||
&crate::cursor_cloud_proxy::TerminalDelivery,
|
||||
) -> Result<bool, LocalRunnerError>,
|
||||
) -> Result<(), LocalRunnerError> {
|
||||
self.expected_shutdown = true;
|
||||
// A failed courtesy response must never prevent process fencing.
|
||||
let cancellation = self.cancel_pending_requests();
|
||||
let result = self.process.terminate_group().map(|_| ()).and(cancellation);
|
||||
let remote_termination = if self.config.provider == "cursor_cloud" {
|
||||
let expected_turn = self.active_provider_turn_id.clone().or_else(|| {
|
||||
self.completed_turn_authority
|
||||
.as_ref()
|
||||
.map(|authority| authority.provider_turn_id.clone())
|
||||
});
|
||||
self.request_classified_until(
|
||||
"paperclip/shutdown",
|
||||
json!({ "threadId": self.thread_id, "turnId": expected_turn }),
|
||||
Some(std::time::Instant::now() + crate::cursor_cloud_proxy::SHUTDOWN_TIMEOUT),
|
||||
)
|
||||
.map_err(ProviderRequestError::into_inner)
|
||||
.and_then(|receipt| {
|
||||
let normalized = crate::cursor_cloud_proxy::shutdown_receipt(
|
||||
&receipt,
|
||||
&self.thread_id,
|
||||
expected_turn.as_deref(),
|
||||
)?;
|
||||
if let Some(value) = receipt
|
||||
.get("cursorCloudTerminalDelivery")
|
||||
.filter(|value| !value.is_null())
|
||||
{
|
||||
let delivery =
|
||||
crate::cursor_cloud_proxy::TerminalDelivery::parse(value, &self.thread_id)?;
|
||||
if Some(delivery.turn_id.as_str()) != expected_turn.as_deref()
|
||||
|| receipt.get("remoteRunId").and_then(Value::as_str)
|
||||
!= Some(delivery.provider_run_id.as_str())
|
||||
|| receipt.get("status").and_then(Value::as_str)
|
||||
!= Some(delivery.status.as_str())
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Cursor Cloud shutdown changed its terminal delivery",
|
||||
));
|
||||
}
|
||||
if retain(&delivery)? {
|
||||
let _ = self.acknowledge_cursor_cloud_terminal(&delivery);
|
||||
}
|
||||
}
|
||||
self.provider_termination_receipt = Some(normalized);
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
let result = self
|
||||
.process
|
||||
.terminate_group()
|
||||
.map(|_| ())
|
||||
.and(remote_termination)
|
||||
.and(cancellation);
|
||||
if let Some(trace) = self.trace.as_mut() {
|
||||
trace.finish();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn provider_termination_receipt(&self) -> Option<&Value> {
|
||||
self.provider_termination_receipt.as_ref()
|
||||
}
|
||||
|
||||
fn cancel_pending_requests(&mut self) -> Result<(), LocalRunnerError> {
|
||||
let pending_runtime = std::mem::take(&mut self.pending_runtime_requests);
|
||||
let pending = std::mem::take(&mut self.pending_tool_requests);
|
||||
@@ -3203,6 +3388,15 @@ impl CodexProvider {
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Result<Value, ProviderRequestError> {
|
||||
self.request_classified_until(method, params, None)
|
||||
}
|
||||
|
||||
fn request_classified_until(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
deadline: Option<std::time::Instant>,
|
||||
) -> Result<Value, ProviderRequestError> {
|
||||
let request_id = self.next_request_id;
|
||||
self.next_request_id = self.next_request_id.checked_add(1).ok_or_else(|| {
|
||||
@@ -3211,8 +3405,16 @@ impl CodexProvider {
|
||||
self.send_frame(&json!({"id": request_id, "method": method, "params": params}))
|
||||
.map_err(ProviderRequestError::Ambiguous)?;
|
||||
loop {
|
||||
let remaining = deadline.map_or(Duration::from_secs(30), |deadline| {
|
||||
deadline.saturating_duration_since(std::time::Instant::now())
|
||||
});
|
||||
if remaining.is_zero() {
|
||||
return Err(ProviderRequestError::Ambiguous(LocalRunnerError::invalid(
|
||||
format!("Provider {method} exceeded its bounded response deadline"),
|
||||
)));
|
||||
}
|
||||
let line = self
|
||||
.receive_provider_stdout_line(Duration::from_secs(30))
|
||||
.receive_provider_stdout_line(remaining)
|
||||
.map_err(ProviderRequestError::Ambiguous)?;
|
||||
let Some(line) = line else {
|
||||
let exit = self
|
||||
@@ -3397,6 +3599,21 @@ fn verified_opencode_launch(
|
||||
Ok(VerifiedProcessLaunch::new(command, args))
|
||||
}
|
||||
|
||||
fn verified_cursor_cloud_launch(
|
||||
profile: &CursorCloudLaunchProfile,
|
||||
) -> Result<VerifiedProcessLaunch, LocalRunnerError> {
|
||||
let command = verify_launch_artifact(&profile.command, "Cursor Cloud proxy command")
|
||||
.map_err(|error| LocalRunnerError::invalid(error.to_string()))?;
|
||||
let proxy = verify_launch_artifact(&profile.proxy_script, "Cursor Cloud proxy script")
|
||||
.map_err(|error| LocalRunnerError::invalid(error.to_string()))?;
|
||||
let proxy = if is_node_interpreter(&profile.command.path) {
|
||||
VerifiedProcessArgument::CommonJsArtifact(proxy)
|
||||
} else {
|
||||
VerifiedProcessArgument::Artifact(proxy)
|
||||
};
|
||||
Ok(VerifiedProcessLaunch::new(command, vec![proxy]))
|
||||
}
|
||||
|
||||
fn json_size(value: &Value, label: &str) -> Result<usize, LocalRunnerError> {
|
||||
serde_json::to_vec(value)
|
||||
.map(|bytes| bytes.len())
|
||||
@@ -4852,6 +5069,7 @@ done
|
||||
1,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&mut |observation| match observation {
|
||||
ProviderStartupObservation::Spawned {
|
||||
process_id,
|
||||
@@ -4926,6 +5144,115 @@ done
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_cloud_proxy_launch_verifies_node_and_script_without_a_native_executable() {
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"paperclip-cursor-cloud-launch-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
fs::create_dir_all(&directory).unwrap();
|
||||
let command = directory.join("node");
|
||||
let proxy = directory.join("cursor-cloud-proxy.cjs");
|
||||
fs::write(&command, b"qualified node").unwrap();
|
||||
fs::write(&proxy, b"module.exports = {};\n").unwrap();
|
||||
let profile = CursorCloudLaunchProfile {
|
||||
command: qualified_artifact(&command),
|
||||
proxy_script: qualified_artifact(&proxy),
|
||||
};
|
||||
let launch = verified_cursor_cloud_launch(&profile);
|
||||
if cfg!(target_os = "windows") {
|
||||
let error = match launch {
|
||||
Ok(_) => panic!("Cursor Cloud Windows launch must remain unsupported"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("verified process snapshots are supported only on Linux and macOS"));
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
return;
|
||||
}
|
||||
let launch = launch.unwrap();
|
||||
assert_eq!(launch.arguments().len(), 1);
|
||||
assert!(matches!(
|
||||
launch.arguments().first(),
|
||||
Some(VerifiedProcessArgument::CommonJsArtifact(_))
|
||||
));
|
||||
fs::write(&proxy, b"unqualified replacement").unwrap();
|
||||
assert!(verified_cursor_cloud_launch(&profile).is_err());
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_cloud_admission_requires_its_exact_proxy_model_and_provider_permission_boundary() {
|
||||
let mut config: CodexProviderConfig = serde_json::from_value(json!({
|
||||
"provider": "cursor_cloud", "driver": "cursor_cloud_api",
|
||||
"providerVersion": CURSOR_CLOUD_PROXY_VERSION, "command": "node",
|
||||
"cwd": std::env::current_dir().unwrap(), "model": "caller-selected-model",
|
||||
"conversationMode": "prepared", "approvalPolicy": "never"
|
||||
}))
|
||||
.unwrap();
|
||||
config.validate().unwrap();
|
||||
assert_eq!(config.skill_instructions_config(), None);
|
||||
assert_eq!(
|
||||
codex_permission_profile("cursor_cloud", false),
|
||||
"paperclip-runner-provider-managed"
|
||||
);
|
||||
for (field, value) in [
|
||||
("driver", json!("codex_app_server")),
|
||||
("providerVersion", json!("unqualified-version")),
|
||||
("model", Value::Null),
|
||||
("model", json!(" ")),
|
||||
("approvalPolicy", json!("on-request")),
|
||||
("readOnly", json!(true)),
|
||||
("externallySandboxed", json!(true)),
|
||||
] {
|
||||
let mut invalid = serde_json::to_value(&config).unwrap();
|
||||
invalid[field] = value;
|
||||
assert!(
|
||||
serde_json::from_value::<CodexProviderConfig>(invalid)
|
||||
.unwrap()
|
||||
.validate()
|
||||
.is_err(),
|
||||
"{field}"
|
||||
);
|
||||
}
|
||||
config.model = Some("different-explicit-model".to_owned());
|
||||
config.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_cloud_environment_contains_only_its_selected_provider_and_private_relay_bindings() {
|
||||
for name in [
|
||||
"CURSOR_API_KEY",
|
||||
"PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_URL",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT",
|
||||
] {
|
||||
assert!(
|
||||
CURSOR_CLOUD_PROVIDER_ENVIRONMENT_KEYS.contains(&name),
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
for name in [
|
||||
"CURSOR_AUTH_TOKEN",
|
||||
"OPENAI_API_KEY",
|
||||
"CODEX_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"GITHUB_TOKEN",
|
||||
"PAPERCLIP_API_KEY",
|
||||
"PAPERCLIP_NATIVE_MCP_TOKEN",
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
] {
|
||||
assert!(
|
||||
!CURSOR_CLOUD_PROVIDER_ENVIRONMENT_KEYS.contains(&name),
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_prepared_context_in_the_durable_provider_config() {
|
||||
let config: CodexProviderConfig = serde_json::from_value(json!({
|
||||
|
||||
@@ -144,6 +144,7 @@ fn eligible_key(key: &str) -> bool {
|
||||
"GIT_CONFIG_",
|
||||
"PAPERCLIP_RUNNER_",
|
||||
"PAPERCLIP_NATIVE_",
|
||||
"PAPERCLIP_CURSOR_CLOUD_",
|
||||
"PAPERCLIP_GITHUB_",
|
||||
"PAPERCLIP_ACPX_",
|
||||
"PAPERCLIP_VERIFIED_",
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::local_runner::LocalRunnerError;
|
||||
|
||||
pub(crate) const SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
fn bounded_id(value: &Value) -> bool {
|
||||
value
|
||||
.as_str()
|
||||
.is_some_and(|id| !id.is_empty() && id.len() <= 240 && !id.chars().any(char::is_control))
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown_receipt(
|
||||
value: &Value,
|
||||
thread_id: &str,
|
||||
expected_turn_id: Option<&str>,
|
||||
) -> Result<Value, LocalRunnerError> {
|
||||
let invalid = || LocalRunnerError::invalid("Cursor Cloud remote termination is unconfirmed");
|
||||
if value.get("remoteStopped") != Some(&Value::Bool(true))
|
||||
|| value.get("threadId").and_then(Value::as_str) != Some(thread_id)
|
||||
|| value.get("turnId").and_then(Value::as_str) != expected_turn_id
|
||||
|| (value
|
||||
.get("turnId")
|
||||
.is_some_and(|turn| !turn.is_null() && !bounded_id(turn)))
|
||||
{
|
||||
return Err(invalid());
|
||||
}
|
||||
let remote_run = value.get("remoteRunId").ok_or_else(invalid)?;
|
||||
let remote_agent = value.get("remoteAgentId").unwrap_or(&Value::Null);
|
||||
if remote_run.is_null() {
|
||||
if !remote_agent.is_null() && !bounded_id(remote_agent) {
|
||||
return Err(invalid());
|
||||
}
|
||||
return Ok(json!({
|
||||
"schema": "paperclip.cursor-cloud.stop-receipt.v1",
|
||||
"remoteStopped": true, "threadId": thread_id, "turnId": expected_turn_id,
|
||||
"remoteAgentId": remote_agent, "remoteRunId": Value::Null,
|
||||
}));
|
||||
}
|
||||
if !bounded_id(remote_run)
|
||||
|| !bounded_id(remote_agent)
|
||||
|| !matches!(
|
||||
value.get("status").and_then(Value::as_str),
|
||||
Some("CANCELLED" | "FINISHED" | "ERROR" | "EXPIRED")
|
||||
)
|
||||
{
|
||||
return Err(invalid());
|
||||
}
|
||||
Ok(json!({
|
||||
"schema": "paperclip.cursor-cloud.stop-receipt.v1",
|
||||
"remoteStopped": true, "threadId": thread_id, "turnId": expected_turn_id,
|
||||
"remoteAgentId": remote_agent, "remoteRunId": remote_run, "status": value["status"],
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct TerminalDelivery {
|
||||
pub schema: String,
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub provider_run_id: String,
|
||||
pub receipt_id: String,
|
||||
pub status: String,
|
||||
pub interrupted: bool,
|
||||
pub result: Option<String>,
|
||||
}
|
||||
|
||||
impl TerminalDelivery {
|
||||
pub(crate) fn parse(value: &Value, thread_id: &str) -> Result<Self, LocalRunnerError> {
|
||||
let invalid = || LocalRunnerError::invalid("Cursor Cloud terminal delivery is invalid");
|
||||
let delivery: Self = serde_json::from_value(value.clone()).map_err(|_| invalid())?;
|
||||
if delivery.schema != "paperclip.cursor-cloud.terminal-delivery.v1"
|
||||
|| delivery.thread_id != thread_id
|
||||
|| ![
|
||||
&delivery.thread_id,
|
||||
&delivery.turn_id,
|
||||
&delivery.provider_run_id,
|
||||
&delivery.receipt_id,
|
||||
]
|
||||
.into_iter()
|
||||
.all(|id| !id.is_empty() && id.len() <= 240 && !id.chars().any(char::is_control))
|
||||
|| !matches!(
|
||||
delivery.status.as_str(),
|
||||
"FINISHED" | "ERROR" | "CANCELLED" | "EXPIRED"
|
||||
)
|
||||
|| delivery
|
||||
.result
|
||||
.as_ref()
|
||||
.is_some_and(|text| text.len() > 262_144 || text.chars().count() > 65_536)
|
||||
{
|
||||
return Err(invalid());
|
||||
}
|
||||
Ok(delivery)
|
||||
}
|
||||
|
||||
pub(crate) fn method(&self) -> &'static str {
|
||||
if self.interrupted || self.status == "CANCELLED" {
|
||||
"turn/interrupted"
|
||||
} else if self.status == "FINISHED" {
|
||||
"turn/completed"
|
||||
} else {
|
||||
"turn/failed"
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn receipt(&self) -> Value {
|
||||
let bytes = serde_json::to_vec(self).expect("terminal delivery is serializable");
|
||||
json!({ "turnId": self.turn_id, "receiptId": self.receipt_id,
|
||||
"batchDigest": format!("sha256:{:x}", Sha256::digest(bytes)) })
|
||||
}
|
||||
|
||||
pub(crate) fn final_params(&self) -> Value {
|
||||
json!({ "threadId": self.thread_id, "turnId": self.turn_id,
|
||||
"item": {"id":"final", "type":"agentMessage", "text":self.result} })
|
||||
}
|
||||
|
||||
pub(crate) fn terminal_params(&self) -> Value {
|
||||
let status = self.method().strip_prefix("turn/").unwrap();
|
||||
let mut value = json!({ "threadId":self.thread_id, "turnId":self.turn_id,
|
||||
"turn":{"id":self.turn_id, "status":status}, "cursorCloudTerminalDelivery":self });
|
||||
if matches!(self.status.as_str(), "ERROR" | "EXPIRED") {
|
||||
value["turn"]["error"] = json!({"code":format!("cursor_cloud_{}",self.status.to_lowercase()),
|
||||
"message":"Cursor Cloud run ended without successful provider completion."});
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn remote_stop_requires_exact_local_identity_and_an_authoritative_remote_terminal() {
|
||||
let receipt = json!({ "remoteStopped": true, "threadId": "thread-1", "turnId": "turn-1",
|
||||
"remoteAgentId": "agent-remote", "remoteRunId": "run-remote", "status": "CANCELLED" });
|
||||
assert_eq!(
|
||||
shutdown_receipt(&receipt, "thread-1", Some("turn-1")).unwrap()["remoteRunId"],
|
||||
"run-remote"
|
||||
);
|
||||
for (field, value) in [
|
||||
("threadId", json!("stale-thread")),
|
||||
("turnId", json!("stale-turn")),
|
||||
("remoteStopped", json!(false)),
|
||||
("remoteStopped", json!("true")),
|
||||
("remoteRunId", json!("")),
|
||||
("remoteAgentId", Value::Null),
|
||||
("status", json!("RUNNING")),
|
||||
("status", json!("CANCEL_REQUESTED")),
|
||||
] {
|
||||
let mut invalid = receipt.clone();
|
||||
invalid[field] = value;
|
||||
assert!(
|
||||
shutdown_receipt(&invalid, "thread-1", Some("turn-1")).is_err(),
|
||||
"{field}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_remote_start_requires_an_explicit_null_identity_and_exact_local_scope() {
|
||||
let receipt = json!({ "remoteStopped": true, "threadId": "thread-1", "turnId": "turn-1", "remoteRunId": null });
|
||||
assert!(shutdown_receipt(&receipt, "thread-1", Some("turn-1")).is_ok());
|
||||
let mut missing = receipt;
|
||||
missing.as_object_mut().unwrap().remove("remoteRunId");
|
||||
assert!(shutdown_receipt(&missing, "thread-1", Some("turn-1")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,12 @@ pub struct OpenCodeLaunchProfile {
|
||||
pub executable: QualifiedLaunchArtifact,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CursorCloudLaunchProfile {
|
||||
pub command: QualifiedLaunchArtifact,
|
||||
pub proxy_script: QualifiedLaunchArtifact,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DurableRunnerConfig {
|
||||
pub connect_url: String,
|
||||
@@ -210,6 +216,7 @@ pub struct DurableRunnerConfig {
|
||||
pub runner_digest: String,
|
||||
pub acpx_launch_profile: Option<AcpxLaunchProfile>,
|
||||
pub opencode_launch_profile: Option<OpenCodeLaunchProfile>,
|
||||
pub cursor_cloud_launch_profile: Option<CursorCloudLaunchProfile>,
|
||||
pub max_outbox_bytes: usize,
|
||||
pub p0_reserve_bytes: usize,
|
||||
pub max_frame_bytes: usize,
|
||||
@@ -348,6 +355,25 @@ impl DurableRunnerConfig {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(profile) = self.cursor_cloud_launch_profile.as_ref() {
|
||||
if [&profile.command, &profile.proxy_script]
|
||||
.iter()
|
||||
.any(|artifact| {
|
||||
!artifact.path.is_absolute()
|
||||
|| artifact.path.to_str().is_none()
|
||||
|| artifact.sha256.len() != 71
|
||||
|| !artifact.sha256.starts_with("sha256:")
|
||||
|| !artifact.sha256[7..]
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
|
||||
})
|
||||
|| profile.command.path == profile.proxy_script.path
|
||||
{
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"Cursor Cloud runner launch profile is malformed",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -371,6 +397,7 @@ mod tests {
|
||||
runner_digest: "sha256:digest".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 64 * 1024,
|
||||
|
||||
@@ -1394,6 +1394,7 @@ fn wait_for_old_authority_outbox_ack(
|
||||
)?;
|
||||
let target = state.highest_source_seq();
|
||||
let deadline = Instant::now() + TERMINAL_RESULT_ACK_TIMEOUT;
|
||||
let mut ack_persistence = CumulativeAckPersistence::default();
|
||||
while state.acked_source_seq < target {
|
||||
if Instant::now() >= deadline {
|
||||
return Err(DurableRunnerError::invalid(
|
||||
@@ -1410,8 +1411,10 @@ fn wait_for_old_authority_outbox_ack(
|
||||
.pointer("/payload/ackedSourceSeq")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| DurableRunnerError::invalid("ACK cursor is required"))?;
|
||||
state.apply_ack(acked, connection.protocol_version)?;
|
||||
store.save(state)?;
|
||||
// As in normal ingress, checkpoint cumulative ACK bursts
|
||||
// without fsyncing every prefix inside the delivery deadline.
|
||||
// The final empty-outbox ACK is always persisted.
|
||||
ack_persistence.apply(state, store, acked, connection.protocol_version)?;
|
||||
}
|
||||
Some("ping") => transport.send_json(&control_envelope(
|
||||
state,
|
||||
@@ -2546,6 +2549,7 @@ mod tests {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 64 * 1024,
|
||||
p0_reserve_bytes: 4096,
|
||||
max_frame_bytes: 64 * 1024,
|
||||
|
||||
@@ -2700,6 +2700,7 @@ mod tests {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 16_384,
|
||||
p0_reserve_bytes: 4096,
|
||||
max_frame_bytes: 65_536,
|
||||
|
||||
@@ -1773,6 +1773,7 @@ mod tests {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 64 * 1024,
|
||||
p0_reserve_bytes: 4096,
|
||||
max_frame_bytes: 64 * 1024,
|
||||
|
||||
@@ -1077,6 +1077,7 @@ mod tests {
|
||||
runner_digest: "sha256:test".into(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 65536,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
|
||||
@@ -16,6 +16,7 @@ mod codex_startup_trust;
|
||||
mod codex_usage;
|
||||
mod configured_environment;
|
||||
mod credential_observer;
|
||||
mod cursor_cloud_proxy;
|
||||
pub mod dot_provider_backend;
|
||||
pub mod durable;
|
||||
pub mod external_provider_backend;
|
||||
|
||||
@@ -2610,6 +2610,7 @@ mod tests {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
|
||||
@@ -257,7 +257,7 @@ impl NativeProviderCommandExecutor {
|
||||
&self.config,
|
||||
ExternalProviderCodec::Grok,
|
||||
)),
|
||||
"codex" | "opencode" => SelectedExecutor::LocalFacade(
|
||||
"codex" | "opencode" | "cursor_cloud" => SelectedExecutor::LocalFacade(
|
||||
CodexCommandExecutor::with_runner_config(&self.state_dir, &self.config),
|
||||
),
|
||||
"acpx" => SelectedExecutor::Acpx(AcpxCommandExecutor::with_runner_config(
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -405,6 +405,7 @@ mod tests {
|
||||
runner_digest: "sha256:digest".into(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 64 * 1024,
|
||||
|
||||
@@ -72,6 +72,7 @@ fn checkpoints_the_sidecar_and_rebinds_consecutive_warm_runs_before_accepting_wo
|
||||
}],
|
||||
}),
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
|
||||
@@ -178,6 +178,7 @@ fn durable_config(directory: &Path) -> DurableRunnerConfig {
|
||||
runner_digest: format!("sha256:{}", "a".repeat(64)),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 16 * 1024 * 1024,
|
||||
p0_reserve_bytes: 1024 * 1024,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
|
||||
@@ -25,6 +25,7 @@ fn config(state_dir: PathBuf) -> DurableRunnerConfig {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 16_384,
|
||||
p0_reserve_bytes: 4096,
|
||||
max_frame_bytes: 65_536,
|
||||
|
||||
+523
-2
@@ -6,8 +6,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use paperclip_runner_core::durable::{
|
||||
AcpxLaunchProfile, Command, CommandExecutor, DurableRunnerConfig, OpenCodeLaunchProfile,
|
||||
QualifiedLaunchArtifact,
|
||||
AcpxLaunchProfile, Command, CommandExecutor, CursorCloudLaunchProfile, DurableRunnerConfig,
|
||||
OpenCodeLaunchProfile, QualifiedLaunchArtifact,
|
||||
};
|
||||
use paperclip_runner_core::native_provider_backend::NativeProviderCommandExecutor;
|
||||
use paperclip_runner_core::provider_bridge::{authorized_tool_catalog_digest, AuthorizedTool};
|
||||
@@ -129,6 +129,7 @@ fn config(state_dir: &Path) -> DurableRunnerConfig {
|
||||
runner_digest: "sha256:test".to_owned(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 1024 * 1024,
|
||||
@@ -321,6 +322,54 @@ fn opencode_config(state_dir: &Path) -> DurableRunnerConfig {
|
||||
config
|
||||
}
|
||||
|
||||
fn cursor_cloud_config(state_dir: &Path) -> DurableRunnerConfig {
|
||||
cursor_cloud_config_with_args(state_dir, "")
|
||||
}
|
||||
|
||||
fn cursor_cloud_config_with_args(state_dir: &Path, extra_args: &str) -> DurableRunnerConfig {
|
||||
let command = state_dir.join("qualified-cursor-cloud-proxy-command");
|
||||
let proxy_script = state_dir.join("qualified-cursor-cloud-proxy-script");
|
||||
fs::write(
|
||||
&command,
|
||||
"#!/bin/sh\nproxy=\"$1\"\nshift\nexec /bin/sh \"$proxy\" \"$@\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
&proxy_script,
|
||||
format!(
|
||||
"#!/bin/sh\nexec '{}' --state-file '{}' --call-log '{}' --require-completion-contract --durable-turn-ids --cursor-cloud-shutdown {}\n",
|
||||
env!("CARGO_BIN_EXE_fake-codex-app-server"),
|
||||
state_dir.join("fake-cursor-cloud-state.json").display(),
|
||||
state_dir.join("fake-cursor-cloud-calls.log").display(),
|
||||
extra_args,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
#[cfg(unix)]
|
||||
for path in [&command, &proxy_script] {
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o500)).unwrap();
|
||||
}
|
||||
let mut config = config(state_dir);
|
||||
config.cursor_cloud_launch_profile = Some(CursorCloudLaunchProfile {
|
||||
command: qualified_artifact(command),
|
||||
proxy_script: qualified_artifact(proxy_script),
|
||||
});
|
||||
config
|
||||
}
|
||||
|
||||
fn cursor_cloud_prepare_payload(directory: &Path) -> Value {
|
||||
let mut payload = opencode_prepare_payload(directory);
|
||||
payload["provider"] = json!({
|
||||
"kind": "cursor_cloud", "provider": "cursor_cloud", "driver": "cursor_cloud_api",
|
||||
"providerVersion": "cursor-cloud-v1",
|
||||
"command": directory.join("qualified-cursor-cloud-proxy-command"),
|
||||
"args": [directory.join("qualified-cursor-cloud-proxy-script")],
|
||||
"cwd": directory, "model": "caller-selected-model", "approvalPolicy": "never",
|
||||
"instructions": "Complete the supplied task.", "conversationMode": "prepared",
|
||||
});
|
||||
payload
|
||||
}
|
||||
|
||||
fn opencode_call_count(state_dir: &Path, method: &str) -> usize {
|
||||
fs::read_to_string(state_dir.join("fake-opencode-calls.log"))
|
||||
.unwrap_or_default()
|
||||
@@ -1619,6 +1668,478 @@ fn executes_opencode_through_the_local_facade_without_codex_event_labels() {
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_facade_keeps_provider_labels_and_requires_semantic_completion() {
|
||||
let directory = temporary_directory("cursor-cloud");
|
||||
let config = cursor_cloud_config(&directory);
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
let prepared = executor
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(prepared.result["provider"], "cursor_cloud");
|
||||
let opened = executor
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap();
|
||||
assert_eq!(opened.result["provider"], "cursor_cloud");
|
||||
assert_eq!(opened.result["driver"], "cursor_cloud_api");
|
||||
executor
|
||||
.execute(&command(
|
||||
3,
|
||||
"turn.start",
|
||||
json!({"text": "Finish the task."}),
|
||||
))
|
||||
.unwrap();
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
let mut observed = Vec::new();
|
||||
while std::time::Instant::now() < deadline {
|
||||
let events = executor.poll_events().unwrap();
|
||||
let count = events.len();
|
||||
observed.extend(events);
|
||||
executor.acknowledge_events(count).unwrap();
|
||||
if observed
|
||||
.iter()
|
||||
.any(|event| event.event_type == "run.terminal")
|
||||
{
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert!(observed
|
||||
.iter()
|
||||
.any(|event| event.event_type == "turn.completed"));
|
||||
let terminal = observed
|
||||
.iter()
|
||||
.find(|event| event.event_type == "run.terminal")
|
||||
.unwrap();
|
||||
assert_eq!(terminal.payload["provider"], "cursor_cloud");
|
||||
assert_eq!(terminal.payload["turnTerminalState"], "completed");
|
||||
assert_eq!(terminal.payload["runTerminalState"], "failed");
|
||||
let result = observed
|
||||
.iter()
|
||||
.find(|event| event.event_type == "run.result.proposed")
|
||||
.unwrap();
|
||||
assert_eq!(result.payload["reportedWorkDisposition"], "needs_review");
|
||||
assert_eq!(
|
||||
result.payload["completionClaim"]["objectiveSatisfied"],
|
||||
false
|
||||
);
|
||||
assert!(result.payload["evidence"].as_array().unwrap().is_empty());
|
||||
assert!(observed
|
||||
.iter()
|
||||
.any(|event| event.event_type == "item.completed"
|
||||
&& event.payload["provider"] == "cursor_cloud"));
|
||||
executor
|
||||
.execute(&command(4, "session.close", json!({})))
|
||||
.unwrap();
|
||||
executor.shutdown().unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_replays_a_durable_terminal_batch_after_proxy_loss_and_a_lost_ack() {
|
||||
for (partial, finishes_between_reads) in [(false, false), (true, false), (false, true)] {
|
||||
let directory = temporary_directory("cursor-cloud-terminal-replay");
|
||||
let delivery_path = directory.join("terminal.json");
|
||||
let hold_ack = directory.join("terminal.json.hold-ack");
|
||||
let config = cursor_cloud_config_with_args(
|
||||
&directory,
|
||||
&format!(
|
||||
"--hold-turn --cursor-cloud-terminal-file '{}' {} {}",
|
||||
delivery_path.display(),
|
||||
if partial { "--held-final-message" } else { "" },
|
||||
if finishes_between_reads {
|
||||
"--cursor-cloud-finish-between-reads"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
),
|
||||
);
|
||||
let mut first = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
first
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
first
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap();
|
||||
first
|
||||
.execute(&command(
|
||||
3,
|
||||
"turn.start",
|
||||
json!({"text":"Retain terminal delivery"}),
|
||||
))
|
||||
.unwrap();
|
||||
let mut observed = Vec::new();
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let events = first.poll_events().unwrap();
|
||||
let count = events.len();
|
||||
observed.extend(events);
|
||||
first.acknowledge_events(count).unwrap();
|
||||
if observed.iter().any(|event| {
|
||||
event.event_type
|
||||
== if partial {
|
||||
"item.completed"
|
||||
} else {
|
||||
"turn.started"
|
||||
}
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
assert!(std::time::Instant::now() < deadline);
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
drop(first);
|
||||
let state_path = directory.join("codex-provider-state.json");
|
||||
let saved: Value = serde_json::from_slice(&fs::read(&state_path).unwrap()).unwrap();
|
||||
fs::write(&delivery_path, serde_json::to_vec(&json!({
|
||||
"schema":"paperclip.cursor-cloud.terminal-delivery.v1", "threadId":saved["threadId"],
|
||||
"turnId":saved["activeProviderTurnId"], "providerRunId":"run-terminal-fixture",
|
||||
"receiptId":"00000000-0000-4000-8000-000000000001", "status":"FINISHED", "interrupted":false,
|
||||
"result":"Durable Cloud final",
|
||||
})).unwrap()).unwrap();
|
||||
fs::write(&hold_ack, b"hold").unwrap();
|
||||
let mut resumed = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
let recovered = resumed.poll_events().unwrap();
|
||||
assert!(
|
||||
recovered
|
||||
.iter()
|
||||
.any(|event| event.event_type == "turn.completed"),
|
||||
"{recovered:?}"
|
||||
);
|
||||
assert!(!recovered
|
||||
.iter()
|
||||
.any(|event| event.event_type == "turn.failed"));
|
||||
observed.extend(recovered.clone());
|
||||
assert_eq!(
|
||||
observed
|
||||
.iter()
|
||||
.filter(|event| event.event_type == "item.completed"
|
||||
&& event.payload.to_string().contains("Durable Cloud final"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(recovered
|
||||
.iter()
|
||||
.any(|event| event.event_type == "run.terminal"
|
||||
&& event.payload["runTerminalState"] == "failed"));
|
||||
resumed.acknowledge_events(recovered.len()).unwrap();
|
||||
assert!(delivery_path.exists());
|
||||
drop(resumed);
|
||||
fs::remove_file(&hold_ack).unwrap();
|
||||
let mut again = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
let replay = again.poll_events().unwrap();
|
||||
assert!(!replay.iter().any(|event| matches!(
|
||||
event.event_type.as_str(),
|
||||
"item.completed" | "turn.completed" | "turn.failed" | "run.terminal"
|
||||
)));
|
||||
assert!(!delivery_path.exists());
|
||||
again.acknowledge_events(replay.len()).unwrap();
|
||||
again.shutdown().unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_stop_saves_and_acknowledges_terminal_delivery_before_proxy_exit() {
|
||||
for hold_ack in [false, true] {
|
||||
let directory = temporary_directory("cursor-cloud-stop-terminal");
|
||||
let delivery_path = directory.join("terminal.json");
|
||||
let config = cursor_cloud_config_with_args(
|
||||
&directory,
|
||||
&format!(
|
||||
"--hold-turn --cursor-cloud-terminal-file '{}'",
|
||||
delivery_path.display()
|
||||
),
|
||||
);
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(&command(
|
||||
3,
|
||||
"turn.start",
|
||||
json!({"text":"Stop and retain the receipt"}),
|
||||
))
|
||||
.unwrap();
|
||||
let state_path = directory.join("codex-provider-state.json");
|
||||
let saved: Value = serde_json::from_slice(&fs::read(&state_path).unwrap()).unwrap();
|
||||
fs::write(&delivery_path, serde_json::to_vec(&json!({
|
||||
"schema":"paperclip.cursor-cloud.terminal-delivery.v1", "threadId":saved["threadId"],
|
||||
"turnId":saved["activeProviderTurnId"], "providerRunId":"run-stopped-fixture",
|
||||
"receiptId":"00000000-0000-4000-8000-000000000002", "status":"CANCELLED", "interrupted":true,
|
||||
"result":null,
|
||||
})).unwrap()).unwrap();
|
||||
if hold_ack {
|
||||
fs::write(directory.join("terminal.json.hold-ack"), b"hold").unwrap();
|
||||
}
|
||||
executor
|
||||
.execute(&command(4, "turn.stop", json!({})))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
delivery_path.exists(),
|
||||
hold_ack,
|
||||
"the Node receipt must be acknowledged after its durable save"
|
||||
);
|
||||
let retained: Value = serde_json::from_slice(&fs::read(&state_path).unwrap()).unwrap();
|
||||
assert_eq!(retained["lifecycle"], "prepared");
|
||||
assert_eq!(
|
||||
retained["cursorCloudTerminalReceipt"]["turnId"],
|
||||
saved["activeProviderTurnId"]
|
||||
);
|
||||
assert!(retained["pendingEvents"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|event| event["eventType"] == "turn.interrupted"));
|
||||
let pending = executor.poll_events().unwrap();
|
||||
executor.acknowledge_events(pending.len()).unwrap();
|
||||
if hold_ack {
|
||||
fs::remove_file(directory.join("terminal.json.hold-ack")).unwrap();
|
||||
executor
|
||||
.execute(&command(
|
||||
5,
|
||||
"run.attach",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(&command(6, "session.open", json!({})))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!delivery_path.exists(),
|
||||
"fresh attachment must retry the saved receipt acknowledgment"
|
||||
);
|
||||
}
|
||||
executor.shutdown().unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_run_attachment_reopens_the_same_thread_in_a_fresh_sidecar() {
|
||||
let directory = temporary_directory("cursor-cloud-cold-attachment");
|
||||
let config = cursor_cloud_config(&directory);
|
||||
let payload = cursor_cloud_prepare_payload(&directory);
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor
|
||||
.execute(&command(1, "run.prepare", payload.clone()))
|
||||
.unwrap();
|
||||
let opened = executor
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(&command(3, "run.attach", payload))
|
||||
.unwrap();
|
||||
let resumed = executor
|
||||
.execute(&command(4, "session.open", json!({})))
|
||||
.unwrap();
|
||||
assert_eq!(resumed.result["status"], "resumed");
|
||||
assert_eq!(
|
||||
resumed.result["providerSessionId"],
|
||||
opened.result["providerSessionId"]
|
||||
);
|
||||
assert_ne!(resumed.result["processId"], opened.result["processId"]);
|
||||
let calls = fs::read_to_string(directory.join("fake-cursor-cloud-calls.log")).unwrap();
|
||||
assert_eq!(
|
||||
calls.lines().filter(|line| *line == "thread/start").count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
calls
|
||||
.lines()
|
||||
.filter(|line| *line == "thread/resume")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
calls
|
||||
.lines()
|
||||
.filter(|line| *line == "paperclip/shutdown")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
executor.shutdown().unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_stop_preserves_active_ownership_until_remote_termination_is_confirmed() {
|
||||
let directory = temporary_directory("cursor-cloud-stop-confirmation");
|
||||
let control = directory.join("shutdown-control");
|
||||
fs::write(&control, "unconfirmed").unwrap();
|
||||
let config = cursor_cloud_config_with_args(
|
||||
&directory,
|
||||
&format!(
|
||||
"--hold-turn --cursor-cloud-shutdown-control '{}'",
|
||||
control.display()
|
||||
),
|
||||
);
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
executor
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap();
|
||||
let started = executor
|
||||
.execute(&command(3, "turn.start", json!({"text": "Keep working."})))
|
||||
.unwrap();
|
||||
let turn_id = started.result["providerTurnId"].clone();
|
||||
let error = executor
|
||||
.execute(&command(4, "turn.stop", json!({})))
|
||||
.unwrap_err();
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("remote termination is unconfirmed"));
|
||||
let state_path = directory.join("codex-provider-state.json");
|
||||
let retained: Value = serde_json::from_slice(&fs::read(&state_path).unwrap()).unwrap();
|
||||
assert_eq!(retained["activeProviderTurnId"], turn_id);
|
||||
assert_eq!(retained["lifecycle"], "turn_active");
|
||||
assert!(retained["providerTerminationReceipt"].is_null());
|
||||
fs::write(&control, "confirmed").unwrap();
|
||||
let stopped = executor
|
||||
.execute(&command(5, "turn.stop", json!({})))
|
||||
.unwrap();
|
||||
assert_eq!(stopped.result["providerExitConfirmed"], true);
|
||||
assert_eq!(
|
||||
stopped.result["providerTerminationReceipt"]["remoteStopped"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
stopped.result["providerTerminationReceipt"]["remoteRunId"],
|
||||
"cursor-run-fixture"
|
||||
);
|
||||
let state: Value = serde_json::from_slice(&fs::read(&state_path).unwrap()).unwrap();
|
||||
assert!(state["activeProviderTurnId"].is_null());
|
||||
assert_eq!(
|
||||
state["providerTerminationReceipt"]["remoteRunId"],
|
||||
"cursor-run-fixture"
|
||||
);
|
||||
let calls = fs::read_to_string(directory.join("fake-cursor-cloud-calls.log")).unwrap();
|
||||
assert_eq!(
|
||||
calls.lines().filter(|line| *line == "turn/start").count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
calls
|
||||
.lines()
|
||||
.filter(|line| *line == "paperclip/shutdown")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
executor.shutdown().unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_launch_rejects_unpinned_commands_and_script_replacement() {
|
||||
let directory = temporary_directory("cursor-cloud-launch-fence");
|
||||
let config = cursor_cloud_config(&directory);
|
||||
let mut invalid = cursor_cloud_prepare_payload(&directory);
|
||||
invalid["provider"]["command"] = json!(env!("CARGO_BIN_EXE_fake-codex-app-server"));
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
assert!(executor
|
||||
.execute(&command(1, "run.prepare", invalid))
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("runner-owned qualified profile"));
|
||||
drop(executor);
|
||||
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
let script = &config
|
||||
.cursor_cloud_launch_profile
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.proxy_script
|
||||
.path;
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(script, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::write(script, "#!/bin/sh\nexit 7\n").unwrap();
|
||||
let error = executor
|
||||
.execute(&command(2, "session.open", json!({})))
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("verified process artifact digest mismatch"),
|
||||
"{error}"
|
||||
);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn cursor_cloud_durable_recovery_rejects_a_changed_proxy_profile_without_rewriting_state() {
|
||||
let directory = temporary_directory("cursor-cloud-profile-recovery");
|
||||
let config = cursor_cloud_config(&directory);
|
||||
let mut first = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
|
||||
first
|
||||
.execute(&command(
|
||||
1,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap();
|
||||
first.shutdown().unwrap();
|
||||
drop(first);
|
||||
let state_path = directory.join("codex-provider-state.json");
|
||||
let saved = fs::read(&state_path).unwrap();
|
||||
let mut changed = config;
|
||||
changed
|
||||
.cursor_cloud_launch_profile
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.proxy_script
|
||||
.sha256 = format!("sha256:{}", "a".repeat(64));
|
||||
let mut recovered = NativeProviderCommandExecutor::with_runner_config(&directory, &changed);
|
||||
for sequence in 2..=3 {
|
||||
let error = recovered
|
||||
.execute(&command(
|
||||
sequence,
|
||||
"run.prepare",
|
||||
cursor_cloud_prepare_payload(&directory),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("Cursor Cloud runner launch profile changed across durable recovery"));
|
||||
}
|
||||
assert_eq!(fs::read(&state_path).unwrap(), saved);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn replacement_shutdown_restores_the_persisted_provider_before_cleanup() {
|
||||
|
||||
@@ -49,6 +49,7 @@ fn real_cli_uses_relative_state_from_another_directory_and_never_process_run_ide
|
||||
runner_digest: "sha256:digest".into(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
cursor_cloud_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 65536,
|
||||
max_frame_bytes: 65536,
|
||||
|
||||
@@ -8,6 +8,12 @@ import { build } from "esbuild";
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
export const verifiedProviderEntrypoints = Object.freeze([
|
||||
Object.freeze({
|
||||
name: "cursor-cloud-app-server-proxy",
|
||||
source: resolve(packageRoot, "src/cli/cursor-cloud-app-server-proxy.ts"),
|
||||
output: resolve(packageRoot, "dist/cli/cursor-cloud-app-server-proxy.js"),
|
||||
verifiedOutput: resolve(packageRoot, "dist/cli/cursor-cloud-app-server-proxy.cjs"),
|
||||
}),
|
||||
Object.freeze({
|
||||
name: "kimi-metadata-probe",
|
||||
source: resolve(packageRoot, "src/cli/kimi-metadata-probe.ts"),
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
} from "../contracts/native-session-backend.js";
|
||||
import type { CodexAppServerTransport } from "../drivers/codex/app-server-transport.js";
|
||||
import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js";
|
||||
import { CURSOR_CLOUD_TERMINAL_DRAIN_TIMEOUT_MS } from "../contracts/cursor-cloud.js";
|
||||
import { cursorCloudDriverDescriptor } from "../drivers/cursor-cloud/cursor-cloud-driver.js";
|
||||
import type { CodexWorkingDirectoryAuthority } from "../drivers/codex/codex-boundaries.js";
|
||||
import { describeRunnerdExternalBotDriver } from "../drivers/dot/runnerd-dot-driver.js";
|
||||
import { HarnessDriverBackend } from "./harness-driver-backend.js";
|
||||
@@ -59,6 +61,7 @@ export interface CodexNativeSessionBackendOptions {
|
||||
|
||||
function transportDriverIdentity(input: NativeExecutionInput): {
|
||||
kind:
|
||||
| "cursor_cloud_api"
|
||||
| "codex_app_server"
|
||||
| "opencode_server"
|
||||
| "claude_managed_agents_api"
|
||||
@@ -68,6 +71,8 @@ function transportDriverIdentity(input: NativeExecutionInput): {
|
||||
version: string;
|
||||
} {
|
||||
switch (input.provider.kind) {
|
||||
case "cursor_cloud":
|
||||
return { kind: "cursor_cloud_api", displayName: "Cursor Cloud (experimental)", version: "cursor-cloud-v1" };
|
||||
case "codex":
|
||||
return {
|
||||
kind: "codex_app_server",
|
||||
@@ -153,7 +158,7 @@ function createTransportBackedNativeSessionBackend(
|
||||
|
||||
const baseInstructions = options.retainedSystemInstructions ?? nativeSystemInstructions(input);
|
||||
const transportFactory = options.transportFactory;
|
||||
return new HarnessDriverBackend(
|
||||
const backend = new HarnessDriverBackend(
|
||||
new CodexAppServerDriver({
|
||||
...(input.provider.model ? { model: input.provider.model } : {}),
|
||||
...(input.provider.kind === "codex" && "webSearch" in input.provider && input.provider.webSearch === "disabled"
|
||||
@@ -203,6 +208,7 @@ function createTransportBackedNativeSessionBackend(
|
||||
capabilities: isCodex
|
||||
? {}
|
||||
: { steering: false, goals: false, threadLineage: false,
|
||||
...(input.provider.kind === "cursor_cloud" ? { runtimeRequestResolution: false } : {}),
|
||||
toolRefreshOnResume: input.provider.kind !== "acpx"
|
||||
|| ACPX_CAPABILITY_PROFILES[input.provider.agent].toolRefreshOnResume === true },
|
||||
collaborationModes: supportsCollaborativePlanning
|
||||
@@ -211,7 +217,17 @@ function createTransportBackedNativeSessionBackend(
|
||||
requireProviderSessionIdentity: options.transportFactory !== undefined,
|
||||
}),
|
||||
preparedContext ? constraints : undefined,
|
||||
input.provider.kind === "cursor_cloud" ? CURSOR_CLOUD_TERMINAL_DRAIN_TIMEOUT_MS : undefined,
|
||||
);
|
||||
if (input.provider.kind === "cursor_cloud") {
|
||||
backend.descriptor = async () => {
|
||||
const descriptor = cursorCloudDriverDescriptor();
|
||||
return { kind: "runner", name: descriptor.kind, version: descriptor.version,
|
||||
capabilities: structuredClone(descriptor.capabilities),
|
||||
runtimeContextCapabilities: structuredClone(descriptor.runtimeContextCapabilities) };
|
||||
};
|
||||
}
|
||||
return backend;
|
||||
}
|
||||
|
||||
/** Inspect the selected runnerd harness without starting a provider process. */
|
||||
|
||||
@@ -542,6 +542,21 @@ describe("HarnessDriverBackend", () => {
|
||||
expect(session.identity()).toEqual({ ...originalIdentity, runId: "run-2" });
|
||||
});
|
||||
|
||||
it.each([undefined, 30_000])("preserves the cancellation terminal bound through open and recovery (%s)", async bound => {
|
||||
const harness = new FakeHarnessSession();
|
||||
const backend = new HarnessDriverBackend({ ...driver, async openSession() { return harness; }, async recoverSession() { return { recovered: true, session: harness }; } }, undefined, bound);
|
||||
const identity = { companyId: "company-1", agentId: "agent-1", issueId: "issue-1", runId: "run-1", sessionId: "session-1" };
|
||||
const opened = await backend.openSession({ identity });
|
||||
const snapshot = await opened.snapshot({ signal: new AbortController().signal });
|
||||
const recovered = await backend.recoverSession(snapshot, { signal: new AbortController().signal });
|
||||
expect(recovered.recovered).toBe(true);
|
||||
for (const session of [opened, recovered.session!]) {
|
||||
const cancellation = session.cancel!({ reason: "durable wait", signal: new AbortController().signal });
|
||||
expect(cancellation.terminalDrainTimeoutMs).toBe(bound);
|
||||
await cancellation.cleanup;
|
||||
}
|
||||
});
|
||||
|
||||
it("restores a persisted terminal before the recovered stream is consumed", async () => {
|
||||
const recoveryDriver: HarnessDriver = {
|
||||
...driver,
|
||||
|
||||
@@ -34,7 +34,7 @@ const MAX_RECOVERY_SEMANTIC_RESULT_DEPTH = 128;
|
||||
export class HarnessDriverBackend implements NativeSessionBackend {
|
||||
readonly #driver: HarnessDriver;
|
||||
|
||||
constructor(driver: HarnessDriver, readonly preparedTaskConstraints?: readonly string[]) {
|
||||
constructor(driver: HarnessDriver, readonly preparedTaskConstraints?: readonly string[], private readonly terminalDrainTimeoutMs?: number) {
|
||||
this.#driver = driver;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export class HarnessDriverBackend implements NativeSessionBackend {
|
||||
.catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
return new HarnessNativeSession(input, session, undefined, (await this.#driver.descriptor()).capabilities.steering);
|
||||
return new HarnessNativeSession(input, session, undefined, (await this.#driver.descriptor()).capabilities.steering, this.terminalDrainTimeoutMs);
|
||||
}
|
||||
|
||||
async recoverSession(
|
||||
@@ -186,6 +186,7 @@ export class HarnessDriverBackend implements NativeSessionBackend {
|
||||
recovered.session,
|
||||
recoveredTerminal,
|
||||
(await this.#driver.descriptor()).capabilities.steering,
|
||||
this.terminalDrainTimeoutMs,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -441,6 +442,7 @@ class HarnessNativeSession implements NativeSession {
|
||||
session: HarnessSession,
|
||||
terminal?: PrpTerminalState | null,
|
||||
steeringSupported = false,
|
||||
private readonly terminalDrainTimeoutMs?: number,
|
||||
) {
|
||||
this.#steeringSupported = steeringSupported;
|
||||
this.#input = structuredClone(input);
|
||||
@@ -766,6 +768,7 @@ class HarnessNativeSession implements NativeSession {
|
||||
this.revokeTurnPublication();
|
||||
const interrupt = this.#session.interrupt;
|
||||
return {
|
||||
...(this.terminalDrainTimeoutMs === undefined ? {} : { terminalDrainTimeoutMs: this.terminalDrainTimeoutMs }),
|
||||
cleanup:
|
||||
interrupt === undefined
|
||||
? Promise.resolve()
|
||||
|
||||
@@ -292,6 +292,23 @@ describe("native backend factory", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports Cursor Cloud capabilities without inheriting native Codex promises", async () => {
|
||||
const input = execution({ kind: "cursor_cloud", model: "explicit-model", cursorCloud: {
|
||||
schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/project" }],
|
||||
} });
|
||||
input.session.driverKind = "cursor_cloud_api";
|
||||
const backend = createRunnerdNativeSessionBackend(preparedExecution(input), {
|
||||
transportFactory: () => { throw new Error("descriptor must not launch the transport"); },
|
||||
});
|
||||
const descriptor = await backend.descriptor();
|
||||
expect(descriptor).toMatchObject({ name: "cursor_cloud_api", version: "cursor-cloud-v1", capabilities: {
|
||||
resume: true, dynamicTools: true, runtimeRequestResolution: false, runtimeRequestHandoff: false,
|
||||
steering: false, goals: false, threadLineage: false,
|
||||
unsupported: expect.arrayContaining(["native_permissions", "native_questions", "preprompt_model_acknowledgment"]),
|
||||
} });
|
||||
expect(descriptor.capabilities.typedEventFamilies).toBeUndefined();
|
||||
});
|
||||
|
||||
it("defers remote ACPX workspace admission to the runner filesystem", async () => {
|
||||
const remoteWorkspace = "/home/daytona/paperclip-workspace";
|
||||
const transport = new FakeCodexTransport();
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env node
|
||||
import { constants } from "node:fs";
|
||||
import { open as openFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { Readable, Writable } from "node:stream";
|
||||
import type { HarnessSession, PersistedHarnessSession } from "../contracts/harness-driver.js";
|
||||
import { parseNativeRuntimeContext } from "../contracts/runtime-context.js";
|
||||
import { CursorCloudDriver, CURSOR_CLOUD_DRIVER_KIND, CURSOR_CLOUD_DRIVER_VERSION, type CursorCloudSessionHandle } from "../drivers/cursor-cloud/cursor-cloud-driver.js";
|
||||
import { cursorCloudRecord, parseCursorCloudBinding, parseNativeCursorCloudConfig } from "../drivers/cursor-cloud/config.js";
|
||||
import { enqueueSerialInput } from "./serial-input-queue.js";
|
||||
|
||||
type Rpc = { id?: string | number; method?: string; params?: unknown; result?: unknown; error?: unknown };
|
||||
const MAX_FRAME_BYTES = 2_097_152;
|
||||
const object = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
const text = (value: unknown, fallback = "") => typeof value === "string" ? value : fallback;
|
||||
|
||||
async function privateJson(path: string, maxBytes: number): Promise<unknown> {
|
||||
const handle = await openFile(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
if (!stat.isFile() || stat.size > maxBytes || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid())) throw new Error("Cursor Cloud configuration file must be owned, private and bounded");
|
||||
const buffer = Buffer.alloc(stat.size + 1);
|
||||
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
||||
if (bytesRead !== stat.size) throw new Error("Cursor Cloud configuration changed during admission");
|
||||
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead)));
|
||||
} finally { await handle.close(); }
|
||||
}
|
||||
|
||||
export function startCursorCloudAppServerProxy(options: {
|
||||
input?: Readable;
|
||||
output?: Writable;
|
||||
diagnostic?: Writable;
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
} = {}): { done: Promise<void>; shutdown(): Promise<void> } {
|
||||
const input = options.input ?? process.stdin;
|
||||
const output = options.output ?? process.stdout;
|
||||
const diagnostic = options.diagnostic ?? process.stderr;
|
||||
const env = options.environment ?? process.env;
|
||||
const pending = new Map<string, { resolve(value: unknown): void; reject(error: Error): void }>();
|
||||
let sequence = 0;
|
||||
let session: CursorCloudSessionHandle | null = null;
|
||||
let cwd = "";
|
||||
let model = "";
|
||||
let closed = false;
|
||||
let shuttingDown: Promise<void> | null = null;
|
||||
let commandQueue = Promise.resolve();
|
||||
let buffer = Buffer.alloc(0);
|
||||
let bootstrapFailed = false;
|
||||
let finish!: () => void;
|
||||
const done = new Promise<void>(resolveDone => { finish = resolveDone; });
|
||||
|
||||
function send(value: unknown): void {
|
||||
const encoded = `${JSON.stringify(value)}\n`;
|
||||
if (Buffer.byteLength(encoded) > MAX_FRAME_BYTES || output.writableLength + Buffer.byteLength(encoded) > 4_194_304) throw new Error("Cursor Cloud output frame/consumer exceeded its bound");
|
||||
output.write(encoded);
|
||||
}
|
||||
function requestController(params: Record<string, unknown>): Promise<unknown> {
|
||||
if (closed || pending.size >= 128) return Promise.reject(new Error("Cursor Cloud controller is closed or exceeds its pending-call bound"));
|
||||
const id = `cursor-cloud-${++sequence}`;
|
||||
const promise = new Promise<unknown>((resolveValue, reject) => pending.set(id, { resolve: resolveValue, reject }));
|
||||
send({ id, method: "item/tool/call", params });
|
||||
return promise;
|
||||
}
|
||||
async function openSession(params: Record<string, unknown>, resume: boolean): Promise<unknown> {
|
||||
if (session) throw new Error("Cursor Cloud thread is already open");
|
||||
const configPath = env.PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH?.trim();
|
||||
if (!configPath) throw new Error("PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH is required");
|
||||
const raw = cursorCloudRecord(await privateJson(configPath, 65_536));
|
||||
const { binding: rawBinding, ...publicConfig } = raw;
|
||||
const binding = parseCursorCloudBinding(rawBinding);
|
||||
const config = parseNativeCursorCloudConfig(publicConfig);
|
||||
if (binding.normalizedSessionId !== env.PAPERCLIP_NORMALIZED_SESSION_ID) throw new Error("Cursor Cloud normalized session binding mismatch");
|
||||
const runtimeContextPath = env.PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH?.trim();
|
||||
const runtimeContext = runtimeContextPath ? parseNativeRuntimeContext(await privateJson(runtimeContextPath, 1_048_576)) : null;
|
||||
cwd = resolve(text(params.cwd, process.cwd())); model = text(params.model);
|
||||
const runId = env.PAPERCLIP_RUN_ID;
|
||||
if (!runId) throw new Error("PAPERCLIP_RUN_ID is required");
|
||||
const relayUrl = env.PAPERCLIP_CURSOR_CLOUD_RELAY_URL;
|
||||
const relayToken = env.PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN;
|
||||
const socketPath = env.PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH;
|
||||
const relayExpiresAt = Number(env.PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT);
|
||||
if (!relayUrl || !relayToken || !socketPath || !Number.isSafeInteger(relayExpiresAt) || relayExpiresAt <= 0) throw new Error("Cursor Cloud semantic relay binding and expiry are required");
|
||||
const driver = new CursorCloudDriver({ model, config, binding, runtimeDirectory: dirname(resolve(configPath)), artifactRoot: env.PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT, relay: { url: relayUrl, token: relayToken, socketPath, expiresAt: relayExpiresAt },
|
||||
instructions: text(params.baseInstructions, "Complete only the supplied Paperclip task. Use Paperclip tools and finish with paperclip_finish or paperclip_block."), runtimeContext,
|
||||
dynamicTools: Array.isArray(params.dynamicTools) ? params.dynamicTools.map(value => cursorCloudRecord(value)) : [],
|
||||
dynamicToolHandler: async call => requestController({ ...call }), environment: env,
|
||||
runnerInstanceId: env.PAPERCLIP_RUNNER_INSTANCE_ID ?? "cursor-cloud-sidecar" });
|
||||
if (resume) {
|
||||
const threadId = text(params.threadId);
|
||||
const snapshot: PersistedHarnessSession = { driverKind: CURSOR_CLOUD_DRIVER_KIND, driverSessionId: threadId, normalizedSessionId: binding.normalizedSessionId, runId, workingDirectory: cwd };
|
||||
const restored = await driver.recoverSession(snapshot);
|
||||
if (!restored.recovered || !restored.session) throw new Error(restored.reason ?? "Cursor Cloud recovery failed");
|
||||
session = restored.session;
|
||||
} else session = await driver.openSession({ runId, normalizedSessionId: binding.normalizedSessionId, workingDirectory: cwd });
|
||||
void pump(session).catch(() => shutdown());
|
||||
return { thread: { id: session.ids().driverSessionId, sessionId: session.ids().driverSessionId, cwd }, submittedModel: model, effectiveModelAcknowledged: false, instructionsAcknowledged: false };
|
||||
}
|
||||
async function pump(opened: HarnessSession): Promise<void> {
|
||||
for await (const event of opened.events()) {
|
||||
const payload = event.payload;
|
||||
const threadId = opened.ids().driverSessionId;
|
||||
const turnId = event.turnId;
|
||||
if (event.eventType === "turn.started") send({ method: "turn/started", params: { threadId, turnId, turn: { id: turnId, status: "inProgress" } } });
|
||||
else if (["item.started", "item.delta", "item.completed"].includes(event.eventType)) {
|
||||
const assistant = payload.kind === "agentMessage";
|
||||
const nativeItem = object(payload.item);
|
||||
const item = payload.kind === "tool" ? { ...nativeItem, type: "builtinToolCall", tool: nativeItem.name,
|
||||
...(nativeItem.result === undefined ? {} : { output: typeof nativeItem.result === "string" ? nativeItem.result : JSON.stringify(nativeItem.result) }) }
|
||||
: payload.item ?? { id: event.itemId, type: assistant ? "agentMessage" : payload.kind, text: payload.text };
|
||||
send({ method: event.eventType === "item.delta" && assistant ? "item/agentMessage/delta" : event.eventType.replace(".", "/"), params: {
|
||||
threadId, turnId, itemId: event.itemId, ...payload, item,
|
||||
...(event.eventType === "item.delta" && assistant ? { delta: payload.text } : {}),
|
||||
} });
|
||||
} else if (["turn.completed", "turn.failed", "turn.interrupted"].includes(event.eventType)) send({ method: event.eventType.replace(".", "/"), params: { threadId, turnId, ...(payload.cursorCloudTerminalDelivery ? { cursorCloudTerminalDelivery: payload.cursorCloudTerminalDelivery } : {}), turn: { id: turnId, status: event.eventType.slice(5), ...(payload.error ? { error: payload.error } : {}) } } });
|
||||
else if (event.eventType === "harness.diagnostic") send({ method: "warning", params: { threadId, turnId, code: payload.code, message: payload.message ?? payload.code } });
|
||||
}
|
||||
}
|
||||
async function stopReceipt(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
if (!session) return { remoteStopped: true, remoteRunId: null };
|
||||
await session.interrupt?.({});
|
||||
const state = await session.read?.() ?? {};
|
||||
const expectedTurnId = typeof params.turnId === "string" ? params.turnId : null;
|
||||
if (expectedTurnId !== null && expectedTurnId !== state.turnId) throw new Error("Cursor Cloud shutdown turn identity changed");
|
||||
const stoppedTurnId = state.pendingTerminalDelivery ? state.turnId : expectedTurnId;
|
||||
if (state.runId && state.phase !== "terminal") throw new Error("Cursor Cloud remote Stop is unconfirmed");
|
||||
if (!state.runId && state.phase !== "idle") throw new Error("Cursor Cloud pending remote create is unconfirmed");
|
||||
return { remoteStopped: true, threadId: session.ids().driverSessionId, turnId: stoppedTurnId ?? null, remoteAgentId: state.agentId ?? null, remoteRunId: state.runId ?? null, ...(state.pendingTerminalDelivery ? { cursorCloudTerminalDelivery: state.pendingTerminalDelivery } : {}), ...(state.providerStatus ? { status: state.providerStatus } : {}) };
|
||||
}
|
||||
async function handle(message: Rpc): Promise<void> {
|
||||
if (message.method === undefined && message.id !== undefined) {
|
||||
const waiter = pending.get(String(message.id)); if (!waiter) return;
|
||||
pending.delete(String(message.id));
|
||||
if (message.error !== undefined) waiter.reject(new Error("Cursor Cloud controller rejected its tool request")); else waiter.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
if (!message.method || message.id === undefined) return;
|
||||
const params = object(message.params);
|
||||
let result: unknown;
|
||||
switch (message.method) {
|
||||
case "initialize": result = { user: { sessionId: "cursor-cloud" }, serverInfo: { name: "cursor-cloud", version: CURSOR_CLOUD_DRIVER_VERSION } }; break;
|
||||
case "thread/start": result = await openSession(params, false); break;
|
||||
case "thread/resume": result = await openSession(params, true); break;
|
||||
case "collaborationMode/list": result = { data: [{ name: "Default", mode: "default", model, reasoningEffort: null }] }; break;
|
||||
case "turn/start": {
|
||||
if (!session) throw new Error("Cursor Cloud thread is not open");
|
||||
if (object(params.collaborationMode).mode === "plan" || params.reasoningMode !== undefined) throw new Error("Cursor Cloud native plan/reasoning override is unsupported");
|
||||
const items = Array.isArray(params.input) ? params.input.map(object) : [];
|
||||
if (items.some(item => item.type !== "text" && item.type !== "inputText")) throw new Error("Cursor Cloud supports text turn input only");
|
||||
const turn = await session.startTurn({ message: { role: "user", text: items.map(item => text(item.text)).join("\n") } });
|
||||
result = { turn: { id: turn.turnId, status: "inProgress" } }; break;
|
||||
}
|
||||
case "turn/interrupt":
|
||||
if (!session?.interrupt) throw new Error("Cursor Cloud thread is not open");
|
||||
await session.interrupt({ ...(typeof params.turnId === "string" ? { turnId: params.turnId } : {}) }); result = true; break;
|
||||
case "thread/read": {
|
||||
if (!session) throw new Error("Cursor Cloud thread is not open");
|
||||
await session.snapshot();
|
||||
const state = await session.read();
|
||||
const activeTurnId = ["creating", "running", "stopping"].includes(String(state.phase)) && typeof state.turnId === "string" ? state.turnId : null;
|
||||
result = { cursorCloudTerminalDelivery: state.pendingTerminalDelivery, thread: { id: session.ids().driverSessionId, cwd, turns: activeTurnId ? [{ id: activeTurnId, status: "inProgress" }] : [] }, transcript: state }; break;
|
||||
}
|
||||
case "paperclip/ackTerminal":
|
||||
if (!session || typeof params.turnId !== "string" || typeof params.receiptId !== "string") throw new Error("Cursor Cloud terminal receipt is missing");
|
||||
await session.acknowledgeTerminalDelivery({ turnId: params.turnId, receiptId: params.receiptId });
|
||||
result = { turnId: params.turnId, receiptId: params.receiptId, acknowledged: true }; break;
|
||||
case "paperclip/shutdown": result = await stopReceipt(params); break;
|
||||
default: throw new Error("Unsupported Cursor Cloud proxy method");
|
||||
}
|
||||
send({ id: message.id, result });
|
||||
}
|
||||
async function shutdown(): Promise<void> {
|
||||
shuttingDown ??= (async () => {
|
||||
closed = true;
|
||||
for (const waiter of pending.values()) waiter.reject(new Error("Cursor Cloud controller closed")); pending.clear();
|
||||
try { await session?.close({ reason: "proxy_shutdown", force: true }); }
|
||||
catch { diagnostic.write("[cursor-cloud] Remote termination is unconfirmed; checkpoint retained.\n"); process.exitCode = 1; }
|
||||
finish();
|
||||
})();
|
||||
return shuttingDown;
|
||||
}
|
||||
function receive(message: Rpc): void {
|
||||
if (closed) return;
|
||||
if (message.method === undefined && message.id !== undefined) { void handle(message); return; }
|
||||
commandQueue = enqueueSerialInput(commandQueue, async () => {
|
||||
if (bootstrapFailed && message.method !== "paperclip/shutdown") throw new Error("Cursor Cloud bootstrap previously failed");
|
||||
await handle(message);
|
||||
}, error => {
|
||||
if (["initialize", "thread/start", "thread/resume"].includes(message.method ?? "")) bootstrapFailed = true;
|
||||
const reason = error instanceof Error ? error.message : "Cursor Cloud request failed";
|
||||
send({ id: message.id, error: { code: -32000, message: reason.slice(0, 2_000) } });
|
||||
});
|
||||
}
|
||||
input.on("data", (chunk: Buffer | string) => {
|
||||
if (closed) return;
|
||||
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
||||
while (true) {
|
||||
const index = buffer.indexOf(10);
|
||||
if (index < 0) break;
|
||||
const line = buffer.subarray(0, index); buffer = buffer.subarray(index + 1);
|
||||
if (line.length > MAX_FRAME_BYTES) { void shutdown(); return; }
|
||||
if (!line.toString().trim()) continue;
|
||||
try { const message = JSON.parse(line.toString()) as Rpc; receive(message); }
|
||||
catch { send({ id: null, error: { code: -32700, message: "Invalid Cursor Cloud JSON-RPC input" } }); }
|
||||
}
|
||||
if (buffer.length > MAX_FRAME_BYTES) void shutdown();
|
||||
});
|
||||
input.on("end", () => { void commandQueue.then(shutdown); });
|
||||
input.on("error", () => { void shutdown(); });
|
||||
return { done, shutdown };
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
||||
const proxy = startCursorCloudAppServerProxy();
|
||||
process.once("SIGTERM", () => { void proxy.shutdown(); });
|
||||
process.once("SIGINT", () => { void proxy.shutdown(); });
|
||||
void proxy.done.then(() => process.exit(process.exitCode ?? 0));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { closeSync, openSync } from "node:fs";
|
||||
import { chmod, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
@@ -15,8 +15,15 @@ const MODEL = "openrouter/deepseek/deepseek-v4-flash-0731";
|
||||
type RpcFrame = { id?: string | number; method?: string; result?: unknown; error?: unknown };
|
||||
|
||||
async function startProxy(root: string, proxy: string, executable: string, runtime: string) {
|
||||
const fd = process.platform === "linux" ? openSync(executable, "r") : undefined;
|
||||
const child = spawn(process.execPath, [proxy, "--paperclip-trusted-opencode-executable", fd === undefined ? executable : "/proc/self/fd/3"], {
|
||||
const launchRoot = join(root, `.paperclip-verified-executable-${randomUUID().replaceAll("-", "")}`);
|
||||
await mkdir(launchRoot, { mode: 0o700 });
|
||||
await chmod(launchRoot, 0o700);
|
||||
const snapshot = join(launchRoot, "launch");
|
||||
await copyFile(executable, snapshot);
|
||||
await chmod(snapshot, 0o500);
|
||||
expect(await readFile(snapshot)).toEqual(await readFile(executable));
|
||||
const fd = process.platform === "linux" ? openSync(snapshot, "r") : undefined;
|
||||
const child = spawn(process.execPath, [proxy, "--paperclip-trusted-opencode-executable", fd === undefined ? snapshot : "/proc/self/fd/3"], {
|
||||
// Keep an enclosing Paperclip run's launch bindings out of this fixture.
|
||||
env: { ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("PAPERCLIP_"))),
|
||||
PAPERCLIP_OPENCODE_RUNTIME_DIR: runtime, OPENROUTER_API_KEY: "fixture-key",
|
||||
@@ -55,10 +62,7 @@ async function startProxy(root: string, proxy: string, executable: string, runti
|
||||
|
||||
it("resumes a session whose OpenCode message history exceeds runnerd's frame limit", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "opencode-proxy-thread-read-"));
|
||||
const launchRoot = join(root, `.paperclip-verified-executable-${randomUUID().replaceAll("-", "")}`);
|
||||
await mkdir(launchRoot, { mode: 0o700 });
|
||||
await chmod(launchRoot, 0o700);
|
||||
const executable = join(launchRoot, "launch");
|
||||
const executable = join(root, "fixture-launch");
|
||||
const fixture = resolve("test/fixtures/fake-opencode-server.mjs");
|
||||
execFileSync("cc", ["-x", "c", "-o", executable, "-"], { input: `#include <unistd.h>\n#include <stdlib.h>\nint main(int argc, char **argv) { char **args = calloc(argc + 2, sizeof(char *)); args[0] = ${JSON.stringify(process.execPath)}; args[1] = ${JSON.stringify(fixture)}; for (int i = 1; i < argc; i++) args[i + 1] = argv[i]; execv(args[0], args); return 127; }` });
|
||||
if (process.platform === "darwin") {
|
||||
|
||||
@@ -38,7 +38,7 @@ function eligibleKey(key: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key)
|
||||
&& !GENERATED_RUNTIME_ENVIRONMENT_KEYS.has(upper)
|
||||
&& !RESERVED_KEYS.has(upper)
|
||||
&& !/^(?:LD_|DYLD_|GIT_CONFIG_|PAPERCLIP_(?:RUNNER_|NATIVE_|GITHUB_|ACPX_|VERIFIED_))/.test(upper);
|
||||
&& !/^(?:LD_|DYLD_|GIT_CONFIG_|PAPERCLIP_(?:RUNNER_|NATIVE_|GITHUB_|ACPX_|CURSOR_CLOUD_|VERIFIED_))/.test(upper);
|
||||
}
|
||||
|
||||
function checkedNames(names: unknown): string[] {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export const CURSOR_CLOUD_DEFAULT_TIMEOUT_MS = 900_000;
|
||||
export const CURSOR_CLOUD_CANCEL_TIMEOUT_MS = 20_000;
|
||||
export const CURSOR_CLOUD_TERMINAL_DRAIN_TIMEOUT_MS = CURSOR_CLOUD_CANCEL_TIMEOUT_MS + 10_000;
|
||||
|
||||
export interface NativeCursorCloudConfig {
|
||||
schema: "paperclip.cursor-cloud.config.v1";
|
||||
repos: Array<{ url: string; startingRef?: string; prUrl?: string }>;
|
||||
env?: { type: "cloud"; name?: string };
|
||||
workOnCurrentBranch?: boolean;
|
||||
autoCreatePR?: boolean;
|
||||
skipReviewerRequest?: boolean;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export function parseNativeCursorCloudConfig(value: unknown): NativeCursorCloudConfig {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("cursor_cloud_config_invalid");
|
||||
const config = value as Record<string, unknown>;
|
||||
const allowed = new Set(["schema", "repos", "env", "workOnCurrentBranch", "autoCreatePR", "skipReviewerRequest", "timeoutMs"]);
|
||||
if (Object.keys(config).some(key => !allowed.has(key)) || config.schema !== "paperclip.cursor-cloud.config.v1"
|
||||
|| !Array.isArray(config.repos) || config.repos.length > 16) throw new Error("cursor_cloud_config_invalid");
|
||||
const validText = (text: unknown) => typeof text === "string" && text.length > 0 && text.length <= 2048 && !/[\x00-\x1f\x7f]/.test(text);
|
||||
const repos = config.repos.map(value => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("cursor_cloud_repository_invalid");
|
||||
const repo = value as Record<string, unknown>;
|
||||
if (Object.keys(repo).some(key => !["url", "startingRef", "prUrl"].includes(key)) || !validText(repo.url)
|
||||
|| (repo.startingRef !== undefined && !validText(repo.startingRef)) || (repo.prUrl !== undefined && !validText(repo.prUrl))) {
|
||||
throw new Error("cursor_cloud_repository_invalid");
|
||||
}
|
||||
const url = new URL(repo.url as string);
|
||||
if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new Error("cursor_cloud_repository_invalid");
|
||||
return { url: repo.url as string, ...(repo.startingRef !== undefined ? { startingRef: repo.startingRef as string } : {}), ...(repo.prUrl !== undefined ? { prUrl: repo.prUrl as string } : {}) };
|
||||
});
|
||||
let env: NativeCursorCloudConfig["env"];
|
||||
if (config.env !== undefined) {
|
||||
if (!config.env || typeof config.env !== "object" || Array.isArray(config.env)) throw new Error("cursor_cloud_environment_invalid");
|
||||
const candidate = config.env as Record<string, unknown>;
|
||||
if (Object.keys(candidate).some(key => !["type", "name"].includes(key)) || candidate.type !== "cloud" || (candidate.name !== undefined && !validText(candidate.name))) throw new Error("cursor_cloud_environment_invalid");
|
||||
env = { type: "cloud", ...(candidate.name ? { name: candidate.name as string } : {}) };
|
||||
}
|
||||
if (repos.length && env?.name) throw new Error("cursor_cloud_repository_and_named_environment_are_exclusive");
|
||||
if (!repos.length && !env?.name) throw new Error("cursor_cloud_repository_or_environment_required");
|
||||
for (const key of ["workOnCurrentBranch", "autoCreatePR", "skipReviewerRequest"]) {
|
||||
if (config[key] !== undefined && typeof config[key] !== "boolean") throw new Error("cursor_cloud_config_invalid");
|
||||
}
|
||||
if (config.timeoutMs !== undefined && (!Number.isSafeInteger(config.timeoutMs) || Number(config.timeoutMs) < 1000 || Number(config.timeoutMs) > 3_600_000)) throw new Error("cursor_cloud_timeout_invalid");
|
||||
return { schema: "paperclip.cursor-cloud.config.v1", repos, ...(env ? { env } : {}),
|
||||
...(config.workOnCurrentBranch !== undefined ? { workOnCurrentBranch: config.workOnCurrentBranch as boolean } : {}),
|
||||
...(config.autoCreatePR !== undefined ? { autoCreatePR: config.autoCreatePR as boolean } : {}),
|
||||
...(config.skipReviewerRequest !== undefined ? { skipReviewerRequest: config.skipReviewerRequest as boolean } : {}),
|
||||
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs as number } : {}) };
|
||||
}
|
||||
@@ -565,6 +565,34 @@ describe("NativeExecutionInputV2 planning", () => {
|
||||
planningContext: null,
|
||||
})).toMatchObject({ task: { workMode: "planning" }, executionMode: "default" });
|
||||
});
|
||||
|
||||
const cloudContext = {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: "0".repeat(64), manifestDigest: "0".repeat(64), rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [], mcp: { assignmentSetId: "none", digest: "0".repeat(64), bindingId: null },
|
||||
} as const;
|
||||
const cloudPlanning = {
|
||||
...planning, schema: "paperclip.native-execution-input.v5",
|
||||
provider: { kind: "cursor_cloud", model: "explicit-cloud-model", cursorCloud: { schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/fixture" }] } },
|
||||
session: { ...planning.session, driverKind: "cursor_cloud_api" },
|
||||
runtimeContext: { ...cloudContext, aggregateDigest: canonicalNativeRuntimeContextDigest(cloudContext) },
|
||||
} as const;
|
||||
|
||||
it("preserves Cursor Cloud semantic planning and its pinned revision in the model envelope", () => {
|
||||
const parsed = parseNativeExecutionInput(cloudPlanning);
|
||||
expect(parsed).toMatchObject({ provider: { kind: "cursor_cloud" }, task: { workMode: "planning" }, executionMode: "plan", planningContext: planning.planningContext });
|
||||
expect(buildNativeModelEnvelope(parsed)).toMatchObject({ executionMode: "plan", task: { workMode: "planning" }, planningContext: planning.planningContext });
|
||||
expect(parseNativeExecutionInput(JSON.parse(JSON.stringify(parsed)))).toEqual(parsed);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["missing context", { planningContext: null }, "planningContext is required"],
|
||||
["standard task", { task: { ...cloudPlanning.task, workMode: "standard" } }, "requires planning work mode"],
|
||||
["default with context", { executionMode: "default" }, "only valid in plan execution mode"],
|
||||
["warm session", { session: { ...cloudPlanning.session, lifecyclePolicy: { mode: "warm", idleTimeoutMs: 1000 } } }, "Cursor Cloud requires"],
|
||||
])("keeps Cursor Cloud planning validation closed for %s", (_name, change, error) => {
|
||||
expect(() => parseNativeExecutionInput({ ...cloudPlanning, ...change })).toThrow(error as string);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["ask", "skill_test"])("NativeExecutionInputV2 %s mode", (workMode) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseNativeCursorCloudConfig, type NativeCursorCloudConfig } from "./cursor-cloud.js";
|
||||
import { resolveGrokDisableWebSearch } from "../drivers/acpx/grok-web-search.js";
|
||||
import { resolveGrokEffort } from "../drivers/acpx/grok-effort.js";
|
||||
import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js";
|
||||
@@ -120,6 +121,7 @@ export interface NativeAcpxProfileSnapshot {
|
||||
}
|
||||
|
||||
export type NativeProviderConfig =
|
||||
| { kind: "cursor_cloud"; model: string; cursorCloud: NativeCursorCloudConfig }
|
||||
| { kind: "codex"; model: string | null; approvalPolicy?: NativeCodexApprovalPolicy }
|
||||
| { kind: "opencode"; model: string; permissionMode?: NativeOpenCodePermissionMode }
|
||||
| {
|
||||
@@ -154,7 +156,7 @@ export type NativeProviderConfig =
|
||||
export type NativeProviderConfigV4 =
|
||||
| { kind: "codex"; model: string | null; approvalPolicy: NativeCodexApprovalPolicy }
|
||||
| { kind: "opencode"; model: string; permissionMode: NativeOpenCodePermissionMode }
|
||||
| Extract<NativeProviderConfig, { kind: "claude_managed" | "aws_agentcore" }>
|
||||
| Extract<NativeProviderConfig, { kind: "claude_managed" | "aws_agentcore" | "cursor_cloud" }>
|
||||
| {
|
||||
kind: "acpx";
|
||||
agent: NativeAcpxAgent;
|
||||
@@ -195,7 +197,7 @@ export interface NativeExecutionInputV1 {
|
||||
};
|
||||
session: {
|
||||
normalizedSessionId: string | null;
|
||||
driverKind: "codex_app_server" | "opencode_server" | "claude_managed_agents_api" | "aws_agentcore_harness_api" | "acpx_runtime";
|
||||
driverKind: "cursor_cloud_api" | "codex_app_server" | "opencode_server" | "claude_managed_agents_api" | "aws_agentcore_harness_api" | "acpx_runtime";
|
||||
protocolVersion: 1;
|
||||
lifecyclePolicy: NativeSessionLifecyclePolicy;
|
||||
};
|
||||
@@ -530,7 +532,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
}
|
||||
if (
|
||||
(
|
||||
session.driverKind !== "codex_app_server"
|
||||
session.driverKind !== "cursor_cloud_api"
|
||||
&& session.driverKind !== "codex_app_server"
|
||||
&& session.driverKind !== "opencode_server"
|
||||
&& session.driverKind !== "claude_managed_agents_api"
|
||||
&& session.driverKind !== "aws_agentcore_harness_api"
|
||||
@@ -569,17 +572,20 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
? { kind: "codex", model: null }
|
||||
: record(input.provider, "input.provider");
|
||||
if (
|
||||
provider.kind !== "codex"
|
||||
provider.kind !== "cursor_cloud"
|
||||
&& provider.kind !== "codex"
|
||||
&& provider.kind !== "opencode"
|
||||
&& provider.kind !== "claude_managed"
|
||||
&& provider.kind !== "aws_agentcore"
|
||||
&& provider.kind !== "acpx"
|
||||
) {
|
||||
throw new NativeExecutionInputError("input.provider.kind must be codex, opencode, claude_managed, aws_agentcore, or acpx");
|
||||
throw new NativeExecutionInputError("input.provider.kind must be cursor_cloud, codex, opencode, claude_managed, aws_agentcore, or acpx");
|
||||
}
|
||||
exactKeys(
|
||||
provider,
|
||||
provider.kind === "claude_managed"
|
||||
provider.kind === "cursor_cloud"
|
||||
? ["kind", "model", "cursorCloud"]
|
||||
: provider.kind === "claude_managed"
|
||||
? ["kind", "model", "managedProfile", "maxSessionListCostUsd"]
|
||||
: provider.kind === "aws_agentcore"
|
||||
? ["kind", "model", "agentCoreProfile", "maxEstimatedSessionCostUsd", "invocationLimits"]
|
||||
@@ -593,7 +599,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
"input.provider",
|
||||
);
|
||||
if (
|
||||
(provider.kind === "codex" && session.driverKind !== "codex_app_server")
|
||||
(provider.kind === "cursor_cloud" && session.driverKind !== "cursor_cloud_api")
|
||||
|| (provider.kind === "codex" && session.driverKind !== "codex_app_server")
|
||||
|| (provider.kind === "opencode" && session.driverKind !== "opencode_server")
|
||||
|| (provider.kind === "claude_managed" && session.driverKind !== "claude_managed_agents_api")
|
||||
|| (provider.kind === "aws_agentcore" && session.driverKind !== "aws_agentcore_harness_api")
|
||||
@@ -608,7 +615,12 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
throw new NativeExecutionInputError("input.provider.model is required for opencode in provider/model form");
|
||||
}
|
||||
let parsedProvider: NativeProviderConfigV9 | NativeProviderConfig | NativeProviderConfigV5;
|
||||
if (provider.kind === "claude_managed") {
|
||||
if (provider.kind === "cursor_cloud") {
|
||||
if (!isV5 || !providerModel || providerModel.length > 240 || /[\x00-\x1f\x7f]/.test(providerModel) || lifecyclePolicy.mode !== "per_turn") {
|
||||
throw new NativeExecutionInputError("Cursor Cloud requires a model and per-turn lifecycle in a v5 or later input");
|
||||
}
|
||||
parsedProvider = { kind: "cursor_cloud", model: providerModel, cursorCloud: parseNativeCursorCloudConfig(provider.cursorCloud) };
|
||||
} else if (provider.kind === "claude_managed") {
|
||||
if (providerModel === null) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for claude_managed");
|
||||
}
|
||||
@@ -994,7 +1006,7 @@ export function buildNativeModelEnvelope(input: NativeExecutionInput, options?:
|
||||
return {
|
||||
schema: NATIVE_MODEL_ENVELOPE_SCHEMA_V1,
|
||||
task: structuredClone(input.task),
|
||||
workspace: input.provider.kind === "claude_managed" || input.provider.kind === "aws_agentcore"
|
||||
workspace: input.provider.kind === "cursor_cloud" || input.provider.kind === "claude_managed" || input.provider.kind === "aws_agentcore"
|
||||
? null
|
||||
: { cwd: input.workspace.cwd },
|
||||
completionContract: structuredClone(input.completionContract.contract),
|
||||
@@ -1020,7 +1032,7 @@ export function buildNativeModelEnvelope(input: NativeExecutionInput, options?:
|
||||
},
|
||||
executionMode: input.executionMode,
|
||||
planningContext: structuredClone(input.planningContext),
|
||||
workspace: input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V8 || input.provider.kind === "claude_managed" || input.provider.kind === "aws_agentcore" ? null : { cwd: input.workspace.cwd },
|
||||
workspace: input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V8 || input.provider.kind === "cursor_cloud" || input.provider.kind === "claude_managed" || input.provider.kind === "aws_agentcore" ? null : { cwd: input.workspace.cwd },
|
||||
completionContract: {
|
||||
...structuredClone(input.completionContract.contract),
|
||||
criteria: input.completionContract.contract.criteria.map((criterion) => {
|
||||
@@ -1043,7 +1055,7 @@ export function buildNativeModelEnvelope(input: NativeExecutionInput, options?:
|
||||
},
|
||||
executionMode: input.executionMode,
|
||||
planningContext: structuredClone(input.planningContext),
|
||||
workspace: input.provider.kind === "claude_managed" || input.provider.kind === "aws_agentcore"
|
||||
workspace: input.provider.kind === "cursor_cloud" || input.provider.kind === "claude_managed" || input.provider.kind === "aws_agentcore"
|
||||
? null
|
||||
: { cwd: input.workspace.cwd },
|
||||
completionContract: structuredClone(input.completionContract.contract),
|
||||
|
||||
@@ -118,6 +118,7 @@ export function nativeRestartInterruptedTurnId(snapshot: Pick<PersistedNativeSes
|
||||
*/
|
||||
export interface NativeSessionCancellation {
|
||||
cleanup: Promise<void>;
|
||||
terminalDrainTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface NativeSessionSnapshotOptions {
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { RUNNER_TOOL_SCOPE_TOKEN_HEADER } from "../drivers/runner-tool-bridge.js";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { cp, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { request } from "node:http";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { createRunnerdNativeSessionBackend } from "../backends/codex-native-backend.js";
|
||||
import type { NativeExecutionInputV5 } from "../contracts/native-execution.js";
|
||||
import { canonicalNativeRuntimeContextDigest, NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest } from "../contracts/runtime-context.js";
|
||||
import { executeNativeSession } from "../native-session-runtime.js";
|
||||
import type { PrpEvent, PrpStructuredRunResult } from "../protocol/replay-contract.js";
|
||||
import type { DurablePrpControlPlane } from "./durable-prp-control-plane.js";
|
||||
import { createRunnerdCodexTransport, defaultCapabilityRunnerdBinary } from "../live/runnerd-codex-transport.js";
|
||||
import { copyOwnedNodeRuntime } from "../test-support/owned-node-runtime.js";
|
||||
|
||||
async function privateCloudNodeCommand(root: string): Promise<string> {
|
||||
const directory = join(root, "provider-node");
|
||||
await mkdir(directory, { mode: 0o700 });
|
||||
const { executable } = await copyOwnedNodeRuntime(directory);
|
||||
// Keep Node's name so the verified launcher selects its descriptor loader.
|
||||
const command = join(directory, process.platform === "win32" ? "node.exe" : "node");
|
||||
await rename(executable, command);
|
||||
return command;
|
||||
}
|
||||
|
||||
function buildPrivateCursorCloudProxy(proxyPath: string): void {
|
||||
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const builderUrl = new URL("../../scripts/build-verified-provider-entrypoints.mjs", import.meta.url).href;
|
||||
execFileSync(process.execPath, ["--input-type=module", "-e", [
|
||||
"import { writeFile } from 'node:fs/promises';",
|
||||
`const { bundleVerifiedProviderEntrypoints } = await import(${JSON.stringify(builderUrl)});`,
|
||||
"const item = (await bundleVerifiedProviderEntrypoints({ write: false })).find(value => value.entrypoint.name === 'cursor-cloud-app-server-proxy');",
|
||||
`await writeFile(${JSON.stringify(proxyPath)}, item.verifiedResult.outputFiles[0].contents, { mode: 0o600 });`,
|
||||
].join("\n")], { cwd: packageRoot, timeout: 20_000, stdio: "pipe" });
|
||||
}
|
||||
|
||||
async function currentCloudScopeHeader(runtime: string, sessionId: string): Promise<string> {
|
||||
const name = `checkpoint-${createHash("sha256").update(sessionId).digest("hex")}.json`;
|
||||
const checkpoint = JSON.parse(await readFile(join(runtime, name), "utf8"));
|
||||
expect(checkpoint.relayTurnToken).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
||||
return `Bearer ${checkpoint.relayTurnToken}`;
|
||||
}
|
||||
|
||||
it("settles a create rejected during real-facade Stop before cold recovery and the next run", async () => {
|
||||
const root = await mkdtemp("/tmp/cc-rejected-stop-");
|
||||
const runtime = join(root, "cloud"), stateDirectory = join(root, "session");
|
||||
const configPath = join(runtime, "config.json"), socketPath = join(root, "mcp.sock"), proxyPath = join(root, "proxy.cjs");
|
||||
const fetchLog = join(root, "fetches.jsonl"), remotePath = join(root, "remote.json");
|
||||
const createSeen = join(root, "create-seen"), stopHeld = join(root, "stop-held"), releaseStop = join(root, "release-stop");
|
||||
const releaseCreate = join(root, "release-create"), rejected = join(root, "rejected");
|
||||
const normalizedSessionId = "cloud-rejected-stop-session";
|
||||
buildPrivateCursorCloudProxy(proxyPath);
|
||||
const providerNodeCommand = await privateCloudNodeCommand(root);
|
||||
const prelude = Buffer.from(`;(() => {
|
||||
const assert = require('node:assert/strict'), fs = require('node:fs'), fsp = require('node:fs/promises');
|
||||
const paths = ${JSON.stringify({ createSeen, stopHeld, releaseStop, releaseCreate, rejected, fetchLog, remotePath })};
|
||||
const wait = async path => { const end = Date.now() + 15000; while (!fs.existsSync(path)) { assert.ok(Date.now() < end, 'controlled gate expired'); await new Promise(resolve => setTimeout(resolve, 5)); } };
|
||||
const rename = fsp.rename; let held = false;
|
||||
fsp.rename = async function(from, to) {
|
||||
await rename.call(this, from, to);
|
||||
if (!held && /checkpoint-[a-f0-9]{64}\\.json$/.test(String(to)) && JSON.parse(fs.readFileSync(to, 'utf8')).phase === 'stopping') {
|
||||
held = true; fs.writeFileSync(paths.stopHeld, 'held'); await wait(paths.releaseStop);
|
||||
}
|
||||
};
|
||||
const now = '2026-10-30T00:00:00Z', json = (value, status = 200) => new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json' } });
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
const target = new URL(String(input)), method = init.method ?? 'GET'; assert.equal(target.origin, 'https://api.cursor.com');
|
||||
fs.appendFileSync(paths.fetchLog, JSON.stringify({ method, path: target.pathname, pid: process.pid }) + '\\n');
|
||||
if (method === 'POST' && target.pathname === '/v1/agents') {
|
||||
if (!fs.existsSync(paths.rejected)) {
|
||||
fs.writeFileSync(paths.createSeen, 'seen'); await wait(paths.releaseCreate); fs.writeFileSync(paths.rejected, 'rejected');
|
||||
return json({ message: 'controlled definitive rejection' }, 422);
|
||||
}
|
||||
const body = JSON.parse(String(init.body));
|
||||
const run = { id: 'run-33333333-3333-4333-8333-333333333333', agentId: body.agentId, status: 'RUNNING', createdAt: now, updatedAt: now };
|
||||
fs.writeFileSync(paths.remotePath, JSON.stringify(run));
|
||||
return json({ agent: { id: body.agentId, status: 'ACTIVE', env: { type: 'cloud' }, url: 'https://cursor.com/agents/' + body.agentId, createdAt: now, updatedAt: now, latestRunId: run.id }, run });
|
||||
}
|
||||
assert.ok(fs.existsSync(paths.remotePath), 'rejected create must not be reconciled as accepted work');
|
||||
const run = JSON.parse(fs.readFileSync(paths.remotePath, 'utf8'));
|
||||
const runPath = '/v1/agents/' + run.agentId + '/runs/' + run.id;
|
||||
if (method === 'GET' && target.pathname === runPath + '/stream') return new Response(new ReadableStream({ start(controller) {
|
||||
init.signal?.addEventListener('abort', () => { try { controller.close(); } catch {} }, { once: true });
|
||||
} }), { headers: { 'content-type': 'text/event-stream' } });
|
||||
if (method === 'POST' && target.pathname === runPath + '/cancel') { run.status = 'CANCELLED'; fs.writeFileSync(paths.remotePath, JSON.stringify(run)); return json({ id: run.id }); }
|
||||
if (method === 'GET' && target.pathname === runPath) return json(run);
|
||||
throw new Error('Unexpected controlled Cloud request');
|
||||
};
|
||||
})();\n`);
|
||||
const built = await readFile(proxyPath), firstLine = built.indexOf(10) + 1;
|
||||
await writeFile(proxyPath, Buffer.concat([built.subarray(0, firstLine), prelude, built.subarray(firstLine)]), { mode: 0o600 });
|
||||
await mkdir(runtime, { mode: 0o700 });
|
||||
const cursorCloud = { schema: "paperclip.cursor-cloud.config.v1" as const, repos: [{ url: "https://github.com/example/repo" }] };
|
||||
await writeFile(configPath, JSON.stringify({ ...cursorCloud, binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId } }), { mode: 0o600 });
|
||||
const identity = { runnerInstanceId: "runner-cloud-rejected", environmentLeaseId: "lease-cloud-rejected", runId: "00000000-0000-4000-8000-000000000781", normalizedSessionId, turnId: "turn-rejected", itemId: "item-rejected" };
|
||||
const environment = { PATH: process.env.PATH, CURSOR_API_KEY: "synthetic-key", PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: configPath,
|
||||
PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT: join(runtime, "artifacts"), PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: socketPath,
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: "synthetic-token", PAPERCLIP_CURSOR_CLOUD_RELAY_URL: "https://relay.example.test/mcp",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000) };
|
||||
const options = { provider: "cursor_cloud" as const, cursorCloud, stateDirectory, runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand,
|
||||
cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" as const }, environment };
|
||||
const first = createRunnerdCodexTransport({ ...options, prpIdentity: identity }), bundles = [first];
|
||||
let stop: Promise<unknown> | undefined;
|
||||
try {
|
||||
await first.transport.request("thread/start", { cwd: root, model: "pinned-model", baseInstructions: "Only the assigned task.", dynamicTools: [] });
|
||||
await first.transport.request("turn/start", { input: [{ type: "text", text: "Controlled rejected task" }] });
|
||||
await vi.waitFor(async () => expect(await readFile(createSeen, "utf8")).toBe("seen"));
|
||||
const checkpointPath = join(runtime, `checkpoint-${createHash("sha256").update(normalizedSessionId).digest("hex")}.json`);
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8"))).toMatchObject({ phase: "creating", pendingOperation: "create_agent" });
|
||||
let stopDone = false;
|
||||
stop = first.transport.request("turn/interrupt", {}).finally(() => { stopDone = true; });
|
||||
await vi.waitFor(async () => expect(await readFile(stopHeld, "utf8")).toBe("held"));
|
||||
await writeFile(releaseCreate, "release");
|
||||
await vi.waitFor(async () => expect(await readFile(rejected, "utf8")).toBe("rejected"));
|
||||
expect(stopDone).toBe(false);
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8"))).toMatchObject({ phase: "stopping", pendingOperation: "create_agent" });
|
||||
await writeFile(releaseStop, "release"); await stop;
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8"))).toMatchObject({ phase: "idle", terminalStatus: "REJECTED", pendingOperation: null, requestedAgentId: null });
|
||||
expect((await readFile(fetchLog, "utf8")).trim().split("\n").map(line => JSON.parse(line))).toMatchObject([{ method: "POST", path: "/v1/agents" }]);
|
||||
await first.transport.close(); expect(first.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
const stopped = JSON.parse(await readFile(join(stateDirectory, "runner", "codex-provider-state.json"), "utf8"));
|
||||
expect(stopped).toMatchObject({ lifecycle: "prepared", providerTerminationReceipt: { remoteStopped: true, remoteRunId: null } });
|
||||
const recovered = createRunnerdCodexTransport({ ...options, prpIdentity: identity, resumeProviderSession: { driverSessionId: normalizedSessionId, providerSessionId: normalizedSessionId } }); bundles.push(recovered);
|
||||
await expect(recovered.transport.request("thread/read", {})).resolves.toMatchObject({ thread: { id: normalizedSessionId } });
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8"))).toMatchObject({ phase: "idle", terminalStatus: "REJECTED", pendingOperation: null });
|
||||
await recovered.transport.close(); expect(recovered.evidence().runnerExited).toBe(true);
|
||||
const next = createRunnerdCodexTransport({ ...options, prpIdentity: { ...identity, runId: "00000000-0000-4000-8000-000000000782", turnId: "turn-next", itemId: "item-next" }, baseInstructions: "Only the next assigned task." }); bundles.push(next);
|
||||
await next.transport.request("thread/read", {});
|
||||
await next.transport.request("turn/start", { input: [{ type: "text", text: "Next admitted task" }] });
|
||||
await vi.waitFor(async () => expect(JSON.parse(await readFile(remotePath, "utf8"))).toMatchObject({ status: "RUNNING" }));
|
||||
await next.transport.request("turn/interrupt", {}); await next.transport.close();
|
||||
expect(next.evidence().runnerExited).toBe(true);
|
||||
expect(JSON.parse(await readFile(remotePath, "utf8"))).toMatchObject({ status: "CANCELLED" });
|
||||
const creates = (await readFile(fetchLog, "utf8")).trim().split("\n").map(line => JSON.parse(line)).filter(call => call.method === "POST" && call.path === "/v1/agents");
|
||||
expect(creates).toHaveLength(2); expect(creates[1].pid).not.toBe(creates[0].pid);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
} finally {
|
||||
await writeFile(releaseCreate, "release"); await writeFile(releaseStop, "release");
|
||||
await stop?.catch(() => undefined);
|
||||
for (const bundle of bundles) await bundle.transport.close().catch(() => undefined);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it("opens and closes the real Cloud facade without a provider prompt", async () => {
|
||||
const root = await mkdtemp("/tmp/cc-runner-launch-");
|
||||
const runtime = join(root, "cloud");
|
||||
const configPath = join(runtime, "config.json");
|
||||
const socketPath = join(root, "mcp.sock");
|
||||
const fetchLog = join(root, "unexpected-fetches.jsonl");
|
||||
const proxyPath = join(root, "cloud-proxy.cjs");
|
||||
buildPrivateCursorCloudProxy(proxyPath);
|
||||
const providerNodeCommand = await privateCloudNodeCommand(root);
|
||||
const builtProxy = await readFile(proxyPath);
|
||||
const firstLine = builtProxy.indexOf(10) + 1;
|
||||
const guard = Buffer.from(`globalThis.fetch = async () => { require("node:fs").appendFileSync(${JSON.stringify(fetchLog)}, "unexpected fetch\\n", { mode: 0o600 }); throw new Error("Cloud launch preflight forbids provider requests"); };\n`);
|
||||
await writeFile(proxyPath, Buffer.concat([builtProxy.subarray(0, firstLine), guard, builtProxy.subarray(firstLine)]), { mode: 0o600 });
|
||||
await mkdir(runtime, { mode: 0o700 });
|
||||
const normalizedSessionId = "cloud-launch-session";
|
||||
const cursorCloud = { schema: "paperclip.cursor-cloud.config.v1" as const, repos: [{ url: "https://github.com/example/repo" }] };
|
||||
await writeFile(configPath, JSON.stringify({ ...cursorCloud, binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId } }), { mode: 0o600 });
|
||||
const bundle = createRunnerdCodexTransport({
|
||||
provider: "cursor_cloud", cursorCloud,
|
||||
stateDirectory: join(root, "runner-session"),
|
||||
runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand,
|
||||
cursorCloudProxyPath: proxyPath,
|
||||
cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" },
|
||||
prpIdentity: { runnerInstanceId: "runner-cloud-launch", environmentLeaseId: "lease-cloud-launch",
|
||||
runId: "00000000-0000-4000-8000-000000000761", normalizedSessionId, turnId: "turn-cloud-launch", itemId: "item-cloud-launch" },
|
||||
environment: {
|
||||
PATH: process.env.PATH,
|
||||
CURSOR_API_KEY: "synthetic-cloud-launch-key",
|
||||
PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: configPath,
|
||||
PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT: join(runtime, "artifacts"),
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: socketPath,
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: "synthetic-cloud-relay-token",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_URL: "https://relay.example.test/mcp/native-runs/00000000-0000-4000-8000-000000000761",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000),
|
||||
},
|
||||
});
|
||||
try {
|
||||
await expect(bundle.transport.request("thread/start", { cwd: root, model: "explicit-model", baseInstructions: "Do only the assigned task.", dynamicTools: [] }))
|
||||
.resolves.toMatchObject({ thread: { id: normalizedSessionId } });
|
||||
expect((await stat(socketPath)).isSocket()).toBe(true);
|
||||
const checkpointName = (await readdir(runtime)).find(name => /^checkpoint-[a-f0-9]{64}\.json$/u.test(name));
|
||||
expect(checkpointName).toBeDefined();
|
||||
const checkpoint = JSON.parse(await readFile(join(runtime, checkpointName!), "utf8"));
|
||||
expect(checkpoint).toMatchObject({ phase: "idle", agentId: null, runId: null, pendingOperation: null });
|
||||
await bundle.transport.close();
|
||||
expect(bundle.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
await expect(stat(fetchLog)).rejects.toHaveProperty("code", "ENOENT");
|
||||
} finally {
|
||||
await bundle.transport.close().catch(() => undefined);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("preserves native Cloud tool identity and redacted output through the real Rust facade", async () => {
|
||||
const root = await mkdtemp("/tmp/cc-tool-facade-");
|
||||
const runtime = join(root, "cloud");
|
||||
const configPath = join(runtime, "config.json");
|
||||
const socketPath = join(root, "mcp.sock");
|
||||
const proxyPath = join(root, "cloud-proxy.cjs");
|
||||
const fetchLog = join(root, "controlled-fetches.jsonl");
|
||||
buildPrivateCursorCloudProxy(proxyPath);
|
||||
const providerNodeCommand = await privateCloudNodeCommand(root);
|
||||
const runId = "run-22222222-2222-4222-8222-222222222222";
|
||||
const fixture = Buffer.from(`;(() => {
|
||||
const assert = require('node:assert/strict'), fs = require('node:fs');
|
||||
let agentId = '', status = 'RUNNING';
|
||||
const now = '2026-10-28T00:00:00Z', runId = ${JSON.stringify(runId)};
|
||||
const json = value => new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } });
|
||||
const run = () => ({ id: runId, agentId, status, createdAt: now, updatedAt: now });
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
const target = new URL(String(input)); assert.equal(target.origin, 'https://api.cursor.com');
|
||||
const path = target.pathname, method = init.method ?? 'GET';
|
||||
fs.appendFileSync(${JSON.stringify(fetchLog)}, JSON.stringify({ method, path }) + '\\n', { mode: 0o600 });
|
||||
if (path === '/v1/agents' && method === 'POST') {
|
||||
agentId = JSON.parse(String(init.body)).agentId;
|
||||
return json({ agent: { id: agentId, status: 'ACTIVE', env: { type: 'cloud' }, url: 'https://cursor.com/agents/' + agentId, createdAt: now, updatedAt: now, latestRunId: runId }, run: run() });
|
||||
}
|
||||
assert.equal(path.startsWith('/v1/agents/' + agentId + '/runs/' + runId), true);
|
||||
if (path.endsWith('/stream') && method === 'GET') return new Response(new ReadableStream({ start(controller) {
|
||||
const args = { toolCallId: 'synthetic-private-argument' };
|
||||
const result = { error: { message: 'Discovery failed synthetic-cloud-launch-key synthetic-cloud-relay-token' } };
|
||||
for (const [index, state] of ['running', 'error'].entries()) controller.enqueue(new TextEncoder().encode('id: ' + (index + 1) + '\\nevent: tool_call\\ndata: ' + JSON.stringify({ callId: 'cloud-native-tool', name: 'get_mcp_tools', status: state, args, result }) + '\\n\\n'));
|
||||
init.signal?.addEventListener('abort', () => { try { controller.close(); } catch {} }, { once: true });
|
||||
} }), { headers: { 'content-type': 'text/event-stream' } });
|
||||
if (path.endsWith('/cancel') && method === 'POST') { status = 'CANCELLED'; return json({ id: runId }); }
|
||||
if (path.endsWith('/' + runId) && method === 'GET') return json(run());
|
||||
throw new Error('Unexpected controlled Cloud request');
|
||||
};
|
||||
})();\n`);
|
||||
const builtProxy = await readFile(proxyPath);
|
||||
const firstLine = builtProxy.indexOf(10) + 1;
|
||||
await writeFile(proxyPath, Buffer.concat([builtProxy.subarray(0, firstLine), fixture, builtProxy.subarray(firstLine)]), { mode: 0o600 });
|
||||
await mkdir(runtime, { mode: 0o700 });
|
||||
const normalizedSessionId = "cloud-tool-facade-session";
|
||||
const cursorCloud = { schema: "paperclip.cursor-cloud.config.v1" as const, repos: [{ url: "https://github.com/example/repo" }] };
|
||||
await writeFile(configPath, JSON.stringify({ ...cursorCloud, binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId } }), { mode: 0o600 });
|
||||
let authority: DurablePrpControlPlane | undefined;
|
||||
const bundle = createRunnerdCodexTransport({
|
||||
provider: "cursor_cloud", cursorCloud, stateDirectory: join(root, "runner-session"),
|
||||
runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath,
|
||||
cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" },
|
||||
prpIdentity: { runnerInstanceId: "runner-cloud-tool", environmentLeaseId: "lease-cloud-tool",
|
||||
runId: "00000000-0000-4000-8000-000000000762", normalizedSessionId, turnId: "turn-cloud-tool", itemId: "item-cloud-tool" },
|
||||
environment: {
|
||||
PATH: process.env.PATH, CURSOR_API_KEY: "synthetic-cloud-launch-key",
|
||||
PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: configPath, PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT: join(runtime, "artifacts"),
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: socketPath, PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: "synthetic-cloud-relay-token",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_URL: "https://relay.example.test/mcp", PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000),
|
||||
},
|
||||
controlPlaneRegistration: async core => { authority = core; await core.start(); return { release: () => undefined }; },
|
||||
});
|
||||
const tools = (): Array<Record<string, unknown>> => authority!.store.state.committedEvents.filter(event => event.eventType.startsWith("tool.execution.")).map(event => ({
|
||||
eventType: event.eventType, ...((event.envelope.payload as Record<string, unknown>).payload as Record<string, unknown>),
|
||||
}));
|
||||
try {
|
||||
const opened = await bundle.transport.request("thread/start", { cwd: root, model: "explicit-model", baseInstructions: "Use the assigned tools.", dynamicTools: [] });
|
||||
await bundle.transport.request("turn/start", { threadId: (opened.thread as { id: string }).id, input: [{ type: "text", text: "Use the tool." }] });
|
||||
await vi.waitFor(() => expect(tools()).toHaveLength(2), { timeout: 10_000 });
|
||||
expect(tools()).toMatchObject([
|
||||
{ eventType: "tool.execution.started", executionId: "cloud-native-tool", transport: "builtin", name: "get_mcp_tools", status: "running", output: expect.stringContaining("Discovery failed [redacted] [redacted]") },
|
||||
{ eventType: "tool.execution.completed", executionId: "cloud-native-tool", transport: "builtin", name: "get_mcp_tools", status: "failed", output: expect.stringContaining("Discovery failed [redacted] [redacted]") },
|
||||
]);
|
||||
for (const tool of tools()) {
|
||||
expect(JSON.parse(String(tool.output))).toEqual({ error: { message: "Discovery failed [redacted] [redacted]" } });
|
||||
expect(tool).not.toHaveProperty("arguments");
|
||||
expect(JSON.stringify(tool)).not.toContain("synthetic-private-argument");
|
||||
}
|
||||
await bundle.transport.close();
|
||||
expect(bundle.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
const calls = (await readFile(fetchLog, "utf8")).trim().split("\n").map(line => JSON.parse(line));
|
||||
expect(calls.filter(call => call.path === "/v1/agents" && call.method === "POST")).toHaveLength(1);
|
||||
expect(calls.filter(call => call.path.endsWith("/cancel") && call.method === "POST")).toHaveLength(1);
|
||||
} finally {
|
||||
await bundle.transport.close().catch(() => undefined);
|
||||
await authority?.stop();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
async function cloudQuestionHandoff(terminalDelayMs: number, governed: boolean, planning = false) {
|
||||
const root = await mkdtemp("/tmp/cc-question-");
|
||||
const runtime = join(root, "cloud");
|
||||
const configPath = join(runtime, "config.json");
|
||||
const socketPath = join(root, "mcp.sock");
|
||||
const proxyPath = join(root, "cloud-proxy.cjs");
|
||||
const fetchLog = join(root, "controlled-fetches.jsonl");
|
||||
buildPrivateCursorCloudProxy(proxyPath);
|
||||
const providerNodeCommand = await privateCloudNodeCommand(root);
|
||||
const runId = "run-22222222-2222-4222-8222-222222222222";
|
||||
const fixture = Buffer.from(`;(() => {
|
||||
const assert = require('node:assert/strict'), fs = require('node:fs');
|
||||
let agentId = '', status = 'RUNNING';
|
||||
const now = '2026-10-28T00:00:00Z', runId = ${JSON.stringify(runId)};
|
||||
const json = value => new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } });
|
||||
const run = () => ({ id: runId, agentId, status, createdAt: now, updatedAt: now });
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
const target = new URL(String(input)); assert.equal(target.origin, 'https://api.cursor.com');
|
||||
const path = target.pathname, method = init.method ?? 'GET';
|
||||
fs.appendFileSync(${JSON.stringify(fetchLog)}, JSON.stringify({ method, path }) + '\\n', { mode: 0o600 });
|
||||
if (path === '/v1/agents' && method === 'POST') {
|
||||
if (${planning}) {
|
||||
const body = JSON.parse(String(init.body));
|
||||
assert.equal(body.mode, 'agent');
|
||||
const envelope = JSON.parse(body.prompt.text.split('\\n\\nPaperclip task:\\n').at(-1));
|
||||
assert.equal(envelope.executionMode, 'plan');
|
||||
assert.equal(envelope.task.workMode, 'planning');
|
||||
assert.equal(envelope.planningContext.baseRevisionId, 'plan-revision-3');
|
||||
assert.equal(envelope.planningContext.markdown, '# Pinned canonical plan');
|
||||
}
|
||||
agentId = JSON.parse(String(init.body)).agentId;
|
||||
return json({ agent: { id: agentId, status: 'ACTIVE', env: { type: 'cloud' }, url: 'https://cursor.com/agents/' + agentId, createdAt: now, updatedAt: now, latestRunId: runId }, run: run() });
|
||||
}
|
||||
assert.equal(path.startsWith('/v1/agents/' + agentId + '/runs/' + runId), true);
|
||||
if (path.endsWith('/stream') && method === 'GET') return new Response(new ReadableStream({ start(controller) {
|
||||
init.signal?.addEventListener('abort', () => { try { controller.close(); } catch {} }, { once: true });
|
||||
} }), { headers: { 'content-type': 'text/event-stream' } });
|
||||
if (path.endsWith('/cancel') && method === 'POST') { await new Promise(resolve => setTimeout(resolve, ${terminalDelayMs})); status = 'FINISHED'; return json({ id: runId }); }
|
||||
if (path.endsWith('/' + runId) && method === 'GET') return json(run());
|
||||
throw new Error('Unexpected controlled Cloud request');
|
||||
};
|
||||
})();\n`);
|
||||
const builtProxy = await readFile(proxyPath);
|
||||
const firstLine = builtProxy.indexOf(10) + 1;
|
||||
await writeFile(proxyPath, Buffer.concat([builtProxy.subarray(0, firstLine), fixture, builtProxy.subarray(firstLine)]), { mode: 0o600 });
|
||||
await mkdir(runtime, { mode: 0o700 });
|
||||
const normalizedSessionId = "cloud-question-session";
|
||||
const cursorCloud = { schema: "paperclip.cursor-cloud.config.v1" as const, repos: [{ url: "https://github.com/example/repo" }] };
|
||||
await writeFile(configPath, JSON.stringify({ ...cursorCloud, binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId } }), { mode: 0o600 });
|
||||
let authority: DurablePrpControlPlane | undefined;
|
||||
const bundle = createRunnerdCodexTransport({
|
||||
provider: "cursor_cloud", cursorCloud, stateDirectory: join(root, "runner-session"),
|
||||
runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath,
|
||||
cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" },
|
||||
prpIdentity: { runnerInstanceId: "runner-cloud-question", environmentLeaseId: "lease-cloud-question",
|
||||
runId: "00000000-0000-4000-8000-000000000763", normalizedSessionId, turnId: "turn-cloud-question", itemId: "item-cloud-question" },
|
||||
environment: {
|
||||
PATH: process.env.PATH, CURSOR_API_KEY: "synthetic-cloud-launch-key",
|
||||
PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: configPath, PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT: join(runtime, "artifacts"),
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: socketPath, PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: "synthetic-cloud-relay-token",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_URL: "https://relay.example.test/mcp", PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000),
|
||||
},
|
||||
controlPlaneRegistration: async core => { authority = core; await core.start(); return { release: () => undefined }; },
|
||||
});
|
||||
const questionApplied = vi.fn(async () => ({ success: true, contentItems: [{ type: "inputText", text: JSON.stringify({ status: "applied", questionId: "question-controlled" }) }] }));
|
||||
bundle.transport.setServerRequestHandler(questionApplied);
|
||||
const mcp = async (body: Record<string, unknown>, sessionId?: string): Promise<{ sessionId?: string; value: any }> => {
|
||||
const scopeHeader = await currentCloudScopeHeader(runtime, normalizedSessionId);
|
||||
return new Promise((accept, reject) => {
|
||||
const req = request({ socketPath, path: "/mcp", method: "POST", headers: { Authorization: "Bearer synthetic-cloud-relay-token", [RUNNER_TOOL_SCOPE_TOKEN_HEADER]: scopeHeader, "content-type": "application/json", ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) } }, response => {
|
||||
let text = "";
|
||||
response.on("data", chunk => { text += chunk; });
|
||||
response.on("end", () => accept({ sessionId: response.headers["mcp-session-id"] as string | undefined, value: JSON.parse(text) }));
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.end(JSON.stringify({ jsonrpc: "2.0", ...body }));
|
||||
});
|
||||
};
|
||||
try {
|
||||
if (governed) {
|
||||
await writeFile(join(root, "AGENTS.md"), "Keep assigned instructions.");
|
||||
const digest = "0".repeat(64);
|
||||
const context = {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: root, fileCount: 1, totalBytes: 27 } },
|
||||
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
};
|
||||
const input: NativeExecutionInputV5 = {
|
||||
schema: "paperclip.native-execution-input.v5", executionMode: planning ? "plan" : "default",
|
||||
planningContext: planning ? { documentId: "plan-document", baseRevisionId: "plan-revision-3", baseRevisionNumber: 3,
|
||||
markdown: "# Pinned canonical plan", sha256: createHash("sha256").update("# Pinned canonical plan").digest("hex"), reviewContext: { feedback: "Keep the requested scope" } } : null,
|
||||
binding: { companyId: "company", agentId: "agent", issueId: "issue", runId: "00000000-0000-4000-8000-000000000763", executionWorkspaceId: "workspace" },
|
||||
task: { identifier: "CLOUD-QUESTION", title: "Ask the assigned question", description: null, prompt: "Ask before proceeding.", workMode: planning ? "planning" : "standard" },
|
||||
workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null },
|
||||
session: { normalizedSessionId, driverKind: "cursor_cloud_api", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
provider: { kind: "cursor_cloud", model: "explicit-model", cursorCloud },
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
completionContract: { id: "contract", sha256: digest, schemaVersion: "paperclip.completion-contract.v1", contract: { revision: "1", objective: "Ask before proceeding.", criteria: [{ id: "question", requirement: "Request the response." }] } },
|
||||
interactionResponses: [], credentialBindings: [],
|
||||
};
|
||||
const yielded: PrpStructuredRunResult = {
|
||||
schema: "paperclip.run_result.v1", reportedWorkDisposition: "yielded", summary: "Waiting for the requested response.",
|
||||
completionClaim: { contractRevision: "1", objectiveSatisfied: false, criteria: [{ criterionId: "question", status: "unknown", evidenceRefs: ["interaction:pending"] }], remainingWork: [{ description: "Resume after the response.", blocksCompletion: true }] },
|
||||
evidence: [{ ref: "interaction:pending" }], verification: [], attentionRequests: [], artifacts: [{ kind: "issue_thread_interaction", ref: "interaction:pending" }],
|
||||
continuation: { kind: "response_wake", summary: "Resume from the answer.", idempotencyKey: "interaction-response:pending" },
|
||||
};
|
||||
const backend = createRunnerdNativeSessionBackend(input, { runnerInstanceId: "runner-cloud-question", transportFactory: () => bundle.transport,
|
||||
dynamicTools: [{ name: "request_human_input", description: "Ask the user a required question.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }],
|
||||
dynamicToolHandler: questionApplied,
|
||||
});
|
||||
const history: PrpEvent[] = [];
|
||||
const completeRun = vi.fn(async () => undefined);
|
||||
const execution = executeNativeSession({ input, backend, runnerInstanceId: "runner-cloud-question", controlPlaneInstanceId: "control-cloud-question", timeoutMs: 120_000,
|
||||
controlPlane: {
|
||||
async openRun() {}, async checkpointSession() {}, completeRun,
|
||||
async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(replay) { return { events: history.filter(event => event.sourceInstanceId === replay.sourceInstanceId && event.sourceSeq > replay.afterSourceSeq), highestContiguousSourceSeq: 0 }; },
|
||||
},
|
||||
resolveGovernedWait: ({ event }) => event.eventType === "item.completed" && event.payload.kind === "dynamicToolCall" ? yielded : null,
|
||||
});
|
||||
void execution.catch(() => undefined);
|
||||
await Promise.race([execution, vi.waitFor(() => expect(authority?.store.state.commands.some(command => command.type === "turn.start" && command.status === "completed")).toBe(true), { timeout: 10_000 })]);
|
||||
const initialized = await mcp({ id: 0, method: "initialize" });
|
||||
const response = await mcp({ id: 1, method: "tools/call", params: { name: "request_human_input", arguments: {} } }, initialized.sessionId);
|
||||
expect(response.value.result.isError).not.toBe(true);
|
||||
await expect(execution).resolves.toMatchObject({ result: yielded });
|
||||
expect(completeRun).toHaveBeenCalledOnce();
|
||||
expect(history.some(event => event.eventType === "turn.interrupted")).toBe(true);
|
||||
} else {
|
||||
const opened = await bundle.transport.request("thread/start", { cwd: root, model: "explicit-model", baseInstructions: "Ask the assigned question.", dynamicTools: [{ name: "request_human_input", description: "Ask the user a required question.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }] });
|
||||
const threadId = (opened.thread as { id: string }).id;
|
||||
const started = await bundle.transport.request("turn/start", { threadId, input: [{ type: "text", text: "Ask before proceeding." }] });
|
||||
const initialized = await mcp({ id: 0, method: "initialize" });
|
||||
const response = await mcp({ id: 1, method: "tools/call", params: { name: "request_human_input", arguments: {} } }, initialized.sessionId);
|
||||
expect(response.value.result.isError).not.toBe(true);
|
||||
expect(JSON.stringify(response.value)).toContain("question-controlled");
|
||||
expect(questionApplied).toHaveBeenCalledOnce();
|
||||
await bundle.transport.request("turn/interrupt", { threadId, turnId: (started.turn as { id: string }).id });
|
||||
await vi.waitFor(() => expect(authority!.store.state.committedEvents.some(event => event.eventType === "turn.interrupted")).toBe(true), { timeout: 5_000 });
|
||||
expect(authority!.store.state.commands.some(command => command.type === "turn.stop")).toBe(false);
|
||||
expect(authority!.store.state.committedEvents.filter(event => ["turn.interrupted", "turn.completed", "turn.failed"].includes(event.eventType))).toHaveLength(1);
|
||||
expect(authority!.store.state.commands.filter(command => command.type === "semantic_tool.result")).toHaveLength(1);
|
||||
}
|
||||
await bundle.transport.close();
|
||||
expect(bundle.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
const calls = (await readFile(fetchLog, "utf8")).trim().split("\n").map(line => JSON.parse(line));
|
||||
expect(calls.filter(call => call.path === "/v1/agents" && call.method === "POST")).toHaveLength(1);
|
||||
expect(calls.filter(call => call.path.endsWith("/cancel") && call.method === "POST")).toHaveLength(1);
|
||||
} finally {
|
||||
await bundle.transport.close().catch(() => undefined);
|
||||
await authority?.stop();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
it.each([0, 18_000])("delivers an applied Cloud question and courtesy-interrupt terminal without Stop (%ims)", async terminalDelayMs => {
|
||||
await cloudQuestionHandoff(terminalDelayMs, false);
|
||||
}, 40_000);
|
||||
|
||||
it.each([0, 18_000])("settles a governed Cloud question after its allowed cancellation latency (%ims)", async terminalDelayMs => {
|
||||
await cloudQuestionHandoff(terminalDelayMs, true);
|
||||
}, 45_000);
|
||||
|
||||
it("preserves semantic planning and its pinned revision through the real Rust Cloud default harness", async () => {
|
||||
await cloudQuestionHandoff(0, true, true);
|
||||
}, 30_000);
|
||||
|
||||
async function cloudColdAuthorityRotation(instructions: string, validShutdown = true) {
|
||||
const root = await mkdtemp("/tmp/cc-cold-rotation-");
|
||||
const runtime = join(root, "cloud");
|
||||
const stateDirectory = join(root, "session");
|
||||
const configPath = join(runtime, "config.json");
|
||||
const socketPath = join(root, "mcp.sock");
|
||||
const proxyPath = join(root, "cloud-proxy.cjs");
|
||||
const fetchLog = join(root, "fetches.jsonl");
|
||||
const remoteStatePath = join(root, "remote.json");
|
||||
buildPrivateCursorCloudProxy(proxyPath);
|
||||
const providerNodeCommand = await privateCloudNodeCommand(root);
|
||||
const fixture = Buffer.from(`;(() => {
|
||||
const assert = require('node:assert/strict'), fs = require('node:fs');
|
||||
if (!${JSON.stringify(validShutdown)}) {
|
||||
const write = process.stdout.write;
|
||||
process.stdout.write = function(chunk, ...args) {
|
||||
try { const frame = JSON.parse(String(chunk)); if (frame.result?.remoteStopped === true) { frame.result.remoteStopped = false; chunk = JSON.stringify(frame) + '\\n'; } } catch {}
|
||||
return write.call(this, chunk, ...args);
|
||||
};
|
||||
}
|
||||
const path = ${JSON.stringify(remoteStatePath)}, now = '2026-10-29T00:00:00Z';
|
||||
const read = () => fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : { agentId: null, runs: [] };
|
||||
const write = state => fs.writeFileSync(path, JSON.stringify(state), { mode: 0o600 });
|
||||
const json = value => new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } });
|
||||
const agent = state => ({ id: state.agentId, status: 'ACTIVE', env: { type: 'cloud' }, url: 'https://cursor.com/agents/' + state.agentId, createdAt: now, updatedAt: now, latestRunId: state.runs.at(-1).id });
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
const target = new URL(String(input)); assert.equal(target.origin, 'https://api.cursor.com');
|
||||
const method = init.method ?? 'GET', state = read();
|
||||
const body = init.body ? JSON.parse(String(init.body)) : null;
|
||||
fs.appendFileSync(${JSON.stringify(fetchLog)}, JSON.stringify({ method, path: target.pathname, body, pid: process.pid }) + '\\n', { mode: 0o600 });
|
||||
if (method === 'POST' && (target.pathname === '/v1/agents' || target.pathname === '/v1/agents/' + state.agentId + '/runs')) {
|
||||
if (target.pathname === '/v1/agents') { assert.equal(state.agentId, null); state.agentId = body.agentId; }
|
||||
const run = { id: 'run-' + String(state.runs.length + 1).padStart(8, '0') + '-2222-4222-8222-222222222222', agentId: state.agentId, status: 'RUNNING', createdAt: now, updatedAt: now };
|
||||
state.runs.push(run); write(state); return json({ agent: agent(state), run });
|
||||
}
|
||||
if (method === 'GET' && target.pathname === '/v1/agents/' + state.agentId) return json(agent(state));
|
||||
const run = state.runs.find(run => target.pathname.startsWith('/v1/agents/' + state.agentId + '/runs/' + run.id));
|
||||
assert.ok(run, 'request must bind the retained Agent and an exact Run');
|
||||
if (target.pathname.endsWith('/stream') && method === 'GET') return new Response(new ReadableStream({ start(controller) {
|
||||
init.signal?.addEventListener('abort', () => { try { controller.close(); } catch {} }, { once: true });
|
||||
} }), { headers: { 'content-type': 'text/event-stream' } });
|
||||
if (target.pathname.endsWith('/cancel') && method === 'POST') { run.status = 'FINISHED'; write(state); return json({ id: run.id }); }
|
||||
if (target.pathname.endsWith('/' + run.id) && method === 'GET') return json(run);
|
||||
throw new Error('Unexpected controlled Cloud request');
|
||||
};
|
||||
})();\n`);
|
||||
const builtProxy = await readFile(proxyPath);
|
||||
const firstLine = builtProxy.indexOf(10) + 1;
|
||||
await writeFile(proxyPath, Buffer.concat([builtProxy.subarray(0, firstLine), fixture, builtProxy.subarray(firstLine)]), { mode: 0o600 });
|
||||
await mkdir(runtime, { mode: 0o700 });
|
||||
const normalizedSessionId = "cloud-cold-session";
|
||||
const cursorCloud = { schema: "paperclip.cursor-cloud.config.v1" as const, repos: [{ url: "https://github.com/example/repo" }] };
|
||||
await writeFile(configPath, JSON.stringify({ ...cursorCloud, binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId } }), { mode: 0o600 });
|
||||
const identity = { runnerInstanceId: "runner-cloud-cold", environmentLeaseId: "lease-cloud-cold", runId: "00000000-0000-4000-8000-000000000771", normalizedSessionId, turnId: "turn-first", itemId: "item-first" };
|
||||
const nextIdentity = { ...identity, runId: "00000000-0000-4000-8000-000000000772", turnId: "turn-second", itemId: "item-second" };
|
||||
const tool = (name: string) => ({ name, description: name, inputSchema: { type: "object", properties: {}, additionalProperties: false } });
|
||||
const environment = (token: string, runId: string) => ({ PATH: process.env.PATH, CURSOR_API_KEY: "synthetic-key",
|
||||
PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: configPath, PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT: join(runtime, "artifacts"),
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: socketPath, PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: token,
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_URL: `https://relay.example.test/mcp/native-runs/${runId}`, PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000) });
|
||||
const options = { provider: "cursor_cloud" as const, cursorCloud, stateDirectory, runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand,
|
||||
cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" as const } };
|
||||
const mcp = async (token: string, body: Record<string, unknown>, sessionId?: string, priorScopeHeader?: string): Promise<{ status: number; sessionId?: string; value: any }> => {
|
||||
const scopeHeader = priorScopeHeader ?? await currentCloudScopeHeader(runtime, normalizedSessionId);
|
||||
return new Promise((accept, reject) => {
|
||||
const req = request({ socketPath, path: "/mcp", method: "POST", headers: { Authorization: `Bearer ${token}`, [RUNNER_TOOL_SCOPE_TOKEN_HEADER]: scopeHeader, "content-type": "application/json", ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) } }, response => {
|
||||
let text = ""; response.on("data", chunk => { text += chunk; });
|
||||
response.on("end", () => {
|
||||
try { accept({ status: response.statusCode!, sessionId: response.headers["mcp-session-id"] as string | undefined, value: text ? JSON.parse(text) : null }); }
|
||||
catch (error) { reject(error); }
|
||||
});
|
||||
});
|
||||
req.on("error", reject); req.end(JSON.stringify({ jsonrpc: "2.0", ...body }));
|
||||
});
|
||||
};
|
||||
const first = createRunnerdCodexTransport({ ...options, prpIdentity: identity, environment: environment("token-first", identity.runId) });
|
||||
const bundles = [first];
|
||||
let secondAuthority: DurablePrpControlPlane | undefined;
|
||||
try {
|
||||
await first.transport.request("thread/start", { cwd: root, model: "pinned-model", baseInstructions: "FIRST instruction grant", dynamicTools: [tool("first_tool")] });
|
||||
await first.transport.request("turn/start", { input: [{ type: "text", text: "Ask before proceeding." }] });
|
||||
await vi.waitFor(async () => expect(JSON.parse(await readFile(remoteStatePath, "utf8")).runs).toHaveLength(1));
|
||||
const firstScopeHeader = await currentCloudScopeHeader(runtime, normalizedSessionId);
|
||||
await first.transport.request("turn/interrupt", {});
|
||||
for await (const event of first.transport.notifications()) if (event.method === "turn/completed") break;
|
||||
if (!validShutdown) {
|
||||
await first.transport.close();
|
||||
const unconfirmed = JSON.parse(await readFile(join(stateDirectory, "runner", "codex-provider-state.json"), "utf8"));
|
||||
expect(unconfirmed.lifecycle).not.toBe("prepared");
|
||||
expect(unconfirmed).not.toHaveProperty("providerTerminationReceipt");
|
||||
expect(first.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
return;
|
||||
}
|
||||
await first.transport.close();
|
||||
expect(first.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
const sameAuthority = createRunnerdCodexTransport({ ...options, prpIdentity: identity,
|
||||
environment: environment("token-first", identity.runId), baseInstructions: "Must not replace same-authority instructions",
|
||||
resumeProviderSession: { driverSessionId: normalizedSessionId, providerSessionId: normalizedSessionId } });
|
||||
bundles.push(sameAuthority);
|
||||
await expect(sameAuthority.transport.request("thread/read", {})).resolves.toMatchObject({ thread: { id: normalizedSessionId } });
|
||||
expect(JSON.parse(await readFile(join(stateDirectory, "runner", "codex-provider-state.json"), "utf8")).config.instructions).toBe("FIRST instruction grant");
|
||||
const sameControl = JSON.parse(await readFile(join(stateDirectory, "control-plane", "control-plane-state.json"), "utf8"));
|
||||
expect(sameControl.runAttachTemplate.provider.instructions).toBe("FIRST instruction grant");
|
||||
expect(sameControl.commands.some((command: { type: string }) => command.type === "run.attach")).toBe(false);
|
||||
expect(JSON.parse(await readFile(remoteStatePath, "utf8")).runs).toHaveLength(1);
|
||||
await sameAuthority.transport.close();
|
||||
expect(sameAuthority.evidence().runnerExited).toBe(true);
|
||||
const priorRunnerBytes = await readFile(join(stateDirectory, "runner", "runner-state.json"));
|
||||
const priorCoreBytes = await readFile(join(stateDirectory, "control-plane", "control-plane-state.json"));
|
||||
const stopped = JSON.parse(await readFile(join(stateDirectory, "runner", "codex-provider-state.json"), "utf8"));
|
||||
expect(stopped.lifecycle).toBe("prepared");
|
||||
expect(stopped.providerTerminationReceipt).toMatchObject({ remoteStopped: true });
|
||||
expect(JSON.parse(priorRunnerBytes.toString()).lifecycle).toBe("suspended");
|
||||
const checkpointName = (await readdir(runtime)).find(name => /^checkpoint-[a-f0-9]{64}\.json$/u.test(name))!;
|
||||
const priorCheckpoint = JSON.parse(await readFile(join(runtime, checkpointName), "utf8"));
|
||||
expect(priorCheckpoint).toMatchObject({ phase: "terminal", ownerRunId: identity.runId, pendingTerminalDelivery: null });
|
||||
const baselineRoot = join(root, "baseline");
|
||||
await cp(stateDirectory, baselineRoot, { recursive: true });
|
||||
const second = createRunnerdCodexTransport({ ...options, prpIdentity: nextIdentity,
|
||||
environment: environment("token-second", nextIdentity.runId), baseInstructions: instructions, resumeDynamicTools: [tool("second_tool")],
|
||||
controlPlaneRegistration: async core => { secondAuthority = core; await core.start(); return { release: () => undefined }; } });
|
||||
bundles.push(second);
|
||||
const handler = vi.fn(async () => ({ success: true, contentItems: [{ type: "inputText", text: "current-tool-response" }] }));
|
||||
second.transport.setServerRequestHandler(handler);
|
||||
if (instructions === "") {
|
||||
await expect(second.transport.request("thread/read", {})).rejects.toThrow("Cursor Cloud instructions are empty");
|
||||
expect((secondAuthority!.store.state.runAttachTemplate!.provider as Record<string, unknown>).instructions).toBe("");
|
||||
expect(JSON.parse(await readFile(remoteStatePath, "utf8")).runs).toHaveLength(1);
|
||||
return;
|
||||
}
|
||||
await expect(second.transport.request("thread/read", {})).resolves.toMatchObject({ thread: { id: normalizedSessionId } });
|
||||
expect(JSON.parse(await readFile(join(runtime, checkpointName), "utf8"))).toMatchObject({ ownerRunId: nextIdentity.runId, agentId: priorCheckpoint.agentId, runId: priorCheckpoint.runId });
|
||||
const archives = await readdir(join(stateDirectory, "authority-epochs"));
|
||||
expect(archives).toHaveLength(1);
|
||||
const archive = join(stateDirectory, "authority-epochs", archives[0]!);
|
||||
expect(await readFile(join(archive, "runner-state.json"))).toEqual(priorRunnerBytes);
|
||||
expect(await readFile(join(archive, "control-plane", "control-plane-state.json"))).toEqual(priorCoreBytes);
|
||||
await second.transport.request("turn/start", { input: [{ type: "text", text: "Continue from the answer." }] });
|
||||
await vi.waitFor(async () => expect(JSON.parse(await readFile(remoteStatePath, "utf8")).runs).toHaveLength(2));
|
||||
const retired = await mcp("token-first", { id: 0, method: "initialize" });
|
||||
expect(retired.status).toBe(401);
|
||||
expect((await mcp("token-second", { id: 0, method: "initialize" }, undefined, firstScopeHeader)).status).toBe(401);
|
||||
const initialized = await mcp("token-second", { id: 0, method: "initialize" });
|
||||
const catalog = await mcp("token-second", { id: 1, method: "tools/list" }, initialized.sessionId);
|
||||
expect(catalog.value.result.tools.map((entry: { name: string }) => entry.name)).toContain("second_tool");
|
||||
expect(catalog.value.result.tools.map((entry: { name: string }) => entry.name)).not.toContain("first_tool");
|
||||
const response = await mcp("token-second", { id: 2, method: "tools/call", params: { name: "second_tool", arguments: {} } }, initialized.sessionId);
|
||||
expect(JSON.stringify(response.value)).toContain("current-tool-response");
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
await second.transport.request("turn/interrupt", {});
|
||||
for await (const event of second.transport.notifications()) if (event.method === "turn/completed") break;
|
||||
await second.transport.close();
|
||||
expect(second.evidence().runnerExited).toBe(true);
|
||||
await expect(stat(socketPath)).rejects.toHaveProperty("code", "ENOENT");
|
||||
const calls = (await readFile(fetchLog, "utf8")).trim().split("\n").map(line => JSON.parse(line));
|
||||
const creates = calls.filter(call => call.method === "POST" && (call.path === "/v1/agents" || call.path.endsWith("/runs")));
|
||||
expect(creates).toHaveLength(2);
|
||||
expect(creates[0].path).toBe("/v1/agents");
|
||||
expect(creates[1].path).toBe(`/v1/agents/${priorCheckpoint.agentId}/runs`);
|
||||
expect(creates[1].pid).not.toBe(creates[0].pid);
|
||||
expect(creates.map(call => call.body.model)).toEqual([{ id: "pinned-model" }, { id: "pinned-model" }]);
|
||||
expect(creates[1].body.prompt.text).toContain(instructions);
|
||||
expect((secondAuthority!.store.state.runAttachTemplate!.provider as Record<string, unknown>).instructions).toBe(instructions);
|
||||
expect(JSON.parse(await readFile(join(stateDirectory, "runner", "codex-provider-state.json"), "utf8")).config.instructions).toBe(instructions);
|
||||
expect(creates[1].body.prompt.text).not.toContain("FIRST instruction grant");
|
||||
expect(creates[1].body.mcpServers).toEqual([{ name: "paperclip", type: "http", url: `https://relay.example.test/mcp/native-runs/${nextIdentity.runId}`, headers: { Authorization: "Bearer token-second", [RUNNER_TOOL_SCOPE_TOKEN_HEADER]: await currentCloudScopeHeader(runtime, normalizedSessionId) } }]);
|
||||
const remote = JSON.parse(await readFile(remoteStatePath, "utf8"));
|
||||
expect(new Set(remote.runs.map((run: { id: string }) => run.id)).size).toBe(2);
|
||||
expect(remote.runs.every((run: { status: string }) => run.status === "FINISHED")).toBe(true);
|
||||
for (const mode of ["warm", "unsettled", "cross-binding", "external", "managed", "agentcore"] as const) {
|
||||
const guardRoot = join(root, `guard-${mode}`);
|
||||
await cp(baselineRoot, guardRoot, { recursive: true });
|
||||
if (mode === "warm") {
|
||||
const state = JSON.parse(priorCoreBytes.toString()); state.warmTransition = {};
|
||||
await writeFile(join(guardRoot, "control-plane", "control-plane-state.json"), JSON.stringify(state));
|
||||
}
|
||||
if (mode === "unsettled") {
|
||||
const state = JSON.parse(priorRunnerBytes.toString()); state.lifecycle = "ready";
|
||||
await writeFile(join(guardRoot, "runner", "runner-state.json"), JSON.stringify(state));
|
||||
}
|
||||
const guard = createRunnerdCodexTransport({ ...options, stateDirectory: guardRoot,
|
||||
provider: mode === "managed" ? "claude_managed" : mode === "agentcore" ? "aws_agentcore" : "cursor_cloud",
|
||||
prpIdentity: mode === "cross-binding" ? { ...nextIdentity, normalizedSessionId: "foreign-session" } : nextIdentity,
|
||||
...(mode === "external" ? { readRunnerState: async () => JSON.parse(priorRunnerBytes.toString()) } : {}),
|
||||
environment: environment("token-second", nextIdentity.runId), baseInstructions: instructions });
|
||||
bundles.push(guard);
|
||||
await expect(guard.transport.request("thread/read", {})).rejects.toThrow({ warm: "native_runner_warm_transition_requires_exact_owned_endpoint", unsettled: "native_runner_authority_rotation_requires_settled_state", "cross-binding": "PRP recovery identity does not match the durable session binding", external: "native_runner_prp_run_rotation_unavailable", managed: "native_runner_prp_run_rotation_unavailable", agentcore: "native_runner_prp_run_rotation_unavailable" }[mode]);
|
||||
await guard.transport.close();
|
||||
}
|
||||
} finally {
|
||||
for (const bundle of bundles.reverse()) await bundle.transport.close().catch(() => undefined);
|
||||
await secondAuthority?.stop();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
it.each([{ label: "revised", instructions: "SECOND instruction grant" }, { label: "empty grant rejected", instructions: "" }])("cold-rotates Cloud authority with the same native Agent and current run grants ($label)", async ({ instructions }) => {
|
||||
await cloudColdAuthorityRotation(instructions);
|
||||
}, 40_000);
|
||||
|
||||
it("retains an unconfirmed Cloud shutdown without manufacturing cold readiness", async () => {
|
||||
await cloudColdAuthorityRotation("Current instructions", false);
|
||||
}, 30_000);
|
||||
@@ -1600,6 +1600,41 @@ it("preserves an explicit OpenCode permission mode at the runner spawn boundary"
|
||||
expect(launches[0]!.environment.PAPERCLIP_OPENCODE_COMMAND).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([undefined, {}, {
|
||||
CURSOR_API_KEY: "explicit-cloud-key",
|
||||
PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: "/private/cloud/config.json",
|
||||
PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT: "/private/cloud/artifacts",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: "/private/cloud/mcp.sock",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: "explicit-cloud-relay",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_URL: "https://relay.example.test/mcp/native-runs/run",
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: "2000000000000",
|
||||
PAPERCLIP_CURSOR_CLOUD_UNRELATED: "must-not-cross",
|
||||
}])("forwards only explicitly supplied Cloud launch bindings at the runner boundary (%j)", (environment: NodeJS.ProcessEnv | undefined) => {
|
||||
const names = [
|
||||
"CURSOR_API_KEY", "PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH", "PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH", "PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_URL", "PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT",
|
||||
] as const;
|
||||
for (const name of names) vi.stubEnv(name, "ambient-must-not-cross");
|
||||
const launches: RunnerProcessLaunchSpec[] = [];
|
||||
try {
|
||||
spawnRunner({
|
||||
connection: { mode: "connect", connectUrl: "ws://127.0.0.1:43127" },
|
||||
stateDirectory: "/tmp/paperclip-runner-test", identity, ticket: "bootstrap-ticket",
|
||||
maxOutboxBytes: 256 * 1024, p0ReserveBytes: 64 * 1024,
|
||||
runnerVersion: expectedRunnerVersion, runnerDigest: expectedRunnerDigest, environment,
|
||||
processLauncher: spec => {
|
||||
launches.push(spec);
|
||||
return { child: { pid: 42, exitCode: null, signalCode: null, kill: () => true },
|
||||
completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }) };
|
||||
},
|
||||
});
|
||||
expect(launches).toHaveLength(1);
|
||||
for (const name of names) expect(launches[0]!.environment[name]).toBe(environment?.[name]);
|
||||
expect(launches[0]!.environment.PAPERCLIP_CURSOR_CLOUD_UNRELATED).toBeUndefined();
|
||||
} finally { vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it("preserves only bounded GitHub credential projection at the runner spawn boundary", () => {
|
||||
const launches: RunnerProcessLaunchSpec[] = [];
|
||||
spawnRunner({
|
||||
|
||||
@@ -4151,6 +4151,12 @@ const runnerExplicitProviderEnvironmentKeys = [
|
||||
"PAPERCLIP_OPENCODE_PERMISSION_MODE",
|
||||
"PAPERCLIP_OPENCODE_RUNTIME_DIR",
|
||||
"PAPERCLIP_OPENCODE_PERFORMANCE_DIAGNOSTICS",
|
||||
"PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH",
|
||||
"PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_URL",
|
||||
"PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT",
|
||||
"PAPERCLIP_RUNNER_INSTANCE_ID",
|
||||
"PAPERCLIP_RUN_ID",
|
||||
"PAPERCLIP_NORMALIZED_SESSION_ID",
|
||||
@@ -4248,6 +4254,9 @@ export function spawnRunner(options: {
|
||||
sidecarScript: string;
|
||||
sidecarScriptSha256: string;
|
||||
};
|
||||
cursorCloudLaunchProfile?: {
|
||||
command: string; commandSha256: string; proxyScript: string; proxyScriptSha256: string;
|
||||
};
|
||||
opencodeLaunchProfile?: {
|
||||
command: string;
|
||||
commandSha256: string;
|
||||
@@ -4337,6 +4346,12 @@ export function spawnRunner(options: {
|
||||
...(options.acpxLaunchProfile.candidateAdmission ? ["--acpx-candidate-admission-json", JSON.stringify(options.acpxLaunchProfile.candidateAdmission)] : []),
|
||||
]
|
||||
: []),
|
||||
...(options.cursorCloudLaunchProfile ? [
|
||||
"--cursor-cloud-proxy-command", options.cursorCloudLaunchProfile.command,
|
||||
"--cursor-cloud-proxy-command-sha256", options.cursorCloudLaunchProfile.commandSha256,
|
||||
"--cursor-cloud-proxy-script", options.cursorCloudLaunchProfile.proxyScript,
|
||||
"--cursor-cloud-proxy-script-sha256", options.cursorCloudLaunchProfile.proxyScriptSha256,
|
||||
] : []),
|
||||
...(options.opencodeLaunchProfile
|
||||
? [
|
||||
"--opencode-proxy-command",
|
||||
@@ -4407,7 +4422,7 @@ export function spawnRunner(options: {
|
||||
maxOutboxBytes: options.maxOutboxBytes, p0ReserveBytes: options.p0ReserveBytes,
|
||||
maxRuntimeMs, maxLifetimeMs: lifetimeDeadline === undefined ? options.maxLifetimeMs : maxRuntimeMs,
|
||||
reconnectGraceMs: options.reconnectGraceMs, lifecyclePolicy: options.lifecyclePolicy,
|
||||
acpxLaunchProfile: options.acpxLaunchProfile, opencodeLaunchProfile: options.opencodeLaunchProfile,
|
||||
acpxLaunchProfile: options.acpxLaunchProfile, opencodeLaunchProfile: options.opencodeLaunchProfile, cursorCloudLaunchProfile: options.cursorCloudLaunchProfile,
|
||||
diagnosticsDirectory: options.diagnosticsDirectory,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -33,6 +33,7 @@ const path = require('node:path');
|
||||
const readline = require('node:readline');
|
||||
const home = process.argv[2], mode = process.argv[3];
|
||||
const requests = [];
|
||||
let deferredAuthorization = '';
|
||||
if(mode==='stubborn') {
|
||||
process.on('SIGTERM',()=>{});
|
||||
const child=require('node:child_process').spawn(process.execPath,['-e',"process.on('SIGTERM',()=>{});setInterval(()=>{},1000)"],{stdio:'ignore'});
|
||||
@@ -43,11 +44,13 @@ reader.on('line', line => {
|
||||
const request=JSON.parse(line); requests.push(request);
|
||||
fs.writeFileSync(path.join(home,'requests.json'),JSON.stringify(requests));
|
||||
if(request.method==='initialize') {
|
||||
if(mode==='early-url') { process.stderr.write('Open the following link to authenticate the ACP server: '+${JSON.stringify(authorizationUrl())}+'\\n'); return; }
|
||||
process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,result:{protocolVersion:1,agentInfo:{name:'antigravity-acp',version:'1.4.3'},agentCapabilities:{_meta:{antigravity:{paperclipAdmission:1}}},authMethods:[{id:'oauth-personal'}]}})+'\\n');
|
||||
} else if(request.method==='authenticate') {
|
||||
const url = mode.startsWith('bad-url') ? 'https://example.invalid/steal' : ${JSON.stringify(authorizationUrl())};
|
||||
const url = mode.startsWith('bad-url')||mode==='late-bad-url' ? 'https://example.invalid/steal' : ${JSON.stringify(authorizationUrl())};
|
||||
const message='Open the following link to authenticate the ACP server: '+url+'\\n';
|
||||
if (!mode.endsWith('url-after-result')) {process.stderr.write(message.slice(0,42));process.stderr.write(message.slice(42));}
|
||||
if(['result-first','late-bad-url','late-duplicate-url'].includes(mode)||mode.endsWith('url-after-result')) deferredAuthorization=message+(mode==='late-duplicate-url'?message:'');
|
||||
else {process.stderr.write(message.slice(0,42));process.stderr.write(message.slice(42));}
|
||||
if(mode==='blocked' || mode==='bad-url' || mode==='stubborn' || mode==='retained') return;
|
||||
if(mode==='error') {process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,error:{code:-32000,message:'do not expose fixture-refresh-token'}})+'\\n');return;}
|
||||
fs.mkdirSync(path.join(home,'antigravity-acp'),{mode:0o700});
|
||||
@@ -56,11 +59,9 @@ reader.on('line', line => {
|
||||
} else {throw new Error('inference/session request forbidden');}
|
||||
});
|
||||
reader.on('close',()=> {
|
||||
if (mode.endsWith('url-after-result')) {
|
||||
const url = mode.startsWith('bad-url') ? 'https://example.invalid/steal' : ${JSON.stringify(authorizationUrl())};
|
||||
// EOF proves the client received the authenticate result before this stderr.
|
||||
process.stderr.write('Open the following link to authenticate the ACP server: '+url+'\\n', () => process.exit(0));
|
||||
} else if(mode==='stubborn'||mode==='retained') setInterval(()=>{},1000);
|
||||
// EOF proves the client received the authenticate result before this stderr.
|
||||
if(deferredAuthorization) process.stderr.write(deferredAuthorization,()=>process.exit(0));
|
||||
else if(mode==='stubborn'||mode==='retained') setInterval(()=>{},1000);
|
||||
else process.exit(0);
|
||||
});
|
||||
`);
|
||||
@@ -102,10 +103,11 @@ describe("Antigravity setup-only Google authentication", () => {
|
||||
|
||||
// The login predicate keeps its five-second bound. Allow ten more seconds
|
||||
// for fixture startup and the durable mocked credential checks on a busy CI worker.
|
||||
it.each(["success", "url-after-result"])("performs native authenticate, retires it, verifies identity, and preserves refresh rotation without repeated status network calls (%s)", async mode => {
|
||||
it.each(["success", "result-first", "url-after-result"])("performs native authenticate, retires it, verifies identity, and preserves refresh rotation without repeated status network calls (%s)", async mode => {
|
||||
const home = await fixture(mode), fetch = googleIdentity(); vi.stubGlobal("fetch", fetch);
|
||||
const login = startAntigravityBrowserLogin(home);
|
||||
await vi.waitFor(() => expect(login.outcome).toBe("success"), { timeout: 5_000 });
|
||||
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5_000 });
|
||||
expect(login.outcome).toBe("success");
|
||||
expect(login.authorizationUrl).toBe(authorizationUrl());
|
||||
expect(JSON.parse(await readFile(join(home, "requests.json"), "utf8")).map((request: { method: string }) => request.method)).toEqual(["initialize", "authenticate"]);
|
||||
expect(setup.spawn.mock.calls[0]![1].env).toMatchObject({ GEMINI_HOME: home, AGY_ACP_FORCE_FILE_STORAGE: "1", BROWSER: "/usr/bin/true" });
|
||||
@@ -125,13 +127,13 @@ describe("Antigravity setup-only Google authentication", () => {
|
||||
expect(fetch).toHaveBeenCalledTimes(6);
|
||||
}, 15_000);
|
||||
|
||||
it.each(["error", "bad-url", "bad-url-after-result"])("rejects %s without leaking provider text or calling Google", async mode => {
|
||||
it.each(["error", "bad-url", "early-url", "late-bad-url", "late-duplicate-url", "bad-url-after-result"])("rejects %s without leaking provider text or calling Google", async mode => {
|
||||
const home = await fixture(mode), fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
|
||||
const login = startAntigravityBrowserLogin(home);
|
||||
await vi.waitFor(() => expect(login.outcome).toBe("failure"), { timeout: 5_000 });
|
||||
await vi.waitFor(() => expect(setup.close).toHaveBeenCalledOnce(), { timeout: 5_000 });
|
||||
expect(login.error).not.toContain("fixture");
|
||||
if (mode.startsWith("bad-url")) expect(login.authorizationUrl).toBeUndefined();
|
||||
if (mode.startsWith("bad-url") || ["early-url", "late-bad-url"].includes(mode)) expect(login.authorizationUrl).toBeUndefined();
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,652 @@ describe("Copilot build-owned installation", () => {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, agentProfileVersion: prior } as never)).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
it("preserves profile 89 while binding generated contract fields in profile 90", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v89-cursor-cloud-qualification-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("9990fd9cdd23df111a180984160a11db7a2f972a60787e8ddea9793678cc268e");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v90-cursor-cloud-qualification-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeRuntimeContextFieldsSourceSha256", "nativeRuntimeContextSourceSha256", "permissionClassifierSourceSha256", "profileCompatibilitySourceSha256", "semanticSidecarSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 89)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 89, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves profile 88 while binding run attachment activity in profile 89", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v88-cursor-cloud-qualification-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("6010dc772d914afec7dd31f35f953ce74b65ab3a648ce54aaee559f908b492f9");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v89-cursor-cloud-qualification-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "harnessSessionSourceSha256", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256", "runtimeRequestFacadeSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 88)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 88, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves profile 87 while binding merged operation timing in profile 88", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v87-cursor-cloud-qualification-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("ce9912c34ae9126e74061f7bc170b59589af7cb6ab26751e250da9e9dd9553c9");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v88-cursor-cloud-qualification-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256", "semanticSidecarSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 87)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 87, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves profile 86 while binding rejected native completion in profile 87", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v86-cursor-cloud-qualification-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("118a4b0a782b56570e5530cca91255da8932c31f827c319b40a5810bd4d446e9");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v87-cursor-cloud-qualification-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeSessionRuntimeSourceSha256", "profileCompatibilitySourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 86)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 86, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves published v65 while binding retained-maintenance waits in v66", async () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v65-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("ca1d95c21b3f49c99b6d7c6bd3c2bf149715db3263927ad653cc0c6f33fb4462");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v66-sleep-maintenance-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 65)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 65, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves the historical sleep-maintenance v64 identity", async () => {
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v64-sleep-maintenance-historical-identity.json", import.meta.url), "utf8"));
|
||||
for (const [name, checksum, changed] of [
|
||||
["copilot-profile-v63-identity.json", "4adb05bd642f0222a3bcc85cfefbeb2988de439f58125012e1c575caedd90798", ["agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256"]],
|
||||
["copilot-profile-v62-sleep-maintenance-historical-identity.json", "e0c93dd7210fbb0115e09b70887ec4ae40621fc5314bd1625e586caf2163c94e", ["agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256", "semanticCompletionContractSourceSha256", "semanticContinuationSourceSha256"]],
|
||||
] as const) {
|
||||
const bytes = readFileSync(new URL(`../../../test/fixtures/${name}`, import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe(checksum);
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(changed);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
const sorted = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", prior.declaration.agentProfileVersion)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: prior.declaration.agentProfileVersion, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves profile 78 while binding retained-session waits in profile 79", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v78-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("da2d7d33dbcdb900f3afb0ad0d0d1502c31f7638772a58009a3300f7e442fd1c");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v79-sleep-maintenance-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 78)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 78, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
|
||||
it("preserves published profile 79 while binding retained cleanup in profile 80", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v79-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("92fb6b5f1a312933f78d650fdc037ded4876d112c472fcfd5c1116cdbb0dae44");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v80-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 79)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 79, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("retains the published profile 75 fixture while advancing the event contract", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v75-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("4178dad6cf43a6026c85f45a0606f30c72570549d517e49f705dbf8f9f41650b");
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 75)).toBe(true);
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 75, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v70 while binding historical Grok recovery decoding in v71", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v70-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("7ab63e2832f9d9c3273b5f799b25a02cba18718e956966c3b79052d74b4bd304");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v71-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "profileCompatibilitySourceSha256"]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 70)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 70, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v69 while binding closed startup diagnostics in v70", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v69-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("a878caa5f40cfad517e852384507b50c1098e219f1dd6822335b1119b03eaea5");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v70-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"admissionDiagnosticsSourceSha256", "agentProfileVersion", "grokAdmissionSourceSha256",
|
||||
"profileCompatibilitySourceSha256", "runtimeHostSourceSha256", "sidecarErrorSourceSha256",
|
||||
]);
|
||||
for (const [name, field] of [["admission-diagnostics.ts", "admissionDiagnosticsSourceSha256"], ["grok-effort.ts", "grokAdmissionSourceSha256"]]) {
|
||||
expect(current.declaration[field]).toBe(createHash("sha256").update(readFileSync(new URL(name!, import.meta.url))).digest("hex"));
|
||||
}
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 69)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 69, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves published and candidate history while binding native Grok admission in v69", async () => {
|
||||
const published = readFileSync(new URL("../../../test/fixtures/copilot-profile-v68-identity.json", import.meta.url));
|
||||
const candidate = readFileSync(new URL("../../../test/fixtures/copilot-profile-v67-grok-native-effort-candidate-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(published).digest("hex")).toBe("9692e8de4e75682c74f8e635e26b8de1f03f1d3e41966dafbb7314d35b75c3cf");
|
||||
expect(createHash("sha256").update(candidate).digest("hex")).toBe("0a20ffe33c92d86f58013582b19c2371f96d93e7e4e0e8096fbf0e7236bd02f4");
|
||||
const prior = JSON.parse(published.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v69-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeAdapterSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
for (const history of [prior, JSON.parse(candidate.toString("utf8"))]) {
|
||||
expect(isSupportedAcpxProfileVersion("copilot", history.declaration.agentProfileVersion)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: history.declaration.agentProfileVersion, commandDigest: history.commandDigest })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves both lineages while binding the merged implementation in v68", async () => {
|
||||
const published = readFileSync(new URL("../../../test/fixtures/copilot-profile-v66-identity.json", import.meta.url));
|
||||
const candidate = readFileSync(new URL("../../../test/fixtures/copilot-profile-v67-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(published).digest("hex")).toBe("9eb0bfd31334210fd41ddb677a165ef7432a2951922e642dddff087e4db8b6bc");
|
||||
expect(createHash("sha256").update(candidate).digest("hex")).toBe("6c818541933b97bea896ca29179ad54c887db9b7d563021da1676a8126f960dd");
|
||||
const prior = JSON.parse(published.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v68-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "providerLaunchSourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
for (const history of [prior, JSON.parse(candidate.toString("utf8"))]) {
|
||||
expect(isSupportedAcpxProfileVersion("copilot", history.declaration.agentProfileVersion)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: history.declaration.agentProfileVersion, commandDigest: history.commandDigest })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves published v65 while binding hot-upgrade adoption in v67", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v65-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("ca1d95c21b3f49c99b6d7c6bd3c2bf149715db3263927ad653cc0c6f33fb4462");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v67-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "providerLaunchSourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 65)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 65, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("retains candidate v66 identity when completion recovery changes in v67", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v66-hot-upgrade-candidate-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("430f7a3bb4f51a22d6f18a515fb32eb4defa5bab60a6a4901f6750e59f2536c6");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v67-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 66)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 66, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("retains the hot-upgrade v59 development identity without replacing published history", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v59-hot-upgrade-candidate-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("b61fe644383acbbaacd209836878243cb96b143807d90c521ef1fbee2a2fcb0c");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const sorted = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 59)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 59, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("retains v65 history while binding the shared native Grok effort transport in v66", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v65-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v66-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "harnessSessionContractSourceSha256", "nativeBackendSourceSha256", "nativeExecutionSourceSha256",
|
||||
"nativeInstructionsSourceSha256", "profileCompatibilitySourceSha256", "providerLaunchSourceSha256", "runtimeAdapterSourceSha256",
|
||||
"runtimeHostSourceSha256", "runtimeRequestBridgeSourceSha256", "runtimeSandboxSourceSha256", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256",
|
||||
"semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 65)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 65, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves published v64 while binding exhausted reconnect cancellation in v65", async () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v64-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("9ee015b4968d8df8d38364626d413f69f6c0e846de20fe1d5f72c7f1706837a9");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v65-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 64)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 64, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves the published startup cancellation candidate as historical v63 evidence", async () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v63-reconnect-candidate-historical-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("1ed0ac81863a588f3a2ff997b0e3f3bf0b64440454eb19282bdcd05d35650207");
|
||||
const candidate = JSON.parse(bytes.toString("utf8"));
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v62-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(candidate.declaration).filter(key => candidate.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
const sorted = Object.fromEntries(Object.entries(candidate.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(candidate.commandDigest);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 63)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 63, commandDigest: candidate.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves published v63 while binding delegated connection guidance in v64", async () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v63-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("4adb05bd642f0222a3bcc85cfefbeb2988de439f58125012e1c575caedd90798");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v64-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:1412842a36a7211d84b6b98a3d1f69e3388bc4814e5825da890a774ae0e3460e");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeRuntimePromptsSourceSha256", "profileCompatibilitySourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 63)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 63, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves both published v62 identities while binding their merged source in v63", async () => {
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v63-identity.json", import.meta.url), "utf8"));
|
||||
for (const [name, checksum, changed] of [
|
||||
["copilot-profile-v62-identity.json", "faa4d1b35267b54e134a8e0c84586fe1196d8fad5525cb53aa195aae53ca6e83", ["agentProfileVersion", "profileCompatibilitySourceSha256", "semanticCompletionContractSourceSha256", "semanticContinuationSourceSha256"]],
|
||||
["copilot-profile-v62-candidate-historical-identity.json", "da3f44a3cfda31cbff558d618e1e9cb1cfb80379b613ff1266e8b866ea666df8", ["agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256"]],
|
||||
] as const) {
|
||||
const bytes = readFileSync(new URL(`../../../test/fixtures/${name}`, import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe(checksum);
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(changed);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
const sorted = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 62)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 62, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves v61 while binding uncertain process checks and controller retirement in v62", async () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v61-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("ec3c8561bc123a599a01da356978b73e6d48ab030842af9f0d63d1f30413e1a5");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v62-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:956276ebee59f880249fa6955adc224af9ca7b6a9da59bcc5da4a8667550c5b3");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 61)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 61, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v60 while binding controller journal retirement in v61", async () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v60-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("e3c45b0311517d05f06ed10f357db713ae157cfa87e4a8becf087c5b6dfa7003");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v61-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 60)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 60, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v59 while binding the Codex-only hosted search restriction in v60", async () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v59-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("a9116d0981cb0feebec31f61cd23d16b3a263fe6a797b1ba1436c6e41198538c");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v60-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeBackendSourceSha256", "nativeExecutionSourceSha256", "profileCompatibilitySourceSha256", "providerLaunchSourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 59)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 59, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v58 while binding the separate remote process reconnect policy in v59", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v58-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v59-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "credentialBindingSourceSha256", "nativeInstructionsSourceSha256", "nativeSessionRuntimeSourceSha256", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256", "semanticBridgeSourceSha256", "semanticSidecarSourceSha256", "transportContractSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 58)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v57 while binding the shared Pi setup and launch repairs in v58", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v57-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v58-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "guardedInstallationSourceSha256", "nativeBackendSourceSha256", "profileCompatibilitySourceSha256", "profileInstallationSourceSha256", "providerLaunchSourceSha256", "runtimeHostSourceSha256", "runtimeRequestBridgeSourceSha256", "semanticBridgeSourceSha256", "semanticSidecarSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 57)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v56 while binding the extracted core prompts in v57", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v56-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v57-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeRuntimeContextSourceSha256", "nativeRuntimePromptsSourceSha256", "profileCompatibilitySourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 56)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v55 while binding reusable case workflow guidance in v56", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v55-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v56-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeRuntimeContextSourceSha256", "profileCompatibilitySourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 55)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v54 while binding current workspace continuation revalidation in v55", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v54-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v55-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:171d865b9b6ff2eda3926dc957caf2993cd7e1fa1b4359e8b60b83f589bd67fa");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
"semanticCompletionContractSourceSha256", "semanticContinuationSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 54)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v53 while binding repository setup completion guidance in v54", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v53-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v54-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:6441ff7f3fc664064bff9e7ae80252ddec7a39236f9a4b9d402785eb8b03b2c9");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "semanticCompletionContractSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 53)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v52 while binding the workspace continuation source in v53", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v52-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v53-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:3614db52d3ddcbc0153c79361a0db2c4324249475d3be490f152b64f916bdd53");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "semanticContinuationSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 52)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v51 when the setup transport contract changes", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v51-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:8c1cddce201eb0e3f2792cc5a8c79edf36b0ce823e728255c1897f95c74a3866");
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 51)).toBe(true);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v49 while binding the extracted provider launch builder", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v49-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v50-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:37a8f6a540a7417bb12becbf283158df8cca925c5136ecc0db14597441fff702");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "providerLaunchSourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 49)).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves qualified v48 while binding completion-proposal recovery in v49", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v48-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v49-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:49171cb881c43270e101a790a109752642870daedc38cd6e91aef08fd7aab038");
|
||||
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeSessionRuntimeSourceSha256", "profileCompatibilitySourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 48)).toBe(true);
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 48, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves qualified v47 while binding the additive case contracts in v48", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v47-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v48-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:55044aa235e88b3fe36ec8a556ee1e3382590870aab907e4462ef7d64af49cea");
|
||||
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeRuntimeContextSourceSha256", "profileCompatibilitySourceSha256",
|
||||
"semanticCompletionContractSourceSha256", "semanticSchemaBundleSourceSha256", "semanticValidatorsSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 47)).toBe(true);
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 47, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves v12 history and changes only the version and four shared source bindings", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8"));
|
||||
const changed = Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort();
|
||||
expect(changed).toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(prior.declaration.agentProfileVersion).toBe(12);
|
||||
expect(current.declaration.agentProfileVersion).toBe(13);
|
||||
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v13 history while binding the shared live request snapshot", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSidecarSourceSha256"]);
|
||||
});
|
||||
|
||||
it("preserves v14 history while binding the corrected outbound ACPX delivery", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]);
|
||||
});
|
||||
|
||||
it("preserves v17 history while binding the shared Grok descriptor schema", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSchemaBundleSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves the historical Muse v18 declaration without granting current launch", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-muse-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "semanticSchemaBundleSourceSha256", "semanticValidatorsSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(prior.declaration.agentProfileVersion).toBe(17);
|
||||
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
for (const override of [
|
||||
{ commandDigest: prior.commandDigest },
|
||||
{ agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
{ commandDigest: prior.commandDigest, agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
]) {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves the historical combined external-provider v19 declaration without granting current launch", async () => {
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v19-muse-historical-identity.json", import.meta.url), "utf8"));
|
||||
expect(current.declaration.agentProfileVersion).toBe(19);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 18)).toBe(true);
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
for (const [name, changed] of [
|
||||
["copilot-profile-v18-identity.json", ["agentProfileVersion", "semanticSchemaBundleSourceSha256", "semanticValidatorsSourceSha256"]],
|
||||
["copilot-profile-v18-muse-historical-identity.json", ["agentProfileVersion", "semanticSchemaBundleSourceSha256"]],
|
||||
] as const) {
|
||||
const prior = JSON.parse(readFileSync(new URL(`../../../test/fixtures/${name}`, import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(changed);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
for (const override of [
|
||||
{ commandDigest: prior.commandDigest },
|
||||
{ agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
{ commandDigest: prior.commandDigest, agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
]) {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
}
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves v18 history while binding the credential update protocol sources", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v19-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "semanticSchemaBundleSourceSha256", "semanticValidationSourceSha256", "semanticValidatorsSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
it("binds both immutable v19 histories in a new combined v20 declaration", async () => {
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v20-identity.json", import.meta.url), "utf8"));
|
||||
expect(current.declaration.agentProfileVersion).toBe(20);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 19)).toBe(true);
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
for (const [name, changed] of [
|
||||
["copilot-profile-v19-identity.json", ["agentProfileVersion", "semanticSchemaBundleSourceSha256", "semanticValidatorsSourceSha256"]],
|
||||
["copilot-profile-v19-muse-historical-identity.json", ["agentProfileVersion", "semanticSchemaBundleSourceSha256", "semanticValidationSourceSha256", "semanticValidatorsSourceSha256"]],
|
||||
] as const) {
|
||||
const prior = JSON.parse(readFileSync(new URL(`../../../test/fixtures/${name}`, import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(changed);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
for (const override of [
|
||||
{ commandDigest: prior.commandDigest },
|
||||
{ agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
{ commandDigest: prior.commandDigest, agentProfileVersion: prior.declaration.agentProfileVersion },
|
||||
]) {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
}
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves v20 history while binding the controller-owned monitor continuation", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v20-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v21-mainline-monitor-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "semanticCompletionContractSourceSha256", "semanticContinuationSourceSha256", "semanticDirectDriverSourceSha256", "semanticSidecarSourceSha256",
|
||||
]);
|
||||
expect(Object.keys(current.declaration).filter(key => !Object.hasOwn(prior.declaration, key))).toEqual(["semanticContinuationSourceSha256"]);
|
||||
expect(Object.keys(prior.declaration).every(key => Object.hasOwn(current.declaration, key))).toBe(true);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 20)).toBe(true);
|
||||
const sorted = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 20, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v22 history while binding the Claude-only preload metadata branch", async () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v22-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v23-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "semanticBridgeSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 22)).toBe(true);
|
||||
const sorted = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: 22, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
|
||||
it("preserves v23 history while binding shared observability diagnostic redaction", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v23-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v24-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "semanticSidecarSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 23)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v85 while binding runtime activity delivery in v86", () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v85-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("15d2790d94d7cc46ac728119ed9b4d07ed2b682be039b3debc716600708d8c1a");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v86-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "nativeSessionRuntimeSourceSha256", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256", "runtimeRequestFacadeSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 85)).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves v86 while binding activity attachment settlement in v87", () => {
|
||||
const priorBytes = readFileSync(new URL("../../../test/fixtures/copilot-profile-v86-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(priorBytes).digest("hex")).toBe("6baeb98adc7132b2afdce8b1d2da1df2f00356677eb4497c49c05aa9def79138");
|
||||
const prior = JSON.parse(priorBytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v87-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "harnessSessionSourceSha256", "profileCompatibilitySourceSha256", "runtimeRequestFacadeSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 86)).toBe(true);
|
||||
});
|
||||
|
||||
it("pins the unchanged executable plus complete patched distribution on every target platform", () => {
|
||||
const fixture = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-message-identity-distributions-1.0.88.json", import.meta.url), "utf8"));
|
||||
const script = readFileSync(new URL("../../../scripts/materialize-copilot-binary.mjs", import.meta.url), "utf8");
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
|
||||
import { verifyCopilotInstallation } from "./copilot-installation.js";
|
||||
import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
|
||||
|
||||
const bytes = (name: string) => readFileSync(new URL(`../../../test/fixtures/${name}`, import.meta.url));
|
||||
|
||||
describe("published identities after the Cursor Cloud qualification merge", () => {
|
||||
it.each([
|
||||
[
|
||||
"copilot-profile-v72-identity.json",
|
||||
"8d10c689a1707e06a5bfc93b37509b9f0b203586ee58c26cb780ab7057081fe7"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v73-identity.json",
|
||||
"ca8256cf778983db0561a0093879c334ae225cc88b75cc25b42459c69e04223f"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v74-identity.json",
|
||||
"2bfb5dc2f7ddac6dcd9229f155706c0f85da55e946e8c1ab60babced3e94201c"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v81-identity.json",
|
||||
"8b9d6b0e7ba3bb34141275f1a62701c3b69e1e917260d3c9984fbaeba6d413b1"
|
||||
],
|
||||
[
|
||||
"grok-profile-v8-identity.json",
|
||||
"164c4e5bf1cd9696c5935fb0010348e575f05c421b4af5496799a7878735bfd9"
|
||||
],
|
||||
[
|
||||
"grok-profile-v12-identity.json",
|
||||
"f43ac83f80fde04d8f184529502d3efc0df1b870d0ac8f863a183ae48eb6b004"
|
||||
]
|
||||
] as const)("keeps published %s byte-identical", (name, checksum) => {
|
||||
expect(createHash("sha256").update(bytes(name)).digest("hex")).toBe(checksum);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch-only Cursor Cloud identities from 157d4119db48eff9de040d3951755e58dcbd1f0a", () => {
|
||||
it.each([
|
||||
[
|
||||
"copilot-profile-v72-cursor-cloud-candidate-historical-identity.json",
|
||||
"e119e6206f4450e7d6b79b4a1522215f9848419dbc0b46cae877d7a5ae600b69"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v73-cursor-cloud-candidate-historical-identity.json",
|
||||
"7e6d7841267f1cbc4117ab9a3cea7c1fb0ffe043b63ff640e3aa97859bd4ebb6"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v74-cursor-cloud-candidate-historical-identity.json",
|
||||
"d3868e9fc601e3efb85e1c6da651d920e4f08efd572075ed29e4bbfd9c3d223b"
|
||||
],
|
||||
[
|
||||
"grok-profile-v8-cursor-cloud-candidate-historical-identity.json",
|
||||
"be0f07888a618794a2fe42e8894f42e2ac02d2c2c6a361560ae3d2b829b4c706"
|
||||
]
|
||||
] as const)("retains %s as qualification history", async (name, checksum) => {
|
||||
const retained = bytes(name);
|
||||
expect(createHash("sha256").update(retained).digest("hex")).toBe(checksum);
|
||||
const identity = JSON.parse(retained.toString("utf8"));
|
||||
const agent = name.startsWith("copilot") ? "copilot" : "grok";
|
||||
expect(isSupportedAcpxProfileVersion(agent, identity.declaration.agentProfileVersion)).toBe(true);
|
||||
expect(identity.commandDigest).not.toBe(QUALIFIED_ACPX_PROFILES[agent].commandDigest);
|
||||
if (agent === "copilot") {
|
||||
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(identity.commandDigest);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: identity.declaration.agentProfileVersion, commandDigest: identity.commandDigest })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("retained Cursor Cloud core profile before semantic planning", () => {
|
||||
it("keeps version 82 byte-identical and decodable without admitting its old launch identity", async () => {
|
||||
const retained = bytes("copilot-profile-v82-cursor-cloud-candidate-historical-identity.json");
|
||||
expect(createHash("sha256").update(retained).digest("hex")).toBe("350564aceac59083b3009c5915c709549f0a257b0d828314184c28cded33e6a6");
|
||||
const identity = JSON.parse(retained.toString("utf8"));
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 82)).toBe(true);
|
||||
expect(identity.commandDigest).not.toBe(QUALIFIED_ACPX_PROFILES.copilot.commandDigest);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, agentProfileVersion: 82, commandDigest: identity.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("published identities from pinned master 54cc522e864291564bc37ddc6004bfa558c02644", () => {
|
||||
it.each([
|
||||
[
|
||||
"copilot-profile-v82-identity.json",
|
||||
"fe04407493a608d4204403d04ef06c79904a206f3c57547a11612c068a55b99f"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v83-identity.json",
|
||||
"31d024e3870f8a7278939f6f1186e2ec6d2bc331d7a89213aceae88c04b40654"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v84-identity.json",
|
||||
"6fe83e309911eb83f8b6e817f0b535329b02fab41ef7749d363f4562af19d4fe"
|
||||
],
|
||||
[
|
||||
"grok-profile-v13-identity.json",
|
||||
"374993b170ea181470f6bc606ec568eb347f987e65338d8b980356d97c97b98b"
|
||||
]
|
||||
] as const)("preserves published %s byte-identical", (name, checksum) => {
|
||||
expect(createHash("sha256").update(bytes(name)).digest("hex")).toBe(checksum);
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualified branch-only identities from 0d968b005be9063b8c3806ecef5f75239dc5ce5b", () => {
|
||||
it.each([
|
||||
[
|
||||
"copilot-profile-v82-cursor-cloud-candidate-historical-identity.json",
|
||||
"350564aceac59083b3009c5915c709549f0a257b0d828314184c28cded33e6a6"
|
||||
],
|
||||
[
|
||||
"copilot-profile-v83-cursor-cloud-candidate-historical-identity.json",
|
||||
"9b4fa60409fc8e0f85ec61ed6378188ccaeb806684aea3b05ae6cd0c451bb364"
|
||||
],
|
||||
[
|
||||
"grok-profile-v13-cursor-cloud-candidate-historical-identity.json",
|
||||
"2db8e1be2c019e88cf089c2ae344c48d5e67c67b2ff72b2e3a21ff9657306261"
|
||||
]
|
||||
] as const)("retains %s as read-only qualification history", async (name, checksum) => {
|
||||
const retained = bytes(name);
|
||||
expect(createHash("sha256").update(retained).digest("hex")).toBe(checksum);
|
||||
const identity = JSON.parse(retained.toString("utf8"));
|
||||
const agent = name.startsWith("copilot") ? "copilot" : "grok";
|
||||
expect(isSupportedAcpxProfileVersion(agent, identity.declaration.agentProfileVersion)).toBe(true);
|
||||
expect(identity.commandDigest).not.toBe(QUALIFIED_ACPX_PROFILES[agent].commandDigest);
|
||||
if (agent === "copilot") {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
|
||||
agentProfileVersion: identity.declaration.agentProfileVersion, commandDigest: identity.commandDigest })).rejects.toThrow("exact pinned");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("published Core041 profile history", () => {
|
||||
it("retains v85 bytes without admitting its stale shared bridge", async () => {
|
||||
const retained = bytes("copilot-profile-v85-cursor-cloud-qualification-historical-identity.json");
|
||||
expect(createHash("sha256").update(retained).digest("hex")).toBe("977d68b327f3964e8337ef8ba00230eb1b1697017db21f8010f664fee3d3c70a");
|
||||
const identity = JSON.parse(retained.toString("utf8"));
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 85)).toBe(true);
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, agentProfileVersion: 85, commandDigest: identity.commandDigest })).rejects.toThrow("exact pinned");
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
|
||||
@@ -23,6 +25,75 @@ describe("ACPX profile versions", () => {
|
||||
});
|
||||
|
||||
describe("historical ACPX profile decoding", () => {
|
||||
it("keeps both reconciled historical lineages immutable and decode-only", () => {
|
||||
const lineage = JSON.parse(readFileSync(new URL("../../../test/fixtures/profile-history-lineage.json", import.meta.url), "utf8")) as {
|
||||
schema: string; historicalDecodeOnly: boolean; executionRequiresExactCurrentProfile: boolean;
|
||||
records: Array<{ path: string; originRevision: string; sha256: string; agent: keyof typeof QUALIFIED_ACPX_PROFILE_DATA;
|
||||
version: number; commandDigest: string; launchAuthority: boolean }>;
|
||||
};
|
||||
expect(lineage.schema).toBe("paperclip.acpx-profile-history-lineage.v1");
|
||||
expect(lineage.historicalDecodeOnly).toBe(true);
|
||||
expect(lineage.executionRequiresExactCurrentProfile).toBe(true);
|
||||
const reconciled = lineage.records.filter(record => [
|
||||
"42e9d3d710a2ef5b387f9bb29bf615973b4adba9", "59da15066acd2077b1161b06217acfbd11f8da04",
|
||||
].includes(record.originRevision));
|
||||
expect(reconciled).toHaveLength(6);
|
||||
const paths = new Set<string>();
|
||||
for (const record of reconciled) {
|
||||
expect(paths.has(record.path), record.path).toBe(false);
|
||||
paths.add(record.path);
|
||||
expect(record.originRevision, record.path).toMatch(/^[a-f0-9]{40}$/u);
|
||||
expect(record.launchAuthority, record.path).toBe(false);
|
||||
const bytes = readFileSync(new URL(`../../../../../${record.path}`, import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex"), record.path).toBe(record.sha256);
|
||||
const identity = JSON.parse(bytes.toString("utf8"));
|
||||
expect(identity.declaration.agent, record.path).toBe(record.agent);
|
||||
expect(identity.declaration.agentProfileVersion, record.path).toBe(record.version);
|
||||
expect(identity.commandDigest, record.path).toBe(record.commandDigest);
|
||||
expect(isSupportedAcpxProfileVersion(record.agent, record.version), record.path).toBe(true);
|
||||
expect(identity.commandDigest, record.path).not.toBe(QUALIFIED_ACPX_PROFILE_DATA[record.agent].commandDigest);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["copilot", "copilot-profile-v88-identity.json", "81e5fbccf057c04e012f638dc7a02c5758bf6ca67f7c4ed0045f3f61525401c2", "published cd5"],
|
||||
["copilot", "copilot-profile-v88-cursor-cloud-qualification-historical-identity.json", "6010dc772d914afec7dd31f35f953ce74b65ab3a648ce54aaee559f908b492f9", "private Cloud qualification"],
|
||||
["grok", "grok-profile-v15-identity.json", "54f7063c82dc5b135a9b5e2b0d8653b5c839a42bbc3a845dbcfb6cd225fa623f", "published cd5"],
|
||||
["grok", "grok-profile-v15-cursor-cloud-qualification-historical-identity.json", "f5d4b41284d60413575f88371e66e940942c256d8b6d9619104deaee181054b2", "private Cloud qualification"],
|
||||
["copilot", "copilot-profile-v86-identity.json", "6baeb98adc7132b2afdce8b1d2da1df2f00356677eb4497c49c05aa9def79138", "published a8"],
|
||||
["copilot", "copilot-profile-v86-cursor-cloud-qualification-historical-identity.json", "118a4b0a782b56570e5530cca91255da8932c31f827c319b40a5810bd4d446e9", "private Cloud qualification"],
|
||||
["copilot", "copilot-profile-v87-identity.json", "505bb72a933d11e3f5aa8a1700d239c06b4d34bb83d5187470483812df996501", "published a8"],
|
||||
["copilot", "copilot-profile-v87-cursor-cloud-qualification-historical-identity.json", "ce9912c34ae9126e74061f7bc170b59589af7cb6ab26751e250da9e9dd9553c9", "private Cloud qualification"],
|
||||
["copilot", "copilot-profile-v85-identity.json", "15d2790d94d7cc46ac728119ed9b4d07ed2b682be039b3debc716600708d8c1a", "published ed8"],
|
||||
["copilot", "copilot-profile-v85-cursor-cloud-qualification-historical-identity.json", "977d68b327f3964e8337ef8ba00230eb1b1697017db21f8010f664fee3d3c70a", "private Cloud qualification"],
|
||||
["grok", "grok-profile-v14-identity.json", "08294ec6bdcbdbe46c8c8b711d8b368558a05f0d84d25c9f5fd0c3b67b79cd17", "published ed8"],
|
||||
["grok", "grok-profile-v14-cursor-cloud-qualification-historical-identity.json", "fa34ba395ff51affed7e7195dc44f4e33a3c6718ada1c80e8e36852cf7af40ac", "private Cloud qualification"],
|
||||
] as const)("keeps %s identity %s immutable and decode-only", (agent, filename, checksum) => {
|
||||
const bytes = readFileSync(new URL(`../../../test/fixtures/${filename}`, import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe(checksum);
|
||||
const identity = JSON.parse(bytes.toString("utf8"));
|
||||
expect(isSupportedAcpxProfileVersion(agent, identity.declaration.agentProfileVersion)).toBe(true);
|
||||
expect(identity.commandDigest).not.toBe(QUALIFIED_ACPX_PROFILE_DATA[agent].commandDigest);
|
||||
});
|
||||
|
||||
it("decodes every retained Copilot identity so saved runs can reach recovery", () => {
|
||||
const root = new URL("../../../test/fixtures/", import.meta.url);
|
||||
const files = readdirSync(root).filter(name => /^copilot-profile-v\d+.*-identity\.json$/u.test(name));
|
||||
expect(files.length).toBeGreaterThan(30);
|
||||
for (const file of files) {
|
||||
const identity = JSON.parse(readFileSync(new URL(file, root), "utf8"));
|
||||
expect(isSupportedAcpxProfileVersion("copilot", identity.declaration.agentProfileVersion), file).toBe(true);
|
||||
}
|
||||
});
|
||||
it("decodes every retained Grok identity so saved runs can reach recovery", () => {
|
||||
const root = new URL("../../../test/fixtures/", import.meta.url);
|
||||
const files = readdirSync(root).filter(name => /^grok-profile-v\d+.*-identity\.json$/u.test(name));
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
for (const file of files) {
|
||||
const identity = JSON.parse(readFileSync(new URL(file, root), "utf8"));
|
||||
expect(isSupportedAcpxProfileVersion("grok", identity.declaration.agentProfileVersion), file).toBe(true);
|
||||
}
|
||||
});
|
||||
it("decodes every revision from 1 through the current one so saved runs reach recovery", () => {
|
||||
for (const [agent, current] of Object.entries(QUALIFIED_ACPX_PROFILE_DATA)) {
|
||||
for (let version = 1; version <= current.agentProfileVersion; version += 1) {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { request } from "node:http";
|
||||
import { CursorCloudCheckpointStore } from "./checkpoint.js";
|
||||
import { RUNNER_TOOL_SCOPE_TOKEN_HEADER } from "../runner-tool-bridge.js";
|
||||
import { CursorCloudDriver, type CursorCloudSessionHandle } from "./cursor-cloud-driver.js";
|
||||
import { startCursorCloudAppServerProxy } from "../../cli/cursor-cloud-app-server-proxy.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const proxies: Array<ReturnType<typeof startCursorCloudAppServerProxy>> = [];
|
||||
afterEach(async () => {
|
||||
for (const proxy of proxies.splice(0)) await proxy.shutdown();
|
||||
vi.restoreAllMocks();
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
async function peer() {
|
||||
const root = await mkdtemp("/tmp/cc-proxy-"); roots.push(root);
|
||||
const configPath = join(root, "config.json");
|
||||
await writeFile(configPath, JSON.stringify({ schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/repo" }], binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId: "session" } }), { mode: 0o600 });
|
||||
const input = new PassThrough(); const output = new PassThrough(); const diagnostic = new PassThrough();
|
||||
const frames: any[] = []; let buffer = "";
|
||||
output.on("data", chunk => { buffer += chunk.toString(); let index; while ((index = buffer.indexOf("\n")) >= 0) { frames.push(JSON.parse(buffer.slice(0, index))); buffer = buffer.slice(index + 1); } });
|
||||
const proxy = startCursorCloudAppServerProxy({ input, output, diagnostic, environment: { CURSOR_API_KEY: "private-key", PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH: configPath, PAPERCLIP_NORMALIZED_SESSION_ID: "session", PAPERCLIP_RUN_ID: "run", PAPERCLIP_CURSOR_CLOUD_RELAY_URL: "https://relay.example.test/mcp", PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN: "private-relay-token", PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH: join(root, "mcp.sock"), PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 600_000) } });
|
||||
proxies.push(proxy);
|
||||
async function rpc(id: number, method: string, params: unknown = {}) { input.write(`${JSON.stringify({ id, method, params })}\n`); await vi.waitFor(() => expect(frames.some(frame => frame.id === id)).toBe(true), { timeout: 5_000 }); return frames.find(frame => frame.id === id); }
|
||||
return { root, input, output, frames, proxy, rpc, socketPath: join(root, "mcp.sock") };
|
||||
}
|
||||
function mcp(socketPath: string, body: Record<string, unknown>, headers: Record<string, string>, sessionId?: string): Promise<{ headers: Record<string, unknown>; value: any }> {
|
||||
return new Promise((accept, reject) => {
|
||||
const req = request({ socketPath, path: "/mcp", method: "POST", headers: { ...headers, "content-type": "application/json", ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) } }, response => {
|
||||
let text = ""; response.on("data", chunk => { text += chunk; }); response.on("end", () => { try { accept({ headers: response.headers, value: JSON.parse(text) }); } catch (error) { reject(error); } });
|
||||
});
|
||||
req.on("error", reject); req.end(JSON.stringify({ jsonrpc: "2.0", ...body }));
|
||||
});
|
||||
}
|
||||
describe("Cursor Cloud bounded stdio proxy", () => {
|
||||
it("initializes/opens without provider work and returns an explicit empty remote shutdown receipt", async () => {
|
||||
const nativeFetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("unexpected paid call"));
|
||||
const f = await peer();
|
||||
try {
|
||||
expect(await f.rpc(1, "initialize")).toMatchObject({ result: { serverInfo: { name: "cursor-cloud" } } });
|
||||
const opened = await f.rpc(2, "thread/start", { model: "explicit-model", cwd: f.root, baseInstructions: "Exact instructions" });
|
||||
expect(opened).toMatchObject({ result: { thread: { id: "session" }, submittedModel: "explicit-model", effectiveModelAcknowledged: false, instructionsAcknowledged: false } });
|
||||
expect(opened.result).not.toHaveProperty("model"); expect(opened.result.thread).not.toHaveProperty("model");
|
||||
expect(await f.rpc(3, "turn/start", { input: [{ type: "image", url: "https://example.com/image.png" }] })).toHaveProperty("error");
|
||||
for (const [id, override] of [[4, { collaborationMode: { mode: "plan" } }], [5, { reasoningMode: "high" }]] as const) {
|
||||
expect(await f.rpc(id, "turn/start", { ...override, input: [{ type: "text", text: "Create the plan." }] }))
|
||||
.toMatchObject({ error: { message: "Cursor Cloud native plan/reasoning override is unsupported" } });
|
||||
}
|
||||
expect(await f.rpc(6, "collaborationMode/list")).toMatchObject({ result: { data: [{ mode: "default" }] } });
|
||||
expect(await f.rpc(7, "paperclip/shutdown", { threadId: "session", turnId: null })).toMatchObject({ result: { remoteStopped: true, threadId: "session", turnId: null, remoteRunId: null } });
|
||||
expect(nativeFetch).not.toHaveBeenCalled();
|
||||
} finally { f.input.end(); await f.proxy.done; }
|
||||
});
|
||||
it("bounds an unterminated input frame and finishes instead of retaining unbounded input", async () => {
|
||||
const f = await peer(); f.input.write("x".repeat(2_097_153)); await f.proxy.done;
|
||||
expect(f.frames).toEqual([]);
|
||||
});
|
||||
it("preserves native tool identity, status and bounded redacted output without assuming MCP transport", async () => {
|
||||
const remoteRunId = "run-22222222-2222-4222-8222-222222222222";
|
||||
const now = "2026-10-28T00:00:00Z";
|
||||
let remoteAgentId = "";
|
||||
const json = (value: unknown) => new Response(JSON.stringify(value), { headers: { "content-type": "application/json" } });
|
||||
const nativeFetch = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
|
||||
const target = new URL(String(url));
|
||||
expect(target.origin).toBe("https://api.cursor.com");
|
||||
if (target.pathname === "/v1/agents" && init?.method === "POST") {
|
||||
remoteAgentId = JSON.parse(String(init.body)).agentId;
|
||||
return json({ agent: { id: remoteAgentId, status: "ACTIVE", env: { type: "cloud" }, url: `https://cursor.com/agents/${remoteAgentId}`, createdAt: now, updatedAt: now, latestRunId: remoteRunId }, run: { id: remoteRunId, agentId: remoteAgentId, status: "RUNNING", createdAt: now, updatedAt: now } });
|
||||
}
|
||||
if (target.pathname.endsWith("/stream")) {
|
||||
const records = [
|
||||
{ callId: "native-discovery", name: "get_mcp_tools", status: "running", args: { toolCallId: "synthetic-private-argument" }, result: { error: "native failure private-key private-relay-token" } },
|
||||
{ callId: "native-discovery", name: "get_mcp_tools", status: "error", args: { toolCallId: "synthetic-private-argument" }, result: { error: "native failure private-key private-relay-token" } },
|
||||
{ callId: "native-read", name: "read_file", status: "completed", args: { path: "synthetic-private-path" }, result: "x".repeat(65_536) },
|
||||
];
|
||||
const body = records.map((data, index) => `id: ${index + 1}\nevent: tool_call\ndata: ${JSON.stringify(data)}\n\n`).join("")
|
||||
+ `id: 4\nevent: result\ndata: ${JSON.stringify({ runId: remoteRunId, status: "FINISHED" })}\n\n`;
|
||||
return new Response(body, { headers: { "content-type": "text/event-stream" } });
|
||||
}
|
||||
if (target.pathname.endsWith(`/${remoteRunId}`)) return json({ id: remoteRunId, agentId: remoteAgentId, status: "FINISHED", createdAt: now, updatedAt: now });
|
||||
throw new Error("Unexpected controlled provider request");
|
||||
});
|
||||
let releaseTerminalWrites!: () => void;
|
||||
const terminalGate = new Promise<void>(resolve => { releaseTerminalWrites = resolve; });
|
||||
let pausedTerminalWrites = 0;
|
||||
const write = CursorCloudCheckpointStore.prototype.write;
|
||||
vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(value) {
|
||||
if (value.phase === "terminal" && value.pendingTerminalDelivery) {
|
||||
pausedTerminalWrites += 1;
|
||||
await terminalGate;
|
||||
}
|
||||
await write.call(this, value);
|
||||
});
|
||||
const f = await peer();
|
||||
try {
|
||||
await f.rpc(1, "initialize");
|
||||
expect(await f.rpc(2, "thread/start", { model: "explicit-model", cwd: f.root })).toMatchObject({ result: { thread: { id: "session" } } });
|
||||
await f.rpc(3, "turn/start", { input: [{ type: "text", text: "Inspect native tool event delivery" }] });
|
||||
await vi.waitFor(() => expect(f.frames.filter(frame => frame.params?.item?.name === "get_mcp_tools" || frame.params?.item?.name === "read_file")).toHaveLength(3), { timeout: 1_000 });
|
||||
await vi.waitFor(() => expect(pausedTerminalWrites).toBe(1), { timeout: 5_000 });
|
||||
const pendingRead = f.rpc(9, "thread/read");
|
||||
await new Promise<void>(accept => setImmediate(accept));
|
||||
expect(f.frames.some(frame => frame.id === 9)).toBe(false);
|
||||
const checkpointPath = join(f.root, (await readdir(f.root)).find(name => /^checkpoint-.*\.json$/.test(name))!);
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8")).phase).toBe("running");
|
||||
releaseTerminalWrites();
|
||||
const committed = await pendingRead;
|
||||
expect(committed.result.cursorCloudTerminalDelivery).toEqual(JSON.parse(await readFile(checkpointPath, "utf8")).pendingTerminalDelivery);
|
||||
await vi.waitFor(() => expect(f.frames.some(frame => frame.method === "turn/completed")).toBe(true), { timeout: 5_000 });
|
||||
const tools = f.frames.filter(frame => frame.params?.item?.name === "get_mcp_tools" || frame.params?.item?.name === "read_file");
|
||||
expect(tools.map(frame => ({ method: frame.method, type: frame.params.item.type, tool: frame.params.item.tool, status: frame.params.item.status }))).toEqual([
|
||||
{ method: "item/started", type: "builtinToolCall", tool: "get_mcp_tools", status: "running" },
|
||||
{ method: "item/completed", type: "builtinToolCall", tool: "get_mcp_tools", status: "error" },
|
||||
{ method: "item/completed", type: "builtinToolCall", tool: "read_file", status: "completed" },
|
||||
]);
|
||||
expect(JSON.parse(tools[0].params.item.output)).toEqual({ error: "native failure [redacted] [redacted]" });
|
||||
expect(JSON.parse(tools[1].params.item.output)).toEqual({ error: "native failure [redacted] [redacted]" });
|
||||
expect(JSON.parse(tools[2].params.item.output)).toEqual({ omitted: true, reason: "payload_limit" });
|
||||
expect(JSON.stringify(tools)).not.toContain("private-key");
|
||||
expect(JSON.stringify(tools)).not.toContain("private-relay-token");
|
||||
const retained = await f.rpc(4, "thread/read");
|
||||
expect(retained.result.thread.turns).toEqual([]);
|
||||
expect(retained.result.cursorCloudTerminalDelivery).toMatchObject({ status: "FINISHED", receiptId: expect.any(String) });
|
||||
const receipt = retained.result.cursorCloudTerminalDelivery;
|
||||
expect(await f.rpc(10, "paperclip/shutdown")).toMatchObject({ result: { cursorCloudTerminalDelivery: retained.result.cursorCloudTerminalDelivery } });
|
||||
expect(await f.rpc(5, "paperclip/ackTerminal", { turnId: receipt.turnId, receiptId: "wrong" })).toHaveProperty("error");
|
||||
expect(await f.rpc(6, "paperclip/ackTerminal", { turnId: receipt.turnId, receiptId: receipt.receiptId })).toMatchObject({ result: { acknowledged: true } });
|
||||
expect((await f.rpc(7, "thread/read")).result.cursorCloudTerminalDelivery).toBeNull();
|
||||
expect(await f.rpc(8, "paperclip/shutdown", { turnId: null })).toMatchObject({ result: { remoteStopped: true, turnId: null } });
|
||||
expect(nativeFetch.mock.calls.filter(([url, init]) => new URL(String(url)).pathname === "/v1/agents" && init?.method === "POST")).toHaveLength(1);
|
||||
} finally { releaseTerminalWrites(); f.input.end(); await f.proxy.done; }
|
||||
});
|
||||
it("round-trips a semantic completion over stdio and certifies shutdown only after native terminal lookup", async () => {
|
||||
const remoteRunId = "run-22222222-2222-4222-8222-222222222222";
|
||||
let remoteAgentId = ""; let status = "RUNNING"; let stream: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
let mcpHeaders!: Record<string, string>;
|
||||
const now = "2026-10-28T00:00:00Z";
|
||||
const json = (value: unknown) => new Response(JSON.stringify(value), { headers: { "content-type": "application/json" } });
|
||||
const nativeFetch = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
|
||||
const path = new URL(String(url)).pathname;
|
||||
if (path === "/v1/agents" && init?.method === "POST") {
|
||||
const body = JSON.parse(String(init.body)); remoteAgentId = body.agentId; mcpHeaders = body.mcpServers[0].headers;
|
||||
return json({ agent: { id: remoteAgentId, status: "ACTIVE", env: { type: "cloud" }, url: `https://cursor.com/agents/${remoteAgentId}`, createdAt: now, updatedAt: now, latestRunId: remoteRunId }, run: { id: remoteRunId, agentId: remoteAgentId, status, createdAt: now, updatedAt: now } });
|
||||
}
|
||||
if (path.endsWith("/stream")) return new Response(new ReadableStream<Uint8Array>({ start(controller) { stream = controller; controller.enqueue(new TextEncoder().encode(`id: 1\nevent: assistant\ndata: {"text":"Working"}\n\n`)); } }), { headers: { "content-type": "text/event-stream" } });
|
||||
if (path.endsWith(`/${remoteRunId}`)) return json({ id: remoteRunId, agentId: remoteAgentId, status, createdAt: now, updatedAt: now });
|
||||
throw new Error("Unexpected controlled provider request");
|
||||
});
|
||||
let openedSession!: CursorCloudSessionHandle;
|
||||
const openSession = CursorCloudDriver.prototype.openSession;
|
||||
vi.spyOn(CursorCloudDriver.prototype, "openSession").mockImplementation(async function(input) {
|
||||
openedSession = await openSession.call(this, input);
|
||||
return openedSession;
|
||||
});
|
||||
const f = await peer();
|
||||
try {
|
||||
await f.rpc(1, "initialize"); await f.rpc(2, "thread/start", { model: "explicit-model", cwd: f.root, baseInstructions: "Use semantic completion." });
|
||||
const started = await f.rpc(3, "turn/start", { input: [{ type: "text", text: "Do the task" }] });
|
||||
await vi.waitFor(() => expect(stream).toBeDefined(), { timeout: 5_000 });
|
||||
expect(mcpHeaders).toMatchObject({ Authorization: "Bearer private-relay-token", [RUNNER_TOOL_SCOPE_TOKEN_HEADER]: expect.stringMatching(/^Bearer [A-Za-z0-9_-]{43}$/) });
|
||||
await expect(mcp(f.socketPath, { id: -1, method: "initialize" }, { Authorization: "Bearer private-relay-token" })).rejects.toBeInstanceOf(SyntaxError);
|
||||
const initialized = await mcp(f.socketPath, { id: 0, method: "initialize" }, mcpHeaders);
|
||||
const input = { reportedWorkDisposition: "done", summary: "Completed.", evidence: [], verification: [], completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [], remainingWork: [] } };
|
||||
const response = mcp(f.socketPath, { id: 1, method: "tools/call", params: { name: "paperclip_finish", arguments: input } }, mcpHeaders, initialized.headers["mcp-session-id"] as string);
|
||||
await vi.waitFor(() => expect(f.frames.some(frame => frame.method === "item/tool/call")).toBe(true), { timeout: 5_000 });
|
||||
const call = f.frames.find(frame => frame.method === "item/tool/call");
|
||||
expect(call.params).toMatchObject({ threadId: "session", turnId: started.result.turn.id, tool: "paperclip_finish", arguments: input });
|
||||
f.input.write(`${JSON.stringify({ id: call.id, result: { success: true, contentItems: [{ type: "inputText", text: "Exact accepted completion receipt" }] } })}\n`);
|
||||
expect((await response).value.result.content[0].text).toContain("Exact accepted completion receipt");
|
||||
expect(f.frames.some(frame => frame.method === "paperclip/runResult" || frame.method === "turn/completed")).toBe(false);
|
||||
const snapshot = openedSession.snapshot.bind(openedSession);
|
||||
let settleDuringRead = true;
|
||||
vi.spyOn(openedSession, "snapshot").mockImplementation(async () => {
|
||||
const value = await snapshot();
|
||||
if (settleDuringRead) {
|
||||
settleDuringRead = false;
|
||||
status = "FINISHED"; stream!.enqueue(new TextEncoder().encode(`id: 2\nevent: result\ndata: ${JSON.stringify({ runId: remoteRunId, status })}\n\n`));
|
||||
await vi.waitFor(() => expect(f.frames.some(frame => frame.method === "turn/completed")).toBe(true), { timeout: 5_000 });
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const racing = await f.rpc(5, "thread/read");
|
||||
expect(Boolean(racing.result.thread.turns.length && racing.result.cursorCloudTerminalDelivery)).toBe(false);
|
||||
expect(await f.rpc(4, "paperclip/shutdown", { threadId: "session", turnId: started.result.turn.id })).toMatchObject({ result: { remoteStopped: true, threadId: "session", turnId: started.result.turn.id, remoteAgentId, remoteRunId, status: "FINISHED" } });
|
||||
expect(nativeFetch.mock.calls.filter(([url, init]) => new URL(String(url)).pathname === "/v1/agents" && init?.method === "POST")).toHaveLength(1);
|
||||
} finally { f.input.end(); await f.proxy.done; }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,523 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { constants } from "node:fs";
|
||||
import { link, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { stageCursorCloudArtifact } from "./artifact-delivery.js";
|
||||
import { reserveCursorCloudArtifactStaging } from "./artifact-staging-budget.js";
|
||||
|
||||
vi.mock("node:fs/promises", async importOriginal => {
|
||||
const original = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return { ...original, open: vi.fn(original.open), link: vi.fn(original.link), unlink: vi.fn(original.unlink) };
|
||||
});
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => { vi.restoreAllMocks(); for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); });
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>(done => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const root = await mkdtemp("/tmp/cc-artifact-"); roots.push(root);
|
||||
const bytes = Buffer.from("downloaded provider result\n"); const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const client = { listArtifacts: vi.fn(async () => ({ items: [{ path: "artifacts/result.txt", sizeBytes: bytes.length, updatedAt: "2026-10-28" }] })), downloadArtifact: vi.fn(async () => bytes) };
|
||||
const args = { contentRef: "cursor-artifact:artifacts/result.txt", filename: "result.txt", byteSize: bytes.length, sha256, contentType: "text/plain" };
|
||||
const controller = new AbortController(); const artifactRoot = join(root, "artifacts"); const directory = join(artifactRoot, sha256);
|
||||
const input = { arguments: args, agentId: "agent", runtimeDirectory: root, artifactRoot, client, signal: controller.signal };
|
||||
return { root, bytes, args, input, controller, directory, target: join(directory, args.filename), normalized: { ...args, contentRef: join(sha256, args.filename) } };
|
||||
}
|
||||
|
||||
async function postLinkWriter(f: Awaited<ReturnType<typeof fixture>>) {
|
||||
const script = join(f.root, `postlink-${randomUUID()}.mjs`);
|
||||
await writeFile(script, `
|
||||
import fs from "node:fs/promises";
|
||||
import { syncBuiltinESMExports } from "node:module";
|
||||
const original = fs.link;
|
||||
fs.link = async (...args) => {
|
||||
await original(...args); process.send({ name: "linked", temporary: args[0] });
|
||||
await new Promise(resolve => process.once("message", resolve));
|
||||
};
|
||||
syncBuiltinESMExports();
|
||||
const { stageCursorCloudArtifact } = await import(${JSON.stringify(new URL("./artifact-delivery.ts", import.meta.url).href)});
|
||||
const bytes = Buffer.from(${JSON.stringify(f.bytes.toString("base64"))}, "base64");
|
||||
const result = await stageCursorCloudArtifact({ arguments: ${JSON.stringify(f.args)}, agentId: "agent", runtimeDirectory: ${JSON.stringify(f.root)}, artifactRoot: ${JSON.stringify(f.input.artifactRoot)},
|
||||
client: { listArtifacts: async () => ({ items: [{ path: "artifacts/result.txt", sizeBytes: bytes.length }] }), downloadArtifact: async () => bytes }, signal: new AbortController().signal });
|
||||
process.send({ name: "completed", result }, () => process.disconnect());
|
||||
`, { mode: 0o600 });
|
||||
const child = spawn(process.execPath, ["--import", new URL("../../../node_modules/tsx/dist/loader.mjs", import.meta.url).pathname, script], { env: {}, stdio: ["ignore", "ignore", "pipe", "ipc"] });
|
||||
let stderr = ""; child.stderr?.on("data", bytes => { stderr = (stderr + bytes).slice(-4_096); });
|
||||
const exited = once(child, "exit");
|
||||
try {
|
||||
const message = await Promise.race([once(child, "message"), exited.then(() => { throw new Error(`postlink fixture exited early: ${stderr}`); })]);
|
||||
expect(message[0]).toMatchObject({ name: "linked" });
|
||||
const temporary = String(message[0].temporary);
|
||||
expect(await readFile(temporary)).toEqual(f.bytes);
|
||||
return { child, exited, temporary, async stop() {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||
await exited;
|
||||
} };
|
||||
} catch (error) {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||
await exited; throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function exitedPid() {
|
||||
const child = spawnSync(process.execPath, ["--input-type=module", "-e", ""], { env: {}, stdio: "ignore" });
|
||||
expect(child.status).toBe(0);
|
||||
expect(() => process.kill(child.pid, 0)).toThrowError(expect.objectContaining({ code: "ESRCH" }));
|
||||
return child.pid;
|
||||
}
|
||||
|
||||
function aliasPath(f: Awaited<ReturnType<typeof fixture>>, pid: number, filename = f.args.filename) {
|
||||
const targetDigest = createHash("sha256").update(filename).digest("hex");
|
||||
return join(f.directory, `.cursor-cloud-${targetDigest}-${pid}-${randomUUID()}-${randomUUID()}.tmp`);
|
||||
}
|
||||
|
||||
describe("Cursor Cloud artifact registration boundary", () => {
|
||||
it("recovers an exact published final at capacity without a new allocation", async () => {
|
||||
const f = await fixture(); const writer = await postLinkWriter(f);
|
||||
try {
|
||||
await writer.stop();
|
||||
for (let count = 1; count < 256; count++) await reserveCursorCloudArtifactStaging({ ...f.input, byteSize: 0 });
|
||||
const budgetPath = join(f.root, ".artifact-staging-budget", "usage.json");
|
||||
const before = await readFile(budgetPath, "utf8");
|
||||
expect(JSON.parse(before).allocations).toBe(256);
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
expect((await lstat(f.target)).nlink).toBe(1);
|
||||
expect(await readFile(budgetPath, "utf8")).toBe(before);
|
||||
expect(f.input.client.downloadArtifact).toHaveBeenCalledOnce();
|
||||
await expect(stageCursorCloudArtifact({ ...f.input, arguments: { ...f.args, filename: "over-budget.txt" } })).rejects.toThrow(/staging budget/);
|
||||
expect(f.input.client.downloadArtifact).toHaveBeenCalledOnce();
|
||||
} finally { await writer.stop(); }
|
||||
}, 20_000);
|
||||
|
||||
it("rejects changed current provider bytes despite a matching verified cached final", async () => {
|
||||
const f = await fixture(); await stageCursorCloudArtifact(f.input);
|
||||
f.input.client.downloadArtifact.mockResolvedValueOnce(Buffer.alloc(f.bytes.length, "x"));
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow("do not match");
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
});
|
||||
|
||||
it("refuses cached bytes that grow after their initial size observation", async () => {
|
||||
const f = await fixture(); await stageCursorCloudArtifact(f.input);
|
||||
f.input.client.downloadArtifact.mockClear();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
let injected = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
if (!injected && String(path) === f.target) {
|
||||
const stat = handle.stat.bind(handle);
|
||||
vi.spyOn(handle, "stat").mockImplementation(async () => {
|
||||
const before = await stat();
|
||||
if (!injected) { injected = true; await writeFile(f.target, Buffer.concat([f.bytes, Buffer.from("extra")])); }
|
||||
return before;
|
||||
});
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow("identity conflict");
|
||||
expect(f.input.client.downloadArtifact).not.toHaveBeenCalled();
|
||||
expect((await lstat(f.target)).size).toBe(f.bytes.length + 5);
|
||||
});
|
||||
|
||||
it("refuses a new download after legacy retained bytes reach the 256 MiB budget", async () => {
|
||||
const f = await fixture();
|
||||
const size = 16_777_216, hash = createHash("sha256").update(Buffer.alloc(size)).digest("hex");
|
||||
const directory = join(f.input.artifactRoot, hash); await mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
for (let index = 0; index < 16; index++) {
|
||||
const file = await open(join(directory, `legacy-${index}`), "wx", 0o600);
|
||||
try { await file.truncate(size); } finally { await file.close(); }
|
||||
}
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow(/staging budget/);
|
||||
expect(f.input.client.downloadArtifact).not.toHaveBeenCalled();
|
||||
await expect(lstat(f.target)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await readdir(directory)).toHaveLength(16);
|
||||
});
|
||||
|
||||
it("counts zero-byte retained files against the 256-allocation budget", async () => {
|
||||
const f = await fixture(); const hash = createHash("sha256").update("").digest("hex");
|
||||
const directory = join(f.input.artifactRoot, hash); await mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
for (let index = 0; index < 256; index++) await writeFile(join(directory, `legacy-${index}`), "", { mode: 0o600 });
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow(/staging budget/);
|
||||
expect(f.input.client.downloadArtifact).not.toHaveBeenCalled();
|
||||
expect(await readdir(directory)).toHaveLength(256);
|
||||
});
|
||||
|
||||
it("recovers an actual postlink writer crash to one final link", async () => {
|
||||
const f = await fixture(); const writer = await postLinkWriter(f);
|
||||
try {
|
||||
const published = await lstat(f.target); expect(published.nlink).toBe(2);
|
||||
await writer.stop();
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
const recovered = await lstat(f.target);
|
||||
expect([recovered.dev, recovered.ino, recovered.nlink]).toEqual([published.dev, published.ino, 1]);
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
await expect(lstat(writer.temporary)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await readdir(f.directory)).toEqual([f.args.filename]);
|
||||
} finally { await writer.stop(); }
|
||||
});
|
||||
|
||||
it("lets concurrent retries adopt one dead publisher alias without replacing the final", async () => {
|
||||
const f = await fixture(); const writer = await postLinkWriter(f);
|
||||
try {
|
||||
await writer.stop(); const published = await lstat(f.target);
|
||||
const foreign = join(f.directory, "unrelated-private-file");
|
||||
await writeFile(foreign, "retained foreign bytes", { mode: 0o600 }); const before = await lstat(foreign);
|
||||
expect(await Promise.all([stageCursorCloudArtifact(f.input), stageCursorCloudArtifact(f.input)])).toEqual([f.normalized, f.normalized]);
|
||||
const recovered = await lstat(f.target); const retained = await lstat(foreign);
|
||||
expect([recovered.dev, recovered.ino, recovered.nlink]).toEqual([published.dev, published.ino, 1]);
|
||||
expect([retained.dev, retained.ino, retained.mode]).toEqual([before.dev, before.ino, before.mode]);
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
expect(await readFile(foreign, "utf8")).toBe("retained foreign bytes");
|
||||
await expect(lstat(writer.temporary)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect((await readdir(f.directory)).sort()).toEqual([f.args.filename, "unrelated-private-file"].sort());
|
||||
} finally { await writer.stop(); }
|
||||
});
|
||||
|
||||
it("never removes another live process's published alias", async () => {
|
||||
const f = await fixture(); const writer = await postLinkWriter(f);
|
||||
try {
|
||||
const before = await lstat(writer.temporary);
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow();
|
||||
const retained = await lstat(writer.temporary);
|
||||
expect([retained.dev, retained.ino, retained.nlink]).toEqual([before.dev, before.ino, 2]);
|
||||
expect(await readFile(writer.temporary)).toEqual(f.bytes);
|
||||
const completed = once(writer.child, "message"); writer.child.send("continue");
|
||||
expect((await completed)[0]).toMatchObject({ name: "completed", result: f.normalized });
|
||||
expect(await writer.exited).toEqual([0, null]);
|
||||
expect((await lstat(f.target)).nlink).toBe(1);
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
} finally { await writer.stop(); }
|
||||
});
|
||||
|
||||
it("never removes an active same-module publisher alias", async () => {
|
||||
const f = await fixture(); const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const linked = deferred(); const resume = deferred(); let alias = "";
|
||||
vi.mocked(link).mockImplementation(async (source, destination) => {
|
||||
await original.link(source, destination);
|
||||
if (!alias && String(destination) === f.target) { alias = String(source); linked.resolve(); await resume.promise; }
|
||||
});
|
||||
const active = stageCursorCloudArtifact(f.input);
|
||||
try {
|
||||
await linked.promise;
|
||||
const before = await lstat(alias);
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow();
|
||||
const retained = await lstat(alias);
|
||||
expect([retained.dev, retained.ino, retained.nlink]).toEqual([before.dev, before.ino, 2]);
|
||||
expect(await readFile(alias)).toEqual(f.bytes);
|
||||
resume.resolve(); expect(await active).toEqual(f.normalized);
|
||||
expect((await lstat(f.target)).nlink).toBe(1);
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
} finally { resume.resolve(); await active.catch(() => {}); }
|
||||
});
|
||||
|
||||
it("recovers only the alias of a finished own attempt after cleanup failure", async () => {
|
||||
const f = await fixture(); const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
let alias = "";
|
||||
vi.mocked(unlink).mockImplementation(async path => {
|
||||
if (!alias && String(path).startsWith(`${f.directory}/`) && String(path).endsWith(".tmp")) {
|
||||
alias = String(path); throw Object.assign(new Error("fixture completed-attempt cleanup failed"), { code: "EIO" });
|
||||
}
|
||||
await original.unlink(path);
|
||||
});
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow("fixture completed-attempt cleanup failed");
|
||||
expect((await lstat(f.target)).nlink).toBe(2);
|
||||
expect(await readFile(alias)).toEqual(f.bytes);
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
await expect(lstat(alias)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect((await lstat(f.target)).nlink).toBe(1);
|
||||
expect(await readdir(f.directory)).toEqual([f.args.filename]);
|
||||
});
|
||||
|
||||
it.each(["foreign", "unknown", "wrong-target", "live-reused-pid", "malformed", "unrelated-inode", "symlink", "unknown-liveness"])("retains %s aliases and refuses to certify multiple links", async mode => {
|
||||
const f = await fixture(); let ownSuccessfulAlias = "";
|
||||
if (mode === "foreign") {
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
vi.mocked(link).mockImplementation(async (source, destination) => {
|
||||
await original.link(source, destination);
|
||||
if (!ownSuccessfulAlias && String(destination) === f.target) ownSuccessfulAlias = String(source);
|
||||
});
|
||||
}
|
||||
await stageCursorCloudArtifact(f.input);
|
||||
const deadPid = exitedPid(); const originalKill = process.kill.bind(process);
|
||||
let alias = mode === "foreign" ? ownSuccessfulAlias
|
||||
: mode === "unknown" ? join(f.directory, `.cursor-cloud-${randomUUID()}.tmp`)
|
||||
: aliasPath(f, mode === "live-reused-pid" ? process.pid : deadPid, mode === "wrong-target" ? "other-result.txt" : f.args.filename);
|
||||
if (mode === "malformed") alias = alias.replace(/-[a-f0-9-]{36}\.tmp$/, "-not-an-attempt.tmp");
|
||||
if (mode === "unrelated-inode" || mode === "symlink") {
|
||||
await link(f.target, join(f.directory, "unknown-final-alias"));
|
||||
if (mode === "unrelated-inode") await writeFile(alias, f.bytes, { mode: 0o600 });
|
||||
else await symlink(f.target, alias);
|
||||
} else await link(f.target, alias);
|
||||
if (mode === "unknown-liveness") vi.spyOn(process, "kill").mockImplementation((pid, signal) => {
|
||||
if (pid === deadPid) throw Object.assign(new Error("fixture owner liveness unknown"), { code: "EPERM" });
|
||||
return originalKill(pid, signal);
|
||||
});
|
||||
const before = await lstat(alias); const final = await lstat(f.target); const names = (await readdir(f.directory)).sort();
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow();
|
||||
const after = await lstat(alias); const retained = await lstat(f.target);
|
||||
expect([after.dev, after.ino, after.mode]).toEqual([before.dev, before.ino, before.mode]);
|
||||
expect([retained.dev, retained.ino, retained.nlink]).toEqual([final.dev, final.ino, 2]);
|
||||
expect(await readFile(alias)).toEqual(f.bytes);
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
expect((await readdir(f.directory)).sort()).toEqual(names);
|
||||
});
|
||||
|
||||
it.each(["entries", "aliases"])("refuses an over-bound %s recovery without deleting aliases", async mode => {
|
||||
const f = await fixture(); await stageCursorCloudArtifact(f.input); const deadPid = exitedPid();
|
||||
const aliases = Array.from({ length: mode === "aliases" ? 33 : 1 }, () => aliasPath(f, deadPid));
|
||||
for (const alias of aliases) await link(f.target, alias);
|
||||
if (mode === "entries") for (let index = 0; index < 256; index += 16) {
|
||||
await Promise.all(Array.from({ length: 16 }, (_, offset) => writeFile(join(f.directory, `unrelated-${index + offset}`), "retained", { mode: 0o600 })));
|
||||
}
|
||||
const before = await lstat(f.target); const names = (await readdir(f.directory)).sort();
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow();
|
||||
const after = await lstat(f.target);
|
||||
expect([after.dev, after.ino, after.nlink]).toEqual([before.dev, before.ino, before.nlink]);
|
||||
expect((await readdir(f.directory)).sort()).toEqual(names);
|
||||
for (const alias of aliases) expect(await readFile(alias)).toEqual(f.bytes);
|
||||
});
|
||||
|
||||
it("does not clean a dead publisher alias after recovery cancellation", async () => {
|
||||
const f = await fixture(); const writer = await postLinkWriter(f); await writer.stop();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
if (String(path) === f.target) {
|
||||
const read = handle.read.bind(handle) as (...args: unknown[]) => Promise<Awaited<ReturnType<typeof handle.read>>>;
|
||||
vi.spyOn(handle, "read").mockImplementation((async (...args: unknown[]) => {
|
||||
const bytes = await read(...args); f.controller.abort(new Error("fixture artifact recovery cancelled")); return bytes;
|
||||
}) as typeof handle.read);
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow("fixture artifact recovery cancelled");
|
||||
expect(await readFile(writer.temporary)).toEqual(f.bytes);
|
||||
expect((await lstat(f.target)).nlink).toBe(2);
|
||||
vi.mocked(open).mockImplementation(original.open);
|
||||
expect(await stageCursorCloudArtifact({ ...f.input, signal: new AbortController().signal })).toEqual(f.normalized);
|
||||
expect((await lstat(f.target)).nlink).toBe(1);
|
||||
});
|
||||
|
||||
it("never exposes a partial final file and allows retry after a failed write", async () => {
|
||||
const f = await fixture();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const partial = deferred(); const resume = deferred(); let injected = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
if (!injected && String(path).startsWith(`${f.directory}/`) && (flags === "wx" || (typeof flags === "number" && (flags & constants.O_EXCL) !== 0))) {
|
||||
injected = true;
|
||||
vi.spyOn(handle, "writeFile").mockImplementation(async () => {
|
||||
await handle.write(f.bytes.subarray(0, 4), 0, 4, 0);
|
||||
partial.resolve(); await resume.promise; throw new Error("fixture partial artifact write failed");
|
||||
});
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
const staging = stageCursorCloudArtifact(f.input);
|
||||
try {
|
||||
await partial.promise;
|
||||
const visible = await readFile(f.target).catch(error => { if (error.code === "ENOENT") return null; throw error; });
|
||||
resume.resolve();
|
||||
await expect(staging).rejects.toThrow("fixture partial artifact write failed");
|
||||
expect(visible).toBeNull();
|
||||
await expect(lstat(f.target)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await readdir(f.directory)).toEqual([]);
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
} finally { resume.resolve(); await staging.catch(() => {}); }
|
||||
});
|
||||
|
||||
it("does not publish an artifact cancelled while its private write is pending", async () => {
|
||||
const f = await fixture();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const written = deferred(); const resume = deferred(); let injected = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
if (!injected && String(path).startsWith(`${f.directory}/`) && (flags === "wx" || (typeof flags === "number" && (flags & constants.O_EXCL) !== 0))) {
|
||||
injected = true; const write = handle.writeFile.bind(handle);
|
||||
vi.spyOn(handle, "writeFile").mockImplementation(async (...args) => { await write(...args); written.resolve(); await resume.promise; });
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
const staging = stageCursorCloudArtifact(f.input);
|
||||
try {
|
||||
await written.promise; f.controller.abort(new Error("fixture artifact cancellation")); resume.resolve();
|
||||
await expect(staging).rejects.toThrow("fixture artifact cancellation");
|
||||
await expect(lstat(f.target)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await readdir(f.directory)).toEqual([]);
|
||||
expect(JSON.parse(await readFile(join(f.root, ".artifact-staging-budget", "usage.json"), "utf8")).allocations).toBe(1);
|
||||
} finally { resume.resolve(); await staging.catch(() => {}); }
|
||||
});
|
||||
|
||||
it("keeps the final absent through an actual writer crash and retries without reaping the orphan", async () => {
|
||||
const f = await fixture();
|
||||
const script = join(f.root, "crash-writer.mjs");
|
||||
await writeFile(script, `
|
||||
import fs from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { syncBuiltinESMExports } from "node:module";
|
||||
const original = fs.open;
|
||||
fs.open = async (...args) => {
|
||||
const handle = await original(...args);
|
||||
const flags = args[1];
|
||||
if (String(args[0]).startsWith(${JSON.stringify(`${f.directory}/`)}) && (flags === "wx" || (typeof flags === "number" && (flags & constants.O_EXCL) !== 0))) {
|
||||
handle.writeFile = async bytes => {
|
||||
await handle.write(bytes.subarray(0, 4), 0, 4, 0); await handle.sync();
|
||||
process.send("partial-written");
|
||||
await new Promise(resolve => process.once("message", resolve));
|
||||
};
|
||||
}
|
||||
return handle;
|
||||
};
|
||||
syncBuiltinESMExports();
|
||||
const { stageCursorCloudArtifact } = await import(${JSON.stringify(new URL("./artifact-delivery.ts", import.meta.url).href)});
|
||||
const bytes = Buffer.from(${JSON.stringify(f.bytes.toString("base64"))}, "base64");
|
||||
await stageCursorCloudArtifact({ arguments: ${JSON.stringify(f.args)}, agentId: "agent", runtimeDirectory: ${JSON.stringify(f.root)}, artifactRoot: ${JSON.stringify(f.input.artifactRoot)},
|
||||
client: { listArtifacts: async () => ({ items: [{ path: "artifacts/result.txt", sizeBytes: bytes.length }] }), downloadArtifact: async () => bytes }, signal: new AbortController().signal });
|
||||
process.send("unexpected-completed");
|
||||
`, { mode: 0o600 });
|
||||
const child = spawn(process.execPath, ["--import", new URL("../../../node_modules/tsx/dist/loader.mjs", import.meta.url).pathname, script], { env: {}, stdio: ["ignore", "ignore", "pipe", "ipc"] });
|
||||
let stderr = ""; child.stderr?.on("data", bytes => { stderr = (stderr + bytes).slice(-4_096); });
|
||||
const exited = once(child, "exit");
|
||||
try {
|
||||
const message = await Promise.race([once(child, "message"), exited.then(() => { throw new Error(`artifact crash fixture exited early: ${stderr}`); })]);
|
||||
expect(message[0]).toBe("partial-written");
|
||||
await expect(lstat(f.target)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
child.kill("SIGKILL"); expect(await exited).toEqual([null, "SIGKILL"]);
|
||||
const orphans = await readdir(f.directory);
|
||||
expect(orphans).toHaveLength(1);
|
||||
const orphan = join(f.directory, orphans[0]!);
|
||||
expect(await readFile(orphan)).toEqual(f.bytes.subarray(0, 4));
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
expect(await readFile(orphan)).toEqual(f.bytes.subarray(0, 4));
|
||||
expect((await readdir(f.directory)).sort()).toEqual([...orphans, f.args.filename].sort());
|
||||
} finally {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||
await exited;
|
||||
}
|
||||
});
|
||||
|
||||
it("concurrently stages identical bytes without replacing the first verified final", async () => {
|
||||
const f = await fixture();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const written = deferred(); const resume = deferred(); let held = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
if (!held && String(path).startsWith(`${f.directory}/`) && (flags === "wx" || (typeof flags === "number" && (flags & constants.O_EXCL) !== 0))) {
|
||||
held = true; const write = handle.writeFile.bind(handle);
|
||||
vi.spyOn(handle, "writeFile").mockImplementation(async (...args) => { await write(...args); written.resolve(); await resume.promise; });
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
const first = stageCursorCloudArtifact(f.input);
|
||||
try {
|
||||
await written.promise;
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
const published = await lstat(f.target);
|
||||
resume.resolve(); expect(await first).toEqual(f.normalized);
|
||||
const retained = await lstat(f.target);
|
||||
expect([retained.dev, retained.ino]).toEqual([published.dev, published.ino]);
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
expect(await readdir(f.directory)).toEqual([f.args.filename]);
|
||||
} finally { resume.resolve(); await first.catch(() => {}); }
|
||||
});
|
||||
|
||||
it("requires directory sync again when retry verifies an already-published final", async () => {
|
||||
const f = await fixture();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const synced: string[] = []; let failures = 2;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
if ([f.directory, f.input.artifactRoot, f.root].includes(String(path))) {
|
||||
const sync = handle.sync.bind(handle);
|
||||
vi.spyOn(handle, "sync").mockImplementation(async () => {
|
||||
synced.push(String(path));
|
||||
if (String(path) === f.directory && failures-- > 0) throw new Error("fixture artifact directory sync failed");
|
||||
await sync();
|
||||
});
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow("fixture artifact directory sync failed");
|
||||
expect(await readFile(f.target)).toEqual(f.bytes);
|
||||
expect(await readdir(f.directory)).toEqual([f.args.filename]);
|
||||
}
|
||||
expect(await stageCursorCloudArtifact(f.input)).toEqual(f.normalized);
|
||||
expect(synced).toEqual([f.root, f.directory, f.directory, f.directory, f.input.artifactRoot, f.root]);
|
||||
});
|
||||
|
||||
it.each(["partial", "wrong-hash", "public-mode", "symlink", "directory"])("refuses an existing %s final unchanged", async mode => {
|
||||
const f = await fixture(); await mkdir(f.directory, { recursive: true, mode: 0o700 });
|
||||
const bytes = mode === "partial" ? f.bytes.subarray(0, 4) : mode === "wrong-hash" ? Buffer.alloc(f.bytes.length, "x") : f.bytes;
|
||||
if (mode === "directory") await mkdir(f.target, { mode: 0o700 });
|
||||
else if (mode === "symlink") {
|
||||
const target = join(f.root, "unrelated-result");
|
||||
await writeFile(target, bytes, { mode: 0o600 }); await symlink(target, f.target);
|
||||
} else await writeFile(f.target, bytes, { mode: mode === "public-mode" ? 0o644 : 0o600 });
|
||||
const before = await lstat(f.target);
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow();
|
||||
const after = await lstat(f.target);
|
||||
expect([after.dev, after.ino, after.mode]).toEqual([before.dev, before.ino, before.mode]);
|
||||
if (mode !== "directory") expect(await readFile(f.target)).toEqual(bytes);
|
||||
expect(await readdir(f.directory)).toEqual([f.args.filename]);
|
||||
});
|
||||
|
||||
it.each(["replace", "symlink"])("retains a foreign %s temporary file without publishing or deleting it", async mode => {
|
||||
const f = await fixture();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
let temporary = "";
|
||||
vi.mocked(open).mockImplementation((async (path, flags, fileMode) => {
|
||||
const handle = await original.open(path, flags, fileMode);
|
||||
if (String(path).startsWith(`${f.directory}/`) && typeof flags === "number" && (flags & constants.O_EXCL) !== 0) {
|
||||
temporary = String(path); const sync = handle.sync.bind(handle);
|
||||
vi.spyOn(handle, "sync").mockImplementation(async () => {
|
||||
await sync(); await rename(temporary, join(f.root, "retained-own-temporary"));
|
||||
if (mode === "replace") await writeFile(temporary, "foreign temporary", { mode: 0o600 });
|
||||
else {
|
||||
const target = join(f.root, "foreign-temporary");
|
||||
await writeFile(target, "foreign temporary", { mode: 0o600 }); await symlink(target, temporary);
|
||||
}
|
||||
});
|
||||
}
|
||||
return handle;
|
||||
}) as typeof open);
|
||||
await expect(stageCursorCloudArtifact(f.input)).rejects.toThrow("temporary ownership changed");
|
||||
expect(await readFile(temporary, "utf8")).toBe("foreign temporary");
|
||||
expect(await readFile(join(f.root, "retained-own-temporary"))).toEqual(f.bytes);
|
||||
await expect(lstat(f.target)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
if (mode === "symlink") expect((await lstat(temporary)).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["fresh", "existing"])("stages %s verified bytes with a contentRef relative to the artifact root", async state => {
|
||||
const root = await mkdtemp("/tmp/cc-artifact-"); roots.push(root);
|
||||
const bytes = Buffer.from("downloaded provider result\n"); const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const client = { listArtifacts: vi.fn(async () => ({ items: [{ path: "artifacts/result.txt", sizeBytes: bytes.length, updatedAt: "2026-10-28" }] })), downloadArtifact: vi.fn(async () => bytes) };
|
||||
const args = { contentRef: "cursor-artifact:artifacts/result.txt", filename: "result.txt", byteSize: bytes.length, sha256, contentType: "text/plain" };
|
||||
const artifactRoot = join(root, "artifacts");
|
||||
const input = { arguments: args, agentId: "agent", runtimeDirectory: root, artifactRoot, client, signal: new AbortController().signal };
|
||||
if (state === "existing") await stageCursorCloudArtifact(input);
|
||||
const normalized = await stageCursorCloudArtifact(input);
|
||||
expect(normalized).toEqual({ ...args, contentRef: join(sha256, "result.txt") });
|
||||
expect(await readFile(join(artifactRoot, normalized.contentRef as string))).toEqual(bytes);
|
||||
await expect(stageCursorCloudArtifact({ arguments: { ...args, sha256: "a".repeat(64) }, agentId: "agent", runtimeDirectory: root, artifactRoot: join(root, "artifacts"), client, signal: new AbortController().signal })).rejects.toThrow("do not match");
|
||||
});
|
||||
it("rejects host paths, traversal, unlisted paths, size mismatches and an unadmitted root", async () => {
|
||||
const root = await mkdtemp("/tmp/cc-artifact-"); roots.push(root);
|
||||
const client = { listArtifacts: vi.fn(async () => ({ items: [] })), downloadArtifact: vi.fn(async () => Buffer.from("not read")) };
|
||||
const input = { agentId: "agent", runtimeDirectory: root, artifactRoot: join(root, "artifacts"), client, signal: new AbortController().signal };
|
||||
for (const contentRef of ["/etc/passwd", "cursor-artifact:../outside", "cursor-artifact:artifacts/missing"]) {
|
||||
await expect(stageCursorCloudArtifact({ ...input, arguments: { contentRef, filename: "file", byteSize: 1, sha256: "a".repeat(64) } })).rejects.toThrow();
|
||||
}
|
||||
await expect(stageCursorCloudArtifact({ ...input, artifactRoot: root, arguments: { contentRef: "cursor-artifact:artifacts/file", filename: "file", byteSize: 1, sha256: "a".repeat(64) } })).rejects.toThrow("outside");
|
||||
expect(client.downloadArtifact).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { constants, type Stats } from "node:fs";
|
||||
import { link, lstat, mkdir, open, opendir, unlink, type FileHandle } from "node:fs/promises";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
import type { CursorCloudClient } from "./cloud-client.js";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { reserveCursorCloudArtifactStaging } from "./artifact-staging-budget.js";
|
||||
|
||||
const artifactPublisherNonce = randomUUID();
|
||||
const activeArtifactAliases = new Set<string>();
|
||||
const retainedOwnArtifactAliases = new Map<string, { dev: number; ino: number }>();
|
||||
const uuidPattern = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
|
||||
const artifactAliasPattern = new RegExp(`^\\.cursor-cloud-([a-f0-9]{64})-([1-9][0-9]*)-(${uuidPattern})-(${uuidPattern})\\.tmp$`);
|
||||
const sameArtifactIdentity = (a: { dev: number; ino: number }, b: { dev: number; ino: number }) => a.dev === b.dev && a.ino === b.ino;
|
||||
const privateArtifact = (stat: Stats) => stat.isFile() && (stat.mode & 0o077) === 0 && (!process.getuid || stat.uid === process.getuid());
|
||||
|
||||
function artifactPublisherIsDead(pid: number): boolean {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
||||
try { process.kill(pid, 0); return false; }
|
||||
catch (error) { return (error as NodeJS.ErrnoException).code === "ESRCH"; }
|
||||
}
|
||||
|
||||
async function readExactArtifact(handle: FileHandle, size: number): Promise<Buffer> {
|
||||
const bytes = Buffer.allocUnsafe(size);
|
||||
let offset = 0;
|
||||
while (offset < size) {
|
||||
const result = await handle.read(bytes, offset, size - offset, offset);
|
||||
if (!result.bytesRead) throw new Error("Cursor Cloud artifact staging identity conflict");
|
||||
offset += result.bytesRead;
|
||||
}
|
||||
if ((await handle.read(Buffer.alloc(1), 0, 1, size)).bytesRead) throw new Error("Cursor Cloud artifact staging identity conflict");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function verifyPublishedArtifact(target: string, bytes: Buffer, sha256: string): Promise<Stats> {
|
||||
const handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
||||
try {
|
||||
const before = await handle.stat(); const pathBefore = await lstat(target);
|
||||
if (!privateArtifact(before) || before.size !== bytes.length || !privateArtifact(pathBefore) || !sameArtifactIdentity(before, pathBefore)
|
||||
|| createHash("sha256").update(await readExactArtifact(handle, bytes.length)).digest("hex") !== sha256) throw new Error("Cursor Cloud artifact staging identity conflict");
|
||||
const after = await handle.stat(); const pathAfter = await lstat(target);
|
||||
if (!privateArtifact(after) || after.size !== bytes.length || !privateArtifact(pathAfter) || !sameArtifactIdentity(before, after)
|
||||
|| !sameArtifactIdentity(after, pathAfter)) throw new Error("Cursor Cloud artifact staging identity conflict");
|
||||
return after;
|
||||
} finally { await handle.close(); }
|
||||
}
|
||||
|
||||
async function recoverPublishedArtifactAliases(input: {
|
||||
directory: string; target: string; filenameDigest: string; bytes: Buffer; sha256: string; signal: AbortSignal;
|
||||
}): Promise<Stats> {
|
||||
input.signal.throwIfAborted();
|
||||
const final = await verifyPublishedArtifact(input.target, input.bytes, input.sha256);
|
||||
if (final.nlink > 1) {
|
||||
const names: string[] = [];
|
||||
for await (const entry of await opendir(input.directory)) {
|
||||
input.signal.throwIfAborted();
|
||||
if (names.length === 256) throw new Error("Cursor Cloud artifact publication recovery exceeds its directory bound");
|
||||
names.push(entry.name);
|
||||
}
|
||||
const aliases = names.map(name => ({ name, match: artifactAliasPattern.exec(name) }))
|
||||
.filter(value => value.match?.[1] === input.filenameDigest);
|
||||
if (aliases.length > 32) throw new Error("Cursor Cloud artifact publication recovery exceeds its alias bound");
|
||||
for (const { name, match } of aliases) {
|
||||
input.signal.throwIfAborted();
|
||||
const alias = join(input.directory, name); const pid = Number(match![2]);
|
||||
const own = retainedOwnArtifactAliases.get(alias);
|
||||
const finishedOwn = pid === process.pid && match![3] === artifactPublisherNonce && own !== undefined;
|
||||
if (activeArtifactAliases.has(alias) || (!finishedOwn && !artifactPublisherIsDead(pid))) continue;
|
||||
const handle = await open(alias, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(error => {
|
||||
if (["ENOENT", "ELOOP"].includes((error as NodeJS.ErrnoException).code ?? "")) return null;
|
||||
throw error;
|
||||
});
|
||||
if (!handle) continue;
|
||||
try {
|
||||
const held = await handle.stat(); const current = await lstat(alias).catch(error => {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!current || !privateArtifact(held) || !privateArtifact(current) || held.size !== input.bytes.length
|
||||
|| !sameArtifactIdentity(held, final) || !sameArtifactIdentity(held, current) || (finishedOwn && !sameArtifactIdentity(held, own!))) continue;
|
||||
input.signal.throwIfAborted();
|
||||
const target = await lstat(input.target); const aliasBefore = await lstat(alias).catch(error => {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!aliasBefore) continue;
|
||||
if (!privateArtifact(target) || !sameArtifactIdentity(target, final) || !privateArtifact(aliasBefore)
|
||||
|| !sameArtifactIdentity(aliasBefore, held)) throw new Error("Cursor Cloud artifact publication ownership changed");
|
||||
await unlink(alias).catch(error => { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; });
|
||||
if (finishedOwn) retainedOwnArtifactAliases.delete(alias);
|
||||
} finally { await handle.close(); }
|
||||
}
|
||||
}
|
||||
const verified = await verifyPublishedArtifact(input.target, input.bytes, input.sha256);
|
||||
if (!sameArtifactIdentity(final, verified) || verified.nlink !== 1) throw new Error("Cursor Cloud artifact has unresolved publication aliases");
|
||||
return verified;
|
||||
}
|
||||
|
||||
export async function stageCursorCloudArtifact(input: {
|
||||
arguments: unknown;
|
||||
agentId: string;
|
||||
runtimeDirectory: string;
|
||||
artifactRoot: string;
|
||||
client: Pick<CursorCloudClient, "listArtifacts" | "downloadArtifact">;
|
||||
signal: AbortSignal;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
if (!input.arguments || typeof input.arguments !== "object" || Array.isArray(input.arguments)) throw new Error("Cursor Cloud deliverable input must be an object");
|
||||
const args = input.arguments as Record<string, unknown>;
|
||||
if (typeof args.contentRef !== "string" || !args.contentRef.startsWith("cursor-artifact:")) throw new Error("Cursor Cloud deliverables require contentRef cursor-artifact:<listed artifact path>");
|
||||
const path = args.contentRef.slice("cursor-artifact:".length);
|
||||
if (!path || path.startsWith("/") || /[\\\0\r\n]/.test(path) || path.split("/").some(part => !part || part === "." || part === "..")) throw new Error("Cursor Cloud artifact path must be a listed relative path");
|
||||
if (!Number.isSafeInteger(args.byteSize) || Number(args.byteSize) < 0 || Number(args.byteSize) > 16_777_216 || typeof args.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(args.sha256)) throw new Error("Cursor Cloud deliverables require exact byteSize and lowercase SHA-256");
|
||||
if (typeof args.filename !== "string" || !args.filename || args.filename.length > 255 || /[\x00-\x1f\x7f/\\]/.test(args.filename) || [".", ".."].includes(args.filename)) throw new Error("Cursor Cloud deliverable filename must be a single safe filename");
|
||||
if (!isAbsolute(input.artifactRoot) || resolve(input.artifactRoot) !== resolve(input.runtimeDirectory, "artifacts")) throw new Error("Cursor Cloud artifact root is outside its admitted session directory");
|
||||
const listed = await input.client.listArtifacts(input.agentId, { signal: input.signal });
|
||||
const artifact = listed.items.find(file => file.path === path);
|
||||
if (!artifact || artifact.sizeBytes !== args.byteSize) throw new Error("Cursor Cloud artifact is absent or its reported byte size does not match the claim");
|
||||
input.signal.throwIfAborted();
|
||||
const directory = join(input.artifactRoot, args.sha256);
|
||||
const contentRef = join(args.sha256, args.filename);
|
||||
const target = join(directory, args.filename);
|
||||
let cached: Buffer | null = null;
|
||||
try {
|
||||
for (const path of [input.artifactRoot, directory]) {
|
||||
const stat = await lstat(path);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid())) throw new Error("Cursor Cloud artifact staging directory must be owned and private");
|
||||
}
|
||||
const handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
if (!privateArtifact(stat) || stat.size !== args.byteSize) throw new Error("Cursor Cloud artifact staging identity conflict");
|
||||
cached = await readExactArtifact(handle, Number(args.byteSize));
|
||||
const after = await handle.stat(), pathAfter = await lstat(target);
|
||||
if (!privateArtifact(after) || !privateArtifact(pathAfter) || !sameArtifactIdentity(stat, after)
|
||||
|| !sameArtifactIdentity(after, pathAfter) || after.size !== args.byteSize || pathAfter.size !== args.byteSize
|
||||
|| cached.length !== args.byteSize) throw new Error("Cursor Cloud artifact staging identity conflict");
|
||||
await verifyPublishedArtifact(target, cached, args.sha256);
|
||||
} finally { await handle.close(); }
|
||||
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
||||
if (!cached) await reserveCursorCloudArtifactStaging({ runtimeDirectory: input.runtimeDirectory, artifactRoot: input.artifactRoot, byteSize: Number(args.byteSize), signal: input.signal });
|
||||
const bytes = await input.client.downloadArtifact(input.agentId, path, { signal: input.signal, maxBytes: Math.max(1, Number(args.byteSize)) });
|
||||
if (bytes.length !== args.byteSize || createHash("sha256").update(bytes).digest("hex") !== args.sha256) throw new Error("Cursor Cloud artifact bytes do not match the claimed size/SHA-256");
|
||||
input.signal.throwIfAborted();
|
||||
if (!cached) {
|
||||
for (const root of [input.artifactRoot, directory]) {
|
||||
await mkdir(root, { recursive: true, mode: 0o700 });
|
||||
const stat = await lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid())) throw new Error("Cursor Cloud artifact staging directory must be owned and private");
|
||||
}
|
||||
}
|
||||
const filenameDigest = createHash("sha256").update(args.filename).digest("hex");
|
||||
if (!cached) {
|
||||
const temporary = join(directory, `.cursor-cloud-${filenameDigest}-${process.pid}-${artifactPublisherNonce}-${randomUUID()}.tmp`);
|
||||
input.signal.throwIfAborted();
|
||||
const handle = await open(temporary, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
||||
activeArtifactAliases.add(temporary);
|
||||
try {
|
||||
const held = await handle.stat();
|
||||
let published = false;
|
||||
try {
|
||||
await handle.writeFile(bytes); await handle.sync();
|
||||
const current = await lstat(temporary);
|
||||
if (!current.isFile() || current.isSymbolicLink() || current.dev !== held.dev || current.ino !== held.ino || current.size !== bytes.length
|
||||
|| (current.mode & 0o077) !== 0 || (process.getuid && current.uid !== process.getuid())) throw new Error("Cursor Cloud artifact temporary ownership changed");
|
||||
input.signal.throwIfAborted();
|
||||
// A hard link publishes complete bytes without overwriting a concurrent final.
|
||||
try { await link(temporary, target); published = true; }
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
||||
await verifyPublishedArtifact(target, bytes, args.sha256);
|
||||
}
|
||||
} finally {
|
||||
const current = await lstat(temporary);
|
||||
if (!current.isFile() || current.isSymbolicLink() || current.dev !== held.dev || current.ino !== held.ino) throw new Error("Cursor Cloud artifact temporary ownership changed");
|
||||
await unlink(temporary).catch(async error => {
|
||||
const retained = await lstat(temporary).catch(() => null);
|
||||
if (published && retained && privateArtifact(retained) && sameArtifactIdentity(retained, held) && retained.nlink > 1) {
|
||||
retainedOwnArtifactAliases.set(temporary, { dev: held.dev, ino: held.ino });
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
try { await handle.close(); } finally { activeArtifactAliases.delete(temporary); }
|
||||
}
|
||||
}
|
||||
const published = await recoverPublishedArtifactAliases({ directory, target, filenameDigest, bytes, sha256: args.sha256, signal: input.signal });
|
||||
// EEXIST retries also finish durability after a prior post-publication failure.
|
||||
for (const root of [directory, input.artifactRoot, input.runtimeDirectory]) {
|
||||
const handle = await open(root, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
||||
try { await handle.sync(); } finally { await handle.close(); }
|
||||
}
|
||||
input.signal.throwIfAborted();
|
||||
const verified = await verifyPublishedArtifact(target, bytes, args.sha256);
|
||||
if (!sameArtifactIdentity(published, verified) || verified.nlink !== 1) throw new Error("Cursor Cloud artifact publication ownership changed");
|
||||
return { ...args, contentRef };
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { chmod, link, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { CursorCloudCheckpointStore } from "./checkpoint.js";
|
||||
import { CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS, CURSOR_CLOUD_STAGING_MAX_BYTES, reserveCursorCloudArtifactStaging } from "./artifact-staging-budget.js";
|
||||
|
||||
vi.mock("node:fs/promises", async importOriginal => {
|
||||
const original = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return { ...original, open: vi.fn(original.open) };
|
||||
});
|
||||
|
||||
const roots: string[] = [];
|
||||
const children = new Set<ChildProcess>();
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await Promise.all([...children].map(child => child.exitCode !== null || child.signalCode !== null ? Promise.resolve() : new Promise<void>(resolve => {
|
||||
child.once("exit", () => resolve()); child.kill("SIGKILL");
|
||||
})));
|
||||
children.clear();
|
||||
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
const MiB = 1_048_576;
|
||||
const schema = "paperclip.cursor-cloud.artifact-budget.v1";
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>(done => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const root = await mkdtemp("/tmp/cc-budget-"); roots.push(root);
|
||||
const runtimeDirectory = join(root, "runtime"); await mkdir(runtimeDirectory, { mode: 0o700 });
|
||||
const artifactRoot = join(root, "artifacts");
|
||||
const directory = join(runtimeDirectory, ".artifact-staging-budget");
|
||||
const usage = join(directory, "usage.json");
|
||||
const store = new CursorCloudCheckpointStore(directory, "budget");
|
||||
const reserve = (byteSize: number, signal = new AbortController().signal) => reserveCursorCloudArtifactStaging({ runtimeDirectory, artifactRoot, byteSize, signal });
|
||||
const budget = async () => JSON.parse(await readFile(usage, "utf8"));
|
||||
const seed = async (allocations: number, reservedBytes: number) => {
|
||||
await reserve(0); await writeFile(usage, JSON.stringify({ schema, allocations, reservedBytes }), { mode: 0o600 });
|
||||
};
|
||||
return { root, runtimeDirectory, artifactRoot, directory, usage, store, reserve, budget, seed };
|
||||
}
|
||||
|
||||
async function worker(f: Awaited<ReturnType<typeof fixture>>, byteSize: number) {
|
||||
const workerRoot = await mkdtemp(join(f.root, "worker-"));
|
||||
const script = join(workerRoot, "reserve.mjs");
|
||||
const source = pathToFileURL(join(dirname(fileURLToPath(import.meta.url)), "artifact-staging-budget.ts")).href;
|
||||
const input = JSON.stringify({ runtimeDirectory: f.runtimeDirectory, artifactRoot: f.artifactRoot, byteSize });
|
||||
await writeFile(script, `import { reserveCursorCloudArtifactStaging } from ${JSON.stringify(source)};
|
||||
process.once('message', async () => {
|
||||
try { await reserveCursorCloudArtifactStaging({ ...${input}, signal: new AbortController().signal }); process.send({ ok: true }); }
|
||||
catch (error) { process.send({ ok: false, error: String(error) }); }
|
||||
finally { process.disconnect(); }
|
||||
});
|
||||
process.send({ ready: true });
|
||||
`, { mode: 0o600 });
|
||||
const loader = createRequire(import.meta.url).resolve("tsx");
|
||||
const child = spawn(process.execPath, ["--import", loader, script], { env: { PATH: process.env.PATH }, stdio: ["ignore", "pipe", "pipe", "ipc"] });
|
||||
children.add(child); let stderr = ""; child.stderr!.on("data", value => { stderr += value; });
|
||||
const ready = new Promise<void>((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.on("message", value => { if ((value as { ready?: boolean }).ready) resolve(); });
|
||||
child.once("exit", code => { if (code !== 0) reject(new Error(`Worker exited ${code}: ${stderr}`)); });
|
||||
});
|
||||
const result = new Promise<{ ok: boolean; error?: string }>((resolve, reject) => {
|
||||
child.on("message", value => { if (typeof (value as { ok?: boolean }).ok === "boolean") resolve(value as { ok: boolean; error?: string }); });
|
||||
child.once("error", reject);
|
||||
child.once("exit", code => { if (code !== 0) reject(new Error(`Worker exited ${code}: ${stderr}`)); });
|
||||
});
|
||||
const exited = new Promise<void>((resolve, reject) => child.once("exit", (code, signal) => {
|
||||
children.delete(child);
|
||||
if (code === 0 && signal === null) resolve(); else reject(new Error(`Worker exited ${code}/${signal}: ${stderr}`));
|
||||
}));
|
||||
void result.catch(() => {}); void exited.catch(() => {});
|
||||
await ready;
|
||||
return { run: async () => { child.send("reserve"); const value = await result; await exited; return value; } };
|
||||
}
|
||||
|
||||
describe("Cursor Cloud persistent artifact staging budget", () => {
|
||||
it("charges each allocation including zero bytes and releases ownership before returning", async () => {
|
||||
expect(CURSOR_CLOUD_STAGING_MAX_BYTES).toBe(256 * MiB);
|
||||
expect(CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS).toBe(256);
|
||||
const f = await fixture(); await f.reserve(0); await f.reserve(7);
|
||||
expect(await f.budget()).toEqual({ schema, allocations: 2, reservedBytes: 7 });
|
||||
expect((await lstat(f.usage)).mode & 0o077).toBe(0);
|
||||
await expect(lstat(`${f.store.path}.lock`)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(lstat(`${f.store.path}.lock.acquire`)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await f.store.acquire(); await f.store.release();
|
||||
});
|
||||
|
||||
it("serializes concurrent byte reservations at the exact aggregate limit", async () => {
|
||||
const f = await fixture();
|
||||
const results = await Promise.allSettled(Array.from({ length: 19 }, () => f.reserve(16 * MiB)));
|
||||
expect(results.filter(result => result.status === "fulfilled")).toHaveLength(16);
|
||||
expect(await f.budget()).toEqual({ schema, allocations: 16, reservedBytes: 256 * MiB });
|
||||
await expect(f.reserve(1)).rejects.toThrow();
|
||||
await f.reserve(0);
|
||||
expect(await f.budget()).toEqual({ schema, allocations: 17, reservedBytes: 256 * MiB });
|
||||
});
|
||||
|
||||
it("bounds concurrent zero-byte import attempts by allocation count", async () => {
|
||||
const f = await fixture(); await f.seed(254, 9);
|
||||
const results = await Promise.allSettled(Array.from({ length: 7 }, () => f.reserve(0)));
|
||||
expect(results.filter(result => result.status === "fulfilled")).toHaveLength(2);
|
||||
expect(await f.budget()).toEqual({ schema, allocations: 256, reservedBytes: 9 });
|
||||
const before = await readFile(f.usage, "utf8"); await expect(f.reserve(0)).rejects.toThrow();
|
||||
expect(await readFile(f.usage, "utf8")).toBe(before);
|
||||
});
|
||||
|
||||
it.each(["allocation", "byte"])("prevents independent processes from reserving the last %s twice", async limit => {
|
||||
const f = await fixture(); await f.seed(limit === "allocation" ? 255 : 0, limit === "allocation" ? 4 : 256 * MiB - 1);
|
||||
const workers = await Promise.all(Array.from({ length: 4 }, () => worker(f, 1)));
|
||||
const results = await Promise.all(workers.map(value => value.run()));
|
||||
expect(results.filter(result => result.ok)).toHaveLength(1);
|
||||
expect(await f.budget()).toEqual({ schema, allocations: limit === "allocation" ? 256 : 1, reservedBytes: limit === "allocation" ? 5 : 256 * MiB });
|
||||
await expect(f.reserve(limit === "allocation" ? 0 : 1)).rejects.toThrow();
|
||||
}, 10_000);
|
||||
|
||||
it("retains charged reservations across independent process restarts", async () => {
|
||||
const f = await fixture(); await f.reserve(3);
|
||||
expect(await (await worker(f, 5)).run()).toEqual({ ok: true });
|
||||
expect(await (await worker(f, 7)).run()).toEqual({ ok: true });
|
||||
expect(await f.budget()).toEqual({ schema, allocations: 3, reservedBytes: 15 });
|
||||
}, 10_000);
|
||||
|
||||
it("seeds existing private files and empty digest directories exactly once", async () => {
|
||||
const f = await fixture(); await mkdir(f.artifactRoot, { mode: 0o700 });
|
||||
const populated = join(f.artifactRoot, "a".repeat(64)); const empty = join(f.artifactRoot, "b".repeat(64));
|
||||
await mkdir(populated, { mode: 0o700 }); await mkdir(empty, { mode: 0o700 });
|
||||
await writeFile(join(populated, "one.txt"), "123", { mode: 0o600 });
|
||||
await writeFile(join(populated, "empty.txt"), "", { mode: 0o600 });
|
||||
await f.reserve(4); expect(await f.budget()).toEqual({ schema, allocations: 4, reservedBytes: 7 });
|
||||
await f.reserve(2); expect(await f.budget()).toEqual({ schema, allocations: 5, reservedBytes: 9 });
|
||||
});
|
||||
|
||||
it("retains an abandoned staging reservation and refuses further imports", async () => {
|
||||
const f = await fixture(); await f.reserve(0); const before = await readFile(f.usage, "utf8");
|
||||
const path = join(f.directory, "usage.pending.tmp"); const bytes = "ambiguous reservation";
|
||||
await writeFile(path, bytes, { mode: 0o600 }); const identity = await lstat(path);
|
||||
await expect(f.reserve(1)).rejects.toThrow();
|
||||
expect(await readFile(path, "utf8")).toBe(bytes); expect(await readFile(f.usage, "utf8")).toBe(before);
|
||||
const after = await lstat(path); expect([after.dev, after.ino, after.mode]).toEqual([identity.dev, identity.ino, identity.mode]);
|
||||
});
|
||||
|
||||
it.each([-1, 0.5, NaN, Infinity, 16 * MiB + 1])("rejects invalid reservation size %s without changing the ledger", async byteSize => {
|
||||
const f = await fixture(); await f.reserve(0); const before = await readFile(f.usage, "utf8");
|
||||
await expect(f.reserve(byteSize)).rejects.toThrow(); expect(await readFile(f.usage, "utf8")).toBe(before);
|
||||
});
|
||||
|
||||
it.each(["{", JSON.stringify({ schema, allocations: -1, reservedBytes: 0 }), JSON.stringify({ schema, allocations: 257, reservedBytes: 0 }), JSON.stringify({ schema, allocations: 1, reservedBytes: 256 * MiB + 1 }), JSON.stringify({ schema, allocations: 0.5, reservedBytes: 0 }), JSON.stringify({ schema, allocations: 1, reservedBytes: 0, extra: true }), JSON.stringify({ schema: "foreign", allocations: 1, reservedBytes: 0 })])("preserves an unverifiable ledger (%s)", async bytes => {
|
||||
const f = await fixture(); await f.reserve(0); await writeFile(f.usage, bytes); const before = await lstat(f.usage);
|
||||
await expect(f.reserve(1)).rejects.toThrow(); expect(await readFile(f.usage, "utf8")).toBe(bytes);
|
||||
const after = await lstat(f.usage); expect([after.dev, after.ino]).toEqual([before.dev, before.ino]);
|
||||
});
|
||||
|
||||
it.each(["public", "symlink", "hardlink"])("preserves a %s ledger and its target", async mode => {
|
||||
const f = await fixture(); await f.reserve(0); const before = await readFile(f.usage, "utf8"); const target = join(f.root, "foreign-ledger");
|
||||
if (mode === "public") await chmod(f.usage, 0o644);
|
||||
if (mode === "symlink") { await rename(f.usage, target); await symlink(target, f.usage); }
|
||||
if (mode === "hardlink") await link(f.usage, target);
|
||||
const identity = await lstat(f.usage); await expect(f.reserve(1)).rejects.toThrow();
|
||||
expect(await readFile(f.usage, "utf8")).toBe(before);
|
||||
const after = await lstat(f.usage); expect([after.dev, after.ino, after.mode, after.nlink]).toEqual([identity.dev, identity.ino, identity.mode, identity.nlink]);
|
||||
if (mode !== "public") expect(await readFile(target, "utf8")).toBe(before);
|
||||
});
|
||||
|
||||
it.each(["live-lock", "ambiguous-guard", "symlink-guard"])("refuses and preserves a foreign %s", async mode => {
|
||||
const f = await fixture(); await f.reserve(0); const before = await readFile(f.usage, "utf8");
|
||||
const path = `${f.store.path}.lock${mode === "live-lock" ? "" : ".acquire"}`;
|
||||
const bytes = mode === "ambiguous-guard" ? "{" : JSON.stringify({ pid: process.pid, token: "foreign-owner" });
|
||||
if (mode === "symlink-guard") { const target = join(f.root, "foreign-guard"); await writeFile(target, bytes, { mode: 0o600 }); await symlink(target, path); }
|
||||
else await writeFile(path, bytes, { mode: 0o600 });
|
||||
const identity = await lstat(path); await expect(f.reserve(1)).rejects.toThrow();
|
||||
expect(await readFile(path, "utf8")).toBe(bytes); expect(await readFile(f.usage, "utf8")).toBe(before);
|
||||
const after = await lstat(path); expect([after.dev, after.ino, after.mode]).toEqual([identity.dev, identity.ino, identity.mode]);
|
||||
});
|
||||
|
||||
it.each(["write", "file-sync", "directory-sync", "parent-sync"])("cannot escape quota after a %s failure", async failure => {
|
||||
const f = await fixture(); await f.seed(255, 256 * MiB - 1); const before = await readFile(f.usage, "utf8");
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises"); let openedTemporary = false; let injected = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode); const temporary = String(path) === join(f.directory, "usage.pending.tmp");
|
||||
if (temporary) openedTemporary = true;
|
||||
const targetDirectory = failure === "directory-sync" ? f.directory : f.runtimeDirectory;
|
||||
return new Proxy(handle, { get(target, key) {
|
||||
if (!injected && temporary && key === (failure === "write" ? "writeFile" : failure === "file-sync" ? "sync" : "unused")) return async () => {
|
||||
injected = true; if (failure === "write") await target.writeFile("{"); throw new Error(`fixture ${failure} failure`);
|
||||
};
|
||||
if (!injected && openedTemporary && String(path) === targetDirectory && key === "sync" && ["directory-sync", "parent-sync"].includes(failure)) return async () => { injected = true; throw new Error(`fixture ${failure} failure`); };
|
||||
const value = Reflect.get(target, key, target); return typeof value === "function" ? value.bind(target) : value;
|
||||
} });
|
||||
}) as typeof open);
|
||||
await expect(f.reserve(1)).rejects.toThrow(`fixture ${failure} failure`); expect(injected).toBe(true);
|
||||
vi.mocked(open).mockImplementation(original.open);
|
||||
if (["write", "file-sync"].includes(failure)) {
|
||||
expect(await readFile(f.usage, "utf8")).toBe(before); await f.reserve(1);
|
||||
}
|
||||
expect(await f.budget()).toEqual({ schema, allocations: 256, reservedBytes: 256 * MiB });
|
||||
await expect(f.reserve(0)).rejects.toThrow();
|
||||
expect((await readdir(f.directory)).filter(name => name.endsWith(".tmp"))).toEqual([]);
|
||||
});
|
||||
|
||||
it("cancels a queued reservation without admitting it or releasing the active owner's quota", async () => {
|
||||
const f = await fixture(); const entered = deferred(); const resume = deferred();
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises"); let held = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
const handle = await original.open(path, flags, mode);
|
||||
return new Proxy(handle, { get(target, key) {
|
||||
if (!held && String(path) === join(f.directory, "usage.pending.tmp") && key === "writeFile") return async (...args: Parameters<typeof handle.writeFile>) => {
|
||||
held = true; entered.resolve(); await resume.promise; return target.writeFile(...args);
|
||||
};
|
||||
const value = Reflect.get(target, key, target); return typeof value === "function" ? value.bind(target) : value;
|
||||
} });
|
||||
}) as typeof open);
|
||||
const active = f.reserve(1); const abort = new AbortController();
|
||||
try {
|
||||
await entered.promise; const cancelled = f.reserve(2, abort.signal); abort.abort(new Error("fixture queued cancellation"));
|
||||
await expect(cancelled).rejects.toThrow("fixture queued cancellation"); resume.resolve(); await active;
|
||||
await f.reserve(3); expect(await f.budget()).toEqual({ schema, allocations: 2, reservedBytes: 4 });
|
||||
} finally { resume.resolve(); await active.catch(() => {}); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { constants, type Stats } from "node:fs";
|
||||
import { lstat, open, opendir, rename, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { CursorCloudCheckpointStore } from "./checkpoint.js";
|
||||
|
||||
export const CURSOR_CLOUD_STAGING_MAX_BYTES = 268_435_456;
|
||||
export const CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS = 256;
|
||||
const queues = new Map<string, Promise<void>>();
|
||||
type Budget = { schema: "paperclip.cursor-cloud.artifact-budget.v1"; allocations: number; reservedBytes: number };
|
||||
const denied = () => new Error("Cursor Cloud artifact staging budget is exhausted or cannot be verified");
|
||||
const same = (a: Stats, b: Stats) => a.dev === b.dev && a.ino === b.ino;
|
||||
const privateEntry = (value: Stats) => (value.mode & 0o077) === 0 && (!process.getuid || value.uid === process.getuid());
|
||||
async function present(path: string): Promise<Stats | null> {
|
||||
try { return await lstat(path); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw error; }
|
||||
}
|
||||
async function privateDirectory(path: string): Promise<Stats> {
|
||||
const value = await lstat(path);
|
||||
if (!value.isDirectory() || value.isSymbolicLink() || !privateEntry(value)) throw denied();
|
||||
return value;
|
||||
}
|
||||
async function seedBudget(artifactRoot: string): Promise<Budget> {
|
||||
const result: Budget = { schema: "paperclip.cursor-cloud.artifact-budget.v1", allocations: 0, reservedBytes: 0 };
|
||||
const root = await present(artifactRoot);
|
||||
if (!root) return result;
|
||||
await privateDirectory(artifactRoot);
|
||||
let directories = 0;
|
||||
for await (const entry of await opendir(artifactRoot)) {
|
||||
if (++directories > CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS || !/^[a-f0-9]{64}$/.test(entry.name)) throw denied();
|
||||
const path = join(artifactRoot, entry.name), before = await privateDirectory(path);
|
||||
let count = 0;
|
||||
for await (const file of await opendir(path)) {
|
||||
if (++result.allocations > CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS) throw denied();
|
||||
const value = await lstat(join(path, file.name));
|
||||
if (!value.isFile() || value.isSymbolicLink() || !privateEntry(value) || value.size > 16_777_216) throw denied();
|
||||
result.reservedBytes += value.size; count++;
|
||||
if (result.reservedBytes > CURSOR_CLOUD_STAGING_MAX_BYTES) throw denied();
|
||||
}
|
||||
if (!count && ++result.allocations > CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS) throw denied();
|
||||
if (!same(before, await privateDirectory(path))) throw denied();
|
||||
}
|
||||
if (!same(root, await privateDirectory(artifactRoot))) throw denied();
|
||||
return result;
|
||||
}
|
||||
async function readBudget(path: string, artifactRoot: string): Promise<{ budget: Budget; identity: Stats | null }> {
|
||||
let handle;
|
||||
try { handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return { budget: await seedBudget(artifactRoot), identity: null }; throw error; }
|
||||
try {
|
||||
const before = await handle.stat();
|
||||
if (!before.isFile() || before.nlink !== 1 || !privateEntry(before) || before.size > 1_024) throw denied();
|
||||
const value = JSON.parse(await handle.readFile("utf8")) as Budget;
|
||||
const after = await lstat(path);
|
||||
if (!same(before, after) || !after.isFile() || !privateEntry(after) || after.nlink !== 1
|
||||
|| value?.schema !== "paperclip.cursor-cloud.artifact-budget.v1"
|
||||
|| Object.keys(value).sort().join(",") !== "allocations,reservedBytes,schema"
|
||||
|| !Number.isSafeInteger(value.allocations) || value.allocations < 0 || value.allocations > CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS
|
||||
|| !Number.isSafeInteger(value.reservedBytes) || value.reservedBytes < 0 || value.reservedBytes > CURSOR_CLOUD_STAGING_MAX_BYTES) throw denied();
|
||||
return { budget: value, identity: before };
|
||||
} finally { await handle.close(); }
|
||||
}
|
||||
async function saveBudget(directory: string, path: string, before: Stats | null, value: Budget): Promise<void> {
|
||||
const temporary = join(directory, "usage.pending.tmp");
|
||||
const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
||||
let owned: Stats | undefined;
|
||||
try {
|
||||
owned = await handle.stat();
|
||||
await handle.writeFile(JSON.stringify(value)); await handle.sync();
|
||||
const current = await present(path), temp = await lstat(temporary);
|
||||
if ((before ? !current || !same(before, current) || !current.isFile() || !privateEntry(current) || current.nlink !== 1 : current !== null)
|
||||
|| !same(owned, temp) || !temp.isFile() || !privateEntry(temp) || temp.nlink !== 1) throw denied();
|
||||
await rename(temporary, path);
|
||||
const directoryHandle = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
||||
try { await directoryHandle.sync(); } finally { await directoryHandle.close(); }
|
||||
} finally {
|
||||
await handle.close();
|
||||
const current = await present(temporary);
|
||||
if (current) {
|
||||
if (!owned || !same(owned, current) || !current.isFile() || !privateEntry(current) || current.nlink !== 1) throw denied();
|
||||
await unlink(temporary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function reserveCursorCloudArtifactStaging(input: {
|
||||
runtimeDirectory: string; artifactRoot: string; byteSize: number; signal: AbortSignal;
|
||||
}): Promise<void> {
|
||||
input.signal.throwIfAborted();
|
||||
if (!Number.isSafeInteger(input.byteSize) || input.byteSize < 0 || input.byteSize > 16_777_216) throw denied();
|
||||
const preceding = queues.get(input.runtimeDirectory) ?? Promise.resolve();
|
||||
let release!: () => void;
|
||||
const held = new Promise<void>(resolve => { release = resolve; });
|
||||
const queued = preceding.then(() => held);
|
||||
queues.set(input.runtimeDirectory, queued);
|
||||
let abort!: () => void;
|
||||
const cancelled = new Promise<never>((_resolve, reject) => {
|
||||
abort = () => reject(input.signal.reason ?? new Error("Cursor Cloud artifact staging cancelled"));
|
||||
input.signal.addEventListener("abort", abort, { once: true });
|
||||
});
|
||||
try {
|
||||
await Promise.race([preceding, cancelled]); input.signal.throwIfAborted();
|
||||
await privateDirectory(input.runtimeDirectory);
|
||||
const directory = join(input.runtimeDirectory, ".artifact-staging-budget");
|
||||
const lock = new CursorCloudCheckpointStore(directory, "budget");
|
||||
await lock.acquire();
|
||||
try {
|
||||
input.signal.throwIfAborted();
|
||||
const path = join(directory, "usage.json"), prior = await readBudget(path, input.artifactRoot);
|
||||
if (prior.budget.allocations >= CURSOR_CLOUD_STAGING_MAX_ALLOCATIONS
|
||||
|| prior.budget.reservedBytes + input.byteSize > CURSOR_CLOUD_STAGING_MAX_BYTES) throw denied();
|
||||
const next = { ...prior.budget, allocations: prior.budget.allocations + 1, reservedBytes: prior.budget.reservedBytes + input.byteSize };
|
||||
input.signal.throwIfAborted();
|
||||
await saveBudget(directory, path, prior.identity, next);
|
||||
const root = await open(input.runtimeDirectory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
||||
try { await root.sync(); } finally { await root.close(); }
|
||||
input.signal.throwIfAborted();
|
||||
} finally { await lock.release(); }
|
||||
} finally {
|
||||
input.signal.removeEventListener("abort", abort); release();
|
||||
void queued.then(() => { if (queues.get(input.runtimeDirectory) === queued) queues.delete(input.runtimeDirectory); });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { lstat, mkdtemp, mkdir, open, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { semanticCanonicalJson } from "../semantic-tool-receipt.js";
|
||||
import { archiveCursorCloudSessionCheckpoint, CursorCloudCheckpointStore, cursorCloudDigest, type CursorCloudCheckpoint } from "./checkpoint.js";
|
||||
|
||||
vi.mock("node:fs/promises", async importOriginal => {
|
||||
const original = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return { ...original, lstat: vi.fn(original.lstat), open: vi.fn(original.open) };
|
||||
});
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => { vi.restoreAllMocks(); for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); });
|
||||
const binding = { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId: "session" };
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>(done => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function deadOwner() {
|
||||
const child = spawnSync(process.execPath, ["--input-type=module", "-e", ""], { env: {}, stdio: "ignore" });
|
||||
expect(child.status).toBe(0);
|
||||
expect(() => process.kill(child.pid, 0)).toThrowError(expect.objectContaining({ code: "ESRCH" }));
|
||||
return { pid: child.pid, token: "dead-owner" };
|
||||
}
|
||||
|
||||
async function fixture(phase: "idle" | "terminal" = "terminal") {
|
||||
const root = await mkdtemp("/tmp/cc-archive-"); roots.push(root);
|
||||
const runtimeDirectory = join(root, "cursor-cloud");
|
||||
const store = new CursorCloudCheckpointStore(runtimeDirectory, "session");
|
||||
await store.acquire();
|
||||
const checkpoint: CursorCloudCheckpoint = { schema: "paperclip.cursor-cloud.checkpoint.v1", driverSessionId: "session", normalizedSessionId: "session",
|
||||
bindingDigest: cursorCloudDigest(semanticCanonicalJson(binding)), configDigest: "a".repeat(64), modelDigest: "b".repeat(64), instructionsDigest: "c".repeat(64),
|
||||
instructionsRevisionPending: false, relayTurnToken: null, ownerRunId: "owner", agentId: phase === "terminal" ? "bc-provider" : null,
|
||||
runId: phase === "terminal" ? "run-provider" : null, turnId: phase === "terminal" ? "turn-provider" : null,
|
||||
phase, pendingOperation: null, idempotencyKey: null, requestedAgentId: null, requestDigest: null, lastEventId: phase === "terminal" ? "opaque" : null, lastEventFingerprints: [],
|
||||
sourceSequence: phase === "terminal" ? 1 : 0, terminalStatus: phase === "terminal" ? "ERROR" : null, deadlineAt: null, pendingTerminalDelivery: null, acceptedCompletion: null };
|
||||
await store.write(checkpoint); await store.release();
|
||||
const input = { runtimeDirectory, driverSessionId: "session", ownerRunId: "owner", binding, socketPath: join(root, "mcp.sock"), archiveDirectory: join(root, "continuity-break", "cursor-cloud") };
|
||||
return { root, store, checkpoint, input };
|
||||
}
|
||||
|
||||
describe("Cursor Cloud checkpoint ownership", () => {
|
||||
it.each(["", 123, "a".repeat(42), "a".repeat(44), "!".repeat(43)])("rejects malformed persisted turn capabilities (%s)", async token => {
|
||||
const f = await fixture();
|
||||
await f.store.acquire();
|
||||
try {
|
||||
await writeFile(f.store.path, JSON.stringify({ ...f.checkpoint, relayTurnToken: token }), { mode: 0o600 });
|
||||
await expect(f.store.read()).rejects.toThrow("Invalid Cursor Cloud checkpoint");
|
||||
} finally { await f.store.release(); }
|
||||
});
|
||||
|
||||
|
||||
it("does not let a delayed dead-owner contender unlink its successor's lock", async () => {
|
||||
const f = await fixture();
|
||||
const lockPath = `${f.store.path}.lock`;
|
||||
await writeFile(lockPath, JSON.stringify(deadOwner()), { mode: 0o600 });
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const firstChecked = deferred(); const secondChecked = deferred();
|
||||
const resumeFirst = deferred(); const resumeSecond = deferred();
|
||||
let lockReads = 0;
|
||||
vi.mocked(lstat).mockImplementation((async (path: Parameters<typeof lstat>[0], options: Parameters<typeof lstat>[1]) => {
|
||||
const stat = await original.lstat(path, options);
|
||||
if (String(path) === lockPath) {
|
||||
const read = ++lockReads;
|
||||
if (read === 2) { firstChecked.resolve(); await resumeFirst.promise; }
|
||||
if (read === 4) { secondChecked.resolve(); await resumeSecond.promise; }
|
||||
}
|
||||
return stat;
|
||||
}) as typeof lstat);
|
||||
const first = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
const second = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
const firstResult = first.acquire();
|
||||
let secondResult: Promise<{ acquired: boolean; error?: unknown }> | undefined;
|
||||
try {
|
||||
await firstChecked.promise;
|
||||
secondResult = second.acquire().then(() => ({ acquired: true }), error => ({ acquired: false, error }));
|
||||
await Promise.race([secondChecked.promise, secondResult]);
|
||||
resumeFirst.resolve(); await firstResult;
|
||||
const firstLock = await readFile(lockPath, "utf8");
|
||||
resumeSecond.resolve();
|
||||
expect(await secondResult).toMatchObject({ acquired: false, error: expect.objectContaining({ message: "Cursor Cloud checkpoint acquisition is in progress or ambiguous" }) });
|
||||
expect(await readFile(lockPath, "utf8")).toBe(firstLock);
|
||||
await first.write({ ...f.checkpoint, sourceSequence: 2 });
|
||||
expect((await first.read())?.sourceSequence).toBe(2);
|
||||
} finally {
|
||||
resumeFirst.resolve(); resumeSecond.resolve();
|
||||
await firstResult.catch(() => {}); await secondResult;
|
||||
await first.release().catch(() => {}); await second.release().catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["live", "dead", "corrupt", "symlink"])("preserves a %s acquisition guard and refuses takeover and archival", async mode => {
|
||||
const f = await fixture();
|
||||
const lockPath = `${f.store.path}.lock`;
|
||||
const guardPath = `${lockPath}.acquire`;
|
||||
const lockBytes = JSON.stringify(deadOwner());
|
||||
await writeFile(lockPath, lockBytes, { mode: 0o600 });
|
||||
const guardBytes = mode === "corrupt" ? "{" : JSON.stringify(mode === "dead" ? deadOwner() : { pid: process.pid, token: "other-acquirer" });
|
||||
if (mode === "symlink") {
|
||||
const target = join(f.root, "retained-acquirer");
|
||||
await writeFile(target, guardBytes, { mode: 0o600 }); await symlink(target, guardPath);
|
||||
} else await writeFile(guardPath, guardBytes, { mode: 0o600 });
|
||||
const checkpointBytes = await readFile(f.store.path, "utf8");
|
||||
const contender = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
try {
|
||||
await expect(contender.acquire()).rejects.toThrow("acquisition");
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow("acquisition");
|
||||
expect(await readFile(lockPath, "utf8")).toBe(lockBytes);
|
||||
expect(await readFile(guardPath, "utf8")).toBe(guardBytes);
|
||||
expect(await readFile(f.store.path, "utf8")).toBe(checkpointBytes);
|
||||
await expect(readdir(f.input.archiveDirectory)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
if (mode === "symlink") expect((await lstat(guardPath)).isSymbolicLink()).toBe(true);
|
||||
} finally { await contender.release().catch(() => {}); }
|
||||
});
|
||||
|
||||
it("automatically recovers a dead main owner and retains exclusive checkpoint and archive ownership", async () => {
|
||||
const f = await fixture(); const lockPath = `${f.store.path}.lock`;
|
||||
await writeFile(lockPath, JSON.stringify(deadOwner()), { mode: 0o600 });
|
||||
const recovered = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
await recovered.acquire();
|
||||
try {
|
||||
expect(recovered.recoveredDeadOwner).toBe(true);
|
||||
expect(await recovered.read()).toEqual(f.checkpoint);
|
||||
const lockBytes = await readFile(lockPath, "utf8");
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow("live owner");
|
||||
expect(await readFile(lockPath, "utf8")).toBe(lockBytes);
|
||||
await expect(lstat(`${lockPath}.acquire`)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally { await recovered.release(); }
|
||||
await writeFile(lockPath, JSON.stringify(deadOwner()), { mode: 0o600 });
|
||||
await archiveCursorCloudSessionCheckpoint(f.input);
|
||||
expect(JSON.parse(await readFile(join(f.input.archiveDirectory, f.store.path.split("/").at(-1)!), "utf8"))).toEqual(f.checkpoint);
|
||||
await expect(lstat(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(lstat(`${lockPath}.acquire`)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("cleans its own acquisition guard when main-lock acquisition fails", async () => {
|
||||
const f = await fixture(); const lockPath = `${f.store.path}.lock`;
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
let failed = false;
|
||||
vi.mocked(open).mockImplementation((async (path, flags, mode) => {
|
||||
if (String(path) === lockPath && flags === "wx" && !failed) {
|
||||
failed = true; throw Object.assign(new Error("fixture main-lock failure"), { code: "EACCES" });
|
||||
}
|
||||
return original.open(path, flags, mode);
|
||||
}) as typeof open);
|
||||
const contender = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
await expect(contender.acquire()).rejects.toThrow();
|
||||
await expect(lstat(`${lockPath}.acquire`)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(await contender.read()).toEqual(f.checkpoint);
|
||||
await contender.acquire(); await contender.release();
|
||||
await expect(lstat(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it.each(["replace", "rewrite", "symlink"])("does not delete an acquisition guard after %s tampering", async mode => {
|
||||
const f = await fixture(); const lockPath = `${f.store.path}.lock`; const guardPath = `${lockPath}.acquire`;
|
||||
const original = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const guardBytes = JSON.stringify({ pid: process.pid, token: "replacement-acquirer" });
|
||||
vi.mocked(open).mockImplementation((async (path, flags, fileMode) => {
|
||||
if (String(path) === lockPath && flags === "wx") {
|
||||
if (mode !== "rewrite") await rm(guardPath);
|
||||
if (mode === "symlink") {
|
||||
const target = join(f.root, "replacement-acquirer");
|
||||
await writeFile(target, guardBytes, { mode: 0o600 }); await symlink(target, guardPath);
|
||||
} else await writeFile(guardPath, guardBytes, { mode: 0o600 });
|
||||
}
|
||||
return original.open(path, flags, fileMode);
|
||||
}) as typeof open);
|
||||
const contender = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
await expect(contender.acquire()).rejects.toThrow("acquisition ownership changed");
|
||||
expect(await readFile(guardPath, "utf8")).toBe(guardBytes);
|
||||
expect(await contender.read()).toEqual(f.checkpoint);
|
||||
await contender.release();
|
||||
expect(await readFile(guardPath, "utf8")).toBe(guardBytes);
|
||||
if (mode === "symlink") expect((await lstat(guardPath)).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["replace", "rewrite", "symlink"])("does not delete the main lock after %s tampering during release", async mode => {
|
||||
const f = await fixture(); const lockPath = `${f.store.path}.lock`;
|
||||
const contender = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session");
|
||||
await contender.acquire();
|
||||
const originalBytes = await readFile(lockPath, "utf8");
|
||||
const replacementBytes = mode === "rewrite" ? JSON.stringify({ pid: process.pid, token: "replacement-owner" }) : originalBytes;
|
||||
if (mode === "replace") {
|
||||
const replacementPath = `${lockPath}.replacement`;
|
||||
await writeFile(replacementPath, replacementBytes, { flag: "wx", mode: 0o600 });
|
||||
const original = await lstat(lockPath), replacement = await lstat(replacementPath);
|
||||
expect([replacement.dev, replacement.ino]).not.toEqual([original.dev, original.ino]);
|
||||
await rename(replacementPath, lockPath);
|
||||
} else {
|
||||
if (mode !== "rewrite") await rm(lockPath);
|
||||
if (mode === "symlink") {
|
||||
const target = join(f.root, "replacement-owner");
|
||||
await writeFile(target, replacementBytes, { mode: 0o600 }); await symlink(target, lockPath);
|
||||
} else await writeFile(lockPath, replacementBytes, { mode: 0o600 });
|
||||
}
|
||||
await expect(contender.release()).rejects.toThrow();
|
||||
expect(await readFile(lockPath, "utf8")).toBe(replacementBytes);
|
||||
expect(await contender.read()).toEqual(f.checkpoint);
|
||||
await expect(lstat(`${lockPath}.acquire`)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
if (mode === "symlink") expect((await lstat(lockPath)).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cursor Cloud replacement checkpoint archive", () => {
|
||||
it.each(["idle", "terminal"] as const)("archives only the exact %s checkpoint and preserves launch and artifact state", async phase => {
|
||||
const f = await fixture(phase);
|
||||
for (const name of ["config.json", "relay-capability.json"]) await writeFile(join(f.input.runtimeDirectory, name), name, { mode: 0o600 });
|
||||
await mkdir(join(f.input.runtimeDirectory, "artifacts"), { mode: 0o700 });
|
||||
await writeFile(join(f.input.runtimeDirectory, "artifacts", "result.txt"), "retained");
|
||||
const bytes = await readFile(f.store.path, "utf8");
|
||||
await archiveCursorCloudSessionCheckpoint(f.input);
|
||||
expect(await readFile(join(f.input.archiveDirectory, f.store.path.split("/").at(-1)!), "utf8")).toBe(bytes);
|
||||
expect((await readdir(f.input.runtimeDirectory)).sort()).toEqual(["artifacts", "config.json", "relay-capability.json"]);
|
||||
for (const name of ["config.json", "relay-capability.json"]) expect(await readFile(join(f.input.runtimeDirectory, name), "utf8")).toBe(name);
|
||||
expect(await readFile(join(f.input.runtimeDirectory, "artifacts", "result.txt"), "utf8")).toBe("retained");
|
||||
const next = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session"); await next.acquire();
|
||||
try { expect(await next.read()).toBeNull(); } finally { await next.release(); }
|
||||
});
|
||||
|
||||
it.each(["requestedAgentId", "idempotencyKey", "requestDigest", "acceptedCompletion"] as const)("does not archive pristine-looking idle state with retained %s evidence", async field => {
|
||||
const f = await fixture("idle");
|
||||
const checkpoint = { ...f.checkpoint, [field]: field === "acceptedCompletion"
|
||||
? { turnId: "turn", callIdDigest: "a".repeat(64), inputDigest: "b".repeat(64), resultDigest: "c".repeat(64) } : "retained" };
|
||||
const bytes = JSON.stringify(checkpoint); await writeFile(f.store.path, bytes);
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow("active, ambiguous or outside");
|
||||
expect(await readFile(f.store.path, "utf8")).toBe(bytes);
|
||||
});
|
||||
|
||||
it.each(["outside", "inside", "dot-child"])("rejects an archive with %s containment", async mode => {
|
||||
const f = await fixture();
|
||||
const bytes = await readFile(f.store.path, "utf8");
|
||||
const archiveDirectory = mode === "outside" ? `${f.root}-other` : join(f.input.runtimeDirectory, mode === "inside" ? "archive" : "..archive");
|
||||
await expect(archiveCursorCloudSessionCheckpoint({ ...f.input, archiveDirectory })).rejects.toThrow("inside the admitted session root");
|
||||
expect(await readFile(f.store.path, "utf8")).toBe(bytes);
|
||||
});
|
||||
|
||||
it("preserves both files when the archive destination already exists", async () => {
|
||||
const f = await fixture();
|
||||
await mkdir(f.input.archiveDirectory, { recursive: true, mode: 0o700 });
|
||||
const destination = join(f.input.archiveDirectory, f.store.path.split("/").at(-1)!);
|
||||
await writeFile(destination, "Existing archive", { mode: 0o600 });
|
||||
const bytes = await readFile(f.store.path, "utf8");
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow("archive already exists");
|
||||
expect(await readFile(f.store.path, "utf8")).toBe(bytes);
|
||||
expect(await readFile(destination, "utf8")).toBe("Existing archive");
|
||||
});
|
||||
|
||||
it("preserves an unknown socket path without archiving its checkpoint", async () => {
|
||||
const f = await fixture();
|
||||
await writeFile(f.input.socketPath, "Unknown owner", { mode: 0o600 });
|
||||
const bytes = await readFile(f.store.path, "utf8");
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow("safe dead-owner recovery proof");
|
||||
expect(await readFile(f.store.path, "utf8")).toBe(bytes);
|
||||
expect(await readFile(f.input.socketPath, "utf8")).toBe("Unknown owner");
|
||||
await expect(readdir(f.input.archiveDirectory)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("does not grant replacement from a missing checkpoint", async () => {
|
||||
const f = await fixture(); await rm(f.store.path);
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow("checkpoint is missing");
|
||||
await expect(readdir(f.input.archiveDirectory)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it.each(["creating", "running", "stopping", "pending-create", "terminal-debt", "deadline", "foreign-owner", "foreign-binding", "foreign-session", "nonpristine-idle", "corrupt", "live-owner", "symlink"])("retains the exact checkpoint on %s refusal", async mode => {
|
||||
const f = await fixture();
|
||||
const checkpoint = { ...f.checkpoint };
|
||||
if (["creating", "running", "stopping"].includes(mode)) checkpoint.phase = mode as CursorCloudCheckpoint["phase"];
|
||||
if (mode === "pending-create") checkpoint.pendingOperation = "create_run";
|
||||
if (mode === "terminal-debt") checkpoint.pendingTerminalDelivery = { schema: "paperclip.cursor-cloud.terminal-delivery.v1", threadId: "session", turnId: "turn-provider", providerRunId: "run-provider", receiptId: "11111111-1111-4111-8111-111111111111", status: "ERROR", interrupted: false, result: null };
|
||||
if (mode === "nonpristine-idle") Object.assign(checkpoint, { phase: "idle", agentId: null, runId: null, turnId: null, terminalStatus: null });
|
||||
if (mode === "deadline") checkpoint.deadlineAt = Date.now() + 60_000;
|
||||
if (mode === "foreign-owner") checkpoint.ownerRunId = "other-owner";
|
||||
if (mode === "foreign-binding") checkpoint.bindingDigest = "f".repeat(64);
|
||||
if (mode === "foreign-session") checkpoint.normalizedSessionId = "other-session";
|
||||
await writeFile(f.store.path, mode === "corrupt" ? "{" : JSON.stringify(checkpoint), { mode: 0o600 });
|
||||
let held: CursorCloudCheckpointStore | undefined;
|
||||
if (mode === "live-owner") { held = new CursorCloudCheckpointStore(f.input.runtimeDirectory, "session"); await held.acquire(); }
|
||||
if (mode === "symlink") { const moved = join(f.root, "retained.json"); await writeFile(moved, JSON.stringify(checkpoint)); await rm(f.store.path); await symlink(moved, f.store.path); }
|
||||
const bytes = await readFile(f.store.path, "utf8");
|
||||
try {
|
||||
await expect(archiveCursorCloudSessionCheckpoint(f.input)).rejects.toThrow();
|
||||
expect(await readFile(f.store.path, "utf8")).toBe(bytes);
|
||||
await expect(readdir(f.input.archiveDirectory)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally { await held?.release(); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { semanticCanonicalJson } from "../semantic-tool-receipt.js";
|
||||
import { parseCursorCloudBinding, type CursorCloudBinding } from "./config.js";
|
||||
import { releaseCursorCloudStaleSocket } from "./stale-socket.js";
|
||||
|
||||
export const cursorCloudDigest = (value: string) => createHash("sha256").update(value).digest("hex");
|
||||
|
||||
export interface CursorCloudTerminalDelivery {
|
||||
schema: "paperclip.cursor-cloud.terminal-delivery.v1";
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
providerRunId: string;
|
||||
receiptId: string;
|
||||
status: "FINISHED" | "ERROR" | "CANCELLED" | "EXPIRED";
|
||||
interrupted: boolean;
|
||||
result: string | null;
|
||||
}
|
||||
|
||||
export interface CursorCloudCheckpoint {
|
||||
schema: "paperclip.cursor-cloud.checkpoint.v1";
|
||||
driverSessionId: string;
|
||||
normalizedSessionId: string;
|
||||
bindingDigest: string;
|
||||
configDigest: string;
|
||||
modelDigest: string;
|
||||
instructionsDigest: string;
|
||||
instructionsRevisionPending: boolean;
|
||||
ownerRunId: string;
|
||||
agentId: string | null;
|
||||
runId: string | null;
|
||||
turnId: string | null;
|
||||
relayTurnToken?: string | null;
|
||||
phase: "idle" | "creating" | "running" | "stopping" | "terminal";
|
||||
pendingOperation: "create_agent" | "create_run" | null;
|
||||
idempotencyKey: string | null;
|
||||
requestedAgentId: string | null;
|
||||
requestDigest: string | null;
|
||||
lastEventId: string | null;
|
||||
lastEventFingerprints: string[];
|
||||
sourceSequence: number;
|
||||
terminalStatus: string | null;
|
||||
deadlineAt: number | null;
|
||||
pendingTerminalDelivery: CursorCloudTerminalDelivery | null;
|
||||
acceptedCompletion: { callIdDigest: string; inputDigest: string; resultDigest: string; turnId: string } | null;
|
||||
}
|
||||
|
||||
export class CursorCloudCheckpointStore {
|
||||
readonly path: string;
|
||||
readonly #lockPath: string;
|
||||
readonly #root: string;
|
||||
#lockToken: string | null = null;
|
||||
#lockIdentity: { dev: number; ino: number } | null = null;
|
||||
recoveredDeadOwner = false;
|
||||
#writes: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(root: string, sessionId: string) {
|
||||
if (!isAbsolute(root) || !sessionId || sessionId.length > 1_024) throw new Error("Cursor Cloud checkpoint requires a private absolute directory and session identity");
|
||||
this.#root = root;
|
||||
this.path = join(root, `checkpoint-${cursorCloudDigest(sessionId)}.json`);
|
||||
this.#lockPath = `${this.path}.lock`;
|
||||
}
|
||||
|
||||
async acquire(): Promise<void> {
|
||||
await mkdir(this.#root, { recursive: true, mode: 0o700 });
|
||||
const root = await lstat(this.#root);
|
||||
if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o077) !== 0 || (process.getuid && root.uid !== process.getuid())) throw new Error("Cursor Cloud runtime directory must be owned and private (0700)");
|
||||
const token = randomUUID();
|
||||
const guardPath = `${this.#lockPath}.acquire`;
|
||||
let guard;
|
||||
try { guard = await open(guardPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600); }
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "EEXIST" || (error as NodeJS.ErrnoException).code === "ELOOP") throw new Error("Cursor Cloud checkpoint acquisition is in progress or ambiguous");
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
const held = await guard.stat();
|
||||
await guard.writeFile(JSON.stringify({ pid: process.pid, token })); await guard.sync();
|
||||
try {
|
||||
// Serialize the complete stale-lock decision and replacement, not just unlink.
|
||||
// An abandoned guard is uncertainty: never recursively take over another guard.
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
const handle = await open(this.#lockPath, "wx", 0o600);
|
||||
let identity;
|
||||
try { identity = await handle.stat(); await handle.writeFile(JSON.stringify({ pid: process.pid, token })); await handle.sync(); }
|
||||
finally { await handle.close(); }
|
||||
this.#lockIdentity = { dev: identity.dev, ino: identity.ino };
|
||||
this.#lockToken = token;
|
||||
return;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "EEXIST" || attempt !== 0) throw new Error("Cursor Cloud session already has an owner");
|
||||
const before = await lstat(this.#lockPath);
|
||||
if (!before.isFile() || before.isSymbolicLink() || (before.mode & 0o077) !== 0) throw new Error("Cursor Cloud checkpoint ownership is unsafe");
|
||||
let owner: { pid?: unknown };
|
||||
try { owner = JSON.parse(await readFile(this.#lockPath, "utf8")); }
|
||||
catch { throw new Error("Cursor Cloud checkpoint owner is unknown"); }
|
||||
if (!Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0) throw new Error("Cursor Cloud checkpoint owner is unknown");
|
||||
try { process.kill(Number(owner.pid), 0); throw new Error("Cursor Cloud session already has a live owner"); }
|
||||
catch (cause) { if ((cause as NodeJS.ErrnoException).code !== "ESRCH") throw cause; }
|
||||
const current = await lstat(this.#lockPath);
|
||||
if (current.ino !== before.ino || current.dev !== before.dev) throw new Error("Cursor Cloud checkpoint owner changed");
|
||||
await unlink(this.#lockPath);
|
||||
this.recoveredDeadOwner = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
let current;
|
||||
try {
|
||||
current = await open(guardPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
||||
const stat = await current.stat();
|
||||
if (!stat.isFile() || stat.dev !== held.dev || stat.ino !== held.ino || (stat.mode & 0o077) !== 0
|
||||
|| (process.getuid && stat.uid !== process.getuid()) || stat.size > 1_024) throw new Error("Cursor Cloud checkpoint acquisition ownership changed");
|
||||
const owner = JSON.parse(await current.readFile("utf8"));
|
||||
if (owner.pid !== process.pid || owner.token !== token) throw new Error("Cursor Cloud checkpoint acquisition ownership changed");
|
||||
} catch { throw new Error("Cursor Cloud checkpoint acquisition ownership changed"); }
|
||||
finally { await current?.close(); }
|
||||
await unlink(guardPath);
|
||||
}
|
||||
} finally { await guard.close(); }
|
||||
}
|
||||
|
||||
async read(): Promise<CursorCloudCheckpoint | null> {
|
||||
let handle;
|
||||
try { handle = await open(this.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw error; }
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > 524_288) throw new Error("Cursor Cloud checkpoint is unsafe or exceeds its bound");
|
||||
const value: unknown = JSON.parse(await handle.readFile("utf8"));
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Cursor Cloud checkpoint");
|
||||
const cp = value as CursorCloudCheckpoint;
|
||||
if (cp.deadlineAt === undefined) cp.deadlineAt = ["creating", "running", "stopping"].includes(cp.phase) ? 0 : null;
|
||||
if (cp.pendingTerminalDelivery === undefined) cp.pendingTerminalDelivery = null;
|
||||
if (cp.instructionsRevisionPending === undefined) cp.instructionsRevisionPending = false;
|
||||
if (cp.relayTurnToken === undefined) cp.relayTurnToken = null;
|
||||
const digestFields = [cp.bindingDigest, cp.configDigest, cp.modelDigest, cp.instructionsDigest];
|
||||
if (cp.schema !== "paperclip.cursor-cloud.checkpoint.v1" || !digestFields.every(v => typeof v === "string" && /^[a-f0-9]{64}$/.test(v))
|
||||
|| !["idle", "creating", "running", "stopping", "terminal"].includes(cp.phase)
|
||||
|| !Number.isSafeInteger(cp.sourceSequence) || cp.sourceSequence < 0
|
||||
|| typeof cp.instructionsRevisionPending !== "boolean"
|
||||
|| (cp.relayTurnToken !== null && (typeof cp.relayTurnToken !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(cp.relayTurnToken)))
|
||||
|| (cp.deadlineAt !== null && (!Number.isSafeInteger(cp.deadlineAt) || cp.deadlineAt < 0))
|
||||
|| !Array.isArray(cp.lastEventFingerprints) || cp.lastEventFingerprints.length > 32
|
||||
|| !cp.lastEventFingerprints.every(v => typeof v === "string" && /^[a-f0-9]{64}$/.test(v))
|
||||
|| ![cp.driverSessionId, cp.normalizedSessionId, cp.ownerRunId].every(v => typeof v === "string" && v.length > 0 && v.length <= 1_024)
|
||||
|| ![cp.agentId, cp.runId, cp.turnId, cp.idempotencyKey, cp.requestedAgentId, cp.requestDigest, cp.lastEventId, cp.terminalStatus].every(v => v === null || (typeof v === "string" && v.length <= 1_024))
|
||||
|| ![null, "create_agent", "create_run"].includes(cp.pendingOperation)) throw new Error("Invalid Cursor Cloud checkpoint");
|
||||
const pending = cp.pendingTerminalDelivery;
|
||||
if (pending !== null && (typeof pending !== "object" || Array.isArray(pending)
|
||||
|| pending.schema !== "paperclip.cursor-cloud.terminal-delivery.v1" || pending.threadId !== cp.driverSessionId
|
||||
|| pending.turnId !== cp.turnId || pending.providerRunId !== cp.runId
|
||||
|| typeof pending.receiptId !== "string" || !/^[a-f0-9-]{36}$/.test(pending.receiptId)
|
||||
|| !["FINISHED", "ERROR", "CANCELLED", "EXPIRED"].includes(pending.status)
|
||||
|| typeof pending.interrupted !== "boolean" || (pending.result !== null && (typeof pending.result !== "string" || pending.result.length > 65_536)))) throw new Error("Invalid Cursor Cloud terminal delivery");
|
||||
return cp;
|
||||
} finally { await handle.close(); }
|
||||
}
|
||||
|
||||
write(value: CursorCloudCheckpoint): Promise<void> {
|
||||
if (!this.#lockToken) return Promise.reject(new Error("Cursor Cloud checkpoint has no owner"));
|
||||
const bytes = JSON.stringify(value);
|
||||
if (Buffer.byteLength(bytes) > 524_288) return Promise.reject(new Error("Cursor Cloud checkpoint exceeds its bound"));
|
||||
const operation = this.#writes.then(async () => {
|
||||
const temporary = `${this.path}.${randomUUID()}.tmp`;
|
||||
const handle = await open(temporary, "wx", 0o600);
|
||||
try { await handle.writeFile(bytes); await handle.sync(); }
|
||||
finally { await handle.close(); }
|
||||
try {
|
||||
await rename(temporary, this.path);
|
||||
const directory = await open(this.#root, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
||||
try { await directory.sync(); } finally { await directory.close(); }
|
||||
} finally { await unlink(temporary).catch(() => {}); }
|
||||
});
|
||||
this.#writes = operation.catch(() => {});
|
||||
return operation;
|
||||
}
|
||||
|
||||
async release(): Promise<void> {
|
||||
await this.#writes;
|
||||
if (!this.#lockToken || !this.#lockIdentity) return;
|
||||
const handle = await open(this.#lockPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
if (!stat.isFile() || stat.dev !== this.#lockIdentity.dev || stat.ino !== this.#lockIdentity.ino || (stat.mode & 0o077) !== 0
|
||||
|| (process.getuid && stat.uid !== process.getuid()) || stat.size > 1_024) throw new Error("Cursor Cloud checkpoint ownership changed");
|
||||
const current = JSON.parse(await handle.readFile("utf8"));
|
||||
if (current.token !== this.#lockToken || current.pid !== process.pid) throw new Error("Cursor Cloud checkpoint ownership changed");
|
||||
} finally { await handle.close(); }
|
||||
await unlink(this.#lockPath);
|
||||
this.#lockToken = null;
|
||||
this.#lockIdentity = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function archiveCursorCloudSessionCheckpoint(input: {
|
||||
runtimeDirectory: string;
|
||||
driverSessionId: string;
|
||||
ownerRunId: string;
|
||||
binding: CursorCloudBinding;
|
||||
socketPath: string;
|
||||
archiveDirectory: string;
|
||||
}): Promise<void> {
|
||||
const binding = parseCursorCloudBinding(input.binding);
|
||||
if (!input.ownerRunId || !isAbsolute(input.archiveDirectory) || !isAbsolute(input.socketPath)) throw new Error("Cursor Cloud replacement requires exact private bindings");
|
||||
const archiveDirectory = resolve(input.archiveDirectory);
|
||||
const sessionRoot = dirname(resolve(input.runtimeDirectory));
|
||||
const archiveRelative = relative(sessionRoot, archiveDirectory);
|
||||
const escapes = (path: string) => path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path);
|
||||
if (!archiveRelative || escapes(archiveRelative) || !escapes(relative(resolve(input.runtimeDirectory), archiveDirectory))) {
|
||||
throw new Error("Cursor Cloud checkpoint archive must be separate state inside the admitted session root");
|
||||
}
|
||||
const store = new CursorCloudCheckpointStore(input.runtimeDirectory, input.driverSessionId);
|
||||
await store.acquire();
|
||||
try {
|
||||
const checkpoint = await store.read();
|
||||
if (!checkpoint) throw new Error("Cursor Cloud replacement checkpoint is missing");
|
||||
const bound = checkpoint.driverSessionId === input.driverSessionId && checkpoint.normalizedSessionId === binding.normalizedSessionId
|
||||
&& checkpoint.ownerRunId === input.ownerRunId && checkpoint.bindingDigest === cursorCloudDigest(semanticCanonicalJson(binding));
|
||||
const pristine = checkpoint.phase === "idle" && checkpoint.agentId === null && checkpoint.runId === null && checkpoint.turnId === null && checkpoint.terminalStatus === null
|
||||
&& checkpoint.sourceSequence === 0 && checkpoint.lastEventId === null && checkpoint.lastEventFingerprints.length === 0
|
||||
&& checkpoint.requestedAgentId === null && checkpoint.idempotencyKey === null && checkpoint.requestDigest === null && checkpoint.acceptedCompletion === null;
|
||||
const terminal = checkpoint.phase === "terminal" && typeof checkpoint.agentId === "string" && checkpoint.agentId.length > 0
|
||||
&& typeof checkpoint.runId === "string" && checkpoint.runId.length > 0 && typeof checkpoint.turnId === "string" && checkpoint.turnId.length > 0
|
||||
&& ["FINISHED", "ERROR", "CANCELLED", "EXPIRED"].includes(checkpoint.terminalStatus ?? "");
|
||||
if (!bound || (!pristine && !terminal) || checkpoint.pendingOperation !== null || checkpoint.pendingTerminalDelivery !== null || checkpoint.deadlineAt !== null) {
|
||||
throw new Error("Cursor Cloud replacement checkpoint is active, ambiguous or outside the admitted session");
|
||||
}
|
||||
await releaseCursorCloudStaleSocket(input.socketPath, store.recoveredDeadOwner);
|
||||
await mkdir(archiveDirectory, { recursive: true, mode: 0o700 });
|
||||
for (let directory = archiveDirectory; ; directory = dirname(directory)) {
|
||||
const stat = await lstat(directory);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid())) throw new Error("Cursor Cloud checkpoint archive must be owned and private (0700)");
|
||||
const handle = await open(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
||||
try { await handle.sync(); } finally { await handle.close(); }
|
||||
if (directory === sessionRoot) break;
|
||||
}
|
||||
const destination = join(archiveDirectory, basename(store.path));
|
||||
try { await lstat(destination); throw new Error("Cursor Cloud checkpoint archive already exists"); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
||||
await rename(store.path, destination);
|
||||
for (const directory of [archiveDirectory, input.runtimeDirectory]) {
|
||||
const handle = await open(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
||||
try { await handle.sync(); } finally { await handle.close(); }
|
||||
}
|
||||
} finally { await store.release(); }
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
CursorCloudClient,
|
||||
CursorCloudClientError,
|
||||
isCursorCloudUnsentRequestError,
|
||||
isCursorCloudRunActive,
|
||||
} from "./cloud-client.js";
|
||||
|
||||
const AGENT = "bc-00000000-0000-0000-0000-000000000001";
|
||||
const RUN = "run-00000000-0000-0000-0000-000000000001";
|
||||
const OTHER_RUN = "run-00000000-0000-0000-0000-000000000002";
|
||||
const API_KEY = "cursor-provider-secret-canary";
|
||||
const TIME = "2026-10-28T16:00:00.000Z";
|
||||
const agent = { id: AGENT, status: "ACTIVE", env: { type: "cloud" }, url: `https://cursor.com/agents/${AGENT}`, createdAt: TIME, updatedAt: TIME };
|
||||
const run = { id: RUN, agentId: AGENT, status: "CREATING", createdAt: TIME, updatedAt: TIME };
|
||||
const cleanup: Array<() => Promise<void>> = [];
|
||||
|
||||
afterEach(async () => { await Promise.all(cleanup.splice(0).map(close => close())); });
|
||||
|
||||
async function serve(handler: (request: IncomingMessage, response: ServerResponse) => void | Promise<void>) {
|
||||
const server = createServer((request, response) => {
|
||||
Promise.resolve(handler(request, response)).catch(() => {
|
||||
if (!response.headersSent) response.writeHead(500);
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
cleanup.push(async () => {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("No test server address");
|
||||
return `http://127.0.0.1:${address.port}`;
|
||||
}
|
||||
|
||||
function json(response: ServerResponse, value: unknown, status = 200) {
|
||||
response.writeHead(status, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify(value));
|
||||
}
|
||||
|
||||
async function body(request: IncomingMessage): Promise<unknown> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of request) chunks.push(Buffer.from(chunk));
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
}
|
||||
|
||||
async function collect<T>(source: AsyncIterable<T>): Promise<T[]> {
|
||||
const result: T[] = [];
|
||||
for await (const item of source) result.push(item);
|
||||
return result;
|
||||
}
|
||||
|
||||
describe("Cursor Cloud v1 HTTP client", () => {
|
||||
it("uses authenticated native paths and preserves exact model, MCP, and run-scoped environment payloads", async () => {
|
||||
const requests: Array<{ path: string; method: string; authorization: string | undefined; key: string | undefined; body?: unknown }> = [];
|
||||
const baseUrl = await serve(async (request, response) => {
|
||||
const entry = {
|
||||
path: request.url!, method: request.method!, authorization: request.headers.authorization,
|
||||
key: request.headers["idempotency-key"] as string | undefined,
|
||||
...(["POST"].includes(request.method!) && !request.url?.endsWith("/cancel") ? { body: await body(request) } : {}),
|
||||
};
|
||||
requests.push(entry);
|
||||
if (entry.path === "/v1/agents" && entry.method === "POST") return json(response, { agent, run }, 201);
|
||||
if (entry.path === `/v1/agents/${AGENT}/runs` && entry.method === "POST") return json(response, { run: { ...run, id: OTHER_RUN } }, 201);
|
||||
if (entry.path.endsWith("/cancel")) return json(response, { id: RUN });
|
||||
if (entry.path === `/v1/agents/${AGENT}` && entry.method === "DELETE") return json(response, { id: AGENT });
|
||||
if (entry.path === `/v1/agents/${AGENT}`) return json(response, agent);
|
||||
if (entry.path === `/v1/agents/${AGENT}/runs/${RUN}`) return json(response, { ...run, status: "WAITING_FOR_BACKGROUND_WORK" });
|
||||
if (entry.path === "/v1/models") return json(response, { items: [{ id: "future-model/exact", displayName: "Future model" }] });
|
||||
if (entry.path.endsWith("/artifacts")) return json(response, { items: [{ path: "proof.json", sizeBytes: 32, updatedAt: TIME }] });
|
||||
json(response, {}, 404);
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
const model = { id: "future-model/exact", params: [{ id: "thinking", value: "high" }] };
|
||||
const mcpServers = [{ name: "paperclip", type: "http" as const, url: "https://tools.example.test/mcp", headers: { Authorization: "Bearer run-secret" } }];
|
||||
expect(await client.createAgent({ prompt: { text: "Create the proof" }, model, repos: [], mcpServers }, { idempotencyKey: "initial-receipt" })).toEqual({ agent, run });
|
||||
await client.createRun(AGENT, { prompt: { text: "Continue" }, model, envVars: { PAPERCLIP_RUN_ID: "new-run" }, mcpServers }, { idempotencyKey: "followup-receipt" });
|
||||
expect((await client.getRun(AGENT, RUN)).status).toBe("WAITING_FOR_BACKGROUND_WORK");
|
||||
expect(await client.getAgent(AGENT)).toEqual(agent);
|
||||
expect(await client.cancelRun(AGENT, RUN)).toEqual({ id: RUN });
|
||||
expect(await client.listModels()).toEqual({ items: [{ id: "future-model/exact", displayName: "Future model" }] });
|
||||
expect(await client.listArtifacts(AGENT)).toEqual({ items: [{ path: "proof.json", sizeBytes: 32, updatedAt: TIME }] });
|
||||
expect(await client.deleteAgent(AGENT)).toEqual({ id: AGENT });
|
||||
expect(requests.every(entry => entry.authorization === `Bearer ${API_KEY}`)).toBe(true);
|
||||
expect(requests[0]).toMatchObject({ path: "/v1/agents", method: "POST", key: "initial-receipt", body: { prompt: { text: "Create the proof" }, model, repos: [], mcpServers } });
|
||||
expect(requests[1]).toMatchObject({ path: `/v1/agents/${AGENT}/runs`, key: "followup-receipt", body: { envVars: { PAPERCLIP_RUN_ID: "new-run" }, model, mcpServers } });
|
||||
expect(isCursorCloudRunActive("WAITING_FOR_BACKGROUND_WORK")).toBe(true);
|
||||
expect(isCursorCloudRunActive("EXPIRED")).toBe(false);
|
||||
});
|
||||
|
||||
it("retains safe HTTP metadata without echoing provider messages or retrying mutations", async () => {
|
||||
let calls = 0;
|
||||
const baseUrl = await serve((_request, response) => {
|
||||
calls += 1;
|
||||
response.setHeader("Retry-After", "2");
|
||||
json(response, { error: { code: "feature_unavailable", message: `Failure with ${API_KEY}`, token: API_KEY } }, 503);
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
const error = await client.createAgent({ prompt: { text: "Do work" } }).catch(error => error);
|
||||
expect(error).toBeInstanceOf(CursorCloudClientError);
|
||||
expect(error).toMatchObject({ code: "http_error", status: 503, providerCode: "feature_unavailable", retryAfterMs: 2_000, retryable: true });
|
||||
expect(String(error)).not.toContain(API_KEY);
|
||||
expect(JSON.stringify(error)).not.toContain(API_KEY);
|
||||
expect(error.cause).toBeUndefined();
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
it("scopes token usage to the exact run and obtains an artifact URL without forwarding credentials to it", async () => {
|
||||
const paths: string[] = [];
|
||||
const usage = { inputTokens: 3, outputTokens: 2, cacheWriteTokens: 0, cacheReadTokens: 1, totalTokens: 6 };
|
||||
const baseUrl = await serve((request, response) => {
|
||||
paths.push(request.url!);
|
||||
if (request.url?.includes("/usage?")) return json(response, { totalUsage: usage, runs: [{ id: RUN, usage }] });
|
||||
if (request.url?.includes("/artifacts/download?")) return json(response, { url: "https://artifacts.example.test/proof?signature=private", expiresAt: TIME });
|
||||
json(response, { ...agent, status: "IDLE" });
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
expect(await client.getUsage(AGENT, { runId: RUN })).toEqual({ totalUsage: usage, runs: [{ id: RUN, usage }] });
|
||||
expect(await client.getArtifactDownloadUrl(AGENT, "artifacts/proof file.json")).toEqual({ url: "https://artifacts.example.test/proof?signature=private", expiresAt: TIME });
|
||||
expect((await client.getAgent(AGENT)).status).toBe("IDLE");
|
||||
expect(paths).toEqual([
|
||||
`/v1/agents/${AGENT}/usage?runId=${RUN}`,
|
||||
`/v1/agents/${AGENT}/artifacts/download?path=artifacts%2Fproof+file.json`,
|
||||
`/v1/agents/${AGENT}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("downloads only the qualified artifact host with no provider credentials and bounded binary bytes", async () => {
|
||||
const calls: Array<{ path: string; authorization?: string }> = [];
|
||||
const bytes = Buffer.from([0, 255, 0, 8, 127]);
|
||||
let host = "agent-stores.s3.us-east-1.amazonaws.com";
|
||||
const baseUrl = await serve((request, response) => {
|
||||
calls.push({ path: request.url!, authorization: request.headers.authorization });
|
||||
if (request.url?.includes("/artifacts/download?")) return json(response, { url: `https://${host}/proof?private-signature`, expiresAt: TIME });
|
||||
response.end(bytes);
|
||||
});
|
||||
const externalHeaders: HeadersInit[] = [];
|
||||
let unexpectedTargets = 0;
|
||||
const transport: typeof fetch = (input, init) => {
|
||||
const url = String(input);
|
||||
if (url.startsWith("https://agent-stores.s3.us-east-1.amazonaws.com/")) {
|
||||
externalHeaders.push(init?.headers ?? {});
|
||||
expect(init?.redirect).toBe("error");
|
||||
return fetch(`${baseUrl}/download`, init);
|
||||
}
|
||||
if (url.startsWith("https://")) { unexpectedTargets += 1; return Promise.reject(new TypeError("Unqualified test target")); }
|
||||
return fetch(input, init);
|
||||
};
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl, fetch: transport });
|
||||
expect(await client.downloadArtifact(AGENT, "proof.bin", { maxBytes: 5 })).toEqual(bytes);
|
||||
expect(externalHeaders).toEqual([{}]);
|
||||
expect(calls[0]?.authorization).toBe(`Bearer ${API_KEY}`);
|
||||
expect(calls[1]?.authorization).toBeUndefined();
|
||||
const downloadError = await client.downloadArtifact(AGENT, "proof.bin", { maxBytes: 4 }).catch(error => error);
|
||||
expect(downloadError).toMatchObject({ code: "payload_too_large" });
|
||||
expect(isCursorCloudUnsentRequestError(downloadError)).toBe(false);
|
||||
const downloads = calls.filter(call => call.path === "/download").length;
|
||||
host = "cloud-agent-artifacts.s3.us-east-1.amazonaws.com";
|
||||
await expect(client.downloadArtifact(AGENT, "proof.bin")).rejects.toMatchObject({ code: "invalid_response" });
|
||||
host = "127.0.0.1";
|
||||
await expect(client.downloadArtifact(AGENT, "proof.bin")).rejects.toMatchObject({ code: "invalid_response" });
|
||||
expect(calls.filter(call => call.path === "/download")).toHaveLength(downloads);
|
||||
expect(unexpectedTargets).toBe(0);
|
||||
await expect(client.downloadArtifact(AGENT, "../../escape")).rejects.toMatchObject({ code: "invalid_request" });
|
||||
});
|
||||
|
||||
it("rejects conflicting identities, unknown statuses, and secret-shaped error codes", async () => {
|
||||
let attempt = 0;
|
||||
const baseUrl = await serve((_request, response) => {
|
||||
attempt += 1;
|
||||
if (attempt === 1) return json(response, { ...run, id: OTHER_RUN });
|
||||
if (attempt === 2) return json(response, { ...run, status: "UNRECOGNIZED" });
|
||||
json(response, { code: API_KEY, message: API_KEY }, 400);
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
await expect(client.getRun(AGENT, RUN)).rejects.toMatchObject({ code: "invalid_response" });
|
||||
await expect(client.getRun(AGENT, RUN)).rejects.toMatchObject({ code: "invalid_response" });
|
||||
const error = await client.getAgent(AGENT).catch(error => error);
|
||||
expect(error.providerCode).toBeUndefined();
|
||||
expect(JSON.stringify(error)).not.toContain(API_KEY);
|
||||
});
|
||||
|
||||
it("bounds request and JSON response bytes before preserving their contents", async () => {
|
||||
let calls = 0;
|
||||
const baseUrl = await serve((_request, response) => { calls += 1; json(response, { items: ["x".repeat(1_024)] }); });
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl, maxRequestBytes: 100, maxResponseBytes: 100 });
|
||||
const requestError = await client.createAgent({ prompt: { text: "x".repeat(200) } }).catch(error => error);
|
||||
expect(requestError).toMatchObject({ code: "payload_too_large" });
|
||||
expect(isCursorCloudUnsentRequestError(requestError)).toBe(true);
|
||||
expect(calls).toBe(0);
|
||||
const responseError = await client.listModels().catch(error => error);
|
||||
expect(responseError).toMatchObject({ code: "payload_too_large" });
|
||||
expect(isCursorCloudUnsentRequestError(responseError)).toBe(false);
|
||||
expect(isCursorCloudUnsentRequestError(new CursorCloudClientError("payload_too_large"))).toBe(false);
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
it("aborts actual HTTP work for deadlines and operator cancellation", async () => {
|
||||
let calls = 0;
|
||||
let signalReady: (() => void) | undefined;
|
||||
const ready = new Promise<void>(resolve => { signalReady = resolve; });
|
||||
let signalClosed: (() => void) | undefined;
|
||||
const closed = new Promise<void>(resolve => { signalClosed = resolve; });
|
||||
const baseUrl = await serve((request, _response) => {
|
||||
calls += 1;
|
||||
if (calls === 2) {
|
||||
request.on("close", () => signalClosed?.());
|
||||
signalReady?.();
|
||||
}
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl, requestTimeoutMs: 35 });
|
||||
await expect(client.getAgent(AGENT)).rejects.toMatchObject({ code: "timeout" });
|
||||
const controller = new AbortController();
|
||||
const pending = client.getAgent(AGENT, { signal: controller.signal, timeoutMs: 5_000 });
|
||||
await ready;
|
||||
controller.abort(new Error(`Do not retain ${API_KEY}`));
|
||||
const error = await pending.catch(error => error);
|
||||
expect(error).toMatchObject({ code: "cancelled" });
|
||||
expect(String(error)).not.toContain(API_KEY);
|
||||
await closed;
|
||||
expect(calls).toBe(2);
|
||||
});
|
||||
|
||||
it("rejects malformed IDs, credential URLs, header injection, and redirects before credential forwarding", async () => {
|
||||
let calls = 0;
|
||||
const baseUrl = await serve((_request, response) => { calls += 1; response.writeHead(307, { Location: "https://example.test/steal" }); response.end(); });
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
await expect(client.getAgent("bc-../../escape")).rejects.toMatchObject({ code: "invalid_request" });
|
||||
await expect(client.createAgent({ prompt: { text: "task" } }, { idempotencyKey: "unsafe\r\nAuthorization: other" })).rejects.toMatchObject({ code: "invalid_request" });
|
||||
expect(calls).toBe(0);
|
||||
await expect(client.getAgent(AGENT)).rejects.toBeInstanceOf(CursorCloudClientError);
|
||||
expect(calls).toBe(1);
|
||||
expect(() => new CursorCloudClient({ apiKey: API_KEY, baseUrl: "https://user:secret@example.test" })).toThrow(CursorCloudClientError);
|
||||
expect(() => new CursorCloudClient({ apiKey: API_KEY, baseUrl: "http://example.test" })).toThrow(CursorCloudClientError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cursor Cloud SSE wire protocol", () => {
|
||||
it("replays the opaque cursor and decodes fragmented UTF-8, CRLF, multiline data, and shared IDs", async () => {
|
||||
let replay: string | undefined;
|
||||
const baseUrl = await serve((request, response) => {
|
||||
replay = request.headers["last-event-id"] as string | undefined;
|
||||
response.writeHead(200, { "Content-Type": "text/event-stream" });
|
||||
const bytes = Buffer.from(': heartbeat\r\nid: opaque/cursor-7\r\nevent: assistant\r\ndata: {"text":\r\ndata: "héllo 🌍"}\r\n\r\nid: opaque/cursor-7\nevent: interaction_update\ndata: {"type":"text-delta"}\n\nevent: done\ndata: {}\n\n');
|
||||
const split = bytes.indexOf(Buffer.from("🌍")) + 2;
|
||||
response.write(bytes.subarray(0, split));
|
||||
setImmediate(() => { response.write(bytes.subarray(split, split + 1)); setImmediate(() => response.end(bytes.subarray(split + 1))); });
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
expect(await collect(client.streamRun(AGENT, RUN, { lastEventId: "previous-opaque" }))).toEqual([
|
||||
{ event: "assistant", id: "opaque/cursor-7", data: { text: "héllo 🌍" } },
|
||||
{ event: "interaction_update", id: "opaque/cursor-7", data: { type: "text-delta" } },
|
||||
{ event: "done", data: {} },
|
||||
]);
|
||||
expect(replay).toBe("previous-opaque");
|
||||
});
|
||||
|
||||
it("cancels the wire reader when the consumer stops", async () => {
|
||||
let connectionClosed: (() => void) | undefined;
|
||||
const closed = new Promise<void>(resolve => { connectionClosed = resolve; });
|
||||
const baseUrl = await serve((request, response) => {
|
||||
request.once("close", () => connectionClosed?.());
|
||||
response.writeHead(200, { "Content-Type": "text/event-stream" });
|
||||
response.write('event: status\ndata: {"status":"RUNNING"}\n\n');
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
for await (const event of client.streamRun(AGENT, RUN)) { expect(event.event).toBe("status"); break; }
|
||||
await closed;
|
||||
});
|
||||
|
||||
it("bounds an unterminated frame and settles an idle connection", async () => {
|
||||
let calls = 0;
|
||||
const baseUrl = await serve((_request, response) => {
|
||||
calls += 1;
|
||||
response.writeHead(200, { "Content-Type": "text/event-stream" });
|
||||
if (calls === 1) response.end(`event: assistant\ndata: ${"x".repeat(200)}`);
|
||||
else response.flushHeaders();
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl, maxSseFrameBytes: 100 });
|
||||
const streamError = await collect(client.streamRun(AGENT, RUN)).catch(error => error);
|
||||
expect(streamError).toMatchObject({ code: "payload_too_large" });
|
||||
expect(isCursorCloudUnsentRequestError(streamError)).toBe(false);
|
||||
const idleClient = new CursorCloudClient({ apiKey: API_KEY, baseUrl, streamIdleTimeoutMs: 35 });
|
||||
await expect(collect(idleClient.streamRun(AGENT, RUN))).rejects.toMatchObject({ code: "timeout" });
|
||||
});
|
||||
|
||||
it("rejects malformed JSON and preserves stream-expiry metadata without logging the body", async () => {
|
||||
let calls = 0;
|
||||
const baseUrl = await serve((_request, response) => {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
response.writeHead(200, { "Content-Type": "text/event-stream" });
|
||||
response.end(`event: assistant\ndata: {${API_KEY}}\n\n`);
|
||||
return;
|
||||
}
|
||||
json(response, { error: { code: "stream_expired", message: API_KEY } }, 410);
|
||||
});
|
||||
const client = new CursorCloudClient({ apiKey: API_KEY, baseUrl });
|
||||
await expect(collect(client.streamRun(AGENT, RUN))).rejects.toMatchObject({ code: "invalid_response" });
|
||||
const error = await collect(client.streamRun(AGENT, RUN)).catch(error => error);
|
||||
expect(error).toMatchObject({ status: 410, providerCode: "stream_expired", retryable: false });
|
||||
expect(JSON.stringify(error)).not.toContain(API_KEY);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,563 @@
|
||||
export type CursorCloudRunStatus =
|
||||
| "CREATING" | "RUNNING" | "WAITING_FOR_BACKGROUND_WORK"
|
||||
| "FINISHED" | "ERROR" | "CANCELLED" | "EXPIRED";
|
||||
|
||||
export interface CursorCloudModelRef {
|
||||
id: string;
|
||||
params?: Array<{ id: string; value: string }>;
|
||||
}
|
||||
|
||||
export interface CursorCloudRepo {
|
||||
url: string;
|
||||
startingRef?: string;
|
||||
prUrl?: string;
|
||||
}
|
||||
|
||||
export type CursorCloudMcpServer = {
|
||||
name: string;
|
||||
type?: "http" | "sse";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
auth?: { CLIENT_ID: string; CLIENT_SECRET?: string; scopes?: string[] };
|
||||
} | {
|
||||
name: string;
|
||||
type?: "stdio";
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
export interface CursorCloudFollowupInput {
|
||||
prompt: { text: string };
|
||||
model?: CursorCloudModelRef;
|
||||
mcpServers?: CursorCloudMcpServer[];
|
||||
envVars?: Record<string, string>;
|
||||
mode?: "agent" | "plan";
|
||||
}
|
||||
|
||||
export interface CursorCloudCreateInput extends CursorCloudFollowupInput {
|
||||
agentId?: string;
|
||||
name?: string;
|
||||
env?: { type: "cloud" | "pool" | "machine"; name?: string };
|
||||
repos?: CursorCloudRepo[];
|
||||
workOnCurrentBranch?: boolean;
|
||||
autoCreatePR?: boolean;
|
||||
skipReviewerRequest?: boolean;
|
||||
customSubagents?: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
model?: "inherit" | string | CursorCloudModelRef;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CursorCloudAgent {
|
||||
id: string;
|
||||
status: "ACTIVE" | "IDLE" | "ARCHIVED";
|
||||
env: { type: "cloud" | "pool" | "machine"; name?: string };
|
||||
url: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name?: string;
|
||||
latestRunId?: string;
|
||||
repos?: CursorCloudRepo[];
|
||||
}
|
||||
|
||||
export interface CursorCloudRun {
|
||||
id: string;
|
||||
agentId: string;
|
||||
status: CursorCloudRunStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
durationMs?: number;
|
||||
result?: string;
|
||||
git?: { branches: Array<{ repoUrl: string; branch?: string; prUrl?: string }> };
|
||||
}
|
||||
|
||||
export interface CursorCloudModel {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
aliases?: string[];
|
||||
parameters?: Array<{ id: string; displayName?: string; values: Array<{ value: string; displayName?: string }> }>;
|
||||
variants?: Array<{ params: Array<{ id: string; value: string }>; displayName: string; description?: string; isDefault?: boolean }>;
|
||||
}
|
||||
|
||||
export interface CursorCloudArtifact {
|
||||
path: string;
|
||||
sizeBytes: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CursorCloudTokenUsage {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheWriteTokens: number;
|
||||
cacheReadTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
export interface CursorCloudUsage {
|
||||
totalUsage: CursorCloudTokenUsage;
|
||||
runs: Array<{ id: string; usageUuid?: string; usage: CursorCloudTokenUsage }>;
|
||||
}
|
||||
|
||||
export interface CursorCloudSseEvent {
|
||||
event: string;
|
||||
id?: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface CursorCloudRequestOptions {
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface CursorCloudMutationOptions extends CursorCloudRequestOptions {
|
||||
idempotencyKey?: string;
|
||||
}
|
||||
|
||||
export interface CursorCloudStreamOptions extends CursorCloudRequestOptions {
|
||||
lastEventId?: string;
|
||||
}
|
||||
|
||||
export interface CursorCloudClientOptions {
|
||||
apiKey: string;
|
||||
baseUrl?: string;
|
||||
fetch?: typeof globalThis.fetch;
|
||||
requestTimeoutMs?: number;
|
||||
streamIdleTimeoutMs?: number;
|
||||
streamTimeoutMs?: number;
|
||||
maxRequestBytes?: number;
|
||||
maxResponseBytes?: number;
|
||||
maxSseFrameBytes?: number;
|
||||
}
|
||||
|
||||
type ClientErrorCode = "configuration" | "invalid_request" | "http_error" | "network_error"
|
||||
| "invalid_response" | "payload_too_large" | "cancelled" | "timeout";
|
||||
|
||||
export class CursorCloudClientError extends Error {
|
||||
readonly code: ClientErrorCode;
|
||||
readonly status?: number;
|
||||
readonly providerCode?: string;
|
||||
readonly retryAfterMs?: number;
|
||||
readonly retryable: boolean;
|
||||
|
||||
constructor(code: ClientErrorCode, details: {
|
||||
status?: number; providerCode?: string; retryAfterMs?: number;
|
||||
} = {}) {
|
||||
super(`Cursor Cloud request failed (${code}${details.status ? `, HTTP ${details.status}` : ""}).`);
|
||||
this.name = "CursorCloudClientError";
|
||||
this.code = code;
|
||||
this.status = details.status;
|
||||
this.providerCode = details.providerCode;
|
||||
this.retryAfterMs = details.retryAfterMs;
|
||||
this.retryable = code === "network_error" || code === "timeout"
|
||||
|| (details.status !== undefined && (details.status === 408 || details.status === 429 || details.status >= 500));
|
||||
}
|
||||
}
|
||||
|
||||
// This private brand is minted only before fetch. The shared error code also
|
||||
// describes oversized responses, downloads and SSE frames after dispatch.
|
||||
class CursorCloudUnsentRequestError extends CursorCloudClientError {
|
||||
constructor() { super("payload_too_large"); }
|
||||
}
|
||||
|
||||
export function isCursorCloudUnsentRequestError(error: unknown): boolean {
|
||||
return error instanceof CursorCloudUnsentRequestError;
|
||||
}
|
||||
|
||||
const RUN_STATUSES = new Set<CursorCloudRunStatus>([
|
||||
"CREATING", "RUNNING", "WAITING_FOR_BACKGROUND_WORK", "FINISHED", "ERROR", "CANCELLED", "EXPIRED",
|
||||
]);
|
||||
const SAFE_PROVIDER_CODES = new Set([
|
||||
"validation_error", "unauthorized", "unauthenticated", "forbidden", "feature_unavailable",
|
||||
"not_found", "agent_not_found", "run_not_found", "agent_busy", "agent_archived", "agent_id_conflict",
|
||||
"run_not_cancellable", "invalid_model", "invalid_last_event_id", "stream_expired", "rate_limited",
|
||||
"rate_limit_exceeded", "internal_error", "repository_access", "key_not_supported", "integration_not_connected",
|
||||
]);
|
||||
const UUID_BODY = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
||||
const AGENT_ID = new RegExp(`^bc-${UUID_BODY}$`);
|
||||
const RUN_ID = new RegExp(`^run-${UUID_BODY}$`);
|
||||
|
||||
export function isCursorCloudRunActive(status: CursorCloudRunStatus): boolean {
|
||||
return status === "CREATING" || status === "RUNNING" || status === "WAITING_FOR_BACKGROUND_WORK";
|
||||
}
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function invalidResponse(): never { throw new CursorCloudClientError("invalid_response"); }
|
||||
function invalidRequest(): never { throw new CursorCloudClientError("invalid_request"); }
|
||||
function nonempty(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; }
|
||||
function agentId(value: unknown): asserts value is string { if (typeof value !== "string" || !AGENT_ID.test(value)) invalidRequest(); }
|
||||
function runId(value: unknown): asserts value is string { if (typeof value !== "string" || !RUN_ID.test(value)) invalidRequest(); }
|
||||
function headerValue(value: string): string {
|
||||
if (!value || value.length > 1_024 || /[\r\n\0]/.test(value)) invalidRequest();
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseAgent(value: unknown, expectedId?: string): CursorCloudAgent {
|
||||
if (!record(value) || typeof value.id !== "string" || !AGENT_ID.test(value.id)
|
||||
|| (expectedId !== undefined && value.id !== expectedId)
|
||||
|| !["ACTIVE", "IDLE", "ARCHIVED"].includes(String(value.status))
|
||||
|| !record(value.env) || !["cloud", "pool", "machine"].includes(String(value.env.type))
|
||||
|| !nonempty(value.url) || !nonempty(value.createdAt) || !nonempty(value.updatedAt)
|
||||
|| (value.latestRunId !== undefined && (typeof value.latestRunId !== "string" || !RUN_ID.test(value.latestRunId)))) invalidResponse();
|
||||
return value as unknown as CursorCloudAgent;
|
||||
}
|
||||
|
||||
function parseRun(value: unknown, expectedAgentId?: string, expectedRunId?: string): CursorCloudRun {
|
||||
if (!record(value) || typeof value.id !== "string" || !RUN_ID.test(value.id)
|
||||
|| typeof value.agentId !== "string" || !AGENT_ID.test(value.agentId)
|
||||
|| (expectedAgentId !== undefined && value.agentId !== expectedAgentId)
|
||||
|| (expectedRunId !== undefined && value.id !== expectedRunId)
|
||||
|| !RUN_STATUSES.has(value.status as CursorCloudRunStatus)
|
||||
|| !nonempty(value.createdAt) || !nonempty(value.updatedAt)
|
||||
|| (value.durationMs !== undefined && (!Number.isSafeInteger(value.durationMs) || Number(value.durationMs) < 0))
|
||||
|| (value.result !== undefined && typeof value.result !== "string")) invalidResponse();
|
||||
return value as unknown as CursorCloudRun;
|
||||
}
|
||||
|
||||
function validatePrompt(input: CursorCloudFollowupInput): void {
|
||||
if (!record(input) || !record(input.prompt) || !nonempty(input.prompt.text)) invalidRequest();
|
||||
if (input.model !== undefined && (!record(input.model) || !nonempty(input.model.id))) invalidRequest();
|
||||
}
|
||||
|
||||
function positiveLimit(value: number | undefined, fallback: number): number {
|
||||
const limit = value ?? fallback;
|
||||
if (!Number.isSafeInteger(limit) || limit <= 0) throw new CursorCloudClientError("configuration");
|
||||
return limit;
|
||||
}
|
||||
|
||||
function requestScope(signal: AbortSignal | undefined, timeoutMs: number) {
|
||||
const controller = new AbortController();
|
||||
let timedOut = false;
|
||||
const cancel = () => controller.abort();
|
||||
if (signal?.aborted) cancel();
|
||||
else signal?.addEventListener("abort", cancel, { once: true });
|
||||
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
|
||||
timer.unref?.();
|
||||
return {
|
||||
signal: controller.signal,
|
||||
abort: () => { timedOut = true; controller.abort(); },
|
||||
error: () => new CursorCloudClientError(timedOut ? "timeout" : "cancelled"),
|
||||
dispose: () => { clearTimeout(timer); signal?.removeEventListener("abort", cancel); },
|
||||
};
|
||||
}
|
||||
|
||||
async function readBoundedBytes(response: Response, maxBytes: number): Promise<Buffer> {
|
||||
const declared = response.headers.get("content-length");
|
||||
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
throw new CursorCloudClientError("payload_too_large");
|
||||
}
|
||||
if (!response.body) invalidResponse();
|
||||
const reader = response.body.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let bytes = 0;
|
||||
try {
|
||||
while (true) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
bytes += chunk.value.byteLength;
|
||||
if (bytes > maxBytes) throw new CursorCloudClientError("payload_too_large");
|
||||
chunks.push(chunk.value);
|
||||
}
|
||||
return Buffer.concat(chunks, bytes);
|
||||
} finally {
|
||||
await reader.cancel().catch(() => {});
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async function readBoundedBody(response: Response, maxBytes: number): Promise<string> {
|
||||
const bytes = await readBoundedBytes(response, maxBytes);
|
||||
try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
||||
catch { invalidResponse(); }
|
||||
}
|
||||
|
||||
export class CursorCloudClient {
|
||||
readonly #apiKey: string;
|
||||
readonly #baseUrl: string;
|
||||
readonly #fetch: typeof globalThis.fetch;
|
||||
readonly #requestTimeoutMs: number;
|
||||
readonly #streamIdleTimeoutMs: number;
|
||||
readonly #streamTimeoutMs: number;
|
||||
readonly #maxRequestBytes: number;
|
||||
readonly #maxResponseBytes: number;
|
||||
readonly #maxSseFrameBytes: number;
|
||||
|
||||
constructor(options: CursorCloudClientOptions) {
|
||||
if (!nonempty(options.apiKey) || /[\r\n\0]/.test(options.apiKey)) throw new CursorCloudClientError("configuration");
|
||||
let base: URL;
|
||||
try { base = new URL(options.baseUrl ?? "https://api.cursor.com"); }
|
||||
catch { throw new CursorCloudClientError("configuration"); }
|
||||
if ((base.protocol !== "https:" && !(base.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(base.hostname)))
|
||||
|| base.username || base.password || base.search || base.hash || base.pathname !== "/") throw new CursorCloudClientError("configuration");
|
||||
this.#apiKey = options.apiKey;
|
||||
this.#baseUrl = base.origin;
|
||||
this.#fetch = options.fetch ?? globalThis.fetch;
|
||||
this.#requestTimeoutMs = positiveLimit(options.requestTimeoutMs, 30_000);
|
||||
this.#streamIdleTimeoutMs = positiveLimit(options.streamIdleTimeoutMs, 60_000);
|
||||
this.#streamTimeoutMs = positiveLimit(options.streamTimeoutMs, 7_200_000);
|
||||
this.#maxRequestBytes = positiveLimit(options.maxRequestBytes, 2_097_152);
|
||||
this.#maxResponseBytes = positiveLimit(options.maxResponseBytes, 4_194_304);
|
||||
this.#maxSseFrameBytes = positiveLimit(options.maxSseFrameBytes, 1_048_576);
|
||||
}
|
||||
|
||||
async createAgent(input: CursorCloudCreateInput, options: CursorCloudMutationOptions = {}): Promise<{ agent: CursorCloudAgent; run: CursorCloudRun }> {
|
||||
validatePrompt(input);
|
||||
if (input.agentId !== undefined) agentId(input.agentId);
|
||||
if (input.agentId !== undefined && input.envVars !== undefined) invalidRequest();
|
||||
const response = await this.#json("POST", "/v1/agents", input, options);
|
||||
if (!record(response)) invalidResponse();
|
||||
const agent = parseAgent(response.agent, input.agentId);
|
||||
const run = parseRun(response.run, agent.id);
|
||||
return { agent, run };
|
||||
}
|
||||
|
||||
async createRun(id: string, input: CursorCloudFollowupInput, options: CursorCloudMutationOptions = {}): Promise<{ run: CursorCloudRun }> {
|
||||
agentId(id);
|
||||
validatePrompt(input);
|
||||
const response = await this.#json("POST", `/v1/agents/${id}/runs`, input, options);
|
||||
if (!record(response)) invalidResponse();
|
||||
return { run: parseRun(response.run, id) };
|
||||
}
|
||||
|
||||
async getAgent(id: string, options: CursorCloudRequestOptions = {}): Promise<CursorCloudAgent> {
|
||||
agentId(id);
|
||||
return parseAgent(await this.#json("GET", `/v1/agents/${id}`, undefined, options), id);
|
||||
}
|
||||
|
||||
async getRun(id: string, run: string, options: CursorCloudRequestOptions = {}): Promise<CursorCloudRun> {
|
||||
agentId(id); runId(run);
|
||||
return parseRun(await this.#json("GET", `/v1/agents/${id}/runs/${run}`, undefined, options), id, run);
|
||||
}
|
||||
|
||||
async cancelRun(id: string, run: string, options: CursorCloudRequestOptions = {}): Promise<{ id: string }> {
|
||||
agentId(id); runId(run);
|
||||
return this.#idResponse(await this.#json("POST", `/v1/agents/${id}/runs/${run}/cancel`, undefined, options), run);
|
||||
}
|
||||
|
||||
async deleteAgent(id: string, options: CursorCloudRequestOptions = {}): Promise<{ id: string }> {
|
||||
agentId(id);
|
||||
return this.#idResponse(await this.#json("DELETE", `/v1/agents/${id}`, undefined, options), id);
|
||||
}
|
||||
|
||||
async listModels(options: CursorCloudRequestOptions = {}): Promise<{ items: CursorCloudModel[] }> {
|
||||
const response = await this.#json("GET", "/v1/models", undefined, options);
|
||||
if (!record(response) || !Array.isArray(response.items)
|
||||
|| !response.items.every(item => record(item) && nonempty(item.id) && nonempty(item.displayName))) invalidResponse();
|
||||
return { items: response.items as CursorCloudModel[] };
|
||||
}
|
||||
|
||||
async listArtifacts(id: string, options: CursorCloudRequestOptions = {}): Promise<{ items: CursorCloudArtifact[] }> {
|
||||
agentId(id);
|
||||
const response = await this.#json("GET", `/v1/agents/${id}/artifacts`, undefined, options);
|
||||
if (!record(response) || !Array.isArray(response.items) || response.items.length > 100
|
||||
|| !response.items.every(item => record(item) && nonempty(item.path) && Number.isSafeInteger(item.sizeBytes)
|
||||
&& Number(item.sizeBytes) >= 0 && nonempty(item.updatedAt))) invalidResponse();
|
||||
return { items: response.items as CursorCloudArtifact[] };
|
||||
}
|
||||
|
||||
async getArtifactDownloadUrl(id: string, path: string, options: CursorCloudRequestOptions = {}): Promise<{ url: string; expiresAt: string }> {
|
||||
agentId(id);
|
||||
if (!nonempty(path) || path.startsWith("/") || /[\\\0]/.test(path) || path.split("/").some(segment => segment === "..")) invalidRequest();
|
||||
const response = await this.#json("GET", `/v1/agents/${id}/artifacts/download?${new URLSearchParams({ path })}`, undefined, options);
|
||||
if (!record(response) || !nonempty(response.url) || !nonempty(response.expiresAt)) invalidResponse();
|
||||
let url: URL;
|
||||
try { url = new URL(response.url); } catch { invalidResponse(); }
|
||||
if (url.protocol !== "https:" || url.username || url.password) invalidResponse();
|
||||
return { url: response.url, expiresAt: response.expiresAt };
|
||||
}
|
||||
|
||||
async getUsage(id: string, query: { runId?: string } = {}, options: CursorCloudRequestOptions = {}): Promise<CursorCloudUsage> {
|
||||
agentId(id);
|
||||
if (query.runId !== undefined) runId(query.runId);
|
||||
const suffix = query.runId ? `?${new URLSearchParams({ runId: query.runId })}` : "";
|
||||
const response = await this.#json("GET", `/v1/agents/${id}/usage${suffix}`, undefined, options);
|
||||
const validUsage = (value: unknown): boolean => record(value)
|
||||
&& ["inputTokens", "outputTokens", "cacheWriteTokens", "cacheReadTokens", "totalTokens"]
|
||||
.every(key => Number.isSafeInteger(value[key]) && Number(value[key]) >= 0);
|
||||
if (!record(response) || !validUsage(response.totalUsage) || !Array.isArray(response.runs)
|
||||
|| !response.runs.every(item => record(item) && typeof item.id === "string" && RUN_ID.test(item.id)
|
||||
&& (query.runId === undefined || item.id === query.runId) && validUsage(item.usage)
|
||||
&& (item.usageUuid === undefined || nonempty(item.usageUuid)))) invalidResponse();
|
||||
return response as unknown as CursorCloudUsage;
|
||||
}
|
||||
|
||||
async downloadArtifact(id: string, path: string, options: CursorCloudRequestOptions & { maxBytes?: number } = {}): Promise<Buffer> {
|
||||
const maxBytes = positiveLimit(options.maxBytes, 16_777_216);
|
||||
if (maxBytes > 16_777_216) invalidRequest();
|
||||
const scope = requestScope(options.signal, positiveLimit(options.timeoutMs, this.#requestTimeoutMs));
|
||||
try {
|
||||
const { url } = await this.getArtifactDownloadUrl(id, path, { ...options, signal: scope.signal });
|
||||
const target = new URL(url);
|
||||
if (target.hostname !== "agent-stores.s3.us-east-1.amazonaws.com" || target.port || target.protocol !== "https:") invalidResponse();
|
||||
if (scope.signal.aborted) throw scope.error();
|
||||
const response = await this.#fetch(url, { method: "GET", signal: scope.signal, redirect: "error" });
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
throw new CursorCloudClientError("http_error", { status: response.status });
|
||||
}
|
||||
return await readBoundedBytes(response, maxBytes);
|
||||
} catch (error) { throw this.#safeError(error, scope); }
|
||||
finally { scope.dispose(); }
|
||||
}
|
||||
|
||||
async *streamRun(id: string, run: string, options: CursorCloudStreamOptions = {}): AsyncGenerator<CursorCloudSseEvent> {
|
||||
agentId(id); runId(run);
|
||||
const headers = this.#headers("text/event-stream");
|
||||
if (options.lastEventId !== undefined) headers["Last-Event-ID"] = headerValue(options.lastEventId);
|
||||
const scope = requestScope(options.signal, positiveLimit(options.timeoutMs, this.#streamTimeoutMs));
|
||||
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
||||
const resetIdle = () => {
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = setTimeout(scope.abort, this.#streamIdleTimeoutMs);
|
||||
idleTimer.unref?.();
|
||||
};
|
||||
resetIdle();
|
||||
try {
|
||||
if (scope.signal.aborted) throw scope.error();
|
||||
const response = await this.#fetch(`${this.#baseUrl}/v1/agents/${id}/runs/${run}/stream`, {
|
||||
method: "GET", headers, signal: scope.signal, redirect: "error",
|
||||
});
|
||||
if (!response.ok) throw await this.#httpError(response);
|
||||
if (!response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream") || !response.body) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
invalidResponse();
|
||||
}
|
||||
reader = response.body.getReader();
|
||||
clearTimeout(idleTimer);
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
const parser = new CursorCloudSseParser(this.#maxSseFrameBytes);
|
||||
while (true) {
|
||||
resetIdle();
|
||||
const chunk = await reader.read();
|
||||
clearTimeout(idleTimer);
|
||||
if (scope.signal.aborted) throw scope.error();
|
||||
if (chunk.done) { yield* parser.push(decoder.decode(), true); break; }
|
||||
for (const event of parser.push(decoder.decode(chunk.value, { stream: true }))) {
|
||||
yield event;
|
||||
if (event.event === "done") return;
|
||||
}
|
||||
}
|
||||
} catch (error) { throw this.#safeError(error, scope); }
|
||||
finally {
|
||||
clearTimeout(idleTimer);
|
||||
await reader?.cancel().catch(() => {});
|
||||
reader?.releaseLock();
|
||||
scope.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#idResponse(response: unknown, expected: string): { id: string } {
|
||||
if (!record(response) || response.id !== expected) invalidResponse();
|
||||
return { id: expected };
|
||||
}
|
||||
|
||||
#headers(accept = "application/json"): Record<string, string> {
|
||||
return { Authorization: `Bearer ${this.#apiKey}`, Accept: accept };
|
||||
}
|
||||
|
||||
async #json(method: string, path: string, input: unknown, options: CursorCloudMutationOptions): Promise<unknown> {
|
||||
const headers = this.#headers();
|
||||
let body: string | undefined;
|
||||
if (input !== undefined) {
|
||||
try { body = JSON.stringify(input); } catch { invalidRequest(); }
|
||||
if (Buffer.byteLength(body) > this.#maxRequestBytes) throw new CursorCloudUnsentRequestError();
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
if (options.idempotencyKey !== undefined) headers["Idempotency-Key"] = headerValue(options.idempotencyKey);
|
||||
const scope = requestScope(options.signal, positiveLimit(options.timeoutMs, this.#requestTimeoutMs));
|
||||
try {
|
||||
if (scope.signal.aborted) throw scope.error();
|
||||
const response = await this.#fetch(`${this.#baseUrl}${path}`, { method, headers, body, signal: scope.signal, redirect: "error" });
|
||||
if (!response.ok) throw await this.#httpError(response);
|
||||
if (!response.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
invalidResponse();
|
||||
}
|
||||
const text = await readBoundedBody(response, this.#maxResponseBytes);
|
||||
try { return JSON.parse(text); } catch { invalidResponse(); }
|
||||
} catch (error) { throw this.#safeError(error, scope); }
|
||||
finally { scope.dispose(); }
|
||||
}
|
||||
|
||||
#safeError(error: unknown, scope: ReturnType<typeof requestScope>): CursorCloudClientError {
|
||||
if (scope.signal.aborted) return scope.error();
|
||||
if (error instanceof CursorCloudClientError) return error;
|
||||
return new CursorCloudClientError(error instanceof TypeError ? "network_error" : "invalid_response");
|
||||
}
|
||||
|
||||
async #httpError(response: Response): Promise<CursorCloudClientError> {
|
||||
let providerCode: string | undefined;
|
||||
try {
|
||||
const value: unknown = JSON.parse(await readBoundedBody(response, Math.min(this.#maxResponseBytes, 65_536)));
|
||||
const code = record(value) ? (record(value.error) ? value.error.code : value.code) : undefined;
|
||||
if (typeof code === "string" && SAFE_PROVIDER_CODES.has(code)) providerCode = code;
|
||||
} catch { }
|
||||
const retryHeader = response.headers.get("retry-after");
|
||||
const delay = retryHeader && /^\d+(\.\d+)?$/.test(retryHeader) ? Number(retryHeader) * 1_000 : undefined;
|
||||
return new CursorCloudClientError("http_error", {
|
||||
status: response.status, providerCode,
|
||||
...(delay !== undefined && Number.isFinite(delay) ? { retryAfterMs: Math.min(delay, 86_400_000) } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CursorCloudSseParser {
|
||||
#buffer = "";
|
||||
#frameBytes = 0;
|
||||
#event = "";
|
||||
#id: string | undefined;
|
||||
#data: string[] = [];
|
||||
|
||||
constructor(readonly maxBytes: number) {}
|
||||
|
||||
*push(text: string, end = false): Generator<CursorCloudSseEvent> {
|
||||
this.#buffer += text;
|
||||
while (true) {
|
||||
const boundary = /[\r\n]/.exec(this.#buffer);
|
||||
if (!boundary || (!end && boundary.index === this.#buffer.length - 1 && boundary[0] === "\r")) break;
|
||||
const delimiter = this.#buffer.startsWith("\r\n", boundary.index) ? 2 : 1;
|
||||
const line = this.#buffer.slice(0, boundary.index);
|
||||
this.#buffer = this.#buffer.slice(boundary.index + delimiter);
|
||||
this.#frameBytes += Buffer.byteLength(line) + delimiter;
|
||||
if (this.#frameBytes > this.maxBytes) throw new CursorCloudClientError("payload_too_large");
|
||||
if (!line) {
|
||||
const event = this.#finish();
|
||||
if (event) yield event;
|
||||
} else if (!line.startsWith(":")) {
|
||||
const colon = line.indexOf(":");
|
||||
const field = colon < 0 ? line : line.slice(0, colon);
|
||||
const value = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
||||
if (field === "event") this.#event = value;
|
||||
if (field === "id" && !value.includes("\0")) this.#id = value;
|
||||
if (field === "data") this.#data.push(value);
|
||||
}
|
||||
}
|
||||
if (this.#frameBytes + Buffer.byteLength(this.#buffer) > this.maxBytes) throw new CursorCloudClientError("payload_too_large");
|
||||
if (end && this.#buffer) invalidResponse();
|
||||
}
|
||||
|
||||
#finish(): CursorCloudSseEvent | undefined {
|
||||
let data: unknown = null;
|
||||
const event = this.#event || "message";
|
||||
const id = this.#id;
|
||||
const present = this.#data.length > 0 || this.#event.length > 0;
|
||||
if (this.#data.length) {
|
||||
try { data = JSON.parse(this.#data.join("\n")); } catch { invalidResponse(); }
|
||||
}
|
||||
this.#data = [];
|
||||
this.#event = "";
|
||||
this.#id = undefined;
|
||||
this.#frameBytes = 0;
|
||||
if (!present) return undefined;
|
||||
if (event.length > 128 || /[\0]/.test(event) || (id !== undefined && (id.length > 1_024 || /[\0]/.test(id)))) invalidResponse();
|
||||
return { event, ...(id !== undefined ? { id } : {}), data };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export { parseNativeCursorCloudConfig, type NativeCursorCloudConfig } from "../../contracts/cursor-cloud.js";
|
||||
|
||||
export const CURSOR_CLOUD_MCP_SERVER_NAME = "paperclip" as const;
|
||||
|
||||
export interface CursorCloudBinding {
|
||||
companyId: string;
|
||||
agentId: string;
|
||||
issueId: string;
|
||||
normalizedSessionId: string;
|
||||
}
|
||||
|
||||
export function cursorCloudRecord(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Cursor Cloud configuration must be an object");
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function parseCursorCloudBinding(value: unknown): CursorCloudBinding {
|
||||
const source = cursorCloudRecord(value);
|
||||
if (Object.keys(source).sort().join(",") !== "agentId,companyId,issueId,normalizedSessionId"
|
||||
|| !Object.values(source).every(value => typeof value === "string" && value.trim().length > 0 && value.length <= 1_024 && !/[\x00-\x1f\x7f]/.test(value))) throw new Error("Cursor Cloud binding is invalid");
|
||||
return { companyId: source.companyId as string, agentId: source.agentId as string, issueId: source.issueId as string, normalizedSessionId: source.normalizedSessionId as string };
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,618 @@
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { chmod, lstat } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, resolve } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import type { HarnessDriver, HarnessDriverDescriptor, HarnessSession, HarnessSessionRecoveryOptions, OpenHarnessSessionInput, PersistedHarnessSession } from "../../contracts/harness-driver.js";
|
||||
import { HarnessCapabilityUnavailableError, HarnessReconciliationError, HarnessStaleTurnError } from "../../contracts/harness-driver.js";
|
||||
import type { NativeRuntimeContextSnapshot } from "../../contracts/runtime-context.js";
|
||||
import type { NativeUserMessage } from "../../contracts/types.js";
|
||||
import { CURSOR_CLOUD_CANCEL_TIMEOUT_MS, CURSOR_CLOUD_DEFAULT_TIMEOUT_MS } from "../../contracts/cursor-cloud.js";
|
||||
import { PRP_BLOCK_TOOL_NAME, PRP_COMPLETION_TOOL_NAME } from "../../contracts/completion-result.js";
|
||||
import type { PrpEvent } from "../../protocol/replay-contract.js";
|
||||
import { RUNNER_TOOL_SCOPE_TOKEN_HEADER, startRunnerToolBridge, type RunnerToolBridge, type RunnerToolCall } from "../runner-tool-bridge.js";
|
||||
import { semanticCanonicalJson } from "../semantic-tool-receipt.js";
|
||||
import { CursorCloudClient, CursorCloudClientError, isCursorCloudUnsentRequestError, isCursorCloudRunActive, type CursorCloudModelRef, type CursorCloudRun, type CursorCloudSseEvent } from "./cloud-client.js";
|
||||
import { CURSOR_CLOUD_MCP_SERVER_NAME, parseNativeCursorCloudConfig, parseCursorCloudBinding, type NativeCursorCloudConfig, type CursorCloudBinding } from "./config.js";
|
||||
import { CursorCloudCheckpointStore, cursorCloudDigest, type CursorCloudCheckpoint, type CursorCloudTerminalDelivery } from "./checkpoint.js";
|
||||
import { cursorCloudInlineInstructions } from "./inline-assets.js";
|
||||
import { stageCursorCloudArtifact } from "./artifact-delivery.js";
|
||||
import { releaseCursorCloudStaleSocket } from "./stale-socket.js";
|
||||
|
||||
export const CURSOR_CLOUD_DRIVER_KIND = "cursor_cloud_api" as const;
|
||||
export const CURSOR_CLOUD_DRIVER_VERSION = "cursor-cloud-v1" as const;
|
||||
const CURSOR_CLOUD_CONTROL_INSTRUCTIONS = [
|
||||
"You are executing in native Paperclip Runner, not a legacy REST adapter.",
|
||||
`Paperclip control tools are provided by the injected MCP server "${CURSOR_CLOUD_MCP_SERVER_NAME}" with controller-managed MCP authentication; you do not need Paperclip REST credentials in the Cloud VM.`,
|
||||
`At the start of each run, call get_mcp_tools({server:"${CURSOR_CLOUD_MCP_SERVER_NAME}"}) to discover its tools, then use the returned tool schemas for Paperclip task context and control.`,
|
||||
"For Paperclip control, do not search unrelated inherited dashboard MCP namespaces or wait for their authentication.",
|
||||
`When you need a human answer, use this server's offered request_human_input tool. Report completion with its offered ${PRP_COMPLETION_TOOL_NAME} tool, or a blocker with its offered ${PRP_BLOCK_TOOL_NAME} tool, following their schemas. Text alone does not submit a question, completion, or blocker.`,
|
||||
].join("\n");
|
||||
type TurnAuthority = { generation: number; ownerRunId: string; turnId: string | null };
|
||||
type RemoteAuthority = TurnAuthority & { agentId: string; runId: string };
|
||||
type PendingAuthority = TurnAuthority & Pick<CursorCloudCheckpoint, "pendingOperation" | "requestedAgentId" | "idempotencyKey" | "agentId">;
|
||||
type PendingTerminal = {
|
||||
authority: RemoteAuthority;
|
||||
delivery: CursorCloudTerminalDelivery;
|
||||
events: Array<{ eventType: PrpEvent["eventType"]; payload: Record<string, unknown>; itemId?: string }>;
|
||||
};
|
||||
type CloudClient = Pick<CursorCloudClient, "createAgent" | "createRun" | "getAgent" | "getRun" | "cancelRun" | "streamRun" | "getUsage" | "listArtifacts" | "downloadArtifact">;
|
||||
|
||||
export interface CursorCloudSessionHandle extends HarnessSession {
|
||||
read(): Promise<Record<string, unknown>>;
|
||||
acknowledgeTerminalDelivery(input: { turnId: string; receiptId: string }): Promise<void>;
|
||||
}
|
||||
|
||||
export interface CursorCloudDriverOptions {
|
||||
model: string;
|
||||
modelParams?: CursorCloudModelRef["params"];
|
||||
config: NativeCursorCloudConfig;
|
||||
binding: CursorCloudBinding;
|
||||
runtimeDirectory: string;
|
||||
artifactRoot?: string;
|
||||
relay: { url: string; token: string; socketPath: string; expiresAt: number };
|
||||
instructions: string;
|
||||
runtimeContext?: NativeRuntimeContextSnapshot | null;
|
||||
dynamicTools?: readonly Readonly<Record<string, unknown>>[];
|
||||
dynamicToolHandler(call: { tool: string; callId: string; threadId: string; turnId: string; arguments: unknown }): Promise<unknown>;
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
client?: CloudClient;
|
||||
runnerInstanceId?: string;
|
||||
cancelTimeoutMs?: number;
|
||||
reconnectDelayMs?: number;
|
||||
}
|
||||
|
||||
class EventQueue {
|
||||
#events: PrpEvent[] = [];
|
||||
#waiter: ((result: IteratorResult<PrpEvent>) => void) | null = null;
|
||||
#closed = false;
|
||||
push(event: PrpEvent): void {
|
||||
if (this.#closed) return;
|
||||
if (this.#waiter) { const waiter = this.#waiter; this.#waiter = null; waiter({ value: event, done: false }); return; }
|
||||
if (this.#events.length >= 2_048) throw new Error("Cursor Cloud event consumer exceeded its bounded queue");
|
||||
this.#events.push(event);
|
||||
}
|
||||
close(): void { this.#closed = true; this.#waiter?.({ value: undefined, done: true }); this.#waiter = null; }
|
||||
async *stream(): AsyncGenerator<PrpEvent> {
|
||||
while (true) {
|
||||
if (this.#events.length) { yield this.#events.shift()!; continue; }
|
||||
if (this.#closed) return;
|
||||
const result = await new Promise<IteratorResult<PrpEvent>>(resolveValue => { this.#waiter = resolveValue; });
|
||||
if (result.done) return;
|
||||
yield result.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
|
||||
function active(cp: CursorCloudCheckpoint): boolean { return cp.phase === "creating" || cp.phase === "running" || cp.phase === "stopping"; }
|
||||
function boundedPositive(value: number | undefined, fallback: number): number {
|
||||
const result = value ?? fallback;
|
||||
if (!Number.isSafeInteger(result) || result < 1 || result > 120_000) throw new Error("Cursor Cloud lifecycle timeout exceeds its bound");
|
||||
return result;
|
||||
}
|
||||
async function withAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
signal.throwIfAborted();
|
||||
let cancelled!: () => void;
|
||||
const cancellation = new Promise<never>((_resolve, reject) => { cancelled = () => reject(new Error("Cursor Cloud lifecycle deadline expired")); signal.addEventListener("abort", cancelled, { once: true }); });
|
||||
try { return await Promise.race([operation, cancellation]); }
|
||||
finally { signal.removeEventListener("abort", cancelled); }
|
||||
}
|
||||
|
||||
export function cursorCloudDriverDescriptor(): HarnessDriverDescriptor {
|
||||
return { kind: CURSOR_CLOUD_DRIVER_KIND, displayName: "Cursor Cloud (experimental)", version: CURSOR_CLOUD_DRIVER_VERSION, protocolVersion: "http+sse/v1",
|
||||
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
||||
capabilities: { resume: true, typedEvents: true, steering: false, queuedFollowUp: false, interruption: true, structuredResult: true, read: true, reconciliation: true, usage: false, dynamicTools: true, toolRefreshOnResume: true, runtimeRequestResolution: false, runtimeRequestHandoff: false, goals: false, threadLineage: false,
|
||||
unsupported: ["native_permissions", "native_questions", "native_plan_decisions", "preprompt_model_acknowledgment", "preprompt_instruction_acknowledgment", "per_run_dollar_limit", "production_usage_accounting", "persistent_agent_directory", "binary_skill_assets", "steering", "goals", "threadLineage"] } };
|
||||
}
|
||||
|
||||
export class CursorCloudDriver implements HarnessDriver {
|
||||
readonly #options: CursorCloudDriverOptions;
|
||||
constructor(options: CursorCloudDriverOptions) {
|
||||
const model = options.model.trim();
|
||||
if (!model || model.length > 512 || /[\x00-\x1f\x7f]/.test(model)) throw new Error("Cursor Cloud requires an explicit model ID");
|
||||
const relay = new URL(options.relay.url);
|
||||
if (relay.protocol !== "https:" || relay.username || relay.password || relay.search || relay.hash) throw new Error("Cursor Cloud semantic relay requires a public credential-free HTTPS URL");
|
||||
if (!options.relay.token || /[\r\n\0]/.test(options.relay.token)) throw new Error("Cursor Cloud semantic relay token is required");
|
||||
if (!Number.isSafeInteger(options.relay.expiresAt) || options.relay.expiresAt <= 0) throw new Error("Cursor Cloud semantic relay expiry is required");
|
||||
if (!isAbsolute(options.runtimeDirectory) || !isAbsolute(options.relay.socketPath)) throw new Error("Cursor Cloud relay socket and runtime directory must be absolute controller bindings");
|
||||
this.#options = { ...options, model, config: parseNativeCursorCloudConfig(options.config), binding: parseCursorCloudBinding(options.binding) };
|
||||
}
|
||||
async descriptor(): Promise<HarnessDriverDescriptor> {
|
||||
return cursorCloudDriverDescriptor();
|
||||
}
|
||||
async openSession(input: OpenHarnessSessionInput): Promise<CursorCloudSessionHandle> { return this.#open(input, false); }
|
||||
async recoverSession(snapshot: PersistedHarnessSession, options?: HarnessSessionRecoveryOptions) {
|
||||
if (snapshot.driverKind !== CURSOR_CLOUD_DRIVER_KIND) return { recovered: false, reason: "Cursor Cloud recovery driver mismatch" };
|
||||
const session = await this.#open({ runId: snapshot.runId ?? "", normalizedSessionId: snapshot.normalizedSessionId ?? snapshot.driverSessionId, workingDirectory: snapshot.workingDirectory ?? this.#options.runtimeDirectory, signal: options?.signal }, true, snapshot.driverSessionId);
|
||||
return { recovered: true, session };
|
||||
}
|
||||
async #open(input: OpenHarnessSessionInput, recovering: boolean, sessionId = input.normalizedSessionId): Promise<CursorCloudSession> {
|
||||
input.signal?.throwIfAborted();
|
||||
if (!input.runId || input.normalizedSessionId !== this.#options.binding.normalizedSessionId) throw new Error("Cursor Cloud session binding mismatch");
|
||||
const instructions = `${await cursorCloudInlineInstructions(this.#options.instructions, this.#options.runtimeContext)}\n\n${CURSOR_CLOUD_CONTROL_INSTRUCTIONS}`;
|
||||
input.signal?.throwIfAborted();
|
||||
const store = new CursorCloudCheckpointStore(this.#options.runtimeDirectory, sessionId);
|
||||
await store.acquire();
|
||||
let opened: CursorCloudSession | null = null;
|
||||
try {
|
||||
const existing = await store.read();
|
||||
if (recovering !== Boolean(existing)) throw new Error(recovering ? "Cursor Cloud checkpoint is missing; refusing a replacement remote create" : "Cursor Cloud checkpoint already exists; resume it explicitly");
|
||||
const bindingDigest = cursorCloudDigest(semanticCanonicalJson(this.#options.binding));
|
||||
const configDigest = cursorCloudDigest(semanticCanonicalJson(this.#options.config));
|
||||
const modelDigest = cursorCloudDigest(semanticCanonicalJson({ id: this.#options.model, params: this.#options.modelParams ?? [] }));
|
||||
const instructionsDigest = cursorCloudDigest(instructions);
|
||||
if (existing && (existing.driverSessionId !== sessionId || existing.normalizedSessionId !== input.normalizedSessionId || existing.bindingDigest !== bindingDigest || existing.configDigest !== configDigest || existing.modelDigest !== modelDigest || (active(existing) && (existing.instructionsDigest !== instructionsDigest || existing.ownerRunId !== input.runId)))) throw new Error("Cursor Cloud recovery identity/configuration/instructions changed");
|
||||
const checkpoint: CursorCloudCheckpoint = existing ?? { schema: "paperclip.cursor-cloud.checkpoint.v1", driverSessionId: sessionId, normalizedSessionId: input.normalizedSessionId, bindingDigest, configDigest, modelDigest, instructionsDigest, instructionsRevisionPending: false, ownerRunId: input.runId, agentId: null, runId: null, turnId: null, phase: "idle", pendingOperation: null, idempotencyKey: null, requestedAgentId: null, requestDigest: null, lastEventId: null, lastEventFingerprints: [], sourceSequence: 0, terminalStatus: null, deadlineAt: null, pendingTerminalDelivery: null, acceptedCompletion: null };
|
||||
if (checkpoint.instructionsDigest !== instructionsDigest) { checkpoint.instructionsDigest = instructionsDigest; checkpoint.instructionsRevisionPending = true; }
|
||||
checkpoint.ownerRunId = input.runId;
|
||||
await store.write(checkpoint);
|
||||
opened = new CursorCloudSession(this.#options, input, instructions, checkpoint, store);
|
||||
await opened.initialize();
|
||||
input.signal?.throwIfAborted();
|
||||
opened.startRecovery();
|
||||
return opened;
|
||||
} catch (error) {
|
||||
if (opened) await opened.detachControllerForRestart().catch(() => store.release());
|
||||
else await store.release();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CursorCloudSession implements CursorCloudSessionHandle {
|
||||
readonly #options: CursorCloudDriverOptions;
|
||||
readonly #input: OpenHarnessSessionInput;
|
||||
readonly #instructions: string;
|
||||
readonly #cp: CursorCloudCheckpoint;
|
||||
readonly #store: CursorCloudCheckpointStore;
|
||||
readonly #client: CloudClient;
|
||||
readonly #events = new EventQueue();
|
||||
readonly #transcript: PrpEvent[] = [];
|
||||
readonly #cancelTimeoutMs: number;
|
||||
#bridge: RunnerToolBridge | null = null;
|
||||
#closed = false;
|
||||
#generation = 0;
|
||||
#stopping = false;
|
||||
#streamAbort: AbortController | null = null;
|
||||
#launch: Promise<void> | null = null;
|
||||
#work: Promise<void> | null = null;
|
||||
#stop: Promise<void> | null = null;
|
||||
#stopCheckpoint: Promise<void> | null = null;
|
||||
#rejectionSettlement: Promise<void> | null = null;
|
||||
#terminalSettlement: Promise<void> | null = null;
|
||||
#pendingTerminal: PendingTerminal | null = null;
|
||||
#turnDeadline: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(options: CursorCloudDriverOptions, input: OpenHarnessSessionInput, instructions: string, checkpoint: CursorCloudCheckpoint, store: CursorCloudCheckpointStore) {
|
||||
this.#options = options; this.#input = input; this.#instructions = instructions; this.#cp = checkpoint; this.#store = store;
|
||||
this.#client = options.client ?? new CursorCloudClient({ apiKey: options.environment?.CURSOR_API_KEY ?? "" });
|
||||
this.#cancelTimeoutMs = boundedPositive(options.cancelTimeoutMs, CURSOR_CLOUD_CANCEL_TIMEOUT_MS);
|
||||
}
|
||||
async initialize(): Promise<void> {
|
||||
const socketDirectory = await lstat(dirname(this.#options.relay.socketPath));
|
||||
if (!socketDirectory.isDirectory() || socketDirectory.isSymbolicLink() || (socketDirectory.mode & 0o077) !== 0 || (process.getuid && socketDirectory.uid !== process.getuid())) throw new Error("Cursor Cloud relay socket directory must be owned and private (0700)");
|
||||
await releaseCursorCloudStaleSocket(this.#options.relay.socketPath, this.#store.recoveredDeadOwner);
|
||||
const tools = [...(this.#options.dynamicTools ?? []).filter(tool => tool.name !== PRP_COMPLETION_TOOL_NAME && tool.name !== PRP_BLOCK_TOOL_NAME && tool.name !== "list_cursor_cloud_artifacts").map(tool => tool.name === "register_deliverable"
|
||||
? { ...tool, description: `${tool.description ?? "Register an inspectable file result."} Cursor Cloud: create the file in the provider artifacts directory, discover its exact path with list_cursor_cloud_artifacts, then use contentRef cursor-artifact:<exact listed relative artifact path>. Supply the actual filename, byteSize and lowercase SHA-256; these are independently verified before registration. Cloud filesystem paths and host paths are not accepted.` }
|
||||
: tool), { name: "list_cursor_cloud_artifacts", description: "List files exposed by the current Cursor Cloud Agent artifact API. Use the exact returned relative path in register_deliverable contentRef cursor-artifact:<path>. This read-only provider helper does not register a deliverable.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }];
|
||||
this.#bridge = await startRunnerToolBridge({ socketPath: this.#options.relay.socketPath, secret: this.#options.relay.token, tools, preserveToolNames: true, timeoutMs: null,
|
||||
sessionScope: () => this.#closed || this.#stopping || this.#pendingTerminal || !active(this.#cp) ? null : this.#cp.turnId,
|
||||
scopeToken: () => this.#closed || this.#stopping || this.#pendingTerminal || !active(this.#cp) ? null : this.#cp.relayTurnToken ?? null,
|
||||
handler: call => this.#dispatch(call) });
|
||||
const socket = await lstat(this.#options.relay.socketPath);
|
||||
if (!socket.isSocket() || socket.isSymbolicLink() || (process.getuid && socket.uid !== process.getuid())) throw new Error("Cursor Cloud bound relay socket ownership is unsafe");
|
||||
await chmod(this.#options.relay.socketPath, 0o600);
|
||||
const secured = await lstat(this.#options.relay.socketPath);
|
||||
if (!secured.isSocket() || secured.isSymbolicLink() || secured.ino !== socket.ino || secured.dev !== socket.dev || (secured.mode & 0o777) !== 0o600 || (process.getuid && secured.uid !== process.getuid())) throw new Error("Cursor Cloud bound relay socket changed while securing it");
|
||||
}
|
||||
#authority(): TurnAuthority { return { generation: this.#generation, ownerRunId: this.#cp.ownerRunId, turnId: this.#cp.turnId }; }
|
||||
#current(authority: TurnAuthority): boolean { return !this.#closed && authority.generation === this.#generation && authority.ownerRunId === this.#cp.ownerRunId && authority.turnId === this.#cp.turnId; }
|
||||
#remote(authority: TurnAuthority): RemoteAuthority | null { return this.#current(authority) && this.#cp.agentId && this.#cp.runId ? { ...authority, agentId: this.#cp.agentId, runId: this.#cp.runId } : null; }
|
||||
#pending(authority: TurnAuthority): PendingAuthority { return { ...authority, pendingOperation: this.#cp.pendingOperation, requestedAgentId: this.#cp.requestedAgentId, idempotencyKey: this.#cp.idempotencyKey, agentId: this.#cp.agentId }; }
|
||||
#currentPending(authority: PendingAuthority): boolean { return this.#current(authority) && active(this.#cp) && authority.pendingOperation === this.#cp.pendingOperation && authority.requestedAgentId === this.#cp.requestedAgentId && authority.idempotencyKey === this.#cp.idempotencyKey && authority.agentId === this.#cp.agentId; }
|
||||
#currentRemote(authority: RemoteAuthority): boolean { return !this.#pendingTerminal && this.#current(authority) && active(this.#cp) && authority.agentId === this.#cp.agentId && authority.runId === this.#cp.runId; }
|
||||
#acceptRun(authority: RemoteAuthority, run: CursorCloudRun): boolean {
|
||||
if (!this.#currentRemote(authority)) return false;
|
||||
if (run.agentId !== authority.agentId || run.id !== authority.runId) throw new HarnessReconciliationError("Cursor Cloud response changed the confirmed Agent/Run identity");
|
||||
return true;
|
||||
}
|
||||
startRecovery(): void { if (active(this.#cp)) { const authority = this.#authority(); this.#armDeadline(); this.#work = this.#recoverActive(authority).catch(error => this.#unresolved(error, authority)); } }
|
||||
ids() { return { driverSessionId: this.#cp.driverSessionId, providerSessionId: this.#cp.agentId, displayId: this.#cp.agentId ?? this.#cp.driverSessionId }; }
|
||||
events(): AsyncIterable<PrpEvent> { return this.#events.stream(); }
|
||||
turnControlCapabilities() { return { steering: false, queuedFollowUp: false }; }
|
||||
async attachRun(input: { runId: string }): Promise<void> {
|
||||
if (active(this.#cp) || this.#closed) throw new Error("Cursor Cloud cannot replace an active run binding");
|
||||
this.#generation += 1; this.#cp.ownerRunId = input.runId; await this.#store.write(this.#cp);
|
||||
}
|
||||
async startTurn(input: { message: NativeUserMessage; requestedCollaborationMode?: "default" | "plan" }): Promise<{ turnId: string; effectiveCollaborationMode: "default" }> {
|
||||
if (input.requestedCollaborationMode === "plan") throw new HarnessCapabilityUnavailableError("plan", "native Cloud plan decisions are unsupported");
|
||||
if (this.#closed || active(this.#cp)) throw new Error("Cursor Cloud session is closed or has unresolved remote work");
|
||||
if (this.#cp.pendingTerminalDelivery) throw new Error("Cursor Cloud terminal delivery must be acknowledged before new work");
|
||||
if (this.#options.relay.expiresAt <= Date.now()) throw new Error("Cursor Cloud semantic relay capability expired; only recovery cleanup is permitted");
|
||||
if (!input.message.text.trim() || Buffer.byteLength(input.message.text) > 1_048_576) throw new Error("Cursor Cloud turn text is empty or exceeds its bound");
|
||||
const previous = structuredClone(this.#cp);
|
||||
const turnId = `turn-${randomUUID()}`;
|
||||
const relayTurnToken = randomBytes(32).toString("base64url");
|
||||
const first = this.#cp.agentId === null;
|
||||
const prompt = `${first || this.#cp.instructionsRevisionPending ? this.#instructions : CURSOR_CLOUD_CONTROL_INSTRUCTIONS}\n\nPaperclip task:\n${input.message.text}`;
|
||||
const request = { prompt: { text: prompt }, model: { id: this.#options.model, ...(this.#options.modelParams ? { params: this.#options.modelParams } : {}) }, mode: "agent" as const,
|
||||
mcpServers: [{ name: CURSOR_CLOUD_MCP_SERVER_NAME, type: "http" as const, url: this.#options.relay.url, headers: { Authorization: `Bearer ${this.#options.relay.token}`, [RUNNER_TOOL_SCOPE_TOKEN_HEADER]: `Bearer ${relayTurnToken}` } }] };
|
||||
this.#cp.turnId = turnId; this.#cp.relayTurnToken = relayTurnToken; this.#cp.runId = null; this.#cp.phase = "creating"; this.#cp.pendingOperation = first ? "create_agent" : "create_run";
|
||||
this.#cp.requestedAgentId = first ? `bc-${randomUUID()}` : null; this.#cp.idempotencyKey = `paperclip-${randomUUID()}`;
|
||||
this.#cp.requestDigest = cursorCloudDigest(semanticCanonicalJson(request)); this.#cp.lastEventId = null; this.#cp.lastEventFingerprints = []; this.#cp.terminalStatus = null; this.#cp.acceptedCompletion = null; this.#cp.pendingTerminalDelivery = null;
|
||||
this.#cp.deadlineAt = Date.now() + (this.#options.config.timeoutMs ?? CURSOR_CLOUD_DEFAULT_TIMEOUT_MS);
|
||||
this.#generation += 1;
|
||||
const authority = this.#pending(this.#authority());
|
||||
this.#stopping = false; this.#stop = null; this.#stopCheckpoint = null;
|
||||
const streamAbort = new AbortController(); this.#streamAbort = streamAbort;
|
||||
const persisted = this.#store.write(this.#cp).catch(async error => {
|
||||
if (this.#current(authority)) {
|
||||
this.#generation += 1;
|
||||
Object.assign(this.#cp, previous);
|
||||
if (previous.relayTurnToken === undefined) delete this.#cp.relayTurnToken;
|
||||
this.#stopping = false; this.#clearDeadline(); streamAbort.abort();
|
||||
if (this.#streamAbort === streamAbort) this.#streamAbort = null;
|
||||
await this.#store.write(this.#cp);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
const launch = (async () => {
|
||||
await persisted;
|
||||
if (!this.#currentPending(authority)) return;
|
||||
const options = { signal: streamAbort.signal, idempotencyKey: authority.idempotencyKey! };
|
||||
const { schema: _schema, timeoutMs: _timeoutMs, ...cloudConfiguration } = this.#options.config;
|
||||
let response;
|
||||
try {
|
||||
response = first
|
||||
? await this.#client.createAgent({ ...cloudConfiguration, ...request, agentId: authority.requestedAgentId! }, options)
|
||||
: await this.#client.createRun(authority.agentId!, request, options);
|
||||
} catch (error) {
|
||||
if (!this.#currentPending(authority) || !(error instanceof CursorCloudClientError)
|
||||
|| (!isCursorCloudUnsentRequestError(error)
|
||||
&& (error.status === undefined || ![400, 401, 403, 404, 422].includes(error.status)))) throw error;
|
||||
const settlement = this.#rejectCreate(error, authority);
|
||||
this.#rejectionSettlement = settlement;
|
||||
try { await settlement; return; }
|
||||
finally { if (this.#rejectionSettlement === settlement) this.#rejectionSettlement = null; }
|
||||
}
|
||||
if (!this.#currentPending(authority)) return;
|
||||
if (response.run.agentId !== (first ? authority.requestedAgentId : authority.agentId)) throw new HarnessReconciliationError("Cursor Cloud create response changed the requested Agent identity");
|
||||
this.#cp.agentId = response.run.agentId; this.#cp.runId = response.run.id; this.#cp.pendingOperation = null; this.#cp.instructionsRevisionPending = false;
|
||||
if (!this.#stopping) this.#cp.phase = "running";
|
||||
await this.#store.write(this.#cp);
|
||||
if (this.#current(authority)) this.#publishRemoteIdentity();
|
||||
})();
|
||||
this.#launch = launch;
|
||||
this.#work = launch.then(async () => { const remote = this.#remote(authority); if (remote && !this.#stopping && active(this.#cp)) await this.#observe(remote); }).catch(error => this.#unresolved(error, authority));
|
||||
await persisted;
|
||||
if (this.#current(authority) && active(this.#cp)) {
|
||||
this.#emit("turn.started", { provider: "cursor_cloud", submittedModel: this.#options.model, effectiveModelAcknowledged: false, instructionsAcknowledged: false });
|
||||
this.#publishRemoteIdentity(); this.#armDeadline();
|
||||
}
|
||||
return { turnId, effectiveCollaborationMode: "default" };
|
||||
}
|
||||
async interrupt(input: { turnId?: string; signal?: AbortSignal } = {}): Promise<void> {
|
||||
if (input.turnId && input.turnId !== this.#cp.turnId) throw new HarnessStaleTurnError(input.turnId);
|
||||
if (this.#pendingTerminal) {
|
||||
const settlement = this.#persistTerminal();
|
||||
await (input.signal ? withAbort(settlement, input.signal) : settlement);
|
||||
return;
|
||||
}
|
||||
if (!active(this.#cp)) return;
|
||||
if (!this.#stop) {
|
||||
const stop = this.#interrupt(this.#authority(), input.signal).finally(() => { if (this.#stop === stop) this.#stop = null; });
|
||||
this.#stop = stop;
|
||||
}
|
||||
return this.#stop;
|
||||
}
|
||||
async #interrupt(authority: TurnAuthority, signal?: AbortSignal): Promise<void> {
|
||||
const launch = this.#launch, streamAbort = this.#streamAbort;
|
||||
this.#stopping = true; this.#cp.phase = "stopping";
|
||||
await (this.#stopCheckpoint = this.#store.write(this.#cp));
|
||||
if (!this.#current(authority) || !active(this.#cp)) return;
|
||||
streamAbort?.abort();
|
||||
const abort = new AbortController();
|
||||
const cancelled = () => abort.abort();
|
||||
signal?.addEventListener("abort", cancelled, { once: true }); if (signal?.aborted) abort.abort();
|
||||
const timer = setTimeout(() => abort.abort(), this.#cancelTimeoutMs);
|
||||
try {
|
||||
if (launch) await withAbort(launch.catch(() => {}), abort.signal);
|
||||
if (!this.#current(authority) || !active(this.#cp)) return;
|
||||
await this.#resolvePending(authority, abort.signal);
|
||||
if (!this.#current(authority) || !active(this.#cp)) return;
|
||||
const remote = this.#remote(authority);
|
||||
if (!remote) throw new HarnessReconciliationError("Cursor Cloud pending create has no confirmed run");
|
||||
let run = await this.#client.getRun(remote.agentId, remote.runId, { signal: abort.signal });
|
||||
if (!this.#acceptRun(remote, run)) return;
|
||||
if (isCursorCloudRunActive(run.status)) {
|
||||
try { await this.#client.cancelRun(remote.agentId, remote.runId, { signal: abort.signal }); }
|
||||
catch (error) { if (!(error instanceof CursorCloudClientError && error.providerCode === "run_not_cancellable")) throw error; }
|
||||
if (!this.#currentRemote(remote)) return;
|
||||
do {
|
||||
run = await this.#client.getRun(remote.agentId, remote.runId, { signal: abort.signal });
|
||||
if (!this.#acceptRun(remote, run)) return;
|
||||
if (!isCursorCloudRunActive(run.status)) break;
|
||||
await delay(100, undefined, { signal: abort.signal });
|
||||
if (!this.#currentRemote(remote)) return;
|
||||
} while (!abort.signal.aborted);
|
||||
}
|
||||
if (isCursorCloudRunActive(run.status)) throw new Error("Remote work remains active");
|
||||
await this.#settle(remote, run, true);
|
||||
} catch { if (this.#current(authority) && active(this.#cp)) throw new HarnessReconciliationError("Cursor Cloud Stop did not confirm remote terminal state; the run remains fenced"); }
|
||||
finally { clearTimeout(timer); signal?.removeEventListener("abort", cancelled); }
|
||||
}
|
||||
async #resolvePending(authority: TurnAuthority, signal?: AbortSignal): Promise<void> {
|
||||
if (!this.#current(authority) || !active(this.#cp)) return;
|
||||
const pending = this.#pending(authority);
|
||||
if (pending.pendingOperation === "create_agent") {
|
||||
const agent = await this.#client.getAgent(pending.requestedAgentId!, { signal });
|
||||
if (!this.#currentPending(pending)) return;
|
||||
if (agent.id !== pending.requestedAgentId || !agent.latestRunId) throw new HarnessReconciliationError("Cursor Cloud pending create has no matching observable run");
|
||||
this.#cp.agentId = agent.id; this.#cp.runId = agent.latestRunId; this.#cp.pendingOperation = null; this.#cp.instructionsRevisionPending = false; await this.#store.write(this.#cp);
|
||||
} else if (pending.pendingOperation === "create_run") {
|
||||
throw new HarnessReconciliationError("Cursor Cloud follow-up outcome is unknown; refusing duplicate create or unbound cancellation");
|
||||
}
|
||||
}
|
||||
async #recoverActive(authority: TurnAuthority): Promise<void> {
|
||||
const streamAbort = new AbortController(); this.#streamAbort = streamAbort;
|
||||
const signal = streamAbort.signal;
|
||||
let run: CursorCloudRun | undefined;
|
||||
let remote: RemoteAuthority | null = null;
|
||||
for (let attempt = 0; this.#current(authority) && !this.#stopping && active(this.#cp); attempt += 1) {
|
||||
try {
|
||||
await this.#resolvePending(authority, signal);
|
||||
if (!this.#current(authority) || this.#stopping || signal.aborted || !active(this.#cp)) return;
|
||||
remote = this.#remote(authority);
|
||||
if (!remote) throw new HarnessReconciliationError("Cursor Cloud recovery is missing remote identity");
|
||||
run = await this.#client.getRun(remote.agentId, remote.runId, { signal });
|
||||
if (!this.#acceptRun(remote, run)) return;
|
||||
break;
|
||||
} catch (error) {
|
||||
if (!this.#current(authority) || this.#stopping || signal.aborted || !active(this.#cp)) return;
|
||||
if (!(error instanceof CursorCloudClientError && error.retryable)) throw error;
|
||||
await this.#unresolved(error, authority);
|
||||
await delay(boundedPositive(this.#options.reconnectDelayMs, Math.min(250 * 2 ** attempt, 2_000)), undefined, { signal })
|
||||
.catch(error => { if (!signal.aborted) throw error; });
|
||||
}
|
||||
}
|
||||
if (!run || !remote || !this.#currentRemote(remote) || this.#stopping || signal.aborted) return;
|
||||
this.#emit("turn.started", { recovered: true, submittedModel: this.#options.model, effectiveModelAcknowledged: false, instructionsAcknowledged: false });
|
||||
this.#publishRemoteIdentity();
|
||||
if (!isCursorCloudRunActive(run.status)) { await this.#settle(remote, run, this.#cp.phase === "stopping"); return; }
|
||||
if (this.#cp.phase === "stopping" || this.#cp.deadlineAt === null || this.#cp.deadlineAt <= Date.now()) { await this.interrupt({ turnId: remote.turnId! }); return; }
|
||||
await this.#observe(remote);
|
||||
}
|
||||
async #readObservedRun(authority: RemoteAuthority, signal: AbortSignal): Promise<CursorCloudRun | null> {
|
||||
for (let attempt = 0; this.#currentRemote(authority) && !this.#stopping && !signal.aborted; attempt += 1) {
|
||||
if (this.#cp.deadlineAt === null || this.#cp.deadlineAt <= Date.now()) return null;
|
||||
try {
|
||||
const run = await withAbort(this.#client.getRun(authority.agentId, authority.runId, { signal }), signal);
|
||||
return this.#currentRemote(authority) && !this.#stopping && !signal.aborted ? run : null;
|
||||
} catch (error) {
|
||||
if (!this.#currentRemote(authority) || this.#stopping || signal.aborted) return null;
|
||||
if (!(error instanceof CursorCloudClientError && error.retryable)) throw error;
|
||||
if (attempt === 0) this.#emit("harness.diagnostic", { code: "cursor_cloud_observation_retry", message: "Cursor Cloud observation is temporarily unavailable; retrying the saved run within its original deadline." });
|
||||
const remaining = (this.#cp.deadlineAt ?? 0) - Date.now();
|
||||
if (remaining <= 0) return null;
|
||||
const backoff = boundedPositive(this.#options.reconnectDelayMs, Math.min(250 * 2 ** Math.min(attempt, 4), 2_000));
|
||||
await delay(Math.min(remaining, Math.max(backoff, error.retryAfterMs ?? 0)), undefined, { signal })
|
||||
.catch(error => { if (!signal.aborted) throw error; });
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async #observe(authority: RemoteAuthority): Promise<void> {
|
||||
for (let attempt = 0; this.#currentRemote(authority) && !this.#stopping; attempt += 1) {
|
||||
if (this.#cp.deadlineAt === null || this.#cp.deadlineAt <= Date.now()) return;
|
||||
const streamAbort = new AbortController(); this.#streamAbort = streamAbort;
|
||||
try {
|
||||
for await (const event of this.#client.streamRun(authority.agentId, authority.runId, { signal: streamAbort.signal, ...(this.#cp.lastEventId ? { lastEventId: this.#cp.lastEventId } : {}) })) {
|
||||
if (!this.#currentRemote(authority) || this.#stopping || streamAbort.signal.aborted) return;
|
||||
await this.#consume(event);
|
||||
if (event.event === "result" || event.event === "done") break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!this.#currentRemote(authority) || this.#stopping || streamAbort.signal.aborted) return;
|
||||
if (error instanceof CursorCloudClientError && error.status === 410) this.#emit("harness.diagnostic", { code: "cursor_cloud_stream_expired", message: "Cursor Cloud stream retention expired; reconciling saved run state." });
|
||||
else if (!(error instanceof CursorCloudClientError && error.retryable)) throw error;
|
||||
}
|
||||
if (!this.#currentRemote(authority) || this.#stopping || streamAbort.signal.aborted) return;
|
||||
const run = await this.#readObservedRun(authority, streamAbort.signal);
|
||||
if (!run || !this.#acceptRun(authority, run) || this.#stopping || streamAbort.signal.aborted) return;
|
||||
if (!isCursorCloudRunActive(run.status)) { await this.#settle(authority, run); return; }
|
||||
const remaining = (this.#cp.deadlineAt ?? 0) - Date.now();
|
||||
if (remaining <= 0) return;
|
||||
await delay(Math.min(remaining, boundedPositive(this.#options.reconnectDelayMs, Math.min(250 * 2 ** Math.min(attempt, 4), 2_000))), undefined, { signal: streamAbort.signal });
|
||||
}
|
||||
}
|
||||
async #consume(event: CursorCloudSseEvent): Promise<void> {
|
||||
const fingerprint = cursorCloudDigest(semanticCanonicalJson(event));
|
||||
if (event.id && event.id === this.#cp.lastEventId && this.#cp.lastEventFingerprints.includes(fingerprint)) return;
|
||||
const data = record(event.data);
|
||||
if (event.event === "status" || event.event === "result") {
|
||||
if (data.runId !== this.#cp.runId || !["CREATING", "RUNNING", "WAITING_FOR_BACKGROUND_WORK", "FINISHED", "ERROR", "CANCELLED", "EXPIRED"].includes(String(data.status))) throw new Error("Cursor Cloud stream lifecycle event has an invalid run identity/status");
|
||||
}
|
||||
if (event.event === "assistant" && typeof data.text === "string") this.#emit("item.delta", { kind: "agentMessage", text: this.#redact(data.text) }, "assistant");
|
||||
else if (event.event === "thinking" && typeof data.text === "string") this.#emit("item.delta", { kind: "reasoning", text: this.#redact(data.text) }, "reasoning");
|
||||
else if (event.event === "tool_call") {
|
||||
if (typeof data.callId !== "string" || !data.callId || data.callId.length > 512 || typeof data.name !== "string" || data.name.length > 512 || !["running", "completed", "error"].includes(String(data.status))) throw new Error("Cursor Cloud tool event omitted identity or status");
|
||||
this.#emit(data.status === "running" ? "item.started" : "item.completed", { kind: "tool", item: { id: data.callId, type: "mcpToolCall", name: data.name, status: data.status, ...(data.args === undefined ? {} : { arguments: this.#redactJson(data.args) }), ...(data.result === undefined ? {} : { result: this.#redactJson(data.result) }), ...(data.truncated ? { truncated: data.truncated } : {}) } }, data.callId);
|
||||
} else if (event.event === "error") this.#emit("harness.diagnostic", { code: "cursor_cloud_stream_error", message: "Cursor Cloud stream reported an error; checking the saved run." });
|
||||
if (event.id) {
|
||||
if (event.id !== this.#cp.lastEventId) this.#cp.lastEventFingerprints = [];
|
||||
if (this.#cp.lastEventFingerprints.length >= 32) throw new Error("Cursor Cloud event cursor has too many variants");
|
||||
this.#cp.lastEventId = event.id; this.#cp.lastEventFingerprints.push(fingerprint);
|
||||
}
|
||||
await this.#store.write(this.#cp);
|
||||
}
|
||||
async #settle(authority: RemoteAuthority, run: CursorCloudRun, interrupted = false): Promise<void> {
|
||||
if (this.#acceptRun(authority, run) && !isCursorCloudRunActive(run.status)) {
|
||||
const turnId = this.#cp.turnId!;
|
||||
const delivery: CursorCloudTerminalDelivery = { schema: "paperclip.cursor-cloud.terminal-delivery.v1", threadId: this.#cp.driverSessionId, turnId, providerRunId: run.id, receiptId: randomUUID(), status: run.status as CursorCloudTerminalDelivery["status"], interrupted: interrupted || run.status === "CANCELLED", result: run.result ? this.#redact(run.result) : null };
|
||||
const events: PendingTerminal["events"] = [];
|
||||
if (delivery.result) events.push({ eventType: "item.completed", payload: { kind: "agentMessage", text: delivery.result, channel: "final" }, itemId: "final" });
|
||||
events.push({ eventType: delivery.interrupted ? "turn.interrupted" : run.status === "FINISHED" ? "turn.completed" : "turn.failed", payload: { cursorCloudTerminalDelivery: delivery, providerStatus: run.status, remoteTerminalConfirmed: true, semanticCompletionAccepted: this.#cp.acceptedCompletion !== null, ...(run.status === "ERROR" || run.status === "EXPIRED" ? { error: { code: `cursor_cloud_${run.status.toLowerCase()}`, message: "Cursor Cloud run ended without successful provider completion." } } : {}) } });
|
||||
this.#pendingTerminal = { authority, delivery, events };
|
||||
await this.#persistTerminal();
|
||||
}
|
||||
}
|
||||
async #persistTerminal(): Promise<void> {
|
||||
if (this.#terminalSettlement) return this.#terminalSettlement;
|
||||
const pending = this.#pendingTerminal;
|
||||
if (!pending) return;
|
||||
const settlement = this.#saveTerminal(pending);
|
||||
this.#terminalSettlement = settlement;
|
||||
try { await settlement; }
|
||||
finally { if (this.#terminalSettlement === settlement) this.#terminalSettlement = null; }
|
||||
}
|
||||
async #saveTerminal(pending: PendingTerminal): Promise<void> {
|
||||
const expiresAt = Date.now() + this.#cancelTimeoutMs;
|
||||
for (let attempt = 0; this.#current(pending.authority) && this.#pendingTerminal === pending; attempt += 1) {
|
||||
// Reserve the final sequence range in the durable candidate, but allocate
|
||||
// events only after it saves. A failed-save diagnostic cannot consume an
|
||||
// unpublished final event's identity or let a snapshot regress the cursor.
|
||||
const checkpoint: CursorCloudCheckpoint = { ...this.#cp, phase: "terminal", terminalStatus: pending.delivery.status,
|
||||
pendingOperation: null, deadlineAt: null, pendingTerminalDelivery: pending.delivery,
|
||||
sourceSequence: this.#cp.sourceSequence + pending.events.length };
|
||||
try { await this.#store.write(checkpoint); }
|
||||
catch {
|
||||
if (attempt === 0 && this.#current(pending.authority)) this.#publish(this.#event("harness.diagnostic", {
|
||||
code: "cursor_cloud_terminal_checkpoint_pending", message: "Cursor Cloud finished, but its final checkpoint could not be saved. Its exact final delivery remains fenced until persistence succeeds.",
|
||||
}));
|
||||
const remaining = expiresAt - Date.now();
|
||||
if (remaining <= 0 || !this.#current(pending.authority)) throw new HarnessReconciliationError("Cursor Cloud terminal checkpoint could not be saved; the delivery remains fenced");
|
||||
await delay(Math.min(remaining, boundedPositive(this.#options.reconnectDelayMs, Math.min(250 * 2 ** Math.min(attempt, 4), 2_000))));
|
||||
continue;
|
||||
}
|
||||
if (!this.#current(pending.authority)) return;
|
||||
const events = pending.events.map(event => this.#event(event.eventType, event.payload, event.itemId, pending.delivery.turnId));
|
||||
Object.assign(this.#cp, checkpoint);
|
||||
this.#pendingTerminal = null; this.#clearDeadline();
|
||||
for (const event of events) this.#publish(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
async #dispatch(call: RunnerToolCall): Promise<unknown> {
|
||||
const turnId = this.#cp.turnId;
|
||||
if (this.#closed || this.#stopping || this.#pendingTerminal || !turnId || !active(this.#cp) || this.#cp.acceptedCompletion || call.signal.aborted) throw new Error("Cursor Cloud tool call has no current run authority");
|
||||
if (call.tool === "list_cursor_cloud_artifacts") {
|
||||
if (!this.#cp.agentId) throw new Error("Cursor Cloud Agent artifact identity is not confirmed");
|
||||
const listed = await this.#client.listArtifacts(this.#cp.agentId, { signal: call.signal });
|
||||
if (listed.items.length > 256) throw new Error("Cursor Cloud artifact catalog exceeds its 256-file bound");
|
||||
const result = { items: listed.items.map(item => ({ path: item.path, sizeBytes: item.sizeBytes })) };
|
||||
if (Buffer.byteLength(JSON.stringify(result)) > 65_536) throw new Error("Cursor Cloud artifact catalog exceeds its byte bound");
|
||||
if (this.#closed || this.#stopping || this.#pendingTerminal || call.signal.aborted || this.#cp.turnId !== turnId || !active(this.#cp)) throw new Error("Cursor Cloud artifact list arrived after run authority ended");
|
||||
return result;
|
||||
}
|
||||
const callId = `${turnId}:${call.callId}`;
|
||||
let args = call.arguments;
|
||||
let normalized: (() => void) | undefined;
|
||||
if (call.tool === "register_deliverable") {
|
||||
if (!this.#options.artifactRoot || !this.#cp.agentId) throw new Error("Cursor Cloud artifact registration is not bound to an admitted run/root");
|
||||
args = await stageCursorCloudArtifact({ arguments: args, agentId: this.#cp.agentId, runtimeDirectory: this.#options.runtimeDirectory, artifactRoot: this.#options.artifactRoot, client: this.#client, signal: call.signal });
|
||||
normalized = call.captureNormalizedInput?.(args);
|
||||
}
|
||||
if (this.#stopping || this.#closed || this.#pendingTerminal || call.signal.aborted || this.#cp.turnId !== turnId) throw new Error("Cursor Cloud run stopped before tool forwarding");
|
||||
const response = this.#options.dynamicToolHandler({ tool: call.tool, callId, threadId: this.#cp.driverSessionId, turnId, arguments: args });
|
||||
normalized?.();
|
||||
const result = await response;
|
||||
if (this.#closed || this.#stopping || this.#pendingTerminal || this.#cp.turnId !== turnId || !active(this.#cp) || call.signal.aborted) throw new Error("Cursor Cloud tool result arrived after its run authority ended");
|
||||
if ((call.tool === PRP_COMPLETION_TOOL_NAME || call.tool === PRP_BLOCK_TOOL_NAME) && record(result).success === true) {
|
||||
this.#cp.acceptedCompletion = { callIdDigest: cursorCloudDigest(callId), inputDigest: cursorCloudDigest(semanticCanonicalJson(call.arguments)), resultDigest: cursorCloudDigest(semanticCanonicalJson(result)), turnId };
|
||||
await this.#store.write(this.#cp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#armDeadline(): void {
|
||||
this.#clearDeadline();
|
||||
const authority = this.#authority();
|
||||
this.#turnDeadline = setTimeout(() => { if (this.#current(authority) && active(this.#cp)) void this.interrupt({ turnId: authority.turnId! }).catch(error => this.#unresolved(error, authority)); }, Math.max(0, (this.#cp.deadlineAt ?? 0) - Date.now()));
|
||||
this.#turnDeadline.unref?.();
|
||||
}
|
||||
#clearDeadline(): void { if (this.#turnDeadline) clearTimeout(this.#turnDeadline); this.#turnDeadline = null; }
|
||||
#publishRemoteIdentity(): void {
|
||||
const agentId = this.#cp.agentId ?? this.#cp.requestedAgentId;
|
||||
if (!agentId) return;
|
||||
this.#emit("harness.diagnostic", { code: "cursor_cloud_remote_identity", message: JSON.stringify({ schema: "paperclip.cursor-cloud.remote-identity.v1", provider: "cursor_cloud", agentId, runId: this.#cp.runId, ownerRunId: this.#cp.ownerRunId, normalizedSessionId: this.#cp.normalizedSessionId, phase: this.#cp.phase }) });
|
||||
}
|
||||
async #rejectCreate(error: CursorCloudClientError, authority: PendingAuthority): Promise<void> {
|
||||
const rejected = { phase: "idle", pendingOperation: null, requestedAgentId: null, terminalStatus: "REJECTED", deadlineAt: null } as const;
|
||||
// Keep admission fenced until the definite rejection is durable. Stop awaits
|
||||
// this launch promise, and its checkpoint must not overwrite the settlement.
|
||||
let stopCheckpoint = this.#stopCheckpoint;
|
||||
for (;;) {
|
||||
if (stopCheckpoint) await stopCheckpoint;
|
||||
if (!this.#currentPending(authority)) return;
|
||||
await this.#store.write({ ...this.#cp, ...rejected });
|
||||
if (!this.#currentPending(authority)) return;
|
||||
if (this.#stopCheckpoint !== stopCheckpoint) {
|
||||
stopCheckpoint = this.#stopCheckpoint;
|
||||
continue;
|
||||
}
|
||||
Object.assign(this.#cp, rejected); this.#clearDeadline();
|
||||
const unsent = isCursorCloudUnsentRequestError(error);
|
||||
this.#emit("turn.failed", { error: { code: unsent ? error.code : error.providerCode ?? "cursor_cloud_launch_rejected", message: unsent
|
||||
? "The encoded Cursor Cloud request exceeds the client limit. Reduce the task or instructions before retrying; no provider request was sent."
|
||||
: "Cursor Cloud rejected the run before accepting provider work. Check the configured account, repository access and explicit model." }, remoteWorkAccepted: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
async #unresolved(_error: unknown, authority: TurnAuthority): Promise<void> {
|
||||
if (!this.#current(authority) || !active(this.#cp)) return;
|
||||
this.#emit("harness.diagnostic", { code: "cursor_cloud_remote_state_unknown", message: "Cursor Cloud work could not be reconciled. Saved identity is retained; no replacement work was created. Stop or reconnect to reconcile it." });
|
||||
}
|
||||
#redact(text: string): string {
|
||||
return this.#redactUnbounded(text).slice(0, 65_536);
|
||||
}
|
||||
#redactUnbounded(text: string): string {
|
||||
for (const secret of [this.#options.environment?.CURSOR_API_KEY, this.#options.relay.token, this.#cp.relayTurnToken]) if (secret) text = text.replaceAll(secret, "[redacted]");
|
||||
return text;
|
||||
}
|
||||
#redactJson(value: unknown): unknown {
|
||||
const text = this.#redactUnbounded(JSON.stringify(value));
|
||||
return Buffer.byteLength(text) > 65_536 ? { omitted: true, reason: "payload_limit" } : JSON.parse(text);
|
||||
}
|
||||
#event(eventType: PrpEvent["eventType"], payload: Record<string, unknown>, itemId?: string, turnId = this.#cp.turnId ?? undefined): PrpEvent {
|
||||
const seq = ++this.#cp.sourceSequence;
|
||||
return { schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceEventId: `cursor-cloud:${this.#cp.ownerRunId}:${seq}`, sourceSeq: seq, sourceInstanceId: this.#options.runnerInstanceId ?? "cursor-cloud-sidecar", sourceKind: "runner", runId: this.#cp.ownerRunId, normalizedSessionId: this.#cp.normalizedSessionId, ...(turnId ? { turnId } : {}), ...(itemId ? { itemId } : {}), eventType, priority: 1, emittedAt: new Date().toISOString(), payload };
|
||||
}
|
||||
#publish(event: PrpEvent): void {
|
||||
if (this.#closed) return;
|
||||
this.#transcript.push(event); if (this.#transcript.length > 1_024) this.#transcript.shift(); this.#events.push(event);
|
||||
}
|
||||
#emit(eventType: PrpEvent["eventType"], payload: Record<string, unknown>, itemId?: string, turnId = this.#cp.turnId ?? undefined): void {
|
||||
if (!this.#closed && !this.#pendingTerminal) this.#publish(this.#event(eventType, payload, itemId, turnId));
|
||||
}
|
||||
async acknowledgeTerminalDelivery(input: { turnId: string; receiptId: string }): Promise<void> {
|
||||
const pending = this.#cp.pendingTerminalDelivery;
|
||||
if (!pending || pending.turnId !== input.turnId || pending.receiptId !== input.receiptId) throw new Error("Cursor Cloud terminal delivery receipt does not match");
|
||||
await this.#store.write({ ...this.#cp, pendingTerminalDelivery: null });
|
||||
this.#cp.pendingTerminalDelivery = null;
|
||||
}
|
||||
async read(): Promise<Record<string, unknown>> { return { provider: "cursor_cloud", agentId: this.#cp.agentId, runId: this.#cp.runId, turnId: this.#cp.turnId, phase: this.#cp.phase, providerStatus: this.#cp.terminalStatus, submittedModel: this.#options.model, effectiveModelAcknowledged: false, instructionsDigest: this.#cp.instructionsDigest, instructionsAcknowledged: false, semanticCompletionReceipt: this.#cp.acceptedCompletion, pendingTerminalDelivery: structuredClone(this.#cp.pendingTerminalDelivery) }; }
|
||||
async reconcile(): Promise<Record<string, unknown>> {
|
||||
if (this.#rejectionSettlement) await this.#rejectionSettlement;
|
||||
if (this.#pendingTerminal) { await this.#persistTerminal(); return this.read(); }
|
||||
const authority = this.#authority();
|
||||
await this.#resolvePending(authority);
|
||||
const remote = this.#remote(authority);
|
||||
if (remote && active(this.#cp)) { const run = await this.#client.getRun(remote.agentId, remote.runId); await this.#settle(remote, run); }
|
||||
return this.read();
|
||||
}
|
||||
async usage(): Promise<Record<string, unknown> | null> { return this.#cp.agentId ? { ...(await this.#client.getUsage(this.#cp.agentId, this.#cp.runId ? { runId: this.#cp.runId } : {})), cost: null, costUnavailable: true } : null; }
|
||||
async transcript() { return { schema: "paperclip-runner/harness-transcript/v1" as const, complete: this.#transcript.length < 1_024, eventCount: this.#transcript.length, events: structuredClone(this.#transcript), omissionReason: this.#transcript.length >= 1_024 ? "bounded_history" : null }; }
|
||||
async snapshot(): Promise<PersistedHarnessSession> { if (this.#rejectionSettlement) await this.#rejectionSettlement; if (this.#pendingTerminal) await this.#persistTerminal(); await this.#store.write(this.#cp); return { driverKind: CURSOR_CLOUD_DRIVER_KIND, driverSessionId: this.#cp.driverSessionId, providerSessionId: this.#cp.agentId, workingDirectory: this.#input.workingDirectory, runId: this.#cp.ownerRunId, normalizedSessionId: this.#cp.normalizedSessionId, activeTurnId: active(this.#cp) ? this.#cp.turnId : null, lastSourceSequence: this.#cp.sourceSequence }; }
|
||||
async detachControllerForRestart(): Promise<void> { if (this.#rejectionSettlement) await this.#rejectionSettlement; if (this.#pendingTerminal) await this.#persistTerminal(); this.#closed = true; this.#streamAbort?.abort(); this.#clearDeadline(); await this.#bridge?.close(); this.#events.close(); await this.#store.write(this.#cp); await this.#store.release(); }
|
||||
async close(_input: { reason: string; force?: boolean }): Promise<void> {
|
||||
if (this.#closed) return;
|
||||
let failure: unknown;
|
||||
try { if (this.#pendingTerminal) await this.#persistTerminal(); else if (active(this.#cp)) await this.interrupt(); } catch (error) { failure = error; }
|
||||
this.#closed = true; this.#streamAbort?.abort(); this.#clearDeadline();
|
||||
await this.#bridge?.close(); this.#events.close();
|
||||
// A failed terminal write may have committed before its acknowledgement was
|
||||
// lost. Retain the last durable checkpoint instead of overwriting it active.
|
||||
if (!this.#pendingTerminal) await this.#store.write(this.#cp);
|
||||
await this.#store.release();
|
||||
if (failure) throw failure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { composeNativeSystemInstructions, type NativeRuntimeAssetReference, type NativeRuntimeContextSnapshot } from "../../contracts/runtime-context.js";
|
||||
import { cursorCloudInlineInstructions } from "./inline-assets.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const hash = (value: string | Buffer) => createHash("sha256").update(value).digest("hex");
|
||||
afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); });
|
||||
async function asset(files: Record<string, string | Buffer>): Promise<NativeRuntimeAssetReference> {
|
||||
const root = await mkdtemp("/tmp/cc-assets-"); roots.push(root);
|
||||
const manifestFiles = [];
|
||||
for (const [path, content] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
const target = join(root, path); await mkdir(join(target, ".."), { recursive: true }); await writeFile(target, content); await chmod(target, 0o444);
|
||||
manifestFiles.push({ path, sha256: hash(content), mode: 0o444, size: Buffer.byteLength(content) });
|
||||
}
|
||||
const totalBytes = manifestFiles.reduce((sum, file) => sum + file.size, 0); const digest = hash(JSON.stringify(manifestFiles));
|
||||
return { schema: "paperclip.runtime-asset.v1", rootPath: root, digest, manifestDigest: hash(`${JSON.stringify({ schema: "paperclip.runtime-asset-manifest.v1", digest, fileCount: manifestFiles.length, totalBytes, files: manifestFiles })}\n`), fileCount: manifestFiles.length, totalBytes };
|
||||
}
|
||||
function context(bundle: NativeRuntimeAssetReference, skills: NativeRuntimeContextSnapshot["skills"] = []): NativeRuntimeContextSnapshot {
|
||||
return { prompt: { revision: "test", text: "Paperclip policy.", digest: hash("Paperclip policy.") }, instructions: { entryPath: "AGENTS.md", bundle }, skills, mcp: { assignmentSetId: "set", digest: hash("set"), bindingId: "binding" }, aggregateDigest: hash("context") };
|
||||
}
|
||||
|
||||
describe("Cursor Cloud immutable inline assets", () => {
|
||||
it("delivers exact instruction siblings and all assigned skill files with virtual labels and no host path claim", async () => {
|
||||
const instructions = await asset({ "AGENTS.md": "Follow references/details.md", "references/details.md": "Exact sibling bytes.\n" });
|
||||
const skill = await asset({ "SKILL.md": "Use references/input.txt", "references/input.txt": "Exact skill input." });
|
||||
const result = await cursorCloudInlineInstructions("local path promises must not survive", context(instructions, [{ key: "skill", runtimeName: "test-skill", versionId: "v1", bundle: skill }]));
|
||||
expect(result).toContain("instructions/references/details.md"); expect(result).toContain("Exact sibling bytes.\\n"); expect(result).toContain("skills/test-skill/references/input.txt");
|
||||
expect(result).not.toContain(instructions.rootPath); expect(result).not.toContain(skill.rootPath); expect(result).not.toContain("AGENT_HOME");
|
||||
});
|
||||
it("rejects symlinks, binary payloads, digest mismatch and oversize before any native work", async () => {
|
||||
const good = await asset({ "AGENTS.md": "safe" });
|
||||
await expect(cursorCloudInlineInstructions("", context({ ...good, digest: hash("wrong") }))).rejects.toThrow("digest mismatch");
|
||||
const binary = await asset({ "AGENTS.md": Buffer.from([0, 255]) });
|
||||
await expect(cursorCloudInlineInstructions("", context(binary))).rejects.toThrow("binary");
|
||||
await symlink("/etc/passwd", join(good.rootPath, "outside"));
|
||||
await expect(cursorCloudInlineInstructions("", context(good))).rejects.toThrow("symlinks");
|
||||
await expect(cursorCloudInlineInstructions("", context({ ...good, totalBytes: 2_000_000 }))).rejects.toThrow("bound");
|
||||
});
|
||||
it("preserves current supplied instructions while removing only the trusted host asset suffix", async () => {
|
||||
const bundle = await asset({ "AGENTS.md": "Exact entry instruction." });
|
||||
const snapshot = context(bundle);
|
||||
snapshot.instructions.workingCopy = { rootPath: "/private/local/agent-files", entryPath: "AGENTS.md", kind: "agent_files" };
|
||||
const result = await cursorCloudInlineInstructions(composeNativeSystemInstructions(snapshot, "Exact entry instruction."), snapshot);
|
||||
expect(result).toContain("Exact entry instruction."); expect(result).not.toContain(bundle.rootPath); expect(result).not.toContain("/private/local/agent-files"); expect(result).not.toContain("AGENT_HOME");
|
||||
expect(await cursorCloudInlineInstructions("Additional current grant instructions.", snapshot)).toContain("Additional current grant instructions.");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { lstat, open, readdir } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { composeNativeSystemInstructions, type NativeRuntimeAssetReference, type NativeRuntimeContextSnapshot } from "../../contracts/runtime-context.js";
|
||||
|
||||
const MAX_INLINE_BYTES = 1_048_576;
|
||||
const MAX_INLINE_FILES = 1_024;
|
||||
const hash = (value: string | Uint8Array) => createHash("sha256").update(value).digest("hex");
|
||||
|
||||
export async function cursorCloudInlineInstructions(instructions: string, context?: NativeRuntimeContextSnapshot | null): Promise<string> {
|
||||
if (!context) {
|
||||
if (!instructions.trim() || Buffer.byteLength(instructions) > MAX_INLINE_BYTES) throw new Error("Cursor Cloud instructions are empty or exceed the inline delivery bound");
|
||||
return instructions;
|
||||
}
|
||||
const files: Array<{ path: string; text: string }> = [];
|
||||
let totalBytes = 0;
|
||||
async function bundle(asset: NativeRuntimeAssetReference, label: string): Promise<void> {
|
||||
if (asset.totalBytes + totalBytes > MAX_INLINE_BYTES || asset.fileCount + files.length > MAX_INLINE_FILES) throw new Error("Cursor Cloud instruction/skill assets exceed the 1 MiB inline delivery bound");
|
||||
const root = resolve(asset.rootPath);
|
||||
const collected: Array<{ path: string; sha256: string; mode: number; size: number; text: string }> = [];
|
||||
async function visit(relative = ""): Promise<void> {
|
||||
const directory = relative ? join(root, relative) : root;
|
||||
const directoryStat = await lstat(directory);
|
||||
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error("Cursor Cloud asset directory is unsafe");
|
||||
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (!entry.name || /[\0\r\n\\]/.test(entry.name) || entry.name === "." || entry.name === "..") throw new Error("Cursor Cloud asset path is unsupported");
|
||||
const path = relative ? `${relative}/${entry.name}` : entry.name;
|
||||
const target = join(root, path);
|
||||
const stat = await lstat(target);
|
||||
if (stat.isSymbolicLink()) throw new Error("Cursor Cloud assets cannot contain symlinks");
|
||||
if (stat.isDirectory()) { await visit(path); continue; }
|
||||
if (!stat.isFile() || stat.size > MAX_INLINE_BYTES - totalBytes || collected.length + files.length >= MAX_INLINE_FILES) throw new Error("Cursor Cloud asset is unsupported or exceeds its bound");
|
||||
const handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
||||
try {
|
||||
const opened = await handle.stat();
|
||||
if (!opened.isFile() || opened.ino !== stat.ino || opened.dev !== stat.dev || opened.size !== stat.size) throw new Error("Cursor Cloud asset changed during admission");
|
||||
const buffer = Buffer.alloc(opened.size + 1);
|
||||
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
||||
if (bytesRead !== opened.size) throw new Error("Cursor Cloud asset changed during admission");
|
||||
const bytes = buffer.subarray(0, bytesRead);
|
||||
totalBytes += bytes.length;
|
||||
if (totalBytes > MAX_INLINE_BYTES || bytes.includes(0)) throw new Error("Cursor Cloud asset is binary or exceeds its inline bound");
|
||||
let content: string;
|
||||
try { content = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
||||
catch { throw new Error("Cursor Cloud supports UTF-8 text instruction/skill assets only"); }
|
||||
collected.push({ path, sha256: hash(bytes), mode: opened.mode & 0o555, size: bytes.length, text: content });
|
||||
} finally { await handle.close(); }
|
||||
}
|
||||
}
|
||||
await visit();
|
||||
collected.sort((a, b) => a.path.localeCompare(b.path));
|
||||
const manifestFiles = collected.map(({ text: _text, ...entry }) => entry);
|
||||
const bytes = manifestFiles.reduce((sum, entry) => sum + entry.size, 0);
|
||||
const digest = hash(JSON.stringify(manifestFiles));
|
||||
const manifest = `${JSON.stringify({ schema: "paperclip.runtime-asset-manifest.v1", digest, fileCount: manifestFiles.length, totalBytes: bytes, files: manifestFiles })}\n`;
|
||||
if (digest !== asset.digest || hash(manifest) !== asset.manifestDigest || collected.length !== asset.fileCount || bytes !== asset.totalBytes) throw new Error("Cursor Cloud instruction/skill asset digest mismatch");
|
||||
for (const file of collected) files.push({ path: `${label}/${file.path}`, text: file.text });
|
||||
}
|
||||
await bundle(context.instructions.bundle, "instructions");
|
||||
if (!files.some(file => file.path === `instructions/${context.instructions.entryPath}`)) throw new Error("Cursor Cloud instruction entry is missing");
|
||||
for (const skill of context.skills) await bundle(skill.bundle, `skills/${skill.runtimeName}`);
|
||||
const localAssetSuffix = composeNativeSystemInstructions(context, "").slice(context.prompt.text.length);
|
||||
const supplied = instructions.endsWith(localAssetSuffix) ? instructions.slice(0, -localAssetSuffix.length).trimEnd() : instructions;
|
||||
const result = [supplied.startsWith(context.prompt.text) ? supplied : `${context.prompt.text}\n\n${supplied}`, context.connectionInstructions?.text,
|
||||
"The following JSON contains complete read-only instruction and skill files. Paths are virtual labels, not filesystem locations. Read the instruction entry and use assigned skills when relevant. Relative references refer to other virtual files in this payload. These files are not mounted in the Cloud VM. Persistent agent-directory edits and executing bundled scripts by local path are unsupported.",
|
||||
`Instruction entry: instructions/${context.instructions.entryPath}`,
|
||||
JSON.stringify({ schema: "paperclip.cursor-cloud.inline-assets.v1", files }),
|
||||
].filter(Boolean).join("\n\n");
|
||||
if (Buffer.byteLength(result) > MAX_INLINE_BYTES) throw new Error("Cursor Cloud rendered instruction/skill payload exceeds 1 MiB");
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { lstat, mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import type { PrpEvent } from "../../protocol/replay-contract.js";
|
||||
import { CursorCloudClient, type CursorCloudRunStatus } from "./cloud-client.js";
|
||||
import { CursorCloudDriver, type CursorCloudSessionHandle } from "./cursor-cloud-driver.js";
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>(accept => { resolve = accept; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
const fixtures: Array<{ root: string; session: CursorCloudSessionHandle; pump: Promise<void>; release(): void }> = [];
|
||||
let forbiddenFetches = 0;
|
||||
beforeEach(() => {
|
||||
forbiddenFetches = 0;
|
||||
vi.stubGlobal("fetch", async () => { forbiddenFetches++; throw new Error("External fetch is forbidden in the observation fixture"); });
|
||||
});
|
||||
afterEach(async () => {
|
||||
try {
|
||||
for (const fixture of fixtures.splice(0)) {
|
||||
fixture.release();
|
||||
try { await fixture.session.close({ reason: "observation_fixture_cleanup" }); await fixture.pump; }
|
||||
finally { await rm(fixture.root, { recursive: true, force: true }); }
|
||||
}
|
||||
expect(forbiddenFetches).toBe(0);
|
||||
} finally { vi.unstubAllGlobals(); }
|
||||
});
|
||||
|
||||
async function fixture(options: { timeoutMs?: number; reconnectDelayMs?: number; holdFourthRead?: boolean; finishOnFourthRead?: boolean; streamStatus?: number } = {}) {
|
||||
const root = await mkdtemp("/tmp/cc-observe-");
|
||||
const socketPath = join(root, "mcp.sock");
|
||||
const fourthRead = deferred(), releaseFourthRead = deferred();
|
||||
const counts = { creates: 0, followups: 0, cancels: 0, streams: 0, reads: 0 };
|
||||
const observations: Array<{ status: CursorCloudRunStatus; deadlineAt: number | null; at: number }> = [];
|
||||
const events: PrpEvent[] = [];
|
||||
const nativeRunId = "run-11111111-1111-4111-8111-111111111111";
|
||||
const timestamp = "2026-10-29T00:00:00Z";
|
||||
let agentId = "", nativeStatus: CursorCloudRunStatus = "RUNNING";
|
||||
const checkpoint = async () => {
|
||||
const name = (await readdir(root)).find(value => /^checkpoint-[a-f0-9]{64}\.json$/.test(value))!;
|
||||
return JSON.parse(await readFile(join(root, name), "utf8"));
|
||||
};
|
||||
const run = (status = nativeStatus) => ({ id: nativeRunId, agentId, status, createdAt: timestamp, updatedAt: timestamp,
|
||||
...(status === "FINISHED" ? { result: "Exact final answer after disconnected observation." } : {}) });
|
||||
const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
|
||||
const nativeFetch: typeof globalThis.fetch = async (url, init) => {
|
||||
const target = new URL(String(url)), method = init?.method ?? "GET";
|
||||
expect(target.origin).toBe("https://controlled.cursor.test");
|
||||
if (target.pathname === "/v1/agents" && method === "POST") {
|
||||
counts.creates++; agentId = JSON.parse(String(init?.body)).agentId;
|
||||
return json({ agent: { id: agentId, status: "ACTIVE", env: { type: "cloud" }, url: "https://cursor.com/agents/fixture", createdAt: timestamp, updatedAt: timestamp, latestRunId: nativeRunId }, run: run() });
|
||||
}
|
||||
if (target.pathname === `/v1/agents/${agentId}/runs` && method === "POST") { counts.followups++; throw new Error("Unexpected replacement work"); }
|
||||
if (target.pathname === `/v1/agents/${agentId}/runs/${nativeRunId}/cancel` && method === "POST") {
|
||||
counts.cancels++; nativeStatus = "CANCELLED"; return json({ id: nativeRunId });
|
||||
}
|
||||
if (target.pathname === `/v1/agents/${agentId}/runs/${nativeRunId}/stream` && method === "GET") {
|
||||
counts.streams++;
|
||||
if (options.streamStatus) return json({ message: "Controlled stream failure" }, options.streamStatus);
|
||||
return new Response(`id: opaque-${counts.streams}\nevent: assistant\ndata: {"text":"Working before disconnect."}\n\n`, { headers: { "content-type": "text/event-stream" } });
|
||||
}
|
||||
if (target.pathname === `/v1/agents/${agentId}/runs/${nativeRunId}` && method === "GET") {
|
||||
counts.reads++;
|
||||
if (options.finishOnFourthRead && counts.reads === 4) nativeStatus = "FINISHED";
|
||||
const observed = run();
|
||||
observations.push({ status: observed.status, deadlineAt: (await checkpoint()).deadlineAt, at: Date.now() });
|
||||
if (counts.reads === 4) {
|
||||
fourthRead.resolve();
|
||||
if (options.holdFourthRead) await releaseFourthRead.promise;
|
||||
}
|
||||
return json(observed);
|
||||
}
|
||||
throw new Error(`Unexpected synthetic API request ${method} ${target.pathname}`);
|
||||
};
|
||||
const driver = new CursorCloudDriver({ model: "explicit-model", config: { schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/repo" }], timeoutMs: options.timeoutMs ?? 10_000 },
|
||||
binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId: "session" }, runtimeDirectory: root,
|
||||
relay: { url: "https://relay.example.test/mcp", token: "synthetic-relay-token", socketPath, expiresAt: Date.now() + 60_000 }, instructions: "Perform only this controlled observation fixture.",
|
||||
dynamicToolHandler: async () => { throw new Error("No semantic tool is called by this fixture"); },
|
||||
client: new CursorCloudClient({ apiKey: "synthetic-key", baseUrl: "https://controlled.cursor.test", fetch: nativeFetch }), reconnectDelayMs: options.reconnectDelayMs ?? 1, cancelTimeoutMs: 200 });
|
||||
const session = await driver.openSession({ runId: "paperclip-run", normalizedSessionId: "session", workingDirectory: root });
|
||||
const pump = (async () => { for await (const event of session.events()) events.push(event); })();
|
||||
fixtures.push({ root, session, pump, release: releaseFourthRead.resolve });
|
||||
const socket = await lstat(socketPath); expect(socket.isSocket()).toBe(true); expect(socket.mode & 0o077).toBe(0);
|
||||
const started = await session.startTurn({ message: { role: "user", text: "Complete after disconnected observation." } });
|
||||
const originalDeadline = (await checkpoint()).deadlineAt as number;
|
||||
return { root, socketPath, session, events, counts, observations, checkpoint, originalDeadline, turnId: started.turnId,
|
||||
fourthRead: fourthRead.promise, releaseFourthRead: releaseFourthRead.resolve, finish: () => { nativeStatus = "FINISHED"; } };
|
||||
}
|
||||
|
||||
const terminals = (events: PrpEvent[]) => events.filter(event => ["turn.completed", "turn.failed", "turn.interrupted"].includes(event.eventType));
|
||||
|
||||
it("continues observation beyond four active disconnects and delivers the exact terminal once", async () => {
|
||||
const f = await fixture({ holdFourthRead: true });
|
||||
await vi.waitFor(() => expect(f.counts.reads).toBe(4), { timeout: 1_000 });
|
||||
await f.fourthRead;
|
||||
expect(f.observations.map(value => value.status)).toEqual(["RUNNING", "RUNNING", "RUNNING", "RUNNING"]);
|
||||
f.finish(); f.releaseFourthRead();
|
||||
await vi.waitFor(() => expect(terminals(f.events)).toHaveLength(1), { timeout: 1_000 });
|
||||
expect(f.counts).toEqual({ creates: 1, followups: 0, cancels: 0, streams: 5, reads: 5 });
|
||||
expect(f.observations.every(value => value.deadlineAt === f.originalDeadline)).toBe(true);
|
||||
expect(Date.now()).toBeLessThan(f.originalDeadline);
|
||||
const terminal = terminals(f.events)[0]; expect(terminal.eventType).toBe("turn.completed"); expect(terminal.turnId).toBe(f.turnId);
|
||||
expect(terminal.payload.cursorCloudTerminalDelivery).toMatchObject({ status: "FINISHED", result: "Exact final answer after disconnected observation." });
|
||||
expect(f.events.filter(event => event.eventType === "item.completed" && event.payload.channel === "final")).toHaveLength(1);
|
||||
expect((await f.checkpoint()).pendingTerminalDelivery).toEqual(terminal.payload.cursorCloudTerminalDelivery);
|
||||
const settled = { ...f.counts }; await delay(30); expect(f.counts).toEqual(settled);
|
||||
});
|
||||
|
||||
it("still settles when the fourth disconnected stream's status read is already terminal", async () => {
|
||||
const f = await fixture({ finishOnFourthRead: true });
|
||||
await vi.waitFor(() => expect(terminals(f.events)).toHaveLength(1), { timeout: 1_000 });
|
||||
expect(f.counts).toEqual({ creates: 1, followups: 0, cancels: 0, streams: 4, reads: 4 });
|
||||
expect((await f.checkpoint()).pendingTerminalDelivery.status).toBe("FINISHED");
|
||||
});
|
||||
|
||||
it("keeps repeated disconnects within the original deadline and cancels only the saved run", async () => {
|
||||
const f = await fixture({ timeoutMs: 1_000, reconnectDelayMs: 20 });
|
||||
await vi.waitFor(() => expect(terminals(f.events)).toHaveLength(1), { timeout: 2_500 });
|
||||
expect(terminals(f.events)[0].eventType).toBe("turn.interrupted");
|
||||
expect(f.counts.streams).toBeGreaterThan(4); expect(f.counts.streams).toBeLessThan(100);
|
||||
expect(f.counts.creates).toBe(1); expect(f.counts.followups).toBe(0); expect(f.counts.cancels).toBe(1);
|
||||
expect(f.observations.every(value => value.deadlineAt === f.originalDeadline)).toBe(true);
|
||||
expect(Date.now()).toBeGreaterThanOrEqual(f.originalDeadline);
|
||||
const settled = { ...f.counts }; await delay(40); expect(f.counts).toEqual(settled);
|
||||
});
|
||||
|
||||
it.each(["Stop", "close"] as const)("%s aborts observation backoff without a replacement or deadline extension", async operation => {
|
||||
const f = await fixture({ reconnectDelayMs: 60_000 });
|
||||
await vi.waitFor(() => expect(f.counts.reads).toBe(1), { timeout: 1_000 });
|
||||
await delay(20);
|
||||
expect((await f.checkpoint()).deadlineAt).toBe(f.originalDeadline);
|
||||
const before = Date.now();
|
||||
if (operation === "Stop") await f.session.interrupt!({ turnId: f.turnId });
|
||||
else await f.session.close({ reason: "controlled_close_during_backoff" });
|
||||
expect(Date.now() - before).toBeLessThan(1_000);
|
||||
expect(terminals(f.events)).toHaveLength(1);
|
||||
expect(f.counts.creates).toBe(1); expect(f.counts.followups).toBe(0); expect(f.counts.cancels).toBe(1); expect(f.counts.streams).toBe(1);
|
||||
const settled = { ...f.counts }; await delay(40); expect(f.counts).toEqual(settled);
|
||||
expect((await f.checkpoint()).deadlineAt).toBeNull();
|
||||
if (operation === "close") expect((await readdir(f.root)).includes("mcp.sock")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps non-transient stream authorization failures fenced without repeated requests", async () => {
|
||||
const f = await fixture({ streamStatus: 401 });
|
||||
await vi.waitFor(() => expect(f.events.some(event => event.eventType === "harness.diagnostic" && event.payload.code === "cursor_cloud_remote_state_unknown")).toBe(true), { timeout: 1_000 });
|
||||
expect(f.counts).toEqual({ creates: 1, followups: 0, cancels: 0, streams: 1, reads: 0 });
|
||||
expect(terminals(f.events)).toHaveLength(0);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { lstat, unlink } from "node:fs/promises";
|
||||
import { connect } from "node:net";
|
||||
|
||||
export async function releaseCursorCloudStaleSocket(path: string, recoveredDeadOwner: boolean): Promise<void> {
|
||||
let before;
|
||||
try { before = await lstat(path); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; throw error; }
|
||||
if (!recoveredDeadOwner || !before.isSocket() || before.isSymbolicLink() || (before.mode & 0o077) !== 0 || (process.getuid && before.uid !== process.getuid())) throw new Error("Cursor Cloud socket exists without safe dead-owner recovery proof");
|
||||
const stale = await new Promise<boolean>(accept => {
|
||||
const socket = connect({ path });
|
||||
const timer = setTimeout(() => { socket.destroy(); accept(false); }, 250);
|
||||
socket.once("connect", () => { clearTimeout(timer); socket.destroy(); accept(false); });
|
||||
socket.once("error", (error: NodeJS.ErrnoException) => { clearTimeout(timer); socket.destroy(); accept(error.code === "ECONNREFUSED"); });
|
||||
});
|
||||
if (!stale) throw new Error("Cursor Cloud socket has a live or unknown owner");
|
||||
const current = await lstat(path);
|
||||
if (current.ino !== before.ino || current.dev !== before.dev || !current.isSocket()) throw new Error("Cursor Cloud socket owner changed during recovery");
|
||||
await unlink(path);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../contracts/completion-result.js";
|
||||
|
||||
import {
|
||||
RUNNER_TOOL_SCOPE_TOKEN_HEADER,
|
||||
canonicalRunnerToolName,
|
||||
startRunnerToolBridge,
|
||||
type RunnerToolBridge,
|
||||
@@ -84,6 +85,33 @@ describe("runner semantic MCP bridge", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("checks scoped credentials again after a delayed body before dispatch", async () => {
|
||||
let scope = "first", token = "first-scope-token", reads = 0;
|
||||
const handler = vi.fn(async () => ({ ok: true }));
|
||||
const bridge = await startRunnerToolBridge({ tools: [tool("documents.read")], handler,
|
||||
sessionScope: () => scope, scopeToken: () => { reads++; return token; } });
|
||||
bridges.push(bridge);
|
||||
const headers = (value: string) => ({ Authorization: `Bearer ${bridge.secret}`, "content-type": "application/json", [RUNNER_TOOL_SCOPE_TOKEN_HEADER]: `Bearer ${value}` });
|
||||
const initialize = (value: string) => fetch(bridge.url, { method: "POST", headers: headers(value), body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize" }) });
|
||||
expect((await rpc(bridge, { id: 0, method: "initialize" })).status).toBe(401);
|
||||
const first = await initialize(token); expect(first.status).toBe(200);
|
||||
const firstSession = first.headers.get("mcp-session-id")!;
|
||||
const previousReads = reads;
|
||||
const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "documents.read", arguments: { path: "note" } } });
|
||||
let bodyController!: ReadableStreamDefaultController<Uint8Array>;
|
||||
const pending = fetch(bridge.url, { method: "POST", headers: { ...headers(token), "mcp-session-id": firstSession },
|
||||
body: new ReadableStream<Uint8Array>({ start(controller) { bodyController = controller; controller.enqueue(new TextEncoder().encode(body.slice(0, 20))); } }),
|
||||
duplex: "half" } as RequestInit);
|
||||
await vi.waitFor(() => expect(reads).toBeGreaterThan(previousReads));
|
||||
scope = "second"; token = "second-scope-token";
|
||||
const second = await initialize(token); expect(second.status).toBe(200);
|
||||
bodyController.enqueue(new TextEncoder().encode(body.slice(20))); bodyController.close();
|
||||
expect((await pending).status).toBe(401);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
const accepted = await fetch(bridge.url, { method: "POST", headers: { ...headers(token), "mcp-session-id": second.headers.get("mcp-session-id")! }, body });
|
||||
expect(accepted.status).toBe(200); expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps private operations callable but undiscoverable", async () => {
|
||||
const handler = vi.fn(async ({ tool: name }) => ({ name }));
|
||||
const bridge = await startRunnerToolBridge({
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
PRP_COMPLETION_TOOL_NAME,
|
||||
} from "../contracts/completion-result.js";
|
||||
|
||||
export const RUNNER_TOOL_SCOPE_TOKEN_HEADER = "x-paperclip-runner-scope-token";
|
||||
|
||||
export interface RunnerToolDefinition {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -53,6 +55,9 @@ export interface RunnerToolBridgeOptions {
|
||||
/** Internal test seam; production bounds headers and body reads to 30 seconds. */
|
||||
requestBodyTimeoutMs?: number;
|
||||
secret?: string;
|
||||
socketPath?: string;
|
||||
sessionScope?: () => string | null;
|
||||
scopeToken?: () => string | null;
|
||||
}
|
||||
|
||||
export interface RunnerToolBridge {
|
||||
@@ -143,6 +148,10 @@ export async function startRunnerToolBridge(
|
||||
validators,
|
||||
calls,
|
||||
controllers,
|
||||
sessionScope: options.sessionScope,
|
||||
scopeToken: options.scopeToken,
|
||||
sessions: new Map<string, string>(),
|
||||
activeScope: null as string | null,
|
||||
handler: options.handler,
|
||||
captureSemanticReceipt: options.captureSemanticReceipt,
|
||||
timeoutMs: options.timeoutMs === null ? null : positiveBoundedInteger(
|
||||
@@ -184,15 +193,25 @@ export async function startRunnerToolBridge(
|
||||
server.requestTimeout = context.requestBodyTimeoutMs;
|
||||
server.headersTimeout = context.requestBodyTimeoutMs;
|
||||
server.on("clientError", (_error, socket) => socket.destroy());
|
||||
await listenLoopback(server);
|
||||
if (options.socketPath) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(options.socketPath, () => {
|
||||
server.off("error", reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await listenLoopback(server);
|
||||
}
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string") {
|
||||
if (address === null || (typeof address === "string" && !options.socketPath)) {
|
||||
await closeServer(server);
|
||||
throw new Error("Runner tool bridge failed to bind a loopback port");
|
||||
}
|
||||
let closed = false;
|
||||
return Object.freeze({
|
||||
url: `http://127.0.0.1:${address.port}/mcp`,
|
||||
url: typeof address === "string" ? "http://localhost/mcp" : `http://127.0.0.1:${address.port}/mcp`,
|
||||
secret,
|
||||
async close() {
|
||||
if (closed) return;
|
||||
@@ -208,6 +227,10 @@ async function handleRequest(
|
||||
response: ServerResponse,
|
||||
context: {
|
||||
secret: string;
|
||||
sessionScope?: () => string | null;
|
||||
scopeToken?: () => string | null;
|
||||
sessions: Map<string, string>;
|
||||
activeScope: string | null;
|
||||
preserveToolNames: boolean;
|
||||
visibleTools: RunnerToolDefinition[];
|
||||
admittedTools: Map<string, RunnerToolDefinition>;
|
||||
@@ -240,6 +263,14 @@ async function handleRequest(
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
const scopeToken = context.scopeToken?.();
|
||||
if (context.scopeToken) {
|
||||
if (!scopeToken) { response.statusCode = 403; response.end(); return; }
|
||||
const supplied = request.headers[RUNNER_TOOL_SCOPE_TOKEN_HEADER];
|
||||
if (typeof supplied !== "string" || !authorized(supplied, scopeToken)) {
|
||||
response.statusCode = 401; response.end(); return;
|
||||
}
|
||||
}
|
||||
let message: Record<string, unknown>;
|
||||
try {
|
||||
message = parseMessage(
|
||||
@@ -253,13 +284,33 @@ async function handleRequest(
|
||||
writeRpc(response, null, undefined, rpcError(-32700, safeError(error)));
|
||||
return;
|
||||
}
|
||||
if (context.scopeToken && scopeToken !== context.scopeToken()) {
|
||||
response.statusCode = 401; response.end(); return;
|
||||
}
|
||||
const id = message.id ?? null;
|
||||
const method = typeof message.method === "string" ? message.method : "";
|
||||
let sessionKey = "";
|
||||
if (context.sessionScope) {
|
||||
const scope = context.sessionScope();
|
||||
if (!scope) { response.statusCode = 403; response.end(); return; }
|
||||
if (context.activeScope !== scope) {
|
||||
for (const controller of context.controllers.values()) controller.abort();
|
||||
context.sessions.clear(); context.calls.clear(); context.controllers.clear();
|
||||
context.activeScope = scope;
|
||||
}
|
||||
if (method !== "initialize") {
|
||||
const supplied = request.headers["mcp-session-id"];
|
||||
if (typeof supplied !== "string" || context.sessions.get(supplied) !== scope) {
|
||||
response.statusCode = 404; response.end(); return;
|
||||
}
|
||||
sessionKey = supplied + ":";
|
||||
}
|
||||
}
|
||||
if (method === "notifications/cancelled") {
|
||||
const params = isRecord(message.params) ? message.params : {};
|
||||
const requestId = params.requestId;
|
||||
if (typeof requestId === "string" || typeof requestId === "number") {
|
||||
context.controllers.get(rpcIdKey(requestId))?.abort();
|
||||
context.controllers.get(sessionKey + rpcIdKey(requestId))?.abort();
|
||||
}
|
||||
response.statusCode = 202;
|
||||
response.end();
|
||||
@@ -271,7 +322,12 @@ async function handleRequest(
|
||||
return;
|
||||
}
|
||||
if (method === "initialize") {
|
||||
response.setHeader("Mcp-Session-Id", randomBytes(16).toString("hex"));
|
||||
const sessionId = randomBytes(16).toString("hex");
|
||||
if (context.sessionScope) {
|
||||
if (context.sessions.size >= 64) { response.statusCode = 429; response.end(); return; }
|
||||
context.sessions.set(sessionId, context.activeScope!);
|
||||
}
|
||||
response.setHeader("Mcp-Session-Id", sessionId);
|
||||
writeRpc(response, id, {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: { tools: { listChanged: false } },
|
||||
@@ -308,8 +364,8 @@ async function handleRequest(
|
||||
);
|
||||
return;
|
||||
}
|
||||
const callId = String(id);
|
||||
const callKey = rpcIdKey(id);
|
||||
const callId = context.sessionScope ? sessionKey + rpcIdKey(id) : String(id);
|
||||
const callKey = sessionKey + rpcIdKey(id);
|
||||
const args = params.arguments ?? {};
|
||||
const validator = context.validators.get(tool);
|
||||
if (validator === undefined || !validator(args)) {
|
||||
|
||||
@@ -120,5 +120,11 @@ export type { RunnerProviderLaunchRequirements, RunnerProviderLaunchEnvironment,
|
||||
|
||||
export { resolveGrokDisableWebSearch } from "./drivers/acpx/grok-web-search.js";
|
||||
|
||||
export { CURSOR_CLOUD_DEFAULT_TIMEOUT_MS, parseNativeCursorCloudConfig, type NativeCursorCloudConfig } from "./contracts/cursor-cloud.js";
|
||||
|
||||
export { CursorCloudClient, CursorCloudClientError, isCursorCloudRunActive } from "./drivers/cursor-cloud/cloud-client.js";
|
||||
|
||||
export { archiveCursorCloudSessionCheckpoint } from "./drivers/cursor-cloud/checkpoint.js";
|
||||
|
||||
export { readProviderFailure, type ProviderFailure } from "./contracts/provider-failure.generated.js";
|
||||
export { providerFailureFromError } from "./drivers/provider-failure.js";
|
||||
@@ -186,6 +186,7 @@ export interface RunnerProviderLaunchEnvironment {
|
||||
codexCommand?: string;
|
||||
opencodeProxyPath: string;
|
||||
acpxSidecarPath: string;
|
||||
cursorCloudProxyPath?: string;
|
||||
}
|
||||
|
||||
export function createRunnerProviderLaunch(
|
||||
@@ -193,6 +194,16 @@ export function createRunnerProviderLaunch(
|
||||
input: RunnerProviderLaunchEnvironment,
|
||||
): Record<string, unknown> {
|
||||
const { identity, instructions: baseInstructions, runtimeContext, settings, provider } = requirements;
|
||||
if (provider === "cursor_cloud") {
|
||||
if (input.options.runnerFilesystemRoot || !requirements.model || requirements.readOnly) {
|
||||
throw new Error("cursor_cloud_local_controller_required: experimental Cursor Cloud supports standard tasks with a local controller");
|
||||
}
|
||||
if (!input.cursorCloudProxyPath) throw new Error("cursor_cloud_proxy_missing");
|
||||
return { kind: "cursor_cloud", provider: "cursor_cloud", driver: "cursor_cloud_api", providerVersion: "cursor-cloud-v1",
|
||||
command: input.providerNodeCommand, args: [input.cursorCloudProxyPath], cwd: input.cwd,
|
||||
model: requirements.model, instructions: baseInstructions, approvalPolicy: "never",
|
||||
conversationMode: requirements.preparedConversation ? "prepared" : "task" };
|
||||
}
|
||||
const acpxAgent = provider === "acpx" ? settings.acpxAgent ?? "codex" : null;
|
||||
const requestedModel = requirements.model ?? "";
|
||||
const includeCodexCollaborationInstructions = provider === "codex" &&
|
||||
|
||||
@@ -771,6 +771,16 @@ it("reopens Copilot with the current registered files after collecting the previ
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("refreshes Cloud instructions on cold rotation without replacing the pinned launch profile", () => {
|
||||
const identity = { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "next-run", normalizedSessionId: "session", turnId: "turn", itemId: "item" };
|
||||
const provider = { kind: "cursor_cloud", provider: "cursor_cloud", instructions: "Prior instructions.", command: "/pinned/node", args: ["/pinned/proxy.cjs"], model: "pinned-model", conversationMode: "task" };
|
||||
const payload = runnerdRecoveryInternals.rotatedRunAttachPayload({ runAttachTemplate: { provider } }, identity, null, undefined,
|
||||
null, { text: "Current instructions.", context: null });
|
||||
expect(payload.provider).toEqual({ ...provider, instructions: "Current instructions." });
|
||||
expect(provider.instructions).toBe("Prior instructions.");
|
||||
expect(runnerdRecoveryInternals.rotatedRunAttachPayload({ runAttachTemplate: { provider } }, identity, null, undefined).provider).toEqual(provider);
|
||||
});
|
||||
|
||||
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") };
|
||||
|
||||
@@ -508,6 +508,11 @@ function retargetRunAttachPayload(
|
||||
provider.runtimeContext = structuredClone(runtimeContext);
|
||||
}
|
||||
payload.provider = provider;
|
||||
} else if (provider.kind === "cursor_cloud" && currentInstructions !== undefined) {
|
||||
provider.instructions = currentInstructions.context && runtimeContext !== undefined
|
||||
? retargetComposedInstructions(currentInstructions.text, currentInstructions.context, runtimeContext)
|
||||
: currentInstructions.text;
|
||||
payload.provider = provider;
|
||||
}
|
||||
if (authorizedTools !== null) payload.authorizedTools = authorizedTools;
|
||||
if (completionContract !== undefined) {
|
||||
@@ -1240,12 +1245,15 @@ export interface CapabilityRunnerdProcessEvidence {
|
||||
export interface CapabilityRunnerdCodexTransportOptions extends RunnerExecutionDriver {
|
||||
observability?: import("../drivers/agent-observability.js").ProviderObservabilityLaunch;
|
||||
homeMode?: "isolated" | "preconfigured";
|
||||
provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx";
|
||||
provider?: "cursor_cloud" | "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx";
|
||||
observeCredentials?: boolean;
|
||||
onCredentialsUpdated?: (event: PrpEvent) => Promise<void>;
|
||||
onAccountingEvent?: (event: PrpEvent) => Promise<void>;
|
||||
onRecoveryAccountingEvent?: (event: PrpEvent, sourceIdentity: DurableRecoveryIdentity) => Promise<void>;
|
||||
recoverSemanticToolInput?: DurablePrpControlPlaneOptions["recoverSemanticToolInput"];
|
||||
cursorCloud?: import("../contracts/cursor-cloud.js").NativeCursorCloudConfig;
|
||||
cursorCloudProxyPath?: string;
|
||||
cursorCloudProxySha256?: string;
|
||||
recoveryPurpose?: "execution" | "stop_cleanup";
|
||||
opencodePermissionMode?: NativeOpenCodePermissionMode;
|
||||
acpxAgent?: QualifiedAcpxAgent;
|
||||
@@ -3035,7 +3043,7 @@ async function settleRetainedRunnerdSessionOwned(
|
||||
}
|
||||
|
||||
type BuildOwnedCliArtifact =
|
||||
"acpx-runtime-sidecar.cjs" | "opencode-app-server-proxy.cjs";
|
||||
"acpx-runtime-sidecar.cjs" | "opencode-app-server-proxy.cjs" | "cursor-cloud-app-server-proxy.cjs";
|
||||
|
||||
function buildOwnedCliArtifactCandidates(
|
||||
artifact: BuildOwnedCliArtifact,
|
||||
@@ -3352,6 +3360,16 @@ function createCapabilityRunnerdProviderEnvironmentBase(input: {
|
||||
? { PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH: input.runtimeContextPath }
|
||||
: {}),
|
||||
};
|
||||
if (input.provider === "cursor_cloud") {
|
||||
const env = input.options.environment ?? {};
|
||||
if (!env.CURSOR_API_KEY?.trim()) throw new Error("cursor_cloud_api_key_required");
|
||||
const selected: NodeJS.ProcessEnv = { CURSOR_API_KEY: env.CURSOR_API_KEY, ...commonIdentity };
|
||||
for (const name of ["PAPERCLIP_CURSOR_CLOUD_ARTIFACT_ROOT", "PAPERCLIP_CURSOR_CLOUD_CONFIG_PATH", "PAPERCLIP_CURSOR_CLOUD_RELAY_SOCKET_PATH", "PAPERCLIP_CURSOR_CLOUD_RELAY_TOKEN", "PAPERCLIP_CURSOR_CLOUD_RELAY_URL", "PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT"]) {
|
||||
if (!env[name]) throw new Error("cursor_cloud_relay_binding_required");
|
||||
selected[name] = env[name];
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
if (input.provider === "opencode") {
|
||||
return {
|
||||
...createSanitizedOpenCodeRunnerEnvironment(input.options.environment),
|
||||
@@ -5141,6 +5159,15 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
));
|
||||
const providerNodeCommand =
|
||||
this.options.providerNodeCommand ?? process.execPath;
|
||||
const cursorCloudProxyPath = provider === "cursor_cloud"
|
||||
? this.options.cursorCloudProxyPath ?? resolveBuildOwnedCliArtifact("cursor-cloud-app-server-proxy.cjs") : undefined;
|
||||
const runnerCursorCloudLaunchProfile = cursorCloudProxyPath ? {
|
||||
command: providerNodeCommand,
|
||||
commandSha256: this.options.providerNodeCommandSha256 ?? `sha256:${createHash("sha256").update(readFileSync(providerNodeCommand)).digest("hex")}`,
|
||||
proxyScript: cursorCloudProxyPath,
|
||||
proxyScriptSha256: this.options.cursorCloudProxySha256 ?? `sha256:${createHash("sha256").update(readFileSync(cursorCloudProxyPath)).digest("hex")}`,
|
||||
} : undefined;
|
||||
|
||||
const effectiveCodexCommand = provider === "codex"
|
||||
? this.options.codexCommand ?? resolveCodexCommand(undefined, this.options.environment, String(params.cwd ?? tmpdir()))
|
||||
: undefined;
|
||||
@@ -5229,10 +5256,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
), {
|
||||
options: this.options, root: this.#root, cwd: String(params.cwd ?? tmpdir()),
|
||||
codexHome, providerNodeCommand, codexCommand: effectiveCodexCommand,
|
||||
opencodeProxyPath, acpxSidecarPath,
|
||||
opencodeProxyPath, acpxSidecarPath, cursorCloudProxyPath,
|
||||
}),
|
||||
};
|
||||
return { runAttachTemplate, runnerAcpxLaunchProfile, runnerOpenCodeLaunchProfile,
|
||||
return { runAttachTemplate, runnerAcpxLaunchProfile, runnerOpenCodeLaunchProfile, runnerCursorCloudLaunchProfile,
|
||||
environment: withRunnerdProviderTrace(
|
||||
createCapabilityRunnerdProviderEnvironment({
|
||||
provider,
|
||||
@@ -5368,6 +5395,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
runnerDigest: runnerArtifact.digest,
|
||||
acpxLaunchProfile: prepared.runnerAcpxLaunchProfile,
|
||||
opencodeLaunchProfile: prepared.runnerOpenCodeLaunchProfile,
|
||||
cursorCloudLaunchProfile: prepared.runnerCursorCloudLaunchProfile,
|
||||
environment: prepared.environment,
|
||||
diagnosticsDirectory: resolve(this.#root, "diagnostics"),
|
||||
processLauncher: this.options.runnerProcessLauncher,
|
||||
@@ -5452,7 +5480,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
controlPlaneDirectory,
|
||||
"control-plane-state.json",
|
||||
);
|
||||
const localProvider =
|
||||
const supportsWarmRecovery =
|
||||
this.options.provider === undefined ||
|
||||
this.options.provider === "codex" ||
|
||||
this.options.provider === "opencode" ||
|
||||
@@ -5461,6 +5489,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
this.options.readRunnerState === undefined &&
|
||||
this.options.runnerStateDirectory === undefined &&
|
||||
this.options.runnerFilesystemRoot === undefined && !this.options.prepareLaunch;
|
||||
const supportsColdRotation = supportsWarmRecovery ||
|
||||
(this.options.provider === "cursor_cloud" && localStateOwner);
|
||||
const localRunnerStatePath = resolve(
|
||||
this.#root,
|
||||
"runner",
|
||||
@@ -5480,7 +5510,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
? readControlPlaneState(controlPlaneDirectory)
|
||||
: null;
|
||||
} catch (error) {
|
||||
if (localProvider && localStateOwner && !hasRunnerWarmBoundary) {
|
||||
if (supportsColdRotation && localStateOwner && !hasRunnerWarmBoundary) {
|
||||
quarantineLocalRuntimeState(this.#root, error);
|
||||
}
|
||||
throw error;
|
||||
@@ -5532,7 +5562,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
// No malformed or unsupported transition is reinterpreted as legacy.
|
||||
if (
|
||||
(!localStateOwner && !this.options.readRunnerState) ||
|
||||
!localProvider ||
|
||||
!supportsWarmRecovery ||
|
||||
!controlPlaneState ||
|
||||
!candidateRunnerState ||
|
||||
(this.options.controlPlaneRegistration !== undefined &&
|
||||
@@ -5636,7 +5666,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
identity.itemId === desiredIdentity.itemId;
|
||||
let rotatedAuthority = false;
|
||||
if (controlPlaneState === null) {
|
||||
if (!localProvider) {
|
||||
if (!supportsColdRotation) {
|
||||
throw new Error("PRP provider resume state is unavailable");
|
||||
}
|
||||
const archivedState = latestArchivedControlPlaneState(
|
||||
@@ -5674,7 +5704,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
identity = desiredIdentity;
|
||||
rotatedAuthority = true;
|
||||
} else if (!exactAuthority && warmRecovery === null) {
|
||||
if (!localProvider || controlPlaneState === null) {
|
||||
if (!supportsColdRotation || controlPlaneState === null) {
|
||||
throw new Error("native_runner_prp_run_rotation_unavailable");
|
||||
}
|
||||
if (localStateOwner) {
|
||||
@@ -5760,6 +5790,15 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
));
|
||||
const providerNodeCommand =
|
||||
this.options.providerNodeCommand ?? process.execPath;
|
||||
const cursorCloudProxyPath = provider === "cursor_cloud"
|
||||
? this.options.cursorCloudProxyPath ?? resolveBuildOwnedCliArtifact("cursor-cloud-app-server-proxy.cjs") : undefined;
|
||||
const runnerCursorCloudLaunchProfile = cursorCloudProxyPath ? {
|
||||
command: providerNodeCommand,
|
||||
commandSha256: this.options.providerNodeCommandSha256 ?? `sha256:${createHash("sha256").update(readFileSync(providerNodeCommand)).digest("hex")}`,
|
||||
proxyScript: cursorCloudProxyPath,
|
||||
proxyScriptSha256: this.options.cursorCloudProxySha256 ?? `sha256:${createHash("sha256").update(readFileSync(cursorCloudProxyPath)).digest("hex")}`,
|
||||
} : undefined;
|
||||
|
||||
const opencodeExecutable =
|
||||
provider === "opencode" && preparesProviderLaunch
|
||||
? resolveRunnerOpenCodeExecutable(this.options)
|
||||
@@ -6139,6 +6178,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
runnerDigest: runnerArtifact.digest,
|
||||
acpxLaunchProfile: runnerAcpxLaunchProfile,
|
||||
opencodeLaunchProfile: runnerOpenCodeLaunchProfile,
|
||||
cursorCloudLaunchProfile: runnerCursorCloudLaunchProfile,
|
||||
environment: this.options.prepareLaunch ? {} : withRunnerdProviderTrace(
|
||||
createCapabilityRunnerdProviderEnvironment({
|
||||
provider,
|
||||
@@ -7988,6 +8028,7 @@ function openedThreadModelProvider(
|
||||
model: unknown,
|
||||
): string {
|
||||
if (provider === "opencode" && typeof model === "string") return model.split("/", 1)[0]!;
|
||||
if (provider === "cursor_cloud") return "cursor";
|
||||
if (provider === "claude_managed") return "anthropic";
|
||||
if (provider === "aws_agentcore") return "aws";
|
||||
if (provider === "acpx") {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { resolveQualifiedAcpxProfile } from "./drivers/acpx/qualified-profiles.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createCodexNativeSessionBackend } from "./backends/codex-native-backend.js";
|
||||
import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } from "./backends/codex-native-backend.js";
|
||||
import { FakeCodexTransport, WORKSPACE } from "./drivers/codex/codex-app-server-driver.test-support.js";
|
||||
|
||||
import type { ControlPlanePort } from "./contracts/control-plane-port.js";
|
||||
@@ -228,6 +230,56 @@ function highestContiguous(events: PrpEvent[]): number {
|
||||
}
|
||||
|
||||
describe("executeNativeSession recovery", () => {
|
||||
it("completes Cursor Cloud semantic planning through its default-only harness without losing the pinned revision", async () => {
|
||||
await writeFile(join(WORKSPACE, "cloud-plan-instructions.md"), "Make the plan only. Do not implement it.\n");
|
||||
const prepared = preparedInput();
|
||||
const context = { ...prepared.runtimeContext, instructions: { ...prepared.runtimeContext.instructions,
|
||||
entryPath: "cloud-plan-instructions.md", bundle: { ...prepared.runtimeContext.instructions.bundle, rootPath: WORKSPACE } } };
|
||||
const planningContext = { documentId: "plan-document", baseRevisionId: "plan-revision-3", baseRevisionNumber: 3,
|
||||
markdown: "# Existing canonical plan", sha256: createHash("sha256").update("# Existing canonical plan").digest("hex"), reviewContext: { feedback: "Keep the requested scope" } };
|
||||
const executionInput: NativeExecutionInputV5 = {
|
||||
...prepared, executionMode: "plan", planningContext,
|
||||
task: { ...prepared.task, workMode: "planning" },
|
||||
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
||||
workspace: { ...input.workspace, cwd: WORKSPACE },
|
||||
session: { ...input.session, driverKind: "cursor_cloud_api", lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
provider: { kind: "cursor_cloud", model: "explicit-cloud-model", cursorCloud: { schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/fixture" }] } },
|
||||
};
|
||||
const planResult: PrpStructuredRunResult = { ...result, summary: "The requested plan is ready.", artifacts: [{ kind: "native_provider_plan", ref: "<proposed_plan>Keep the requested scope.</proposed_plan>" }] };
|
||||
const transport = new FakeCodexTransport();
|
||||
transport.confirmCollaborationMode = false;
|
||||
const backend = createRunnerdNativeSessionBackend(executionInput, { runnerInstanceId: "runner-recovery", transportFactory: () => transport });
|
||||
const committed: PrpEvent[] = [];
|
||||
const completeRun = vi.fn(async () => undefined);
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, async checkpointSession() {},
|
||||
async appendEvent(event) { committed.push(structuredClone(event)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(query) { const events = committed.filter(event => event.sourceInstanceId === query.sourceInstanceId); return { events: events.filter(event => event.sourceSeq > query.afterSourceSeq).slice(0, query.limit), highestContiguousSourceSeq: highestContiguous(events) }; },
|
||||
completeRun,
|
||||
};
|
||||
const abort = new AbortController();
|
||||
const completion = executeNativeSession({ input: executionInput, backend, controlPlane: port,
|
||||
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", signal: abort.signal });
|
||||
let failure: unknown;
|
||||
void completion.catch(error => { failure = error; });
|
||||
try {
|
||||
await vi.waitFor(() => expect(transport.calls.some(call => call.method === "turn/start") || failure).toBeTruthy());
|
||||
if (failure) await completion;
|
||||
const started = transport.calls.find(call => call.method === "turn/start")!;
|
||||
const envelope = JSON.parse((started.params.input as Array<{ text: string }>)[0]!.text);
|
||||
expect(envelope).toMatchObject({ executionMode: "plan", task: { workMode: "planning" }, planningContext });
|
||||
expect(transport.calls.find(call => call.method === "thread/start")?.params.baseInstructions).toContain("Make the plan only. Do not implement it.");
|
||||
expect(started.params).not.toHaveProperty("collaborationMode");
|
||||
transport.push("turn/started", { threadId: "thread-1", turn: { id: "turn-1", status: "inProgress" } });
|
||||
transport.push("paperclip/runResult", { threadId: "thread-1", turnId: "turn-1", result: planResult });
|
||||
transport.push("turn/completed", { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } });
|
||||
await expect(completion).resolves.toMatchObject({ result: planResult, terminal: { runTerminalState: "succeeded" } });
|
||||
expect(completeRun).toHaveBeenCalledOnce();
|
||||
expect(committed.some(event => event.eventType === "run.result.accepted")).toBe(true);
|
||||
expect(executionInput.planningContext).toEqual(planningContext);
|
||||
} finally { abort.abort(); await completion.catch(() => undefined); }
|
||||
});
|
||||
|
||||
it.each([false, true].flatMap(recovered => [
|
||||
{ recovered, cwd: "/home/agent" },
|
||||
{ recovered, cwd: "/projects/repository/worktrees/task" },
|
||||
@@ -8077,6 +8129,68 @@ describe("executeNativeSession recovery", () => {
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "default", bound: undefined, limit: 15_000 },
|
||||
{ name: "not-finite", bound: Number.NaN, limit: 15_000 },
|
||||
{ name: "infinite", bound: Infinity, limit: 15_000 },
|
||||
{ name: "negative", bound: -1, limit: 15_000 },
|
||||
{ name: "nonpositive", bound: 0, limit: 15_000 },
|
||||
{ name: "oversized", bound: 30_001, limit: 15_000 },
|
||||
{ name: "fractional", bound: 30_000.5, limit: 15_000 },
|
||||
{ name: "extended-missing", bound: 30_000, limit: 30_000 },
|
||||
{ name: "short-operation", bound: 30_000, limit: 250, operationMs: 1_000 },
|
||||
{ name: "foreign-terminal", bound: 30_000, limit: 30_000, fault: "foreign" },
|
||||
{ name: "unproven-terminal", bound: 30_000, limit: 0, fault: "unproven" },
|
||||
{ name: "startup-abort", bound: 30_000, limit: 100, fault: "abort" },
|
||||
])("bounds governed terminal observation without finalizing an unproven wait: $name", async ({ bound, limit, operationMs, fault }) => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const trigger = { ...runnerEvent(1, "item.completed", { kind: "dynamicToolCall" }), turnId: "turn-waiting" };
|
||||
const history: PrpEvent[] = [];
|
||||
let checkpoint: PersistedNativeSession | undefined;
|
||||
let releaseStream!: () => void;
|
||||
const streamClosed = new Promise<void>(resolve => { releaseStream = resolve; });
|
||||
let cancelAt = 0;
|
||||
const cancel = vi.fn(({ signal }: { signal: AbortSignal }) => { cancelAt = Date.now(); signal.addEventListener("abort", releaseStream, { once: true }); return { cleanup: Promise.resolve(), terminalDrainTimeoutMs: bound }; });
|
||||
const complete = vi.fn(async () => undefined);
|
||||
const session: NativeSession = {
|
||||
identity: () => identity,
|
||||
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
||||
async startTurn() { if (fault === "abort") await new Promise((_, reject) => setTimeout(() => reject(new Error("controlled startup abort")), 100)); return { turnId: trigger.turnId }; }, cancel,
|
||||
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity, activeTurnId: trigger.turnId }; },
|
||||
async result() { return null; }, async close() { releaseStream(); },
|
||||
async *events() {
|
||||
yield trigger;
|
||||
if (fault === "foreign" || fault === "unproven") yield { ...runnerEvent(2, "turn.interrupted", { providerTerminalObserved: fault !== "unproven" }), turnId: fault === "foreign" ? "foreign-turn" : trigger.turnId };
|
||||
await streamClosed;
|
||||
},
|
||||
};
|
||||
const backend: NativeSessionBackend = { async descriptor() { return { kind: "mock", name: "bounded-cancellation", version: "1", capabilities: await session.capabilities() }; }, async openSession() { return session; } };
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, completeRun: complete,
|
||||
async checkpointSession(value) { checkpoint = structuredClone(value); },
|
||||
async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(request) { return { events: history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: 0 }; },
|
||||
};
|
||||
const execution = executeNativeSession({ input, backend, controlPlane: port, timeoutMs: operationMs ?? 120_000,
|
||||
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", resolveGovernedWait: () => yieldedResult });
|
||||
let settled = false;
|
||||
void execution.then(() => { settled = true; }, () => { settled = true; });
|
||||
await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce());
|
||||
if (limit > 0) {
|
||||
await vi.advanceTimersByTimeAsync(Math.max(0, limit - 1 - (Date.now() - cancelAt)));
|
||||
expect(settled).toBe(false);
|
||||
}
|
||||
await vi.advanceTimersByTimeAsync(fault === "abort" ? 1_002 : 2);
|
||||
if (fault === "abort") await expect(execution).rejects.toThrow("controlled startup abort");
|
||||
else await expect(execution).rejects.toMatchObject({ code: "native_provider_terminal_failed" });
|
||||
expect(checkpoint?.governedWait).toEqual({ sourceEvent: trigger, result: yieldedResult });
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
expect(history.some(event => event.eventType === "run.terminal")).toBe(false);
|
||||
if (fault === "foreign") expect(history).toEqual([trigger]);
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
|
||||
it.each(["interrupt_acknowledgement", "runtime_request.cancelled", "runtime_request.expired"])("retains %s in contiguous governed-wait history for recovery", async cancellationKind => {
|
||||
const trigger = { ...runnerEvent(1, "item.completed", { kind: "dynamicToolCall" }), turnId: "turn-waiting" };
|
||||
const acknowledgement: PrpEvent = { ...runnerEvent(2, cancellationKind === "interrupt_acknowledgement" ? "item.completed" : cancellationKind as "runtime_request.cancelled" | "runtime_request.expired",
|
||||
|
||||
@@ -54,6 +54,7 @@ const REUSABLE_SESSION_CANCELLATION_SETTLEMENT_GRACE_MS = 10_000;
|
||||
// (1s). Allow that bounded settlement plus transport scheduling; never treat
|
||||
// elapsed time or a cancellation acknowledgement as usage/terminal evidence.
|
||||
const GOVERNED_ACCOUNTING_DRAIN_MS = 15_000;
|
||||
const MAX_GOVERNED_ACCOUNTING_DRAIN_MS = 30_000;
|
||||
const DEFAULT_NATIVE_CHECKPOINT_TIMEOUT_MS = 30_000;
|
||||
type NativeSessionCleanupDomain = string;
|
||||
|
||||
@@ -980,7 +981,11 @@ async function consumeTurn(
|
||||
const drainAbort = new AbortController();
|
||||
const stopDrain = () => drainAbort.abort(appendAbort.signal.reason);
|
||||
appendAbort.signal.addEventListener("abort", stopDrain, { once: true });
|
||||
const drainMs = Math.min(GOVERNED_ACCOUNTING_DRAIN_MS, timeoutMs > 0 ? Math.max(1, Math.floor(timeoutMs / 4)) : GOVERNED_ACCOUNTING_DRAIN_MS);
|
||||
const requestedDrainMs = cancellation.terminalDrainTimeoutMs;
|
||||
const terminalDrainMs = typeof requestedDrainMs === "number" && Number.isSafeInteger(requestedDrainMs)
|
||||
&& requestedDrainMs > 0 && requestedDrainMs <= MAX_GOVERNED_ACCOUNTING_DRAIN_MS
|
||||
? requestedDrainMs : GOVERNED_ACCOUNTING_DRAIN_MS;
|
||||
const drainMs = Math.min(terminalDrainMs, timeoutMs > 0 ? Math.max(1, Math.floor(timeoutMs / 4)) : terminalDrainMs);
|
||||
let drainTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const drained = (async () => {
|
||||
while (!drainAbort.signal.aborted) {
|
||||
@@ -2647,7 +2652,11 @@ export async function executeNativeSession(
|
||||
...(recovered && modelEnvelope.schema === "paperclip.native-continuation.v1"
|
||||
? { continuation: true as const } : {}),
|
||||
requestedCollaborationMode:
|
||||
"executionMode" in input ? input.executionMode : "default",
|
||||
// Paperclip planning remains in the envelope and tool authority.
|
||||
// Cloud's qualified harness exposes only default collaboration.
|
||||
input.provider.kind === "cursor_cloud"
|
||||
? "default"
|
||||
: "executionMode" in input ? input.executionMode : "default",
|
||||
});
|
||||
await checkpoint();
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,8 @@ export const providerDescriptorSchema = {
|
||||
"acpx",
|
||||
"openai_dot",
|
||||
"grok_bot",
|
||||
"muse"
|
||||
"muse",
|
||||
"cursor_cloud"
|
||||
]
|
||||
},
|
||||
"driver": {
|
||||
@@ -1238,7 +1239,8 @@ export const providerDescriptorSchema = {
|
||||
"acpx_runtime",
|
||||
"openai_dot_mcp",
|
||||
"grok_bot_mcp",
|
||||
"muse_external"
|
||||
"muse_external",
|
||||
"cursor_cloud_api"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
@@ -1534,6 +1536,19 @@ export const providerDescriptorSchema = {
|
||||
"const": "remote_service"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "cursor_cloud"
|
||||
},
|
||||
"driver": {
|
||||
"const": "cursor_cloud_api"
|
||||
},
|
||||
"executionKind": {
|
||||
"const": "local_process"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 22,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "ddd98aabe20f4d40b04bb58126bda548a9d85b8234a3f9ea84b6db414e9b4814",
|
||||
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
|
||||
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
|
||||
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"commandDigest": "sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 23,
|
||||
"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": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "ddd98aabe20f4d40b04bb58126bda548a9d85b8234a3f9ea84b6db414e9b4814",
|
||||
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
|
||||
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
|
||||
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"commandDigest": "sha256:87e275270e50b42290d10ecec9dfce86ffca2b7a7519cb60c584004adfc86437",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 24,
|
||||
"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": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "6fc6ceb1222cec5586952cf51e2df045ecf751adfc937f5125550a311f5d3896",
|
||||
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
|
||||
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
|
||||
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"commandDigest": "sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 10,
|
||||
"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.v1",
|
||||
"semanticReceiptSourceSha256": "3d3d0fe5ce0e4dbfc78cf95b1bfcff4d4b3760c548e74c6c12db5bcacdd46ae2",
|
||||
"semanticBridgeSourceSha256": "ab8f872344b2beb50e3f9970e0631c451a5493e66d259582ecc9a214624269b5",
|
||||
"toolEvidenceSourceSha256": "f0a93aab9940051972028d6f2610b4018a5a0d6b2326b7c61b767b38442629ca"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"commandDigest": "sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 11,
|
||||
"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": "a261497560840da66613a5f4a3dafeba05e53d3a8647c180c9604b3cf983dc23",
|
||||
"semanticDirectDriverSourceSha256": "31e36917ee3592fa6d8f4b86632129fdffae20c45aed2e0e367ccf523e064f88",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
|
||||
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
|
||||
"semanticSchemaBundleSourceSha256": "bb55e18c5563bf5ec226e301c3399229304f295e986865e96554f193535368f2",
|
||||
"semanticSidecarProtocolSourceSha256": "165cad76f224d60f0dc7928736d47a16787eaa8ab9e4c2222488da5fc8ad632e"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 12,
|
||||
"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": "d58076d59ec23fe2261cba3aa0dca3e9f189105f0a5fa7ca3eeaac05cb338f31",
|
||||
"semanticDirectDriverSourceSha256": "31e36917ee3592fa6d8f4b86632129fdffae20c45aed2e0e367ccf523e064f88",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
|
||||
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
|
||||
"semanticSchemaBundleSourceSha256": "bb55e18c5563bf5ec226e301c3399229304f295e986865e96554f193535368f2",
|
||||
"semanticSidecarProtocolSourceSha256": "165cad76f224d60f0dc7928736d47a16787eaa8ab9e4c2222488da5fc8ad632e",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:3ff08fbe76fe4549c9eb01e8794428d8909c65c151d775220f2ec111d9e6f7c1",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 13,
|
||||
"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": "560010031c58cf6d492784f62fadc96f33bec9c1d12e67fece48b4dfcee518ed",
|
||||
"semanticDirectDriverSourceSha256": "2db465e56ba41f06b406e039e566d9e3aea0a014fc6a47a6a54d5bc682dabcab",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
|
||||
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
|
||||
"semanticSchemaBundleSourceSha256": "2974c75590f816fbdbc7806f531698fdc8d62a99601a17625b81de0f074d6383",
|
||||
"semanticSidecarProtocolSourceSha256": "72d9800603a652d6580084bb3b5dac4d4dd6d644324b0d15d293d7c198fd6b55",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"commandDigest": "sha256:9e11997fe602749000ebef97a9933c976afe1783be884871488c1e0d3e7d7b34",
|
||||
"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": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "5cf0ad62e06071f59eb27cedd7aa5d5852826ab1dfb96dbda846adc1535a9eee",
|
||||
"semanticDirectDriverSourceSha256": "0cb9b66ad41e643c9e018eeb6d6468eb306d0b1ad7d2776ffa9dd11d2b28abca",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "b74133bd72f92d6bd0652c1772a45227f6a549232554528fef2761b17b8f02e7",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "7dc408a9c5114e7eb28f46fbab5d4cac23789095ab2e37725a3e15518f88232a",
|
||||
"runtimeSandboxSourceSha256": "d78b859d170b931b8fc310577e6d1d01a1c20f168ad48c5de84bca2ea325aad9",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "f5e825c368ebd739afa51711bfbbc72ac4d19a9d23bcafd9738ff0e337f55eaf"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+43
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"commandDigest": "sha256:3ad622b836afef0849a53ed4d9ae7b00dfd91d290bb8b62ce6390357f26c0456",
|
||||
"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": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "5cf0ad62e06071f59eb27cedd7aa5d5852826ab1dfb96dbda846adc1535a9eee",
|
||||
"semanticDirectDriverSourceSha256": "0cb9b66ad41e643c9e018eeb6d6468eb306d0b1ad7d2776ffa9dd11d2b28abca",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "1572b19f84410959ecccbfcb5ffc168ddd85d1a4f8238fabeaaf662f6289fe25",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "7dc408a9c5114e7eb28f46fbab5d4cac23789095ab2e37725a3e15518f88232a",
|
||||
"runtimeSandboxSourceSha256": "e56b7afda01cd4d3b16559c3a76939a967ab2993aa73848588bc6ab3aa223c4b",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "f5e825c368ebd739afa51711bfbbc72ac4d19a9d23bcafd9738ff0e337f55eaf"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+43
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"commandDigest": "sha256:2d826980ee935f9f32108ef3528657a9b0b658dfe9a8ff5bfd499f17aa908fb6",
|
||||
"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": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "f65b3d667c7d2657384284aba14557e8ea736fef2ae458bacc98d64f280232dd",
|
||||
"semanticDirectDriverSourceSha256": "5012d883c2dd192fff20308a87e0b4001fa4b258a774cec4751728bfe781185d",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "1572b19f84410959ecccbfcb5ffc168ddd85d1a4f8238fabeaaf662f6289fe25",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "5a056b64e0c187bd74aa7d04265a9b1db20e2f0d9d93880e0d628b4d25a55fc7",
|
||||
"runtimeSandboxSourceSha256": "e56b7afda01cd4d3b16559c3a76939a967ab2993aa73848588bc6ab3aa223c4b",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "f5e825c368ebd739afa51711bfbbc72ac4d19a9d23bcafd9738ff0e337f55eaf"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+43
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"commandDigest": "sha256:bb7c8cdd1ee01065015b0c88e32fac953d5fbe2b2bb224781df57ca87c820a47",
|
||||
"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": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "f65b3d667c7d2657384284aba14557e8ea736fef2ae458bacc98d64f280232dd",
|
||||
"semanticDirectDriverSourceSha256": "5012d883c2dd192fff20308a87e0b4001fa4b258a774cec4751728bfe781185d",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"nativeExecutionSourceSha256": "98aad5290e0ffef0628e5f6ef2e5e9589efc55b7fe29c5009681001f123c54f0",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "1572b19f84410959ecccbfcb5ffc168ddd85d1a4f8238fabeaaf662f6289fe25",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "5a056b64e0c187bd74aa7d04265a9b1db20e2f0d9d93880e0d628b4d25a55fc7",
|
||||
"runtimeSandboxSourceSha256": "e56b7afda01cd4d3b16559c3a76939a967ab2993aa73848588bc6ab3aa223c4b",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "f5e825c368ebd739afa51711bfbbc72ac4d19a9d23bcafd9738ff0e337f55eaf"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+43
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"commandDigest": "sha256:8fb78eb902e7b162045d887b21c4738a4cd18326015c523c762601fd45f9c8e7",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "f65b3d667c7d2657384284aba14557e8ea736fef2ae458bacc98d64f280232dd",
|
||||
"semanticDirectDriverSourceSha256": "5012d883c2dd192fff20308a87e0b4001fa4b258a774cec4751728bfe781185d",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"guardedInstallationSourceSha256": "be0271e809639a4e6e0ecb21f3f04e6d7f17b2374682930a8b85798c1eb70e71",
|
||||
"nativeExecutionSourceSha256": "f34b5e28109ed183d922fdd9d6c15acc4a1e57dcda118ca9a114c8905dd0a3d0",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "1572b19f84410959ecccbfcb5ffc168ddd85d1a4f8238fabeaaf662f6289fe25",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "5a056b64e0c187bd74aa7d04265a9b1db20e2f0d9d93880e0d628b4d25a55fc7",
|
||||
"runtimeSandboxSourceSha256": "e56b7afda01cd4d3b16559c3a76939a967ab2993aa73848588bc6ab3aa223c4b",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "9bbe8329064ee88418528394f8e1cdc716a0b2b35092dc70729b8850e46b7a68"
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fb4a232460dc9e6c7e7221cb4ddd66d7cb82fa0b35e1b3eb04f2d1ee5ee18c86",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:2ec904f44218f51058fa83d7437b26c4551e1c65e6f2c55f5a6386fa4061e96d",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
|
||||
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "969c1c47bc538422210e3789bd53249ebf0f6be335a38c7997d201f0783dfd6b",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:5e79c2b1a42b98a545bafe9e93f8c6b6c423bec1c18dc1f0a3cf48477b757389",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
|
||||
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "2232194f5da2efb3681a5727d93af5253fe7416fdea41bc4fc98ca5deeb0e4b8",
|
||||
"semanticSchemaBundleSourceSha256": "1e2b5d573a929c34a7104efe6dea9621218fb7fe7eac3851a24c41d8284c8dd4",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"commandDigest": "sha256:c2825f706f839071ce5aa9ddc2a058d785a55644964ff9c2ba878ac9ccc66fef",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 19,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "f65b3d667c7d2657384284aba14557e8ea736fef2ae458bacc98d64f280232dd",
|
||||
"semanticDirectDriverSourceSha256": "5012d883c2dd192fff20308a87e0b4001fa4b258a774cec4751728bfe781185d",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"runtimeRequestBridgeSourceSha256": "646c01cc073e62f577ecebb6554d785b08edb3fd6e2fcb4db402e17974edd25b",
|
||||
"runtimeRequestFacadeSourceSha256": "773b860fb7a1444bc769569fbc8a3b67bd08d7ade3e7a843358e4c707f182b72",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "be0271e809639a4e6e0ecb21f3f04e6d7f17b2374682930a8b85798c1eb70e71",
|
||||
"nativeExecutionSourceSha256": "f290d8307f57803aac861a1ba6f847774d38c89e62f32293d69effd289d5c0c4",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "1572b19f84410959ecccbfcb5ffc168ddd85d1a4f8238fabeaaf662f6289fe25",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "5a056b64e0c187bd74aa7d04265a9b1db20e2f0d9d93880e0d628b4d25a55fc7",
|
||||
"runtimeSandboxSourceSha256": "e56b7afda01cd4d3b16559c3a76939a967ab2993aa73848588bc6ab3aa223c4b",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "9bbe8329064ee88418528394f8e1cdc716a0b2b35092dc70729b8850e46b7a68"
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 19,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "e9ca8d1a7449fe28b63dcd0f858bfc233f1ec791e159cdd8436d6ea34a3089fe",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:38abf359adeef43f1f19230245c6e2051c55fdd79571882ef6f00b7cb2958d93",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 19,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
|
||||
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "0703401404effcd5d431b5178bde78e9f63f797a39389add22add7c33986f75c",
|
||||
"semanticSchemaBundleSourceSha256": "95551282468ec16bec4c3ddd0a77a9b2b0e044d7ff018354d4229c3f26e3e228",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:2fec061f29f9cac4f6aa70f9329e1b27ce0c9cf4825030fdbb78979ced9bc201",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 19,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
|
||||
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "2232194f5da2efb3681a5727d93af5253fe7416fdea41bc4fc98ca5deeb0e4b8",
|
||||
"semanticSchemaBundleSourceSha256": "559bde5f5e6433080fc5afd54ea35dc51a378f395b2c45728261b328b7d4d3fc",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"commandDigest": "sha256:6ab53285d7cb0eaf79fafec9e0bf0f1a42044761b4b0cf129e0159c8c2fad539",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 20,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "55a912125cd9cb71c80aafb7dc013aabf88002b7a8c95b3c74c3201ebc8ef527",
|
||||
"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": "6f872180254a747262c3b4a084012704676d7d0b9d9ceb9837709a0bd9c75f70",
|
||||
"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": "f65b3d667c7d2657384284aba14557e8ea736fef2ae458bacc98d64f280232dd",
|
||||
"semanticDirectDriverSourceSha256": "5012d883c2dd192fff20308a87e0b4001fa4b258a774cec4751728bfe781185d",
|
||||
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
|
||||
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
|
||||
"semanticCompletionContractSourceSha256": "bd4bb79d2be5c4ee3765df67dfb4da98ef95bc532cac872be40cb41a6bff6cda",
|
||||
"semanticValidatorsSourceSha256": "c3e7328c0d27feb8ee2ca53b1e1c6963c204939ed22b875add748de77c99b52b",
|
||||
"semanticSchemaBundleSourceSha256": "619f2149a89f952253f8e178645ce4bf986215884b04ce0b7c5ae2ac940849f7",
|
||||
"semanticSidecarProtocolSourceSha256": "66f717653936a80ef3897348adead666c842b6e56278521f4f2541d0a430b51b",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"nativeSessionRuntimeSourceSha256": "882ca4304ff32077e0e16eb7366512d10f9c08d75d3f1204e6a7f6c60dbea03e",
|
||||
"nativeRuntimeContextSourceSha256": "b61065b993d79a8cb6525fe6b398049150d8c9c19dac18dd1d0ce57ae38e1f99",
|
||||
"nativeSessionContractSourceSha256": "0262d1bc0b43894473bde2f05064455ebf73fbb1cd1c0cfa5f42e19596ef80fb",
|
||||
"harnessSessionContractSourceSha256": "ffc9e67b4e864ab310f22d94edc4e0898222a4e576ffc6baa422d505909a6868",
|
||||
"harnessBackendSourceSha256": "31a1da9cb93fcf9eef65162217ef0e35199557b06d8d232b1265a2af3f3072d7",
|
||||
"harnessSessionSourceSha256": "a9d748b8530544342bbc9f59968816664872782a0865db76ca15f28a1705697b",
|
||||
"transportContractSourceSha256": "d0e7fd07ae41623ec19160b1e1c6895b1ebc3de53bb6aafc79efae6c1e32bcdb",
|
||||
"nativeBackendSourceSha256": "b408aed94ed9125c62ca8cae030f79add2dc511f3c109063710eb978e7e4c65e",
|
||||
"nativeBackendFactorySourceSha256": "4c8435dc9930b9823268625387e29e1a92d95696e9ebeb4a7127ec467909b283",
|
||||
"nativeInstructionsSourceSha256": "e7c46e81cc9c93f9a4f805b5091ef08c59ea70c62f208cfda70d6edeed363898",
|
||||
"runtimeRequestBridgeSourceSha256": "0d371a09048a592495ae5a0531ad9e9d619e26018318897fabb5b45a10c05837",
|
||||
"runtimeRequestFacadeSourceSha256": "773b860fb7a1444bc769569fbc8a3b67bd08d7ade3e7a843358e4c707f182b72",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "be0271e809639a4e6e0ecb21f3f04e6d7f17b2374682930a8b85798c1eb70e71",
|
||||
"nativeExecutionSourceSha256": "c55bdafb9a5ed97c09117c168ecd0e496ea8ae8b6e84200d90c375a143892fdc",
|
||||
"installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1",
|
||||
"credentialBindingSourceSha256": "d54e5f125225921e8e69d83343c7457448ed79fb1543a50e5d8554f37d2207da",
|
||||
"sidecarErrorSourceSha256": "bfc8da59815a8c31cb79d2560b04492ee93736188ac629009e69a4e0a7a1829c",
|
||||
"profileSourceSha256": "1572b19f84410959ecccbfcb5ffc168ddd85d1a4f8238fabeaaf662f6289fe25",
|
||||
"metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f",
|
||||
"runtimeHostSourceSha256": "7a43934ef7ff7ecd9c6f2da729c439efd636e3453022cd44ad035d0b596c4eeb",
|
||||
"runtimeAdapterSourceSha256": "5a056b64e0c187bd74aa7d04265a9b1db20e2f0d9d93880e0d628b4d25a55fc7",
|
||||
"runtimeSandboxSourceSha256": "e56b7afda01cd4d3b16559c3a76939a967ab2993aa73848588bc6ab3aa223c4b",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "9bbe8329064ee88418528394f8e1cdc716a0b2b35092dc70729b8850e46b7a68"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:7fa710c697582b1b4d85cdf833178b1940656cb6efb44ac4d356b2f27e6b3d4b",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 20,
|
||||
"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": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
|
||||
"semanticSchemaBundleSourceSha256": "36ad8a83a17111167d3cc4ef4123b36fa648a1825acd59bd90b3f8b566a4d335",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
Loaded 100 of 222 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user