@sema-agent/server 7.141.0 → 7.142.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 (171) hide show
  1. package/MIGRATION.md +215 -0
  2. package/USAGE.md +67 -12
  3. package/dist/approval-ask-audit-store.d.ts +54 -38
  4. package/dist/approval-ask-audit-store.js +58 -39
  5. package/dist/approval-card.d.ts +8 -8
  6. package/dist/approval-crash-convergence.d.ts +17 -7
  7. package/dist/approval-crash-convergence.js +94 -22
  8. package/dist/approval-deny-reasons.d.ts +34 -30
  9. package/dist/approval-deny-reasons.js +21 -6
  10. package/dist/approval-policy-settlement.d.ts +1 -1
  11. package/dist/approval-reconciler.d.ts +33 -1
  12. package/dist/approval-reconciler.js +43 -40
  13. package/dist/auth-bridge.d.ts +9 -6
  14. package/dist/auth-bridge.js +9 -3
  15. package/dist/boot/adoption.js +2 -1
  16. package/dist/boot/budget-tracing.d.ts +16 -0
  17. package/dist/boot/budget-tracing.js +10 -6
  18. package/dist/boot/device-lane.d.ts +1 -1
  19. package/dist/boot/execution-env.d.ts +24 -4
  20. package/dist/boot/execution-env.js +26 -23
  21. package/dist/boot/execution-lane.js +2 -1
  22. package/dist/boot/exit-boundary.js +5 -4
  23. package/dist/boot/memory-consolidation.js +2 -1
  24. package/dist/boot/parked-revive-gate.js +2 -1
  25. package/dist/boot/permission-rules-audit.d.ts +0 -28
  26. package/dist/boot/permission-rules-audit.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -1
  28. package/dist/boot/reapers.js +4 -3
  29. package/dist/boot/resolve-spec.js +11 -2
  30. package/dist/boot/retention-lane.js +5 -3
  31. package/dist/boot/runner-deps.js +16 -13
  32. package/dist/boot/runtime-caps.js +3 -2
  33. package/dist/boot/session-faces.js +4 -3
  34. package/dist/boot/shutdown.js +3 -2
  35. package/dist/boot/stage-04-stores.js +3 -2
  36. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  37. package/dist/boot/stage-05-execution-env.js +8 -6
  38. package/dist/boot/stage-06-runners.d.ts +1 -1
  39. package/dist/boot/stage-06-runners.js +2 -1
  40. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  41. package/dist/boot/stage-07-capability-layer.js +2 -1
  42. package/dist/boot/stage-08-reapers.d.ts +1 -1
  43. package/dist/boot/stage-08-reapers.js +3 -2
  44. package/dist/boot/stage-09-leader.d.ts +1 -1
  45. package/dist/boot/stage-10-http-server.d.ts +1 -1
  46. package/dist/boot/stage-10-http-server.js +5 -4
  47. package/dist/boot/stores.js +3 -2
  48. package/dist/boot/web-admin.js +17 -17
  49. package/dist/boot/webfetch-summarize-lane.js +2 -1
  50. package/dist/boot/workflow-orchestration.js +3 -2
  51. package/dist/capabilities/builtin-tools.d.ts +2 -1
  52. package/dist/capabilities/builtin-tools.js +2 -4
  53. package/dist/capabilities/composite-members.d.ts +2 -1
  54. package/dist/capabilities/composite-members.js +2 -1
  55. package/dist/capability-facts.d.ts +24 -0
  56. package/dist/capability-facts.js +23 -0
  57. package/dist/config-catalog.js +1 -0
  58. package/dist/config-types.d.ts +15 -4
  59. package/dist/config.d.ts +6 -0
  60. package/dist/config.js +17 -3
  61. package/dist/execution-lane-caps.d.ts +2 -1
  62. package/dist/fleet/fleet-bus.d.ts +6 -3
  63. package/dist/hooks/hook-runner.d.ts +6 -0
  64. package/dist/hooks/hook-runner.js +30 -1
  65. package/dist/http/dispatch.js +4 -2
  66. package/dist/http/execution-context.d.ts +2 -2
  67. package/dist/http/idempotency.d.ts +1 -1
  68. package/dist/http/principal-gate.d.ts +3 -1
  69. package/dist/http/principal-gate.js +17 -8
  70. package/dist/http/resume-legs.js +5 -9
  71. package/dist/http/route-table.js +2 -0
  72. package/dist/http/routes/attachments.js +18 -0
  73. package/dist/http/routes/capabilities.js +5 -2
  74. package/dist/http/routes/memory-policy.js +26 -6
  75. package/dist/http/routes/runs.js +3 -3
  76. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  77. package/dist/http/routes/session-memory-sync.js +157 -0
  78. package/dist/http/routes/sessions-list.d.ts +2 -1
  79. package/dist/http/routes/tasks.js +7 -5
  80. package/dist/http/run-read-face.d.ts +7 -0
  81. package/dist/http/run-read-face.js +9 -3
  82. package/dist/http/send.d.ts +5 -0
  83. package/dist/http/send.js +9 -2
  84. package/dist/http/server.d.ts +14 -8
  85. package/dist/http/server.js +2 -4
  86. package/dist/http/submit-replay.d.ts +4 -3
  87. package/dist/http/submit-replay.js +2 -2
  88. package/dist/http/sync-submit-body.d.ts +21 -4
  89. package/dist/http/sync-submit-body.js +13 -0
  90. package/dist/identity-source.d.ts +21 -2
  91. package/dist/identity-source.js +6 -0
  92. package/dist/memory-incident-sink.d.ts +19 -6
  93. package/dist/memory-incident-sink.js +8 -4
  94. package/dist/memory-sync-client.d.ts +21 -21
  95. package/dist/memory-sync-client.js +50 -42
  96. package/dist/memory-sync-identity.d.ts +24 -0
  97. package/dist/memory-sync-identity.js +57 -0
  98. package/dist/memory-sync.d.ts +7 -1
  99. package/dist/memory-sync.js +6 -3
  100. package/dist/observability/fail-open.d.ts +27 -10
  101. package/dist/observability/fail-open.js +20 -7
  102. package/dist/observability/run-terminal.d.ts +92 -34
  103. package/dist/observability/run-terminal.js +167 -162
  104. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  105. package/dist/plugins/approval-ask-store-file.js +48 -7
  106. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  107. package/dist/plugins/approval-ask-store-memory.js +9 -8
  108. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  109. package/dist/plugins/approval-ask-store-sql.js +49 -19
  110. package/dist/plugins/file-run-store.d.ts +6 -1
  111. package/dist/plugins/file-run-store.js +11 -1
  112. package/dist/plugins/local-session-store.d.ts +36 -7
  113. package/dist/plugins/local-session-store.js +76 -32
  114. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  115. package/dist/plugins/local-task-attachment-store.js +67 -43
  116. package/dist/plugins/memory-key-guards.d.ts +3 -3
  117. package/dist/plugins/memory-run-store.d.ts +6 -1
  118. package/dist/plugins/memory-run-store.js +11 -1
  119. package/dist/plugins/one-time-migrations.d.ts +27 -1
  120. package/dist/plugins/one-time-migrations.js +57 -3
  121. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  122. package/dist/plugins/remote-env-e2b.js +111 -28
  123. package/dist/plugins/remote-env-k8s.d.ts +31 -5
  124. package/dist/plugins/remote-env-k8s.js +86 -19
  125. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  126. package/dist/plugins/remote-env-local-docker.js +40 -12
  127. package/dist/plugins/run-store-sql.d.ts +9 -1
  128. package/dist/plugins/run-store-sql.js +9 -1
  129. package/dist/plugins/store-backend.d.ts +10 -7
  130. package/dist/plugins/store-backend.js +1 -0
  131. package/dist/plugins/store-contracts.d.ts +26 -1
  132. package/dist/plugins/store-contracts.js +3 -0
  133. package/dist/plugins/task-attachment-store.d.ts +112 -14
  134. package/dist/plugins/task-attachment-store.js +221 -28
  135. package/dist/plugins/workspace-fill.d.ts +60 -0
  136. package/dist/plugins/workspace-fill.js +106 -0
  137. package/dist/project-bundle.d.ts +38 -0
  138. package/dist/project-bundle.js +609 -0
  139. package/dist/run-local.js +2 -8
  140. package/dist/runs.js +1 -2
  141. package/dist/sandbox-egress-policy.d.ts +4 -1
  142. package/dist/sandbox-egress-policy.js +3 -1
  143. package/dist/sandbox-git-plane.d.ts +31 -12
  144. package/dist/sandbox-git-plane.js +24 -4
  145. package/dist/security.d.ts +9 -0
  146. package/dist/security.js +8 -0
  147. package/dist/session-error-wire.d.ts +24 -0
  148. package/dist/session-error-wire.js +7 -0
  149. package/dist/task-settings.d.ts +10 -2
  150. package/dist/task-settings.js +15 -2
  151. package/dist/terminal.d.ts +39 -19
  152. package/dist/terminal.js +65 -22
  153. package/dist/tool-approval.d.ts +4 -3
  154. package/dist/tool-approval.js +12 -14
  155. package/dist/trace/core-keyset-guard.d.ts +123 -6
  156. package/dist/trace/engine-notice-wire.d.ts +8 -2
  157. package/dist/trace/engine-notice-wire.js +4 -2
  158. package/dist/trace/exit-seat.d.ts +2 -1
  159. package/dist/trace/ledger-sink.d.ts +8 -4
  160. package/dist/trace/ledger-sink.js +2 -2
  161. package/dist/trace/project.d.ts +18 -1
  162. package/dist/trace/project.js +16 -0
  163. package/dist/trace/self-minted-wire.d.ts +251 -2
  164. package/dist/trace/self-minted-wire.js +82 -0
  165. package/dist/trace/wire-projection-faces.d.ts +1 -1
  166. package/dist/trace/wire-projection-faces.js +1 -1
  167. package/dist/web-admin-client/apply-effective.js +21 -1
  168. package/dist/web-admin-client/effective-keys.js +2 -0
  169. package/dist/web-admin-client/types.d.ts +4 -0
  170. package/package.json +1 -1
  171. package/wire-changes/server@7.142.0.json +639 -0
@@ -0,0 +1,609 @@
1
+ import { Readable } from "node:stream";
2
+ import { once } from "node:events";
3
+ import { createGunzip, createGzip } from "node:zlib";
4
+ export const PROJECT_BUNDLE_MIME = "application/vnd.sema.project-bundle";
5
+ export function attachmentKindOf(mime) {
6
+ return mime === PROJECT_BUNDLE_MIME ? "project_bundle" : "attachment";
7
+ }
8
+ export const PROJECT_BUNDLE_INVALID_REASONS = ["not_archive", "unsafe_path", "entry_type", "reserved_path", "duplicate_path", "expanded_too_large", "too_many_entries"];
9
+ export const PROJECT_BUNDLE_MAX_EXPANDED_BYTES = 512 * 1024 * 1024;
10
+ export const PROJECT_BUNDLE_MAX_ENTRIES = 200_000;
11
+ export const PROJECT_BUNDLE_RESERVED_TOP_LEVEL = [".git", "attachments"];
12
+ class Refusal extends Error {
13
+ reason;
14
+ detail;
15
+ entryPath;
16
+ constructor(reason, detail, entryPath) {
17
+ super(detail);
18
+ this.reason = reason;
19
+ this.detail = detail;
20
+ this.entryPath = entryPath;
21
+ }
22
+ }
23
+ const BLOCK = 512;
24
+ const PATH_MAX_BYTES = 4095;
25
+ const NAME_MAX_BYTES = 255;
26
+ const MAX_LINK_HOPS = 40;
27
+ const YIELD_STEPS = 1 << 16;
28
+ const META_MAX_BYTES = 1024 * 1024;
29
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
30
+ export async function readProjectBundle(bytes, opts = {}) {
31
+ const maxExpanded = opts.limits?.maxExpandedBytes ?? PROJECT_BUNDLE_MAX_EXPANDED_BYTES;
32
+ const maxEntries = opts.limits?.maxEntries ?? PROJECT_BUNDLE_MAX_ENTRIES;
33
+ const writer = opts.canonical === true ? createCanonicalWriter() : undefined;
34
+ const gunzip = createGunzip();
35
+ try {
36
+ const walk = new TarWalk(maxEntries, writer);
37
+ let expanded = 0;
38
+ for await (const chunk of Readable.from([Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)]).pipe(gunzip)) {
39
+ expanded += chunk.length;
40
+ if (expanded > maxExpanded)
41
+ throw new Refusal("expanded_too_large", `the bundle expands past ${maxExpanded} bytes`);
42
+ await walk.feed(chunk);
43
+ if (walk.done)
44
+ break;
45
+ }
46
+ const judged = await walk.finish();
47
+ const canonical = writer === undefined ? undefined : await writer.end();
48
+ return { ok: true, entries: judged.entries, topLevel: judged.topLevel, ...(canonical !== undefined ? { canonical } : {}) };
49
+ }
50
+ catch (e) {
51
+ writer?.abort();
52
+ if (e instanceof Refusal)
53
+ return { ok: false, reason: e.reason, ...(e.entryPath !== undefined ? { path: e.entryPath } : {}), detail: e.detail };
54
+ return { ok: false, reason: "not_archive", detail: `not a gzip-compressed tar: ${e instanceof Error ? e.message : String(e)}` };
55
+ }
56
+ finally {
57
+ gunzip.destroy();
58
+ }
59
+ }
60
+ class TarWalk {
61
+ writer;
62
+ done = false;
63
+ chunks = [];
64
+ queued = 0;
65
+ entries = 0;
66
+ tree;
67
+ body;
68
+ pax = {};
69
+ unsupportedPax;
70
+ longName;
71
+ longLink;
72
+ zeroBlocks = 0;
73
+ constructor(maxEntries, writer) {
74
+ this.writer = writer;
75
+ this.tree = new BundleTree(maxEntries);
76
+ }
77
+ async feed(chunk) {
78
+ this.chunks.push(chunk);
79
+ this.queued += chunk.length;
80
+ for (;;) {
81
+ if (this.done)
82
+ return;
83
+ if (this.body !== undefined) {
84
+ if (!(await this.drainBody()))
85
+ return;
86
+ continue;
87
+ }
88
+ if (this.queued < BLOCK)
89
+ return;
90
+ await this.header(this.take(BLOCK));
91
+ }
92
+ }
93
+ async finish() {
94
+ if (!this.done && (this.body !== undefined || this.queued !== 0))
95
+ throw new Refusal("not_archive", "the archive ends inside an entry");
96
+ await this.tree.judge();
97
+ return { entries: this.entries, topLevel: this.tree.topLevel() };
98
+ }
99
+ take(n) {
100
+ const out = Buffer.allocUnsafe(n);
101
+ let off = 0;
102
+ while (off < n) {
103
+ const head = this.chunks[0];
104
+ const k = Math.min(head.length, n - off);
105
+ head.copy(out, off, 0, k);
106
+ off += k;
107
+ if (k === head.length)
108
+ this.chunks.shift();
109
+ else
110
+ this.chunks[0] = head.subarray(k);
111
+ }
112
+ this.queued -= n;
113
+ return out;
114
+ }
115
+ async drainBody() {
116
+ const b = this.body;
117
+ while (b.remaining > 0) {
118
+ if (this.queued === 0)
119
+ return false;
120
+ const head = this.chunks[0];
121
+ const k = Math.min(head.length, b.remaining);
122
+ const part = head.subarray(0, k);
123
+ if (k === head.length)
124
+ this.chunks.shift();
125
+ else
126
+ this.chunks[0] = head.subarray(k);
127
+ this.queued -= k;
128
+ b.remaining -= k;
129
+ if (b.sink === "write")
130
+ await this.writer.content(part);
131
+ else if (b.sink === "meta")
132
+ b.meta.push(Buffer.from(part));
133
+ }
134
+ if (this.queued < b.pad)
135
+ return false;
136
+ if (b.pad > 0)
137
+ this.take(b.pad);
138
+ this.body = undefined;
139
+ if (b.sink === "write")
140
+ await this.writer.endContent();
141
+ if (b.sink === "meta")
142
+ this.applyMeta(b.metaKind, Buffer.concat(b.meta));
143
+ return true;
144
+ }
145
+ async header(h) {
146
+ if (h.every((x) => x === 0)) {
147
+ this.zeroBlocks++;
148
+ if (this.zeroBlocks >= 2)
149
+ this.done = true;
150
+ return;
151
+ }
152
+ if (this.zeroBlocks > 0)
153
+ throw new Refusal("not_archive", "a header follows the end-of-archive marker");
154
+ if (!checksumHolds(h))
155
+ throw new Refusal("not_archive", "a tar header's checksum does not hold");
156
+ const magic = h.toString("latin1", 257, 263);
157
+ const posix = magic === "ustar\0";
158
+ if (!posix && magic !== "ustar ")
159
+ throw new Refusal("not_archive", "not a ustar archive (no ustar magic)");
160
+ const type = String.fromCharCode(h[156]);
161
+ const headerSize = octal(h, 124, 12, "size");
162
+ if (type === "x" || type === "g" || type === "L" || type === "K") {
163
+ if (headerSize > META_MAX_BYTES)
164
+ throw new Refusal("not_archive", `an extended header of ${headerSize} bytes is past the ${META_MAX_BYTES}-byte bound`);
165
+ this.body = { remaining: headerSize, pad: padOf(headerSize), sink: "meta", meta: [], metaKind: type };
166
+ return;
167
+ }
168
+ const size = this.pax.size ?? headerSize;
169
+ const rawName = this.pax.path ?? this.longName ?? joinUstarName(posix ? cstr(h, 345, 155) : "", cstr(h, 0, 100));
170
+ const rawLink = this.pax.linkpath ?? this.longLink ?? cstr(h, 157, 100);
171
+ const unsupported = this.unsupportedPax;
172
+ this.pax = {};
173
+ this.unsupportedPax = undefined;
174
+ this.longName = undefined;
175
+ this.longLink = undefined;
176
+ const entryType = type === "0" || type === "\0" ? "file" : type === "5" ? "dir" : type === "2" ? "symlink" : undefined;
177
+ const name = decodeName(rawName);
178
+ if (entryType === undefined)
179
+ throw new Refusal("entry_type", `'${name}' is a ${tarTypeWord(type)} — a project bundle holds regular files, directories and symbolic links only`, name);
180
+ if (unsupported !== undefined) {
181
+ throw new Refusal("entry_type", `'${name}' carries the pax extension '${printable(unsupported)}' — a project bundle holds plain regular files, directories and symbolic links only (a sparse map, a volume offset or an unknown extension changes how the entry reads)`, name);
182
+ }
183
+ const p = landingOf(name);
184
+ if (p === "") {
185
+ if (entryType !== "dir")
186
+ throw new Refusal("unsafe_path", `an entry with an empty path is not a ${entryType}`, name);
187
+ this.body = { remaining: size, pad: padOf(size), sink: "skip" };
188
+ return;
189
+ }
190
+ const slash = p.indexOf("/");
191
+ if (PROJECT_BUNDLE_RESERVED_TOP_LEVEL.includes(slash === -1 ? p : p.slice(0, slash)))
192
+ throw new Refusal("reserved_path", `'${p}' is under a reserved top-level name (${PROJECT_BUNDLE_RESERVED_TOP_LEVEL.join(", ")})`, p);
193
+ let linkname = "";
194
+ if (entryType === "symlink") {
195
+ linkname = decodeName(rawLink);
196
+ if (linkname === "" || linkname.startsWith("/") || /[\x00-\x1f\x7f]/.test(linkname) || Buffer.byteLength(linkname) > PATH_MAX_BYTES) {
197
+ throw new Refusal("unsafe_path", `the symbolic link '${p}' needs a non-empty relative target of at most ${PATH_MAX_BYTES} bytes without control characters`, p);
198
+ }
199
+ }
200
+ this.tree.place(p, entryType, linkname);
201
+ this.entries++;
202
+ const entry = { path: p, type: entryType, mode: octal(h, 100, 8, "mode"), mtime: octalOrZero(h, 136, 12), size: entryType === "file" ? size : 0, linkname };
203
+ if (this.writer !== undefined)
204
+ await this.writer.entry(entry);
205
+ this.body = { remaining: size, pad: padOf(size), sink: entryType === "file" && this.writer !== undefined ? "write" : "skip" };
206
+ }
207
+ applyMeta(kind, data) {
208
+ if (kind === "L")
209
+ return void (this.longName = data.toString("latin1").replace(/\0+$/, ""));
210
+ if (kind === "K")
211
+ return void (this.longLink = data.toString("latin1").replace(/\0+$/, ""));
212
+ const records = parsePax(data);
213
+ for (const key of records.keys()) {
214
+ if (paxKeyReading(key) !== "unsupported")
215
+ continue;
216
+ if (kind === "g")
217
+ throw new Refusal("entry_type", `a global pax header carries the extension '${printable(key)}' — a project bundle holds plain regular files, directories and symbolic links only`);
218
+ this.unsupportedPax ??= key;
219
+ }
220
+ if (kind === "g") {
221
+ if (records.has("path") || records.has("linkpath") || records.has("size"))
222
+ throw new Refusal("not_archive", "a global pax header sets path / linkpath / size");
223
+ return;
224
+ }
225
+ const sizeText = records.get("size");
226
+ if (sizeText !== undefined && !/^\d{1,12}$/.test(sizeText))
227
+ throw new Refusal("not_archive", "a pax size record is not a decimal byte count");
228
+ this.pax = {
229
+ ...(records.has("path") ? { path: records.get("path") } : {}),
230
+ ...(records.has("linkpath") ? { linkpath: records.get("linkpath") } : {}),
231
+ ...(sizeText !== undefined ? { size: Number(sizeText) } : {}),
232
+ };
233
+ }
234
+ }
235
+ function padOf(size) {
236
+ return (BLOCK - (size % BLOCK)) % BLOCK;
237
+ }
238
+ function checksumHolds(h) {
239
+ const stored = octalOrNaN(h, 148, 8);
240
+ let unsigned = 0;
241
+ let signed = 0;
242
+ for (let i = 0; i < BLOCK; i++) {
243
+ const b = i >= 148 && i < 156 ? 0x20 : h[i];
244
+ unsigned += b;
245
+ signed += b > 127 ? b - 256 : b;
246
+ }
247
+ return stored === unsigned || stored === signed;
248
+ }
249
+ function octalOrNaN(h, off, len) {
250
+ const s = h.toString("latin1", off, off + len).replace(/[\0 ]+$/, "").replace(/^ +/, "");
251
+ return /^[0-7]+$/.test(s) ? parseInt(s, 8) : Number.NaN;
252
+ }
253
+ function octal(h, off, len, field) {
254
+ if (h[off] & 0x80)
255
+ throw new Refusal("not_archive", `the ${field} field is in base-256 form`);
256
+ const s = h.toString("latin1", off, off + len).replace(/[\0 ]+$/, "").replace(/^ +/, "");
257
+ if (s === "")
258
+ return 0;
259
+ if (!/^[0-7]+$/.test(s))
260
+ throw new Refusal("not_archive", `the ${field} field is not octal`);
261
+ return parseInt(s, 8);
262
+ }
263
+ function octalOrZero(h, off, len) {
264
+ const v = octalOrNaN(h, off, len);
265
+ return Number.isFinite(v) && v >= 0 ? v : 0;
266
+ }
267
+ function cstr(h, off, len) {
268
+ const end = h.indexOf(0, off);
269
+ return h.toString("latin1", off, end === -1 || end > off + len ? off + len : end);
270
+ }
271
+ function joinUstarName(prefix, name) {
272
+ return prefix === "" ? name : `${prefix}/${name}`;
273
+ }
274
+ function decodeName(raw) {
275
+ try {
276
+ return UTF8.decode(Buffer.from(raw, "latin1"));
277
+ }
278
+ catch {
279
+ throw new Refusal("unsafe_path", "an entry path is not valid UTF-8", raw.replace(/[^\x20-\x7e]/g, "?"));
280
+ }
281
+ }
282
+ function parsePax(data) {
283
+ const out = new Map();
284
+ let off = 0;
285
+ while (off < data.length) {
286
+ const sp = data.indexOf(0x20, off);
287
+ if (sp === -1)
288
+ throw new Refusal("not_archive", "a pax record has no length");
289
+ const lenText = data.toString("latin1", off, sp);
290
+ if (!/^\d{1,7}$/.test(lenText))
291
+ throw new Refusal("not_archive", "a pax record length is not decimal");
292
+ const len = Number(lenText);
293
+ if (len <= sp - off + 1 || off + len > data.length || data[off + len - 1] !== 0x0a)
294
+ throw new Refusal("not_archive", "a pax record is malformed");
295
+ const rec = data.toString("latin1", sp + 1, off + len - 1);
296
+ const eq = rec.indexOf("=");
297
+ if (eq <= 0)
298
+ throw new Refusal("not_archive", "a pax record has no key");
299
+ out.set(rec.slice(0, eq), rec.slice(eq + 1));
300
+ off += len;
301
+ }
302
+ return out;
303
+ }
304
+ function tarTypeWord(type) {
305
+ switch (type) {
306
+ case "1":
307
+ return "hard link";
308
+ case "3":
309
+ return "character device";
310
+ case "4":
311
+ return "block device";
312
+ case "6":
313
+ return "FIFO";
314
+ case "7":
315
+ return "contiguous file";
316
+ case "S":
317
+ return "sparse file";
318
+ default:
319
+ return `tar entry of type '${type.replace(/[^\x20-\x7e]/g, "?")}'`;
320
+ }
321
+ }
322
+ function landingOf(name) {
323
+ if (/[\x00-\x1f\x7f]/.test(name))
324
+ throw new Refusal("unsafe_path", `an entry path carries a control character`, name.replace(/[\x00-\x1f\x7f]/g, "?"));
325
+ if (name.startsWith("/"))
326
+ throw new Refusal("unsafe_path", `'${name}' is an absolute path`, name);
327
+ const segs = [];
328
+ for (const seg of name.split("/")) {
329
+ if (seg === "" || seg === ".")
330
+ continue;
331
+ if (seg === "..")
332
+ throw new Refusal("unsafe_path", `'${name}' has a '..' segment`, name);
333
+ segs.push(seg);
334
+ }
335
+ const p = segs.join("/");
336
+ if (Buffer.byteLength(p) > PATH_MAX_BYTES)
337
+ throw tooLong(p);
338
+ const wide = segs.find((seg) => Buffer.byteLength(seg) > NAME_MAX_BYTES);
339
+ if (wide !== undefined)
340
+ throw new Refusal("unsafe_path", `'${p}' has a name of ${Buffer.byteLength(wide)} bytes — the sandbox's filesystem holds at most ${NAME_MAX_BYTES} bytes per name`, p);
341
+ return p;
342
+ }
343
+ function tooLong(p) {
344
+ return new Refusal("unsafe_path", `an entry path of ${Buffer.byteLength(p)} bytes (starting '${printable(p.slice(0, 64))}…') — the sandbox's filesystem holds at most ${PATH_MAX_BYTES} bytes per path`);
345
+ }
346
+ function printable(s) {
347
+ return s.replace(/[^\x20-\x7e]/g, "?");
348
+ }
349
+ function paxKeyReading(key) {
350
+ switch (key) {
351
+ case "path":
352
+ case "linkpath":
353
+ case "size":
354
+ return key;
355
+ case "mtime":
356
+ case "atime":
357
+ case "ctime":
358
+ case "uid":
359
+ case "gid":
360
+ case "uname":
361
+ case "gname":
362
+ case "comment":
363
+ case "charset":
364
+ case "hdrcharset":
365
+ case "SCHILY.fflags":
366
+ case "SCHILY.dev":
367
+ case "SCHILY.ino":
368
+ case "SCHILY.nlink":
369
+ case "LIBARCHIVE.creationtime":
370
+ case "LIBARCHIVE.symlinktype":
371
+ return "metadata";
372
+ default:
373
+ return PAX_METADATA_FAMILIES.some((family) => key.startsWith(family)) ? "metadata" : "unsupported";
374
+ }
375
+ }
376
+ const PAX_METADATA_FAMILIES = ["SCHILY.xattr.", "LIBARCHIVE.xattr.", "SCHILY.acl.", "security.", "RHT.security."];
377
+ class BundleTree {
378
+ maxObjects;
379
+ root = { path: "", parent: undefined, kind: "dir", children: new Map(), linkname: "", firstUnder: "" };
380
+ byPath = new Map();
381
+ links = [];
382
+ objects = 0;
383
+ offense;
384
+ steps = 0;
385
+ constructor(maxObjects) {
386
+ this.maxObjects = maxObjects;
387
+ }
388
+ place(p, type, linkname) {
389
+ const cut = p.lastIndexOf("/");
390
+ const parent = cut === -1 ? this.root : this.dirAt(p.slice(0, cut), p);
391
+ if (parent === undefined)
392
+ return;
393
+ if (parent.kind === "file" || parent.kind === "symlink")
394
+ return this.offend(p, parent);
395
+ const name = cut === -1 ? p : p.slice(cut + 1);
396
+ const known = parent.children.get(name);
397
+ if (known === undefined) {
398
+ this.add(parent, name, p, type, linkname, p);
399
+ return;
400
+ }
401
+ if (known.kind !== "implied")
402
+ throw new Refusal("duplicate_path", `'${p}' appears twice`, p);
403
+ known.kind = type;
404
+ known.linkname = linkname;
405
+ if (type === "symlink")
406
+ this.links.push(known);
407
+ if (type !== "dir")
408
+ this.offend(known.firstUnder, known);
409
+ }
410
+ dirAt(dirPath, entryPath) {
411
+ const known = this.byPath.get(dirPath);
412
+ if (known !== undefined)
413
+ return known;
414
+ const ends = [];
415
+ let base = this.root;
416
+ for (let end = dirPath.length;;) {
417
+ ends.push(end);
418
+ const up = dirPath.lastIndexOf("/", end - 1);
419
+ if (up === -1)
420
+ break;
421
+ const found = this.byPath.get(dirPath.slice(0, up));
422
+ if (found !== undefined) {
423
+ base = found;
424
+ break;
425
+ }
426
+ end = up;
427
+ }
428
+ let cur = base;
429
+ for (let k = ends.length - 1; k >= 0; k--) {
430
+ if (cur.kind === "file" || cur.kind === "symlink") {
431
+ this.offend(entryPath, cur);
432
+ return undefined;
433
+ }
434
+ const end = ends[k];
435
+ cur = this.add(cur, dirPath.slice(dirPath.lastIndexOf("/", end - 1) + 1, end), dirPath.slice(0, end), "implied", "", entryPath);
436
+ }
437
+ return cur;
438
+ }
439
+ add(parent, name, p, kind, linkname, firstUnder) {
440
+ if (this.objects >= this.maxObjects)
441
+ throw new Refusal("too_many_entries", `the bundle unpacks to more than ${this.maxObjects} filesystem objects (its entries and the directories their paths imply)`);
442
+ this.objects++;
443
+ const node = { path: p, parent, kind, children: new Map(), linkname, firstUnder };
444
+ parent.children.set(name, node);
445
+ this.byPath.set(p, node);
446
+ if (kind === "symlink")
447
+ this.links.push(node);
448
+ return node;
449
+ }
450
+ offend(entryPath, blocker) {
451
+ this.offense ??= { entryPath, blocker };
452
+ }
453
+ topLevel() {
454
+ return [...this.root.children.keys()].sort();
455
+ }
456
+ async judge() {
457
+ for (const link of this.links) {
458
+ this.landingOf(link, 0);
459
+ if (this.steps >= YIELD_STEPS) {
460
+ this.steps = 0;
461
+ await new Promise((resolve) => setImmediate(resolve));
462
+ }
463
+ }
464
+ if (this.offense !== undefined)
465
+ throw this.offenseRefusal(this.offense);
466
+ }
467
+ offenseRefusal({ entryPath, blocker }) {
468
+ if (blocker.kind === "file")
469
+ return new Refusal("unsafe_path", `'${entryPath}' is under the regular file '${blocker.path}'`, entryPath);
470
+ const end = this.walk(this.landingOf(blocker, 0), entryPath.slice(blocker.path.length + 1), 0, blocker);
471
+ const top = end.node === this.root && end.depth > 0 ? end.top : undefined;
472
+ if (top !== undefined && PROJECT_BUNDLE_RESERVED_TOP_LEVEL.includes(top)) {
473
+ return new Refusal("reserved_path", `'${entryPath}' lands under '${top}' through the symbolic link '${blocker.path}' of the bundle — a reserved top-level name`, entryPath);
474
+ }
475
+ return new Refusal("unsafe_path", `'${entryPath}' is written through a symbolic link of the bundle ('${blocker.path}')`, entryPath);
476
+ }
477
+ landingOf(link, hops) {
478
+ if (link.landing !== undefined)
479
+ return link.landing;
480
+ if (hops >= MAX_LINK_HOPS)
481
+ throw new Refusal("unsafe_path", `the symbolic link '${link.path}' -> '${link.linkname}' does not land within ${MAX_LINK_HOPS} links (the bundle's links form a loop, or a chain that deep)`, link.path);
482
+ const at = this.walk({ node: link.parent, depth: 0, top: undefined }, link.linkname, hops + 1, link);
483
+ link.landing = at;
484
+ return at;
485
+ }
486
+ walk(from, text, hops, owner) {
487
+ let { node, depth, top } = from;
488
+ for (let i = 0; i <= text.length;) {
489
+ let j = text.indexOf("/", i);
490
+ if (j === -1)
491
+ j = text.length;
492
+ const seg = text.slice(i, j);
493
+ i = j + 1;
494
+ this.steps++;
495
+ if (seg === "" || seg === ".")
496
+ continue;
497
+ if (seg === "..") {
498
+ if (depth > 0) {
499
+ depth--;
500
+ if (depth === 0)
501
+ top = undefined;
502
+ continue;
503
+ }
504
+ if (node.parent === undefined)
505
+ throw new Refusal("unsafe_path", `the symbolic link '${owner.path}' -> '${owner.linkname}' does not stay inside the bundle (resolved through the bundle's own links)`, owner.path);
506
+ node = node.parent;
507
+ continue;
508
+ }
509
+ if (depth > 0) {
510
+ depth++;
511
+ continue;
512
+ }
513
+ const child = node.children.get(seg);
514
+ if (child === undefined) {
515
+ if (node === this.root)
516
+ top = seg;
517
+ depth = 1;
518
+ continue;
519
+ }
520
+ if (child.kind === "symlink") {
521
+ ({ node, depth, top } = this.landingOf(child, hops));
522
+ continue;
523
+ }
524
+ node = child;
525
+ }
526
+ return { node, depth, top };
527
+ }
528
+ }
529
+ function createCanonicalWriter() {
530
+ const gz = createGzip();
531
+ const out = [];
532
+ gz.on("data", (b) => out.push(b));
533
+ let pending = 0;
534
+ const write = async (b) => {
535
+ if (!gz.write(b))
536
+ await once(gz, "drain");
537
+ };
538
+ return {
539
+ async entry(e) {
540
+ const name = e.type === "dir" ? `${e.path}/` : e.path;
541
+ const needsPax = Buffer.byteLength(name) > 100 || Buffer.byteLength(e.linkname) > 100;
542
+ if (needsPax) {
543
+ const recs = Buffer.from(paxRecord("path", name) + (Buffer.byteLength(e.linkname) > 100 ? paxRecord("linkpath", e.linkname) : ""), "utf8");
544
+ await write(ustarHeader({ name: "PaxHeader", type: "x", mode: 0o644, size: recs.length, mtime: e.mtime, linkname: "" }));
545
+ await write(recs);
546
+ await write(Buffer.alloc(padOf(recs.length)));
547
+ }
548
+ const mode = e.type === "file" ? (e.mode & 0o111 ? 0o755 : 0o644) : e.type === "dir" ? 0o755 : 0o777;
549
+ const type = e.type === "file" ? "0" : e.type === "dir" ? "5" : "2";
550
+ await write(ustarHeader({ name: needsPax ? truncateUtf8(name, 100) : name, type, mode, size: e.size, mtime: e.mtime, linkname: needsPax ? truncateUtf8(e.linkname, 100) : e.linkname }));
551
+ pending = e.size;
552
+ },
553
+ async content(part) {
554
+ await write(part);
555
+ },
556
+ async endContent() {
557
+ await write(Buffer.alloc(padOf(pending)));
558
+ pending = 0;
559
+ },
560
+ async end() {
561
+ await write(Buffer.alloc(BLOCK * 2));
562
+ gz.end();
563
+ await once(gz, "end");
564
+ return new Uint8Array(Buffer.concat(out));
565
+ },
566
+ abort() {
567
+ gz.destroy();
568
+ },
569
+ };
570
+ }
571
+ function paxRecord(key, value) {
572
+ const body = ` ${key}=${value}\n`;
573
+ let len = Buffer.byteLength(body) + 1;
574
+ while (String(len).length + Buffer.byteLength(body) !== len)
575
+ len = String(len).length + Buffer.byteLength(body);
576
+ return `${len}${body}`;
577
+ }
578
+ function truncateUtf8(s, max) {
579
+ let b = Buffer.from(s, "utf8");
580
+ if (b.length <= max)
581
+ return s;
582
+ b = b.subarray(0, max);
583
+ return b.toString("utf8").replace(/�+$/, "");
584
+ }
585
+ function ustarHeader(f) {
586
+ const h = Buffer.alloc(BLOCK, 0);
587
+ Buffer.from(f.name, "utf8").copy(h, 0, 0, 100);
588
+ h.write(octalField(f.mode, 8), 100, "latin1");
589
+ h.write(octalField(0, 8), 108, "latin1");
590
+ h.write(octalField(0, 8), 116, "latin1");
591
+ h.write(octalField(f.size, 12), 124, "latin1");
592
+ h.write(octalField(Math.min(f.mtime, 0o77777777777), 12), 136, "latin1");
593
+ h.write(" ", 148, "latin1");
594
+ h.write(f.type, 156, "latin1");
595
+ Buffer.from(f.linkname, "utf8").copy(h, 157, 0, 100);
596
+ h.write("ustar\0", 257, "latin1");
597
+ h.write("00", 263, "latin1");
598
+ h.write(octalField(0, 8), 329, "latin1");
599
+ h.write(octalField(0, 8), 337, "latin1");
600
+ let sum = 0;
601
+ for (let i = 0; i < BLOCK; i++)
602
+ sum += h[i];
603
+ h.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, "latin1");
604
+ return h;
605
+ }
606
+ function octalField(n, width) {
607
+ return `${Math.max(0, Math.floor(n)).toString(8).padStart(width - 1, "0")}\0`;
608
+ }
609
+ //# sourceMappingURL=project-bundle.js.map
package/dist/run-local.js CHANGED
@@ -770,14 +770,8 @@ export async function runLocal(argv, deps = {}) {
770
770
  }
771
771
  if (result.terminal.kind !== "completed" || unfinishedStop !== undefined)
772
772
  exitCode = 1;
773
- if (memorySync) {
774
- try {
775
- await memorySync.syncOnce("post-task");
776
- }
777
- catch (err) {
778
- logger.warn("memory_sync_failed", { reason: "post-task", err: String(err) });
779
- }
780
- }
773
+ if (memorySync)
774
+ await memorySync.syncOnce("post-task");
781
775
  await settleSessionBackground();
782
776
  runner.sessions.dispose();
783
777
  subRunner.sessions.dispose();
package/dist/runs.js CHANGED
@@ -352,7 +352,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
352
352
  const parkLeg = async (word, res) => {
353
353
  const grafted = await graftParkToolCallId(res, checkpointCards);
354
354
  await sink.appendParked(word, grafted);
355
- await flushModelUsage();
356
355
  const sealed = sealResultForExit(grafted);
357
356
  reached = { kind: word, result: sealed };
358
357
  await parkRowWrite[word](sealed);
@@ -376,7 +375,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
376
375
  acceptance?.settle(safe);
377
376
  };
378
377
  const instructionLeg = createInstructionSelectionLeg();
379
- const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData), legSections: () => ({ instructions: instructionLeg.segment(), skills: runSkillsOf(spec) }) });
378
+ const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData), legSections: () => ({ instructions: instructionLeg.segment(), skills: runSkillsOf(spec) }), usage: { modelUsage, taskId } });
380
379
  const append = (type, data) => sink.append(type, data);
381
380
  const flush = () => sink.flush();
382
381
  const flushModelUsage = () => appendModelUsageDelta(append, modelUsage, taskId);
@@ -52,7 +52,10 @@ export interface EgressPolicyInputs {
52
52
  * 读数 → 缺口(纯函数、全函数:不抛;顺序:读不到的表 → 策略缺席 → 逐节点逐地址 → IPv6)。一张表读不到,只跳过要它的那一判;另一张照判。
53
53
  */
54
54
  export declare function sandboxEgressGaps(inputs: EgressPolicyInputs): SandboxEgressGap[];
55
- /** 读数的说法:每个缺口一行 warn `sandbox_egress_policy_gap` + 一次 `recordFailOpen`(P-DEBT);零缺口一行 info `sandbox_egress_policy_checked`。 */
55
+ /**
56
+ * 读数的说法:每个缺口一行 warn `sandbox_egress_policy_gap` + 一次 `recordFailOpen`(P-DEBT);零缺口一行 info `sandbox_egress_policy_checked`。
57
+ * `unreadable` 的 `detail` 是控制面请求的失败句(客户端异常原文)⇒ 经出口座(`operator-log-text`)出门,与起服面其余运维行里的抛出物文本同一读法。
58
+ */
56
59
  export declare function sayEgressPolicyReading(gaps: readonly SandboxEgressGap[], logger: {
57
60
  info(msg: string, fields?: Record<string, unknown>): void;
58
61
  warn(msg: string, fields?: Record<string, unknown>): void;
@@ -1,5 +1,6 @@
1
1
  import { BlockList, isIP } from "node:net";
2
2
  import { recordFailOpen } from "./observability/fail-open.js";
3
+ import { seatText } from "./trace/exit-seat.js";
3
4
  export const SANDBOX_IPV6_WORDS = ["enabled", "disabled"];
4
5
  export const SANDBOX_IPV6_DEFAULT = "enabled";
5
6
  export function sandboxPodSysctls(ipv6) {
@@ -229,7 +230,8 @@ export function sayEgressPolicyReading(gaps, logger, namespace) {
229
230
  }
230
231
  for (const gap of gaps) {
231
232
  recordFailOpen("server.sandbox-egress.policy-gap", `kind=${gap.kind}${gap.kind === "unreadable" ? ` what=${gap.what}` : ""}`);
232
- logger?.warn("sandbox_egress_policy_gap", { namespace, ...gap, effect: effectOf(gap) });
233
+ const shown = gap.kind === "unreadable" ? { ...gap, detail: seatText("operator-log-text", gap.detail) } : gap;
234
+ logger?.warn("sandbox_egress_policy_gap", { namespace, ...shown, effect: effectOf(gap) });
233
235
  }
234
236
  }
235
237
  //# sourceMappingURL=sandbox-egress-policy.js.map