@sema-agent/core 5.52.0 → 5.54.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 +99 -0
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +1 -1
- package/dist/brain/anthropic.js +17 -4
- package/dist/core/a2a.js +12 -1
- package/dist/core/cache-break-detector.js +9 -3
- package/dist/core/mcp.d.ts +168 -5
- package/dist/core/mcp.js +200 -21
- package/dist/core/permission-rule-model.d.ts +140 -21
- package/dist/core/permission-rule-model.js +76 -17
- package/dist/core/permission-rule-org.d.ts +4 -3
- package/dist/core/permission-rule-org.js +12 -3
- package/dist/core/protocol-naming.d.ts +25 -2
- package/dist/core/protocol-naming.js +11 -0
- package/dist/core/runner/prepare-safety-scan.js +7 -0
- package/dist/core/runner/prepare-task.js +24 -3
- package/dist/core/runner/runtask.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow-types.d.ts +16 -6
- package/dist/orchestration/workflow-types.js +10 -4
- package/dist/orchestration/workflow.js +32 -6
- package/dist/stores/file/background-agent-store.js +1 -0
- package/dist/stores/file/checkpoint-store.d.ts +6 -2
- package/dist/stores/file/checkpoint-store.js +1 -0
- package/dist/stores/file/fs-atomic.d.ts +151 -10
- package/dist/stores/file/fs-atomic.js +208 -32
- package/dist/stores/file/index.d.ts +26 -3
- package/dist/stores/file/index.js +25 -2
- package/dist/stores/file/shared-ledger.d.ts +40 -5
- package/dist/stores/file/shared-ledger.js +24 -8
- package/dist/stores/file/workflow-run-store.d.ts +8 -1
- package/dist/stores/file/workflow-run-store.js +1 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
- package/dist/tools/fs/bash-readonly-classifier.js +58 -47
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +3 -1
|
@@ -35,6 +35,17 @@
|
|
|
35
35
|
* compensation was the closest of the four and still wrong twice: it does not cover an unwritable
|
|
36
36
|
* FILE (the reopen throws too), and its reopen error REPLACES the fault that caused the swap. The
|
|
37
37
|
* answer is AppendLog's lazy reopen — {@link LedgerCore.compact}.
|
|
38
|
+
* · **(2026-08-22, cross-process defect probe) — one authority per directory needs one WRITER per
|
|
39
|
+
* directory, and that half was never enforced.** RB-55/59/134 closed the IN-PROCESS shape (two
|
|
40
|
+
* instances, two private replays, two winners); two OS PROCESSES over one directory reproduced it
|
|
41
|
+
* verbatim (a human approval and its refusal each reported as the winner) — and worse, the one that
|
|
42
|
+
* never loaded the other's rows ERASED them at its next {@link LedgerCore.compact} (the snapshot is
|
|
43
|
+
* rewritten from the in-memory map, and the live ledger is then truncated). The class docs all
|
|
44
|
+
* credited a fence they did not hold: `root/LOCK` belongs to `FileStorageBackend`, which never
|
|
45
|
+
* constructs the workflow-run store at all and which a directly-constructed store (all three are root
|
|
46
|
+
* exports) never goes through. So the fence now lives WITH the authority: {@link LedgerCore.bootstrap}
|
|
47
|
+
* takes `dir/LOCK` ({@link acquireStoreDirLock}) and the last {@link LedgerCore.release} drops it —
|
|
48
|
+
* exclusive across processes, refcount-joined in-process, exactly like the authority it guards.
|
|
38
49
|
* · **RB-150 / RB-167 — compaction is HOUSEKEEPING.** It runs after the operation is durable AND
|
|
39
50
|
* applied, so its failure must never be reported as the operation's failure: a committed
|
|
40
51
|
* once-only CAS surfaced as "this did not happen" is the worst possible direction (the caller
|
|
@@ -57,6 +68,9 @@ export interface LedgerPaths {
|
|
|
57
68
|
}
|
|
58
69
|
/** The per-store half: how a SNAPSHOT row is keyed, and how one LEDGER event folds into the map. */
|
|
59
70
|
export interface LedgerModel<Row, Ev> {
|
|
71
|
+
/** What this ledger IS, in an operator's words ("checkpoint ledger") — the noun the directory-fence
|
|
72
|
+
* refusal names, so the message points at the store that is actually in use rather than at a path. */
|
|
73
|
+
label: string;
|
|
60
74
|
keyOf(row: Row): string;
|
|
61
75
|
apply(rows: Map<string, Row>, ev: Ev): void;
|
|
62
76
|
}
|
|
@@ -77,9 +91,21 @@ export declare class LedgerCore<Row, Ev> {
|
|
|
77
91
|
private readonly locks;
|
|
78
92
|
private events;
|
|
79
93
|
private handle;
|
|
94
|
+
/** This directory's cross-process writer fence, held for as long as the authority exists. */
|
|
95
|
+
private fence;
|
|
96
|
+
/** Set when the last holder left: this object is no longer the directory's authority (see compact). */
|
|
97
|
+
private released;
|
|
80
98
|
constructor(key: string, paths: LedgerPaths, model: LedgerModel<Row, Ev>, table: Map<string, LedgerCore<Row, Ev>>);
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
99
|
+
/**
|
|
100
|
+
* Rebuild the authority: take the directory's cross-process writer fence, then the snapshot (the
|
|
101
|
+
* compacted base), then the live ledger's events in order (last-writer-wins), then open the append
|
|
102
|
+
* handle. Only the FIRST instance over the directory runs it.
|
|
103
|
+
*
|
|
104
|
+
* The fence comes FIRST because everything after it assumes what the fence establishes: the replayed
|
|
105
|
+
* map is treated as THE authority for the directory (writes are decided against memory, and
|
|
106
|
+
* compaction rewrites the on-disk state FROM memory). Replaying first and locking after would be a
|
|
107
|
+
* window in which a second process's view is already stale.
|
|
108
|
+
*/
|
|
83
109
|
bootstrap(): void;
|
|
84
110
|
/**
|
|
85
111
|
* Serialize an op behind any in-flight op on the same key — the in-process per-key async mutex (the
|
|
@@ -93,11 +119,20 @@ export declare class LedgerCore<Row, Ev> {
|
|
|
93
119
|
* memory and disk disagreeing. Then the best-effort housekeeping compaction (RB-150/RB-167).
|
|
94
120
|
*/
|
|
95
121
|
commit(ev: Ev, fsync: boolean, compactEvery: number): void;
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
122
|
+
/**
|
|
123
|
+
* Rewrite the snapshot from the authoritative map (atomic) and truncate the live ledger. After
|
|
124
|
+
* this a replay reads the whole state from the snapshot alone — the round-trip is identical.
|
|
125
|
+
*
|
|
126
|
+
* REFUSED once this authority has been RELEASED. Compaction is the one operation that does not touch
|
|
127
|
+
* the append fd (it rewrites whole files), so a store object kept alive past its `close()` could still
|
|
128
|
+
* run it — rewriting the directory from a map that stopped being the authority, over rows a SUCCESSOR
|
|
129
|
+
* (here or in another process, holding the fence this core gave back) has since written. That is the
|
|
130
|
+
* same erasure the directory fence exists to prevent, arriving through a stale reference instead of a
|
|
131
|
+
* second process, so it is refused at the same door rather than trusted to caller discipline.
|
|
132
|
+
*/
|
|
98
133
|
compact(): void;
|
|
99
134
|
/** Drop one instance's share. The LAST holder evicts the authority — identity-checked, so a stale
|
|
100
|
-
* holder can never revoke a REBUILT entry — and closes the append fd. */
|
|
135
|
+
* holder can never revoke a REBUILT entry — retires it (see {@link compact}) and closes the append fd. */
|
|
101
136
|
release(): void;
|
|
102
137
|
/** Test/inspection: events appended since the last compaction. */
|
|
103
138
|
get pendingEvents(): number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords } from "./fs-atomic.js";
|
|
1
|
+
import { acquireStoreDirLock, AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, } from "./fs-atomic.js";
|
|
2
2
|
export class LedgerCore {
|
|
3
3
|
key;
|
|
4
4
|
paths;
|
|
@@ -9,6 +9,8 @@ export class LedgerCore {
|
|
|
9
9
|
locks = new Map();
|
|
10
10
|
events = 0;
|
|
11
11
|
handle;
|
|
12
|
+
fence;
|
|
13
|
+
released = false;
|
|
12
14
|
constructor(key, paths, model, table) {
|
|
13
15
|
this.key = key;
|
|
14
16
|
this.paths = paths;
|
|
@@ -16,13 +18,21 @@ export class LedgerCore {
|
|
|
16
18
|
this.table = table;
|
|
17
19
|
}
|
|
18
20
|
bootstrap() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
21
|
+
this.fence = acquireStoreDirLock(this.paths.dir, { label: this.model.label });
|
|
22
|
+
try {
|
|
23
|
+
for (const row of readJsonlRecords(this.paths.snapshotPath))
|
|
24
|
+
this.rows.set(this.model.keyOf(row), row);
|
|
25
|
+
const events = readJsonlRecords(this.paths.ledgerPath);
|
|
26
|
+
this.events = events.length;
|
|
27
|
+
for (const ev of events)
|
|
28
|
+
this.model.apply(this.rows, ev);
|
|
29
|
+
this.handle = new AppendLog(this.paths.ledgerPath);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
this.fence.release();
|
|
33
|
+
this.fence = undefined;
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
withLock(key, fn) {
|
|
28
38
|
const prev = this.locks.get(key) ?? Promise.resolve();
|
|
@@ -48,6 +58,9 @@ export class LedgerCore {
|
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
compact() {
|
|
61
|
+
if (this.released) {
|
|
62
|
+
throw new Error("file ledger: this authority was released (its store was closed) — refusing to compact a directory it no longer owns");
|
|
63
|
+
}
|
|
51
64
|
const lines = [...this.rows.values()].map((r) => JSON.stringify(r)).join("\n");
|
|
52
65
|
atomicWriteFile(this.paths.tmpDir, this.paths.snapshotPath, lines.length ? `${lines}\n` : "");
|
|
53
66
|
this.log.closeForSwap();
|
|
@@ -60,7 +73,10 @@ export class LedgerCore {
|
|
|
60
73
|
return;
|
|
61
74
|
if (this.table.get(this.key) === this)
|
|
62
75
|
this.table.delete(this.key);
|
|
76
|
+
this.released = true;
|
|
63
77
|
this.handle?.close();
|
|
78
|
+
this.fence?.release();
|
|
79
|
+
this.fence = undefined;
|
|
64
80
|
}
|
|
65
81
|
get pendingEvents() {
|
|
66
82
|
return this.events;
|
|
@@ -35,6 +35,13 @@ export declare class FileWorkflowRunStore implements WorkflowRunStore {
|
|
|
35
35
|
compactNow(): void;
|
|
36
36
|
/** Test/inspection helper: number of stored runs. */
|
|
37
37
|
get size(): number;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Release the append handle (best-effort). The LAST holder over the directory also drops its
|
|
40
|
+
* cross-process writer fence (`<dir>/LOCK`), so a successor process can open the same run ledger.
|
|
41
|
+
* There is no OTHER door: this store is never constructed by `FileStorageBackend`, so nothing else
|
|
42
|
+
* ever hands its fence back — the doc line that used to credit "the backend factory" here named a
|
|
43
|
+
* release that could not happen under any assembly, for a lock this store did not hold in the first
|
|
44
|
+
* place (see the class note above).
|
|
45
|
+
*/
|
|
39
46
|
close(): void;
|
|
40
47
|
}
|
|
@@ -4,6 +4,7 @@ import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdat
|
|
|
4
4
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
5
5
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
6
6
|
const runLedgers = new SharedLedgerTable({
|
|
7
|
+
label: "workflow-run ledger",
|
|
7
8
|
keyOf: (run) => run.id,
|
|
8
9
|
apply: (runs, ev) => {
|
|
9
10
|
if (ev.t === "delete") {
|
|
@@ -40,6 +40,29 @@ export interface LeadingCommandNameOptions {
|
|
|
40
40
|
* operator character counts, exactly as without this option.
|
|
41
41
|
*/
|
|
42
42
|
quotedOperatorsAreText?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Read a PATH-PREFIXED `argv[0]` (`./gradlew`, `/usr/bin/git`, `bin/tool`) as an ordinary command
|
|
45
|
+
* name instead of refusing it.
|
|
46
|
+
*
|
|
47
|
+
* OPT-IN, and used by ONE family of callers: the persisted permission-RULE lane
|
|
48
|
+
* ({@link import("../../core/permission-rule-model.js").parseAllowRuleText} and its matcher), where
|
|
49
|
+
* both sides of the comparison are literal text a person read on an approval card. The refusal this
|
|
50
|
+
* option lifts exists for the argv[0]-NAME filters (the read-only allowlist, the coarse
|
|
51
|
+
* command-name policy, the skill specifier): those compare a bare token against a name set, and a
|
|
52
|
+
* path prefix is how a caller reaches a program the set never vetted. A rule lane compares the whole
|
|
53
|
+
* command line instead, so `./gradlew` there is not a way past a name set — it IS the name that was
|
|
54
|
+
* approved, and refusing it made the single most ordinary build command in a repository unable to
|
|
55
|
+
* carry a standing approval at all.
|
|
56
|
+
*
|
|
57
|
+
* What it does NOT lift: the argv[0] metacharacter refusal (quotes/braces/globs/`~` still make the
|
|
58
|
+
* parsed token differ from the program bash runs), the leading env-assignment refusal, and every
|
|
59
|
+
* shell operator. And it does not make a path-prefixed name equal its basename anywhere — the rule
|
|
60
|
+
* lane matches on the text as written, so `./gradlew` and `gradlew` stay two different commands. The
|
|
61
|
+
* one place a basename IS taken is the rule lane's own interpreter refusal, which must read
|
|
62
|
+
* `/usr/bin/node` as `node` (see
|
|
63
|
+
* {@link import("../../core/permission-rule-model.js").BARE_INTERPRETER_NAMES}).
|
|
64
|
+
*/
|
|
65
|
+
pathPrefixedNameIsText?: boolean;
|
|
43
66
|
}
|
|
44
67
|
/**
|
|
45
68
|
* The SINGLE fail-closed simple-command parser shared by `bash_readonly` ({@link coarseReadonlyCheck}), the
|
|
@@ -48,6 +71,8 @@ export interface LeadingCommandNameOptions {
|
|
|
48
71
|
* `argv[0]` command NAME of a SINGLE simple command, rejecting anything that could chain past or escape an
|
|
49
72
|
* argv[0]-name filter: shell operators (pipes / redirects / `;` / `&&` / `$(…)` / subshells / backticks /
|
|
50
73
|
* newlines / backslash), a path-prefixed command (`/usr/bin/foo`), or a leading env-assignment (`FOO=bar cmd`).
|
|
74
|
+
* The path-prefix refusal is the one arm a caller may lift, opt-in and for the rule lane only — see
|
|
75
|
+
* {@link LeadingCommandNameOptions.pathPrefixedNameIsText} for why it is sound exactly there.
|
|
51
76
|
*
|
|
52
77
|
* Returns `{ name }` for a parseable single bare command, or `{ reject }` with a human reason otherwise. It
|
|
53
78
|
* does NOT inspect ARGUMENTS for write flags or consult any allowlist — that is the caller's job (the
|
|
@@ -66,6 +91,52 @@ export declare function parseLeadingCommandName(command: string, options?: Leadi
|
|
|
66
91
|
* design/154: this `effect:"read"` DECLARATION face deliberately stays strict-single-command; only the
|
|
67
92
|
* classify face ({@link classifyCompoundReadonly} via {@link import("./fs-bash.js").bashReversibilityProbe}) segments compounds. */
|
|
68
93
|
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>, options?: LeadingCommandNameOptions): string | undefined;
|
|
94
|
+
/** What {@link splitShellCompoundSegments} produces: the connector-delimited segments, plus which of
|
|
95
|
+
* them a PIPE fed (the read-only classifier's stdin arm needs that distinction; a rule lane does not). */
|
|
96
|
+
export interface ShellCompoundSegments {
|
|
97
|
+
segments: string[];
|
|
98
|
+
/** Parallel to {@link segments}: was this segment preceded by `|` (rather than `;`/`&&`/`||`/nothing)? */
|
|
99
|
+
pipeFed: boolean[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Split a compound command into the segments bash would run, or refuse the whole string.
|
|
103
|
+
*
|
|
104
|
+
* THE one segmentation in this repository. Extracted from {@link classifyCompoundReadonlyDetailed}
|
|
105
|
+
* (whose behaviour it reproduces exactly) when a SECOND face needed segments — the persisted
|
|
106
|
+
* permission-rule lane, which must judge every segment of a compound against the deny/ask rules rather
|
|
107
|
+
* than reading the whole string as one unmatched blob. The same argument that keeps ONE
|
|
108
|
+
* {@link parseLeadingCommandName} applies with more force here: a second splitter would drift, and
|
|
109
|
+
* drift in a segmentation is drift in what a deny rule is even looking at.
|
|
110
|
+
*
|
|
111
|
+
* Three steps, in this order and load-bearing:
|
|
112
|
+
* 1. WHOLE-STRING hard reject of {@link SHELL_SEGMENT_HARD_REJECT} — redirection, substitution,
|
|
113
|
+
* subshells, escapes, line breaks. This is what makes step 3's quote mask EXACT: with those
|
|
114
|
+
* characters gone, `'`/`"` pairing is the entirety of quoting, so the mask's regions are exactly
|
|
115
|
+
* bash's quoted regions and the unquoted connectors are exactly bash's command boundaries. Callers
|
|
116
|
+
* that tolerate a narrow redirection subset (the read-only classify face and its two data-free
|
|
117
|
+
* spellings) remove those words BEFORE calling; nothing here restores them.
|
|
118
|
+
* 2. one trailing `;` is stripped — a no-op terminator, not an empty command. OPTIONAL, because it is
|
|
119
|
+
* the one step whose answer depends on what the caller is asking. A face that asks "which programs
|
|
120
|
+
* does this run" wants it stripped (`ls;` runs `ls`, nothing else). A face that asks "is this ONE
|
|
121
|
+
* command" must NOT strip it: `"keep"` makes `ls;` two segments, the second empty, so the caller's
|
|
122
|
+
* own empty-command rule puts the whole string outside its lane — which is what keeps a statement
|
|
123
|
+
* like "this rule form never matches a command containing a connector" literally true instead of
|
|
124
|
+
* true-except-for-one-spelling.
|
|
125
|
+
* 3. the quote-aware connector scan over `;` `&&` `||` `|`. A lone `&` is REFUSED rather than split
|
|
126
|
+
* on: it backgrounds a process that outlives the command, which is not a combinator any face here
|
|
127
|
+
* is willing to reason about. Unbalanced quoting disables the mask, which degrades to the
|
|
128
|
+
* quote-blind scan — strictly MORE segments, each of which the caller must still vet, i.e. the
|
|
129
|
+
* fail-closed direction.
|
|
130
|
+
*
|
|
131
|
+
* Empty segments are NOT refused here (`;;`, a leading `;`, a trailing `&&`) — they come back as empty
|
|
132
|
+
* strings, and every caller's per-segment vetting rejects them through its own empty-command rule. The
|
|
133
|
+
* split does not judge segments; it says where they are.
|
|
134
|
+
*/
|
|
135
|
+
export declare function splitShellCompoundSegments(source: string, options?: {
|
|
136
|
+
trailingTerminator?: "strip" | "keep";
|
|
137
|
+
}): ShellCompoundSegments | {
|
|
138
|
+
reject: string;
|
|
139
|
+
};
|
|
69
140
|
/**
|
|
70
141
|
* RB-412 — the READ-BOUNDARY face of the read-only classification (opt-in).
|
|
71
142
|
*
|
|
@@ -45,8 +45,9 @@ export function parseLeadingCommandName(command, options) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
const first = trimmed.split(/\s+/)[0];
|
|
48
|
-
if (first.includes("/"))
|
|
48
|
+
if (first.includes("/") && options?.pathPrefixedNameIsText !== true) {
|
|
49
49
|
return { reject: "the command must be a bare name resolved via PATH (no path prefix)" };
|
|
50
|
+
}
|
|
50
51
|
if (first.includes("="))
|
|
51
52
|
return { reject: "leading environment-variable assignments are not allowed" };
|
|
52
53
|
if (/["'{}*?[\]~]/.test(first))
|
|
@@ -62,6 +63,55 @@ export function coarseReadonlyCheck(command, allow, options) {
|
|
|
62
63
|
return undefined;
|
|
63
64
|
}
|
|
64
65
|
const SHELL_SEGMENT_HARD_REJECT = /[<>$()`\n\r\\]/;
|
|
66
|
+
export function splitShellCompoundSegments(source, options) {
|
|
67
|
+
if (SHELL_SEGMENT_HARD_REJECT.test(source)) {
|
|
68
|
+
return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
69
|
+
}
|
|
70
|
+
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
71
|
+
const segments = [];
|
|
72
|
+
const pipeFed = [false];
|
|
73
|
+
const mask = quoteMask(scanned);
|
|
74
|
+
const quoted = (i) => mask.balanced && mask.quoted[i] === true;
|
|
75
|
+
let cur = "";
|
|
76
|
+
for (let i = 0; i < scanned.length; i++) {
|
|
77
|
+
const c = scanned[i];
|
|
78
|
+
if (quoted(i)) {
|
|
79
|
+
cur += c;
|
|
80
|
+
}
|
|
81
|
+
else if (c === "&") {
|
|
82
|
+
if (scanned[i + 1] === "&") {
|
|
83
|
+
segments.push(cur);
|
|
84
|
+
cur = "";
|
|
85
|
+
pipeFed.push(false);
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
return { reject: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
|
|
90
|
+
}
|
|
91
|
+
else if (c === "|") {
|
|
92
|
+
if (scanned[i + 1] === "|") {
|
|
93
|
+
segments.push(cur);
|
|
94
|
+
cur = "";
|
|
95
|
+
pipeFed.push(false);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
segments.push(cur);
|
|
100
|
+
cur = "";
|
|
101
|
+
pipeFed.push(true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (c === ";") {
|
|
105
|
+
segments.push(cur);
|
|
106
|
+
cur = "";
|
|
107
|
+
pipeFed.push(false);
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
cur += c;
|
|
111
|
+
}
|
|
112
|
+
segments.push(cur);
|
|
113
|
+
return { segments, pipeFed };
|
|
114
|
+
}
|
|
65
115
|
const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
|
|
66
116
|
function stripExemptRedirections(command) {
|
|
67
117
|
if (!command.includes(">"))
|
|
@@ -532,52 +582,13 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
532
582
|
if (!trimmed)
|
|
533
583
|
return { reason: "empty command" };
|
|
534
584
|
const redirectionStripped = stripExemptRedirections(trimmed);
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
const quoted = (i) => mask.balanced && mask.quoted[i] === true;
|
|
543
|
-
let cur = "";
|
|
544
|
-
for (let i = 0; i < source.length; i++) {
|
|
545
|
-
const c = source[i];
|
|
546
|
-
if (quoted(i)) {
|
|
547
|
-
cur += c;
|
|
548
|
-
}
|
|
549
|
-
else if (c === "&") {
|
|
550
|
-
if (source[i + 1] === "&") {
|
|
551
|
-
segments.push(cur);
|
|
552
|
-
cur = "";
|
|
553
|
-
pipeFed.push(false);
|
|
554
|
-
i++;
|
|
555
|
-
}
|
|
556
|
-
else
|
|
557
|
-
return { reason: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
|
|
558
|
-
}
|
|
559
|
-
else if (c === "|") {
|
|
560
|
-
if (source[i + 1] === "|") {
|
|
561
|
-
segments.push(cur);
|
|
562
|
-
cur = "";
|
|
563
|
-
pipeFed.push(false);
|
|
564
|
-
i++;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
segments.push(cur);
|
|
568
|
-
cur = "";
|
|
569
|
-
pipeFed.push(true);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
else if (c === ";") {
|
|
573
|
-
segments.push(cur);
|
|
574
|
-
cur = "";
|
|
575
|
-
pipeFed.push(false);
|
|
576
|
-
}
|
|
577
|
-
else
|
|
578
|
-
cur += c;
|
|
579
|
-
}
|
|
580
|
-
segments.push(cur);
|
|
585
|
+
const terminatorStripped = redirectionStripped.endsWith(";")
|
|
586
|
+
? redirectionStripped.slice(0, -1)
|
|
587
|
+
: redirectionStripped;
|
|
588
|
+
const split = splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
589
|
+
if ("reject" in split)
|
|
590
|
+
return { reason: split.reject };
|
|
591
|
+
const { segments, pipeFed } = split;
|
|
581
592
|
for (const segment of segments) {
|
|
582
593
|
const reason = coarseReadonlyCheck(segment, allow, { quotedOperatorsAreText: true });
|
|
583
594
|
if (reason !== undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
"count":
|
|
3
|
+
"count": 1651,
|
|
4
4
|
"exports": {
|
|
5
5
|
"A2ATaskState": "type",
|
|
6
6
|
"A2ATaskStateReversal": "type",
|
|
@@ -319,6 +319,8 @@
|
|
|
319
319
|
"FileStorageBackend": "class",
|
|
320
320
|
"FileStorageBackendOptions": "interface",
|
|
321
321
|
"FileStorageCorruptReadInfo": "interface",
|
|
322
|
+
"FileStoreLockError": "class",
|
|
323
|
+
"FileStoreLockErrorCode": "type",
|
|
322
324
|
"FileStrategyStore": "class",
|
|
323
325
|
"FileStrategyStoreOptions": "interface",
|
|
324
326
|
"FileToolResultStore": "class",
|