@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.
- package/CHANGELOG.md +86 -0
- package/README.md +23 -12
- package/dist/{activation-Cwy3_Qn_.js → activation-Ce4zUcu8.js} +4 -4
- package/dist/{activation-Cwy3_Qn_.js.map → activation-Ce4zUcu8.js.map} +1 -1
- package/dist/{activation-CWx7I1PU.d.ts → activation-xxvLF1-1.d.ts} +2 -2
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -3
- package/dist/{bundle-Cy5igJdv.js → bundle-CcpL2lfr.js} +2 -2
- package/dist/{bundle-Cy5igJdv.js.map → bundle-CcpL2lfr.js.map} +1 -1
- package/dist/candidate-execution/index.js +4 -4
- package/dist/continuation-CIyKnS1Q.js +451 -0
- package/dist/continuation-CIyKnS1Q.js.map +1 -0
- package/dist/{coordination-manager-D9z7To7p.js → coordination-manager-sa_PkISw.js} +247 -149
- package/dist/coordination-manager-sa_PkISw.js.map +1 -0
- package/dist/{coordination-map-routing-CKRNZSaV.js → coordination-map-routing-XZXTlCmn.js} +2 -4
- package/dist/coordination-map-routing-XZXTlCmn.js.map +1 -0
- package/dist/{delegate-ws-KBA2H.js → delegate-DhTkJIta.js} +3 -3
- package/dist/{delegate-ws-KBA2H.js.map → delegate-DhTkJIta.js.map} +1 -1
- package/dist/durable.d.ts +71 -4
- package/dist/durable.js +162 -13
- package/dist/durable.js.map +1 -1
- package/dist/{errors-AzSuGI23.js → errors-DG8yufqX.js} +8 -1
- package/dist/errors-DG8yufqX.js.map +1 -0
- package/dist/{failure-cause-BzjUUsRz.js → failure-cause-CVB-YE88.js} +17 -16
- package/dist/failure-cause-CVB-YE88.js.map +1 -0
- package/dist/{graph-registry-7X2nrreR.js → graph-registry-czAwYwNj.js} +133 -9
- package/dist/graph-registry-czAwYwNj.js.map +1 -0
- package/dist/{graph-registry-gYbq4L5Z.js → graph-registry-uTIyyy7A.js} +1 -1
- package/dist/graph-tSIT_eWK.js +1793 -0
- package/dist/graph-tSIT_eWK.js.map +1 -0
- package/dist/{improve-BxfqTsZy.js → improve-H08hiIw9.js} +19 -48
- package/dist/improve-H08hiIw9.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +16 -16
- package/dist/intelligence.d.ts +2 -2
- package/dist/intelligence.js +8 -8
- package/dist/intelligence.js.map +1 -1
- package/dist/kernel.d.ts +2 -2
- package/dist/kernel.js +16 -15
- package/dist/{loop-runner-CUMypgRw.d.ts → loop-runner-BGwhcn1p.d.ts} +2 -2
- package/dist/{loop-runner-BmGZ4y1W.js → loop-runner-ChqK8kSz.js} +3 -3
- package/dist/{loop-runner-BmGZ4y1W.js.map → loop-runner-ChqK8kSz.js.map} +1 -1
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +7 -5
- package/dist/mcp/bin.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +9 -9
- package/dist/mcp/memory-bin.js +2 -2
- package/dist/{model-policy-JOcg3NRs.js → model-policy-yxOrDTTj.js} +9 -9
- package/dist/model-policy-yxOrDTTj.js.map +1 -0
- package/dist/{prepare-Dtfk_Wi_.js → prepare-BnF8nGIg.js} +4 -4
- package/dist/{prepare-Dtfk_Wi_.js.map → prepare-BnF8nGIg.js.map} +1 -1
- package/dist/{profile-CNSQjOes.js → profile-CD25rTJF.js} +4 -4
- package/dist/{profile-CNSQjOes.js.map → profile-CD25rTJF.js.map} +1 -1
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +2 -2
- package/dist/{provision-supervisor-CeQTPHRr.js → provision-supervisor-Dx4TXTMM.js} +8 -8
- package/dist/{provision-supervisor-CeQTPHRr.js.map → provision-supervisor-Dx4TXTMM.js.map} +1 -1
- package/dist/{run-cancellation-D_4vJ522.js → run-cancellation-DYszFJtQ.js} +4 -4
- package/dist/{run-cancellation-D_4vJ522.js.map → run-cancellation-DYszFJtQ.js.map} +1 -1
- package/dist/{run-state-CTZVngI_.js → run-state-DHBoJjaF.js} +6 -6
- package/dist/{run-state-CTZVngI_.js.map → run-state-DHBoJjaF.js.map} +1 -1
- package/dist/{runtime-COm9uBBx.js → runtime-CaM5Gbbv.js} +76 -987
- package/dist/runtime-CaM5Gbbv.js.map +1 -0
- package/dist/{sandbox-profile-compat-oFGWESLn.js → sandbox-profile-compat-l-SLQnGb.js} +2 -2
- package/dist/{sandbox-profile-compat-oFGWESLn.js.map → sandbox-profile-compat-l-SLQnGb.js.map} +1 -1
- package/dist/{search-executor-DTEIl58w.js → search-executor-B9L8W_1O.js} +17 -46
- package/dist/search-executor-B9L8W_1O.js.map +1 -0
- package/dist/{server-zO9Nz9Nk.js → server-BlDV8sj_.js} +32 -8
- package/dist/{server-zO9Nz9Nk.js.map → server-BlDV8sj_.js.map} +1 -1
- package/dist/{settle-record-U7fkQEZa.js → settle-record-WWQasnDq.js} +12 -4
- package/dist/settle-record-WWQasnDq.js.map +1 -0
- package/dist/{snapshot-Cgj2Wzi6.js → snapshot-BKI_w3JD.js} +19 -5
- package/dist/snapshot-BKI_w3JD.js.map +1 -0
- package/dist/{structural-rollout-BwTwTUXF.js → structural-rollout-C1xxoWcN.js} +39 -57
- package/dist/structural-rollout-C1xxoWcN.js.map +1 -0
- package/dist/{supervise-tmqDU6YT.js → supervise-DWiSgPLB.js} +103 -111
- package/dist/supervise-DWiSgPLB.js.map +1 -0
- package/dist/{supervision-discovery-CGbXayGv.js → supervision-discovery-C9kfBLKy.js} +4 -4
- package/dist/{supervision-discovery-CGbXayGv.js.map → supervision-discovery-C9kfBLKy.js.map} +1 -1
- package/dist/{supervisor-1A27fE6k.js → supervisor-Dpeu7rnZ.js} +209 -999
- package/dist/supervisor-Dpeu7rnZ.js.map +1 -0
- package/dist/{scope-gIYQ5ZiL.js → supervisor-accounting-Bw4MJk-b.js} +755 -458
- package/dist/supervisor-accounting-Bw4MJk-b.js.map +1 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +13 -13
- package/dist/{tool-server-B9q2dtnW.js → tool-server-DYwRbi0I.js} +3 -3
- package/dist/{tool-server-B9q2dtnW.js.map → tool-server-DYwRbi0I.js.map} +1 -1
- package/dist/{top-web-CS4PD4ck.js → top-web-B3HOAtrZ.js} +9 -7
- package/dist/{top-web-CS4PD4ck.js.map → top-web-B3HOAtrZ.js.map} +1 -1
- package/dist/tui/bin.js +6 -6
- package/dist/tui/bin.js.map +1 -1
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +2 -2
- package/dist/{types-w3Oo1uRJ.d.ts → types-C_qbnw9N.d.ts} +1263 -1497
- package/dist/{util-C_cdPNC4.js → util-fcckrkKp.js} +39 -113
- package/dist/util-fcckrkKp.js.map +1 -0
- package/dist/{workspace-streams-B7yNMahl.js → workspace-streams-DA9R3j-p.js} +2 -2
- package/dist/{workspace-streams-B7yNMahl.js.map → workspace-streams-DA9R3j-p.js.map} +1 -1
- package/package.json +1 -1
- package/dist/coordination-manager-D9z7To7p.js.map +0 -1
- package/dist/coordination-map-routing-CKRNZSaV.js.map +0 -1
- package/dist/errors-AzSuGI23.js.map +0 -1
- package/dist/failure-cause-BzjUUsRz.js.map +0 -1
- package/dist/graph-2_nX4ib4.js +0 -579
- package/dist/graph-2_nX4ib4.js.map +0 -1
- package/dist/graph-registry-7X2nrreR.js.map +0 -1
- package/dist/improve-BxfqTsZy.js.map +0 -1
- package/dist/model-policy-JOcg3NRs.js.map +0 -1
- package/dist/runtime-COm9uBBx.js.map +0 -1
- package/dist/scope-gIYQ5ZiL.js.map +0 -1
- package/dist/search-executor-DTEIl58w.js.map +0 -1
- package/dist/settle-record-U7fkQEZa.js.map +0 -1
- package/dist/snapshot-Cgj2Wzi6.js.map +0 -1
- package/dist/structural-rollout-BwTwTUXF.js.map +0 -1
- package/dist/supervise-tmqDU6YT.js.map +0 -1
- package/dist/supervisor-1A27fE6k.js.map +0 -1
- package/dist/util-C_cdPNC4.js.map +0 -1
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as ConfigError, m as ValidationError, n as AnalystError, s as PlannerError } from "./errors-
|
|
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-
|
|
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-
|
|
5
|
-
import { M as contentAddress,
|
|
6
|
-
import { n as detachedSnapshot, t as detachedFrozen } from "./snapshot-
|
|
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-
|
|
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-
|
|
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,
|
|
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-
|
|
14
|
-
import { y as foldCoordinationRecords } from "./run-cancellation-
|
|
15
|
-
import { a as downMessageRefusalReasons, p as createInMemoryRunContext } from "./coordination-manager-
|
|
16
|
-
import { M as kernelPromptRegistry, S as validateFlowGraph, T as validateGraphTopology, h as runBrainLoop, p as coordinationControlToolNames, t as supervise } from "./supervise-
|
|
17
|
-
import { _ as compileGraphParameters, a as graphRegistry, b as
|
|
18
|
-
import "./run-state-
|
|
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-
|
|
21
|
-
import "./delegate-
|
|
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 {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
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 {
|
|
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-
|
|
9556
|
+
//# sourceMappingURL=runtime-CaM5Gbbv.js.map
|