@sema-agent/core 5.65.0 → 6.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 +46 -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 +2 -1
- package/dist/core/governance-codes.js +14 -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 +20 -0
- package/dist/core/permission-rule-consent.js +12 -3
- package/dist/core/permission-rule-model.d.ts +67 -7
- package/dist/core/permission-rule-model.js +53 -7
- package/dist/core/permission-rule-store.js +15 -10
- package/dist/core/permission-rule-sync.js +15 -11
- 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 +443 -79
- 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/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 +24 -0
- package/dist/core/types.d.ts +784 -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 +18 -8
- package/dist/index.js +14 -6
- 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.js +1 -0
- 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 +155 -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) {
|
|
@@ -256,6 +256,7 @@ class FilePermissionRuleStore {
|
|
|
256
256
|
};
|
|
257
257
|
}),
|
|
258
258
|
apply: async (delta, opts) => this.serialize(() => {
|
|
259
|
+
delta = structuredClone(delta);
|
|
259
260
|
const cur = this.current();
|
|
260
261
|
if (cur.rev !== opts.expectedRev)
|
|
261
262
|
return { conflict: true, rev: cur.rev };
|
|
@@ -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";
|
|
@@ -74,6 +74,35 @@ export declare const BASH_READONLY_CONFINEMENT_NOTE = "bash_readonly is confined
|
|
|
74
74
|
* false-stale defect for those targets. Round-tripping the real bytes sidesteps the case analysis
|
|
75
75
|
* and stays correct if the encoding rules ever change. */
|
|
76
76
|
export declare function persistedTextOf(encoded: string | Uint8Array): string;
|
|
77
|
+
/**
|
|
78
|
+
* #483 — one APPLIED edit as the Edit tool's `details` card records it: the RESOLVED old/new strings
|
|
79
|
+
* (post quote/escape resolution, post deletion-newline widening) in the normalized originalFile
|
|
80
|
+
* coordinate. This is the card shape a transcript replay must re-apply, so the application step is a
|
|
81
|
+
* single source shared by the Edit tool's own apply loop and the replay — two spellings of "apply"
|
|
82
|
+
* could silently drift (e.g. one expanding `$&` and one not) and the replay's recomputed hash would
|
|
83
|
+
* refuse edits the tool actually made.
|
|
84
|
+
*/
|
|
85
|
+
export interface RecordedEdit {
|
|
86
|
+
oldString: string;
|
|
87
|
+
newString: string;
|
|
88
|
+
replaceAll: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* #483 — apply ONE recorded edit mechanically (no matching layers, no validation: the recording site
|
|
92
|
+
* already resolved and validated the strings; a replay consumer that cannot prove `oldString` is
|
|
93
|
+
* present must fall back to retracting its record, never guess here).
|
|
94
|
+
*
|
|
95
|
+
* Two load-bearing details, both shared with the Edit tool's historical inline apply:
|
|
96
|
+
* - Function-form replacement: the callback's return value is NOT interpreted as a replacement
|
|
97
|
+
* pattern, so `$&`/`$$`/`` $` ``/`$'` in `newString` are written literally (S-1; the replaceAll
|
|
98
|
+
* split/join path was always literal).
|
|
99
|
+
* - `oldString === ""` is the whole-content OVERWRITE shape: the Edit tool's whitespace-only-file
|
|
100
|
+
* overwrite lane records `{oldString: "", newString: <full new text>}` after REPLACING the file's
|
|
101
|
+
* content wholesale (fs-write.ts, the `old_string:""` existing-file branch). The tool's own apply
|
|
102
|
+
* loop never reaches this arm (checkEditMatch refuses an empty old_string before application), so
|
|
103
|
+
* it exists for the replay consumer alone — a plain `replace("", ...)` would PREPEND instead.
|
|
104
|
+
*/
|
|
105
|
+
export declare function applyRecordedEdit(text: string, edit: RecordedEdit): string;
|
|
77
106
|
/** fidelity-sweep F3 (stopgap wording): render the read-before-edit refusal, appending the slice/bash
|
|
78
107
|
* escape hatch when the target is over the Read byte cap — a bare "read it first" is un-followable
|
|
79
108
|
* there (a default whole-file Read refuses such files). Best-effort: an un-stattable file keeps the
|
|
@@ -114,7 +143,7 @@ export declare function resolveAutoDownsampler(): Promise<ImageDownsampler | und
|
|
|
114
143
|
* proxy — `KMu` @279761 pre-filters with a PER-EXTENSION chars-per-token coefficient (json/jsonl = 2,
|
|
115
144
|
* i.e. half of the 4 assumed here) and then confirms with a REAL tokenizer (`BLd` @518589); the
|
|
116
145
|
* budget itself is also overridable per call (`fileReadingLimits`) and by the `tengu_amber_wren` gate
|
|
117
|
-
* (`eze` @335063), neither of which has a sema lane. Consequence, stated rather than hidden: on
|
|
146
|
+
* (`eze` @335063 (cli250.js:39563)), neither of which has a sema lane. Consequence, stated rather than hidden: on
|
|
118
147
|
* token-dense text (JSON/CJK/base64) this proxy UNDER-counts, so sema serves a page CC would have cut
|
|
119
148
|
* further; on token-sparse text it over-counts and paginates earlier than CC. A real tokenizer here
|
|
120
149
|
* would need a provider-neutral counting seam (BYOM — no single vocabulary), which is why the proxy
|
|
@@ -416,6 +445,28 @@ export declare function seedReadFileStateFromContext(state: ReadFileState, key:
|
|
|
416
445
|
* compaction. This text lives in the transcript and does not.
|
|
417
446
|
*/
|
|
418
447
|
export declare function seedReadFileStateFromTranscript(state: ReadFileState, key: string, content: string, lastReadAt: number): void;
|
|
448
|
+
/**
|
|
449
|
+
* #483 rung-1 — copy a SESSION-inherited read-file state into a new task's map, under the new task's
|
|
450
|
+
* own containment judgment (CC parity: readFileState is session-scoped — minted once per REPL session,
|
|
451
|
+
* mutated across turns, with no turn-boundary or abort clear site; sema's per-task map inherits the
|
|
452
|
+
* previous turn's live state instead).
|
|
453
|
+
*
|
|
454
|
+
* Two rules, each a claim inheritance cannot honestly skip:
|
|
455
|
+
* - **Every key passes the CURRENT task's own key judgment** (`judgeKey` = the same resolveKey
|
|
456
|
+
* containment the transcript-replay seed uses: write roots ∪ read roots, resolved read face). A
|
|
457
|
+
* root or face narrowed between the turns must narrow what the state remembers, exactly as the
|
|
458
|
+
* replay leg already judges — an unjudged copy would smuggle out-of-face entries into the task
|
|
459
|
+
* (and into its checkpoints).
|
|
460
|
+
* - **`seededFromContext` entries do not inherit.** That flag claims the file's text lives in the
|
|
461
|
+
* system-prompt lane OF THIS TASK — a claim only this task's own startup seeding (T10) can make.
|
|
462
|
+
* A still-declared file is re-seeded the same turn (no loss); a no-longer-declared one must not
|
|
463
|
+
* keep answering reads with an "already in your context" reminder that is no longer true.
|
|
464
|
+
*
|
|
465
|
+
* Entries are copied by REFERENCE (ReadEntry values are never mutated in place — every write-back
|
|
466
|
+
* `set`s a fresh object), so recorded facts (view / isPartialView / lastReadAt) ride verbatim: the
|
|
467
|
+
* dedup stub and the partial-view refusal keep working across turns exactly as inside one task.
|
|
468
|
+
*/
|
|
469
|
+
export declare function inheritSessionReadFileState(target: ReadFileState, inherited: ReadFileState, judgeKey: (recordedKey: string) => Promise<string | undefined>): Promise<void>;
|
|
419
470
|
/**
|
|
420
471
|
* RB-197 (form-one audit, CC 2.1.220 @388663-388664): a landed compaction summarizes old Read results OUT
|
|
421
472
|
* of the context, so every non-seeded read-state entry must be dropped — CC snapshots then
|
|
@@ -39,6 +39,11 @@ export const BASH_READONLY_CONFINEMENT_NOTE = "bash_readonly is confined to the
|
|
|
39
39
|
export function persistedTextOf(encoded) {
|
|
40
40
|
return decodeTextBytes(typeof encoded === "string" ? Buffer.from(encoded, "utf8") : encoded).text;
|
|
41
41
|
}
|
|
42
|
+
export function applyRecordedEdit(text, edit) {
|
|
43
|
+
if (edit.oldString === "")
|
|
44
|
+
return edit.newString;
|
|
45
|
+
return edit.replaceAll ? text.split(edit.oldString).join(edit.newString) : text.replace(edit.oldString, () => edit.newString);
|
|
46
|
+
}
|
|
42
47
|
export async function notReadRefusalText(env, toolName, key, v, signal, fallbackHint) {
|
|
43
48
|
const base = `Error (${toolName}): ${v.message}`;
|
|
44
49
|
if (v.partialView === true && !isNotebookPath(key))
|
|
@@ -276,6 +281,15 @@ export function seedReadFileStateFromTranscript(state, key, content, lastReadAt)
|
|
|
276
281
|
const text = normalizeFileText(content);
|
|
277
282
|
state.set(key, { hash: sha256(text), totalLines: countLines(text), truncated: false, lastReadAt });
|
|
278
283
|
}
|
|
284
|
+
export async function inheritSessionReadFileState(target, inherited, judgeKey) {
|
|
285
|
+
for (const [key, entry] of inherited) {
|
|
286
|
+
if (entry.seededFromContext === true)
|
|
287
|
+
continue;
|
|
288
|
+
const judged = await judgeKey(key);
|
|
289
|
+
if (judged !== undefined)
|
|
290
|
+
target.set(judged, entry);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
279
293
|
export function applyCompactionToReadFileState(state, attachedComplete, preserveKeys = []) {
|
|
280
294
|
const preserve = new Set(preserveKeys);
|
|
281
295
|
for (const [k, v] of [...state]) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
-
import type { BeforeWriteHook } from "../../core/types.js";
|
|
2
|
+
import type { BeforeWriteHook, TrackFileEditHook } from "../../core/types.js";
|
|
3
3
|
import { type ReadFileState } from "./safety.js";
|
|
4
4
|
import { type CwdRef } from "./fs-shared.js";
|
|
5
5
|
/**
|
|
@@ -11,9 +11,9 @@ import { type CwdRef } from "./fs-shared.js";
|
|
|
11
11
|
* MemoryEngine's write gate (`MemoryEngine.gateWrite`) — a non-memory path passes with one string
|
|
12
12
|
* prefix comparison there (零开销直通). Absent hook ⇒ byte-identical behavior.
|
|
13
13
|
*/
|
|
14
|
-
export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook } from "../../core/types.js";
|
|
15
|
-
export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
|
|
16
|
-
export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
|
|
14
|
+
export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook } from "../../core/types.js";
|
|
15
|
+
export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
|
|
16
|
+
export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
|
|
17
17
|
/**
|
|
18
18
|
* design v1.163 — NotebookEdit: replace/insert/delete a single cell in a .ipynb. CC-parity tool over the SAME hand-band
|
|
19
19
|
* safety skeleton as Edit/Write (resolveKey containment → requireRead read-before-edit → checkStale content-hash
|
|
@@ -21,4 +21,4 @@ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileSt
|
|
|
21
21
|
* .ipynb as the RB-227 cell projection but records read state (hash/totalLines) in the RAW notebook-text
|
|
22
22
|
* coordinate — that raw-coordinate read record is what this tool's freshness check depends on.
|
|
23
23
|
*/
|
|
24
|
-
export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
|
|
24
|
+
export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
|