@sema-agent/core 7.16.0 → 7.17.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.
Files changed (193) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-recall.d.ts +0 -8
  49. package/dist/core/memory.d.ts +1 -1
  50. package/dist/core/oracle-isolation.js +2 -2
  51. package/dist/core/parent-spec-seat.d.ts +66 -0
  52. package/dist/core/parent-spec-seat.js +47 -0
  53. package/dist/core/permission-rule-model.d.ts +11 -6
  54. package/dist/core/permission-rule-model.js +53 -37
  55. package/dist/core/permission-rules.js +8 -3
  56. package/dist/core/protocol-naming.d.ts +2 -2
  57. package/dist/core/remote-env.d.ts +1 -1
  58. package/dist/core/retired-keys.d.ts +19 -0
  59. package/dist/core/retired-keys.js +15 -0
  60. package/dist/core/runner/active-skill-scope.js +3 -3
  61. package/dist/core/runner/assemble-result.d.ts +15 -13
  62. package/dist/core/runner/assemble-result.js +5 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  64. package/dist/core/runner/contracts.d.ts +10 -9
  65. package/dist/core/runner/gate-exit.js +15 -1
  66. package/dist/core/runner/halt-attribution.d.ts +35 -0
  67. package/dist/core/runner/halt-attribution.js +16 -0
  68. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  69. package/dist/core/runner/memory-consolidation.js +0 -1
  70. package/dist/core/runner/prepare-artifact.d.ts +53 -0
  71. package/dist/core/runner/prepare-artifact.js +77 -0
  72. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  73. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  74. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  75. package/dist/core/runner/prepare-config-doors.js +5 -2
  76. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  77. package/dist/core/runner/prepare-context-lane.js +2 -13
  78. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  79. package/dist/core/runner/prepare-file-history.js +3 -52
  80. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  81. package/dist/core/runner/prepare-hands-readface.js +3 -2
  82. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  83. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  84. package/dist/core/runner/prepare-memory.d.ts +4 -6
  85. package/dist/core/runner/prepare-memory.js +4 -2
  86. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  87. package/dist/core/runner/prepare-policy-chain.js +15 -30
  88. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  89. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  90. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  91. package/dist/core/runner/prepare-task.js +10 -9
  92. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  93. package/dist/core/runner/resume-admission.js +2 -1
  94. package/dist/core/runner/resume-internals-and-config.js +4 -0
  95. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  96. package/dist/core/runner/retired-runner-deps.js +7 -0
  97. package/dist/core/runner/run-harness-handlers.js +2 -3
  98. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  99. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  100. package/dist/core/runner/session-file-state-replay.js +3 -3
  101. package/dist/core/runner/session-rule-policy.js +3 -4
  102. package/dist/core/runner/stream-halt-verbs.js +1 -1
  103. package/dist/core/runner/stream-steer-verb.js +1 -4
  104. package/dist/core/runner/workspace-path.js +3 -0
  105. package/dist/core/runner-deps.d.ts +30 -49
  106. package/dist/core/safe-notify.d.ts +1 -1
  107. package/dist/core/sensitive-path-policy.d.ts +1 -1
  108. package/dist/core/sensitive-path-policy.js +4 -4
  109. package/dist/core/session.d.ts +2 -2
  110. package/dist/core/skill-tool-specifier.d.ts +1 -1
  111. package/dist/core/skills-directory.d.ts +1 -1
  112. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  113. package/dist/core/strategy-store.d.ts +8 -11
  114. package/dist/core/task-event.d.ts +20 -17
  115. package/dist/core/task-limits.d.ts +1 -1
  116. package/dist/core/task-notification.d.ts +5 -5
  117. package/dist/core/task-registry-agent.d.ts +5 -5
  118. package/dist/core/task-registry-monitor.d.ts +2 -2
  119. package/dist/core/task-registry-shared.d.ts +27 -27
  120. package/dist/core/task-registry-shared.js +3 -13
  121. package/dist/core/task-registry.d.ts +8 -8
  122. package/dist/core/task-result.d.ts +37 -37
  123. package/dist/core/task-spec.d.ts +5 -5
  124. package/dist/core/task-stream.d.ts +16 -11
  125. package/dist/core/tool-catalog-entries.d.ts +1 -0
  126. package/dist/core/tool-catalog-entries.js +5 -0
  127. package/dist/core/tool-conformance.d.ts +0 -2
  128. package/dist/core/tool-conformance.js +1 -3
  129. package/dist/core/tool-errors.d.ts +5 -0
  130. package/dist/core/tool-errors.js +1 -0
  131. package/dist/core/tool-policy.d.ts +51 -22
  132. package/dist/core/tool-policy.js +28 -5
  133. package/dist/core/tool-result-budget.d.ts +1 -1
  134. package/dist/core/tool-result-store.d.ts +11 -11
  135. package/dist/core/tool-spec.d.ts +34 -38
  136. package/dist/core/tools.d.ts +2 -2
  137. package/dist/core/trace.d.ts +12 -15
  138. package/dist/core/untrusted-text.d.ts +1 -1
  139. package/dist/core/wiring-manifest.d.ts +4 -3
  140. package/dist/core/workflow-journal-store.d.ts +11 -11
  141. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  142. package/dist/engine/execution-env/node-execution-env.js +4 -2
  143. package/dist/engine/harness/agent-harness.d.ts +47 -4
  144. package/dist/engine/harness/agent-harness.js +52 -9
  145. package/dist/engine/loop/agent-loop.js +15 -8
  146. package/dist/engine/loop/types.d.ts +5 -0
  147. package/dist/index.d.ts +19 -14
  148. package/dist/index.js +15 -10
  149. package/dist/internal/harness.d.ts +1 -0
  150. package/dist/orchestration/run-spec.d.ts +3 -0
  151. package/dist/orchestration/run-spec.js +3 -3
  152. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  153. package/dist/orchestration/run-workflow-tool.js +23 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  155. package/dist/orchestration/workflow-primitives.js +1 -4
  156. package/dist/orchestration/workflow-types.d.ts +34 -4
  157. package/dist/orchestration/workflow-types.js +2 -0
  158. package/dist/orchestration/workflow.d.ts +39 -7
  159. package/dist/orchestration/workflow.js +234 -113
  160. package/dist/prompt-assembly/assemble.d.ts +2 -2
  161. package/dist/prompt-assembly/assemble.js +9 -9
  162. package/dist/prompt-assembly/composer.d.ts +6 -7
  163. package/dist/prompt-assembly/composer.js +10 -10
  164. package/dist/prompt-assembly/epoch.js +1 -1
  165. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  166. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  167. package/dist/prompt-assembly/types.d.ts +14 -14
  168. package/dist/prompts/coordinator.d.ts +0 -13
  169. package/dist/prompts/coordinator.js +0 -3
  170. package/dist/stores/file/checkpoint-store.js +1 -5
  171. package/dist/stores/file/file-history-store.js +2 -2
  172. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  173. package/dist/stores/file/permission-rule-store.js +4 -148
  174. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  175. package/dist/tools/artifact/artifact-text.js +68 -0
  176. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  177. package/dist/tools/artifact/artifact-tool.js +351 -0
  178. package/dist/tools/artifact/local-stub.d.ts +26 -0
  179. package/dist/tools/artifact/local-stub.js +296 -0
  180. package/dist/tools/fs/bash-readonly-classifier.d.ts +74 -8
  181. package/dist/tools/fs/bash-readonly-classifier.js +171 -60
  182. package/dist/tools/fs/fs-bash.d.ts +14 -10
  183. package/dist/tools/fs/fs-bash.js +19 -27
  184. package/dist/tools/fs/fs-search-tools.js +5 -2
  185. package/dist/tools/fs/fs-shared.d.ts +2 -0
  186. package/dist/tools/fs/fs-shared.js +3 -11
  187. package/dist/tools/fs/index.d.ts +4 -0
  188. package/dist/tools/fs/index.js +4 -1
  189. package/dist/tools/fs/safety.d.ts +117 -1
  190. package/dist/tools/fs/safety.js +131 -31
  191. package/dist/tools/fs/search.js +8 -6
  192. package/package.json +2 -1
  193. package/test/export-surface.snapshot.json +115 -39
@@ -0,0 +1,296 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+ import {} from "../../core/artifact-host.js";
5
+ import { acquireStoreDirLock } from "../../stores/file/fs-atomic.js";
6
+ import { normalizePublishedPath } from "./artifact-tool.js";
7
+ export const EVAL_STUB_URL_PREFIX = "eval-stub://artifact/";
8
+ export const LOCAL_STUB_MAX_FILE_BYTES = 16 * 1024 * 1024;
9
+ export const LOCAL_STUB_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
10
+ const LEDGER_FILE = "ledger.json";
11
+ const TMP_DIR = ".tmp";
12
+ const LOCK_FILE = "LOCK";
13
+ const RESERVED_NAMES = new Set([LEDGER_FILE, TMP_DIR, LOCK_FILE].map((n) => n.toLowerCase()));
14
+ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
15
+ const ATTEMPT_WINDOW = 512;
16
+ const emptyLedger = () => ({ v: 1, artifacts: {}, attempts: {} });
17
+ const isRow = (x, key) => {
18
+ if (x === null || typeof x !== "object")
19
+ return false;
20
+ const r = x;
21
+ if (typeof r.slug !== "string" || r.slug !== key || !SLUG_RE.test(r.slug) || r.slug.startsWith(".") || RESERVED_NAMES.has(r.slug.toLowerCase()))
22
+ return false;
23
+ if (typeof r.url !== "string" || !Number.isSafeInteger(r.version) || r.version < 1 || !Array.isArray(r.files))
24
+ return false;
25
+ return r.files.every((f) => f !== null && typeof f === "object" && typeof f.path === "string" && normalizePublishedPath(f.path).ok && Number.isSafeInteger(f.bytes));
26
+ };
27
+ function parseLedger(text, path) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(text);
31
+ }
32
+ catch {
33
+ throw new Error(`artifact stub ledger is corrupt (not JSON): ${path}`);
34
+ }
35
+ const l = parsed;
36
+ if (l === null || typeof l !== "object" || l.v !== 1 || l.artifacts === null || typeof l.artifacts !== "object" || l.attempts === null || typeof l.attempts !== "object") {
37
+ throw new Error(`artifact stub ledger is corrupt (unexpected shape): ${path}`);
38
+ }
39
+ for (const [key, row] of Object.entries(l.artifacts))
40
+ if (!isRow(row, key))
41
+ throw new Error(`artifact stub ledger is corrupt (bad artifact row): ${path}`);
42
+ return { v: 1, artifacts: { ...l.artifacts }, attempts: { ...l.attempts } };
43
+ }
44
+ function fsyncDirBestEffort(dir) {
45
+ try {
46
+ const fd = openSync(dir, "r");
47
+ try {
48
+ fsyncSync(fd);
49
+ }
50
+ finally {
51
+ closeSync(fd);
52
+ }
53
+ }
54
+ catch {
55
+ }
56
+ }
57
+ function fsyncTreeBestEffort(dir) {
58
+ for (const entry of readdirSync(dir, { withFileTypes: true }))
59
+ if (entry.isDirectory())
60
+ fsyncTreeBestEffort(join(dir, entry.name));
61
+ fsyncDirBestEffort(dir);
62
+ }
63
+ function writeFsync(path, bytes) {
64
+ const fd = openSync(path, "w", 0o600);
65
+ try {
66
+ writeFileSync(fd, bytes);
67
+ fsyncSync(fd);
68
+ }
69
+ finally {
70
+ closeSync(fd);
71
+ }
72
+ }
73
+ function slugBase(fileName) {
74
+ const dot = fileName.lastIndexOf(".");
75
+ const stem = (dot > 0 ? fileName.slice(0, dot) : fileName).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
76
+ return stem.length > 0 ? stem : "artifact";
77
+ }
78
+ export class LocalArtifactStub {
79
+ dir;
80
+ #mint = new Map();
81
+ #chain = Promise.resolve();
82
+ constructor(opts) {
83
+ if (typeof opts?.dir !== "string" || opts.dir.length === 0)
84
+ throw new Error("localArtifactStub: `dir` is required");
85
+ this.dir = resolve(opts.dir);
86
+ }
87
+ capabilities() {
88
+ return { plane: "local-stub", actions: ["publish"], defaultVisibility: "private", maxFileBytes: LOCAL_STUB_MAX_FILE_BYTES, maxTotalBytes: LOCAL_STUB_MAX_TOTAL_BYTES, multiFile: false, storageDir: this.dir };
89
+ }
90
+ onMount(runId) {
91
+ this.#mint.set(runId, new Set());
92
+ }
93
+ ownMinted(runId) {
94
+ return new Set(this.#mint.get(runId) ?? []);
95
+ }
96
+ publish(req, ctx) {
97
+ const run = this.#chain.then(() => this.#publishLocked(req, ctx));
98
+ this.#chain = run.then(() => undefined, () => undefined);
99
+ return run;
100
+ }
101
+ #ledgerPath() {
102
+ return join(this.dir, LEDGER_FILE);
103
+ }
104
+ #loadLedger() {
105
+ const p = this.#ledgerPath();
106
+ if (!existsSync(p))
107
+ return emptyLedger();
108
+ return parseLedger(readFileSync(p, "utf8"), p);
109
+ }
110
+ #versionDir(slug, version) {
111
+ return join(this.dir, slug, `v${version}`);
112
+ }
113
+ #snapshotOf(row) {
114
+ const base = this.#versionDir(row.slug, row.version);
115
+ const realBase = realpathSync(base);
116
+ const files = row.files.map((f) => {
117
+ const target = realpathSync(resolve(base, ...f.path.split("/")));
118
+ if (!target.startsWith(realBase + sep))
119
+ throw new Error(`artifact stub ledger is corrupt (a file resolves outside its version directory): ${this.#ledgerPath()}`);
120
+ return { path: f.path, content: new Uint8Array(readFileSync(target)), ...(f.mediaType !== undefined ? { mediaType: f.mediaType } : {}) };
121
+ });
122
+ return {
123
+ files,
124
+ ...(row.title !== undefined ? { title: row.title } : {}),
125
+ ...(row.description !== undefined ? { description: row.description } : {}),
126
+ ...(row.favicon !== undefined ? { favicon: row.favicon } : {}),
127
+ ...(row.label !== undefined ? { label: row.label } : {}),
128
+ };
129
+ }
130
+ #mintSlug(ledger, requested, firstFile) {
131
+ const fold = requested?.toLowerCase();
132
+ if (requested !== undefined && fold !== undefined && SLUG_RE.test(requested) && !RESERVED_NAMES.has(fold) && !requested.startsWith(".") && !Object.keys(ledger.artifacts).some((s) => s.toLowerCase() === fold))
133
+ return requested;
134
+ const base = slugBase(firstFile);
135
+ const taken = new Set(Object.keys(ledger.artifacts).map((s) => s.toLowerCase()));
136
+ for (;;) {
137
+ const slug = `${base}-${randomBytes(3).toString("hex")}`;
138
+ if (!taken.has(slug))
139
+ return slug;
140
+ }
141
+ }
142
+ #commitLedger(next) {
143
+ const ledgerTmp = join(this.dir, `${LEDGER_FILE}.${randomBytes(4).toString("hex")}.tmp`);
144
+ try {
145
+ const text = JSON.stringify(next, null, 2);
146
+ writeFsync(ledgerTmp, text);
147
+ const verified = parseLedger(readFileSync(ledgerTmp, "utf8"), ledgerTmp);
148
+ if (JSON.stringify(verified) !== JSON.stringify(next))
149
+ throw new Error("artifact stub ledger did not read back as written");
150
+ renameSync(ledgerTmp, this.#ledgerPath());
151
+ }
152
+ catch (err) {
153
+ try {
154
+ rmSync(ledgerTmp, { force: true });
155
+ }
156
+ catch {
157
+ }
158
+ throw err;
159
+ }
160
+ fsyncDirBestEffort(this.dir);
161
+ }
162
+ #withAttempt(ledger, attemptId, attempt) {
163
+ const entries = Object.entries(ledger.attempts).filter(([k]) => k !== attemptId);
164
+ while (entries.length >= ATTEMPT_WINDOW)
165
+ entries.shift();
166
+ entries.push([attemptId, attempt]);
167
+ return Object.fromEntries(entries);
168
+ }
169
+ #replayOf(ledger, attempt) {
170
+ if ("ref" in attempt)
171
+ return { ok: true, ref: attempt.ref };
172
+ if (attempt.code === "conflict") {
173
+ const row = ledger.artifacts[attempt.slug];
174
+ return { ok: false, code: "conflict", currentVersion: row !== undefined ? String(row.version) : attempt.currentVersion, current: row !== undefined ? this.#snapshotOf(row) : "reread" };
175
+ }
176
+ return { ok: false, code: attempt.code, message: attempt.message };
177
+ }
178
+ async #publishLocked(req, ctx) {
179
+ mkdirSync(this.dir, { recursive: true, mode: 0o700 });
180
+ const lease = acquireStoreDirLock(this.dir, { label: "artifact stub", fileName: LOCK_FILE });
181
+ try {
182
+ const ledger = this.#loadLedger();
183
+ const replay = ledger.attempts[ctx.attemptId];
184
+ if (replay !== undefined)
185
+ return this.#replayOf(ledger, replay);
186
+ const decided = this.#decide(ledger, req, ctx);
187
+ if (!decided.ok) {
188
+ const attempt = decided.outcome.code === "conflict" ? { code: "conflict", slug: decided.slug ?? "", currentVersion: decided.outcome.currentVersion } : { code: decided.outcome.code, message: decided.outcome.message };
189
+ this.#commitLedger({ ...ledger, attempts: this.#withAttempt(ledger, ctx.attemptId, attempt) });
190
+ return decided.outcome;
191
+ }
192
+ const { slug, version, existing, files, mint } = decided;
193
+ const versionDir = this.#versionDir(slug, version);
194
+ const tmpDir = join(this.dir, TMP_DIR, `${slug}-v${version}-${randomBytes(4).toString("hex")}`);
195
+ let staged = false;
196
+ try {
197
+ if (existsSync(versionDir))
198
+ rmSync(versionDir, { recursive: true, force: true });
199
+ mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
200
+ for (const f of files) {
201
+ const target = join(tmpDir, ...f.path.split("/"));
202
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
203
+ writeFsync(target, f.content);
204
+ }
205
+ fsyncTreeBestEffort(tmpDir);
206
+ mkdirSync(dirname(versionDir), { recursive: true, mode: 0o700 });
207
+ renameSync(tmpDir, versionDir);
208
+ staged = true;
209
+ fsyncDirBestEffort(dirname(versionDir));
210
+ const now = new Date().toISOString();
211
+ const url = `${EVAL_STUB_URL_PREFIX}${slug}`;
212
+ const row = {
213
+ slug,
214
+ url,
215
+ version,
216
+ ...(req.title !== undefined ? { title: req.title } : existing?.title !== undefined ? { title: existing.title } : {}),
217
+ ...(req.description !== undefined ? { description: req.description } : existing?.description !== undefined ? { description: existing.description } : {}),
218
+ ...(req.favicon !== undefined ? { favicon: req.favicon } : existing?.favicon !== undefined ? { favicon: existing.favicon } : {}),
219
+ ...(req.label !== undefined ? { label: req.label } : {}),
220
+ files: files.map((f) => ({ path: f.path, ...(f.mediaType !== undefined ? { mediaType: f.mediaType } : {}), bytes: f.content.byteLength })),
221
+ createdAt: existing?.createdAt ?? now,
222
+ updatedAt: now,
223
+ };
224
+ const ref = { url, slug, version: String(version), ...(row.title !== undefined ? { title: row.title } : {}) };
225
+ this.#commitLedger({ v: 1, artifacts: { ...ledger.artifacts, [slug]: row }, attempts: this.#withAttempt(ledger, ctx.attemptId, { ref }) });
226
+ mint.add(slug);
227
+ return { ok: true, ref };
228
+ }
229
+ catch (err) {
230
+ for (const orphan of [tmpDir, ...(staged ? [versionDir] : [])]) {
231
+ try {
232
+ rmSync(orphan, { recursive: true, force: true });
233
+ }
234
+ catch {
235
+ }
236
+ }
237
+ throw err;
238
+ }
239
+ }
240
+ finally {
241
+ lease.release();
242
+ }
243
+ }
244
+ #decide(ledger, req, ctx) {
245
+ const refuse = (code, message, slug) => ({ ok: false, outcome: { ok: false, code, message }, ...(slug !== undefined ? { slug } : {}) });
246
+ if (req.removeFiles !== undefined && req.removeFiles.length > 0)
247
+ return refuse("refused", "the local stub does not apply removeFiles");
248
+ if (req.files.length === 0)
249
+ return refuse("refused", "a publish needs at least one file");
250
+ const seen = new Set();
251
+ const files = [];
252
+ let total = 0;
253
+ for (const f of req.files) {
254
+ const norm = normalizePublishedPath(f.path);
255
+ if (!norm.ok)
256
+ return refuse("refused", `published path ${JSON.stringify(f.path).slice(0, 120)} ${norm.reason}`);
257
+ const fold = norm.path.toLowerCase();
258
+ if (seen.has(fold))
259
+ return refuse("refused", `published path ${JSON.stringify(norm.path).slice(0, 120)} collides with another entry`);
260
+ seen.add(fold);
261
+ total += f.content.byteLength;
262
+ if (f.content.byteLength > LOCAL_STUB_MAX_FILE_BYTES)
263
+ return refuse("payload_too_large", `${norm.path.slice(0, 120)} is ${f.content.byteLength} bytes; the limit is ${LOCAL_STUB_MAX_FILE_BYTES} bytes per file`);
264
+ files.push({ path: norm.path, content: f.content, ...(f.mediaType !== undefined ? { mediaType: f.mediaType } : {}) });
265
+ }
266
+ if (total > LOCAL_STUB_MAX_TOTAL_BYTES)
267
+ return refuse("payload_too_large", `the files total ${total} bytes; the limit is ${LOCAL_STUB_MAX_TOTAL_BYTES} bytes per version`);
268
+ const mint = this.#mint.get(ctx.runId) ?? new Set();
269
+ this.#mint.set(ctx.runId, mint);
270
+ if (req.url !== undefined) {
271
+ if (!req.url.startsWith(EVAL_STUB_URL_PREFIX))
272
+ return refuse("url_foreign", "not an eval-stub:// link this run published");
273
+ const slug = req.url.slice(EVAL_STUB_URL_PREFIX.length);
274
+ if (!mint.has(slug))
275
+ return refuse("url_foreign", "this run did not publish that artifact", slug);
276
+ const existing = ledger.artifacts[slug];
277
+ if (existing === undefined)
278
+ return refuse("not_found", "the artifact is no longer in the ledger", slug);
279
+ if (req.baseVersion !== undefined && req.baseVersion !== String(existing.version) && req.force !== true) {
280
+ return { ok: false, outcome: { ok: false, code: "conflict", currentVersion: String(existing.version), current: this.#snapshotOf(existing) }, slug };
281
+ }
282
+ return { ok: true, slug, version: existing.version + 1, existing, files, mint };
283
+ }
284
+ return { ok: true, slug: this.#mintSlug(ledger, req.slug, files[0].path), version: 1, existing: undefined, files, mint };
285
+ }
286
+ rows() {
287
+ return Object.values(this.#loadLedger().artifacts).map((r) => ({ url: r.url, slug: r.slug, version: String(r.version), ...(r.title !== undefined ? { title: r.title } : {}) }));
288
+ }
289
+ versionFiles(slug, version) {
290
+ const dir = this.#versionDir(slug, version);
291
+ return existsSync(dir) ? readdirSync(dir, { recursive: true }).map(String).sort() : [];
292
+ }
293
+ }
294
+ export function localArtifactStub(opts) {
295
+ return new LocalArtifactStub(opts);
296
+ }
@@ -209,9 +209,10 @@ export interface BashReadonlyRootBoundary {
209
209
  /** Base for resolving relative operands. Defaults to `roots[0]`. Callers tracking an observable `cd`
210
210
  * should pass the LIVE working directory, or a `..` operand will be judged against the wrong base. */
211
211
  cwd?: string;
212
- /** Expansion for a leading `~`. Without it a `~`-rooted operand is unresolvable and demotes (with no
213
- * out-of-root signal the classifier does not know where it points, which is a different verdict
214
- * from knowing it points outside). */
212
+ /** Expansion for a leading `~` and the value the read faces substitute for the `$HOME` / `${HOME}` word
213
+ * spellings before segmenting ({@link foldHomeVariableSpelling}). Without it a `~`-rooted operand is unresolvable
214
+ * and demotes (with no out-of-root signal — the classifier does not know where it points, which is a
215
+ * different verdict from knowing it points outside). */
215
216
  homeDir?: string;
216
217
  /** The sensitive-path read deny judge over LEXICALLY RESOLVED operands: a hit
217
218
  * demotes the command (ask, never auto-allow), independently of the roots — in-root operands are
@@ -248,11 +249,13 @@ export interface BashReadonlyRootBoundary {
248
249
  export interface CompoundReadonlyVerdict {
249
250
  /** Demotion reason (the same string {@link classifyCompoundReadonly} returns), or undefined. */
250
251
  reason?: string;
251
- /** Set only when the SOLE obstacle to the read-only verdict was the root boundary: every segment is
252
- * an allowlisted reader, no blocking/device/backgrounding arm fired, and the offending arguments are
253
- * statically resolved paths outside the roots. A gate can therefore offer the narrow "allow reading
254
- * from <dir>" approval instead of a generic shell prompt. Absent for every other demotion an
255
- * unresolvable path deliberately does NOT carry it (nothing concrete to grant). */
252
+ /** Set when a listed reader's statically resolved path operand lies outside the roots. With `reason` the
253
+ * containment sentence itself, the roots were the SOLE obstacle (every segment a listed reader, no
254
+ * blocking/device/guarded-form arm fired, every operand resolved) and the narrow "allow reading from <dir>"
255
+ * approval would make the command auto-allowable; with `reason` a SHAPE refusal (follow mode, a device, a
256
+ * blocking stdin read, a guarded verb form, multi-cd) or an UNRESOLVABLE-operand refusal (another operand
257
+ * the walk could not read) the finding rides beside the sentence — the read of that path is the same read,
258
+ * so the boundary's objection is stated either way, while the command stays refused after any grant. */
256
259
  outOfRootRead?: true;
257
260
  /** The resolved out-of-root paths, de-duplicated, in first-seen order. Present iff `outOfRootRead`. */
258
261
  outOfRootPaths?: readonly string[];
@@ -312,6 +315,15 @@ export interface CompoundReadonlyVerdict {
312
315
  * and miss the other.
313
316
  */
314
317
  undecidedPaths?: readonly string[];
318
+ /**
319
+ * Operands (written spellings) the walk could NOT resolve at all — a `~user` form, a `~` under no declared
320
+ * home, an argument the shell rewrites (brace/variable/command expansion), a `cd` target it cannot follow
321
+ * (`cd -`, bare `cd`, a pattern). The STRUCTURAL form of the unresolvable sentence: `reason` carries the
322
+ * first one's sentence, this member every one of them, and both survive whatever other sentence (a shape
323
+ * refusal) takes precedence. CONSUMER CONTRACT: non-empty ⇒ the boundary could not read where the command
324
+ * reads — a fail-closed ask (plain: nothing was declared), never a vouch.
325
+ */
326
+ unresolvedOperands?: readonly string[];
315
327
  /**
316
328
  * Operands of a RECURSIVE/EXPANDING read form (`grep -r`, `ls -R`, `du`, … — see
317
329
  * {@link RECURSIVE_READ_FORMS}) judged with a {@link BashReadonlyRootBoundary.denyMatch} seat wired.
@@ -343,6 +355,51 @@ export interface CompoundReadonlyVerdict {
343
355
  * uses the host's native separator.
344
356
  */
345
357
  export declare function formatOutOfRootReadApprovalOption(directory: string): string;
358
+ /** Resolve one operand against the boundary's base, WITHOUT touching the filesystem. Returns undefined
359
+ * when the token cannot be resolved statically — the fail-closed direction, reported as its own
360
+ * demotion rather than silently treated as in-root.
361
+ *
362
+ * The resolution is TEXTUAL, and its limitation runs in BOTH directions (only the first shows up as
363
+ * a refusal, so only that half tends to get reported):
364
+ * · false REFUSAL — a non-canonical spelling of an in-root path (a symlinked `/tmp` → `/private/tmp`
365
+ * prefix) does not textually match the canonical roots and is refused;
366
+ * · false ALLOW — a symlink INSIDE a root that points outside it (`<root>/vendor` → `/etc`) resolves
367
+ * to a textual prefix match, so `cat vendor/passwd` is judged in-root and the real command follows
368
+ * the link. This face is therefore lexical containment, NOT filesystem containment: it is weaker
369
+ * than the structured Read/Grep tools, which canonicalize through `realpath` before comparing
370
+ * (see safety.ts's `canonicalizeAndEnforceRoot`). Closing it means resolving each candidate against
371
+ * the live filesystem, which this function deliberately cannot do (it is synchronous, pure, and
372
+ * shared with the approval-gate face); the seam for it is the ENFORCING caller, which holds the
373
+ * ExecutionEnv.
374
+ *
375
+ * The verdict now carries {@link CompoundReadonlyVerdict.checkedPaths} (the
376
+ * candidates that landed inside), and `createBashReadonlyTool` canonicalizes them on a LOCAL env
377
+ * before running — closing BOTH directions above for that face (an escaping symlink is refused; a
378
+ * non-canonical spelling that resolves back inside is no longer refused). Two residuals stay here by
379
+ * construction, and belong to the caller rather than to this function: the compound/approval-gate face
380
+ * has no filesystem and stays lexical, and a REMOTE env keeps the lexical behaviour (its
381
+ * `canonicalPath` is an RPC per candidate). */
382
+ export declare function resolveOperandLexically(base: string | undefined, operand: string, homeDir: string | undefined): string | undefined;
383
+ /**
384
+ * The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
385
+ * command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
386
+ * double-quoted, followed by `/` or by the word's end. Those are exactly the words whose expansion is the home
387
+ * directory and nothing else, so the word is rewritten to what bash hands the program — the value itself, in
388
+ * place, with the word's quote structure untouched (`"$HOME/x"` → `"/home/u/x"`, `$HOME"/x"` → `/home/u"/x"`)
389
+ * — and every arm reads an ordinary absolute path; no tilde is minted, so the tilde-prefix rule of
390
+ * {@link tokenizeSegment} judges only tildes the command actually wrote. Substituted only against a DECLARED
391
+ * home (`homeDir`): with none declared the child's `$HOME` may differ from what `~` falls back to, so the
392
+ * spelling is left as written and meets the substitution hard-reject. The value must survive this file's own
393
+ * reading of the rewritten word: the UNQUOTED spelling only for a value with no `$IFS` whitespace, glob or
394
+ * shell metacharacter (word splitting / pathname expansion / a connector this file would then split on — none
395
+ * of which a real expansion is subject to here), the double-quoted one for any value carrying no `"`, `$`,
396
+ * backtick, backslash or line break (the characters that would change the quote structure or meet the
397
+ * hard-reject). Everything else is left as written: a single-quoted spelling does not expand, and a mid-word
398
+ * or suffixed one (`x$HOME`, `$HOME.bak`, `$HOMES`) still meets the substitution hard-reject. Bails whole on
399
+ * an escape or a backtick in the command (the hard-reject refuses those strings regardless, and the quote
400
+ * walk below is exact only without them) and on unbalanced quoting.
401
+ */
402
+ export declare function foldHomeVariableSpelling(command: string, homeDir: string | undefined): string;
346
403
  /**
347
404
  * {@link classifyCompoundReadonly} with the structured verdict. The name/operator/blocking arms
348
405
  * run FIRST and unchanged, so an existing demotion keeps its exact reason and never acquires the
@@ -568,3 +625,12 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
568
625
  * ({@link NON_ASCII_WHITESPACE}), so the grammar's tokenization and bash's stay identical.
569
626
  */
570
627
  export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
628
+ /**
629
+ * The poll-loop face's STRUCTURED verdict — the same {@link CompoundReadonlyVerdict} the compound face
630
+ * returns, so a consumer reads the boundary's structural findings (deny row, containment, recursive or
631
+ * undecided operands) off the loop body exactly as off a plain compound, beside whatever sentence the
632
+ * loop grammar or the body's shape produced. {@link classifyBoundedReadonlyPollLoop} is its `reason`
633
+ * projection. A refusal of the LOOP GRAMMAR itself (not a bounded loop, an unlisted body program, an
634
+ * unvetted sleep) carries no structure: no reader segment was walked.
635
+ */
636
+ export declare function classifyBoundedReadonlyPollLoopDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;