@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.
- package/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +10 -8
- package/build/src/cli/continuation-adapter.js +22 -4
- package/build/src/cli/validate-workflow-artifacts.js +21 -1
- package/build/src/cli/workflow-sidecar.js +15 -20
- package/build/src/cli/workflow.js +58 -11
- package/build/src/continuation-driver.d.ts +27 -3
- package/build/src/continuation-driver.js +97 -14
- package/build/src/index.d.ts +2 -2
- package/build/src/index.js +1 -1
- package/build/src/tools/build-universal-bundles.js +52 -25
- package/build/src/tools/validate-source-tree.js +5 -2
- package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/context/scripts/hooks/stop-goal-fit.js +244 -73
- package/context/scripts/hooks/workflow-steering.js +24 -15
- package/context/scripts/telemetry/lib/pricing.sh +18 -6
- package/context/scripts/telemetry/lib/transport.sh +139 -37
- package/context/scripts/telemetry/lib/usage.sh +282 -32
- package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/context/scripts/telemetry/telemetry.sh +76 -1
- package/docs/coordination-guide.md +21 -5
- package/docs/fixture-ownership.md +2 -1
- package/docs/public-workflow-cli.md +44 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
- package/evals/acceptance/prove-capture-teeth.sh +13 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
- package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
- package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
- package/evals/integration/test_actor_identity.sh +63 -0
- package/evals/integration/test_assignment_provider_local_file.sh +34 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -1
- package/evals/integration/test_bundle_install.sh +106 -17
- package/evals/integration/test_bundle_lifecycle.sh +17 -3
- package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
- package/evals/integration/test_ci_actor_identity.sh +2 -2
- package/evals/integration/test_command_log_concurrency.sh +11 -0
- package/evals/integration/test_command_log_fork_classification.sh +11 -0
- package/evals/integration/test_command_log_integrity.sh +11 -0
- package/evals/integration/test_current_json_per_actor.sh +416 -21
- package/evals/integration/test_enforcer_expects_driven.sh +11 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
- package/evals/integration/test_evidence_capture_hook.sh +25 -6
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
- package/evals/integration/test_gate_bypass_chain.sh +11 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
- package/evals/integration/test_goal_fit_hook.sh +257 -9
- package/evals/integration/test_goal_fit_rederive.sh +11 -0
- package/evals/integration/test_install_merge.sh +141 -3
- package/evals/integration/test_liveness_conflict_injection.sh +39 -3
- package/evals/integration/test_liveness_heartbeat.sh +46 -9
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
- package/evals/integration/test_public_workflow_cli.sh +6 -6
- package/evals/integration/test_session_resume_roundtrip.sh +30 -0
- package/evals/integration/test_stop_hook_release.sh +111 -11
- package/evals/integration/test_telemetry_tool_usage.sh +394 -0
- package/evals/integration/test_verify_hold.sh +64 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
- package/evals/integration/test_workflow_steering_hook.sh +20 -4
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +123 -0
- package/package.json +1 -1
- package/packaging/README.md +21 -0
- package/packaging/codex-legacy-agents-fingerprints.json +33 -0
- package/scripts/README.md +2 -1
- package/scripts/audit-codex-legacy-agents.js +43 -0
- package/scripts/classify-codex-legacy-agents.js +71 -0
- package/scripts/hooks/evidence-capture.js +18 -6
- package/scripts/hooks/lib/actor-identity.js +62 -29
- package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/scripts/hooks/lib/current-pointer.js +114 -12
- package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
- package/scripts/hooks/stop-goal-fit.js +244 -73
- package/scripts/hooks/workflow-steering.js +24 -15
- package/scripts/install-codex-home.sh +8 -7
- package/scripts/telemetry/lib/pricing.sh +18 -6
- package/scripts/telemetry/lib/transport.sh +71 -0
- package/scripts/telemetry/lib/usage.sh +282 -32
- package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/scripts/telemetry/telemetry.sh +76 -1
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +93 -6
- package/src/cli/continuation-adapter.ts +33 -7
- package/src/cli/continuation-driver.test.mjs +741 -2
- package/src/cli/public-api.test.mjs +1 -0
- package/src/cli/validate-workflow-artifacts.ts +19 -1
- package/src/cli/workflow-sidecar.ts +18 -23
- package/src/cli/workflow.ts +71 -11
- package/src/continuation-driver.ts +125 -19
- package/src/index.ts +4 -0
- package/src/tools/build-universal-bundles.ts +61 -25
- 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
|
|
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
|
-
|
|
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", "
|
|
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, "
|
|
463
|
-
assert.deepEqual(request.next_action.skills, ["
|
|
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, "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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;
|