@sema-agent/core 7.23.1 → 7.23.3
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 +40 -0
- package/dist/agents/agent-transcript-tool.js +2 -1
- package/dist/agents/cascade.js +2 -1
- package/dist/agents/list-agents-tool.js +2 -1
- package/dist/agents/observer.js +3 -2
- package/dist/agents/peer-session-drain.js +2 -1
- package/dist/agents/retain-ledger.js +2 -1
- package/dist/agents/send-message-tool.js +11 -10
- package/dist/agents/subagent.js +18 -17
- package/dist/agents/teacher.js +5 -4
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +2 -1
- package/dist/brain/errors.js +24 -15
- package/dist/brain/failover.js +2 -1
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.js +4 -3
- package/dist/core/artifact-host.js +2 -1
- package/dist/core/ask-question.js +3 -2
- package/dist/core/auto-promote.js +3 -2
- package/dist/core/gate-bounded-await.js +2 -1
- package/dist/core/gate-fold.js +4 -1
- package/dist/core/gate-lanes.js +4 -2
- package/dist/core/git-worktree-env.js +6 -5
- package/dist/core/hooks.js +4 -4
- package/dist/core/lsp-session.js +3 -2
- package/dist/core/mcp.js +4 -3
- package/dist/core/memory-admission.js +2 -1
- package/dist/core/memory-engine/consolidation-driver.js +5 -4
- package/dist/core/memory-engine/consolidation.js +2 -1
- package/dist/core/memory-engine/delegation-settlement.js +4 -3
- package/dist/core/memory-engine/distiller.js +8 -4
- package/dist/core/memory-engine/engine.js +63 -55
- package/dist/core/memory-engine/file-backend.js +11 -10
- package/dist/core/memory-engine/layout.js +5 -4
- package/dist/core/memory-engine/session-incarnation.js +2 -1
- package/dist/core/memory.js +2 -1
- package/dist/core/oracle-isolation.js +3 -2
- package/dist/core/permission-rule-store.js +2 -1
- package/dist/core/runner/announce-once-ledger.js +2 -1
- package/dist/core/runner/compaction-seams.js +3 -2
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/git-leg-delivery.js +4 -3
- package/dist/core/runner/git-status-frame.js +2 -1
- package/dist/core/runner/mcp-redial.js +2 -1
- package/dist/core/runner/memory-consolidation-driver.js +2 -1
- package/dist/core/runner/permission-rule-lanes.js +3 -2
- package/dist/core/runner/prepare-artifact.js +2 -1
- package/dist/core/runner/prepare-ask-lane.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +4 -3
- package/dist/core/runner/prepare-inherited-gate.js +2 -1
- package/dist/core/runner/prepare-memory-engine-session.js +2 -1
- package/dist/core/runner/prepare-memory.js +5 -4
- package/dist/core/runner/prepare-policy-chain.js +3 -2
- package/dist/core/runner/prepare-project-context.js +3 -2
- package/dist/core/runner/prepare-protocol-tools.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.js +2 -1
- package/dist/core/runner/prepare-task.js +6 -5
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/resume-apply.js +5 -4
- package/dist/core/runner/resume-preflight.js +3 -2
- package/dist/core/runner/run-git-lane.js +4 -3
- package/dist/core/runner/run-leg.js +4 -3
- package/dist/core/runner/run-recovery-lanes.js +3 -2
- package/dist/core/runner/run-settle-and-teardown.js +2 -1
- package/dist/core/runner/run-stop-and-final-verify.js +2 -1
- package/dist/core/runner/run-telemetry-and-budget-seats.js +2 -1
- package/dist/core/runner/run-terminal-adoption.js +6 -5
- package/dist/core/runner/run-turn-boundary.js +5 -4
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner/stream-settle-backstop.js +5 -4
- package/dist/core/runner/stream-steer-verb.js +2 -1
- package/dist/core/runner/strict-output-schema.js +2 -1
- package/dist/core/runner/teardown-bounded.js +2 -1
- package/dist/core/safe-notify.js +2 -1
- package/dist/core/skills-directory.js +2 -1
- package/dist/core/strategy-store.js +2 -1
- package/dist/core/task-registry-agent.js +2 -1
- package/dist/core/task-registry-monitor.js +2 -1
- package/dist/core/task-registry-workflow.js +2 -1
- package/dist/core/task-registry.js +3 -2
- package/dist/core/thrown-value.d.ts +79 -0
- package/dist/core/thrown-value.js +56 -0
- package/dist/core/tool-errors.d.ts +0 -6
- package/dist/core/tool-errors.js +36 -14
- package/dist/core/tool-model-gate.js +2 -1
- package/dist/core/tool-policy.js +6 -13
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/tool-spec.d.ts +12 -0
- package/dist/core/write-protect.js +3 -2
- package/dist/engine/compaction/compaction.js +3 -4
- package/dist/engine/harness/agent-harness.js +2 -1
- package/dist/engine/harness/types.js +8 -7
- package/dist/engine/loop/agent-loop.js +7 -6
- package/dist/engine/lsp/node-lsp-manager.js +5 -4
- package/dist/engine/lsp/stdio-lsp-transport.js +12 -5
- package/dist/orchestration/dev-vm-script-runner.js +2 -1
- package/dist/orchestration/run-workflow-tool.js +7 -6
- package/dist/orchestration/workflow-governance.js +2 -1
- package/dist/orchestration/workflow-sandbox-conformance.js +3 -2
- package/dist/orchestration/workflow-script-store.js +2 -1
- package/dist/orchestration/workflow.js +15 -14
- package/dist/server/http.js +3 -2
- package/dist/stores/file/adoption/adopt.js +3 -2
- package/dist/stores/file/fs-atomic.js +2 -1
- package/dist/stores/file/permission-rule-adopt.js +2 -1
- package/dist/stores/file/permission-rule-store.js +2 -1
- package/dist/stores/file/session-store.js +4 -3
- package/dist/stores/file/strategy-store.js +7 -6
- package/dist/tools/artifact/artifact-tool.js +3 -2
- package/dist/tools/artifact/conformance.js +4 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -9
- package/dist/tools/fs/bash-readonly-classifier.js +7 -3
- package/dist/tools/fs/fs-bash.d.ts +16 -10
- package/dist/tools/fs/fs-bash.js +84 -35
- package/dist/tools/fs/fs-write.js +3 -2
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/fs/search.js +3 -2
- package/dist/tools/monitor.js +3 -2
- package/dist/tools/sql.js +2 -1
- package/dist/tools/web.js +5 -15
- package/dist/tools/worktree.js +12 -9
- package/package.json +2 -1
- package/wire-changes/core@7.23.2.json +42 -0
- package/wire-changes/core@7.23.3.json +25 -0
|
@@ -23,6 +23,7 @@ import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
|
|
|
23
23
|
import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentParkedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
|
|
24
24
|
import { WorkflowScriptError } from "./workflow-meta.js";
|
|
25
25
|
import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS } from "../brain/timeout.js";
|
|
26
|
+
import { asThrownError, thrownText } from "../core/thrown-value.js";
|
|
26
27
|
export * from "./workflow-types.js";
|
|
27
28
|
const MAX_TRANSCRIPT_CHARS = 4000;
|
|
28
29
|
const WORKFLOW_RESULT_MAX = 4000;
|
|
@@ -193,7 +194,7 @@ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
|
|
|
193
194
|
}
|
|
194
195
|
rethrowIfMaxAgents(s.e);
|
|
195
196
|
errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
|
|
196
|
-
emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(
|
|
197
|
+
emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(thrownText(s.e), 300)}`);
|
|
197
198
|
return null;
|
|
198
199
|
});
|
|
199
200
|
if (dropped > 0)
|
|
@@ -203,7 +204,7 @@ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
|
|
|
203
204
|
function workflowFanOutSlotError(index, e) {
|
|
204
205
|
const code = e?.code;
|
|
205
206
|
const kind = typeof code === "string" && code.length > 0 ? code : e instanceof Error ? e.name : "Error";
|
|
206
|
-
return { index, kind, message: boundedRedactedSummary(
|
|
207
|
+
return { index, kind, message: boundedRedactedSummary(thrownText(e), 300) };
|
|
207
208
|
}
|
|
208
209
|
export class WorkflowResultTooLargeError extends Error {
|
|
209
210
|
size;
|
|
@@ -841,7 +842,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
841
842
|
};
|
|
842
843
|
const parkSeat = opts.checkpointStore;
|
|
843
844
|
const parkedByOrdinal = [];
|
|
844
|
-
const errText = (err) => (
|
|
845
|
+
const errText = (err) => (thrownText(err));
|
|
845
846
|
const withoutToken = (text, token) => (token === "" ? text : text.split(token).join("<token>"));
|
|
846
847
|
const parkBindingBroken = (ordinal, why) => new WorkflowParkRefusal("workflow.park_binding_broken", `workflow resume: ordinal ${ordinal}'s park cannot be bound to its checkpoint (${why}) — the recorded park is inconsistent. Decide the checkpoint through the deployment's approval channel and start a fresh run.`, { ordinal });
|
|
847
848
|
const readParkTruth = async (seat, key, ordinal) => {
|
|
@@ -980,7 +981,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
980
981
|
}
|
|
981
982
|
catch (err) {
|
|
982
983
|
if (dbg)
|
|
983
|
-
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${
|
|
984
|
+
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${thrownText(err)}`);
|
|
984
985
|
}
|
|
985
986
|
return;
|
|
986
987
|
}
|
|
@@ -993,7 +994,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
993
994
|
}
|
|
994
995
|
catch (err) {
|
|
995
996
|
if (dbg)
|
|
996
|
-
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} APPEND-ERROR ${
|
|
997
|
+
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} APPEND-ERROR ${thrownText(err)}`);
|
|
997
998
|
throw err;
|
|
998
999
|
}
|
|
999
1000
|
};
|
|
@@ -1271,7 +1272,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1271
1272
|
inheritedModelSnap = opts.defaultModel?.();
|
|
1272
1273
|
}
|
|
1273
1274
|
catch (err) {
|
|
1274
|
-
inheritedModelError =
|
|
1275
|
+
inheritedModelError = asThrownError(err);
|
|
1275
1276
|
}
|
|
1276
1277
|
}
|
|
1277
1278
|
const inheritedThinkingSnap = spec.thinking === undefined ? opts.defaultThinking?.() : undefined;
|
|
@@ -1781,17 +1782,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1781
1782
|
const journaled = journalAppend(callKey, salvaged ?? {
|
|
1782
1783
|
taskId: callKey,
|
|
1783
1784
|
sessionId: "",
|
|
1784
|
-
result: boundedRedactedSummary(
|
|
1785
|
+
result: boundedRedactedSummary(thrownText(err), 500),
|
|
1785
1786
|
terminal: {
|
|
1786
1787
|
kind: "failed",
|
|
1787
1788
|
...(err instanceof WorkflowAgentBlockedError ? { code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : err instanceof WorkflowBudgetExceededError ? { code: err.code } : {}),
|
|
1788
|
-
message: boundedRedactedSummary(
|
|
1789
|
+
message: boundedRedactedSummary(thrownText(err), 500),
|
|
1789
1790
|
},
|
|
1790
1791
|
stats: { turns: 0, tokens: 0, costMicroUsd: 0, usageMissing: true },
|
|
1791
1792
|
}, label).catch(() => undefined);
|
|
1792
1793
|
if (!((err instanceof WorkflowBudgetExceededError || err instanceof WorkflowAgentParkedError) && rec.startedAt === undefined))
|
|
1793
1794
|
await journaled;
|
|
1794
|
-
bceTerminal(callKey, "failed", rec.output ?? (
|
|
1795
|
+
bceTerminal(callKey, "failed", rec.output ?? (thrownText(err)), rec.sessionId, rec.stats);
|
|
1795
1796
|
}
|
|
1796
1797
|
throw err;
|
|
1797
1798
|
}
|
|
@@ -1969,7 +1970,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1969
1970
|
}
|
|
1970
1971
|
recordFailed();
|
|
1971
1972
|
releaseLeg();
|
|
1972
|
-
bceTerminal(callKey, "failed",
|
|
1973
|
+
bceTerminal(callKey, "failed", thrownText(err), undefined, undefined);
|
|
1973
1974
|
throw err;
|
|
1974
1975
|
}
|
|
1975
1976
|
if (childSessionId !== undefined && !finalized) {
|
|
@@ -2012,11 +2013,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
2012
2013
|
void journalAppend(callKey, {
|
|
2013
2014
|
taskId: callKey,
|
|
2014
2015
|
sessionId: "",
|
|
2015
|
-
terminal: { kind: "failed", message: boundedRedactedSummary(
|
|
2016
|
-
result: boundedRedactedSummary(
|
|
2016
|
+
terminal: { kind: "failed", message: boundedRedactedSummary(thrownText(err), 500) },
|
|
2017
|
+
result: boundedRedactedSummary(thrownText(err), 500),
|
|
2017
2018
|
stats: { turns: partialSpend?.turns ?? 0, tokens: partialSpend?.tokens ?? 0, costMicroUsd: partialSpend?.costMicroUsd ?? 0 },
|
|
2018
2019
|
}).catch(() => undefined);
|
|
2019
|
-
bceTerminal(callKey, "failed",
|
|
2020
|
+
bceTerminal(callKey, "failed", thrownText(err), undefined, rec.stats);
|
|
2020
2021
|
throw err;
|
|
2021
2022
|
}
|
|
2022
2023
|
rollbackUsageBeat();
|
|
@@ -2377,7 +2378,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
2377
2378
|
run.endedAt = now();
|
|
2378
2379
|
if (parkedLeg !== undefined && !(err instanceof WorkflowAgentParkedError))
|
|
2379
2380
|
err = parkedError();
|
|
2380
|
-
run.error =
|
|
2381
|
+
run.error = thrownText(err);
|
|
2381
2382
|
closeAbandonedAgents(run.endedAt);
|
|
2382
2383
|
{
|
|
2383
2384
|
let thrownCode;
|
package/dist/server/http.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import { thrownText } from "../core/thrown-value.js";
|
|
2
3
|
const DEFAULT_MAX_BODY = 5 * 1024 * 1024;
|
|
3
4
|
class HttpError extends Error {
|
|
4
5
|
status;
|
|
@@ -25,7 +26,7 @@ async function readJson(req, maxBytes) {
|
|
|
25
26
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
26
27
|
}
|
|
27
28
|
catch (e) {
|
|
28
|
-
throw new HttpError(400, `invalid JSON request body: ${
|
|
29
|
+
throw new HttpError(400, `invalid JSON request body: ${thrownText(e)}`);
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
function setCors(res, origin) {
|
|
@@ -47,7 +48,7 @@ export function createTaskServer(opts) {
|
|
|
47
48
|
void handle(req, res).catch((err) => {
|
|
48
49
|
if (!res.headersSent) {
|
|
49
50
|
const status = err instanceof HttpError ? err.status : 500;
|
|
50
|
-
sendJson(res, status, { error:
|
|
51
|
+
sendJson(res, status, { error: thrownText(err) });
|
|
51
52
|
}
|
|
52
53
|
else {
|
|
53
54
|
res.end();
|
|
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { adoptFilePermissionRuleStore } from "../permission-rule-adopt.js";
|
|
5
5
|
import { BootLock, atomicWriteFile, ensureDir, sanitizeScope } from "../fs-atomic.js";
|
|
6
|
+
import { thrownText } from "../../../core/thrown-value.js";
|
|
6
7
|
import { AdoptionError, NOT_MIGRATED_BY_DESIGN, readRootAdoptionFile, writeRootAdoptionFile, } from "./marker.js";
|
|
7
8
|
const CONFIG_ACCOUNT_FILE = "adoption-configs.json";
|
|
8
9
|
function baselineConfigs(toPrincipal) {
|
|
@@ -491,7 +492,7 @@ export async function adoptLocalDataRoot(opts) {
|
|
|
491
492
|
catch (err) {
|
|
492
493
|
if (err instanceof AdoptionError)
|
|
493
494
|
throw err;
|
|
494
|
-
throw new AdoptionError("adoption_rule_leg_refused", `the permission-rule leg refused: ${
|
|
495
|
+
throw new AdoptionError("adoption_rule_leg_refused", `the permission-rule leg refused: ${thrownText(err)}`);
|
|
495
496
|
}
|
|
496
497
|
if (nested.status === "stalled") {
|
|
497
498
|
return {
|
|
@@ -533,7 +534,7 @@ export async function adoptLocalDataRoot(opts) {
|
|
|
533
534
|
return {
|
|
534
535
|
status: "stalled",
|
|
535
536
|
phase: 4,
|
|
536
|
-
error: `the "${leg.store}" carriage leg failed: ${
|
|
537
|
+
error: `the "${leg.store}" carriage leg failed: ${thrownText(err)} — resume when it is healthy`,
|
|
537
538
|
};
|
|
538
539
|
}
|
|
539
540
|
if (rows !== undefined && (!Number.isSafeInteger(rows) || rows < 0)) {
|
|
@@ -5,6 +5,7 @@ import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
|
|
8
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
8
9
|
const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
|
|
9
10
|
export function sanitizePathComponent(raw) {
|
|
10
11
|
if (raw === "" || raw === "." || raw === ".." || !SAFE_COMPONENT.test(raw)) {
|
|
@@ -411,7 +412,7 @@ export class BootLock {
|
|
|
411
412
|
}
|
|
412
413
|
catch (err) {
|
|
413
414
|
throw new FileStoreLockError("store.lock_unreadable", `file store: this process still owns the lock file at ${this.lockPath} from an earlier release that ` +
|
|
414
|
-
`could not remove it, and removing it now failed too (${
|
|
415
|
+
`could not remove it, and removing it now failed too (${thrownText(err)}); ` +
|
|
415
416
|
`remove it manually`, this.lockPath, process.pid);
|
|
416
417
|
}
|
|
417
418
|
abandonedLocks.delete(this.abandonKey);
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { writerOf } from "../../core/permission-rule-store.js";
|
|
4
4
|
import { createPermissionRuleStoreProvider } from "../../core/permission-rule-provider.js";
|
|
5
5
|
import { syncPermissionRules } from "../../core/permission-rule-sync.js";
|
|
6
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
6
7
|
import { FileDurableRulePartitionProvider, LOCAL_OWNER_FILE, principalRuleFileName, readRuleAdoptionFile, writeRuleAdoptionFile, } from "./permission-rule-store.js";
|
|
7
8
|
export async function adoptFilePermissionRuleStore(opts) {
|
|
8
9
|
if (typeof opts.toPrincipal !== "string" || opts.toPrincipal === "") {
|
|
@@ -63,7 +64,7 @@ export async function adoptFilePermissionRuleStore(opts) {
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
catch (err) {
|
|
66
|
-
return { status: "stalled", phase: 4, error: `the first sync round failed: ${
|
|
67
|
+
return { status: "stalled", phase: 4, error: `the first sync round failed: ${thrownText(err)}` };
|
|
67
68
|
}
|
|
68
69
|
if (!syncResult.ok) {
|
|
69
70
|
return {
|
|
@@ -6,6 +6,7 @@ import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDel
|
|
|
6
6
|
import { canonicalize } from "../../core/canonical-json.js";
|
|
7
7
|
import { BootLock } from "./fs-atomic.js";
|
|
8
8
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
9
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
9
10
|
function isRuleFile(v) {
|
|
10
11
|
const p = v;
|
|
11
12
|
return typeof p === "object" && p !== null && p.schemaVersion === 4 && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number" && (p.quarantined === undefined || Array.isArray(p.quarantined));
|
|
@@ -139,7 +140,7 @@ class FileDurableRulePartition {
|
|
|
139
140
|
const e = err;
|
|
140
141
|
if (e?.code === "ENOENT")
|
|
141
142
|
return { absent: true };
|
|
142
|
-
const why = `could not read ${this.file}: ${
|
|
143
|
+
const why = `could not read ${this.file}: ${thrownText(err)}; loading zero rules`;
|
|
143
144
|
this.disclose(why);
|
|
144
145
|
return { unreadable: why };
|
|
145
146
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { BaseSessionStorage, StoredSession, SessionError, getEntriesToFork, uuidv7, validateEntriesForImport, } from "../../internal/harness.js";
|
|
4
4
|
import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent } from "./fs-atomic.js";
|
|
5
5
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
6
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
6
7
|
function containSinkThenable(r) {
|
|
7
8
|
if (typeof r?.then === "function") {
|
|
8
9
|
r.then(undefined, () => {
|
|
@@ -171,7 +172,7 @@ export class FileSessionRepo {
|
|
|
171
172
|
}
|
|
172
173
|
catch (err) {
|
|
173
174
|
if (!(err instanceof SessionError && err.code === "not_found")) {
|
|
174
|
-
this.disclose(this.pathFor(id), `session log unreadable (${
|
|
175
|
+
this.disclose(this.pathFor(id), `session log unreadable (${thrownText(err)}) — skipped from the listing`);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
}
|
|
@@ -200,7 +201,7 @@ export class FileSessionRepo {
|
|
|
200
201
|
}
|
|
201
202
|
catch (err) {
|
|
202
203
|
if (!(err instanceof SessionError && err.code === "not_found")) {
|
|
203
|
-
this.disclose(this.pathFor(id), `session log unreadable (${
|
|
204
|
+
this.disclose(this.pathFor(id), `session log unreadable (${thrownText(err)}) — skipped from the placed listing`);
|
|
204
205
|
}
|
|
205
206
|
continue;
|
|
206
207
|
}
|
|
@@ -238,7 +239,7 @@ export class FileSessionRepo {
|
|
|
238
239
|
rmSync(this.pathFor(metadata.id), { force: true });
|
|
239
240
|
}
|
|
240
241
|
catch (err) {
|
|
241
|
-
throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${
|
|
242
|
+
throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${thrownText(err)}`, err instanceof Error ? err : undefined);
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
async fork(sourceMetadata, options = {}) {
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { atomicWriteFile } from "./fs-atomic.js";
|
|
5
5
|
import { createSafeNotifier, observeThenableRejection } from "../../core/safe-notify.js";
|
|
6
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
6
7
|
import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_TEXT_BYTES, compileStrategyQuery, resolveCapacityCap, resolveFindLimit, scoreStoredStrategy, storedStrategyShapeIssue, strategyDedupKey, validateStrategyForWrite, } from "../../core/strategy-store.js";
|
|
7
8
|
function configRefusal(message, code) {
|
|
8
9
|
const e = new Error(message);
|
|
@@ -32,14 +33,14 @@ export class FileStrategyStore {
|
|
|
32
33
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
33
34
|
}
|
|
34
35
|
catch (err) {
|
|
35
|
-
throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${
|
|
36
|
+
throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
|
|
36
37
|
}
|
|
37
38
|
let st;
|
|
38
39
|
try {
|
|
39
40
|
st = statSync(root);
|
|
40
41
|
}
|
|
41
42
|
catch (err) {
|
|
42
|
-
throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${
|
|
43
|
+
throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
|
|
43
44
|
}
|
|
44
45
|
if (!st.isDirectory()) {
|
|
45
46
|
throw configRefusal(`FileStrategyStore: root ${root} exists and is not a directory`, "config.strategy_root_invalid");
|
|
@@ -145,7 +146,7 @@ export class FileStrategyStore {
|
|
|
145
146
|
catch (err) {
|
|
146
147
|
if (err.code === "ENOENT")
|
|
147
148
|
continue;
|
|
148
|
-
this.incident({ op: "find", error: `unstat-able entry skipped: ${
|
|
149
|
+
this.incident({ op: "find", error: `unstat-able entry skipped: ${thrownText(err)}`, path: p });
|
|
149
150
|
continue;
|
|
150
151
|
}
|
|
151
152
|
if (!fst.isFile()) {
|
|
@@ -163,7 +164,7 @@ export class FileStrategyStore {
|
|
|
163
164
|
catch (err) {
|
|
164
165
|
if (err.code === "ENOENT")
|
|
165
166
|
continue;
|
|
166
|
-
this.incident({ op: "find", error: `unreadable entry skipped: ${
|
|
167
|
+
this.incident({ op: "find", error: `unreadable entry skipped: ${thrownText(err)}`, path: p });
|
|
167
168
|
continue;
|
|
168
169
|
}
|
|
169
170
|
let parsed;
|
|
@@ -217,7 +218,7 @@ export class FileStrategyStore {
|
|
|
217
218
|
this.incident({ op: "parse", error: `corrupt entry quarantined (${reason})`, path: p });
|
|
218
219
|
}
|
|
219
220
|
catch (err) {
|
|
220
|
-
this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${
|
|
221
|
+
this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${thrownText(err)}`, path: p });
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
writeEntry(dir, path, entry) {
|
|
@@ -286,7 +287,7 @@ export class FileStrategyStore {
|
|
|
286
287
|
catch (err) {
|
|
287
288
|
if (err.code === "ENOENT")
|
|
288
289
|
continue;
|
|
289
|
-
this.incident({ op: "evict", error:
|
|
290
|
+
this.incident({ op: "evict", error: thrownText(err), id: le.entry.id, path: le.path });
|
|
290
291
|
}
|
|
291
292
|
}
|
|
292
293
|
}
|
|
@@ -7,6 +7,7 @@ import { errorResult } from "../../core/tools.js";
|
|
|
7
7
|
import { inlineUntrusted } from "../../core/untrusted-text.js";
|
|
8
8
|
import { actionDescription, artifactDescription, PARAM_TEXT, urlDescription } from "./artifact-text.js";
|
|
9
9
|
import { ARTIFACT_REJECT_BREAKER_THRESHOLD, artifactRejectBannerFor, artifactRejectKeyOf, createArtifactRejectBreaker } from "./reject-breaker.js";
|
|
10
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
10
11
|
export const MAX_FILES_PER_PUBLISH = 255;
|
|
11
12
|
const LIST_LIMIT_DEFAULT = 25;
|
|
12
13
|
const LIST_LIMIT_MAX = 100;
|
|
@@ -93,10 +94,10 @@ function callHost(action, fn, signal) {
|
|
|
93
94
|
}
|
|
94
95
|
catch (err) {
|
|
95
96
|
signal?.removeEventListener("abort", onAbort);
|
|
96
|
-
resolve({ ok: false, kind: "threw", message:
|
|
97
|
+
resolve({ ok: false, kind: "threw", message: thrownText(err) });
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
|
-
p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message:
|
|
100
|
+
p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message: thrownText(err) })).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
function buildSchema(caps) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { thrownText } from "../../core/thrown-value.js";
|
|
2
3
|
import { readArtifactHostCapabilities, } from "../../core/artifact-host.js";
|
|
3
4
|
class Violation extends Error {
|
|
4
5
|
}
|
|
@@ -25,7 +26,7 @@ async function publish(h, r, attemptId) {
|
|
|
25
26
|
return await h.host.publish(r, ctxOf(h, attemptId));
|
|
26
27
|
}
|
|
27
28
|
catch (err) {
|
|
28
|
-
return fail(`publish threw: ${
|
|
29
|
+
return fail(`publish threw: ${thrownText(err)}`);
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
async function publishOk(h, r, what, attemptId) {
|
|
@@ -282,7 +283,7 @@ define({ id: "read.unknown_url_typed", describe: "`read` of a url this host neve
|
|
|
282
283
|
out = await h.host.read({ url: FOREIGN_URL }, ctxOf(h));
|
|
283
284
|
}
|
|
284
285
|
catch (err) {
|
|
285
|
-
return fail(`read threw: ${
|
|
286
|
+
return fail(`read threw: ${thrownText(err)}`);
|
|
286
287
|
}
|
|
287
288
|
check(!out.ok && (out.code === "not_found" || out.code === "forbidden"), `an unknown url answered ${out.ok ? "ok" : out.code}`);
|
|
288
289
|
return out.ok ? "" : out.code;
|
|
@@ -340,7 +341,7 @@ export async function runArtifactConformance(host, opts = {}) {
|
|
|
340
341
|
row = { id: v.id, status: "pass", detail: await v.run(h) };
|
|
341
342
|
}
|
|
342
343
|
catch (err) {
|
|
343
|
-
row = { id: v.id, status: "fail", detail: err instanceof Violation ? err.message : `threw: ${
|
|
344
|
+
row = { id: v.id, status: "fail", detail: err instanceof Violation ? err.message : `threw: ${thrownText(err)}` };
|
|
344
345
|
}
|
|
345
346
|
results.push(row);
|
|
346
347
|
}
|
|
@@ -284,10 +284,12 @@ export interface BashReadonlyRootBoundary {
|
|
|
284
284
|
* undecided contract. What REMAINS of ① is the form table's stated open set (see the KNOWN OPEN
|
|
285
285
|
* SET note at {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment
|
|
286
286
|
* allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
|
|
287
|
-
* ② LEXICAL only — an in-root symlink whose target is a
|
|
288
|
-
* guarded path reads as its innocent spelling
|
|
289
|
-
*
|
|
290
|
-
*
|
|
287
|
+
* ② LEXICAL only HERE — an in-root symlink whose target is a
|
|
288
|
+
* guarded path reads as its innocent spelling at this seat, exactly as it does for the containment half,
|
|
289
|
+
* because the classify auto-allow lane has no I/O seat by contract. The CANONICAL view is judged by the
|
|
290
|
+
* full shell's execute-time recheck, which asks THIS judge (the same function object, forwarded) where
|
|
291
|
+
* each candidate really lands — a `checkedPaths` literal and a `undecidedPaths` pattern's expansions
|
|
292
|
+
* alike. The `bash_readonly` face wires no deny seat at all, so its own recheck carries nothing. The structured read faces judge BOTH
|
|
291
293
|
* views (canonical + lexical); what this seat is and is not is stated once, at
|
|
292
294
|
* `shell.read_boundary.friction_floor`. The `bash_readonly` face deliberately does NOT wire this seat (its
|
|
293
295
|
* roots containment + command allowlist double gate is the deployment's own read-safety
|
|
@@ -527,7 +529,16 @@ export declare function resolveOperandSpellings(base: string | undefined, operan
|
|
|
527
529
|
export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary | undefined,
|
|
528
530
|
/** The {@link ShellScanDecline} members THIS SEAT reads as evidence (tables and argument: `fs-bash.ts`); one
|
|
529
531
|
* the table omits ends the scan with "exempt". */
|
|
530
|
-
declinesAreEvidence: ReadonlySet<ShellScanDecline>):
|
|
532
|
+
declinesAreEvidence: ReadonlySet<ShellScanDecline>): RefusedCommandScan;
|
|
533
|
+
/** What ONE scan of a refused command found — TWO facts, because a seat's table answers only the first. `mayRead`
|
|
534
|
+
* is the whitelist's verdict ("may read a file the boundary never judged"), which a seat turns into a MANDATE.
|
|
535
|
+
* `declined` is the scan's own report that it could not judge where a segment reads AT ALL
|
|
536
|
+
* ({@link SHELL_SCAN_DECLINES}), recorded BEFORE the seat's table and so the SAME on every seat: a seat omitting
|
|
537
|
+
* that decline has ruled only that an ANSWERER may retire the ask, never that nothing happened. */
|
|
538
|
+
export interface RefusedCommandScan {
|
|
539
|
+
readonly mayRead: boolean;
|
|
540
|
+
readonly declined: boolean;
|
|
541
|
+
}
|
|
531
542
|
/**
|
|
532
543
|
* The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
|
|
533
544
|
* command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
|
|
@@ -645,10 +656,10 @@ export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
|
|
|
645
656
|
* base across segments) and dropping it would place every later relative operand against a base bash
|
|
646
657
|
* has already left. A move the walk cannot model (`pushd`/`popd`) is not silently tolerated either —
|
|
647
658
|
* see the `paths`-less gated answer below.
|
|
648
|
-
* ·
|
|
649
|
-
*
|
|
650
|
-
* deny-listed operand HIDE the fact that it also sits outside the roots
|
|
651
|
-
*
|
|
659
|
+
* · BOTH halves from ONE extraction, in TWO passes: the CONTAINMENT pass alone is deny-free, because
|
|
660
|
+
* {@link BashReadonlyRootBoundary.denyMatch} outranks containment inside the walk and returns early,
|
|
661
|
+
* which would let a deny-listed operand HIDE the fact that it also sits outside the roots. A SECOND
|
|
662
|
+
* pass over the SAME readers does carry that judge, and is what answers `denied` (see below).
|
|
652
663
|
* · `face: "open"` is honoured: under the open read face containment is structurally satisfied, an
|
|
653
664
|
* out-of-roots operand is not a finding, and this gate is silent — a full-read deployment sees no
|
|
654
665
|
* gate here, by the same one mechanism the face already defines.
|
|
@@ -257,7 +257,9 @@ function tokensMoveBase(toks) {
|
|
|
257
257
|
}
|
|
258
258
|
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
259
259
|
export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
|
|
260
|
-
|
|
260
|
+
const scan = { declined: false };
|
|
261
|
+
const exempt = refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, 0);
|
|
262
|
+
return { mayRead: !exempt, declined: scan.declined };
|
|
261
263
|
}
|
|
262
264
|
const STDIN_ONLY_READER_FLAGS = new Map([
|
|
263
265
|
["cat", /^-[AbeEnstTuv]+$/],
|
|
@@ -279,7 +281,7 @@ function whitelistSegment(segment) {
|
|
|
279
281
|
redirects: tokens.filter((t) => t.kind === "redirect"),
|
|
280
282
|
};
|
|
281
283
|
}
|
|
282
|
-
function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, depth, baseMovedOutside = false) {
|
|
284
|
+
function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, depth, baseMovedOutside = false) {
|
|
283
285
|
if (depth > 4)
|
|
284
286
|
return false;
|
|
285
287
|
const lexed = lexShellText(command, { mode: "lenient" });
|
|
@@ -305,6 +307,8 @@ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, dep
|
|
|
305
307
|
if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE))
|
|
306
308
|
return false;
|
|
307
309
|
const decline = shellScanDeclineOf(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED);
|
|
310
|
+
if (decline !== undefined)
|
|
311
|
+
scan.declined = true;
|
|
308
312
|
if (decline !== undefined && declinesAreEvidence.has(decline))
|
|
309
313
|
return false;
|
|
310
314
|
declined ??= decline;
|
|
@@ -322,7 +326,7 @@ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, dep
|
|
|
322
326
|
(walked.undecidedPaths?.length ?? 0) === 0 && (walked.unresolvedOperands?.length ?? 0) === 0 && (walked.recursiveReadPaths?.length ?? 0) === 0;
|
|
323
327
|
if (fullyGreen)
|
|
324
328
|
continue;
|
|
325
|
-
if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, depth + 1, baseMoved))
|
|
329
|
+
if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, scan, depth + 1, baseMoved))
|
|
326
330
|
return false;
|
|
327
331
|
}
|
|
328
332
|
const stdinFileJudgedInside = (word) => {
|
|
@@ -68,16 +68,22 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
|
|
|
68
68
|
* The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
|
|
69
69
|
* is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
|
|
70
70
|
* xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
|
|
71
|
-
* SHAPE classifier, which refuses
|
|
72
|
-
* may retire
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
71
|
+
* SHAPE classifier, which refuses every one of these shapes: what survives is a CARD, and the only question
|
|
72
|
+
* this table answers is who may retire it. It keeps NEITHER — an ANSWERER may (a person, the auto-mode
|
|
73
|
+
* classifier, a person's stored allow rule) — empty by the argument, not by omission.
|
|
74
|
+
*
|
|
75
|
+
* WHAT AN EMPTY TABLE DOES NOT BUY, corrected where it was once claimed: "the shape classifier refuses these
|
|
76
|
+
* shapes anyway, so the only question left is the stored answer" was true of the PROBE and false of the OUTCOME
|
|
77
|
+
* — the allow layer's other members are not that classifier, and both cleared exactly these cards at ZERO asks. An
|
|
78
|
+
* empty table says only "no MANDATE HERE"; that the boundary DECLINED is a separate fact, carried on every ask this
|
|
79
|
+
* seat raises ({@link BoundaryAnswer}'s weaker rung), standing every ANSWERLESS arm down while every answerer's lane stays open.
|
|
80
|
+
* THE COST, stated rather than mitigated: once that ask is retired BY AN ANSWERER, an out-of-root or deny-listed
|
|
81
|
+
* read HIDDEN inside such a command is not asked again. A command's words are not the argv its programs receive (a
|
|
82
|
+
* stream is re-split by its consumer, an unread option may move the working directory first, stdin text is a
|
|
83
|
+
* command line nobody parsed), so a scan of them cannot be the guarantee it was claimed to be. "A read the boundary
|
|
84
|
+
* objects to is never retired by a stored rule" does NOT hold on the classify seat for this class, and holds
|
|
85
|
+
* unchanged elsewhere. One word-level check survives on both seats and is not part of the trade: a token the scan
|
|
86
|
+
* DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
|
|
81
87
|
*/
|
|
82
88
|
export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
|
|
83
89
|
/** The classify doctrine's table — see above; empty by the argument. */
|