@sema-agent/core 7.26.3 → 7.28.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 +59 -0
- package/dist/agents/builtin-agents.d.ts +6 -15
- package/dist/agents/cascade.d.ts +29 -33
- package/dist/agents/observer.d.ts +18 -20
- package/dist/agents/subagent.d.ts +58 -70
- package/dist/agents/tool-filter.d.ts +10 -12
- package/dist/agents/verify.d.ts +44 -49
- package/dist/brain/input-too-long.d.ts +10 -12
- package/dist/brain/media-degrade.d.ts +4 -5
- package/dist/brain/reasoning.d.ts +10 -12
- package/dist/brain/repetition.d.ts +7 -9
- package/dist/brain/request-params.d.ts +20 -25
- package/dist/brain/route-adjudicator.d.ts +4 -4
- package/dist/brain/route-conformance.d.ts +2 -2
- package/dist/brain/status-sink.d.ts +12 -14
- package/dist/brain/terminal-cause.d.ts +12 -15
- package/dist/brain/timeout.d.ts +17 -19
- package/dist/core/auto-mode-directive.d.ts +6 -4
- package/dist/core/auto-mode-directive.js +1 -5
- package/dist/core/engine-notice.d.ts +13 -6
- package/dist/core/memory-engine/consolidation.d.ts +11 -15
- package/dist/core/memory-engine/engine.d.ts +45 -54
- package/dist/core/memory-engine/tools.d.ts +17 -23
- package/dist/core/permission-rule-model.js +5 -3
- package/dist/core/runner/contracts.d.ts +31 -7
- package/dist/core/runner/denial-limit-arms.d.ts +17 -15
- package/dist/core/runner/denial-limit-arms.js +4 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +1 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +1 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/tool-policy.d.ts +23 -2
- package/dist/core/tool-policy.js +106 -103
- package/dist/index.d.ts +6 -1
- package/dist/shell/lex.d.ts +279 -0
- package/dist/shell/lex.js +1253 -0
- package/dist/stores/file/background-agent-store.d.ts +4 -0
- package/dist/stores/file/background-agent-store.js +10 -0
- package/dist/stores/file/checkpoint-store.d.ts +3 -0
- package/dist/stores/file/checkpoint-store.js +18 -0
- package/dist/stores/file/corrupt-read-sink.d.ts +20 -0
- package/dist/stores/file/corrupt-read-sink.js +15 -0
- package/dist/stores/file/file-history-store.d.ts +1 -2
- package/dist/stores/file/file-history-store.js +3 -8
- package/dist/stores/file/fs-atomic.d.ts +8 -3
- package/dist/stores/file/fs-atomic.js +19 -2
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/mailbox-store.d.ts +3 -0
- package/dist/stores/file/mailbox-store.js +24 -18
- package/dist/stores/file/permission-rule-store.js +18 -1
- package/dist/stores/file/session-policy-store.d.ts +17 -1
- package/dist/stores/file/session-policy-store.js +3 -12
- package/dist/stores/file/session-store.d.ts +1 -5
- package/dist/stores/file/session-store.js +9 -14
- package/dist/stores/file/shared-ledger.d.ts +18 -0
- package/dist/stores/file/shared-ledger.js +16 -2
- package/dist/stores/file/tool-result-store.d.ts +9 -1
- package/dist/stores/file/tool-result-store.js +17 -2
- package/dist/stores/file/workflow-journal-store.d.ts +10 -1
- package/dist/stores/file/workflow-journal-store.js +8 -12
- package/dist/stores/file/workflow-run-store.d.ts +3 -0
- package/dist/stores/file/workflow-run-store.js +10 -0
- package/dist/tools/artifact/local-stub.js +16 -1
- package/dist/tools/fs/bash-program-position.d.ts +2 -4
- package/dist/tools/fs/bash-program-position.js +2 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +1 -1
- package/dist/tools/fs/shell-read-check.js +1 -1
- package/dist/tools/fs/shell-read-establish.js +11 -1
- package/dist/tools/fs/shell-read-plan.d.ts +7 -2
- package/dist/tools/fs/shell-read-plan.js +48 -10
- package/package.json +2 -2
- package/test/export-surface.snapshot.json +3 -1
- package/wire-changes/core@7.27.0.json +152 -0
- package/wire-changes/core@7.28.0.json +594 -0
- package/dist/tools/fs/bash-lexer.d.ts +0 -152
- package/dist/tools/fs/bash-lexer.js +0 -627
|
@@ -3,12 +3,15 @@ import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance, ToolResultRefConflictError, } from "../../core/tool-result-store.js";
|
|
5
5
|
import { ensureDir, sanitizePathComponent, writeThenLink } from "./fs-atomic.js";
|
|
6
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
7
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
7
8
|
export class FileToolResultStore {
|
|
8
9
|
dir;
|
|
9
|
-
|
|
10
|
+
discloseCorrupt;
|
|
11
|
+
constructor(root, opts) {
|
|
10
12
|
assertAdoptionBootGate(root, "FileToolResultStore");
|
|
11
13
|
this.dir = join(root, "tool-results");
|
|
14
|
+
this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead);
|
|
12
15
|
ensureDir(this.dir);
|
|
13
16
|
}
|
|
14
17
|
locate(ref) {
|
|
@@ -59,15 +62,26 @@ export class FileToolResultStore {
|
|
|
59
62
|
return this.readOwnerFile(ref, { loud: false });
|
|
60
63
|
}
|
|
61
64
|
readOwnerFile(ref, opts) {
|
|
65
|
+
const ownerPath = (() => {
|
|
66
|
+
try {
|
|
67
|
+
return this.ownerPathFor(ref);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
if (ownerPath === undefined)
|
|
74
|
+
return undefined;
|
|
62
75
|
let raw;
|
|
63
76
|
try {
|
|
64
|
-
raw = readFileSync(
|
|
77
|
+
raw = readFileSync(ownerPath, "utf8");
|
|
65
78
|
}
|
|
66
79
|
catch (err) {
|
|
67
80
|
if (err.code === "ENOENT")
|
|
68
81
|
return undefined;
|
|
69
82
|
if (opts.loud)
|
|
70
83
|
throw err;
|
|
84
|
+
this.discloseCorrupt?.({ path: ownerPath, reason: `unreadable owner sidecar (${err.code ?? "unknown"}) — read as unowned` });
|
|
71
85
|
return undefined;
|
|
72
86
|
}
|
|
73
87
|
const parsed = parseOwnerRecord(raw);
|
|
@@ -76,6 +90,7 @@ export class FileToolResultStore {
|
|
|
76
90
|
if (opts.loud) {
|
|
77
91
|
throw new Error(`tool-result store: owner record for ref ${JSON.stringify(ref)} is present but unreadable — refusing to write over it`);
|
|
78
92
|
}
|
|
93
|
+
this.discloseCorrupt?.({ path: ownerPath, reason: "not-an-object: owner sidecar is present but is not an owner record — read as unowned" });
|
|
79
94
|
return undefined;
|
|
80
95
|
}
|
|
81
96
|
writeOwnerFile(ref, provenance) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type WorkflowJournalEntry, type WorkflowJournalStore, type ResumeClaimArgs } from "../../core/workflow-journal-store.js";
|
|
2
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
2
3
|
/**
|
|
3
4
|
* SVC-2 — the shared size bound for a journaled workflow-agent entry, so ALL backends answer an oversize
|
|
4
5
|
* TaskResult with the SAME refusal (no dialect divergence). Inlined from service's `workflow-journal-limits` on
|
|
@@ -46,6 +47,12 @@ export declare const RESUME_CLAIM_SWEEP_INTERVAL_MS = 60000;
|
|
|
46
47
|
* stays well clear of that handover rather than adding a second unlinker to it.
|
|
47
48
|
*/
|
|
48
49
|
export declare const RESUME_CLAIM_REAP_GRACE_MS: number;
|
|
50
|
+
export interface FileWorkflowJournalStoreOptions {
|
|
51
|
+
/** Disclosure seat for a ledger line this store DROPPED on replay (not JSON, not a record, or a record
|
|
52
|
+
* with none of this ledger's members) — a resume that silently finds "nothing to replay" re-runs the
|
|
53
|
+
* work with no signal. Never fires on a missing ledger; swallow-guarded. */
|
|
54
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
55
|
+
}
|
|
49
56
|
export declare class FileWorkflowJournalStore implements WorkflowJournalStore {
|
|
50
57
|
private readonly fsyncEnabled;
|
|
51
58
|
private readonly dir;
|
|
@@ -65,7 +72,9 @@ export declare class FileWorkflowJournalStore implements WorkflowJournalStore {
|
|
|
65
72
|
* ledger per append (a journaled result can be MBs; a many-agent workflow would go O(n²) on disk). One replay
|
|
66
73
|
* discovers it; every later append compares against the cache. */
|
|
67
74
|
private get scopes();
|
|
68
|
-
|
|
75
|
+
/** The swallow-guarded corrupt-read seat (undefined = the old silent tolerance). */
|
|
76
|
+
private readonly discloseCorrupt;
|
|
77
|
+
constructor(root: string, fsyncEnabled?: boolean, opts?: FileWorkflowJournalStoreOptions);
|
|
69
78
|
private pathFor;
|
|
70
79
|
/** CC local form: the run's on-disk journal path for the notification diagnostics line.
|
|
71
80
|
* Traversal-shaped ids (LLM-controlled on some paths) degrade to undefined, never throw. */
|
|
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { callKeyOrdinal } from "../../core/workflow-journal-store.js";
|
|
5
|
-
import { AppendLog } from "./fs-atomic.js";
|
|
5
|
+
import { AppendLog, readJsonlRecords } from "./fs-atomic.js";
|
|
6
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
7
|
import { canonicalStoreKey, sanitizePathComponent, writeThenLink } from "./fs-atomic.js";
|
|
7
8
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
8
9
|
import { assertJournalEntryFits } from "../../core/workflow-journal-store.js";
|
|
@@ -32,8 +33,10 @@ export class FileWorkflowJournalStore {
|
|
|
32
33
|
get scopes() {
|
|
33
34
|
return this.shared.scopes;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
discloseCorrupt;
|
|
37
|
+
constructor(root, fsyncEnabled = true, opts) {
|
|
36
38
|
this.fsyncEnabled = fsyncEnabled;
|
|
39
|
+
this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead);
|
|
37
40
|
assertAdoptionBootGate(root, "FileWorkflowJournalStore");
|
|
38
41
|
this.dir = join(root, "workflow-journal");
|
|
39
42
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
@@ -72,18 +75,11 @@ export class FileWorkflowJournalStore {
|
|
|
72
75
|
return undefined;
|
|
73
76
|
let scope;
|
|
74
77
|
const byOrdinal = new Map();
|
|
75
|
-
for (const
|
|
76
|
-
if (!
|
|
78
|
+
for (const rec of readJsonlRecords(path, this.discloseCorrupt)) {
|
|
79
|
+
if (typeof rec.callKey !== "string" || typeof rec.scope !== "string" || !Number.isFinite(rec.ordinal)) {
|
|
80
|
+
this.discloseCorrupt?.({ path, reason: "missing-required: a journal line carries no callKey/scope pair (skipped; the rest of the replay is intact)" });
|
|
77
81
|
continue;
|
|
78
|
-
let rec;
|
|
79
|
-
try {
|
|
80
|
-
rec = JSON.parse(line);
|
|
81
82
|
}
|
|
82
|
-
catch {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (typeof rec.callKey !== "string" || typeof rec.scope !== "string")
|
|
86
|
-
continue;
|
|
87
83
|
scope = scope ?? rec.scope;
|
|
88
84
|
if (rec.scope !== scope)
|
|
89
85
|
continue;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type WorkflowRunListByScopeOptions, type WorkflowRunStore, type WorkflowRunSummary } from "../../core/workflow-run-store.js";
|
|
2
2
|
import type { WorkflowRun } from "../../orchestration/workflow.js";
|
|
3
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
3
4
|
export interface FileWorkflowRunStoreOptions {
|
|
4
5
|
/** When false, a ledger append is NOT fsync'd (the observability store is non-engine-critical). Default true. */
|
|
5
6
|
fsync?: boolean;
|
|
6
7
|
/** Compact the ledger into a snapshot once it exceeds this many events (then truncate). Default 1000. */
|
|
7
8
|
compactEvery?: number;
|
|
9
|
+
/** The observability ledger's seat; see {@link import("./shared-ledger.js").LedgerPaths.onCorruptRead}. */
|
|
10
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
8
11
|
}
|
|
9
12
|
export declare class FileWorkflowRunStore implements WorkflowRunStore {
|
|
10
13
|
private readonly fsyncEnabled;
|
|
@@ -2,7 +2,9 @@ import { join } from "node:path";
|
|
|
2
2
|
import { assertRetentionPolicy } from "../../core/retention-policy.js";
|
|
3
3
|
import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js";
|
|
4
4
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
5
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
5
6
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
7
|
+
const isRunRow = (x) => x !== null && typeof x === "object" && typeof x.id === "string";
|
|
6
8
|
const runLedgers = new SharedLedgerTable({
|
|
7
9
|
label: "workflow-run ledger",
|
|
8
10
|
keyOf: (run) => run.id,
|
|
@@ -13,6 +15,13 @@ const runLedgers = new SharedLedgerTable({
|
|
|
13
15
|
}
|
|
14
16
|
runs.set(ev.id, ev.run);
|
|
15
17
|
},
|
|
18
|
+
judgeRow: isRunRow,
|
|
19
|
+
judgeEvent: (x) => {
|
|
20
|
+
const ev = x;
|
|
21
|
+
if (typeof ev.id !== "string")
|
|
22
|
+
return false;
|
|
23
|
+
return ev.t === "delete" ? true : isRunRow(ev.run);
|
|
24
|
+
},
|
|
16
25
|
});
|
|
17
26
|
export class FileWorkflowRunStore {
|
|
18
27
|
fsyncEnabled;
|
|
@@ -30,6 +39,7 @@ export class FileWorkflowRunStore {
|
|
|
30
39
|
this.ledger = runLedgers.acquire({
|
|
31
40
|
dir,
|
|
32
41
|
tmpDir: join(root, "tmp"),
|
|
42
|
+
...(opts.onCorruptRead !== undefined ? { onCorruptRead: guardCorruptSink(opts.onCorruptRead) } : {}),
|
|
33
43
|
ledgerPath: join(dir, "runs.jsonl"),
|
|
34
44
|
snapshotPath: join(dir, "runs.snapshot.jsonl"),
|
|
35
45
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
-
import {} from "../../core/artifact-host.js";
|
|
4
|
+
import { ARTIFACT_HOST_FAILURE_CODES, } from "../../core/artifact-host.js";
|
|
5
5
|
import { physicalPathOf } from "../../core/physical-path.js";
|
|
6
6
|
import { acquireStoreDirLock } from "../../stores/file/fs-atomic.js";
|
|
7
7
|
import { normalizePublishedPath } from "./artifact-tool.js";
|
|
@@ -25,6 +25,18 @@ const isRow = (x, key) => {
|
|
|
25
25
|
return false;
|
|
26
26
|
return r.files.every((f) => f !== null && typeof f === "object" && typeof f.path === "string" && normalizePublishedPath(f.path).ok && Number.isSafeInteger(f.bytes));
|
|
27
27
|
};
|
|
28
|
+
const isAttempt = (x) => {
|
|
29
|
+
if (x === null || typeof x !== "object" || Array.isArray(x))
|
|
30
|
+
return false;
|
|
31
|
+
const a = x;
|
|
32
|
+
if ("ref" in a) {
|
|
33
|
+
const r = a.ref;
|
|
34
|
+
return r !== null && typeof r === "object" && typeof r.url === "string" && typeof r.slug === "string" && typeof r.version === "string";
|
|
35
|
+
}
|
|
36
|
+
if (typeof a.code !== "string" || !ARTIFACT_HOST_FAILURE_CODES.includes(a.code))
|
|
37
|
+
return false;
|
|
38
|
+
return a.code === "conflict" ? typeof a.slug === "string" && typeof a.currentVersion === "string" : typeof a.message === "string";
|
|
39
|
+
};
|
|
28
40
|
function parseLedger(text, path) {
|
|
29
41
|
let parsed;
|
|
30
42
|
try {
|
|
@@ -40,6 +52,9 @@ function parseLedger(text, path) {
|
|
|
40
52
|
for (const [key, row] of Object.entries(l.artifacts))
|
|
41
53
|
if (!isRow(row, key))
|
|
42
54
|
throw new Error(`artifact stub ledger is corrupt (bad artifact row): ${path}`);
|
|
55
|
+
for (const attempt of Object.values(l.attempts))
|
|
56
|
+
if (!isAttempt(attempt))
|
|
57
|
+
throw new Error(`artifact stub ledger is corrupt (bad attempt record): ${path}`);
|
|
43
58
|
return { v: 1, artifacts: { ...l.artifacts }, attempts: { ...l.attempts } };
|
|
44
59
|
}
|
|
45
60
|
function fsyncDirBestEffort(dir) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* readable, minted into one sentence by {@link programPositionRefusalSentence}. Reading less than this reader
|
|
9
9
|
* does (an option skipped, a chain member missed) is how one face used to vouch for a program another refused.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { SHELL_CONTROL_PREFIXES, type SegmentTokens } from "../../shell/lex.js";
|
|
12
12
|
/** The verdict every unresolvable / out-of-root / unbounded finding ends on. The findings differ in what
|
|
13
13
|
* they found; the consequence is one consequence, and a copy that drifts reads as a second rule. */
|
|
14
14
|
export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
|
|
@@ -35,9 +35,7 @@ export declare function writtenAssignmentPrefix(token: string): {
|
|
|
35
35
|
readonly appends: boolean;
|
|
36
36
|
readonly settlesWholeName: boolean;
|
|
37
37
|
} | undefined;
|
|
38
|
-
|
|
39
|
-
* judged). */
|
|
40
|
-
export declare const SHELL_CONTROL_PREFIXES: ReadonlySet<string>;
|
|
38
|
+
export { SHELL_CONTROL_PREFIXES };
|
|
41
39
|
/** Shell block words that name no command of their own (a loop head, a block close, a case head). */
|
|
42
40
|
export declare const SHELL_BLOCK_WORDS: ReadonlySet<string>;
|
|
43
41
|
/** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SHELL_CONTROL_PREFIXES } from "../../shell/lex.js";
|
|
1
2
|
export const NOT_AUTO_ALLOWED = "— not auto-allowed";
|
|
2
3
|
export function programBasename(word) {
|
|
3
4
|
return word.includes("/") ? (word.split("/").pop() ?? word) : word;
|
|
@@ -12,7 +13,7 @@ export function writtenAssignmentPrefix(token) {
|
|
|
12
13
|
const bracket = written.indexOf("[");
|
|
13
14
|
return { prefix, name: bracket < 0 ? written : written.slice(0, bracket), appends, settlesWholeName: !appends && bracket < 0 };
|
|
14
15
|
}
|
|
15
|
-
export
|
|
16
|
+
export { SHELL_CONTROL_PREFIXES };
|
|
16
17
|
export const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]);
|
|
17
18
|
export function isBareControlWord(toks, i, words) {
|
|
18
19
|
const word = toks.folded[i];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SegmentTokens, type ShellSegmentConnector } from "
|
|
1
|
+
import { type SegmentTokens, type ShellSegmentConnector } from "../../shell/lex.js";
|
|
2
2
|
/**
|
|
3
3
|
* Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
|
|
4
4
|
* The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
|
|
@@ -31,7 +31,7 @@ import { type SegmentTokens, type ShellSegmentConnector } from "./bash-lexer.js"
|
|
|
31
31
|
* value-taking options defeat the stdin-floor operand count without a per-verb option-value model
|
|
32
32
|
* — those are a follow-on slice, not a silent drop.
|
|
33
33
|
*/
|
|
34
|
-
export type { ShellSegmentConnector } from "
|
|
34
|
+
export type { ShellSegmentConnector } from "../../shell/lex.js";
|
|
35
35
|
export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
|
|
36
36
|
/**
|
|
37
37
|
* The verbs the CLASSIFY face admits ON TOP of the shared list ({@link bashClassifyDefaultAllow}) —
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
|
|
2
|
-
import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "
|
|
2
|
+
import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "../../shell/lex.js";
|
|
3
3
|
import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRefusalSentence, ASSIGNMENT_PREFIX, readProgramPosition, writtenAssignmentPrefix, } from "./bash-program-position.js";
|
|
4
4
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
5
5
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
@@ -8,7 +8,7 @@ import { planShellReads } from "./shell-read-plan.js";
|
|
|
8
8
|
import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
|
|
9
9
|
import { allFindings, decideShellRead, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
|
|
10
10
|
import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
|
|
11
|
-
import { wordsOf, lexShellText } from "
|
|
11
|
+
import { wordsOf, lexShellText } from "../../shell/lex.js";
|
|
12
12
|
function containmentDetails(paths) {
|
|
13
13
|
return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
|
|
14
14
|
}
|
|
@@ -98,7 +98,17 @@ function lexicalReadsOf(candidate, boundary) {
|
|
|
98
98
|
}
|
|
99
99
|
const bases = basesFor(candidate, boundary);
|
|
100
100
|
if (bases === "unmodelled") {
|
|
101
|
-
|
|
101
|
+
const floors = candidate.floorBases ?? [boundary.cwd ?? boundary.roots[0] ?? ""];
|
|
102
|
+
const spellings = [candidate.spelled];
|
|
103
|
+
for (const floorBase of floors) {
|
|
104
|
+
const viaFloor = floorBase === "" ? undefined : resolveOperandSpellings(floorBase, candidate.spelled, boundary.homeDir);
|
|
105
|
+
if (viaFloor === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
spellings.push(viaFloor.folded);
|
|
108
|
+
if (viaFloor.spelled !== undefined)
|
|
109
|
+
spellings.push(viaFloor.spelled);
|
|
110
|
+
}
|
|
111
|
+
return [{ candidate, established: "unestablishable", spellings, cause: `"${candidate.raw}" is relative to a working directory this command's text does not settle` }];
|
|
102
112
|
}
|
|
103
113
|
const out = [];
|
|
104
114
|
const seen = new Set();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The read boundary today re-derives "which words may be read" at several sites (the compound walk, the
|
|
5
5
|
* containment-only gate, the refused-command whitelist, the execute-time recheck), each with its own idea
|
|
6
6
|
* of launchers, option slots, redirections and re-entry. This module is the single extraction those sites
|
|
7
|
-
* are to consume: a pure, zero-I/O function over the lenient lexer (`
|
|
7
|
+
* are to consume: a pure, zero-I/O function over the lenient lexer (`src/shell/lex.ts`) and the program-position
|
|
8
8
|
* reader (`bash-program-position.ts`), returning every path word a command MAY read, each tagged with its
|
|
9
9
|
* PROVENANCE (why it is a candidate) and the working-directory candidates it is relative to. Establishment
|
|
10
10
|
* (`shell-read-establish.ts`) and judgment are downstream; nothing here decides allow/ask/deny.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* otherwise loses the base for what follows.
|
|
27
27
|
* Every ambiguity resolves toward MORE candidates, never fewer.
|
|
28
28
|
*/
|
|
29
|
-
import { type LenientConnector, type LexedRedirect, type LexedWord } from "
|
|
29
|
+
import { type LenientConnector, type LexedRedirect, type LexedWord } from "../../shell/lex.js";
|
|
30
30
|
import { type ShellScanDecline } from "./bash-program-position.js";
|
|
31
31
|
/** WHY a word is a read candidate — a closed set, registered in `docs/CLOSED-SETS.md`. */
|
|
32
32
|
export declare const READ_CANDIDATE_PROVENANCES: readonly ["operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file"];
|
|
@@ -55,6 +55,11 @@ export interface ReadCandidate {
|
|
|
55
55
|
* EMPTY for an absolute or home-rooted spelling; `"unmodelled"` for a relative word in a segment whose base the
|
|
56
56
|
* text does not settle (an unfollowable move before it, a loop this parser cannot count) — it cannot be placed. */
|
|
57
57
|
readonly bases: readonly string[] | "unmodelled";
|
|
58
|
+
/** Only beside `bases: "unmodelled"`: the working directories the text was certainly standing in when it LOST
|
|
59
|
+
* track (the reachable candidate set at the unprovable move; the declared cwd when nothing had moved yet).
|
|
60
|
+
* A deny row is a floor, so the judge resolves the spelling against these for deny rows ONLY — never to
|
|
61
|
+
* establish a read. `cd sub && cd $'\x2e' && cat <deny row under sub>` (bash: `cd .`) is refused through it. */
|
|
62
|
+
readonly floorBases?: readonly string[];
|
|
58
63
|
}
|
|
59
64
|
/** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
|
|
60
65
|
export interface SegmentReach {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lexShell, PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, UNRESOLVED_VIEW, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "../../shell/lex.js";
|
|
2
2
|
import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, writtenAssignmentPrefix, } from "./bash-program-position.js";
|
|
3
3
|
import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, assignmentWordPayload, attachedOptionPayloads, foldHomeVariableSpelling, resolveOperandLexically, hasUnquotedExpansionMetachar, hasUnquotedGlobMetachar, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
|
|
4
4
|
import { isAbsolutePathForm } from "./safety.js";
|
|
@@ -16,14 +16,25 @@ export const READ_CANDIDATE_PROVENANCES = [
|
|
|
16
16
|
];
|
|
17
17
|
const NESTING_MAX = 4;
|
|
18
18
|
const BASE_CANDIDATE_MAX = 256;
|
|
19
|
+
function firstRefusedUnresolvedSentence(lexed) {
|
|
20
|
+
for (const segment of lexed.segments) {
|
|
21
|
+
for (const token of segment.tokens) {
|
|
22
|
+
const word = token.kind === "word" ? token : token.kind === "redirect" ? token.target : undefined;
|
|
23
|
+
if (word?.unresolved !== undefined && UNRESOLVED_VIEW[word.unresolved] === "refuse")
|
|
24
|
+
return UNRESOLVED_SENTENCE[word.unresolved];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
19
29
|
export function planShellReads(command, decl, depth = 0) {
|
|
20
30
|
const shape = { grouped: false, backgrounded: false, strayRedirection: false };
|
|
21
31
|
if (depth > NESTING_MAX)
|
|
22
32
|
return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape };
|
|
23
33
|
const homeReassigned = /(^|[\s;&|(){}`])(?:export\s+)?HOME\+?=/.test(command);
|
|
24
|
-
const lexed =
|
|
25
|
-
if ("
|
|
26
|
-
return { segments: [], unreadable: lexed.
|
|
34
|
+
const lexed = lexShell(foldHomeVariableSpelling(command, homeReassigned ? undefined : decl.homeDir), { mode: "lenient" });
|
|
35
|
+
if ("parseFailure" in lexed)
|
|
36
|
+
return { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape };
|
|
37
|
+
const unreadable = firstRefusedUnresolvedSentence(lexed);
|
|
27
38
|
const nestedPlan = (text) => planShellReads(text, decl, depth + 1);
|
|
28
39
|
const projected = lexed.segments.map((segment) => projectSegment(segment));
|
|
29
40
|
const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, nestedPlan));
|
|
@@ -34,6 +45,11 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
34
45
|
const unmodelled = facts.map(() => false);
|
|
35
46
|
let candidates = [{ base: decl.cwd, skippedCd: -1 }];
|
|
36
47
|
let baseLost = false;
|
|
48
|
+
let floorBases;
|
|
49
|
+
const loseBase = (reachable) => {
|
|
50
|
+
baseLost = true;
|
|
51
|
+
floorBases ??= [...new Set(reachable.map((c) => c.base))];
|
|
52
|
+
};
|
|
37
53
|
let andRunStart = 0;
|
|
38
54
|
let crossedAllAnd = true;
|
|
39
55
|
let crossedAnyOr = false;
|
|
@@ -68,7 +84,7 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
68
84
|
if (move === undefined || baseLost)
|
|
69
85
|
return;
|
|
70
86
|
if (typeof move.target === "string" || move.target.expands !== false) {
|
|
71
|
-
|
|
87
|
+
loseBase(reachable);
|
|
72
88
|
return;
|
|
73
89
|
}
|
|
74
90
|
const target = move.target.spelled;
|
|
@@ -78,7 +94,7 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
78
94
|
if (c.skippedCd < andRunStart) {
|
|
79
95
|
const moved = joinBase(c.base, target, decl.homeDir);
|
|
80
96
|
if (moved === undefined) {
|
|
81
|
-
|
|
97
|
+
loseBase(reachable);
|
|
82
98
|
return;
|
|
83
99
|
}
|
|
84
100
|
branched.push({ base: moved, skippedCd: c.skippedCd });
|
|
@@ -93,7 +109,7 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
93
109
|
}
|
|
94
110
|
const next = [...byBase.values()];
|
|
95
111
|
if (next.length > BASE_CANDIDATE_MAX)
|
|
96
|
-
|
|
112
|
+
loseBase(reachable);
|
|
97
113
|
else
|
|
98
114
|
candidates = next;
|
|
99
115
|
};
|
|
@@ -116,7 +132,7 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
116
132
|
if (frame.beats === undefined) {
|
|
117
133
|
for (let k = frame.bodyStart; k < si; k++)
|
|
118
134
|
unmodelled[k] = true;
|
|
119
|
-
|
|
135
|
+
loseBase(candidates.filter((c) => c.skippedCd < andRunStart));
|
|
120
136
|
continue;
|
|
121
137
|
}
|
|
122
138
|
for (let beat = 1; beat < frame.beats && !baseLost; beat++) {
|
|
@@ -133,7 +149,11 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
133
149
|
if (f.empty)
|
|
134
150
|
continue;
|
|
135
151
|
const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
|
|
136
|
-
const
|
|
152
|
+
const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined;
|
|
153
|
+
const withBases = (c) => {
|
|
154
|
+
const relative = relativeSpelling(c.spelled);
|
|
155
|
+
return { ...c, bases: relative ? bases : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) };
|
|
156
|
+
};
|
|
137
157
|
segments.push({
|
|
138
158
|
text: f.text,
|
|
139
159
|
connector: f.connector,
|
|
@@ -151,7 +171,7 @@ export function planShellReads(command, decl, depth = 0) {
|
|
|
151
171
|
reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
|
|
152
172
|
});
|
|
153
173
|
}
|
|
154
|
-
return { segments, shape };
|
|
174
|
+
return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
|
|
155
175
|
}
|
|
156
176
|
function projectSegment(segment) {
|
|
157
177
|
const tokens = [...segment.tokens];
|
|
@@ -178,6 +198,24 @@ function projectSegment(segment) {
|
|
|
178
198
|
};
|
|
179
199
|
}
|
|
180
200
|
function segmentFacts(segment, p, decl, nestedPlan) {
|
|
201
|
+
const facts = segmentFactsRead(segment, p, decl, nestedPlan);
|
|
202
|
+
const unresolvedRaw = new Set();
|
|
203
|
+
for (const t of segment.tokens) {
|
|
204
|
+
const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
|
|
205
|
+
if (w?.unresolved !== undefined && UNRESOLVED_VIEW[w.unresolved] === "refuse")
|
|
206
|
+
unresolvedRaw.add(w.raw);
|
|
207
|
+
}
|
|
208
|
+
if (unresolvedRaw.size === 0)
|
|
209
|
+
return facts;
|
|
210
|
+
const unsettle = (c) => (unresolvedRaw.has(c.raw) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c);
|
|
211
|
+
const move = facts.baseMove;
|
|
212
|
+
return {
|
|
213
|
+
...facts,
|
|
214
|
+
reads: facts.reads.map(unsettle),
|
|
215
|
+
...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function segmentFactsRead(segment, p, decl, nestedPlan) {
|
|
181
219
|
const nested = [];
|
|
182
220
|
for (const body of segment.substitutions)
|
|
183
221
|
nested.push(nestedPlan(body));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.28.0",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"pretest": "npm run gate:debts",
|
|
53
53
|
"test": "vitest run --exclude \"**/*.local.test.ts\"",
|
|
54
54
|
"test:watch": "vitest",
|
|
55
|
-
"gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json",
|
|
55
|
+
"gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json --full",
|
|
56
56
|
"gate:live": "node scripts/run-live-gate.mjs",
|
|
57
57
|
"smoke": "tsx src/examples/smoke.ts",
|
|
58
58
|
"eval": "tsx eval/run.ts",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
|
|
3
3
|
"_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
|
|
4
|
-
"count":
|
|
4
|
+
"count": 2450,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"AggregateBudgetOptions": "interface",
|
|
85
85
|
"AnthropicBrainConfig": "interface",
|
|
86
86
|
"AppendLog": "class",
|
|
87
|
+
"ApprovalSeatOutcome": "type",
|
|
87
88
|
"ApproverPosture": "type",
|
|
88
89
|
"ArtifactAction": "type",
|
|
89
90
|
"ArtifactCallContext": "interface",
|
|
@@ -2535,6 +2536,7 @@
|
|
|
2535
2536
|
"AggregateBudgetOptions": "advanced",
|
|
2536
2537
|
"AnthropicBrainConfig": "advanced",
|
|
2537
2538
|
"AppendLog": "advanced",
|
|
2539
|
+
"ApprovalSeatOutcome": "advanced",
|
|
2538
2540
|
"ApproverPosture": "advanced",
|
|
2539
2541
|
"ArtifactAction": "advanced",
|
|
2540
2542
|
"ArtifactCallContext": "advanced",
|