@tangle-network/agent-runtime 6.0.8 → 8.0.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 (119) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +23 -12
  3. package/dist/{activation-Cwy3_Qn_.js → activation-Ce4zUcu8.js} +4 -4
  4. package/dist/{activation-Cwy3_Qn_.js.map → activation-Ce4zUcu8.js.map} +1 -1
  5. package/dist/{activation-CWx7I1PU.d.ts → activation-xxvLF1-1.d.ts} +2 -2
  6. package/dist/agent.d.ts +1 -1
  7. package/dist/agent.js +3 -3
  8. package/dist/{bundle-Cy5igJdv.js → bundle-CcpL2lfr.js} +2 -2
  9. package/dist/{bundle-Cy5igJdv.js.map → bundle-CcpL2lfr.js.map} +1 -1
  10. package/dist/candidate-execution/index.js +4 -4
  11. package/dist/continuation-CIyKnS1Q.js +451 -0
  12. package/dist/continuation-CIyKnS1Q.js.map +1 -0
  13. package/dist/{coordination-manager-D9z7To7p.js → coordination-manager-sa_PkISw.js} +247 -149
  14. package/dist/coordination-manager-sa_PkISw.js.map +1 -0
  15. package/dist/{coordination-map-routing-CKRNZSaV.js → coordination-map-routing-XZXTlCmn.js} +2 -4
  16. package/dist/coordination-map-routing-XZXTlCmn.js.map +1 -0
  17. package/dist/{delegate-ws-KBA2H.js → delegate-DhTkJIta.js} +3 -3
  18. package/dist/{delegate-ws-KBA2H.js.map → delegate-DhTkJIta.js.map} +1 -1
  19. package/dist/durable.d.ts +71 -4
  20. package/dist/durable.js +162 -13
  21. package/dist/durable.js.map +1 -1
  22. package/dist/{errors-AzSuGI23.js → errors-DG8yufqX.js} +8 -1
  23. package/dist/errors-DG8yufqX.js.map +1 -0
  24. package/dist/{failure-cause-BzjUUsRz.js → failure-cause-CVB-YE88.js} +17 -16
  25. package/dist/failure-cause-CVB-YE88.js.map +1 -0
  26. package/dist/{graph-registry-7X2nrreR.js → graph-registry-czAwYwNj.js} +133 -9
  27. package/dist/graph-registry-czAwYwNj.js.map +1 -0
  28. package/dist/{graph-registry-gYbq4L5Z.js → graph-registry-uTIyyy7A.js} +1 -1
  29. package/dist/graph-tSIT_eWK.js +1793 -0
  30. package/dist/graph-tSIT_eWK.js.map +1 -0
  31. package/dist/{improve-BxfqTsZy.js → improve-H08hiIw9.js} +19 -48
  32. package/dist/improve-H08hiIw9.js.map +1 -0
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +16 -16
  35. package/dist/intelligence.d.ts +2 -2
  36. package/dist/intelligence.js +8 -8
  37. package/dist/intelligence.js.map +1 -1
  38. package/dist/kernel.d.ts +2 -2
  39. package/dist/kernel.js +16 -15
  40. package/dist/{loop-runner-CUMypgRw.d.ts → loop-runner-BGwhcn1p.d.ts} +2 -2
  41. package/dist/{loop-runner-BmGZ4y1W.js → loop-runner-ChqK8kSz.js} +3 -3
  42. package/dist/{loop-runner-BmGZ4y1W.js.map → loop-runner-ChqK8kSz.js.map} +1 -1
  43. package/dist/loop-runner-bin.d.ts +1 -1
  44. package/dist/loop-runner-bin.js +1 -1
  45. package/dist/mcp/bin.js +7 -5
  46. package/dist/mcp/bin.js.map +1 -1
  47. package/dist/mcp/index.d.ts +1 -1
  48. package/dist/mcp/index.js +9 -9
  49. package/dist/mcp/memory-bin.js +2 -2
  50. package/dist/{model-policy-JOcg3NRs.js → model-policy-yxOrDTTj.js} +9 -9
  51. package/dist/model-policy-yxOrDTTj.js.map +1 -0
  52. package/dist/{prepare-Dtfk_Wi_.js → prepare-BnF8nGIg.js} +4 -4
  53. package/dist/{prepare-Dtfk_Wi_.js.map → prepare-BnF8nGIg.js.map} +1 -1
  54. package/dist/{profile-CNSQjOes.js → profile-CD25rTJF.js} +4 -4
  55. package/dist/{profile-CNSQjOes.js.map → profile-CD25rTJF.js.map} +1 -1
  56. package/dist/profiles.d.ts +1 -1
  57. package/dist/profiles.js +2 -2
  58. package/dist/{provision-supervisor-CeQTPHRr.js → provision-supervisor-Dx4TXTMM.js} +8 -8
  59. package/dist/{provision-supervisor-CeQTPHRr.js.map → provision-supervisor-Dx4TXTMM.js.map} +1 -1
  60. package/dist/{run-cancellation-D_4vJ522.js → run-cancellation-DYszFJtQ.js} +4 -4
  61. package/dist/{run-cancellation-D_4vJ522.js.map → run-cancellation-DYszFJtQ.js.map} +1 -1
  62. package/dist/{run-state-CTZVngI_.js → run-state-DHBoJjaF.js} +6 -6
  63. package/dist/{run-state-CTZVngI_.js.map → run-state-DHBoJjaF.js.map} +1 -1
  64. package/dist/{runtime-COm9uBBx.js → runtime-CaM5Gbbv.js} +76 -987
  65. package/dist/runtime-CaM5Gbbv.js.map +1 -0
  66. package/dist/{sandbox-profile-compat-oFGWESLn.js → sandbox-profile-compat-l-SLQnGb.js} +2 -2
  67. package/dist/{sandbox-profile-compat-oFGWESLn.js.map → sandbox-profile-compat-l-SLQnGb.js.map} +1 -1
  68. package/dist/{search-executor-DTEIl58w.js → search-executor-B9L8W_1O.js} +17 -46
  69. package/dist/search-executor-B9L8W_1O.js.map +1 -0
  70. package/dist/{server-zO9Nz9Nk.js → server-BlDV8sj_.js} +32 -8
  71. package/dist/{server-zO9Nz9Nk.js.map → server-BlDV8sj_.js.map} +1 -1
  72. package/dist/{settle-record-U7fkQEZa.js → settle-record-WWQasnDq.js} +12 -4
  73. package/dist/settle-record-WWQasnDq.js.map +1 -0
  74. package/dist/{snapshot-Cgj2Wzi6.js → snapshot-BKI_w3JD.js} +19 -5
  75. package/dist/snapshot-BKI_w3JD.js.map +1 -0
  76. package/dist/{structural-rollout-BwTwTUXF.js → structural-rollout-C1xxoWcN.js} +39 -57
  77. package/dist/structural-rollout-C1xxoWcN.js.map +1 -0
  78. package/dist/{supervise-tmqDU6YT.js → supervise-DWiSgPLB.js} +103 -111
  79. package/dist/supervise-DWiSgPLB.js.map +1 -0
  80. package/dist/{supervision-discovery-CGbXayGv.js → supervision-discovery-C9kfBLKy.js} +4 -4
  81. package/dist/{supervision-discovery-CGbXayGv.js.map → supervision-discovery-C9kfBLKy.js.map} +1 -1
  82. package/dist/{supervisor-1A27fE6k.js → supervisor-Dpeu7rnZ.js} +209 -999
  83. package/dist/supervisor-Dpeu7rnZ.js.map +1 -0
  84. package/dist/{scope-gIYQ5ZiL.js → supervisor-accounting-Bw4MJk-b.js} +755 -458
  85. package/dist/supervisor-accounting-Bw4MJk-b.js.map +1 -0
  86. package/dist/testing.d.ts +1 -1
  87. package/dist/testing.js +13 -13
  88. package/dist/{tool-server-B9q2dtnW.js → tool-server-DYwRbi0I.js} +3 -3
  89. package/dist/{tool-server-B9q2dtnW.js.map → tool-server-DYwRbi0I.js.map} +1 -1
  90. package/dist/{top-web-CS4PD4ck.js → top-web-B3HOAtrZ.js} +9 -7
  91. package/dist/{top-web-CS4PD4ck.js.map → top-web-B3HOAtrZ.js.map} +1 -1
  92. package/dist/tui/bin.js +6 -6
  93. package/dist/tui/bin.js.map +1 -1
  94. package/dist/tui/index.d.ts +1 -1
  95. package/dist/tui/index.js +2 -2
  96. package/dist/{types-w3Oo1uRJ.d.ts → types-C_qbnw9N.d.ts} +1263 -1497
  97. package/dist/{util-C_cdPNC4.js → util-fcckrkKp.js} +39 -113
  98. package/dist/util-fcckrkKp.js.map +1 -0
  99. package/dist/{workspace-streams-B7yNMahl.js → workspace-streams-DA9R3j-p.js} +2 -2
  100. package/dist/{workspace-streams-B7yNMahl.js.map → workspace-streams-DA9R3j-p.js.map} +1 -1
  101. package/package.json +1 -1
  102. package/dist/coordination-manager-D9z7To7p.js.map +0 -1
  103. package/dist/coordination-map-routing-CKRNZSaV.js.map +0 -1
  104. package/dist/errors-AzSuGI23.js.map +0 -1
  105. package/dist/failure-cause-BzjUUsRz.js.map +0 -1
  106. package/dist/graph-2_nX4ib4.js +0 -579
  107. package/dist/graph-2_nX4ib4.js.map +0 -1
  108. package/dist/graph-registry-7X2nrreR.js.map +0 -1
  109. package/dist/improve-BxfqTsZy.js.map +0 -1
  110. package/dist/model-policy-JOcg3NRs.js.map +0 -1
  111. package/dist/runtime-COm9uBBx.js.map +0 -1
  112. package/dist/scope-gIYQ5ZiL.js.map +0 -1
  113. package/dist/search-executor-DTEIl58w.js.map +0 -1
  114. package/dist/settle-record-U7fkQEZa.js.map +0 -1
  115. package/dist/snapshot-Cgj2Wzi6.js.map +0 -1
  116. package/dist/structural-rollout-BwTwTUXF.js.map +0 -1
  117. package/dist/supervise-tmqDU6YT.js.map +0 -1
  118. package/dist/supervisor-1A27fE6k.js.map +0 -1
  119. package/dist/util-C_cdPNC4.js.map +0 -1
@@ -1,25 +1,26 @@
1
- import { A as isTraceAnalysisStore, Br as executableAgentProfileSnapshot, G as settledToIteration, Mt as coordinationProfileToolPrefix, Vr as executableAgentSpecSnapshot, X as rollingDispatch, ei as runAbortable, rn as insideCursorNamespace } from "./scope-gIYQ5ZiL.js";
2
- import { i as ConfigError, m as ValidationError, n as AnalystError, s as PlannerError } from "./errors-AzSuGI23.js";
3
- import { D as runAgentic, E as refine, M as renderReport, N as profileChatClient, O as sample, j as observe, k as ObservationError, v as loadAuthoredStrategy, x as strategyAuthorContract, y as requestStrategySource } from "./structural-rollout-BwTwTUXF.js";
4
- import { I as armDeadlineTimer, T as zeroSpend, a as cloneSpend, c as errorMessage, d as isAbortError, f as isAsyncIterable, h as randomSuffix, i as chargedTokens, l as fnv1a32Hex, n as addSpend, p as mapWithConcurrency, u as hasCompleteCacheBreakdown, v as sleep, y as stringifySafe } from "./util-C_cdPNC4.js";
5
- import { M as contentAddress, _t as notifySandboxEventObserver, pt as createSandboxTurnEventCollector, r as runtimeFailureCauseSchema } from "./failure-cause-BzjUUsRz.js";
6
- import { n as detachedSnapshot, t as detachedFrozen } from "./snapshot-Cgj2Wzi6.js";
7
- import { C as createExecutor, D as runAgentRounds, O as assertBoxlessPromptOptions, P as defaultSelectWinner, V as routerBrain, _ as withDriverExecutor, b as createExecutorRegistry, bt as verdictFromJudgeScore, gt as CheckUnavailableError, j as createSandboxLineage, k as readPromptOptions, n as createSupervisor, q as createWorktreeCliExecutor, v as InMemoryResultBlobStore, vt as gateOnDeliverable, y as InMemorySpawnJournal, yt as mapExecutorResult } from "./supervisor-1A27fE6k.js";
8
- import { a as concreteProfileModel, c as profileModelExecutionSettings, i as concreteModelId, l as profileProviderModel, o as enforceTokenLimits, p as canonicalObservedModelParts, t as assertExecutableAgentProfile, v as harnessRunsAgent } from "./model-policy-JOcg3NRs.js";
1
+ import { Gr as executableAgentProfileSnapshot, It as coordinationProfileToolPrefix, Kr as executableAgentSpecSnapshot, P as isTraceAnalysisStore, Y as settledToIteration, et as rollingDispatch, si as runAbortable, sn as insideCursorNamespace } from "./supervisor-accounting-Bw4MJk-b.js";
2
+ import { i as ConfigError, m as ValidationError, n as AnalystError, s as PlannerError } from "./errors-DG8yufqX.js";
3
+ import { D as runAgentic, E as refine, M as renderReport, N as profileChatClient, O as sample, j as observe, k as ObservationError, v as loadAuthoredStrategy, x as strategyAuthorContract, y as requestStrategySource } from "./structural-rollout-C1xxoWcN.js";
4
+ import { I as armDeadlineTimer, T as zeroSpend, a as cloneSpend, c as errorMessage, d as isAbortError, f as isAsyncIterable, h as randomSuffix, i as chargedTokens, l as fnv1a32Hex, n as addSpend, p as mapWithConcurrency, u as hasCompleteCacheBreakdown, v as sleep, y as stringifySafe } from "./util-fcckrkKp.js";
5
+ import { M as contentAddress, mt as createSandboxTurnEventCollector, r as runtimeFailureCauseSchema, vt as notifySandboxEventObserver } from "./failure-cause-CVB-YE88.js";
6
+ import { n as detachedSnapshot, t as detachedFrozen } from "./snapshot-BKI_w3JD.js";
7
+ import { C as createExecutor, D as runAgentRounds, O as assertBoxlessPromptOptions, P as defaultSelectWinner, V as routerBrain, _ as withDriverExecutor, b as createExecutorRegistry, bt as verdictFromJudgeScore, gt as CheckUnavailableError, j as createSandboxLineage, k as readPromptOptions, n as createSupervisor, q as createWorktreeCliExecutor, v as InMemoryResultBlobStore, vt as gateOnDeliverable, y as InMemorySpawnJournal, yt as mapExecutorResult } from "./supervisor-Dpeu7rnZ.js";
8
+ import { a as concreteProfileModel, c as profileModelExecutionSettings, i as concreteModelId, l as profileProviderModel, o as enforceTokenLimits, p as canonicalObservedModelParts, t as assertExecutableAgentProfile, v as harnessRunsAgent } from "./model-policy-yxOrDTTj.js";
9
9
  import { a as readCommittedJsonLines, c as appendCommittedJsonLine, i as readCommittedJsonFile, o as upsertCommittedJsonLine, s as withJsonlFileLock, t as isNoEntError } from "./jsonl-file-DsxVI0E_.js";
10
- import { A as markPutStreamVerified, L as immutableCandidateValue, N as canonicalCandidateDigest$1, S as captureMaterializedWorkspace, T as scanMaterializedWorkspaceManifest, g as persistCandidateOutputArtifact, y as WorkspaceScanLimitError, z as sha256Bytes$1 } from "./workspace-streams-B7yNMahl.js";
10
+ import { A as markPutStreamVerified, L as immutableCandidateValue, N as canonicalCandidateDigest$1, S as captureMaterializedWorkspace, z as sha256Bytes$1 } from "./workspace-streams-DA9R3j-p.js";
11
+ import "./continuation-CIyKnS1Q.js";
11
12
  import { a as writeAtomicDurableFile } from "./durable-file-DstVeHcj.js";
12
13
  import { i as notifyRuntimeHookEvent } from "./runtime-hooks-CsW2pfFk.js";
13
- import { a as reconcileInterruptedSearchCalls, c as searchExecutor, r as dedicatedLane, x as runtimeShipDecision } from "./search-executor-DTEIl58w.js";
14
- import { y as foldCoordinationRecords } from "./run-cancellation-D_4vJ522.js";
15
- import { a as downMessageRefusalReasons, p as createInMemoryRunContext } from "./coordination-manager-D9z7To7p.js";
16
- import { M as kernelPromptRegistry, S as validateFlowGraph, T as validateGraphTopology, h as runBrainLoop, p as coordinationControlToolNames, t as supervise } from "./supervise-tmqDU6YT.js";
17
- import { _ as compileGraphParameters, a as graphRegistry, b as graphRouteWhenSchema, d as recordGraphRun, g as bindGraphParameters, h as assertGraphTriggerBindings, i as graphFromStored, l as lookupGraph, m as taskShapeDigest, o as graphRunsFor, r as graphDigest, t as digestOfGraph, v as graphTriggerBindingsSchema, y as graphFileSchema } from "./graph-registry-7X2nrreR.js";
18
- import "./run-state-CTZVngI_.js";
14
+ import { a as reconcileInterruptedSearchCalls, c as searchExecutor, r as dedicatedLane, x as runtimeShipDecision } from "./search-executor-B9L8W_1O.js";
15
+ import { y as foldCoordinationRecords } from "./run-cancellation-DYszFJtQ.js";
16
+ import { a as downMessageRefusalReasons, p as createInMemoryRunContext } from "./coordination-manager-sa_PkISw.js";
17
+ import { M as kernelPromptRegistry, S as validateFlowGraph, T as validateGraphTopology, h as runBrainLoop, p as coordinationControlToolNames, t as supervise } from "./supervise-DWiSgPLB.js";
18
+ import { C as assertJsonData, _ as compileGraphParameters, a as graphRegistry, b as graphFileSchema, d as recordGraphRun, g as bindGraphParameters, h as assertGraphTriggerBindings, i as graphFromStored, l as lookupGraph, m as taskShapeDigest, o as graphRunsFor, r as graphDigest, t as digestOfGraph, v as graphTriggerBindingsSchema, x as graphRouteWhenSchema, y as graphCheckNodeSchema } from "./graph-registry-czAwYwNj.js";
19
+ import "./run-state-DHBoJjaF.js";
20
+ import { o as runIsolatedCheck } from "./graph-tSIT_eWK.js";
19
21
  import { i as redactProtectedValue, r as redactProtectedReason } from "./protected-redaction-DilFZch5.js";
20
- import "./provision-supervisor-CeQTPHRr.js";
21
- import "./delegate-ws-KBA2H.js";
22
- import "./graph-2_nX4ib4.js";
22
+ import "./provision-supervisor-Dx4TXTMM.js";
23
+ import "./delegate-DhTkJIta.js";
23
24
  import { agentProfileSchema, canonicalAgentProfileDigest, canonicalCandidateDigest, sha256Bytes, validateAgentProfileSecurity } from "@tangle-network/agent-interface";
24
25
  import { CODING_HARNESSES, DEFAULT_TRACE_ANALYST_KINDS, InMemoryTraceStore, OUTPUT_VALUE, benjaminiHochberg, buildTrajectory, computeFindingId as computeFindingId$1, confidenceInterval, createTraceAnalyst, expandProfileAxes, makeFinding as makeFinding$1, pairedBootstrap, paretoFrontier, wilcoxonSignedRank, wilson } from "@tangle-network/agent-eval";
25
26
  import { createAgentRunOutcomeTracker } from "@tangle-network/sandbox/runtime";
@@ -29,10 +30,9 @@ import { redact } from "@tangle-network/agent-eval/traces";
29
30
  import { createHash, createHmac, randomUUID } from "node:crypto";
30
31
  import { appendFileSync, chmodSync, constants, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
31
32
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
32
- import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
33
- import { chmod, link, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
34
- import { promisify } from "node:util";
35
- import { tmpdir } from "node:os";
33
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
34
+ import { link, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
35
+ import { homedir, tmpdir } from "node:os";
36
36
  import { Readable } from "node:stream";
37
37
  import { createInterface } from "node:readline";
38
38
  import { SearchRecorder, asha, beam, createRunCostLedger, estimateNode, fsCampaignStorage, openSearchLedger, renderSearchSummary, runProfileMatrix, runSearch, searchClaimReserveUsd, searchModelIdentity, surfaceDiff } from "@tangle-network/agent-eval/campaign";
@@ -735,923 +735,6 @@ function deterministicCompletion(check) {
735
735
  };
736
736
  } };
737
737
  }
738
- // src/runtime/isolated-checker-outputs.ts
739
- const MAX_FILES = 16;
740
- const MAX_BYTES = 67108864;
741
- const MAX_TIMEOUT_MS = 12e4;
742
- /** Validate authored declarations before starting a check. The returned paths are detached and frozen. */
743
- function validateIsolatedCheckOutputs(value) {
744
- if (value === null || typeof value !== "object" || Array.isArray(value)) throw new ValidationError("check outputs must be a declaration object");
745
- const item = value;
746
- if (Object.keys(item).some((key) => ![
747
- "paths",
748
- "maxFileBytes",
749
- "maxTotalBytes",
750
- "timeoutMs"
751
- ].includes(key))) throw new ValidationError("check outputs has an unknown field");
752
- if (!Array.isArray(item.paths) || item.paths.length < 1 || item.paths.length > MAX_FILES) throw new ValidationError("check outputs must name between 1 and 16 files");
753
- const paths = [];
754
- for (const path of item.paths) {
755
- if (typeof path !== "string" || path.length > 512 || !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/u.test(path) || path.split("/").some((part) => part === "." || part === ".." || part === ".git" || part === ".sidecar") || paths.some((prior) => prior === path || prior.startsWith(`${path}/`) || path.startsWith(`${prior}/`))) throw new ValidationError("check outputs paths must be distinct portable relative files");
756
- paths.push(path);
757
- }
758
- for (const key of ["maxFileBytes", "maxTotalBytes"]) if (typeof item[key] !== "number" || !Number.isSafeInteger(item[key]) || item[key] < 1 || item[key] > MAX_BYTES) throw new ValidationError("check output byte limits must be positive integers at most 64 MiB");
759
- const maxFileBytes = item.maxFileBytes;
760
- const maxTotalBytes = item.maxTotalBytes;
761
- if (maxFileBytes > maxTotalBytes) throw new ValidationError("check outputs maxFileBytes must not exceed maxTotalBytes");
762
- if (item.timeoutMs !== void 0 && (typeof item.timeoutMs !== "number" || !Number.isSafeInteger(item.timeoutMs) || item.timeoutMs < 1 || item.timeoutMs > MAX_TIMEOUT_MS)) throw new ValidationError("check outputs timeoutMs must be a positive integer at most 120000");
763
- return Object.freeze({
764
- paths: Object.freeze(paths),
765
- maxFileBytes,
766
- maxTotalBytes,
767
- ...item.timeoutMs === void 0 ? {} : { timeoutMs: item.timeoutMs }
768
- });
769
- }
770
- /** Resolve and validate trusted ports before any isolated work starts. */
771
- function prepareIsolatedCheckOutputs(options) {
772
- if (options === void 0) return void 0;
773
- const { executionId, artifacts, ...declaration } = options;
774
- const validated = validateIsolatedCheckOutputs(declaration);
775
- if (typeof executionId !== "string" || executionId.trim() === "" || executionId.length > 200 || /\p{Cc}/u.test(executionId)) throw new ValidationError("check outputs executionId must be a nonempty bounded identifier");
776
- if (typeof artifacts?.put !== "function" || typeof artifacts?.read !== "function") throw new ValidationError("check outputs requires a readable output artifact port");
777
- return Object.freeze({
778
- ...validated,
779
- executionId,
780
- artifacts
781
- });
782
- }
783
- var OutputCaptureError = class extends Error {
784
- reason;
785
- constructor(reason) {
786
- super(reason);
787
- this.reason = reason;
788
- }
789
- };
790
- /** Capture runs after command settlement, even on command failure or cancellation. There are no retries. */
791
- async function retainIsolatedCheckOutputs(options, inputDigest, read) {
792
- const controller = new AbortController();
793
- const clearTimer = armDeadlineTimer(options.timeoutMs ?? 3e4, () => controller.abort(), true);
794
- const files = [];
795
- const missing = [];
796
- let totalBytes = 0;
797
- try {
798
- for (const path of options.paths) {
799
- let phase = "capture-failed";
800
- try {
801
- const bytes = await runAbortable(() => read(path, Math.min(options.maxFileBytes, options.maxTotalBytes - totalBytes), controller.signal), controller.signal, "output capture deadline exceeded");
802
- totalBytes += bytes.byteLength;
803
- phase = "store-failed";
804
- const artifact = await runAbortable(() => persistCandidateOutputArtifact(options.artifacts, {
805
- executionId: options.executionId,
806
- purpose: "grader-evidence",
807
- bytes,
808
- signal: controller.signal
809
- }), controller.signal, "output persistence deadline exceeded");
810
- files.push({
811
- path,
812
- artifact
813
- });
814
- } catch (error) {
815
- missing.push({
816
- path,
817
- reason: controller.signal.aborted ? "timeout" : error instanceof OutputCaptureError ? error.reason : phase
818
- });
819
- }
820
- }
821
- } finally {
822
- clearTimer();
823
- }
824
- return Object.freeze({
825
- executionId: options.executionId,
826
- inputDigest,
827
- status: missing.length === 0 ? "retained" : "incomplete",
828
- files: Object.freeze(files.map((file) => Object.freeze(file))),
829
- missing: Object.freeze(missing.map((file) => Object.freeze(file)))
830
- });
831
- }
832
- /** The local namespace has exited; the trusted host must keep its private copy stable. */
833
- function localCheckOutputReader(root) {
834
- return async (path, maxBytes, signal) => {
835
- try {
836
- const rootInfo = await lstat(root);
837
- if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) throw new OutputCaptureError("unsafe-path");
838
- let current = root;
839
- const parts = path.split("/");
840
- let expected;
841
- for (let index = 0; index < parts.length; index++) {
842
- current = join(current, parts[index]);
843
- const info = await lstat(current, { bigint: true });
844
- if (info.isSymbolicLink() || (index < parts.length - 1 ? !info.isDirectory() : !info.isFile() || info.nlink !== 1n)) throw new OutputCaptureError("unsafe-path");
845
- expected = info;
846
- }
847
- signal.throwIfAborted();
848
- const file = await open(current, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
849
- try {
850
- const before = await file.stat({ bigint: true });
851
- if (!before.isFile() || before.nlink !== 1n || before.dev !== expected?.dev || before.ino !== expected?.ino) throw new OutputCaptureError("unsafe-path");
852
- if (before.size > BigInt(maxBytes)) throw new OutputCaptureError("size-limit");
853
- const chunks = [];
854
- let total = 0;
855
- const buffer = Buffer.allocUnsafe(Math.min(65536, maxBytes + 1));
856
- while (true) {
857
- signal.throwIfAborted();
858
- const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, maxBytes - total + 1), null);
859
- if (bytesRead === 0) break;
860
- total += bytesRead;
861
- if (total > maxBytes) throw new OutputCaptureError("size-limit");
862
- chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
863
- }
864
- const after = await file.stat({ bigint: true });
865
- if (before.size !== BigInt(total) || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || after.nlink !== 1n) throw new OutputCaptureError("capture-failed");
866
- return Buffer.concat(chunks, total);
867
- } finally {
868
- await file.close();
869
- }
870
- } catch (error) {
871
- if (error?.code === "ENOENT") throw new OutputCaptureError("missing");
872
- if (["ELOOP", "ENOTDIR"].includes(error?.code ?? "")) throw new OutputCaptureError("unsafe-path");
873
- throw error;
874
- }
875
- };
876
- }
877
- /** Preserve a whole box only after its native lifecycle owner confirms stopped state. */
878
- async function stopCheckBoxForRecovery(box) {
879
- const controller = new AbortController();
880
- const clearTimer = armDeadlineTimer(3e4, () => controller.abort(), true);
881
- try {
882
- await runAbortable(() => box.stop({
883
- wait: true,
884
- signal: controller.signal
885
- }), controller.signal, "check recovery stop deadline exceeded");
886
- return true;
887
- } catch {
888
- return false;
889
- } finally {
890
- clearTimer();
891
- }
892
- }
893
- /** Whether kill and process exit were confirmed; recovery custody still needs a whole-box stop. */
894
- async function stopCheckProcess(child) {
895
- const controller = new AbortController();
896
- const clearTimer = armDeadlineTimer(5e3, () => controller.abort(), true);
897
- try {
898
- await runAbortable(async () => {
899
- await child.kill("SIGKILL", { tree: true });
900
- await child.wait();
901
- }, controller.signal, "check process stop deadline exceeded");
902
- return true;
903
- } catch {
904
- return false;
905
- } finally {
906
- clearTimer();
907
- }
908
- }
909
- const isolatedCheckOutputCaptureProgram = `import base64, errno, hashlib, json, os, stat, sys
910
- fds=[]
911
- def fail(reason):
912
- print(json.dumps({'error':reason})); sys.exit(0)
913
- try:
914
- root,path,limit=sys.argv[1],sys.argv[2],int(sys.argv[3])
915
- directory=os.open(root,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW); fds.append(directory)
916
- parts=path.split('/')
917
- for part in parts[:-1]:
918
- directory=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=directory); fds.append(directory)
919
- before=os.stat(parts[-1],dir_fd=directory,follow_symlinks=False)
920
- if not stat.S_ISREG(before.st_mode) or before.st_nlink!=1: fail('unsafe-path')
921
- fd=os.open(parts[-1],os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=directory); fds.append(fd)
922
- opened=os.fstat(fd)
923
- if not stat.S_ISREG(opened.st_mode) or opened.st_nlink!=1 or (before.st_dev,before.st_ino)!=(opened.st_dev,opened.st_ino): fail('unsafe-path')
924
- if opened.st_size>limit: fail('size-limit')
925
- chunks=[]; total=0
926
- while True:
927
- chunk=os.read(fd,min(1048576,limit-total+1))
928
- if not chunk: break
929
- total+=len(chunk)
930
- if total>limit: fail('size-limit')
931
- chunks.append(chunk)
932
- after=os.fstat(fd)
933
- if (opened.st_size,opened.st_mtime_ns,opened.st_ctime_ns,opened.st_nlink)!=(after.st_size,after.st_mtime_ns,after.st_ctime_ns,after.st_nlink) or total!=opened.st_size: fail('capture-failed')
934
- data=b''.join(chunks)
935
- print(json.dumps({'content':base64.b64encode(data).decode('ascii'),'sha256':'sha256:'+hashlib.sha256(data).hexdigest()}))
936
- except OSError as error:
937
- fail('missing' if error.errno==errno.ENOENT else 'unsafe-path' if error.errno in (errno.ELOOP,errno.ENOTDIR) else 'capture-failed')
938
- finally:
939
- for fd in reversed(fds): os.close(fd)
940
- `;
941
- /** A box with declared outputs needs python3; missing capture tooling fails closed and keeps custody. */
942
- function boxCheckOutputReader(box, root, timeoutMs) {
943
- return async (path, maxBytes, signal) => {
944
- const child = await box.process.spawnExact("/usr/bin/python3", [
945
- "-I",
946
- "-S",
947
- "-c",
948
- isolatedCheckOutputCaptureProgram,
949
- root,
950
- path,
951
- String(maxBytes)
952
- ], {
953
- env: {
954
- PATH: "/usr/bin:/bin",
955
- LANG: "C"
956
- },
957
- inheritEnv: false,
958
- timeoutMs
959
- });
960
- let stopping;
961
- const stop = () => {
962
- stopping ??= stopCheckProcess(child);
963
- };
964
- signal.addEventListener("abort", stop, { once: true });
965
- if (signal.aborted) stop();
966
- const chunks = [];
967
- const limit = Math.ceil(maxBytes / 3) * 4 + 2048;
968
- let length = 0;
969
- let overflow = false;
970
- const collect = async (stream, keep) => {
971
- for await (const text of stream) {
972
- length += Buffer.byteLength(text);
973
- if (length > limit) {
974
- overflow = true;
975
- stop();
976
- } else if (keep) chunks.push(text);
977
- }
978
- };
979
- try {
980
- await Promise.all([collect(child.stdout(), true), collect(child.stderr(), false)]);
981
- if (overflow || await child.wait() !== 0) throw new OutputCaptureError("capture-failed");
982
- signal.throwIfAborted();
983
- const value = JSON.parse(chunks.join(""));
984
- if (value.error === "missing" || value.error === "unsafe-path" || value.error === "size-limit" || value.error === "capture-failed") throw new OutputCaptureError(value.error);
985
- if (typeof value.content !== "string" || typeof value.sha256 !== "string") throw new OutputCaptureError("capture-failed");
986
- const bytes = Buffer.from(value.content, "base64");
987
- if (bytes.byteLength > maxBytes) throw new OutputCaptureError("size-limit");
988
- if (sha256Bytes$1(bytes) !== value.sha256) throw new OutputCaptureError("capture-failed");
989
- return bytes;
990
- } catch (error) {
991
- stop();
992
- throw error;
993
- } finally {
994
- signal.removeEventListener("abort", stop);
995
- await stopping;
996
- }
997
- };
998
- }
999
- // src/runtime/isolated-checker.ts
1000
- const toolchains = [
1001
- "/usr",
1002
- "/bin",
1003
- "/lib",
1004
- "/lib64"
1005
- ];
1006
- const contains = (parent, child) => {
1007
- const path = relative(parent, child);
1008
- return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep);
1009
- };
1010
- /**
1011
- * Run an untrusted check in Linux Bubblewrap, or in its own Sandbox box when `box` is set.
1012
- * Never falls back to host execution.
1013
- *
1014
- * Bubblewrap requires /usr/bin/bwrap and permission to create Linux namespaces.
1015
- * Only trusted system toolchains, private proc/dev/tmp, and the writable copy are mounted.
1016
- * The canonical input path remains the working directory. Requested output files are retained
1017
- * before cleanup; otherwise copy writes are discarded.
1018
- * Time and output are bounded; input copy bytes and entries are bounded when requested.
1019
- * The command's memory and generated files require a resource-limited placement.
1020
- * Callers must keep the input and trusted toolchains stable while preparing the check.
1021
- *
1022
- * A box check creates one fresh box with no owner secrets and blocked egress (or strict egress to
1023
- * the placement's named domains), delivers the tree's regular files verified by sha256, runs the
1024
- * command there with only the environment `env` names, and deletes the box after successful
1025
- * requested retention. Incomplete retention returns private recovery custody only after the
1026
- * native owner confirms the box stopped. Box file capture requires /usr/bin/python3. Local recovery scratch is the
1027
- * caller's responsibility; the resource-limited placement requirement still applies.
1028
- */
1029
- async function runIsolatedCheck(options) {
1030
- if (options.box) return runInBox(options, options.box);
1031
- let scratch;
1032
- let outputs;
1033
- let result;
1034
- try {
1035
- const retention = prepareIsolatedCheckOutputs(options.outputs);
1036
- if (process.platform !== "linux") throw new Error("Linux Bubblewrap namespaces are required");
1037
- const { timeoutMs, maxOutputBytes, maxInputBytes, maxInputEntries } = checkLimits(options);
1038
- if (options.signal?.aborted) return {
1039
- succeeded: false,
1040
- reason: "cancelled",
1041
- diagnostic: "Check cancelled"
1042
- };
1043
- const { workspace, tree } = await protectedTree(options);
1044
- const binds = [];
1045
- for (const path of toolchains) {
1046
- let actual;
1047
- try {
1048
- actual = await realpath(path);
1049
- } catch (error) {
1050
- if (error.code === "ENOENT") continue;
1051
- throw error;
1052
- }
1053
- for (const mount of [path, actual]) if (contains(mount, workspace) || contains(workspace, mount) || contains(mount, tree) || contains(tree, mount)) throw new Error(`Toolchain overlaps protected workspace: ${path}`);
1054
- binds.push("--ro-bind", actual, path);
1055
- }
1056
- if (["/proc", "/dev"].some((path) => contains(path, tree) || contains(tree, path))) throw new Error("Tree overlaps a reserved namespace mount");
1057
- scratch = await mkdtemp(join(tmpdir(), "runtime-check-"));
1058
- if (contains(tree, scratch)) throw new Error("Temporary storage must be outside the input tree");
1059
- const copy = join(scratch, "tree");
1060
- await copyBoundedTree(tree, copy, maxInputBytes, maxInputEntries);
1061
- const input = retention ? await scanMaterializedWorkspaceManifest(copy) : void 0;
1062
- if (retention && input) assertOutputPathsUnoccupied(retention.paths, input);
1063
- result = await execute([
1064
- "--unshare-all",
1065
- "--die-with-parent",
1066
- "--new-session",
1067
- "--cap-drop",
1068
- "ALL",
1069
- "--clearenv",
1070
- "--setenv",
1071
- "PATH",
1072
- "/usr/bin:/bin",
1073
- "--setenv",
1074
- "HOME",
1075
- "/tmp",
1076
- "--setenv",
1077
- "LANG",
1078
- "C",
1079
- ...Object.entries(options.env ?? {}).flatMap(([name, value]) => [
1080
- "--setenv",
1081
- name,
1082
- value
1083
- ]),
1084
- ...binds,
1085
- "--proc",
1086
- "/proc",
1087
- "--dev",
1088
- "/dev",
1089
- "--tmpfs",
1090
- "/tmp",
1091
- "--bind",
1092
- copy,
1093
- tree,
1094
- "--chdir",
1095
- tree,
1096
- "--",
1097
- ...options.command
1098
- ], timeoutMs, maxOutputBytes, options.signal);
1099
- if (retention && input && (result.succeeded || result.reason !== "refused")) {
1100
- outputs = await retainIsolatedCheckOutputs(retention, canonicalCandidateDigest(input), localCheckOutputReader(copy));
1101
- if (outputs.status === "incomplete") outputs = {
1102
- ...outputs,
1103
- source: {
1104
- kind: "local",
1105
- path: scratch
1106
- }
1107
- };
1108
- result = withRetainedOutputs(result, outputs);
1109
- }
1110
- } catch (error) {
1111
- result = {
1112
- succeeded: false,
1113
- reason: error instanceof InputLimitError ? "input-limit" : "refused",
1114
- diagnostic: String(error)
1115
- };
1116
- }
1117
- if (scratch && outputs?.status !== "incomplete") try {
1118
- await removeScratch(scratch);
1119
- } catch (error) {
1120
- const cleanupDiagnostic = String(error);
1121
- result = result.succeeded ? {
1122
- succeeded: false,
1123
- reason: "cleanup-failed",
1124
- diagnostic: cleanupDiagnostic,
1125
- stdout: result.value.stdout,
1126
- stderr: result.value.stderr,
1127
- exitCode: 0
1128
- } : {
1129
- ...result,
1130
- cleanupDiagnostic
1131
- };
1132
- }
1133
- return outputs ? {
1134
- ...result,
1135
- outputs
1136
- } : result;
1137
- }
1138
- /** A preexisting input file must never masquerade as newly produced evaluator evidence. */
1139
- function assertOutputPathsUnoccupied(paths, input) {
1140
- for (const path of paths) if (input.files.some((file) => file.path === path || file.path.startsWith(`${path}/`) || path.startsWith(`${file.path}/`))) throw new Error("Declared check output path overlaps an input file");
1141
- }
1142
- function withRetainedOutputs(result, outputs) {
1143
- if (!result.succeeded || outputs.status === "retained") return {
1144
- ...result,
1145
- outputs
1146
- };
1147
- return {
1148
- succeeded: false,
1149
- reason: "failed",
1150
- diagnostic: "Check outputs were not completely retained",
1151
- stdout: result.value.stdout,
1152
- stderr: result.value.stderr,
1153
- exitCode: 0,
1154
- outputs
1155
- };
1156
- }
1157
- function checkLimits(options) {
1158
- const timeoutMs = options.timeoutMs ?? 3e4;
1159
- const maxOutputBytes = options.maxOutputBytes ?? 1048576;
1160
- const maxInputBytes = options.maxInputBytes;
1161
- const maxInputEntries = options.maxInputEntries;
1162
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647 || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxInputBytes !== void 0 && (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1) || maxInputEntries !== void 0 && (!Number.isSafeInteger(maxInputEntries) || maxInputEntries < 1)) throw new Error("Invalid execution limits");
1163
- if (!options.command.length || !options.command[0]) throw new Error("A command is required");
1164
- return {
1165
- timeoutMs,
1166
- maxOutputBytes,
1167
- maxInputBytes,
1168
- maxInputEntries
1169
- };
1170
- }
1171
- var InputLimitError = class extends Error {};
1172
- /** Copy in bounded chunks, preserving symlink text without reading its target. */
1173
- async function copyBoundedTree(source, destination, maxBytes, maxEntries) {
1174
- let bytes = 0;
1175
- let entries = 0;
1176
- const buffer = Buffer.allocUnsafe(1048576);
1177
- const visit = async (from, to, isRoot = false) => {
1178
- if (!isRoot && ++entries > (maxEntries ?? Number.MAX_SAFE_INTEGER)) throw new InputLimitError("Input tree exceeds maxInputEntries");
1179
- const info = await lstat(from);
1180
- if (info.isSymbolicLink()) await symlink(await readlink(from), to);
1181
- else if (info.isDirectory()) {
1182
- await mkdir(to, { mode: 448 });
1183
- for await (const entry of await opendir(from)) await visit(join(from, entry.name), join(to, entry.name));
1184
- await chmod(to, info.mode & 511);
1185
- } else if (info.isFile()) {
1186
- if (maxBytes !== void 0 && info.size > maxBytes - bytes) throw new InputLimitError("Input tree exceeds maxInputBytes");
1187
- const input = await open(from, constants.O_RDONLY | constants.O_NOFOLLOW);
1188
- try {
1189
- const output = await open(to, "wx", info.mode & 511);
1190
- try {
1191
- while (true) {
1192
- const { bytesRead } = await input.read(buffer, 0, maxBytes === void 0 ? buffer.length : Math.min(buffer.length, maxBytes - bytes + 1), null);
1193
- if (bytesRead === 0) break;
1194
- bytes += bytesRead;
1195
- if (maxBytes !== void 0 && bytes > maxBytes) throw new InputLimitError("Input tree exceeds maxInputBytes");
1196
- let written = 0;
1197
- while (written < bytesRead) {
1198
- const result = await output.write(buffer, written, bytesRead - written);
1199
- if (result.bytesWritten === 0) throw new Error("Input copy made no write progress");
1200
- written += result.bytesWritten;
1201
- }
1202
- }
1203
- } finally {
1204
- await output.close();
1205
- }
1206
- await chmod(to, info.mode & 511);
1207
- } finally {
1208
- await input.close();
1209
- }
1210
- } else throw new Error(`Input tree contains an unsupported entry: ${from}`);
1211
- };
1212
- await visit(source, destination, true);
1213
- }
1214
- async function checkInputEntries(root, maxEntries) {
1215
- let count = 0;
1216
- const visit = async (path, isRoot = false) => {
1217
- if (!isRoot && ++count > maxEntries) throw new InputLimitError("Input tree exceeds maxInputEntries");
1218
- if (!(await lstat(path)).isDirectory()) return;
1219
- for await (const entry of await opendir(path)) await visit(join(path, entry.name));
1220
- };
1221
- await visit(root, true);
1222
- }
1223
- async function protectedTree(options) {
1224
- const workspace = await realpath(options.workspaceRoot);
1225
- const tree = await realpath(options.tree);
1226
- if (!contains(workspace, tree) || !(await stat(tree)).isDirectory()) throw new Error("Tree must be a directory inside the workspace");
1227
- return {
1228
- workspace,
1229
- tree
1230
- };
1231
- }
1232
- /** Box-workspace-relative directory that receives the tree and is the command's cwd. */
1233
- const BOX_TREE = "tree";
1234
- /** Setup headroom in the requested native lifetime policy; this is not a deletion guarantee. */
1235
- const BOX_SETUP_SECONDS = 900;
1236
- /** The box's own process timeout is a backstop behind this module's deadline. */
1237
- const BOX_PROCESS_BACKSTOP_MS = 3e4;
1238
- /** The box's process supervisor prefixes this to stderr when it cannot start the executable. */
1239
- const BOX_EXEC_FAILURE = "process-supervisor: exec failed";
1240
- async function runInBox(options, placement) {
1241
- let box;
1242
- let evidence;
1243
- let outputs;
1244
- let retention;
1245
- let maxLifetimeSeconds;
1246
- let commandAttempted = false;
1247
- let commandSettled = false;
1248
- let result;
1249
- try {
1250
- retention = prepareIsolatedCheckOutputs(options.outputs);
1251
- const { timeoutMs, maxOutputBytes, maxInputBytes, maxInputEntries } = checkLimits(options);
1252
- maxLifetimeSeconds = Math.ceil(timeoutMs / 1e3) + BOX_SETUP_SECONDS;
1253
- if (options.signal?.aborted) return {
1254
- succeeded: false,
1255
- reason: "cancelled",
1256
- diagnostic: "Check cancelled"
1257
- };
1258
- const { tree } = await protectedTree(options);
1259
- if (maxInputEntries !== void 0) await checkInputEntries(tree, maxInputEntries);
1260
- const captured = await captureMaterializedWorkspace(tree, { limits: {
1261
- maxFiles: maxInputEntries ?? Number.MAX_SAFE_INTEGER,
1262
- maxFileBytes: maxInputBytes ?? Number.MAX_SAFE_INTEGER,
1263
- maxTotalFileBytes: maxInputBytes ?? Number.MAX_SAFE_INTEGER
1264
- } });
1265
- if (retention) assertOutputPathsUnoccupied(retention.paths, captured.manifest);
1266
- if (placement.builderAccounts.length === 0 || placement.builderAccounts.some((id) => !id)) throw new Error("builderAccounts must name the account of every key the judged run holds");
1267
- const memoryMB = placement.resources?.memoryMB;
1268
- if (memoryMB !== void 0 && (!Number.isSafeInteger(memoryMB) || memoryMB < 1 || placement.resources?.advisory === true || placement.resources?.advisoryFields?.includes("memoryMB"))) throw new Error("Check box requires an explicit finite resources.memoryMB limit");
1269
- const account = (await placement.client.getIdentity()).customerId;
1270
- if (!account) throw new Error("Sandbox returned no account for the check key");
1271
- if (placement.builderAccounts.includes(account)) throw new Error(`Check account ${account} is an account the judged run holds a key to, so the run can reach the check box`);
1272
- options.signal?.throwIfAborted();
1273
- box = await placement.client.createIsolated({
1274
- environment: placement.environment,
1275
- agent: placement.containers === true,
1276
- bare: false,
1277
- ephemeral: placement.containers !== true,
1278
- egressPolicy: egressFor(placement),
1279
- ...placement.resources ? { resources: placement.resources } : {},
1280
- maxLifetimeSeconds,
1281
- idempotencyKey: `agent-runtime-check-${randomUUID()}`
1282
- }, options.signal ? { signal: options.signal } : void 0);
1283
- const receipt = box.createReceipt();
1284
- if (receipt?.outcome !== "created" || receipt.ownerContext !== "isolated" || !Array.isArray(receipt.injectedSecrets) || receipt.injectedSecrets.length !== 0) throw new Error("Sandbox did not confirm a fresh box with no owner secrets");
1285
- const grantedMemoryMB = box.resources?.memoryMB;
1286
- if (memoryMB !== void 0 && (typeof grantedMemoryMB !== "number" || !Number.isSafeInteger(grantedMemoryMB) || grantedMemoryMB < 1 || grantedMemoryMB > memoryMB)) throw new Error("Sandbox did not confirm the requested finite memory cgroup limit");
1287
- const egress = (await box.egress.get()).policy;
1288
- const wanted = egressFor(placement);
1289
- if (egress.mode !== wanted.mode || wanted.mode === "strict" && (egress.includeImplicitDomains === true || [...egress.allowDomains ?? []].sort().join(",") !== [...wanted.allowDomains ?? []].sort().join(","))) throw new Error(`Check box egress is ${egress.mode} ${JSON.stringify(egress.allowDomains ?? [])}, not ${wanted.mode} ${JSON.stringify(wanted.allowDomains ?? [])}`);
1290
- const staging = await mkdtemp(join(tmpdir(), "runtime-check-box-"));
1291
- try {
1292
- for (const file of captured.files) {
1293
- const path = join(staging, file.path);
1294
- await mkdir(dirname(path), { recursive: true });
1295
- await writeFile(path, file.bytes, { mode: file.mode });
1296
- }
1297
- options.signal?.throwIfAborted();
1298
- await box.fs.uploadDir(staging, BOX_TREE);
1299
- } finally {
1300
- await rm(staging, {
1301
- recursive: true,
1302
- force: true
1303
- });
1304
- }
1305
- await verifyBoxTree(box, captured.manifest.files);
1306
- evidence = {
1307
- sandboxId: box.id,
1308
- account,
1309
- ...memoryMB === void 0 ? {} : { memoryMB: grantedMemoryMB },
1310
- input: captured.manifest,
1311
- inputDigest: canonicalCandidateDigest(captured.manifest)
1312
- };
1313
- commandAttempted = true;
1314
- const execution = await executeInBox(box, options.command, timeoutMs, maxOutputBytes, options.env ?? {}, options.signal);
1315
- result = execution.result;
1316
- commandSettled = execution.commandSettled;
1317
- } catch (error) {
1318
- result = {
1319
- succeeded: false,
1320
- reason: options.signal?.aborted ? "cancelled" : error instanceof InputLimitError || error instanceof WorkspaceScanLimitError ? "input-limit" : "refused",
1321
- diagnostic: String(error)
1322
- };
1323
- }
1324
- if (box && evidence && retention && maxLifetimeSeconds !== void 0 && commandAttempted && (!commandSettled || result.succeeded || result.reason !== "refused")) {
1325
- outputs = commandSettled ? await retainIsolatedCheckOutputs(retention, evidence.inputDigest, boxCheckOutputReader(box, BOX_TREE, retention.timeoutMs ?? 3e4)) : {
1326
- executionId: retention.executionId,
1327
- inputDigest: evidence.inputDigest,
1328
- status: "incomplete",
1329
- files: [],
1330
- missing: retention.paths.map((path) => ({
1331
- path,
1332
- reason: "capture-failed"
1333
- }))
1334
- };
1335
- result = withRetainedOutputs(result, outputs);
1336
- if (outputs.status === "incomplete") {
1337
- if (await stopCheckBoxForRecovery(box)) outputs = {
1338
- ...outputs,
1339
- source: {
1340
- kind: "sandbox",
1341
- sandboxId: box.id,
1342
- state: "stopped"
1343
- }
1344
- };
1345
- else if (!result.succeeded) result = {
1346
- ...result,
1347
- cleanupDiagnostic: "Recovery source stop was not confirmed; box deletion was requested"
1348
- };
1349
- }
1350
- }
1351
- if (box && outputs?.source?.kind !== "sandbox") try {
1352
- await box.delete();
1353
- } catch (error) {
1354
- const cleanupDiagnostic = `Check box ${box.id} was not deleted: ${String(error)}`;
1355
- result = result.succeeded ? {
1356
- succeeded: false,
1357
- reason: "cleanup-failed",
1358
- diagnostic: cleanupDiagnostic,
1359
- stdout: result.value.stdout,
1360
- stderr: result.value.stderr,
1361
- exitCode: 0
1362
- } : {
1363
- ...result,
1364
- cleanupDiagnostic: [result.cleanupDiagnostic, cleanupDiagnostic].filter(Boolean).join("; ")
1365
- };
1366
- }
1367
- return {
1368
- ...result,
1369
- ...evidence ? { box: evidence } : {},
1370
- ...outputs ? { outputs } : {}
1371
- };
1372
- }
1373
- /** Refuse unless the box reads each file with its manifest digest. `find` names the files: a box
1374
- * process takes 256 arguments at most. A name holding a newline or backslash, which coreutils
1375
- * escapes, does not compare. */
1376
- async function verifyBoxTree(box, files) {
1377
- const child = await box.process.spawnExact("find", [
1378
- ".",
1379
- "-type",
1380
- "f",
1381
- "-exec",
1382
- "sha256sum",
1383
- "{}",
1384
- "+"
1385
- ], {
1386
- cwd: BOX_TREE,
1387
- env: {
1388
- PATH: "/usr/bin:/bin",
1389
- LANG: "C"
1390
- },
1391
- inheritEnv: false,
1392
- timeoutMs: BOX_SETUP_SECONDS * 1e3
1393
- });
1394
- let listing = "";
1395
- for await (const text of child.stdout()) listing += text;
1396
- if (await child.wait() !== 0) throw new Error("Check box could not hash the tree it received");
1397
- const read = new Set(listing.split("\n").map((line) => `sha256:${line.slice(0, 64)} ${line.slice(68)}`));
1398
- for (const file of files) if (!read.has(`${file.sha256} ${file.path}`)) throw new Error(`Check box received different bytes for ${file.path}`);
1399
- }
1400
- /** Blocked unless the placement names domains; then strict, with no implicit domain list. */
1401
- function egressFor(placement) {
1402
- const domains = [...new Set(placement.egress ?? [])].filter((domain) => domain.trim() !== "");
1403
- return domains.length === 0 ? { mode: "blocked" } : {
1404
- mode: "strict",
1405
- allowDomains: domains,
1406
- includeImplicitDomains: false
1407
- };
1408
- }
1409
- async function executeInBox(box, command, timeoutMs, limit, env, signal) {
1410
- const [executable, ...args] = command;
1411
- let child;
1412
- const deadline = new AbortController();
1413
- const stdout = [];
1414
- const stderr = [];
1415
- let bytes = 0;
1416
- let failure;
1417
- let stopping;
1418
- let stopped = false;
1419
- const stop = (reason) => {
1420
- failure ??= reason;
1421
- deadline.abort();
1422
- if (child) stopping ??= stopCheckProcess(child);
1423
- };
1424
- const clearTimer = armDeadlineTimer(timeoutMs, () => stop("timeout"), true);
1425
- const cancel = () => stop("cancelled");
1426
- signal?.addEventListener("abort", cancel, { once: true });
1427
- if (signal?.aborted) cancel();
1428
- const capture = async (stream, target) => {
1429
- for await (const text of stream) {
1430
- if (deadline.signal.aborted) break;
1431
- const chunk = Buffer.from(text);
1432
- const remaining = Math.max(0, limit - bytes);
1433
- bytes += chunk.length;
1434
- if (remaining) target.push(chunk.subarray(0, remaining));
1435
- if (bytes > limit) stop("output-limit");
1436
- }
1437
- };
1438
- let completion;
1439
- try {
1440
- child = await runAbortable(() => {
1441
- const launching = box.process.spawnExact(executable, args, {
1442
- cwd: BOX_TREE,
1443
- env: {
1444
- PATH: "/usr/bin:/bin",
1445
- HOME: "/tmp",
1446
- LANG: "C",
1447
- ...env
1448
- },
1449
- inheritEnv: false,
1450
- timeoutMs: timeoutMs + BOX_PROCESS_BACKSTOP_MS
1451
- });
1452
- launching.then((late) => {
1453
- if (deadline.signal.aborted) stopCheckProcess(late);
1454
- }, () => void 0);
1455
- return launching;
1456
- }, deadline.signal, "Check interrupted");
1457
- const running = child;
1458
- completion = await runAbortable(async () => {
1459
- await Promise.all([capture(running.stdout(), stdout), capture(running.stderr(), stderr)]);
1460
- return {
1461
- exitCode: await running.wait(),
1462
- exitSignal: (await running.status()).exitSignal
1463
- };
1464
- }, deadline.signal, "Check interrupted");
1465
- } catch (error) {
1466
- if (child) stopping ??= stopCheckProcess(child);
1467
- if (!failure) throw error;
1468
- } finally {
1469
- clearTimer();
1470
- signal?.removeEventListener("abort", cancel);
1471
- stopped = await stopping === true;
1472
- }
1473
- const out = Buffer.concat(stdout).toString();
1474
- const err = Buffer.concat(stderr).toString();
1475
- const exitCode = completion?.exitCode ?? null;
1476
- const exitSignal = completion?.exitSignal;
1477
- const evidence = {
1478
- stdout: out,
1479
- stderr: err,
1480
- exitCode: exitSignal ? null : exitCode
1481
- };
1482
- const finish = (result) => ({
1483
- result,
1484
- commandSettled: completion !== void 0 || stopped
1485
- });
1486
- if (failure) return finish({
1487
- succeeded: false,
1488
- reason: failure,
1489
- diagnostic: err || failure,
1490
- ...evidence
1491
- });
1492
- if (exitCode === 127 && err.startsWith(BOX_EXEC_FAILURE)) return finish({
1493
- succeeded: false,
1494
- reason: "refused",
1495
- diagnostic: err,
1496
- ...evidence
1497
- });
1498
- if (exitSignal) return finish({
1499
- succeeded: false,
1500
- reason: "failed",
1501
- diagnostic: err || `Check killed by ${exitSignal}`,
1502
- ...evidence
1503
- });
1504
- if (exitCode !== 0) return finish({
1505
- succeeded: false,
1506
- reason: "failed",
1507
- diagnostic: err || out || `Check exited ${exitCode}`,
1508
- ...evidence
1509
- });
1510
- return finish({
1511
- succeeded: true,
1512
- value: {
1513
- stdout: out,
1514
- stderr: err
1515
- }
1516
- });
1517
- }
1518
- function execute(args, timeoutMs, limit, signal) {
1519
- return new Promise((done) => {
1520
- const child = spawn("/usr/bin/bwrap", [
1521
- "--json-status-fd",
1522
- "3",
1523
- ...args
1524
- ], {
1525
- env: {},
1526
- stdio: [
1527
- "ignore",
1528
- "pipe",
1529
- "pipe",
1530
- "pipe"
1531
- ],
1532
- detached: true
1533
- });
1534
- const stdout = [];
1535
- const stderr = [];
1536
- let bytes = 0;
1537
- let status = "";
1538
- let failure;
1539
- const stop = (reason) => {
1540
- failure ??= reason;
1541
- if (child.pid) try {
1542
- process.kill(-child.pid, "SIGKILL");
1543
- } catch (error) {
1544
- if (error.code !== "ESRCH") child.kill("SIGKILL");
1545
- }
1546
- };
1547
- const clearTimer = armDeadlineTimer(timeoutMs, () => stop("timeout"), true);
1548
- const cancel = () => stop("cancelled");
1549
- signal?.addEventListener("abort", cancel, { once: true });
1550
- if (signal?.aborted) cancel();
1551
- const capture = (target) => (chunk) => {
1552
- const remaining = Math.max(0, limit - bytes);
1553
- bytes += chunk.length;
1554
- if (remaining) target.push(chunk.subarray(0, remaining));
1555
- if (bytes > limit) stop("output-limit");
1556
- };
1557
- child.stdio[3]?.on("data", (chunk) => {
1558
- if (status.length + chunk.length > 16384) {
1559
- stop("output-limit");
1560
- return;
1561
- }
1562
- status += chunk.toString();
1563
- });
1564
- child.stdout.on("data", capture(stdout));
1565
- child.stderr.on("data", capture(stderr));
1566
- let spawnError;
1567
- child.on("error", (error) => {
1568
- spawnError = error;
1569
- });
1570
- child.on("close", (code) => {
1571
- clearTimer();
1572
- signal?.removeEventListener("abort", cancel);
1573
- const out = Buffer.concat(stdout).toString();
1574
- const err = Buffer.concat(stderr).toString();
1575
- const evidence = {
1576
- stdout: out,
1577
- stderr: err,
1578
- exitCode: code
1579
- };
1580
- const executed = status.split("\n").some((line) => {
1581
- try {
1582
- const record = JSON.parse(line);
1583
- return record !== null && typeof record === "object" && "exit-code" in record && typeof record["exit-code"] === "number" && record["exit-code"] === code;
1584
- } catch {
1585
- return false;
1586
- }
1587
- });
1588
- if (failure) done({
1589
- succeeded: false,
1590
- reason: failure,
1591
- diagnostic: err || failure,
1592
- ...evidence
1593
- });
1594
- else if (spawnError) done({
1595
- succeeded: false,
1596
- reason: "refused",
1597
- diagnostic: String(spawnError),
1598
- ...evidence
1599
- });
1600
- else if (!executed) done({
1601
- succeeded: false,
1602
- reason: "refused",
1603
- diagnostic: err || "Bubblewrap did not confirm command execution",
1604
- ...evidence
1605
- });
1606
- else if (code !== 0) done({
1607
- succeeded: false,
1608
- reason: "failed",
1609
- diagnostic: err || out || `Check exited ${code}`,
1610
- ...evidence
1611
- });
1612
- else done({
1613
- succeeded: true,
1614
- value: {
1615
- stdout: out,
1616
- stderr: err
1617
- }
1618
- });
1619
- });
1620
- });
1621
- }
1622
- const executeFile = promisify(execFile);
1623
- /** GNU tools traverse relative to open directories, including trees deeper than PATH_MAX.
1624
- * Recursive chmod ignores encountered symlinks; rm never follows their targets. */
1625
- async function removeScratch(path) {
1626
- const options = {
1627
- env: {},
1628
- timeout: 1e4,
1629
- maxBuffer: 16384
1630
- };
1631
- let permissionError;
1632
- try {
1633
- await executeFile("/bin/chmod", [
1634
- "--recursive",
1635
- "--preserve-root",
1636
- "u+rwX",
1637
- "--",
1638
- path
1639
- ], options);
1640
- } catch (error) {
1641
- permissionError = error;
1642
- }
1643
- try {
1644
- await executeFile("/bin/rm", [
1645
- "--recursive",
1646
- "--force",
1647
- "--one-file-system",
1648
- "--",
1649
- path
1650
- ], options);
1651
- } catch (error) {
1652
- throw new Error([permissionError, error].filter((value) => value !== void 0).map(String).join("; "));
1653
- }
1654
- }
1655
738
  // src/runtime/declared-check.ts
1656
739
  /**
1657
740
  * A declared check: the one frozen evaluator program that decides "done" for a run, named by
@@ -7736,7 +6819,7 @@ function graphDesignerExtraTools(env) {
7736
6819
  })();
7737
6820
  if (name === "designer_history") return (async () => {
7738
6821
  const args = asRecord(raw ?? {}, "designer_history");
7739
- const { listGraphRuns } = await import("./graph-registry-gYbq4L5Z.js");
6822
+ const { listGraphRuns } = await import("./graph-registry-uTIyyy7A.js");
7740
6823
  const all = await listGraphRuns(registry);
7741
6824
  return { runs: (args.taskShape === void 0 ? all : all.filter((row) => row.taskDigest === taskShapeDigest(asRecord(args.taskShape, "designer_history.taskShape")))).map(summarize) };
7742
6825
  })();
@@ -7747,7 +6830,7 @@ function graphDesignerExtraTools(env) {
7747
6830
  if (mode === "replay") {
7748
6831
  const digest = args.graphDigest;
7749
6832
  if (typeof digest !== "string" || digest.length === 0) throw new ValidationError("designer_run_graph: replay mode requires graphDigest from lookup");
7750
- const { loadGraph } = await import("./graph-registry-gYbq4L5Z.js");
6833
+ const { loadGraph } = await import("./graph-registry-uTIyyy7A.js");
7751
6834
  const stored = await loadGraph(registry, digest);
7752
6835
  if (!stored) throw new ValidationError(`designer_run_graph: no stored graph ${digest.slice(0, 12)} — re-lookup`);
7753
6836
  const graph = env.bindCheck(graphFromStored(stored, () => {
@@ -7803,14 +6886,6 @@ var GraphFileResolutionError = class extends Error {
7803
6886
  function refuse$2(message) {
7804
6887
  throw new ValidationError(`loadGraphFile: ${message}`);
7805
6888
  }
7806
- function jsonData(value, visiting = /* @__PURE__ */ new Set()) {
7807
- if (value === null || typeof value === "string" || typeof value === "boolean") return;
7808
- if (typeof value === "number" && Number.isFinite(value)) return;
7809
- if (!value || typeof value !== "object" || visiting.has(value)) refuse$2("definition must be finite, acyclic JSON data");
7810
- visiting.add(value);
7811
- for (const child of Object.values(value)) jsonData(child, visiting);
7812
- visiting.delete(value);
7813
- }
7814
6889
  function bindParameters(raw, supplied) {
7815
6890
  const definitions = graphFileSchema.shape.params.parse(raw.params) ?? {};
7816
6891
  for (const key of Object.keys(supplied ?? {})) if (!Object.hasOwn(definitions, key)) refuse$2(`unknown parameter ${key}`);
@@ -7826,7 +6901,7 @@ function bindParameters(raw, supplied) {
7826
6901
  if (type) {
7827
6902
  if (!(type === "integer" ? Number.isSafeInteger(value) : type === "null" ? value === null : type === "array" ? Array.isArray(value) : type === "object" ? value !== null && typeof value === "object" && !Array.isArray(value) : typeof value === type)) refuse$2(`parameter ${key} must be ${type}`);
7828
6903
  }
7829
- jsonData(value);
6904
+ assertJsonData(value, "loadGraphFile");
7830
6905
  return [key, value];
7831
6906
  }));
7832
6907
  }
@@ -7920,7 +6995,7 @@ async function loadGraphFile(file, options = {}) {
7920
6995
  if (document.errors.length) refuse$2(document.errors.map((error) => error.message).join("; "));
7921
6996
  if (document.warnings.length) refuse$2(document.warnings.map((warning) => warning.message).join("; "));
7922
6997
  const raw = format === ".json" ? JSON.parse(text) : document.toJS({ maxAliasCount: 100 });
7923
- jsonData(raw);
6998
+ assertJsonData(raw, "loadGraphFile");
7924
6999
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) refuse$2("definition must be an object");
7925
7000
  const source = raw;
7926
7001
  if (Array.isArray(source.nodes) && source.nodes.some((node) => node && typeof node === "object" && "call" in node)) refuse$2("call vertices are not supported by this loader");
@@ -7933,6 +7008,7 @@ async function loadGraphFile(file, options = {}) {
7933
7008
  const bindingOptions = options.trigger === void 0 ? { defer: ["trigger"] } : {};
7934
7009
  const compiled = compileGraphParameters(authored, bindings, bindingOptions);
7935
7010
  const bound = graphFileSchema.parse(compiled.value);
7011
+ if (bound.entry && bound.continuation) refuse$2("continuation with entry has no root manager");
7936
7012
  const boundOn = on === void 0 ? void 0 : bindGraphParameters(on, bindings, bindingOptions);
7937
7013
  if (bound.check && bound.deliverable) refuse$2("use check or deliverable, not both");
7938
7014
  let deliverable = bound.deliverable;
@@ -7957,15 +7033,25 @@ async function loadGraphFile(file, options = {}) {
7957
7033
  if (!deliverable) refuse$2("check or deliverable is required");
7958
7034
  if (deliverable.checkRef === "route" && !deliverable.when) refuse$2("route deliverable requires when");
7959
7035
  if (deliverable.checkRef === "command" && !deliverable.command) refuse$2("command deliverable requires command");
7960
- const nodes = await Promise.all(bound.nodes.map(async (node) => ({
7961
- id: node.id,
7962
- profile: await expandProfile(node, {
7963
- ...options,
7964
- cwd: options.cwd ?? dirname(file)
7965
- })
7966
- })));
7036
+ const nodes = await Promise.all(bound.nodes.map(async (node) => {
7037
+ if (node.check === void 0) return {
7038
+ id: node.id,
7039
+ profile: await expandProfile(node, {
7040
+ ...options,
7041
+ cwd: options.cwd ?? dirname(file)
7042
+ })
7043
+ };
7044
+ if (node.profile || node.seat || node.needs) refuse$2(`check node ${node.id} runs a command and takes no profile, seat or needs`);
7045
+ if (node.id === "params" || node.id === "trigger") refuse$2(`reserved node id ${node.id}`);
7046
+ return {
7047
+ id: node.id,
7048
+ profile: { name: node.id },
7049
+ check: node.check
7050
+ };
7051
+ }));
7967
7052
  const nodeIds = new Set(nodes.map((node) => node.id));
7968
7053
  for (const id of Object.keys(options.nodeSeats ?? {})) if (!nodeIds.has(id)) refuse$2(`seat override references unknown node ${id}`);
7054
+ if (nodes.some((node) => node.check) && !bound.entry) refuse$2("check nodes run only in a flow graph (one with entry)");
7969
7055
  if (deliverable.when) {
7970
7056
  for (const condition of "all" in deliverable.when ? deliverable.when.all : [deliverable.when]) if (condition.node !== void 0 && !nodeIds.has(condition.node)) refuse$2(`check references unknown node ${condition.node}`);
7971
7057
  }
@@ -7978,11 +7064,14 @@ async function loadGraphFile(file, options = {}) {
7978
7064
  node: entry,
7979
7065
  task: {}
7980
7066
  } : entry) } : {},
7981
- ...bound.gates ? { gates: bound.gates } : {}
7067
+ ...bound.gates ? { gates: bound.gates } : {},
7068
+ ...bound.workspace ? { workspace: bound.workspace } : {},
7069
+ ...bound.continuation ? { continuation: bound.continuation } : {}
7982
7070
  };
7983
7071
  const triggerBindings = compiled.triggerBindings.flatMap((slot) => {
7984
7072
  const [top, index, field, ...tail] = slot.path;
7985
7073
  if (top === "nodes") {
7074
+ if (field === "check") refuse$2("trigger slots in a check node command are not shell data");
7986
7075
  if (field === "id") return [slot, {
7987
7076
  ...slot,
7988
7077
  path: [
@@ -8042,7 +7131,8 @@ async function loadGraphFile(file, options = {}) {
8042
7131
  "budget",
8043
7132
  "gates",
8044
7133
  "entry",
8045
- "deliverable"
7134
+ "deliverable",
7135
+ "continuation"
8046
7136
  ].includes(String(top)) ? [slot] : [];
8047
7137
  });
8048
7138
  assertGraphTriggerBindings(topology, triggerBindings);
@@ -8064,6 +7154,19 @@ async function loadGraphFile(file, options = {}) {
8064
7154
  ...boundOn === void 0 ? {} : { on: boundOn }
8065
7155
  };
8066
7156
  }
7157
+ /**
7158
+ * The directory a graph run's nodes work in. `workspace: cwd` (the default) is `cwd`. `workspace:
7159
+ * fresh` is a new empty directory the run owns, `<root>/<runId>` (default root
7160
+ * `~/.tangle/workspaces`); a resumed run with the same id reuses it. The host passes the result as
7161
+ * the backend's working directory, where agent and check nodes both run.
7162
+ */
7163
+ async function graphWorkspace(graph, options) {
7164
+ if ((graph.workspace ?? "cwd") === "cwd") return options.cwd;
7165
+ if (!/^(?!\.\.?$)[A-Za-z0-9._-]+$/.test(options.runId)) throw new ValidationError(`graphWorkspace: run id ${JSON.stringify(options.runId)} is not a directory name`);
7166
+ const directory = join(options.root ?? join(homedir(), ".tangle", "workspaces"), options.runId);
7167
+ await mkdir(directory, { recursive: true });
7168
+ return directory;
7169
+ }
8067
7170
  // src/runtime/supervise/graph-session-observation.ts
8068
7171
  function measured(fact) {
8069
7172
  return fact && !fact.partial && fact.value !== null ? fact.value : void 0;
@@ -9126,7 +8229,9 @@ const storedGraphSchema = graphFileSchema.pick({
9126
8229
  }).required().extend({
9127
8230
  observed: z.literal(false).optional(),
9128
8231
  triggerBindings: graphTriggerBindingsSchema.optional(),
9129
- nodes: z.array(node).min(1),
8232
+ nodes: z.array(node.extend({ check: graphCheckNodeSchema.optional() })).min(1),
8233
+ workspace: graphFileSchema.shape.workspace,
8234
+ continuation: graphFileSchema.shape.continuation,
9130
8235
  edges: z.array(edge),
9131
8236
  entry: entry.optional(),
9132
8237
  gates: gates.optional()
@@ -9134,40 +8239,19 @@ const storedGraphSchema = graphFileSchema.pick({
9134
8239
  function refuse(message) {
9135
8240
  throw new ValidationError(`validateStoredGraph: ${message}`);
9136
8241
  }
9137
- /** Inspect descriptors instead of invoking accessors; JSON has no getters, symbols, prototypes,
9138
- * sparse arrays, undefined, closures or cycles. Repeated non-cyclic references remain JSON data. */
9139
- function assertJsonData(value, visiting = /* @__PURE__ */ new Set()) {
9140
- if (value === null || typeof value === "string" || typeof value === "boolean") return;
9141
- if (typeof value === "number" && Number.isFinite(value)) return;
9142
- if (!value || typeof value !== "object" || visiting.has(value)) refuse("expected finite, acyclic JSON data without executable values");
9143
- const array = Array.isArray(value);
9144
- const prototype = Object.getPrototypeOf(value);
9145
- if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) refuse("expected plain JSON objects and arrays");
9146
- visiting.add(value);
9147
- const descriptors = Object.getOwnPropertyDescriptors(value);
9148
- const keys = Reflect.ownKeys(descriptors);
9149
- if (array && keys.length !== value.length + 1) refuse("sparse arrays are not canonical JSON");
9150
- for (const key of keys) {
9151
- if (array && key === "length") continue;
9152
- const property = typeof key === "string" ? descriptors[key] : void 0;
9153
- if (!property || !property.enumerable || !("value" in property)) refuse("JSON data cannot contain symbols, hidden properties or accessors");
9154
- if (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length)) refuse("arrays may contain only indexed JSON values");
9155
- assertJsonData(property.value, visiting);
9156
- }
9157
- visiting.delete(value);
9158
- }
9159
8242
  function assertExecutableNodes(graph) {
9160
- for (const item of graph.nodes) assertExecutableAgentProfile(item.profile, `validateStoredGraph node ${item.id}`);
8243
+ for (const item of graph.nodes) if (item.check === void 0) assertExecutableAgentProfile(item.profile, `validateStoredGraph node ${item.id}`);
9161
8244
  for (const item of graph.edges) if (item.kind === "maps" && item.graph) assertExecutableNodes(item.graph);
9162
8245
  }
9163
8246
  /** Validate compiled, runnable StoredGraph data. Authored params/seats/on envelopes belong to
9164
8247
  * loadGraphFile; observations belong to the registry's observation importer, never this entry.
9165
8248
  * No check function or model call is executed while validating environment-owned references. */
9166
8249
  function validateStoredGraph(value, options = {}) {
9167
- assertJsonData(value);
8250
+ assertJsonData(value, "validateStoredGraph");
9168
8251
  const parsed = storedGraphSchema.safeParse(value);
9169
8252
  if (!parsed.success) refuse(parsed.error.message);
9170
8253
  const graph = parsed.data;
8254
+ if (graph.entry && graph.continuation) refuse("continuation with entry has no root manager");
9171
8255
  if (graph.triggerBindings !== void 0) assertGraphTriggerBindings(graph, graph.triggerBindings);
9172
8256
  const check = graph.deliverable;
9173
8257
  if (check.checkRef === "command") {
@@ -9187,6 +8271,11 @@ function validateStoredGraph(value, options = {}) {
9187
8271
  const ids = new Set(graph.nodes.map((item) => item.id));
9188
8272
  for (const condition of "all" in check.when ? check.when.all : [check.when]) if (condition.node !== void 0 && !ids.has(condition.node)) refuse(`check references unknown node ${condition.node}`);
9189
8273
  }
8274
+ for (const item of graph.nodes) {
8275
+ if (item.check === void 0) continue;
8276
+ if (!graph.entry || check.command) refuse("check nodes need a flow graph without a command check");
8277
+ if (item.profile.name !== item.id || Object.keys(item.profile).length !== 1) refuse(`check node ${item.id} carries a profile; its profile is { name: id }`);
8278
+ }
9190
8279
  assertExecutableNodes(graph);
9191
8280
  const registry = options.registry ?? kernelPromptRegistry();
9192
8281
  if (graph.entry) validateFlowGraph(graph, registry, options.analysts);
@@ -10462,6 +9551,6 @@ async function runInWorkspace(ws, body, opts = {}) {
10462
9551
  function tail(s) {
10463
9552
  return s.slice(-400);
10464
9553
  }
10465
- export { runBenchmark as $, pairwiseSignificance as $t, searchPresets as A, sanitizeMcpToolSchema as At, graphFromSessionFacts as B, assertDeclaredCheck as Bt, withUntrackedArtifacts as C, harvestCorpus as Ct, SEARCH_SPEC_CORRELATION_KEY as D, connectStdioMcp as Dt, createFencedSqlRunContext as E, McpSpawnFault as Et, loadProfile as F, secretEnvOfMcpServer as Ft, chatWorkerSeam as G, readDeclaredCheck as Gt, loadGraphFile as H, declaredCheckDeliverable as Ht, pickBestProfile as I, inlineSandboxClient as It, strategyTournament as J, completionAuthorizes as Jt, createChatSessionStore as K, runIsolatedCheck as Kt, profileRegistry as L, loopCampaignDispatch as Lt, connectRemoteSupervisor as M, mcpSecretEnvMetadataKey as Mt, connectRemoteSupervisorControl as N, resolveMcpServerLaunch as Nt, assertSearchSpec as O, materializeLocalMcp as Ot, bestProfileFor as P, resolveSecretEnv as Pt, printBenchmarkReport as Q, leaderboard as Qt, profileRunsForName as R, loopDispatch as Rt, copyUntrackedIntoClone as S, HarvestError as St, validateStoredGraph as T, resolveSandboxClient as Tt, graphDesignerExtraTools as U, declaredCheckDigest as Ut, GraphFileResolutionError as V, checkProgramDigest as Vt, chatTransportExecutor as W, declaredCheckJudge as Wt, openSandboxRun as X, sentinelCompletion as Xt, SandboxRunAbortError as Y, deterministicCompletion as Yt, createS3PrivateCasStore as Z, stopSentinel as Zt, NOTE_MAX_CHARS as _, buildSteerContext as _t, localShell as a, openSqlRunStore as an, FileCorpus as at, composeWorkerEvidence as b, observationFromRegistry as bt, createVerifierEnvironment as c, fanout as ct, harvestSurfaceDiffs as d, panel as dt, renderLeaderboardHtml as en, createPrivateCasArtifactPort as et, analystsFromRegistry as f, pipeline as ft, EVIDENCE_MAX_CHARS as g, assertTraceDerivedFindings as gt, runCoderChecks as h, widen as ht, jjWorkspace as i, SqlRunOwnershipError as in, runPersonified as it, searchSpecDigest as j, envKeyProvider as jt, compileSearchSpec as k, createMcpEnvironment as kt, boxSurfaceReader as l, flatWidenGate as lt, worktreeFanout as m, verify as mt, makeFinding$1 as n, renderLeaderboardSvg as nn, trajectoryReport as nt, runInWorkspace as o, InMemoryCorpus as ot, failuresAnalyst as p, selectValidWinner as pt, runStrategyEvolution as q, validateIsolatedCheckOutputs as qt, gitWorkspace as r, renderPairwiseMarkdown as rn, definePersona as rt, createWaterfallCollector as s, renderCorpusToInstructions as st, computeFindingId$1 as t, renderLeaderboardMarkdown as tn, equalKOnCost as tt, fsSurfaceReader as u, loopUntil as ut, VERIFY_TAIL_CHARS as v, createScopeAnalyst as vt, analyzeTrace as w, defineLeaderboard as wt, settledWorkerOut as x, inProcessSandboxClient as xt, closingWorkerNote as y, registryScopeAnalyst as yt, recordProfileRun as z, superviseDispatch as zt };
9554
+ export { printBenchmarkReport as $, renderLeaderboardHtml as $t, searchPresets as A, createMcpEnvironment as At, graphFromSessionFacts as B, superviseDispatch as Bt, withUntrackedArtifacts as C, HarvestError as Ct, SEARCH_SPEC_CORRELATION_KEY as D, McpSpawnFault as Dt, createFencedSqlRunContext as E, resolveSandboxClient as Et, loadProfile as F, resolveSecretEnv as Ft, chatTransportExecutor as G, declaredCheckJudge as Gt, graphWorkspace as H, checkProgramDigest as Ht, pickBestProfile as I, secretEnvOfMcpServer as It, runStrategyEvolution as J, deterministicCompletion as Jt, chatWorkerSeam as K, readDeclaredCheck as Kt, profileRegistry as L, inlineSandboxClient as Lt, connectRemoteSupervisor as M, envKeyProvider as Mt, connectRemoteSupervisorControl as N, mcpSecretEnvMetadataKey as Nt, assertSearchSpec as O, connectStdioMcp as Ot, bestProfileFor as P, resolveMcpServerLaunch as Pt, createS3PrivateCasStore as Q, pairwiseSignificance as Qt, profileRunsForName as R, loopCampaignDispatch as Rt, copyUntrackedIntoClone as S, inProcessSandboxClient as St, validateStoredGraph as T, defineLeaderboard as Tt, loadGraphFile as U, declaredCheckDeliverable as Ut, GraphFileResolutionError as V, assertDeclaredCheck as Vt, graphDesignerExtraTools as W, declaredCheckDigest as Wt, SandboxRunAbortError as X, stopSentinel as Xt, strategyTournament as Y, sentinelCompletion as Yt, openSandboxRun as Z, leaderboard as Zt, NOTE_MAX_CHARS as _, assertTraceDerivedFindings as _t, localShell as a, runPersonified as at, composeWorkerEvidence as b, registryScopeAnalyst as bt, createVerifierEnvironment as c, renderCorpusToInstructions as ct, harvestSurfaceDiffs as d, loopUntil as dt, renderLeaderboardMarkdown as en, runBenchmark as et, analystsFromRegistry as f, panel as ft, EVIDENCE_MAX_CHARS as g, widen as gt, runCoderChecks as h, verify as ht, jjWorkspace as i, openSqlRunStore as in, definePersona as it, searchSpecDigest as j, sanitizeMcpToolSchema as jt, compileSearchSpec as k, materializeLocalMcp as kt, boxSurfaceReader as l, fanout as lt, worktreeFanout as m, selectValidWinner as mt, makeFinding$1 as n, renderPairwiseMarkdown as nn, equalKOnCost as nt, runInWorkspace as o, FileCorpus as ot, failuresAnalyst as p, pipeline as pt, createChatSessionStore as q, completionAuthorizes as qt, gitWorkspace as r, SqlRunOwnershipError as rn, trajectoryReport as rt, createWaterfallCollector as s, InMemoryCorpus as st, computeFindingId$1 as t, renderLeaderboardSvg as tn, createPrivateCasArtifactPort as tt, fsSurfaceReader as u, flatWidenGate as ut, VERIFY_TAIL_CHARS as v, buildSteerContext as vt, analyzeTrace as w, harvestCorpus as wt, settledWorkerOut as x, observationFromRegistry as xt, closingWorkerNote as y, createScopeAnalyst as yt, recordProfileRun as z, loopDispatch as zt };
10466
9555
 
10467
- //# sourceMappingURL=runtime-COm9uBBx.js.map
9556
+ //# sourceMappingURL=runtime-CaM5Gbbv.js.map