@sema-agent/core 7.23.0 → 7.23.2
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 +64 -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.d.ts +1 -1
- package/dist/core/gate-bounded-await.js +3 -2
- package/dist/core/gate-crash-trace.d.ts +7 -0
- package/dist/core/gate-crash-trace.js +3 -0
- package/dist/core/gate-fold.js +6 -2
- package/dist/core/gate-lanes.js +6 -3
- package/dist/core/git-worktree-env.js +6 -5
- package/dist/core/hooks.js +6 -5
- 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 -5
- package/dist/core/runner/gate-exit.js +0 -3
- 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 +4 -4
- 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-lexer.d.ts +12 -5
- package/dist/tools/fs/bash-lexer.js +5 -2
- package/dist/tools/fs/bash-program-position.d.ts +18 -0
- package/dist/tools/fs/bash-program-position.js +13 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +23 -7
- package/dist/tools/fs/bash-readonly-classifier.js +74 -41
- package/dist/tools/fs/fs-bash.d.ts +28 -0
- package/dist/tools/fs/fs-bash.js +31 -13
- 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.1.json +205 -0
- package/wire-changes/core@7.23.2.json +42 -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
|
}
|
|
@@ -92,20 +92,27 @@ export interface SegmentTokens {
|
|
|
92
92
|
/** What a substitution leaves in a word's FOLDED spelling: a marker no argv word can spell, so a consumer that
|
|
93
93
|
* judges words sees "this word carries text the shell computes" rather than the computing text itself. */
|
|
94
94
|
export declare const SUBSTITUTION_PLACEHOLDER = " subst";
|
|
95
|
+
export type QuoteOpen = '"' | "'" | undefined;
|
|
95
96
|
/**
|
|
96
|
-
* Which
|
|
97
|
-
* quote characters themselves
|
|
97
|
+
* Which QUOTE holds each character of `s`, by the same pairing rule the lexer walks with, and the boolean view
|
|
98
|
+
* of it. The pairing quote characters themselves belong to the region they bound — they are not operators
|
|
99
|
+
* either way — so `openedBy` names that region's quote character at their own index too.
|
|
100
|
+
*
|
|
101
|
+
* `quoted` is a VIEW of `openedBy`, minted in the same step: a consumer asking only "is this inside quotes"
|
|
102
|
+
* reads it, one asking WHICH quote (single quotes suppress every expansion, double quotes suppress neither `$`
|
|
103
|
+
* nor the backtick) reads `openedBy`, and the two cannot drift.
|
|
98
104
|
*
|
|
99
105
|
* `balanced` is false when a region is still open at the end of the string. Callers must then NOT use the mask:
|
|
100
106
|
* an unterminated quote is a string bash itself cannot parse, and every consumer falls back to the quote-BLIND
|
|
101
107
|
* reading, which is the stricter one.
|
|
102
108
|
*
|
|
103
109
|
* EXACTNESS. This models bash only for the residual character space the strict faces operate in, where backslash
|
|
104
|
-
* escapes
|
|
105
|
-
*
|
|
106
|
-
*
|
|
110
|
+
* escapes and subshells are rejected outright — there `'` and `"` pairing is the whole of quoting, so a region
|
|
111
|
+
* computed here is exactly a region bash quotes. A caller deciding about `$` or a backtick on this mask owes
|
|
112
|
+
* the argument in `bash-readonly-classifier.ts`. It is NOT a general shell quoting parser.
|
|
107
113
|
*/
|
|
108
114
|
export declare function quoteMask(s: string): {
|
|
115
|
+
openedBy: readonly QuoteOpen[];
|
|
109
116
|
quoted: readonly boolean[];
|
|
110
117
|
balanced: boolean;
|
|
111
118
|
};
|
|
@@ -5,14 +5,17 @@ function quoteStep(open, ch) {
|
|
|
5
5
|
return open === ch ? undefined : open;
|
|
6
6
|
}
|
|
7
7
|
export function quoteMask(s) {
|
|
8
|
+
const openedBy = new Array(s.length).fill(undefined);
|
|
8
9
|
const quoted = new Array(s.length).fill(false);
|
|
9
10
|
let open;
|
|
10
11
|
for (let i = 0; i < s.length; i++) {
|
|
11
12
|
const before = open;
|
|
12
13
|
open = quoteStep(open, s[i]);
|
|
13
|
-
|
|
14
|
+
const holds = before ?? open;
|
|
15
|
+
openedBy[i] = holds;
|
|
16
|
+
quoted[i] = holds !== undefined;
|
|
14
17
|
}
|
|
15
|
-
return { quoted, balanced: open === undefined };
|
|
18
|
+
return { openedBy, quoted, balanced: open === undefined };
|
|
16
19
|
}
|
|
17
20
|
export function literalTildeSpelling(word) {
|
|
18
21
|
return word.startsWith("~") ? `./${word}` : word;
|
|
@@ -114,6 +114,9 @@ export type ProgramPositionRefusal = {
|
|
|
114
114
|
readonly kind: "control_syntax_at_program_position";
|
|
115
115
|
readonly owner: string;
|
|
116
116
|
readonly word: string;
|
|
117
|
+
/** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL stands at the program
|
|
118
|
+
* position. Absent ⇒ genuine control syntax, which IS one. */
|
|
119
|
+
readonly runsShell?: true;
|
|
117
120
|
};
|
|
118
121
|
export interface ProgramPosition {
|
|
119
122
|
/** Indices of the bare control words stripped in front (`!`, `if`, `time`, …), in order; their declared options
|
|
@@ -159,5 +162,20 @@ export declare function readProgramPosition(toks: SegmentTokens): ProgramPositio
|
|
|
159
162
|
* stdin AND the program it runs is not one the caller accepts as a stream target. ONE reading, shared by every
|
|
160
163
|
* consumer; WHICH targets are accepted is the caller's declaration, not this reader's. */
|
|
161
164
|
export declare function argvReentryUnjudged(pos: ProgramPosition, acceptedTargets: ReadonlySet<string>): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* The two ways this reader DECLINES rather than judges — a CLOSED set, because which of them a seat reads as
|
|
167
|
+
* evidence is a table each seat declares against (`fs-bash.ts`, where the argument is), not a boolean buried
|
|
168
|
+
* in a caller. `unreadable_program_position`: the walk cannot name the program bash will run (`env -- ls`).
|
|
169
|
+
* `stream_fed_operands`: the operands arrive on a STREAM it never saw (`… | xargs cat`). Neither names a WORD.
|
|
170
|
+
*/
|
|
171
|
+
export declare const SHELL_SCAN_DECLINES: readonly ["unreadable_program_position", "stream_fed_operands"];
|
|
172
|
+
export type ShellScanDecline = (typeof SHELL_SCAN_DECLINES)[number];
|
|
173
|
+
/** Does a SHELL stand where this segment's program would be — as the program itself (`sh -c`), or as a launcher
|
|
174
|
+
* ROW handing its operands to one (`watch "…"`)? ONE predicate for the re-entry class, so the two spellings
|
|
175
|
+
* cannot be answered differently; which names are shells is the caller's declaration. Not a decline. */
|
|
176
|
+
export declare function programPositionRunsShell(pos: ProgramPosition, shellProgram: RegExp): boolean;
|
|
177
|
+
/** Which decline, if any, this program position raises — ONE reading of the two facts above, so no consumer
|
|
178
|
+
* re-derives either. A refusal that says a SHELL runs is NOT one: it names what runs. */
|
|
179
|
+
export declare function shellScanDeclineOf(pos: ProgramPosition, acceptedStreamTargets: ReadonlySet<string>): ShellScanDecline | undefined;
|
|
162
180
|
/** ONE sentence per refusal kind, minted here and nowhere else. */
|
|
163
181
|
export declare function programPositionRefusalSentence(refusal: ProgramPositionRefusal): string;
|
|
@@ -107,7 +107,7 @@ export function readProgramPosition(toks) {
|
|
|
107
107
|
}
|
|
108
108
|
const launchers = [{ index: i, name: basename }];
|
|
109
109
|
const owner = head;
|
|
110
|
-
const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh" });
|
|
110
|
+
const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true });
|
|
111
111
|
let owedOperands = LAUNCHER_TABLE.get(basename)?.requiredOperands ?? 0;
|
|
112
112
|
let lastRow = LAUNCHER_TABLE.get(basename);
|
|
113
113
|
const chain0 = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin: false, runsDefaultProgram: false, program, refusal });
|
|
@@ -158,6 +158,18 @@ export function argvReentryUnjudged(pos, acceptedTargets) {
|
|
|
158
158
|
const runs = pos.program?.basename ?? (pos.runsDefaultProgram ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined);
|
|
159
159
|
return runs === undefined || !acceptedTargets.has(runs);
|
|
160
160
|
}
|
|
161
|
+
export const SHELL_SCAN_DECLINES = ["unreadable_program_position", "stream_fed_operands"];
|
|
162
|
+
export function programPositionRunsShell(pos, shellProgram) {
|
|
163
|
+
if (pos.refusal !== undefined)
|
|
164
|
+
return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
|
|
165
|
+
return pos.program !== undefined && shellProgram.test(pos.program.basename);
|
|
166
|
+
}
|
|
167
|
+
export function shellScanDeclineOf(pos, acceptedStreamTargets) {
|
|
168
|
+
if (pos.refusal !== undefined) {
|
|
169
|
+
return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true ? undefined : "unreadable_program_position";
|
|
170
|
+
}
|
|
171
|
+
return argvReentryUnjudged(pos, acceptedStreamTargets) ? "stream_fed_operands" : undefined;
|
|
172
|
+
}
|
|
161
173
|
export function programPositionRefusalSentence(refusal) {
|
|
162
174
|
switch (refusal.kind) {
|
|
163
175
|
case "option_at_program_position":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ShellSegmentConnector } from "./bash-lexer.js";
|
|
2
|
+
import { type ShellScanDecline } from "./bash-program-position.js";
|
|
2
3
|
/**
|
|
3
4
|
* Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
|
|
4
5
|
* The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
|
|
@@ -31,7 +32,6 @@ import { type ShellSegmentConnector } from "./bash-lexer.js";
|
|
|
31
32
|
* value-taking options defeat the stdin-floor operand count without a per-verb option-value model
|
|
32
33
|
* — those are a follow-on slice, not a silent drop.
|
|
33
34
|
*/
|
|
34
|
-
export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
|
|
35
35
|
export type { ShellSegmentConnector } from "./bash-lexer.js";
|
|
36
36
|
export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
|
|
37
37
|
/**
|
|
@@ -174,7 +174,7 @@ export interface ShellCompoundSegments {
|
|
|
174
174
|
* drift in a segmentation is drift in what a deny rule is even looking at.
|
|
175
175
|
*
|
|
176
176
|
* Three steps, in this order and load-bearing:
|
|
177
|
-
* 1. WHOLE-STRING hard reject
|
|
177
|
+
* 1. WHOLE-STRING hard reject ({@link hardRejectReason}) — redirection, substitution,
|
|
178
178
|
* subshells, escapes, line breaks. This is what makes step 3's quote mask EXACT: with those
|
|
179
179
|
* characters gone, `'`/`"` pairing is the entirety of quoting, so the mask's regions are exactly
|
|
180
180
|
* bash's quoted regions and the unquoted connectors are exactly bash's command boundaries. Callers
|
|
@@ -524,7 +524,19 @@ export declare function resolveOperandSpellings(base: string | undefined, operan
|
|
|
524
524
|
* here-document, a here-string or a literal in-root stdin file; a directory change (which then makes every relative
|
|
525
525
|
* stdin literal in the command unjudged). A substitution body (`$(…)`, backticks, `<(…)`) is judged on its own: green
|
|
526
526
|
* under the compound walk, or exempt under this whitelist; a here-document body is text, not commands. */
|
|
527
|
-
export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary
|
|
527
|
+
export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary | undefined,
|
|
528
|
+
/** The {@link ShellScanDecline} members THIS SEAT reads as evidence (tables and argument: `fs-bash.ts`); one
|
|
529
|
+
* the table omits ends the scan with "exempt". */
|
|
530
|
+
declinesAreEvidence: ReadonlySet<ShellScanDecline>): RefusedCommandScan;
|
|
531
|
+
/** What ONE scan of a refused command found — TWO facts, because a seat's table answers only the first. `mayRead`
|
|
532
|
+
* is the whitelist's verdict ("may read a file the boundary never judged"), which a seat turns into a MANDATE.
|
|
533
|
+
* `declined` is the scan's own report that it could not judge where a segment reads AT ALL
|
|
534
|
+
* ({@link SHELL_SCAN_DECLINES}), recorded BEFORE the seat's table and so the SAME on every seat: a seat omitting
|
|
535
|
+
* that decline has ruled only that an ANSWERER may retire the ask, never that nothing happened. */
|
|
536
|
+
export interface RefusedCommandScan {
|
|
537
|
+
readonly mayRead: boolean;
|
|
538
|
+
readonly declined: boolean;
|
|
539
|
+
}
|
|
528
540
|
/**
|
|
529
541
|
* The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
|
|
530
542
|
* command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
|
|
@@ -602,6 +614,10 @@ export interface OutOfRootReadGateVerdict {
|
|
|
602
614
|
* operand is placed against a base bash has already left, so no path can be named honestly. Read it
|
|
603
615
|
* as display/triage evidence; emptiness is not "not gated". */
|
|
604
616
|
readonly paths: readonly string[];
|
|
617
|
+
/** Did the deployment's DENY judge speak about an operand this command names? The other half of the same
|
|
618
|
+
* question, over the very same readers, in a second pass WITH the deny judge — which is what lets the
|
|
619
|
+
* containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing. */
|
|
620
|
+
readonly denied?: true;
|
|
605
621
|
}
|
|
606
622
|
/** Test seam for the lockstep pin that the launcher table (the ONE launcher truth, in the program-position
|
|
607
623
|
* reader's module) stays a SUBSET of the rule lane's curated `BARE_INTERPRETER_NAMES`. Exported for that one
|
|
@@ -638,10 +654,10 @@ export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
|
|
|
638
654
|
* base across segments) and dropping it would place every later relative operand against a base bash
|
|
639
655
|
* has already left. A move the walk cannot model (`pushd`/`popd`) is not silently tolerated either —
|
|
640
656
|
* see the `paths`-less gated answer below.
|
|
641
|
-
* ·
|
|
642
|
-
*
|
|
643
|
-
* deny-listed operand HIDE the fact that it also sits outside the roots
|
|
644
|
-
*
|
|
657
|
+
* · BOTH halves from ONE extraction, in TWO passes: the CONTAINMENT pass alone is deny-free, because
|
|
658
|
+
* {@link BashReadonlyRootBoundary.denyMatch} outranks containment inside the walk and returns early,
|
|
659
|
+
* which would let a deny-listed operand HIDE the fact that it also sits outside the roots. A SECOND
|
|
660
|
+
* pass over the SAME readers does carry that judge, and is what answers `denied` (see below).
|
|
645
661
|
* · `face: "open"` is honoured: under the open read face containment is structurally satisfied, an
|
|
646
662
|
* out-of-roots operand is not a finding, and this gate is silent — a full-read deployment sees no
|
|
647
663
|
* gate here, by the same one mechanism the face already defines.
|