@stdd/plugin 0.9.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 (80) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/.codex-plugin/plugin.json +21 -0
  3. package/LICENSE +21 -0
  4. package/README.md +47 -0
  5. package/extensions/stdd.mjs +77 -0
  6. package/hooks/claude-hooks.json +28 -0
  7. package/hooks/codex-hooks.json +28 -0
  8. package/package.json +38 -0
  9. package/runtime/adapters/README.md +158 -0
  10. package/runtime/cli/check.mjs +555 -0
  11. package/runtime/cli/ci.mjs +190 -0
  12. package/runtime/cli/claude-hooks.mjs +689 -0
  13. package/runtime/cli/config.mjs +27 -0
  14. package/runtime/cli/evidence.mjs +249 -0
  15. package/runtime/cli/generated-files.mjs +1693 -0
  16. package/runtime/cli/held-fs.mjs +415 -0
  17. package/runtime/cli/init.mjs +883 -0
  18. package/runtime/cli/ledger.mjs +1470 -0
  19. package/runtime/cli/lib.mjs +909 -0
  20. package/runtime/cli/path-bytes.mjs +83 -0
  21. package/runtime/cli/policy.mjs +112 -0
  22. package/runtime/cli/recorders.mjs +188 -0
  23. package/runtime/cli/review-fs.mjs +825 -0
  24. package/runtime/cli/review.mjs +1065 -0
  25. package/runtime/cli/runtime.mjs +32 -0
  26. package/runtime/cli/scope.mjs +185 -0
  27. package/runtime/cli/snapshot.mjs +897 -0
  28. package/runtime/cli/state-validation.mjs +168 -0
  29. package/runtime/cli/status.mjs +580 -0
  30. package/runtime/cli/stdd.mjs +536 -0
  31. package/runtime/cli/worker-fs.mjs +971 -0
  32. package/runtime/cli/worker-metadata.mjs +139 -0
  33. package/runtime/cli/worker.mjs +779 -0
  34. package/runtime/method/README.md +634 -0
  35. package/runtime/method/reference-commands.md +147 -0
  36. package/runtime/method/reference-generated-state.md +151 -0
  37. package/runtime/method/reference-integration.md +233 -0
  38. package/runtime/package.json +65 -0
  39. package/runtime/playbooks/brainstorming.md +46 -0
  40. package/runtime/playbooks/debugging.md +36 -0
  41. package/runtime/playbooks/delegate-slice.md +129 -0
  42. package/runtime/playbooks/finish-change.md +46 -0
  43. package/runtime/playbooks/implement.md +26 -0
  44. package/runtime/playbooks/investigation.md +33 -0
  45. package/runtime/playbooks/managed-playbooks.json +14 -0
  46. package/runtime/playbooks/planning.md +177 -0
  47. package/runtime/playbooks/pr-green.md +50 -0
  48. package/runtime/playbooks/start-change.md +37 -0
  49. package/runtime/playbooks/worktrees.md +45 -0
  50. package/runtime/prebuilds/stdd-fs/darwin-arm64/stdd-fs +0 -0
  51. package/runtime/prebuilds/stdd-fs/darwin-x64/stdd-fs +0 -0
  52. package/runtime/prebuilds/stdd-fs/linux-arm64/stdd-fs +0 -0
  53. package/runtime/prebuilds/stdd-fs/linux-x64/stdd-fs +0 -0
  54. package/runtime/prebuilds/stdd-fs/manifest.json +47 -0
  55. package/runtime/prebuilds/stdd-fs/win32-arm64/stdd-fs.exe +0 -0
  56. package/runtime/prebuilds/stdd-fs/win32-x64/stdd-fs.exe +0 -0
  57. package/runtime/sdk/adapters.mjs +279 -0
  58. package/runtime/sdk/file-observation.mjs +12 -0
  59. package/runtime/sdk/index.d.ts +140 -0
  60. package/runtime/sdk/index.mjs +31 -0
  61. package/runtime/sdk/native-fs.mjs +1235 -0
  62. package/runtime/sdk/path.mjs +71 -0
  63. package/runtime/sdk/text.mjs +42 -0
  64. package/runtime/sdk/workflow.mjs +294 -0
  65. package/runtime/templates/deferred-design.md +47 -0
  66. package/runtime/templates/github-stdd.yml +42 -0
  67. package/runtime/templates/gitlab-stdd.yml +72 -0
  68. package/runtime/templates/pr-description.md +35 -0
  69. package/scripts/adopting-root.mjs +42 -0
  70. package/scripts/stdd-hook.mjs +72 -0
  71. package/skills/stdd-brainstorming/SKILL.md +48 -0
  72. package/skills/stdd-debugging/SKILL.md +38 -0
  73. package/skills/stdd-delegate-slice/SKILL.md +118 -0
  74. package/skills/stdd-finish-change/SKILL.md +40 -0
  75. package/skills/stdd-implement/SKILL.md +28 -0
  76. package/skills/stdd-investigation/SKILL.md +35 -0
  77. package/skills/stdd-planning/SKILL.md +165 -0
  78. package/skills/stdd-pr-green/SKILL.md +52 -0
  79. package/skills/stdd-start-change/SKILL.md +39 -0
  80. package/skills/stdd-worktrees/SKILL.md +46 -0
@@ -0,0 +1,971 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ openNativeRepoPath,
5
+ openOrCreateNativeRepoDirectory,
6
+ readNativeFile,
7
+ verifyNativeRepoDirectory,
8
+ writeNativeFileContent,
9
+ } from "./held-fs.mjs";
10
+ import { sha256 } from "./lib.mjs";
11
+ import { viewPath } from "./path-bytes.mjs";
12
+
13
+ export const WORKER_DELETIONS_REL = ".stdd/worker-deletions";
14
+ export const WORKER_FILE_MODES = new Set([0o600, 0o644, 0o755]);
15
+ export const WORKER_DIRECTORY_MODES = new Set([0o700, 0o755]);
16
+ const WORKER_NATIVE_READ_LIMIT = 64 * 1024 * 1024;
17
+
18
+ export const workerPathForMatch = (relative) => Buffer.from(relative, "utf8").toString("latin1");
19
+ export const workerViewPath = (relative) => viewPath(workerPathForMatch(relative));
20
+
21
+ function sameNativeIdentity(left, right) {
22
+ return (
23
+ left?.version === right?.version &&
24
+ left?.platform === right?.platform &&
25
+ left?.volume === right?.volume &&
26
+ left?.fileId === right?.fileId &&
27
+ left?.kind === right?.kind
28
+ );
29
+ }
30
+
31
+ function sameNativeObservation(left, right) {
32
+ return (
33
+ sameNativeIdentity(left?.identity, right?.identity) &&
34
+ left?.owner === right?.owner &&
35
+ left?.permissions === right?.permissions &&
36
+ left?.linkCount === right?.linkCount &&
37
+ left?.size === right?.size &&
38
+ left?.modifiedNs === right?.modifiedNs &&
39
+ left?.changedNs === right?.changedNs
40
+ );
41
+ }
42
+
43
+ function nativeMode(observation, fallback = null) {
44
+ if (observation.identity.platform === "win32") return fallback;
45
+ const parsed = Number(observation.permissions);
46
+ return Number.isSafeInteger(parsed) ? parsed & 0o777 : null;
47
+ }
48
+
49
+ function assertWorkerFileObservation(
50
+ context,
51
+ relative,
52
+ observation,
53
+ modeHint = null,
54
+ legacyMode = null,
55
+ ) {
56
+ if (observation.identity.kind !== "file" || observation.linkCount !== "1") {
57
+ throw new Error(
58
+ `worker path ${workerViewPath(relative)} must be a single-linked regular file or symlink`,
59
+ );
60
+ }
61
+ if (
62
+ observation.identity.platform !== "win32" &&
63
+ observation.owner !== context.root.observation.owner
64
+ ) {
65
+ throw new Error(`worker path ${workerViewPath(relative)} must be owned by the worker root owner`);
66
+ }
67
+ const mode = nativeMode(observation, modeHint);
68
+ if (!WORKER_FILE_MODES.has(mode) && mode !== legacyMode) {
69
+ throw new Error(
70
+ `worker path ${workerViewPath(relative)} has unsupported mode ${
71
+ mode === null ? "unknown" : `0${mode.toString(8)}`
72
+ }; supported file modes are 0600, 0644, and 0755`,
73
+ );
74
+ }
75
+ return mode;
76
+ }
77
+
78
+ function symlinkTargetBytes(state) {
79
+ if (typeof state?.targetBase64 === "string") return Buffer.from(state.targetBase64, "base64");
80
+ return Buffer.from(state?.target ?? "", "utf8");
81
+ }
82
+
83
+ function symlinkTargetString(state, relative, platform = process.platform) {
84
+ const bytes = symlinkTargetBytes(state);
85
+ const windowsTarget =
86
+ (platform ?? process.platform) === "win32" || state?.portable?.sandbox?.platform === "win32";
87
+ let target = bytes.toString(windowsTarget ? "utf16le" : "utf8");
88
+ if (!Buffer.from(target, windowsTarget ? "utf16le" : "utf8").equals(bytes)) {
89
+ throw new Error(
90
+ windowsTarget
91
+ ? `worker symlink ${workerViewPath(relative)} has a malformed UTF-16LE target that protocol-v1 cannot publish`
92
+ : `worker symlink ${workerViewPath(relative)} has a non-UTF-8 target that protocol-v1 cannot publish`,
93
+ );
94
+ }
95
+ if (windowsTarget) {
96
+ if (target.startsWith("\\??\\UNC\\")) target = `\\\\${target.slice(8)}`;
97
+ else if (target.startsWith("\\??\\")) target = target.slice(4);
98
+ }
99
+ return target;
100
+ }
101
+
102
+ export function preflightWorkerCreationState(
103
+ relative,
104
+ state,
105
+ inheritedLegacyMode = null,
106
+ platform = null,
107
+ ) {
108
+ if (state === null) return;
109
+ if (state.type === "file") {
110
+ if (
111
+ (inheritedLegacyMode !== null && state.mode !== inheritedLegacyMode) ||
112
+ (inheritedLegacyMode === null && !WORKER_FILE_MODES.has(state.mode))
113
+ ) {
114
+ throw new Error(`worker path ${workerViewPath(relative)} has an unsupported creation mode`);
115
+ }
116
+ return;
117
+ }
118
+ if (state.type === "symlink") {
119
+ symlinkTargetString(state, relative, platform);
120
+ return;
121
+ }
122
+ throw new Error(`worker path ${workerViewPath(relative)} has an unsupported file type`);
123
+ }
124
+
125
+ async function closeNative(context, ...values) {
126
+ const capabilities = new Set(
127
+ values
128
+ .flat()
129
+ .map((value) => (typeof value === "string" ? value : value?.cap))
130
+ .filter((cap) => cap && cap !== context.root.cap),
131
+ );
132
+ for (const cap of capabilities) await context.session.closeCapability(cap).catch(() => {});
133
+ }
134
+
135
+ async function openNativeParent(context, relative, label) {
136
+ const parentRelative = path.posix.dirname(relative);
137
+ return openNativeRepoPath(context, parentRelative, label);
138
+ }
139
+
140
+ async function statNativeChild(context, parent, name) {
141
+ try {
142
+ return (await context.session.stat(parent.cap, name)).observation;
143
+ } catch (error) {
144
+ if (error?.code === "not-found") return null;
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ export async function readNativeWorkerPath(
150
+ context,
151
+ relative,
152
+ { bytes = false, modeHint = null, legacyMode = null } = {},
153
+ ) {
154
+ let parent;
155
+ let file;
156
+ try {
157
+ try {
158
+ parent = await openNativeParent(
159
+ context,
160
+ relative,
161
+ `worker path parent ${JSON.stringify(path.posix.dirname(relative))}`,
162
+ );
163
+ } catch (error) {
164
+ if (error?.code === "not-found") return { state: null, bytes: null, observation: null };
165
+ throw error;
166
+ }
167
+ const name = path.posix.basename(relative);
168
+ const observed = await statNativeChild(context, parent, name);
169
+ if (observed === null) return { state: null, bytes: null, observation: null };
170
+ if (observed.identity.kind === "symlink") {
171
+ const result = await context.session.readLink(parent.cap, name, observed.identity);
172
+ const targetBytes = Buffer.from(result.data, "base64");
173
+ const after = await context.session.stat(parent.cap, name);
174
+ if (!sameNativeObservation(observed, after.observation)) {
175
+ throw new Error(`worker path ${workerViewPath(relative)} changed while reading its symlink`);
176
+ }
177
+ const target = targetBytes.toString("utf8");
178
+ return {
179
+ state: {
180
+ type: "symlink",
181
+ target,
182
+ targetBase64: targetBytes.toString("base64"),
183
+ hash: sha256(Buffer.concat([Buffer.from("link:"), targetBytes])),
184
+ },
185
+ bytes: null,
186
+ observation: observed,
187
+ };
188
+ }
189
+ const mode = assertWorkerFileObservation(context, relative, observed, modeHint, legacyMode);
190
+ file = await context.session.openChild(parent.cap, name);
191
+ if (!sameNativeObservation(observed, file.observation)) {
192
+ throw new Error(`worker path ${workerViewPath(relative)} changed before it could be read`);
193
+ }
194
+ const content = await readNativeFile(context, file, WORKER_NATIVE_READ_LIMIT);
195
+ const finalPath = await context.session.stat(parent.cap, name);
196
+ if (!sameNativeObservation(observed, finalPath.observation)) {
197
+ throw new Error(`worker path ${workerViewPath(relative)} changed while reading`);
198
+ }
199
+ return {
200
+ state: { type: "file", hash: sha256(content), mode },
201
+ bytes: bytes ? content : null,
202
+ observation: observed,
203
+ };
204
+ } finally {
205
+ await closeNative(context, file, parent);
206
+ }
207
+ }
208
+
209
+ export function stateWithPortableIdentity(state, sourceObservation, sandboxObservation) {
210
+ if (state === null) return null;
211
+ return {
212
+ ...state,
213
+ portable: {
214
+ source: sourceObservation.identity,
215
+ sandbox: sandboxObservation.identity,
216
+ },
217
+ };
218
+ }
219
+
220
+ export function sameWorkerState(left, right) {
221
+ if (left === null || left === undefined || right === null || right === undefined) {
222
+ return (left ?? null) === (right ?? null);
223
+ }
224
+ if (left.type !== right.type || left.hash !== right.hash) return false;
225
+ if (left.type === "file") return left.mode === right.mode;
226
+ if (left.type === "symlink") {
227
+ return symlinkTargetBytes(left).equals(symlinkTargetBytes(right));
228
+ }
229
+ return false;
230
+ }
231
+
232
+ async function ensureWorkerParent(context, relative, mode = 0o755, beforeCommit = null) {
233
+ const parentRelative = path.posix.dirname(relative);
234
+ return openOrCreateNativeRepoDirectory(context, parentRelative, {
235
+ mode,
236
+ label: `worker parent for ${JSON.stringify(relative)}`,
237
+ beforeCommit,
238
+ });
239
+ }
240
+
241
+ export async function preflightWorkerParent(context, relative) {
242
+ const segments = path.posix.dirname(relative) === "." ? [] : path.posix.dirname(relative).split("/");
243
+ let current = context.root;
244
+ const opened = [];
245
+ try {
246
+ for (const segment of segments) {
247
+ try {
248
+ current = await context.session.openChild(current.cap, segment);
249
+ } catch (error) {
250
+ if (error?.code === "not-found") return;
251
+ throw error;
252
+ }
253
+ opened.push(current);
254
+ if (current.observation.identity.kind !== "directory") {
255
+ throw new Error(`worker parent for ${workerViewPath(relative)} contains a non-directory`);
256
+ }
257
+ }
258
+ } finally {
259
+ await closeNative(context, opened);
260
+ }
261
+ }
262
+
263
+ async function flushAndVerifyParent(context, relative, parent) {
264
+ await context.session.flush(parent.cap, "namespace", parent.observation.identity);
265
+ await verifyNativeRepoDirectory(
266
+ context,
267
+ path.posix.dirname(relative),
268
+ parent.observation.identity,
269
+ `worker parent for ${JSON.stringify(relative)}`,
270
+ );
271
+ }
272
+
273
+ export async function writeNewWorkerPath(context, relative, result) {
274
+ if (result.state === null) return { state: null, observation: null };
275
+ const parent = await ensureWorkerParent(
276
+ context,
277
+ relative,
278
+ relative.startsWith(".stdd/") ? 0o700 : 0o755,
279
+ );
280
+ let created;
281
+ try {
282
+ const name = path.posix.basename(relative);
283
+ if (result.state.type === "symlink") {
284
+ created = await context.session.symlink(
285
+ parent.cap,
286
+ name,
287
+ symlinkTargetString(result.state, relative, result.observation?.identity?.platform ?? null),
288
+ );
289
+ } else {
290
+ created = await context.session.createFile(parent.cap, name, result.state.mode);
291
+ await writeNativeFileContent(context, created, result.bytes);
292
+ }
293
+ await flushAndVerifyParent(context, relative, parent);
294
+ const published = await context.session.stat(parent.cap, name);
295
+ if (!sameNativeIdentity(published.observation.identity, created.observation.identity)) {
296
+ const error = new Error(
297
+ `worker destination ${workerViewPath(relative)} changed after publication`,
298
+ );
299
+ error.mutation = "committed";
300
+ throw error;
301
+ }
302
+ if (result.state.type === "symlink") {
303
+ const linked = await context.session.readLink(parent.cap, name, created.observation.identity);
304
+ if (!Buffer.from(linked.data, "base64").equals(symlinkTargetBytes(result.state))) {
305
+ throw new Error(`worker destination ${workerViewPath(relative)} has the wrong symlink target`);
306
+ }
307
+ }
308
+ return { state: result.state, observation: published.observation };
309
+ } finally {
310
+ await closeNative(context, created, parent);
311
+ }
312
+ }
313
+
314
+ async function settleWorkerTemporary(
315
+ context,
316
+ relative,
317
+ workerId,
318
+ staged = null,
319
+ assertCollectionContext = null,
320
+ legacyMode = null,
321
+ ) {
322
+ const inspected = await readNativeWorkerPath(context, relative, {
323
+ bytes: true,
324
+ modeHint: legacyMode ?? 0o644,
325
+ legacyMode,
326
+ });
327
+ if (inspected.state === null) return;
328
+ if (staged && !sameNativeIdentity(inspected.observation.identity, staged.observation.identity)) {
329
+ throw new Error(`worker temporary ${workerViewPath(relative)} changed before cleanup`);
330
+ }
331
+ let settledState = inspected.state;
332
+ let expectedObservation = inspected.observation;
333
+ if (inspected.state.type === "file") {
334
+ if (assertCollectionContext !== null) await assertCollectionContext();
335
+ const parent = await openNativeParent(context, relative, "worker temporary cleanup parent");
336
+ let file;
337
+ try {
338
+ file = await context.session.openChild(parent.cap, path.posix.basename(relative));
339
+ if (!sameNativeIdentity(file.observation.identity, inspected.observation.identity)) {
340
+ throw new Error(`worker temporary ${workerViewPath(relative)} changed before cleanup`);
341
+ }
342
+ let offset = 0;
343
+ const length = inspected.bytes.length;
344
+ while (offset < length) {
345
+ const chunk = Buffer.alloc(Math.min(64 * 1024, length - offset));
346
+ const result = await context.session.write(file.cap, offset, chunk, file.observation.identity);
347
+ if (result.written < 1) throw new Error("worker temporary cleanup made no progress");
348
+ offset += result.written;
349
+ }
350
+ await context.session.flush(file.cap, "all", file.observation.identity);
351
+ await context.session.truncate(file.cap, 0, file.observation.identity);
352
+ await context.session.flush(file.cap, "all", file.observation.identity);
353
+ } finally {
354
+ await closeNative(context, file, parent);
355
+ }
356
+ const zeroed = await readNativeWorkerPath(context, relative, {
357
+ modeHint: inspected.state.mode,
358
+ legacyMode,
359
+ });
360
+ settledState = zeroed.state;
361
+ expectedObservation = zeroed.observation;
362
+ }
363
+ const identityKey = sha256(JSON.stringify(expectedObservation.identity)).slice("sha256:".length);
364
+ await quarantineWorkerDeletion(
365
+ context,
366
+ relative,
367
+ workerId,
368
+ settledState,
369
+ expectedObservation,
370
+ assertCollectionContext,
371
+ `${relative}#temporary-${identityKey}`,
372
+ legacyMode,
373
+ );
374
+ }
375
+
376
+ async function assertNativeExpectedState(context, relative, expectedState, legacyMode = null) {
377
+ const live = await readNativeWorkerPath(context, relative, {
378
+ legacyMode,
379
+ modeHint: expectedState?.type === "file" ? expectedState.mode : null,
380
+ });
381
+ if (!sameWorkerState(live.state, expectedState)) {
382
+ throw new Error(
383
+ `worker collection path ${workerViewPath(relative)} changed after preflight ` +
384
+ `(${JSON.stringify({ expected: expectedState, actual: live.state })})`,
385
+ );
386
+ }
387
+ return live;
388
+ }
389
+
390
+ export async function publishWorkerFile(
391
+ context,
392
+ relative,
393
+ content,
394
+ mode,
395
+ expectedState,
396
+ assertCollectionContext = null,
397
+ workerId = "worker-000000000000000000000000",
398
+ inheritedLegacyMode = null,
399
+ ) {
400
+ if (
401
+ (inheritedLegacyMode !== null &&
402
+ (!Number.isInteger(inheritedLegacyMode) ||
403
+ inheritedLegacyMode < 0 ||
404
+ inheritedLegacyMode > 0o777 ||
405
+ mode !== inheritedLegacyMode)) ||
406
+ (inheritedLegacyMode === null && !WORKER_FILE_MODES.has(mode))
407
+ ) {
408
+ throw new Error(
409
+ `worker path ${workerViewPath(relative)} may use only its exact inherited legacy mode`,
410
+ );
411
+ }
412
+ await assertNativeExpectedState(context, relative, expectedState);
413
+ if (assertCollectionContext !== null) await assertCollectionContext();
414
+ const beforeCommit = async () => {
415
+ await assertNativeExpectedState(context, relative, expectedState);
416
+ if (assertCollectionContext !== null) await assertCollectionContext();
417
+ };
418
+ const parent = await ensureWorkerParent(context, relative, 0o755, beforeCommit);
419
+ const token = sha256(`${workerId}:${relative}`).slice(7, 31);
420
+ const temp = `.stdd-worker-collect-${token}.tmp`;
421
+ const temporaryRelative =
422
+ path.posix.dirname(relative) === "." ? temp : `${path.posix.dirname(relative)}/${temp}`;
423
+ let staged;
424
+ try {
425
+ await beforeCommit();
426
+ const stagedMode = WORKER_FILE_MODES.has(mode) ? mode : 0o600;
427
+ staged = await context.session.createFile(parent.cap, temp, stagedMode);
428
+ await writeNativeFileContent(context, staged, content);
429
+ if (mode !== stagedMode) {
430
+ const changed = await context.session.setMode(staged.cap, mode, staged.observation.identity);
431
+ staged = { ...staged, observation: changed.observation };
432
+ await context.session.flush(staged.cap, "all", staged.observation.identity);
433
+ }
434
+ await beforeCommit();
435
+ try {
436
+ await context.session.rename({
437
+ fromParent: parent.cap,
438
+ from: temp,
439
+ expected: staged.observation.identity,
440
+ toParent: parent.cap,
441
+ to: path.posix.basename(relative),
442
+ replace: "never",
443
+ });
444
+ } catch (error) {
445
+ if (!["possible", "committed"].includes(error?.mutation)) throw error;
446
+ const rebound = await context.session.stat(parent.cap, path.posix.basename(relative));
447
+ if (!sameNativeIdentity(rebound.observation.identity, staged.observation.identity)) throw error;
448
+ }
449
+ await flushAndVerifyParent(context, relative, parent);
450
+ return staged.observation;
451
+ } catch (error) {
452
+ try {
453
+ await settleWorkerTemporary(
454
+ context,
455
+ temporaryRelative,
456
+ workerId,
457
+ staged,
458
+ assertCollectionContext,
459
+ inheritedLegacyMode,
460
+ );
461
+ error.message = `${error.message}; inactive worker temporary was securely quarantined`;
462
+ } catch (cleanupError) {
463
+ error.message = `${error.message}; worker temporary cleanup failed: ${cleanupError.message}`;
464
+ }
465
+ throw error;
466
+ } finally {
467
+ await closeNative(context, staged, parent);
468
+ }
469
+ }
470
+
471
+ export async function publishWorkerSymlink(
472
+ context,
473
+ relative,
474
+ state,
475
+ workerId,
476
+ expectedState,
477
+ assertCollectionContext = null,
478
+ ) {
479
+ await assertNativeExpectedState(context, relative, expectedState);
480
+ if (assertCollectionContext !== null) await assertCollectionContext();
481
+ const beforeCommit = async () => {
482
+ await assertNativeExpectedState(context, relative, expectedState);
483
+ if (assertCollectionContext !== null) await assertCollectionContext();
484
+ };
485
+ const parent = await ensureWorkerParent(context, relative, 0o755, beforeCommit);
486
+ const temp = `.stdd-worker-link-${workerId.slice("worker-".length)}-${sha256(relative).slice(7, 23)}`;
487
+ const temporaryRelative =
488
+ path.posix.dirname(relative) === "." ? temp : `${path.posix.dirname(relative)}/${temp}`;
489
+ let staged;
490
+ try {
491
+ await beforeCommit();
492
+ staged = await context.session.symlink(parent.cap, temp, symlinkTargetString(state, relative));
493
+ await beforeCommit();
494
+ try {
495
+ await context.session.rename({
496
+ fromParent: parent.cap,
497
+ from: temp,
498
+ expected: staged.observation.identity,
499
+ toParent: parent.cap,
500
+ to: path.posix.basename(relative),
501
+ replace: "never",
502
+ });
503
+ } catch (error) {
504
+ if (!["possible", "committed"].includes(error?.mutation)) throw error;
505
+ const rebound = await context.session.stat(parent.cap, path.posix.basename(relative));
506
+ if (!sameNativeIdentity(rebound.observation.identity, staged.observation.identity)) throw error;
507
+ }
508
+ await flushAndVerifyParent(context, relative, parent);
509
+ const linked = await context.session.readLink(
510
+ parent.cap,
511
+ path.posix.basename(relative),
512
+ staged.observation.identity,
513
+ );
514
+ if (!Buffer.from(linked.data, "base64").equals(symlinkTargetBytes(state))) {
515
+ throw new Error(`worker collection could not verify ${workerViewPath(relative)}`);
516
+ }
517
+ return staged.observation;
518
+ } catch (error) {
519
+ try {
520
+ await settleWorkerTemporary(context, temporaryRelative, workerId, staged, assertCollectionContext);
521
+ error.message = `${error.message}; inactive worker temporary was quarantined`;
522
+ } catch (cleanupError) {
523
+ error.message = `${error.message}; worker temporary cleanup failed: ${cleanupError.message}`;
524
+ }
525
+ throw error;
526
+ } finally {
527
+ await closeNative(context, staged, parent);
528
+ }
529
+ }
530
+
531
+ function workerDeletionToken(relative) {
532
+ return sha256(Buffer.from(relative, "utf8")).slice("sha256:".length);
533
+ }
534
+
535
+ export function workerDeletionQuarantinePath(relative, workerId) {
536
+ return `${WORKER_DELETIONS_REL}/${workerId}/${workerDeletionToken(relative)}`;
537
+ }
538
+
539
+ function workerDeletionInventory(workerId, relative, expectedState, expectedIdentity) {
540
+ return Buffer.from(
541
+ `${JSON.stringify(
542
+ {
543
+ schema: 1,
544
+ workerId,
545
+ path: relative,
546
+ state: expectedState,
547
+ expectedIdentity,
548
+ },
549
+ null,
550
+ 2,
551
+ )}\n`,
552
+ );
553
+ }
554
+
555
+ function exactKeys(value, keys) {
556
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
557
+ const actual = Object.keys(value).sort();
558
+ const expected = [...keys].sort();
559
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
560
+ }
561
+
562
+ function validWorkerDeletionState(state) {
563
+ if (state?.type === "file") {
564
+ return (
565
+ exactKeys(state, ["type", "hash", "mode"]) &&
566
+ /^sha256:[0-9a-f]{64}$/.test(state.hash) &&
567
+ Number.isInteger(state.mode) &&
568
+ state.mode >= 0 &&
569
+ state.mode <= 0o777
570
+ );
571
+ }
572
+ return (
573
+ state?.type === "symlink" &&
574
+ exactKeys(state, ["type", "target", "targetBase64", "hash"]) &&
575
+ typeof state.target === "string" &&
576
+ typeof state.targetBase64 === "string" &&
577
+ Buffer.from(state.targetBase64, "base64").toString("base64") === state.targetBase64 &&
578
+ /^sha256:[0-9a-f]{64}$/.test(state.hash)
579
+ );
580
+ }
581
+
582
+ function parseWorkerDeletionInventory(bytes, workerId, relative = null, token = null) {
583
+ let parsed;
584
+ try {
585
+ parsed = JSON.parse(bytes.toString("utf8"));
586
+ } catch (error) {
587
+ throw new Error(`worker deletion quarantine inventory is malformed: ${error.message}`);
588
+ }
589
+ if (
590
+ !exactKeys(parsed, ["schema", "workerId", "path", "state", "expectedIdentity"]) ||
591
+ parsed.schema !== 1 ||
592
+ parsed.workerId !== workerId ||
593
+ typeof parsed.path !== "string" ||
594
+ (relative !== null && parsed.path !== relative) ||
595
+ (token !== null && workerDeletionToken(parsed.path) !== token) ||
596
+ !validWorkerDeletionState(parsed.state) ||
597
+ !exactKeys(parsed.expectedIdentity, ["version", "platform", "volume", "fileId", "kind"]) ||
598
+ parsed.expectedIdentity.version !== 2 ||
599
+ !["linux", "darwin", "win32"].includes(parsed.expectedIdentity.platform) ||
600
+ typeof parsed.expectedIdentity.volume !== "string" ||
601
+ typeof parsed.expectedIdentity.fileId !== "string" ||
602
+ parsed.expectedIdentity.kind !== parsed.state.type
603
+ ) {
604
+ throw new Error("worker deletion quarantine inventory does not match its exact provenance schema");
605
+ }
606
+ return parsed;
607
+ }
608
+
609
+ async function assertPrivateWorkerObject(context, held, label, kind) {
610
+ if (
611
+ held.observation.identity.kind !== kind ||
612
+ (held.observation.identity.platform !== "win32" &&
613
+ held.observation.owner !== context.root.observation.owner) ||
614
+ (kind === "file" && held.observation.linkCount !== "1")
615
+ ) {
616
+ throw new Error(`${label} is not an exact owner-private ${kind}`);
617
+ }
618
+ try {
619
+ await context.session.verifyPrivate(held.cap);
620
+ } catch (error) {
621
+ throw new Error(`${label} is not owner-private: ${error.message}`, { cause: error });
622
+ }
623
+ }
624
+
625
+ async function openPrivateWorkerPath(
626
+ context,
627
+ targetRelative,
628
+ { create = false, beforeCommit = null } = {},
629
+ ) {
630
+ const segments = targetRelative.split("/");
631
+ let current = context.root;
632
+ const opened = [];
633
+ try {
634
+ for (const [index, segment] of segments.entries()) {
635
+ try {
636
+ current = await context.session.openChild(current.cap, segment);
637
+ } catch (error) {
638
+ if (!create || error?.code !== "not-found") throw error;
639
+ if (beforeCommit !== null) await beforeCommit();
640
+ current = await context.session.createDirectory(current.cap, segment, 0o700);
641
+ }
642
+ opened.push(current);
643
+ if (index > 0) {
644
+ await assertPrivateWorkerObject(
645
+ context,
646
+ current,
647
+ `worker deletion quarantine ancestor ${segments.slice(0, index + 1).join("/")}`,
648
+ "directory",
649
+ );
650
+ }
651
+ }
652
+ for (const held of opened.slice(0, -1)) await closeNative(context, held);
653
+ return current;
654
+ } catch (error) {
655
+ await closeNative(context, opened);
656
+ throw error;
657
+ }
658
+ }
659
+
660
+ export async function preflightPrivateWorkerQuarantine(context, relative, workerId) {
661
+ const segments = workerDeletionQuarantinePath(relative, workerId).split("/");
662
+ let current = context.root;
663
+ const opened = [];
664
+ try {
665
+ for (const [index, segment] of segments.entries()) {
666
+ try {
667
+ current = await context.session.openChild(current.cap, segment);
668
+ } catch (error) {
669
+ if (error?.code === "not-found") return;
670
+ throw error;
671
+ }
672
+ opened.push(current);
673
+ if (index > 0) {
674
+ await assertPrivateWorkerObject(
675
+ context,
676
+ current,
677
+ `worker deletion quarantine ancestor ${segments.slice(0, index + 1).join("/")}`,
678
+ "directory",
679
+ );
680
+ }
681
+ }
682
+ } finally {
683
+ await closeNative(context, opened);
684
+ }
685
+ }
686
+
687
+ async function openPrivateWorkerQuarantine(context, relative, workerId, options = {}) {
688
+ const containerRelative = workerDeletionQuarantinePath(relative, workerId);
689
+ return {
690
+ container: await openPrivateWorkerPath(context, containerRelative, options),
691
+ containerRelative,
692
+ };
693
+ }
694
+
695
+ export async function readWorkerDeletionQuarantineState(context, relative, workerId) {
696
+ let container;
697
+ let inventory;
698
+ try {
699
+ try {
700
+ ({ container } = await openPrivateWorkerQuarantine(context, relative, workerId));
701
+ } catch (error) {
702
+ if (error?.code === "not-found") return null;
703
+ throw error;
704
+ }
705
+ try {
706
+ inventory = await context.session.openChild(container.cap, "inventory.json");
707
+ } catch (error) {
708
+ if (error?.code === "not-found") return null;
709
+ throw error;
710
+ }
711
+ await assertPrivateWorkerObject(context, inventory, "worker deletion inventory", "file");
712
+ const parsed = parseWorkerDeletionInventory(
713
+ await readNativeFile(context, inventory, 64 * 1024),
714
+ workerId,
715
+ relative,
716
+ );
717
+ const payload = await readNativeWorkerPath(
718
+ {
719
+ ...context,
720
+ root: container,
721
+ rootPath: path.join(context.rootPath, workerDeletionQuarantinePath(relative, workerId)),
722
+ },
723
+ "payload",
724
+ { modeHint: parsed.state?.mode ?? null, legacyMode: parsed.state?.mode ?? null },
725
+ );
726
+ // Inventory is deliberately durable before the identity-conditioned
727
+ // move. A retry resumes that exact move when the payload is not there yet.
728
+ if (payload.state === null) return null;
729
+ if (
730
+ !sameWorkerState(payload.state, parsed.state) ||
731
+ !sameNativeIdentity(payload.observation.identity, parsed.expectedIdentity)
732
+ ) {
733
+ throw new Error("worker deletion quarantine payload does not match its inventory");
734
+ }
735
+ return payload.state;
736
+ } finally {
737
+ await closeNative(context, inventory, container);
738
+ }
739
+ }
740
+
741
+ export async function quarantineWorkerDeletion(
742
+ context,
743
+ relative,
744
+ workerId,
745
+ expectedState,
746
+ expectedObservation,
747
+ assertCollectionContext = null,
748
+ quarantineRelative = relative,
749
+ legacyMode = null,
750
+ ) {
751
+ const existing = await readWorkerDeletionQuarantineState(context, quarantineRelative, workerId);
752
+ if (sameWorkerState(existing, expectedState)) return;
753
+ const beforeCommit = async () => {
754
+ const live = await assertNativeExpectedState(context, relative, expectedState, legacyMode);
755
+ if (!sameNativeIdentity(live.observation?.identity, expectedObservation?.identity)) {
756
+ throw new Error(`worker deletion source ${workerViewPath(relative)} changed after preflight`);
757
+ }
758
+ if (assertCollectionContext !== null) await assertCollectionContext();
759
+ };
760
+ await beforeCommit();
761
+ const sourceParent = await openNativeParent(context, relative, "worker deletion source parent");
762
+ const { container, containerRelative } = await openPrivateWorkerQuarantine(
763
+ context,
764
+ quarantineRelative,
765
+ workerId,
766
+ { create: true, beforeCommit },
767
+ );
768
+ let inventory;
769
+ try {
770
+ await beforeCommit();
771
+ const inventoryBytes = workerDeletionInventory(
772
+ workerId,
773
+ quarantineRelative,
774
+ expectedState,
775
+ expectedObservation.identity,
776
+ );
777
+ try {
778
+ inventory = await context.session.openChild(container.cap, "inventory.json");
779
+ const existingBytes = await readNativeFile(context, inventory, 64 * 1024);
780
+ if (!existingBytes.equals(inventoryBytes)) {
781
+ throw new Error("worker deletion quarantine inventory conflicts with this collection");
782
+ }
783
+ } catch (error) {
784
+ if (error?.code !== "not-found") throw error;
785
+ await beforeCommit();
786
+ inventory = await context.session.createFile(container.cap, "inventory.json", 0o600);
787
+ await writeNativeFileContent(context, inventory, inventoryBytes);
788
+ }
789
+ await assertPrivateWorkerObject(context, inventory, "worker deletion inventory", "file");
790
+ await context.session.flush(container.cap, "namespace", container.observation.identity);
791
+ await beforeCommit();
792
+ await assertPrivateWorkerObject(
793
+ context,
794
+ container,
795
+ `worker deletion quarantine ${containerRelative}`,
796
+ "directory",
797
+ );
798
+ try {
799
+ await context.session.rename({
800
+ fromParent: sourceParent.cap,
801
+ from: path.posix.basename(relative),
802
+ expected: expectedObservation.identity,
803
+ toParent: container.cap,
804
+ to: "payload",
805
+ replace: "never",
806
+ });
807
+ } catch (error) {
808
+ if (!["possible", "committed"].includes(error?.mutation)) throw error;
809
+ const retained = await readNativeWorkerPath(
810
+ {
811
+ ...context,
812
+ root: container,
813
+ rootPath: path.join(context.rootPath, containerRelative),
814
+ },
815
+ "payload",
816
+ { modeHint: expectedState?.mode ?? null },
817
+ );
818
+ if (
819
+ !sameWorkerState(retained.state, expectedState) ||
820
+ !sameNativeIdentity(retained.observation?.identity, expectedObservation.identity)
821
+ ) {
822
+ throw error;
823
+ }
824
+ }
825
+ for (const directory of [sourceParent, container]) {
826
+ await context.session.flush(directory.cap, "namespace", directory.observation.identity);
827
+ }
828
+ await assertPrivateWorkerObject(
829
+ context,
830
+ container,
831
+ `worker deletion quarantine ${containerRelative}`,
832
+ "directory",
833
+ );
834
+ await verifyNativeRepoDirectory(
835
+ context,
836
+ path.posix.dirname(relative),
837
+ sourceParent.observation.identity,
838
+ `worker deletion source parent for ${workerViewPath(relative)}`,
839
+ );
840
+ await verifyNativeRepoDirectory(
841
+ context,
842
+ containerRelative,
843
+ container.observation.identity,
844
+ `worker deletion quarantine for ${workerViewPath(relative)}`,
845
+ );
846
+ } finally {
847
+ await closeNative(context, inventory, container, sourceParent);
848
+ }
849
+ }
850
+
851
+ export async function workerQuarantineInventory(context, workerIds) {
852
+ const inventory = [];
853
+ for (const workerId of [...new Set(workerIds)].sort()) {
854
+ let root;
855
+ try {
856
+ root = await openPrivateWorkerPath(context, `${WORKER_DELETIONS_REL}/${workerId}`);
857
+ } catch (error) {
858
+ if (error?.code === "not-found" || error?.cause?.code === "not-found") continue;
859
+ throw error;
860
+ }
861
+ try {
862
+ let cursor = null;
863
+ do {
864
+ const page = await context.session.list(root.cap, { cursor, limit: 256 });
865
+ cursor = page.cursor;
866
+ for (const entry of page.entries) {
867
+ if (!/^[0-9a-f]{64}$/.test(entry.name)) continue;
868
+ let container;
869
+ let inventoryFile;
870
+ try {
871
+ container = await context.session.openChild(root.cap, entry.name);
872
+ await assertPrivateWorkerObject(
873
+ context,
874
+ container,
875
+ `retained worker quarantine ${workerId}/${entry.name}`,
876
+ "directory",
877
+ );
878
+ inventoryFile = await context.session.openChild(container.cap, "inventory.json");
879
+ await assertPrivateWorkerObject(
880
+ context,
881
+ inventoryFile,
882
+ `retained worker quarantine ${workerId}/${entry.name}/inventory.json`,
883
+ "file",
884
+ );
885
+ const parsed = parseWorkerDeletionInventory(
886
+ await readNativeFile(context, inventoryFile, 64 * 1024),
887
+ workerId,
888
+ null,
889
+ entry.name,
890
+ );
891
+ const nested = {
892
+ ...context,
893
+ root: container,
894
+ rootPath: path.join(context.rootPath, WORKER_DELETIONS_REL, workerId, entry.name),
895
+ };
896
+ const payload = await readNativeWorkerPath(nested, "payload", {
897
+ modeHint: parsed.state?.mode ?? null,
898
+ legacyMode: parsed.state?.mode ?? null,
899
+ });
900
+ if (
901
+ payload.state !== null &&
902
+ sameWorkerState(payload.state, parsed.state) &&
903
+ sameNativeIdentity(payload.observation.identity, parsed.expectedIdentity)
904
+ ) {
905
+ inventory.push({
906
+ relative: `${WORKER_DELETIONS_REL}/${workerId}/${entry.name}`,
907
+ provenance: `worker ${workerId}, deleted ${workerViewPath(parsed.path)}`,
908
+ });
909
+ }
910
+ } catch (error) {
911
+ if (error?.code !== "not-found") throw error;
912
+ // An interrupted location without inventory or payload is not yet proven inventory.
913
+ } finally {
914
+ await closeNative(context, inventoryFile, container);
915
+ }
916
+ }
917
+ } while (cursor !== null);
918
+ } finally {
919
+ await closeNative(context, root);
920
+ }
921
+ }
922
+ return inventory.sort((left, right) => left.relative.localeCompare(right.relative));
923
+ }
924
+
925
+ // Read-only managed-sandbox commands intentionally do not require the helper.
926
+ // This compatibility reader performs no mutation; create and collect use the
927
+ // capability-relative functions above for every transaction observation.
928
+ export function readWorkerPathState(root, relative, { bytes = false } = {}) {
929
+ const absolute = path.join(root, ...relative.split("/"));
930
+ let observed;
931
+ try {
932
+ observed = fs.lstatSync(absolute, { bigint: true });
933
+ } catch (error) {
934
+ if (error.code === "ENOENT") return { state: null, bytes: null };
935
+ throw error;
936
+ }
937
+ if (observed.isSymbolicLink()) {
938
+ const targetBytes = fs.readlinkSync(absolute, { encoding: "buffer" });
939
+ const after = fs.lstatSync(absolute, { bigint: true });
940
+ if (!after.isSymbolicLink() || observed.dev !== after.dev || observed.ino !== after.ino) {
941
+ throw new Error(`worker path ${workerViewPath(relative)} changed while reading its symlink`);
942
+ }
943
+ return {
944
+ state: {
945
+ type: "symlink",
946
+ target: targetBytes.toString("utf8"),
947
+ targetBase64: targetBytes.toString("base64"),
948
+ hash: sha256(Buffer.concat([Buffer.from("link:"), targetBytes])),
949
+ },
950
+ bytes: null,
951
+ };
952
+ }
953
+ if (!observed.isFile() || observed.nlink !== 1n) {
954
+ throw new Error(
955
+ `worker path ${workerViewPath(relative)} must be a single-linked regular file or symlink`,
956
+ );
957
+ }
958
+ const mode = Number(observed.mode & 0o777n);
959
+ const content = fs.readFileSync(absolute);
960
+ const after = fs.lstatSync(absolute, { bigint: true });
961
+ if (
962
+ observed.dev !== after.dev ||
963
+ observed.ino !== after.ino ||
964
+ observed.size !== after.size ||
965
+ observed.mtimeNs !== after.mtimeNs ||
966
+ observed.ctimeNs !== after.ctimeNs
967
+ ) {
968
+ throw new Error(`worker path ${workerViewPath(relative)} changed while reading`);
969
+ }
970
+ return { state: { type: "file", hash: sha256(content), mode }, bytes: bytes ? content : null };
971
+ }