@kontourai/flow-agents 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +1 -1
  4. package/build/src/cli/assignment-provider.js +4 -18
  5. package/build/src/cli/continuation-adapter.d.ts +10 -8
  6. package/build/src/cli/continuation-adapter.js +22 -4
  7. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  8. package/build/src/cli/workflow-sidecar.js +15 -20
  9. package/build/src/cli/workflow.js +58 -11
  10. package/build/src/continuation-driver.d.ts +27 -3
  11. package/build/src/continuation-driver.js +97 -14
  12. package/build/src/index.d.ts +2 -2
  13. package/build/src/index.js +1 -1
  14. package/build/src/tools/build-universal-bundles.js +52 -25
  15. package/build/src/tools/validate-source-tree.js +5 -2
  16. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  17. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  18. package/context/scripts/hooks/workflow-steering.js +24 -15
  19. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  20. package/context/scripts/telemetry/lib/transport.sh +139 -37
  21. package/context/scripts/telemetry/lib/usage.sh +282 -32
  22. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  23. package/context/scripts/telemetry/telemetry.sh +76 -1
  24. package/docs/coordination-guide.md +21 -5
  25. package/docs/fixture-ownership.md +2 -1
  26. package/docs/public-workflow-cli.md +44 -0
  27. package/docs/spec/runtime-hook-surface.md +8 -4
  28. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  29. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  30. package/evals/ci/run-baseline.sh +2 -0
  31. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  32. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  33. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  34. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  35. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  36. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  38. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  42. package/evals/integration/test_actor_identity.sh +63 -0
  43. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  44. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  45. package/evals/integration/test_bundle_install.sh +106 -17
  46. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  47. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  48. package/evals/integration/test_ci_actor_identity.sh +2 -2
  49. package/evals/integration/test_command_log_concurrency.sh +11 -0
  50. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  51. package/evals/integration/test_command_log_integrity.sh +11 -0
  52. package/evals/integration/test_current_json_per_actor.sh +416 -21
  53. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  54. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  55. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  56. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  57. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  58. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  59. package/evals/integration/test_gate_lockdown.sh +10 -0
  60. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  61. package/evals/integration/test_goal_fit_hook.sh +257 -9
  62. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  63. package/evals/integration/test_install_merge.sh +141 -3
  64. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  65. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  66. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  67. package/evals/integration/test_public_workflow_cli.sh +6 -6
  68. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  69. package/evals/integration/test_stop_hook_release.sh +111 -11
  70. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  71. package/evals/integration/test_verify_hold.sh +64 -1
  72. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  73. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  74. package/evals/run.sh +2 -0
  75. package/evals/static/test_universal_bundles.sh +123 -0
  76. package/package.json +1 -1
  77. package/packaging/README.md +21 -0
  78. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  79. package/scripts/README.md +2 -1
  80. package/scripts/audit-codex-legacy-agents.js +43 -0
  81. package/scripts/classify-codex-legacy-agents.js +71 -0
  82. package/scripts/hooks/evidence-capture.js +18 -6
  83. package/scripts/hooks/lib/actor-identity.js +62 -29
  84. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  85. package/scripts/hooks/lib/current-pointer.js +114 -12
  86. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  87. package/scripts/hooks/stop-goal-fit.js +244 -73
  88. package/scripts/hooks/workflow-steering.js +24 -15
  89. package/scripts/install-codex-home.sh +8 -7
  90. package/scripts/telemetry/lib/pricing.sh +18 -6
  91. package/scripts/telemetry/lib/transport.sh +71 -0
  92. package/scripts/telemetry/lib/usage.sh +282 -32
  93. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  94. package/scripts/telemetry/telemetry.sh +76 -1
  95. package/src/cli/assignment-provider.ts +5 -26
  96. package/src/cli/builder-flow-runtime.test.mjs +93 -6
  97. package/src/cli/continuation-adapter.ts +33 -7
  98. package/src/cli/continuation-driver.test.mjs +741 -2
  99. package/src/cli/public-api.test.mjs +1 -0
  100. package/src/cli/validate-workflow-artifacts.ts +19 -1
  101. package/src/cli/workflow-sidecar.ts +18 -23
  102. package/src/cli/workflow.ts +71 -11
  103. package/src/continuation-driver.ts +125 -19
  104. package/src/index.ts +4 -0
  105. package/src/tools/build-universal-bundles.ts +61 -25
  106. package/src/tools/validate-source-tree.ts +5 -2
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { createHash, generateKeyPairSync, sign } from "node:crypto";
7
+ import { spawnSync } from "node:child_process";
7
8
 
8
9
  import { FLOW_RUN_EVIDENCE_MANIFEST_PATH, runDir } from "@kontourai/flow";
9
10
  import {
@@ -32,6 +33,20 @@ const ACTOR = { runtime: "codex", session_id: "runtime-projection", host: "test-
32
33
  const ACTOR_KEY = "codex:runtime-projection:test-host";
33
34
  const AUTHORITY_KEY_ID = "runtime-test";
34
35
  const AUTHORITY_KEYS = generateKeyPairSync("ed25519");
36
+ const AMBIENT_IDENTITY_ENV_KEYS = [
37
+ "FLOW_AGENTS_ACTOR",
38
+ "CODEX_THREAD_ID",
39
+ "CODEX_SESSION_ID",
40
+ "CLAUDE_CODE_SESSION_ID",
41
+ "OPENCODE_SESSION_ID",
42
+ "PI_SESSION_ID",
43
+ "GITHUB_ACTIONS",
44
+ "GITLAB_CI",
45
+ "CIRCLECI",
46
+ "JENKINS_URL",
47
+ "TF_BUILD",
48
+ "BUILDKITE",
49
+ ];
35
50
 
36
51
  function makeSession(slug = "runtime-projection") {
37
52
  const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-builder-runtime-"));
@@ -432,18 +447,67 @@ test("continuation driver advances two real FlowDefinition steps unattended", as
432
447
  assert.deepEqual(events.filter((event) => event.type === "turn_started").map((event) => event.current_step), ["pull-work", "design-probe"]);
433
448
  });
434
449
 
435
- test("public workflow drive invokes an explicit adapter but preserves canonical completion authority", async () => {
450
+ test("public workflow evidence accepts only live signed turn authority after ordinary ancestry resolution mismatches", async () => {
436
451
  const session = makeSession("continuation-driver-cli");
437
452
  claimAmbientSessionAssignment(session);
453
+ fs.writeFileSync(path.join(session.projectRoot, "AGENTS.md"), "# Test Repo\n");
454
+ fs.writeFileSync(path.join(session.sessionDir, `${session.slug}--deliver.md`), "# Continuation\n\nstatus: executing\ntype: deliver\n");
455
+ fs.writeFileSync(path.join(session.sessionDir, `${session.slug}--pull-work.md`), "# Pull Work\n\nSelected continuation fixture.\n");
438
456
  await startBuilderFlowSession({ sessionDir: session.sessionDir });
457
+ const unrelatedSlug = "unrelated-pointer-session";
458
+ const unrelatedDir = path.join(session.artifactRoot, unrelatedSlug);
459
+ fs.mkdirSync(unrelatedDir, { recursive: true });
460
+ fs.writeFileSync(path.join(unrelatedDir, `${unrelatedSlug}--deliver.md`), "# Unrelated\n\nstatus: executing\ntype: deliver\n");
461
+ writeJson(path.join(unrelatedDir, "state.json"), {
462
+ schema_version: "1.0", task_slug: unrelatedSlug, status: "executing", phase: "execution",
463
+ updated_at: new Date().toISOString(), next_action: { status: "continue", summary: "Unrelated work" },
464
+ });
465
+ writeJson(path.join(session.artifactRoot, "current.json"), { active_slug: unrelatedSlug, artifact_dir: unrelatedSlug });
439
466
  const adapter = path.join(session.projectRoot, "adapter.mjs");
440
467
  const commandFile = path.join(session.projectRoot, "adapter-command.json");
468
+ const authorityFile = path.join(session.sessionDir, "continuation-driver", "active-turn.json");
441
469
  fs.writeFileSync(adapter, `
442
470
  import fs from "node:fs";
471
+ import { spawnSync } from "node:child_process";
443
472
  let input = "";
444
473
  for await (const chunk of process.stdin) input += chunk;
474
+ const request = JSON.parse(input);
445
475
  fs.writeFileSync("adapter-request.json", input);
446
- process.stdout.write(JSON.stringify({ status: "completed", summary: "adapter says done" }));
476
+ const evidenceEnv = { ...process.env };
477
+ for (const key of ${JSON.stringify(AMBIENT_IDENTITY_ENV_KEYS)}) delete evidenceEnv[key];
478
+ const ordinaryChild = ${JSON.stringify(path.resolve(import.meta.dirname, "../../scripts/hooks/lib/actor-identity.js"))};
479
+ const childIdentity = (await import("node:module")).createRequire(import.meta.url)(ordinaryChild).resolveActorIdentity(evidenceEnv);
480
+ const record = (expectation) => {
481
+ const evidence = { kind: "artifact", file: ${JSON.stringify(`.kontourai/flow-agents/${session.slug}/${session.slug}--pull-work.md`)}, summary: "adapter child records " + expectation };
482
+ return spawnSync(process.execPath, [${JSON.stringify(path.resolve(import.meta.dirname, "../../build/src/cli.js"))}, "workflow", "evidence", "--session-dir", ${JSON.stringify(session.sessionDir)}, "--expectation", expectation, "--status", "pass", "--summary", "adapter child records " + expectation, "--evidence-ref-json", JSON.stringify(evidence), "--json"], { cwd: ${JSON.stringify(session.projectRoot)}, encoding: "utf8", env: evidenceEnv });
483
+ };
484
+ const evidenceRuns = request.current_step === "pull-work"
485
+ ? [record("selected-work")]
486
+ : [record("pickup-probe-readiness"), record("probe-decisions-or-accepted-gaps")];
487
+ if (evidenceRuns.some((result) => result.status !== 0)) {
488
+ fs.writeFileSync("adapter-evidence-error.json", JSON.stringify(evidenceRuns.map((result) => ({ status: result.status, stderr: result.stderr, stdout: result.stdout }))));
489
+ throw new Error("public workflow evidence rejected signed adapter authority: " + evidenceRuns.map((result) => result.stderr).join("\\n"));
490
+ }
491
+ const lifecycleRuns = [
492
+ ["pause", "--reason", "capability must not pause"],
493
+ ["resume"],
494
+ ["release", "--reason", "capability must not release"],
495
+ ["cancel"],
496
+ ["archive"],
497
+ ].map((args) => spawnSync(process.execPath, [${JSON.stringify(path.resolve(import.meta.dirname, "../../build/src/cli.js"))}, "workflow", ...args, "--session-dir", ${JSON.stringify(session.sessionDir)}], { cwd: ${JSON.stringify(session.projectRoot)}, encoding: "utf8", env: evidenceEnv }));
498
+ const currentPointer = (await import("node:module")).createRequire(import.meta.url)(${JSON.stringify(path.resolve(import.meta.dirname, "../../scripts/hooks/lib/current-pointer.js"))});
499
+ currentPointer.writePerActorCurrent(${JSON.stringify(session.artifactRoot)}, childIdentity.actor, { active_slug: ${JSON.stringify(unrelatedSlug)}, artifact_dir: ${JSON.stringify(unrelatedSlug)} });
500
+ fs.writeFileSync(${JSON.stringify(path.join(session.artifactRoot, "current.json"))}, JSON.stringify({ active_slug: ${JSON.stringify(unrelatedSlug)}, artifact_dir: ${JSON.stringify(unrelatedSlug)} }));
501
+ const unrelatedAssignment = ${JSON.stringify(path.join(session.artifactRoot, "assignment", `${unrelatedSlug}.json`))};
502
+ fs.mkdirSync((await import("node:path")).dirname(unrelatedAssignment), { recursive: true });
503
+ fs.writeFileSync(unrelatedAssignment, JSON.stringify({ schema_version: "1.0", role: "AssignmentClaimRecord", subject_id: ${JSON.stringify(unrelatedSlug)}, actor: childIdentity.actorStruct, actor_key: childIdentity.actor, artifact_dir: ${JSON.stringify(unrelatedSlug)}, status: "claimed" }));
504
+ const canonicalState = JSON.parse(fs.readFileSync(${JSON.stringify(path.join(runDir(session.slug, session.projectRoot), "state.json"))}, "utf8"));
505
+ const authorityValidation = (await import("node:module")).createRequire(import.meta.url)(${JSON.stringify(path.resolve(import.meta.dirname, "../../scripts/hooks/lib/continuation-turn-authority.js"))}).validateActiveTurnAuthority({ sessionDir: ${JSON.stringify(session.sessionDir)}, runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID, turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET, canonicalState });
506
+ const hookEnv = { ...process.env, FLOW_AGENTS_GOAL_FIT_MODE: "block" };
507
+ const hook = spawnSync(process.execPath, [${JSON.stringify(path.resolve(import.meta.dirname, "../../scripts/hooks/stop-goal-fit.js"))}], { cwd: ${JSON.stringify(session.projectRoot)}, input: JSON.stringify({ hook_event_name: "Stop", cwd: ${JSON.stringify(session.projectRoot)} }), encoding: "utf8", env: hookEnv });
508
+ fs.writeFileSync("adapter-authority.json", JSON.stringify({ turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET || null, runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID || null, childIdentity, evidenceRuns: evidenceRuns.map((result) => ({ status: result.status, stderr: result.stderr })), lifecycleRuns: lifecycleRuns.map((result) => ({ status: result.status, stderr: result.stderr })), unrelatedAssignment: JSON.parse(fs.readFileSync(unrelatedAssignment, "utf8")), unrelatedHandoffExists: fs.existsSync(${JSON.stringify(path.join(unrelatedDir, "handoff.json"))}), active: fs.existsSync(${JSON.stringify(authorityFile)}), authorityValidation, hookStatus: hook.status, hookStderr: hook.stderr }));
509
+ if (hook.status !== 0) throw new Error("Stop hook blocked canonical continuation: " + hook.stderr);
510
+ process.stdout.write(JSON.stringify({ status: "completed", summary: "adapter advanced " + request.current_step }));
447
511
  `);
448
512
  writeJson(commandFile, { argv: [process.execPath, adapter] });
449
513
 
@@ -451,7 +515,7 @@ test("public workflow drive invokes an explicit adapter but preserves canonical
451
515
  "drive",
452
516
  "--session-dir", session.sessionDir,
453
517
  "--adapter-command-file", commandFile,
454
- "--max-turns", "1",
518
+ "--max-turns", "2",
455
519
  "--turn-timeout-ms", "5000",
456
520
  "--barrier-wait-ms", "0",
457
521
  "--json",
@@ -459,14 +523,37 @@ test("public workflow drive invokes an explicit adapter but preserves canonical
459
523
 
460
524
  assert.equal(rc, 0);
461
525
  const request = readJson(path.join(session.projectRoot, "adapter-request.json"));
462
- assert.equal(request.current_step, "pull-work");
463
- assert.deepEqual(request.next_action.skills, ["pull-work"]);
526
+ assert.equal(request.current_step, "design-probe", fs.existsSync(path.join(session.projectRoot, "adapter-evidence-error.json")) ? fs.readFileSync(path.join(session.projectRoot, "adapter-evidence-error.json"), "utf8") : "adapter did not advance canonical Flow");
527
+ assert.deepEqual(request.next_action.skills, ["pickup-probe"]);
464
528
  assert.equal(Object.hasOwn(request, "system_prompt"), false);
529
+ const authority = readJson(path.join(session.projectRoot, "adapter-authority.json"));
530
+ assert.match(authority.turnSecret, /^[A-Za-z0-9_-]{43}$/);
531
+ assert.equal(authority.runId, session.slug);
532
+ assert.equal(authority.childIdentity.source, "process-ancestry");
533
+ assert.ok(authority.evidenceRuns.every((result) => result.status === 0), JSON.stringify(authority.evidenceRuns));
534
+ assert.ok(authority.lifecycleRuns.every((result) => result.status !== 0), JSON.stringify(authority.lifecycleRuns));
535
+ assert.equal(authority.unrelatedAssignment.status, "claimed", "Stop does not release the unrelated per-actor/global-pointer session");
536
+ assert.equal(authority.unrelatedHandoffExists, false, "Stop does not relax or hand off the unrelated session");
537
+ assert.equal(authority.active, true);
538
+ assert.equal(authority.authorityValidation.valid, true, authority.authorityValidation.reason);
539
+ assert.equal(authority.hookStatus, 0, `the blocking Stop hook returns control during the signed adapter turn: ${authority.hookStderr}`);
540
+ assert.match(authority.hookStderr, /continuation driver active turn is authorized/);
541
+ assert.equal(fs.existsSync(authorityFile), false, "adapter turn authority is removed after the child exits");
465
542
  const driverState = readJson(path.join(session.sessionDir, "continuation-driver", "state.json"));
466
543
  assert.equal(driverState.status, "budget_exhausted");
467
544
  const canonical = await recoverBuilderFlowSession({ sessionDir: session.sessionDir });
468
545
  assert.equal(canonical.run.state.status, "active");
469
- assert.equal(canonical.run.state.current_step, "pull-work");
546
+ assert.equal(canonical.run.state.current_step, "plan");
547
+ const events = fs.readFileSync(path.join(session.sessionDir, "continuation-driver", "events.jsonl"), "utf8").trim().split("\n").map(JSON.parse);
548
+ assert.deepEqual(events.filter((event) => event.type === "turn_started").map((event) => event.current_step), ["pull-work", "design-probe"]);
549
+ assert.equal(events.some((event) => event.type === "gate_not_advanced"), false);
550
+ const replayEnv = { ...process.env, FLOW_AGENTS_CONTINUATION_TURN_SECRET: authority.turnSecret, FLOW_AGENTS_CONTINUATION_RUN_ID: authority.runId };
551
+ for (const key of AMBIENT_IDENTITY_ENV_KEYS) delete replayEnv[key];
552
+ const replay = spawnSync(process.execPath, [path.resolve(import.meta.dirname, "../../build/src/cli.js"), "workflow", "evidence", "--session-dir", session.sessionDir, "--expectation", "implementation-plan", "--status", "pass", "--summary", "replayed turn capability", "--evidence-ref-json", JSON.stringify({ kind: "artifact", file: "adapter.mjs", summary: "replay fixture" })], { cwd: session.projectRoot, encoding: "utf8", env: replayEnv });
553
+ assert.notEqual(replay.status, 0, "copied capability values fail after active-turn cleanup");
554
+ replayEnv.FLOW_AGENTS_CONTINUATION_TURN_SECRET = "A".repeat(43);
555
+ const forged = spawnSync(process.execPath, [path.resolve(import.meta.dirname, "../../build/src/cli.js"), "workflow", "evidence", "--session-dir", session.sessionDir, "--expectation", "implementation-plan", "--status", "pass", "--summary", "forged turn capability", "--evidence-ref-json", JSON.stringify({ kind: "artifact", file: "adapter.mjs", summary: "forgery fixture" })], { cwd: session.projectRoot, encoding: "utf8", env: replayEnv });
556
+ assert.notEqual(forged.status, 0, "fake nonce and digest without a signed active turn fail ownership");
470
557
  });
471
558
 
472
559
  test("public workflow drive rejects a non-owner before adapter execution", async () => {
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { createHash } from "node:crypto";
5
+ import { ContinuationAdapterTimeoutError } from "../continuation-driver.js";
5
6
  import type { ContinuationBarrier, ContinuationTurnRequest, ContinuationTurnResult } from "../continuation-driver.js";
6
7
 
7
8
  export type ContinuationAdapterCommand = {
@@ -24,7 +25,7 @@ export function loadContinuationAdapterCommand(commandFileInput: string): Contin
24
25
  export async function executeContinuationAdapter(
25
26
  commandFileInput: string,
26
27
  request: ContinuationTurnRequest,
27
- options: { cwd: string; timeoutMs: number },
28
+ options: ContinuationAdapterOptions,
28
29
  ): Promise<ContinuationTurnResult> {
29
30
  const command = loadContinuationAdapterCommand(commandFileInput);
30
31
  return executeLoadedContinuationAdapter(command, request, options);
@@ -33,15 +34,19 @@ export async function executeContinuationAdapter(
33
34
  export async function executeLoadedContinuationAdapter(
34
35
  command: ContinuationAdapterCommand,
35
36
  request: ContinuationTurnRequest,
36
- options: { cwd: string; timeoutMs: number },
37
+ options: ContinuationAdapterOptions,
37
38
  ): Promise<ContinuationTurnResult> {
39
+ assertLoadedContinuationAdapterIntegrity(command);
40
+ assertPositiveInteger(options.timeoutMs, "continuation adapter timeoutMs", 1, 86_400_000);
41
+ return await spawnAdapter(command, request, options);
42
+ }
43
+
44
+ export function assertLoadedContinuationAdapterIntegrity(command: ContinuationAdapterCommand): void {
38
45
  for (const entry of command.integrity) {
39
46
  if (sha256File(entry.file, "continuation adapter integrity file") !== entry.sha256) {
40
47
  throw new Error(`continuation adapter integrity changed after mission binding: ${entry.file}`);
41
48
  }
42
49
  }
43
- assertPositiveInteger(options.timeoutMs, "continuation adapter timeoutMs", 1, 86_400_000);
44
- return await spawnAdapter(command, request, options);
45
50
  }
46
51
 
47
52
  export async function waitForContinuationBarrier(
@@ -73,12 +78,12 @@ export async function waitForContinuationBarrier(
73
78
  function spawnAdapter(
74
79
  command: ContinuationAdapterCommand,
75
80
  request: ContinuationTurnRequest,
76
- options: { cwd: string; timeoutMs: number },
81
+ options: ContinuationAdapterOptions,
77
82
  ): Promise<ContinuationTurnResult> {
78
83
  return new Promise((resolve, reject) => {
79
84
  const child = spawn(command.argv[0]!, command.argv.slice(1), {
80
85
  cwd: options.cwd,
81
- env: process.env,
86
+ env: adapterEnvironment(options),
82
87
  detached: process.platform !== "win32",
83
88
  shell: false,
84
89
  stdio: ["pipe", "pipe", "pipe"],
@@ -93,7 +98,7 @@ function spawnAdapter(
93
98
  const maxBytes = 4 * 1024 * 1024;
94
99
  const timeout = setTimeout(() => {
95
100
  if (settled) return;
96
- terminationError = new Error(`continuation adapter timed out after ${options.timeoutMs}ms`);
101
+ terminationError = new ContinuationAdapterTimeoutError(options.timeoutMs);
97
102
  terminateProcessGroup(child.pid, "SIGTERM");
98
103
  forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
99
104
  }, options.timeoutMs);
@@ -160,6 +165,27 @@ function spawnAdapter(
160
165
  });
161
166
  }
162
167
 
168
+ type ContinuationAdapterOptions = {
169
+ cwd: string;
170
+ timeoutMs: number;
171
+ continuationTurnSecret?: string;
172
+ continuationRunId?: string;
173
+ };
174
+
175
+ function adapterEnvironment(options: ContinuationAdapterOptions): NodeJS.ProcessEnv {
176
+ const env = { ...process.env };
177
+ delete env.FLOW_AGENTS_CONTINUATION_TURN_SECRET;
178
+ delete env.FLOW_AGENTS_CONTINUATION_RUN_ID;
179
+ delete env.FLOW_AGENTS_CONTINUATION_TURN_NONCE;
180
+ delete env.FLOW_AGENTS_CONTINUATION_TURN_PUBLIC_KEY_DIGEST;
181
+ delete env.FLOW_AGENTS_CONTINUATION_ACTOR_B64;
182
+ const { continuationTurnSecret: turnSecret, continuationRunId: runId } = options;
183
+ if (!turnSecret || !runId) return env;
184
+ env.FLOW_AGENTS_CONTINUATION_TURN_SECRET = turnSecret;
185
+ env.FLOW_AGENTS_CONTINUATION_RUN_ID = runId;
186
+ return env;
187
+ }
188
+
163
189
  function validateAdapterCommand(value: unknown): Pick<ContinuationAdapterCommand, "argv"> {
164
190
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("continuation adapter command file must contain an object");
165
191
  const argv = (value as { argv?: unknown }).argv;