@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,825 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { openNativeRepoMutation, readNativeFile, writeNativeFileContent } from "./held-fs.mjs";
6
+ import { MAX_SUBPROCESS_BUFFER } from "./runtime.mjs";
7
+ import { isReviewInodeIdentity, sameReviewPrivateState } from "./state-validation.mjs";
8
+
9
+ const REVIEW_REQUEST_ID_PATTERN = /^rev-(?:[0-9a-f]{8}|[0-9a-f]{32})$/u;
10
+ const REVIEW_DIRECTORY_PATTERN = /^stdd-review-(?:[0-9A-Za-z]{6}|[0-9a-f]{8}|[0-9a-f]{32})$/u;
11
+ const REVIEW_PRIVATE_COMPANIONS = new Set(["last-message.txt"]);
12
+ const NATIVE_CHUNK_BYTES = 64 * 1024;
13
+ const REVIEW_INVENTORY_NAME = "inventory.json";
14
+ const REVIEW_RETAINED_NAME = "private";
15
+
16
+ function isReviewOwnedArtifact(request, name) {
17
+ if (name === `${request.id}.md` || REVIEW_PRIVATE_COMPANIONS.has(name)) return true;
18
+ return new RegExp(`^\\.${request.id}-(?:[0-9a-f]{32}|artifact-[0-9a-f]{32})\\.retired$`).test(name);
19
+ }
20
+
21
+ function stableObservation(observation) {
22
+ const { identity, owner, permissions, linkCount } = observation;
23
+ return { identity, owner, permissions, linkCount };
24
+ }
25
+
26
+ function sameNativeIdentity(left, right) {
27
+ return (
28
+ left?.version === right?.version &&
29
+ left?.platform === right?.platform &&
30
+ left?.volume === right?.volume &&
31
+ left?.fileId === right?.fileId &&
32
+ left?.kind === right?.kind
33
+ );
34
+ }
35
+
36
+ function recordedObservationMatches(current, recorded, kind, { ignoreLinkCount = false } = {}) {
37
+ if (recorded?.identity?.version === 2) {
38
+ return (
39
+ sameNativeIdentity(current.identity, recorded.identity) &&
40
+ current.identity.kind === kind &&
41
+ current.owner === recorded.owner &&
42
+ current.permissions === recorded.permissions &&
43
+ (ignoreLinkCount || current.linkCount === recorded.linkCount)
44
+ );
45
+ }
46
+ if (!isReviewInodeIdentity(recorded) || current.identity.platform !== "linux") return false;
47
+ return (
48
+ current.identity.volume === recorded.dev &&
49
+ current.identity.fileId === recorded.ino &&
50
+ current.identity.kind === kind &&
51
+ current.owner === recorded.uid &&
52
+ current.permissions === recorded.mode &&
53
+ (ignoreLinkCount || current.linkCount === recorded.nlink)
54
+ );
55
+ }
56
+
57
+ function privateObservation(observation, mode) {
58
+ return (
59
+ (observation.identity.kind === "directory" || observation.linkCount === "1") &&
60
+ (observation.identity.platform === "win32"
61
+ ? /^O:([^:]+)D:P\(A;;FA;;;\1\)\(A;;FA;;;SY\)\(A;;FA;;;BA\)$/.test(observation.permissions)
62
+ : (Number(observation.permissions) & 0o777) === mode)
63
+ );
64
+ }
65
+
66
+ function reviewOwner(request) {
67
+ return request.privateState.version === 2
68
+ ? request.privateState.directory.owner
69
+ : request.privateState.directory.uid;
70
+ }
71
+
72
+ function recordedOwner(recorded) {
73
+ return recorded?.identity?.version === 2 ? recorded.owner : recorded?.uid;
74
+ }
75
+
76
+ function pathApiFor(platform) {
77
+ return platform === "win32" ? path.win32 : path.posix;
78
+ }
79
+
80
+ function samePlatformPath(left, right, platform) {
81
+ return platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
82
+ }
83
+
84
+ function canonicalTempRootPath(value, platform) {
85
+ if (typeof value !== "string") return null;
86
+ const pathApi = pathApiFor(platform);
87
+ if (!pathApi.isAbsolute(value)) return null;
88
+ let normalized = pathApi.normalize(value);
89
+ if (platform === "win32") {
90
+ normalized = normalized.replace(/^([a-z]):/u, (_, drive) => `${drive.toUpperCase()}:`);
91
+ }
92
+ return normalized;
93
+ }
94
+
95
+ function parseReviewLocation(request) {
96
+ if (
97
+ typeof request?.briefPath !== "string" ||
98
+ typeof request?.id !== "string" ||
99
+ !REVIEW_REQUEST_ID_PATTERN.test(request.id)
100
+ ) {
101
+ return null;
102
+ }
103
+ const platform =
104
+ request.privateState?.version === 2
105
+ ? request.privateState.tempRoot?.identity?.platform
106
+ : contextPlatformPathKind();
107
+ const pathApi = pathApiFor(platform);
108
+ const tempRoot =
109
+ request.privateState?.version === 2
110
+ ? canonicalTempRootPath(request.privateState.tempRootPath, platform)
111
+ : path.resolve(os.tmpdir());
112
+ if (
113
+ tempRoot === null ||
114
+ (request.privateState?.version === 2 && tempRoot !== request.privateState.tempRootPath)
115
+ ) {
116
+ return null;
117
+ }
118
+ const briefPath = pathApi.resolve(request.briefPath);
119
+ const directoryPath = pathApi.dirname(briefPath);
120
+ const directoryName = pathApi.basename(directoryPath);
121
+ if (
122
+ !samePlatformPath(pathApi.dirname(directoryPath), tempRoot, platform) ||
123
+ !REVIEW_DIRECTORY_PATTERN.test(directoryName) ||
124
+ pathApi.basename(briefPath) !== `${request.id}.md`
125
+ ) {
126
+ return null;
127
+ }
128
+ return {
129
+ tempRoot,
130
+ briefPath,
131
+ directoryPath,
132
+ directoryName,
133
+ quarantineName: `stdd-review-quarantine-${request.id.slice(4)}`,
134
+ };
135
+ }
136
+
137
+ function contextPlatformPathKind() {
138
+ return path.sep === "\\" ? "win32" : "linux";
139
+ }
140
+
141
+ async function listNativeDirectory(context, directory) {
142
+ const entries = [];
143
+ let cursor = null;
144
+ do {
145
+ const page = await context.session.list(directory.cap, { cursor, limit: 256 });
146
+ entries.push(...page.entries);
147
+ cursor = page.cursor;
148
+ } while (cursor !== null);
149
+ return entries;
150
+ }
151
+
152
+ async function closeCapabilities(context, capabilities) {
153
+ for (const cap of [...capabilities].reverse()) {
154
+ await context.session.closeCapability(cap).catch(() => {});
155
+ }
156
+ }
157
+
158
+ function register(capabilities, value) {
159
+ capabilities.add(value.cap);
160
+ return value;
161
+ }
162
+
163
+ function hashBytes(bytes) {
164
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
165
+ }
166
+
167
+ function exactPrivateState(request) {
168
+ return sameReviewPrivateState(request?.privateState, request?.privateState);
169
+ }
170
+
171
+ function inventoryBytes(request, location) {
172
+ return Buffer.from(
173
+ `${JSON.stringify({
174
+ schema: 1,
175
+ kind: "private-review",
176
+ request: request.id,
177
+ brief: request.brief ?? request.briefHash ?? null,
178
+ original: location.directoryName,
179
+ retained: `${location.quarantineName}/${REVIEW_RETAINED_NAME}`,
180
+ directory: request.privateState.directory,
181
+ })}\n`,
182
+ );
183
+ }
184
+
185
+ /** Exact ledger-derived candidate for read-only doctor inventory; never scans OS temp. */
186
+ export function reviewRetainedInventoryExpectation(request) {
187
+ if (!exactPrivateState(request)) return null;
188
+ const location = parseReviewLocation(request);
189
+ if (!location) return null;
190
+ return {
191
+ request,
192
+ location,
193
+ owner: reviewOwner(request),
194
+ inventory: inventoryBytes(request, location),
195
+ quarantinePath: pathApiFor(request.privateState.tempRoot.identity?.platform ?? "linux").join(
196
+ location.tempRoot,
197
+ location.quarantineName,
198
+ ),
199
+ };
200
+ }
201
+
202
+ async function inspectRetainedSettlement(context, request, location, capabilities) {
203
+ let quarantine;
204
+ try {
205
+ quarantine = register(
206
+ capabilities,
207
+ await context.session.openChild(context.root.cap, location.quarantineName),
208
+ );
209
+ } catch (error) {
210
+ if (error?.code === "not-found") return null;
211
+ throw error;
212
+ }
213
+ if (
214
+ quarantine.observation.identity.kind !== "directory" ||
215
+ !privateObservation(quarantine.observation, 0o700) ||
216
+ quarantine.observation.owner !== reviewOwner(request)
217
+ ) {
218
+ return false;
219
+ }
220
+ const entries = await listNativeDirectory(context, quarantine);
221
+ const names = entries.map((entry) => entry.name).sort();
222
+ if (names.length !== 2 || names[0] !== REVIEW_INVENTORY_NAME || names[1] !== REVIEW_RETAINED_NAME) {
223
+ return false;
224
+ }
225
+ const inventory = register(
226
+ capabilities,
227
+ await context.session.openChild(quarantine.cap, REVIEW_INVENTORY_NAME),
228
+ );
229
+ const retained = register(
230
+ capabilities,
231
+ await context.session.openChild(quarantine.cap, REVIEW_RETAINED_NAME),
232
+ );
233
+ if (
234
+ !privateObservation(inventory.observation, 0o600) ||
235
+ inventory.observation.owner !== reviewOwner(request) ||
236
+ !(await readNativeFile(context, inventory, 64 * 1024)).equals(inventoryBytes(request, location)) ||
237
+ !recordedObservationMatches(retained.observation, request.privateState.directory, "directory")
238
+ ) {
239
+ return false;
240
+ }
241
+ const retainedEntries = await listNativeDirectory(context, retained);
242
+ const retainedNames = retainedEntries.map((entry) => entry.name).sort();
243
+ const capturedNames = Object.keys(request.privateState.artifacts).sort();
244
+ if (
245
+ retainedNames.length !== capturedNames.length ||
246
+ retainedNames.some((name, index) => name !== capturedNames[index]) ||
247
+ retainedNames.some((name) => !isReviewOwnedArtifact(request, name))
248
+ ) {
249
+ return false;
250
+ }
251
+ for (const name of retainedNames) {
252
+ const artifact = register(capabilities, await context.session.openChild(retained.cap, name));
253
+ if (
254
+ recordedOwner(request.privateState.artifacts[name]) !== reviewOwner(request) ||
255
+ artifact.observation.owner !== reviewOwner(request) ||
256
+ !recordedObservationMatches(artifact.observation, request.privateState.artifacts[name], "file") ||
257
+ !privateObservation(artifact.observation, 0o600) ||
258
+ artifact.observation.size !== "0"
259
+ ) {
260
+ return false;
261
+ }
262
+ }
263
+ return { quarantine, retained };
264
+ }
265
+
266
+ /** Inspect one exact ledger-proven retained review location through one native helper session. */
267
+ export async function inspectReviewRetainedInventory(request, { context = null, strict = false } = {}) {
268
+ const expected = reviewRetainedInventoryExpectation(request);
269
+ const recordedPlatform =
270
+ request?.privateState?.version === 2 ? request.privateState.tempRoot.identity?.platform : "linux";
271
+ if (!expected) return null;
272
+ const ownsContext = context === null;
273
+ let transaction = context;
274
+ const capabilities = new Set();
275
+ try {
276
+ transaction ??= await openReviewFsTransaction(
277
+ "retained review inventory native filesystem helper",
278
+ request,
279
+ );
280
+ if (
281
+ transaction.root.observation.identity.platform !== recordedPlatform ||
282
+ !recordedObservationMatches(
283
+ transaction.root.observation,
284
+ request.privateState.tempRoot,
285
+ "directory",
286
+ {
287
+ ignoreLinkCount: true,
288
+ },
289
+ )
290
+ ) {
291
+ return null;
292
+ }
293
+ const retained = await inspectRetainedSettlement(
294
+ transaction,
295
+ request,
296
+ expected.location,
297
+ capabilities,
298
+ );
299
+ return retained
300
+ ? { path: expected.quarantinePath, provenance: `review request ${request.id}` }
301
+ : null;
302
+ } catch (error) {
303
+ if (strict) throw error;
304
+ return null;
305
+ } finally {
306
+ if (transaction) {
307
+ await closeCapabilities(transaction, capabilities);
308
+ if (ownsContext) await transaction.close();
309
+ }
310
+ }
311
+ }
312
+
313
+ export async function openReviewFsTransaction(
314
+ label = "review native filesystem helper",
315
+ request = null,
316
+ ) {
317
+ // macOS commonly exposes /var as a symlink to /private/var; open-root
318
+ // intentionally rejects symlink traversal, so bind new and legacy-current
319
+ // transactions to the physical temp root.
320
+ let tempRoot;
321
+ if (
322
+ request?.privateState?.version === 2 &&
323
+ typeof request.privateState.tempRootPath === "string" &&
324
+ path.isAbsolute(request.privateState.tempRootPath)
325
+ ) {
326
+ tempRoot = request.privateState.tempRootPath;
327
+ } else if (
328
+ request?.privateState?.version !== 2 &&
329
+ typeof request?.briefPath === "string" &&
330
+ path.isAbsolute(request.briefPath)
331
+ ) {
332
+ tempRoot = path.dirname(path.dirname(request.briefPath));
333
+ } else {
334
+ tempRoot = fs.realpathSync.native(os.tmpdir());
335
+ }
336
+ return openNativeRepoMutation(tempRoot, label);
337
+ }
338
+
339
+ async function wipeReviewArtifact(context, file) {
340
+ const current = await context.session.stat(file.cap);
341
+ const expected = current.observation.identity;
342
+ if (!privateObservation(current.observation, 0o600)) {
343
+ throw new Error("partial private review artifact is not owner-private");
344
+ }
345
+ let offset = 0;
346
+ const size = Number(current.observation.size);
347
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_SUBPROCESS_BUFFER) {
348
+ throw new Error("partial private review artifact has an unsafe size");
349
+ }
350
+ const zeroes = Buffer.alloc(NATIVE_CHUNK_BYTES);
351
+ while (offset < size) {
352
+ const length = Math.min(zeroes.length, size - offset);
353
+ const result = await context.session.write(file.cap, offset, zeroes.subarray(0, length), expected);
354
+ if (result.written < 1) throw new Error("could not overwrite a private review artifact");
355
+ offset += result.written;
356
+ }
357
+ await context.session.flush(file.cap, "all", expected);
358
+ await context.session.truncate(file.cap, 0, expected);
359
+ await context.session.flush(file.cap, "all", expected);
360
+ const wiped = await context.session.stat(file.cap);
361
+ if (
362
+ !recordedObservationMatches(wiped.observation, stableObservation(current.observation), "file") ||
363
+ !privateObservation(wiped.observation, 0o600) ||
364
+ wiped.observation.size !== "0"
365
+ ) {
366
+ throw new Error("partial private review artifact was not wiped");
367
+ }
368
+ }
369
+
370
+ async function wipePartialReviewCreation(context, directory, files) {
371
+ if (!directory) return false;
372
+ const failures = [];
373
+ if (!privateObservation(directory.observation, 0o700)) {
374
+ failures.push(new Error("partial private review directory is not owner-private"));
375
+ }
376
+ for (const file of files) {
377
+ try {
378
+ if (file.observation.owner !== directory.observation.owner) {
379
+ throw new Error("partial private review artifact owner does not match its directory owner");
380
+ }
381
+ await wipeReviewArtifact(context, file);
382
+ } catch (error) {
383
+ failures.push(error);
384
+ }
385
+ }
386
+ try {
387
+ await context.session.flush(directory.cap, "namespace", directory.observation.identity);
388
+ } catch (error) {
389
+ failures.push(error);
390
+ }
391
+ if (failures.length > 0) {
392
+ throw new AggregateError(
393
+ failures,
394
+ failures.map((failure) => failure?.message ?? String(failure)).join("; "),
395
+ );
396
+ }
397
+ return true;
398
+ }
399
+
400
+ /** Create all source-bearing review artifacts in one preflighted helper session. */
401
+ export async function createReviewPrivateArtifacts(
402
+ id,
403
+ brief,
404
+ { lastMessage = false, context: suppliedContext = null } = {},
405
+ ) {
406
+ if (!REVIEW_REQUEST_ID_PATTERN.test(id)) throw new Error("invalid review request id");
407
+ const context =
408
+ suppliedContext ??
409
+ (await openReviewFsTransaction("private review creation native filesystem helper"));
410
+ const ownsContext = suppliedContext === null;
411
+ const capabilities = new Set();
412
+ const directoryName = `stdd-review-${id.slice(4)}`;
413
+ let directory = null;
414
+ const createdFiles = [];
415
+ try {
416
+ directory = register(
417
+ capabilities,
418
+ await context.session.createDirectory(context.root.cap, directoryName, 0o700),
419
+ );
420
+ if (!privateObservation(directory.observation, 0o700)) {
421
+ throw new Error(
422
+ `private review directory is not owner-private (${JSON.stringify(directory.observation)})`,
423
+ );
424
+ }
425
+ const artifacts = Object.create(null);
426
+ for (const [name, content] of [
427
+ [`${id}.md`, brief],
428
+ ...(lastMessage ? [["last-message.txt", ""]] : []),
429
+ ]) {
430
+ const file = register(capabilities, await context.session.createFile(directory.cap, name, 0o600));
431
+ createdFiles.push(file);
432
+ await writeNativeFileContent(context, file, content);
433
+ const current = await context.session.stat(file.cap);
434
+ if (
435
+ !sameNativeIdentity(current.observation.identity, file.observation.identity) ||
436
+ !privateObservation(current.observation, 0o600) ||
437
+ current.observation.owner !== directory.observation.owner
438
+ ) {
439
+ throw new Error(`private review artifact ${name} changed during creation`);
440
+ }
441
+ artifacts[name] = stableObservation(current.observation);
442
+ }
443
+ await context.session.flush(directory.cap, "namespace", directory.observation.identity);
444
+ const reboundRoot = register(capabilities, await context.session.openRoot(context.rootPath));
445
+ if (!sameNativeIdentity(reboundRoot.observation.identity, context.root.observation.identity)) {
446
+ throw new Error("OS temp root changed during private review creation");
447
+ }
448
+ const reboundDirectory = register(
449
+ capabilities,
450
+ await context.session.openChild(reboundRoot.cap, directoryName),
451
+ );
452
+ if (!sameNativeIdentity(reboundDirectory.observation.identity, directory.observation.identity)) {
453
+ throw new Error("private review directory changed during creation");
454
+ }
455
+ return {
456
+ briefPath: path.join(context.rootPath, directoryName, `${id}.md`),
457
+ outPath: path.join(context.rootPath, directoryName, "last-message.txt"),
458
+ privateState: {
459
+ version: 2,
460
+ tempRootPath: canonicalTempRootPath(
461
+ context.rootPath,
462
+ context.root.observation.identity.platform,
463
+ ),
464
+ tempRoot: stableObservation(context.root.observation),
465
+ directory: stableObservation(reboundDirectory.observation),
466
+ artifacts,
467
+ },
468
+ };
469
+ } catch (error) {
470
+ let wiped = false;
471
+ try {
472
+ wiped = await wipePartialReviewCreation(context, directory, createdFiles);
473
+ } catch (wipeError) {
474
+ error.message = `${error.message}; partial private review wipe failed: ${wipeError.message}`;
475
+ }
476
+ error.message = `${error.message}; partial private review state ${
477
+ wiped ? "was wiped and retained" : "may be retained"
478
+ } at ${path.join(context.rootPath, directoryName)} for manual remediation`;
479
+ throw error;
480
+ } finally {
481
+ await closeCapabilities(context, capabilities);
482
+ if (ownsContext) await context.close();
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Bind the complete source namespace before the caller commits its terminal
488
+ * ledger event. The same capabilities remain live for the subsequent wipe
489
+ * and retained move.
490
+ */
491
+ export async function prepareReviewBriefSettlement(context, request, { expectedHash = null } = {}) {
492
+ const location = parseReviewLocation(request);
493
+ const capabilities = new Set();
494
+ try {
495
+ if (!location || !exactPrivateState(request)) return { state: "unsafe", context, capabilities };
496
+ if (
497
+ !recordedObservationMatches(context.root.observation, request.privateState.tempRoot, "directory", {
498
+ ignoreLinkCount: true,
499
+ })
500
+ ) {
501
+ return { state: "unsafe", context, capabilities };
502
+ }
503
+ let directory;
504
+ try {
505
+ directory = register(
506
+ capabilities,
507
+ await context.session.openChild(context.root.cap, location.directoryName),
508
+ );
509
+ } catch (error) {
510
+ if (error?.code !== "not-found") throw error;
511
+ const retained = await inspectRetainedSettlement(context, request, location, capabilities);
512
+ return { state: retained ? "retained" : "unsafe", context, capabilities, location, retained };
513
+ }
514
+ if (
515
+ !recordedObservationMatches(directory.observation, request.privateState.directory, "directory") ||
516
+ !privateObservation(directory.observation, 0o700)
517
+ ) {
518
+ return { state: "unsafe", context, capabilities };
519
+ }
520
+ const entries = await listNativeDirectory(context, directory);
521
+ const names = entries.map((entry) => entry.name).sort();
522
+ const capturedNames = Object.keys(request.privateState.artifacts).sort();
523
+ if (
524
+ names.length !== capturedNames.length ||
525
+ names.some((name, index) => name !== capturedNames[index]) ||
526
+ names.some((name) => !isReviewOwnedArtifact(request, name))
527
+ ) {
528
+ return { state: "unsafe", context, capabilities };
529
+ }
530
+ const artifacts = [];
531
+ for (const name of names) {
532
+ const artifact = register(capabilities, await context.session.openChild(directory.cap, name));
533
+ if (
534
+ recordedOwner(request.privateState.artifacts[name]) !== reviewOwner(request) ||
535
+ artifact.observation.owner !== reviewOwner(request) ||
536
+ !recordedObservationMatches(
537
+ artifact.observation,
538
+ request.privateState.artifacts[name],
539
+ "file",
540
+ ) ||
541
+ !privateObservation(artifact.observation, 0o600)
542
+ ) {
543
+ return { state: "unsafe", context, capabilities };
544
+ }
545
+ artifacts.push({ name, file: artifact });
546
+ }
547
+ if (expectedHash !== null) {
548
+ if (!/^sha256:[0-9a-f]{64}$/.test(expectedHash)) {
549
+ return { state: "unsafe", context, capabilities };
550
+ }
551
+ const brief = artifacts.find((artifact) => artifact.name === `${request.id}.md`);
552
+ if (
553
+ !brief ||
554
+ hashBytes(await readNativeFile(context, brief.file, MAX_SUBPROCESS_BUFFER)) !== expectedHash
555
+ ) {
556
+ return { state: "unsafe", context, capabilities };
557
+ }
558
+ }
559
+ return { state: "source", context, capabilities, request, location, directory, artifacts };
560
+ } catch (error) {
561
+ await closeCapabilities(context, capabilities);
562
+ throw error;
563
+ }
564
+ }
565
+
566
+ async function createOrVerifyQuarantine(prepared) {
567
+ const { context, request, location, capabilities } = prepared;
568
+ let quarantine;
569
+ try {
570
+ quarantine = register(
571
+ capabilities,
572
+ await context.session.openChild(context.root.cap, location.quarantineName),
573
+ );
574
+ } catch (error) {
575
+ if (error?.code !== "not-found") throw error;
576
+ quarantine = register(
577
+ capabilities,
578
+ await context.session.createDirectory(context.root.cap, location.quarantineName, 0o700),
579
+ );
580
+ }
581
+ if (
582
+ quarantine.observation.identity.kind !== "directory" ||
583
+ !privateObservation(quarantine.observation, 0o700) ||
584
+ quarantine.observation.owner !== reviewOwner(request)
585
+ ) {
586
+ throw new Error("private review quarantine is not an owner-private directory");
587
+ }
588
+ let inventory = null;
589
+ try {
590
+ inventory = register(
591
+ capabilities,
592
+ await context.session.openChild(quarantine.cap, REVIEW_INVENTORY_NAME),
593
+ );
594
+ } catch (error) {
595
+ if (error?.code !== "not-found") throw error;
596
+ }
597
+ const stagedName = `.inventory-${request.id.slice(4)}.tmp`;
598
+ if (inventory === null) {
599
+ let staged;
600
+ try {
601
+ staged = register(capabilities, await context.session.openChild(quarantine.cap, stagedName));
602
+ } catch (error) {
603
+ if (error?.code !== "not-found") throw error;
604
+ staged = register(
605
+ capabilities,
606
+ await context.session.createFile(quarantine.cap, stagedName, 0o600),
607
+ );
608
+ }
609
+ if (
610
+ !privateObservation(staged.observation, 0o600) ||
611
+ staged.observation.owner !== reviewOwner(request)
612
+ ) {
613
+ throw new Error("private review quarantine has an unsafe inventory temporary");
614
+ }
615
+ await writeNativeFileContent(context, staged, inventoryBytes(request, location));
616
+ staged = { ...staged, observation: (await context.session.stat(staged.cap)).observation };
617
+ try {
618
+ await context.session.rename({
619
+ fromParent: quarantine.cap,
620
+ from: stagedName,
621
+ expected: staged.observation.identity,
622
+ toParent: quarantine.cap,
623
+ to: REVIEW_INVENTORY_NAME,
624
+ replace: "never",
625
+ });
626
+ } catch (error) {
627
+ if (!["possible", "committed"].includes(error?.mutation)) throw error;
628
+ }
629
+ await context.session.flush(quarantine.cap, "namespace", quarantine.observation.identity);
630
+ inventory = register(
631
+ capabilities,
632
+ await context.session.openChild(quarantine.cap, REVIEW_INVENTORY_NAME),
633
+ );
634
+ if (!sameNativeIdentity(inventory.observation.identity, staged.observation.identity)) {
635
+ throw new Error("private review quarantine inventory publication was replaced");
636
+ }
637
+ }
638
+ if (
639
+ !privateObservation(inventory.observation, 0o600) ||
640
+ inventory.observation.owner !== reviewOwner(request) ||
641
+ !(await readNativeFile(context, inventory, 64 * 1024)).equals(inventoryBytes(request, location))
642
+ ) {
643
+ throw new Error("private review quarantine has mismatched ledger provenance");
644
+ }
645
+ const names = (await listNativeDirectory(context, quarantine)).map((entry) => entry.name).sort();
646
+ if (names.length !== 1 || names[0] !== REVIEW_INVENTORY_NAME) {
647
+ throw new Error("private review quarantine contains an unknown or conflicting sibling");
648
+ }
649
+ return quarantine;
650
+ }
651
+
652
+ async function preflightExistingQuarantineOwner(prepared) {
653
+ const { context, request, location, capabilities } = prepared;
654
+ let quarantine;
655
+ try {
656
+ quarantine = register(
657
+ capabilities,
658
+ await context.session.openChild(context.root.cap, location.quarantineName),
659
+ );
660
+ } catch (error) {
661
+ if (error?.code === "not-found") return;
662
+ throw error;
663
+ }
664
+ if (
665
+ quarantine.observation.identity.kind !== "directory" ||
666
+ !privateObservation(quarantine.observation, 0o700) ||
667
+ quarantine.observation.owner !== reviewOwner(request)
668
+ ) {
669
+ throw new Error("private review quarantine is not an owner-private directory");
670
+ }
671
+ }
672
+
673
+ async function retainedMoveCommitted(prepared, quarantine) {
674
+ const { context, directory, location, capabilities } = prepared;
675
+ let source = null;
676
+ let retained = null;
677
+ try {
678
+ source = register(
679
+ capabilities,
680
+ await context.session.openChild(context.root.cap, location.directoryName),
681
+ );
682
+ } catch (error) {
683
+ if (error?.code !== "not-found") throw error;
684
+ }
685
+ try {
686
+ retained = register(
687
+ capabilities,
688
+ await context.session.openChild(quarantine.cap, REVIEW_RETAINED_NAME),
689
+ );
690
+ } catch (error) {
691
+ if (error?.code !== "not-found") throw error;
692
+ }
693
+ return (
694
+ source === null &&
695
+ retained !== null &&
696
+ sameNativeIdentity(retained.observation.identity, directory.observation.identity)
697
+ );
698
+ }
699
+
700
+ export async function settlePreparedReviewBrief(prepared) {
701
+ if (prepared.state === "retained") return true;
702
+ if (prepared.state !== "source") return false;
703
+ const { context, directory, artifacts, location } = prepared;
704
+ await preflightExistingQuarantineOwner(prepared);
705
+ for (const artifact of artifacts) {
706
+ await wipeReviewArtifact(context, artifact.file);
707
+ await context.session.closeCapability(artifact.file.cap);
708
+ prepared.capabilities.delete(artifact.file.cap);
709
+ }
710
+ const finalEntries = await listNativeDirectory(context, directory);
711
+ if (
712
+ finalEntries.length !== artifacts.length ||
713
+ finalEntries.some((entry) => {
714
+ const artifact = artifacts.find((candidate) => candidate.name === entry.name);
715
+ return (
716
+ !artifact || !sameNativeIdentity(entry.observation.identity, artifact.file.observation.identity)
717
+ );
718
+ })
719
+ ) {
720
+ return false;
721
+ }
722
+ const quarantine = await createOrVerifyQuarantine(prepared);
723
+ try {
724
+ await context.session.rename({
725
+ fromParent: context.root.cap,
726
+ from: location.directoryName,
727
+ expected: directory.observation.identity,
728
+ toParent: quarantine.cap,
729
+ to: REVIEW_RETAINED_NAME,
730
+ replace: "never",
731
+ });
732
+ } catch (error) {
733
+ if (
734
+ !["possible", "committed"].includes(error?.mutation) ||
735
+ !(await retainedMoveCommitted(prepared, quarantine))
736
+ ) {
737
+ throw error;
738
+ }
739
+ }
740
+ await context.session.flush(quarantine.cap, "namespace", quarantine.observation.identity);
741
+ await context.session.flush(context.root.cap, "namespace", context.root.observation.identity);
742
+ if (!(await retainedMoveCommitted(prepared, quarantine))) return false;
743
+ const reboundRoot = register(prepared.capabilities, await context.session.openRoot(context.rootPath));
744
+ if (!sameNativeIdentity(reboundRoot.observation.identity, context.root.observation.identity))
745
+ return false;
746
+ const reboundQuarantine = register(
747
+ prepared.capabilities,
748
+ await context.session.openChild(reboundRoot.cap, location.quarantineName),
749
+ );
750
+ const reboundRetained = register(
751
+ prepared.capabilities,
752
+ await context.session.openChild(reboundQuarantine.cap, REVIEW_RETAINED_NAME),
753
+ );
754
+ if (!sameNativeIdentity(reboundRetained.observation.identity, directory.observation.identity))
755
+ return false;
756
+ return Boolean(
757
+ await inspectRetainedSettlement(context, prepared.request, location, prepared.capabilities),
758
+ );
759
+ }
760
+
761
+ export async function closePreparedReviewBrief(prepared) {
762
+ await closeCapabilities(prepared.context, prepared.capabilities);
763
+ }
764
+
765
+ /** Settle one request with one helper session when no wider transaction owns one. */
766
+ export async function removeReviewBrief(
767
+ request,
768
+ { dryRun = false, expectedHash = null, context = null } = {},
769
+ ) {
770
+ const ownedContext = context === null;
771
+ const transaction =
772
+ context ?? (await openReviewFsTransaction("review native filesystem helper", request));
773
+ let prepared;
774
+ try {
775
+ prepared = await prepareReviewBriefSettlement(transaction, request, { expectedHash });
776
+ if (dryRun) return prepared.state === "source" || prepared.state === "retained";
777
+ return await settlePreparedReviewBrief(prepared);
778
+ } finally {
779
+ if (prepared) await closePreparedReviewBrief(prepared);
780
+ if (ownedContext) await transaction.close();
781
+ }
782
+ }
783
+
784
+ export async function readVerifiedReviewArtifact(request, name) {
785
+ if (!REVIEW_PRIVATE_COMPANIONS.has(name)) return null;
786
+ const location = parseReviewLocation(request);
787
+ if (!location || !exactPrivateState(request)) return null;
788
+ const context = await openReviewFsTransaction("private review read native filesystem helper", request);
789
+ const capabilities = new Set();
790
+ try {
791
+ if (
792
+ !recordedObservationMatches(context.root.observation, request.privateState.tempRoot, "directory", {
793
+ ignoreLinkCount: true,
794
+ })
795
+ ) {
796
+ return null;
797
+ }
798
+ const directory = register(
799
+ capabilities,
800
+ await context.session.openChild(context.root.cap, location.directoryName),
801
+ );
802
+ if (
803
+ !recordedObservationMatches(directory.observation, request.privateState.directory, "directory") ||
804
+ !privateObservation(directory.observation, 0o700)
805
+ ) {
806
+ return null;
807
+ }
808
+ const file = register(capabilities, await context.session.openChild(directory.cap, name));
809
+ if (
810
+ recordedOwner(request.privateState.artifacts?.[name]) !== reviewOwner(request) ||
811
+ file.observation.owner !== reviewOwner(request) ||
812
+ !recordedObservationMatches(file.observation, request.privateState.artifacts?.[name], "file") ||
813
+ !privateObservation(file.observation, 0o600)
814
+ ) {
815
+ return null;
816
+ }
817
+ return (await readNativeFile(context, file, MAX_SUBPROCESS_BUFFER)).toString("utf8");
818
+ } catch (error) {
819
+ if (error?.code === "not-found" || error?.code === "symlink-rejected") return null;
820
+ throw error;
821
+ } finally {
822
+ await closeCapabilities(context, capabilities);
823
+ await context.close();
824
+ }
825
+ }