@sema-agent/core 5.65.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +11 -0
- package/dist/agents/verify.d.ts +1 -1
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/errors.d.ts +29 -0
- package/dist/brain/errors.js +20 -0
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +1 -0
- package/dist/brain/status-sink.js +12 -1
- package/dist/brain/stream-engine.js +17 -6
- package/dist/core/auto-compaction.d.ts +26 -0
- package/dist/core/auto-compaction.js +7 -2
- package/dist/core/auto-mode-arming.d.ts +138 -0
- package/dist/core/auto-mode-arming.js +181 -0
- package/dist/core/auto-mode-defaults.d.ts +13 -0
- package/dist/core/auto-mode-defaults.js +5 -0
- package/dist/core/auto-mode-prompt.d.ts +14 -3
- package/dist/core/auto-mode-prompt.js +10 -7
- package/dist/core/auto-mode-rebuild.d.ts +75 -0
- package/dist/core/auto-mode-rebuild.js +41 -0
- package/dist/core/auto-mode.d.ts +15 -0
- package/dist/core/auto-mode.js +4 -2
- package/dist/core/checkpoint-store.d.ts +18 -0
- package/dist/core/context-edit.d.ts +47 -5
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/file-history-retention.d.ts +106 -0
- package/dist/core/file-history-retention.js +36 -0
- package/dist/core/file-history-store.d.ts +768 -0
- package/dist/core/file-history-store.js +880 -0
- package/dist/core/governance-codes.d.ts +13 -3
- package/dist/core/governance-codes.js +15 -0
- package/dist/core/hooks.d.ts +39 -7
- package/dist/core/hooks.js +38 -21
- package/dist/core/lsp.d.ts +2 -2
- package/dist/core/mcp.d.ts +29 -7
- package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
- package/dist/core/memory-engine/consolidation-driver.js +71 -4
- package/dist/core/memory-engine/consolidation.d.ts +25 -2
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +84 -1
- package/dist/core/memory-engine/distiller.js +68 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +328 -15
- package/dist/core/memory-engine/engine.js +355 -29
- package/dist/core/memory-engine/file-backend.d.ts +30 -0
- package/dist/core/memory-engine/file-backend.js +14 -13
- package/dist/core/memory-engine/frontmatter.d.ts +22 -1
- package/dist/core/memory-engine/frontmatter.js +3 -0
- package/dist/core/memory-engine/header-hints.d.ts +5 -0
- package/dist/core/memory-engine/index.d.ts +5 -4
- package/dist/core/memory-engine/index.js +5 -4
- package/dist/core/memory-engine/layout.d.ts +88 -2
- package/dist/core/memory-engine/layout.js +112 -3
- package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
- package/dist/core/memory-engine/provenance-wording.js +3 -0
- package/dist/core/memory-engine/tools.d.ts +89 -8
- package/dist/core/memory-engine/tools.js +263 -22
- package/dist/core/memory-engine/types.d.ts +64 -1
- package/dist/core/memory-recall.d.ts +6 -0
- package/dist/core/memory.d.ts +27 -1
- package/dist/core/memory.js +16 -2
- package/dist/core/permission-rule-consent.d.ts +62 -2
- package/dist/core/permission-rule-consent.js +105 -14
- package/dist/core/permission-rule-model.d.ts +118 -16
- package/dist/core/permission-rule-model.js +57 -9
- package/dist/core/permission-rule-session.d.ts +124 -0
- package/dist/core/permission-rule-session.js +121 -0
- package/dist/core/permission-rule-store.d.ts +65 -2
- package/dist/core/permission-rule-store.js +75 -16
- package/dist/core/permission-rule-sync.d.ts +9 -0
- package/dist/core/permission-rule-sync.js +52 -19
- package/dist/core/retention-policy.d.ts +9 -0
- package/dist/core/retention-policy.js +5 -2
- package/dist/core/retention.d.ts +13 -2
- package/dist/core/runner/assemble-result.d.ts +19 -1
- package/dist/core/runner/assemble-result.js +17 -2
- package/dist/core/runner/compaction-call-options.d.ts +93 -0
- package/dist/core/runner/compaction-call-options.js +3 -0
- package/dist/core/runner/memory-capture-optout.d.ts +80 -0
- package/dist/core/runner/memory-capture-optout.js +53 -0
- package/dist/core/runner/prepare-config-doors.d.ts +5 -0
- package/dist/core/runner/prepare-config-doors.js +16 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
- package/dist/core/runner/prepare-hands-readface.js +99 -7
- package/dist/core/runner/prepare-memory.d.ts +88 -0
- package/dist/core/runner/prepare-memory.js +305 -24
- package/dist/core/runner/prepare-task.d.ts +141 -1
- package/dist/core/runner/prepare-task.js +478 -81
- package/dist/core/runner/runtask.d.ts +9 -20
- package/dist/core/runner/runtask.js +133 -96
- package/dist/core/runner/session-file-state-replay.d.ts +18 -10
- package/dist/core/runner/session-file-state-replay.js +52 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/runner/turn-attachments.d.ts +22 -12
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +6 -1
- package/dist/core/session.d.ts +34 -1
- package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-history-store-contract.js +720 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
- package/dist/core/task-registry-shared.js +11 -1
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +172 -1
- package/dist/core/tool-policy.js +32 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +31 -2
- package/dist/core/types.d.ts +795 -89
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/workflow-run-store-contract.js +6 -2
- package/dist/core/workflow-run-store.d.ts +4 -1
- package/dist/engine/compaction/compaction.d.ts +88 -10
- package/dist/engine/compaction/compaction.js +109 -30
- package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
- package/dist/engine/execution-env/node-execution-env.js +28 -0
- package/dist/engine/harness/agent-harness.d.ts +52 -1
- package/dist/engine/harness/agent-harness.js +36 -1
- package/dist/engine/harness/types.d.ts +26 -1
- package/dist/engine/llm/types.d.ts +50 -4
- package/dist/engine/loop/agent-loop.d.ts +5 -1
- package/dist/engine/loop/agent-loop.js +25 -0
- package/dist/engine/loop/types.d.ts +19 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
- package/dist/engine/session/session.js +1 -1
- package/dist/index.d.ts +20 -9
- package/dist/index.js +16 -7
- package/dist/orchestration/run-workflow-tool.d.ts +20 -2
- package/dist/orchestration/run-workflow-tool.js +22 -3
- package/dist/orchestration/workflow-governance.d.ts +59 -1
- package/dist/orchestration/workflow-governance.js +61 -8
- package/dist/orchestration/workflow-meta.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +56 -13
- package/dist/orchestration/workflow-types.d.ts +78 -2
- package/dist/orchestration/workflow.d.ts +20 -0
- package/dist/orchestration/workflow.js +163 -14
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompts/default.d.ts +7 -7
- package/dist/stores/file/file-history-store.d.ts +368 -0
- package/dist/stores/file/file-history-store.js +1248 -0
- package/dist/stores/file/index.d.ts +22 -13
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/permission-rule-store.d.ts +11 -0
- package/dist/stores/file/permission-rule-store.js +23 -9
- package/dist/stores/file/strategy-store.d.ts +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
- package/dist/tools/fs/bash-readonly-classifier.js +106 -4
- package/dist/tools/fs/fs-bash.js +9 -5
- package/dist/tools/fs/fs-shared.d.ts +52 -1
- package/dist/tools/fs/fs-shared.js +14 -0
- package/dist/tools/fs/fs-write.d.ts +5 -5
- package/dist/tools/fs/fs-write.js +71 -14
- package/dist/tools/fs/index.d.ts +6 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +2 -1
- package/package.json +5 -1
- package/test/export-surface.snapshot.json +169 -23
- package/dist/core/file-snapshot-store.d.ts +0 -165
- package/dist/core/file-snapshot-store.js +0 -259
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
- package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
- package/dist/stores/file/file-snapshot-store.d.ts +0 -58
- package/dist/stores/file/file-snapshot-store.js +0 -353
|
@@ -4,12 +4,13 @@ import type { ToolResultStore } from "../../core/tool-result-store.js";
|
|
|
4
4
|
import { type EvictPolicy } from "../../core/session-store.js";
|
|
5
5
|
import type { SessionStore } from "../../core/session.js";
|
|
6
6
|
import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
7
|
+
import { type FileFileHistoryStoreOptions } from "./file-history-store.js";
|
|
8
|
+
import type { FileHistoryStore } from "../../core/file-history-store.js";
|
|
7
9
|
import type { StrategyStore } from "../../core/strategy-store.js";
|
|
8
10
|
import type { BackgroundAgentStore } from "../../core/background-agent-store.js";
|
|
9
11
|
import type { MailboxStore } from "../../core/mailbox-store.js";
|
|
10
12
|
import type { RosterStore } from "../../agents/roster-store.js";
|
|
11
13
|
import type { SessionPolicyStore } from "../../core/session-policy-store.js";
|
|
12
|
-
import type { FileSnapshotStore } from "../../core/file-snapshot-store.js";
|
|
13
14
|
import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
|
|
14
15
|
import type { UsageWindowStore } from "../../core/usage-window-store.js";
|
|
15
16
|
export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
@@ -17,7 +18,7 @@ export { FileMemoryStore } from "./memory-store.js";
|
|
|
17
18
|
export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
|
|
18
19
|
export { FileToolResultStore } from "./tool-result-store.js";
|
|
19
20
|
export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type SessionPolicyCorruptReadInfo } from "./session-policy-store.js";
|
|
20
|
-
export {
|
|
21
|
+
export { FileFileHistoryStore, type FileFileHistoryStoreOptions } from "./file-history-store.js";
|
|
21
22
|
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
|
|
22
23
|
export { FileUsageWindowStore } from "./usage-window-store.js";
|
|
23
24
|
export { FileStrategyStore, type FileStrategyStoreOptions } from "./strategy-store.js";
|
|
@@ -64,9 +65,13 @@ export interface FileStorageBackendOptions {
|
|
|
64
65
|
evict?: EvictPolicy;
|
|
65
66
|
/** Checkpoint store tuning (fsync cadence for `put`/`setPendingSteer`, ledger compaction threshold). */
|
|
66
67
|
checkpoint?: FileCheckpointStoreOptions;
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* design/381 §片2 — file-history tuning, today just the boundary RETENTION bound (omitted ⇒ the
|
|
70
|
+
* engine default; `{ retention: { keep: "unbounded" } }` hands the GC schedule to the deployment).
|
|
71
|
+
* The corrupt-read sink is deliberately NOT accepted here: it is wired from {@link
|
|
72
|
+
* FileStorageBackendOptions.onCorruptRead} above, because one failure grammar takes one sink.
|
|
73
|
+
*/
|
|
74
|
+
fileHistory?: Omit<FileFileHistoryStoreOptions, "onCorruptRead">;
|
|
70
75
|
/** design/81 Slice 5 — an optional {@link import("../../core/memory.js").Embedder}. Injected ⇒ the memory
|
|
71
76
|
* store does portable (in-process) vector recall over a rebuildable sidecar index (`vectorMode:"portable"`);
|
|
72
77
|
* absent ⇒ the lexical floor, byte-identical to before. Config-driven (the deployment injects it); OFF by
|
|
@@ -84,10 +89,9 @@ export interface FileStorageBackendOptions {
|
|
|
84
89
|
* nowhere for the deployments that need it most.
|
|
85
90
|
*
|
|
86
91
|
* Reach TODAY: {@link FileStorageBackend.sessionPolicyStore} (rules read as ABSENT ⇒ an unconstrained
|
|
87
|
-
* task)
|
|
88
|
-
* could not read) and
|
|
89
|
-
*
|
|
90
|
-
* store that grows the seat later is wired here in the same constructor.
|
|
92
|
+
* task) and {@link FileStorageBackend.sessionStore}'s repo (a session listing that silently omits what
|
|
93
|
+
* it could not read). The aggregated roster and mailbox stores receive the same sink. A store that
|
|
94
|
+
* grows the seat later is wired here in the same constructor.
|
|
91
95
|
*
|
|
92
96
|
* The payload is the UNION shape ({@link FileStorageCorruptReadInfo}): `path` + `reason` always,
|
|
93
97
|
* with the session-policy face's `sessionId`/`principal` present only when the reading store keys by
|
|
@@ -123,10 +127,6 @@ export declare class FileStorageBackend {
|
|
|
123
127
|
/** design/99 §E6 — file-backed per-session permission rules (E6). Wire into `RunnerDeps.sessionPolicyStore`
|
|
124
128
|
* for a local deployment that needs session rules to survive a restart. */
|
|
125
129
|
readonly sessionPolicyStore: SessionPolicyStore;
|
|
126
|
-
/** design/101 §E19 — file-backed working-tree snapshots (rewind-files). Wire into
|
|
127
|
-
* `RunnerDeps.fileSnapshotStore`. Content-addressed blobs persist across restarts (local-only; a remote env
|
|
128
|
-
* defers rewind to its own VM-snapshot backend). */
|
|
129
|
-
readonly fileSnapshotStore: FileSnapshotStore;
|
|
130
130
|
/** SVC-2 (onboarded per) — file-backed durable workflow resume journal. Wire into
|
|
131
131
|
* `RunnerDeps.workflowJournalStore` so `resumeFromRunId` survives a process restart on a TOC box. */
|
|
132
132
|
readonly workflowJournalStore: WorkflowJournalStore;
|
|
@@ -152,6 +152,15 @@ export declare class FileStorageBackend {
|
|
|
152
152
|
/** design/147 S1c — the durable name→agent roster (layer 0.5 resolution), `root/roster.json`.
|
|
153
153
|
* Wire into `RunnerDeps.rosterStore` so name-addressing survives a restart alongside the rows. */
|
|
154
154
|
readonly rosterStore: RosterStore;
|
|
155
|
+
/**
|
|
156
|
+
* design/381 slice 2 — the durable per-edited-file rewind history over the same data root
|
|
157
|
+
* (`root/file-history/`). Wire into `RunnerDeps.fileHistoryStore` so `/rewind` survives a restart:
|
|
158
|
+
* WIRING IS THE OPT-IN (DV-9), so a deployment that omits it runs with rewind off, exactly as
|
|
159
|
+
* before this member existed. It replaces the retired whole-tree `fileSnapshotStore` member —
|
|
160
|
+
* per-edited-file means no size gate, no exclusion table, no tree traversal, and files the agent
|
|
161
|
+
* never edited are structurally out of reach of a restore.
|
|
162
|
+
*/
|
|
163
|
+
readonly fileHistoryStore: FileHistoryStore;
|
|
155
164
|
/**
|
|
156
165
|
* design/84 Seam B (TOC) — the per-scope consolidation lock for `consolidateScope`'s `acquire` injection
|
|
157
166
|
* point: `consolidateScope(scope, { store: backend.memoryStore, llm, acquire: backend.consolidationLock })`.
|
|
@@ -8,19 +8,19 @@ import { FileMemoryStore } from "./memory-store.js";
|
|
|
8
8
|
import { FileSessionRepo } from "./session-store.js";
|
|
9
9
|
import { FileToolResultStore } from "./tool-result-store.js";
|
|
10
10
|
import { FileSessionPolicyStore } from "./session-policy-store.js";
|
|
11
|
-
import { FileFileSnapshotStore } from "./file-snapshot-store.js";
|
|
12
11
|
import { FileWorkflowJournalStore } from "./workflow-journal-store.js";
|
|
13
12
|
import { FileUsageWindowStore } from "./usage-window-store.js";
|
|
14
13
|
import { FileBackgroundAgentStore } from "./background-agent-store.js";
|
|
15
14
|
import { FileMailboxStore } from "./mailbox-store.js";
|
|
16
15
|
import { FileRosterStore } from "../../agents/roster-store.js";
|
|
17
16
|
import { FileStrategyStore } from "./strategy-store.js";
|
|
17
|
+
import { FileFileHistoryStore } from "./file-history-store.js";
|
|
18
18
|
export { FileCheckpointStore } from "./checkpoint-store.js";
|
|
19
19
|
export { FileMemoryStore } from "./memory-store.js";
|
|
20
20
|
export { FileSessionRepo } from "./session-store.js";
|
|
21
21
|
export { FileToolResultStore } from "./tool-result-store.js";
|
|
22
22
|
export { FileSessionPolicyStore } from "./session-policy-store.js";
|
|
23
|
-
export {
|
|
23
|
+
export { FileFileHistoryStore } from "./file-history-store.js";
|
|
24
24
|
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
|
|
25
25
|
export { FileUsageWindowStore } from "./usage-window-store.js";
|
|
26
26
|
export { FileStrategyStore } from "./strategy-store.js";
|
|
@@ -36,13 +36,13 @@ export class FileStorageBackend {
|
|
|
36
36
|
memoryStore;
|
|
37
37
|
toolResultStore;
|
|
38
38
|
sessionPolicyStore;
|
|
39
|
-
fileSnapshotStore;
|
|
40
39
|
workflowJournalStore;
|
|
41
40
|
usageWindowStore;
|
|
42
41
|
backgroundAgentStore;
|
|
43
42
|
mailboxStore;
|
|
44
43
|
strategyStore;
|
|
45
44
|
rosterStore;
|
|
45
|
+
fileHistoryStore;
|
|
46
46
|
consolidationLock;
|
|
47
47
|
lock;
|
|
48
48
|
fileAgentRows;
|
|
@@ -72,7 +72,6 @@ export class FileStorageBackend {
|
|
|
72
72
|
this.memoryStore = guardedMemoryStore(this.fileMemory, opts.utilityGate);
|
|
73
73
|
this.toolResultStore = new FileToolResultStore(this.root);
|
|
74
74
|
this.sessionPolicyStore = new FileSessionPolicyStore(this.root, corruptRead);
|
|
75
|
-
this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds, corruptRead);
|
|
76
75
|
this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
|
|
77
76
|
built.push(() => this.fileWorkflowJournal.dispose());
|
|
78
77
|
this.workflowJournalStore = this.fileWorkflowJournal;
|
|
@@ -93,6 +92,7 @@ export class FileStorageBackend {
|
|
|
93
92
|
}
|
|
94
93
|
: {}),
|
|
95
94
|
});
|
|
95
|
+
this.fileHistoryStore = new FileFileHistoryStore(this.root, { ...(opts.fileHistory ?? {}), ...(corruptRead ?? {}) });
|
|
96
96
|
this.consolidationLock = createFileConsolidationLock(join(this.root, "consolidation-locks"));
|
|
97
97
|
}
|
|
98
98
|
catch (err) {
|
|
@@ -111,6 +111,17 @@ declare class FilePermissionRuleStore implements WritablePermissionRuleStore {
|
|
|
111
111
|
/** Read and validate. Any refusal answers with an empty set AND says so — never a silent empty store. */
|
|
112
112
|
private read;
|
|
113
113
|
private disclose;
|
|
114
|
+
/**
|
|
115
|
+
* design/382 §4.3 — the AT-REST half of the durable two-member scope face: rows carrying a session
|
|
116
|
+
* scope, read out of persisted bytes (a hand edit or a foreign writer — the engine's own write arms
|
|
117
|
+
* refuse them loudly), are DROPPED from every read face and DISCLOSED. Never quarantined (the
|
|
118
|
+
* quarantine area is itself durable), never silently ridden: a session authorization must not gain
|
|
119
|
+
* an afterlife by being planted where the store cannot legally hold it. The write base
|
|
120
|
+
* (`current()`) is deliberately NOT screened — foreign bytes are left in place for a person to look
|
|
121
|
+
* at, exactly like every other damaged-row posture here; they simply never reach a live view, the
|
|
122
|
+
* sync wire (`readRaw`) or adjudication.
|
|
123
|
+
*/
|
|
124
|
+
private screenDurableRows;
|
|
114
125
|
private write;
|
|
115
126
|
list(): Promise<StoredAllowRules>;
|
|
116
127
|
/** The quarantine area (design/182 §5.2/§8.3): introspection only — never part of `list()`. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { closeSync, constants as FS, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
-
import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, foldDelta, } from "../../core/permission-rule-store.js";
|
|
4
|
+
import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, } from "../../core/permission-rule-store.js";
|
|
5
5
|
import { canonicalize } from "../../core/canonical-json.js";
|
|
6
6
|
import { BootLock } from "./fs-atomic.js";
|
|
7
7
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
@@ -158,6 +158,17 @@ class FilePermissionRuleStore {
|
|
|
158
158
|
catch {
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
screenDurableRows(file) {
|
|
162
|
+
const isSession = (scope) => scope?.kind === "session";
|
|
163
|
+
const rules = file.rules.filter((r) => !isSession(r?.scope));
|
|
164
|
+
const tombstones = file.tombstones.filter((t) => !isSession(t?.scope));
|
|
165
|
+
const quarantined = (file.quarantined ?? []).filter((q) => !isSession(q?.scope));
|
|
166
|
+
const dropped = file.rules.length - rules.length + (file.tombstones.length - tombstones.length) + ((file.quarantined ?? []).length - quarantined.length);
|
|
167
|
+
if (dropped > 0) {
|
|
168
|
+
this.disclose(`${this.file} carries ${dropped} session-scope row(s) — a session authorization cannot live in the durable store (design/382 §4.3); those rows are ignored on every read face and never ride sync`);
|
|
169
|
+
}
|
|
170
|
+
return { rules, tombstones, quarantined };
|
|
171
|
+
}
|
|
161
172
|
write(next) {
|
|
162
173
|
assertSafeDir(this.dir);
|
|
163
174
|
atomicPublish(this.dir, this.file, JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2));
|
|
@@ -166,16 +177,17 @@ class FilePermissionRuleStore {
|
|
|
166
177
|
const r = this.read();
|
|
167
178
|
if (!("file" in r))
|
|
168
179
|
return { ...EMPTY_READ };
|
|
180
|
+
const screened = this.screenDurableRows(r.file);
|
|
169
181
|
return {
|
|
170
|
-
rules: applyTombstones(
|
|
171
|
-
tombstones:
|
|
182
|
+
rules: applyTombstones(screened.rules, screened.tombstones),
|
|
183
|
+
tombstones: screened.tombstones,
|
|
172
184
|
rev: r.file.rev,
|
|
173
185
|
checksum: r.file.checksum,
|
|
174
186
|
};
|
|
175
187
|
}
|
|
176
188
|
async quarantined() {
|
|
177
189
|
const r = this.read();
|
|
178
|
-
return "file" in r ? (r.file.quarantined
|
|
190
|
+
return "file" in r ? this.screenDurableRows(r.file).quarantined : [];
|
|
179
191
|
}
|
|
180
192
|
async readOrgState() {
|
|
181
193
|
const r = this.read();
|
|
@@ -245,17 +257,19 @@ class FilePermissionRuleStore {
|
|
|
245
257
|
}),
|
|
246
258
|
readRaw: async () => this.serialize(() => {
|
|
247
259
|
const cur = this.current();
|
|
260
|
+
const screened = this.screenDurableRows(cur);
|
|
248
261
|
return {
|
|
249
262
|
actor: cur.actor,
|
|
250
263
|
counter: cur.counter,
|
|
251
264
|
rev: cur.rev,
|
|
252
|
-
rules: structuredClone(
|
|
253
|
-
tombstones: structuredClone(
|
|
265
|
+
rules: structuredClone(screened.rules),
|
|
266
|
+
tombstones: structuredClone(screened.tombstones),
|
|
254
267
|
...(cur.sync?.observedVector !== undefined ? { observedVector: structuredClone(cur.sync.observedVector) } : {}),
|
|
255
|
-
quarantined: structuredClone(
|
|
268
|
+
quarantined: structuredClone(screened.quarantined),
|
|
256
269
|
};
|
|
257
270
|
}),
|
|
258
271
|
apply: async (delta, opts) => this.serialize(() => {
|
|
272
|
+
delta = structuredClone(delta);
|
|
259
273
|
const cur = this.current();
|
|
260
274
|
if (cur.rev !== opts.expectedRev)
|
|
261
275
|
return { conflict: true, rev: cur.rev };
|
|
@@ -286,8 +300,8 @@ class FilePermissionRuleStore {
|
|
|
286
300
|
return { rev: cur.rev + 1, sync: report };
|
|
287
301
|
}
|
|
288
302
|
const next = delta.kind === "redemption-add"
|
|
289
|
-
? (assertRedemptionNotQuarantined(cur.quarantined ?? [], delta), { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) })
|
|
290
|
-
: (assertDeleteDeltaCarriesNoAdd(delta), { ...cur, rev: cur.rev + 1, tombstones: [...cur.tombstones, delta.tombstone] });
|
|
303
|
+
? (assertWriteDeltaScopeDurable(delta), assertRedemptionNotQuarantined(cur.quarantined ?? [], delta), { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) })
|
|
304
|
+
: (assertWriteDeltaScopeDurable(delta), assertDeleteDeltaCarriesNoAdd(delta), { ...cur, rev: cur.rev + 1, tombstones: [...cur.tombstones, delta.tombstone] });
|
|
291
305
|
if (!this.writeAndVerify(next)) {
|
|
292
306
|
throw new Error("the permission-rule store did not survive its own write; the change was not committed");
|
|
293
307
|
}
|
|
@@ -15,7 +15,7 @@ import { type StoredStrategy, type StrategyStore, type StrategyStoreIncident } f
|
|
|
15
15
|
* Layout: `<root>/<scopeDir>/<id>.json`, one strategy per file, whole-file atomic replace on write.
|
|
16
16
|
* `scopeDir = slug(scope) + "-" + sha256(scope).slice(0,24)` — the slug is a strict `[a-z0-9-]`
|
|
17
17
|
* whitelist purely for readability; IDENTITY lives in the 96-bit hash, so no scope value, however
|
|
18
|
-
*
|
|
18
|
+
* adversarial, can traverse out of the root or collide another scope's directory. Ids are gated to
|
|
19
19
|
* `[A-Za-z0-9_-]{1,64}` at save (the escalation loop mints UUIDs; the interface is public, so a
|
|
20
20
|
* hand-rolled id must not be able to name a path).
|
|
21
21
|
*
|
|
@@ -52,8 +52,8 @@ export declare class FileStrategyStore implements StrategyStore {
|
|
|
52
52
|
private scopeDirPath;
|
|
53
53
|
/** The scope dir must be a REAL directory — a symlink here means the store's namespace was tampered
|
|
54
54
|
* with (a link could point retrieval at another scope's data, or writes out of the root). The
|
|
55
|
-
* check-then-use window against a same-uid local
|
|
56
|
-
*
|
|
55
|
+
* check-then-use window against a same-uid untrusted local process is out of the threat model: such a
|
|
56
|
+
* process already holds the same rights as the store itself. */
|
|
57
57
|
private assertScopeDirSafe;
|
|
58
58
|
private incident;
|
|
59
59
|
/** Sweep crashed writers' stale temps (once per scope per instance). Fresh temps are left alone —
|
|
@@ -126,6 +126,18 @@ export declare function parseLeadingCommandName(command: string, options?: Leadi
|
|
|
126
126
|
* design/154: this `effect:"read"` DECLARATION face deliberately stays strict-single-command; only the
|
|
127
127
|
* classify face ({@link classifyCompoundReadonly} via {@link import("./fs-bash.js").bashReversibilityProbe}) segments compounds. */
|
|
128
128
|
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>, options?: LeadingCommandNameOptions): string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Does this text carry a bare redirection operator — the ONE metacharacter family
|
|
131
|
+
* {@link splitShellCompoundSegments} can be asked to tolerate at its whole-string gate (#490 修①,
|
|
132
|
+
* `redirection: "tolerate"`)?
|
|
133
|
+
*
|
|
134
|
+
* Exported because the tolerating caller owes a PER-SEGMENT judgement in its place, and that judgement
|
|
135
|
+
* must read the same character class this module tolerated — two hand-kept lists would drift, and a
|
|
136
|
+
* drift here is a segment nobody judged. Says nothing about what a caller should DO about a hit: the
|
|
137
|
+
* rule lane's offer face contributes no member for such a segment; the read-only classify face removes
|
|
138
|
+
* its exempt spellings before splitting at all.
|
|
139
|
+
*/
|
|
140
|
+
export declare function carriesShellRedirection(text: string): boolean;
|
|
129
141
|
/** What {@link splitShellCompoundSegments} produces: the connector-delimited segments, plus which of
|
|
130
142
|
* them a PIPE fed (the read-only classifier's stdin arm needs that distinction; a rule lane does not). */
|
|
131
143
|
export interface ShellCompoundSegments {
|
|
@@ -149,7 +161,10 @@ export interface ShellCompoundSegments {
|
|
|
149
161
|
* characters gone, `'`/`"` pairing is the entirety of quoting, so the mask's regions are exactly
|
|
150
162
|
* bash's quoted regions and the unquoted connectors are exactly bash's command boundaries. Callers
|
|
151
163
|
* that tolerate a narrow redirection subset (the read-only classify face and its two data-free
|
|
152
|
-
* spellings) remove those words BEFORE calling; nothing here restores them.
|
|
164
|
+
* spellings) remove those words BEFORE calling; nothing here restores them. `redirection:
|
|
165
|
+
* "tolerate"` (#490 修①) narrows this step to {@link rejectedSansRedirection} — the mask argument
|
|
166
|
+
* is untouched (`<`/`>` neither quote nor bound) and the refusal moves to the caller's per-segment
|
|
167
|
+
* vetting via {@link carriesShellRedirection}. The default refuses the whole string as it always did.
|
|
153
168
|
* 2. one trailing `;` is stripped — a no-op terminator, not an empty command. OPTIONAL, because it is
|
|
154
169
|
* the one step whose answer depends on what the caller is asking. A face that asks "which programs
|
|
155
170
|
* does this run" wants it stripped (`ls;` runs `ls`, nothing else). A face that asks "is this ONE
|
|
@@ -169,6 +184,7 @@ export interface ShellCompoundSegments {
|
|
|
169
184
|
*/
|
|
170
185
|
export declare function splitShellCompoundSegments(source: string, options?: {
|
|
171
186
|
trailingTerminator?: "strip" | "keep";
|
|
187
|
+
redirection?: "reject" | "tolerate";
|
|
172
188
|
}): ShellCompoundSegments | {
|
|
173
189
|
reject: string;
|
|
174
190
|
};
|
|
@@ -179,7 +195,7 @@ export declare function splitShellCompoundSegments(source: string, options?: {
|
|
|
179
195
|
* path: `ls /etc` and `cat /etc/passwd` classified read-only, and a deployment whose manual-mode shell
|
|
180
196
|
* gate auto-allows the read-only verdict served the whole filesystem with no approval prompt. CC 2.1.220
|
|
181
197
|
* demotes exactly these shapes (its per-command path extractor + working-directory containment check,
|
|
182
|
-
* pretty220 @457568) and renders an approval option naming the directory (@905642).
|
|
198
|
+
* pretty220 @457568) and renders an approval option naming the directory (@905642 (cli250.js:66007)).
|
|
183
199
|
*
|
|
184
200
|
* Supplying this object turns the check ON; omitting it leaves the classifier byte-identical to its
|
|
185
201
|
* pre-RB-412 behaviour, so a consumer pinned to an older calling convention is unaffected.
|
|
@@ -313,7 +329,7 @@ export interface CompoundReadonlyVerdict {
|
|
|
313
329
|
}
|
|
314
330
|
/**
|
|
315
331
|
* RB-412 — the single minting point for the out-of-root-read approval option text, so a gate rendering
|
|
316
|
-
* the prompt and any other consumer cannot drift apart. Shape follows CC 2.1.220 (@905642): the option
|
|
332
|
+
* the prompt and any other consumer cannot drift apart. Shape follows CC 2.1.220 (@905642 (cli250.js:66007)): the option
|
|
317
333
|
* shows the LAST segment of the grantable directory followed by a separator, not the full path.
|
|
318
334
|
*
|
|
319
335
|
* `directory` is the directory to be granted — a caller holding a filesystem (this classifier does not)
|
|
@@ -366,6 +382,74 @@ opts?: {
|
|
|
366
382
|
* `createBashReadonlyTool` is that caller for the `bash_readonly` leg and does it on a local env.
|
|
367
383
|
*/
|
|
368
384
|
export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|
|
385
|
+
/**
|
|
386
|
+
* #502 — what {@link classifyOutOfRootReadGate} answers. Two members and one rule between them:
|
|
387
|
+
* `paths` is EVIDENCE for a `gated` answer, never the answer itself. A caller decides on `gated`.
|
|
388
|
+
*/
|
|
389
|
+
export interface OutOfRootReadGateVerdict {
|
|
390
|
+
/** True ⇔ this command is structurally gated on the read boundary. */
|
|
391
|
+
readonly gated: boolean;
|
|
392
|
+
/** The RESOLVED out-of-root paths behind a `gated` answer. EMPTY when the gate fired for a reason
|
|
393
|
+
* that resolves none — a working-directory move this walk cannot follow, after which every relative
|
|
394
|
+
* operand is placed against a base bash has already left, so no path can be named honestly. Read it
|
|
395
|
+
* as display/triage evidence; emptiness is not "not gated". */
|
|
396
|
+
readonly paths: readonly string[];
|
|
397
|
+
}
|
|
398
|
+
/** Test seam for the lockstep pin that this mirror stays a SUBSET of the rule lane's curated table.
|
|
399
|
+
* Exported for that one assertion — production code reads the constant directly. */
|
|
400
|
+
export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
|
|
401
|
+
/**
|
|
402
|
+
* #502 — the OUT-OF-ROOT READ GATE asked as its OWN question: does any listed reader in this command
|
|
403
|
+
* name a path outside the allowed directories?
|
|
404
|
+
*
|
|
405
|
+
* {@link classifyCompoundReadonlyDetailed} answers a different question — "is this whole command
|
|
406
|
+
* auto-allowable?" — and it stops at the FIRST obstacle. That makes its `outOfRootRead` signal precise
|
|
407
|
+
* for the shape it was built for and silent for three shapes where the containment fact is just as
|
|
408
|
+
* true: a compound whose OTHER segment is not a listed reader (`cat <outside> | curl …` refuses on the
|
|
409
|
+
* unlisted name and never reaches the boundary walk at all), an operand that ALSO matches the
|
|
410
|
+
* sensitive-path deny judge (whose more-specific reason short-circuits the walk), and a recursive form
|
|
411
|
+
* whose traversal root sits outside. This face answers the containment question in all of them, so a
|
|
412
|
+
* consumer that must treat "reads outside the roots" as a STRUCTURAL gate rather than as one possible
|
|
413
|
+
* reason for a demotion has one predicate to read.
|
|
414
|
+
*
|
|
415
|
+
* What it judges and what it does not:
|
|
416
|
+
* · SEGMENTS whose PROGRAM is in `allow` — the deployment's own read-command table, the analogue of
|
|
417
|
+
* the reference implementation's path-restricted command list. The program is read after the two
|
|
418
|
+
* decorations the shell itself strips before choosing it (leading `NAME=value` assignments, a path
|
|
419
|
+
* prefix), so `/bin/cat` is judged as `cat`: recognizing readers by their literal spelling alone let
|
|
420
|
+
* a rule the persisted grammar admits (`Bash(/bin/cat:*)`) carry the exact read this gate exists for.
|
|
421
|
+
* Every other segment is skipped, so this face never invents a path judgement for a verb the
|
|
422
|
+
* deployment never declared readable.
|
|
423
|
+
* · `cd` is judged whether or not the deployment listed it, because the walk MODELS it (it threads the
|
|
424
|
+
* base across segments) and dropping it would place every later relative operand against a base bash
|
|
425
|
+
* has already left. A move the walk cannot model (`pushd`/`popd`) is not silently tolerated either —
|
|
426
|
+
* see the `paths`-less gated answer below.
|
|
427
|
+
* · The CONTAINMENT half only: {@link BashReadonlyRootBoundary.denyMatch} is deliberately NOT passed
|
|
428
|
+
* on. The deny judge outranks containment inside the walk and returns early, which would let a
|
|
429
|
+
* deny-listed operand HIDE the fact that it also sits outside the roots — the deny demotion is
|
|
430
|
+
* unchanged on the full face, and this one keeps answering its own question underneath it.
|
|
431
|
+
* · `face: "open"` is honoured: under the open read face containment is structurally satisfied, an
|
|
432
|
+
* out-of-roots operand is not a finding, and this gate is silent — a full-read deployment sees no
|
|
433
|
+
* gate here, by the same one mechanism §2.0 already defines.
|
|
434
|
+
*
|
|
435
|
+
* Answers `gated` with the resolved paths behind it, and NOT gated for every whole-string refusal (an
|
|
436
|
+
* unparseable grammar, a substitution, a redirection) and for an operand that cannot be resolved
|
|
437
|
+
* statically. That silence is the honest answer, not a miss: those shapes name no concrete directory,
|
|
438
|
+
* so there is nothing for a path gate to be about.
|
|
439
|
+
*
|
|
440
|
+
* It deliberately does NOT reuse {@link CompoundReadonlyVerdict}: one arm of this face is gated while
|
|
441
|
+
* resolving no path at all (the unmodelled working-directory move), and that arm cannot fill an
|
|
442
|
+
* `outOfRootPaths` whose contract says the entries ARE resolved out-of-root paths. A result type that
|
|
443
|
+
* can say "gated, and here is nothing" is the one that lets the arm exist without lying.
|
|
444
|
+
*
|
|
445
|
+
* The bounded poll-loop grammar is judged through its BODY, the same text that face re-classifies —
|
|
446
|
+
* without it, the one control structure this file auto-allows would be the one shape whose reads no
|
|
447
|
+
* gate looks at. The body is judged in a single pass (the per-iteration `cd` accumulation the
|
|
448
|
+
* auto-allow face models is not replicated here): a loop that climbs out of the roots over its beats
|
|
449
|
+
* is refused by that face on its own terms, and under-reporting here can only cost a gate, never grant
|
|
450
|
+
* one.
|
|
451
|
+
*/
|
|
452
|
+
export declare function classifyOutOfRootReadGate(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary): OutOfRootReadGateVerdict;
|
|
369
453
|
/**
|
|
370
454
|
* design/154 — compound read-only classification, reason-only face. Returns the demotion reason, or
|
|
371
455
|
* undefined when the command classifies read-only. ⚠️ `undefined` is NOT "safe to auto-execute":
|
|
@@ -66,8 +66,45 @@ export function coarseReadonlyCheck(command, allow, options) {
|
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
68
|
const SHELL_SEGMENT_HARD_REJECT = /[<>$()`\n\r\\]/;
|
|
69
|
+
export function carriesShellRedirection(text) {
|
|
70
|
+
return SHELL_REDIRECTION_OPERATORS.test(text);
|
|
71
|
+
}
|
|
72
|
+
const SHELL_REDIRECTION_OPERATORS = /[<>]/;
|
|
73
|
+
const SHELL_CONNECTOR_CHARS = /[|&;]/;
|
|
74
|
+
function redirectionPrecedesConnector(source) {
|
|
75
|
+
const mask = quoteMask(source);
|
|
76
|
+
const quoted = (i) => mask.balanced && mask.quoted[i] === true;
|
|
77
|
+
const isSpace = (c) => c === " " || c === "\t";
|
|
78
|
+
for (let i = 0; i < source.length; i++) {
|
|
79
|
+
if (quoted(i) || !SHELL_REDIRECTION_OPERATORS.test(source[i]))
|
|
80
|
+
continue;
|
|
81
|
+
let j = i + 1;
|
|
82
|
+
while (j < source.length && !quoted(j) && (SHELL_REDIRECTION_OPERATORS.test(source[j]) || isSpace(source[j])))
|
|
83
|
+
j++;
|
|
84
|
+
if (j < source.length && !quoted(j) && SHELL_CONNECTOR_CHARS.test(source[j]))
|
|
85
|
+
return true;
|
|
86
|
+
i = j - 1;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function rejectedSansRedirection(source) {
|
|
91
|
+
if (redirectionPrecedesConnector(source)) {
|
|
92
|
+
return "a redirection followed by `|`, `&` or `;` is not allowed — the operator's own characters cannot be told apart from a command boundary";
|
|
93
|
+
}
|
|
94
|
+
for (const ch of source) {
|
|
95
|
+
if (SHELL_SEGMENT_HARD_REJECT.test(ch) && !SHELL_REDIRECTION_OPERATORS.test(ch)) {
|
|
96
|
+
return "command/variable substitution, subshells, escapes, and line breaks are not allowed";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
69
101
|
export function splitShellCompoundSegments(source, options) {
|
|
70
|
-
if (
|
|
102
|
+
if (options?.redirection === "tolerate") {
|
|
103
|
+
const refused = rejectedSansRedirection(source);
|
|
104
|
+
if (refused !== undefined)
|
|
105
|
+
return { reject: refused };
|
|
106
|
+
}
|
|
107
|
+
else if (SHELL_SEGMENT_HARD_REJECT.test(source)) {
|
|
71
108
|
return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
72
109
|
}
|
|
73
110
|
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
@@ -677,15 +714,18 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
677
714
|
}
|
|
678
715
|
return findings;
|
|
679
716
|
}
|
|
680
|
-
|
|
717
|
+
function segmentCompoundForReadonly(command) {
|
|
681
718
|
const trimmed = command.trim();
|
|
682
719
|
if (!trimmed)
|
|
683
|
-
return {
|
|
720
|
+
return { reject: "empty command" };
|
|
684
721
|
const redirectionStripped = stripExemptRedirections(trimmed);
|
|
685
722
|
const terminatorStripped = redirectionStripped.endsWith(";")
|
|
686
723
|
? redirectionStripped.slice(0, -1)
|
|
687
724
|
: redirectionStripped;
|
|
688
|
-
|
|
725
|
+
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
726
|
+
}
|
|
727
|
+
export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts) {
|
|
728
|
+
const split = segmentCompoundForReadonly(command);
|
|
689
729
|
if ("reject" in split)
|
|
690
730
|
return { reason: split.reject };
|
|
691
731
|
const { segments, pipeFed } = split;
|
|
@@ -884,6 +924,68 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
884
924
|
return {};
|
|
885
925
|
return evaluateReadBoundary([toks], boundary);
|
|
886
926
|
}
|
|
927
|
+
const NOT_GATED = { gated: false, paths: [] };
|
|
928
|
+
const ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
929
|
+
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
930
|
+
const COMMAND_LAUNCHERS = new Set([
|
|
931
|
+
"env", "command", "builtin", "exec", "noglob", "xargs",
|
|
932
|
+
"sudo", "doas", "pkexec", "su", "runuser", "chroot", "setpriv",
|
|
933
|
+
"nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
|
|
934
|
+
"setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run", "parallel",
|
|
935
|
+
"strace", "ltrace", "valgrind", "firejail", "bwrap",
|
|
936
|
+
]);
|
|
937
|
+
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
938
|
+
const LAUNCHER_SCAN_HEAD = "cat";
|
|
939
|
+
function programBasename(word) {
|
|
940
|
+
return word.includes("/") ? (word.split("/").pop() ?? word) : word;
|
|
941
|
+
}
|
|
942
|
+
export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
943
|
+
const containmentOnly = {
|
|
944
|
+
roots: boundary.roots,
|
|
945
|
+
...(boundary.cwd !== undefined ? { cwd: boundary.cwd } : {}),
|
|
946
|
+
...(boundary.homeDir !== undefined ? { homeDir: boundary.homeDir } : {}),
|
|
947
|
+
...(boundary.face !== undefined ? { face: boundary.face } : {}),
|
|
948
|
+
};
|
|
949
|
+
const judge = (text) => {
|
|
950
|
+
const split = segmentCompoundForReadonly(text);
|
|
951
|
+
if ("reject" in split)
|
|
952
|
+
return NOT_GATED;
|
|
953
|
+
const readers = [];
|
|
954
|
+
let baseMoved = false;
|
|
955
|
+
for (const segment of split.segments) {
|
|
956
|
+
const toks = tokenizeSegment(segment);
|
|
957
|
+
if (toks.folded.length === 0)
|
|
958
|
+
continue;
|
|
959
|
+
let i = 0;
|
|
960
|
+
while (i < toks.folded.length && ASSIGNMENT_WORD.test(toks.folded[i]))
|
|
961
|
+
i++;
|
|
962
|
+
const written = toks.folded[i];
|
|
963
|
+
if (written === undefined)
|
|
964
|
+
continue;
|
|
965
|
+
const name = programBasename(written);
|
|
966
|
+
if (!allow.has(name) && COMMAND_LAUNCHERS.has(name)) {
|
|
967
|
+
readers.push({ folded: [LAUNCHER_SCAN_HEAD, ...toks.folded.slice(i)], raw: [LAUNCHER_SCAN_HEAD, ...toks.raw.slice(i)] });
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (UNMODELLED_BASE_MOVERS.has(name))
|
|
971
|
+
baseMoved = true;
|
|
972
|
+
if (!allow.has(name) && name !== "cd")
|
|
973
|
+
continue;
|
|
974
|
+
readers.push(i === 0 && name === written ? toks : { folded: [name, ...toks.folded.slice(i + 1)], raw: [toks.raw[i] ?? name, ...toks.raw.slice(i + 1)] });
|
|
975
|
+
}
|
|
976
|
+
if (readers.length === 0)
|
|
977
|
+
return NOT_GATED;
|
|
978
|
+
if (baseMoved)
|
|
979
|
+
return { gated: true, paths: [] };
|
|
980
|
+
const verdict = evaluateReadBoundary(readers, containmentOnly);
|
|
981
|
+
return verdict.outOfRootRead === true ? { gated: true, paths: verdict.outOfRootPaths ?? [] } : NOT_GATED;
|
|
982
|
+
};
|
|
983
|
+
const direct = judge(command);
|
|
984
|
+
if (direct.gated)
|
|
985
|
+
return direct;
|
|
986
|
+
const shape = POLL_LOOP_SHAPE.exec(command.trim());
|
|
987
|
+
return shape !== null ? judge(shape[3]) : NOT_GATED;
|
|
988
|
+
}
|
|
887
989
|
export function classifyCompoundReadonly(command, allow, boundary) {
|
|
888
990
|
return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
|
|
889
991
|
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-
|
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
13
13
|
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
14
|
-
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
14
|
+
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
15
15
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
16
16
|
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
17
17
|
function operandFamily(paths) {
|
|
@@ -28,9 +28,10 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
28
28
|
const command = a?.command;
|
|
29
29
|
if (typeof command !== "string")
|
|
30
30
|
return { reversible: false };
|
|
31
|
-
if (a?.run_in_background === true)
|
|
32
|
-
return { reversible: false };
|
|
33
31
|
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
32
|
+
const outOfRootGate = () => resolved !== undefined && classifyOutOfRootReadGate(command, allowSet, resolved).gated ? { mandated: true } : {};
|
|
33
|
+
if (a?.run_in_background === true)
|
|
34
|
+
return { reversible: false, ...outOfRootGate() };
|
|
34
35
|
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
35
36
|
if (detailed.reason === undefined) {
|
|
36
37
|
if (detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0) {
|
|
@@ -40,6 +41,7 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
40
41
|
const others = detailed.undecidedPaths.filter((p) => !recursiveSet.has(p));
|
|
41
42
|
return {
|
|
42
43
|
reversible: false,
|
|
44
|
+
...outOfRootGate(),
|
|
43
45
|
cause: {
|
|
44
46
|
code: RECURSIVE_READ_CAUSE_CODE,
|
|
45
47
|
roots: operandFamily(recursive),
|
|
@@ -47,11 +49,13 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
47
49
|
},
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
|
-
return { reversible: false };
|
|
52
|
+
return { reversible: false, ...outOfRootGate() };
|
|
51
53
|
}
|
|
52
54
|
return { reversible: true };
|
|
53
55
|
}
|
|
54
|
-
return
|
|
56
|
+
return classifyBoundedReadonlyPollLoop(command, allowSet, resolved) === undefined
|
|
57
|
+
? { reversible: true }
|
|
58
|
+
: { reversible: false, ...outOfRootGate() };
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
61
|
export const FULL_SHELL_CONTRACT_ID = "core.bash@1";
|