@apexops/kernel 0.10.0-next.5 → 0.11.0-next.1

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 (65) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/directory-lock.d.ts +118 -0
  3. package/dist/directory-lock.d.ts.map +1 -0
  4. package/dist/directory-lock.js +587 -0
  5. package/dist/directory-lock.js.map +1 -0
  6. package/dist/files.d.ts +63 -0
  7. package/dist/files.d.ts.map +1 -1
  8. package/dist/files.js +158 -2
  9. package/dist/files.js.map +1 -1
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/object-store.js +2 -2
  15. package/dist/object-store.js.map +1 -1
  16. package/dist/project-store.d.ts +2 -1
  17. package/dist/project-store.d.ts.map +1 -1
  18. package/dist/project-store.js +1 -0
  19. package/dist/project-store.js.map +1 -1
  20. package/dist/repeat-files.d.ts +66 -0
  21. package/dist/repeat-files.d.ts.map +1 -0
  22. package/dist/repeat-files.js +214 -0
  23. package/dist/repeat-files.js.map +1 -0
  24. package/dist/repeat-guard.d.ts +197 -0
  25. package/dist/repeat-guard.d.ts.map +1 -0
  26. package/dist/repeat-guard.js +462 -0
  27. package/dist/repeat-guard.js.map +1 -0
  28. package/dist/review-capture.d.ts +170 -0
  29. package/dist/review-capture.d.ts.map +1 -0
  30. package/dist/review-capture.js +570 -0
  31. package/dist/review-capture.js.map +1 -0
  32. package/dist/run-repository.d.ts +52 -2
  33. package/dist/run-repository.d.ts.map +1 -1
  34. package/dist/run-repository.js +198 -52
  35. package/dist/run-repository.js.map +1 -1
  36. package/dist/tasks.d.ts.map +1 -1
  37. package/dist/tasks.js +16 -2
  38. package/dist/tasks.js.map +1 -1
  39. package/dist/test/files.test.d.ts +2 -0
  40. package/dist/test/files.test.d.ts.map +1 -0
  41. package/dist/test/files.test.js +140 -0
  42. package/dist/test/files.test.js.map +1 -0
  43. package/dist/test/policies.test.js +1 -1
  44. package/dist/test/policies.test.js.map +1 -1
  45. package/dist/test/repeat-guard.test.d.ts +2 -0
  46. package/dist/test/repeat-guard.test.d.ts.map +1 -0
  47. package/dist/test/repeat-guard.test.js +1106 -0
  48. package/dist/test/repeat-guard.test.js.map +1 -0
  49. package/dist/test/review-capture.test.d.ts +2 -0
  50. package/dist/test/review-capture.test.d.ts.map +1 -0
  51. package/dist/test/review-capture.test.js +291 -0
  52. package/dist/test/review-capture.test.js.map +1 -0
  53. package/dist/test/run-repository.test.js +451 -7
  54. package/dist/test/run-repository.test.js.map +1 -1
  55. package/dist/test/workflow.test.js +30 -1
  56. package/dist/test/workflow.test.js.map +1 -1
  57. package/dist/test/writer-transfer.test.js +6 -1
  58. package/dist/test/writer-transfer.test.js.map +1 -1
  59. package/dist/validator-registry.d.ts.map +1 -1
  60. package/dist/validator-registry.js +10 -1
  61. package/dist/validator-registry.js.map +1 -1
  62. package/dist/workflow-validator-ownership.d.ts.map +1 -1
  63. package/dist/workflow-validator-ownership.js +2 -1
  64. package/dist/workflow-validator-ownership.js.map +1 -1
  65. package/package.json +3 -3
@@ -0,0 +1,462 @@
1
+ import { CONTRACT_VERSION, RepeatGuardRecordsV1Schema, contractMetadata, registerContractFormats, } from "@apexops/contracts";
2
+ import { Value } from "@sinclair/typebox/value";
3
+ import { constants } from "node:fs";
4
+ import { lstat, mkdir, open } from "node:fs/promises";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { sha256Json, sha256Text } from "./canonical.js";
7
+ import { DirectoryLock, DirectoryLockShareCloseError } from "./directory-lock.js";
8
+ import { EventJournal } from "./event-journal.js";
9
+ import { atomicWriteJson, withWriteFence, WriteFence } from "./files.js";
10
+ import { repeatFilesHeld, snapshotRepeatFiles, } from "./repeat-files.js";
11
+ import { canonicalWorktreePath } from "./run-repository.js";
12
+ /** Run-scoped file holding the bounded set of calls that may still be answered from their original result. */
13
+ export const REPEAT_GUARD_FILE = ".repeat-guard.json";
14
+ /** Run-scoped, hash-chained audit journal of answered repeats. Separate from the run journal, whose head it keeps. */
15
+ export const REPEAT_JOURNAL_DIRECTORY = "repeats";
16
+ export const REPEAT_EVENT_TYPE = "call.repeated";
17
+ /**
18
+ * Workspace-scoped directory lock serializing guarded calls across processes from record lookup through record
19
+ * publication. It does not depend on the selected run, so calls that change the selection are serialized too.
20
+ */
21
+ export const REPEAT_LOCK_FILE = ".repeat-guard.lock";
22
+ export const DEFAULT_REPEAT_LOCK_WAIT_MS = 30_000;
23
+ const REPEAT_LOCK_TTL_MS = 30_000;
24
+ // Poll interval of a read waiting for the workspace lock or for a share window to open.
25
+ const READ_LOCK_RETRY_MS = 20;
26
+ export const DEFAULT_REPEAT_WINDOW_MS = 10 * 60 * 1000;
27
+ export const MAX_REPEAT_RECORDS = 16;
28
+ export const MAX_REPEAT_RESULT_BYTES = 64 * 1024;
29
+ const MAX_REPEAT_GUARD_FILE_BYTES = contractMetadata[RepeatGuardRecordsV1Schema.$id].maxBytes;
30
+ const OPERATION = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u;
31
+ const AUDIT_APPEND_ATTEMPTS = 3;
32
+ /** Another guarded call in the workspace is still running; the caller should refresh state and retry later. */
33
+ /** The caller cancelled the call before it started; nothing was looked up, executed or recorded. */
34
+ export class RepeatGuardCancelledError extends Error {
35
+ constructor() {
36
+ super("The state-changing call was cancelled before it started");
37
+ this.name = "RepeatGuardCancelledError";
38
+ }
39
+ }
40
+ export class RepeatGuardBusyError extends Error {
41
+ constructor() {
42
+ super("Another state-changing call in this workspace is still in progress");
43
+ this.name = "RepeatGuardBusyError";
44
+ }
45
+ }
46
+ /**
47
+ * The workspace changed while its lock holder waited without the lock, so a decision taken during the wait would apply
48
+ * to state it was not taken against. Nothing after the wait ran; the caller should refresh state and run again.
49
+ */
50
+ export class RepeatGuardStaleError extends Error {
51
+ constructor() {
52
+ super("Workspace state changed while the command waited without the workspace lock");
53
+ this.name = "RepeatGuardStaleError";
54
+ }
55
+ }
56
+ /**
57
+ * A snapshot read reached a step that needs the workspace lock, such as finishing a pending recovery. Nothing was
58
+ * written; {@link withRepeatGuardReadLock} discards the attempt and waits for the lock instead.
59
+ */
60
+ export class RepeatGuardSnapshotUnavailableError extends Error {
61
+ constructor() {
62
+ super("The read needs the workspace lock");
63
+ this.name = "RepeatGuardSnapshotUnavailableError";
64
+ }
65
+ }
66
+ /**
67
+ * A holder's share window could not be closed, so snapshot readers might still trust it. Every later atomic write and
68
+ * share in the holder's operation fails with this error, and so does the operation itself, even if it handled the
69
+ * error; releasing the lock then closes the window. The caller should run the operation again.
70
+ */
71
+ export class RepeatGuardShareCloseError extends Error {
72
+ constructor(cause) {
73
+ super("The workspace lock could not be closed to readers; nothing more was written", { cause });
74
+ this.name = "RepeatGuardShareCloseError";
75
+ }
76
+ }
77
+ function normalizedArguments(value) {
78
+ if (value === undefined)
79
+ return null;
80
+ const serialized = JSON.stringify(value);
81
+ if (serialized === undefined)
82
+ throw new TypeError("Repeat call arguments must be JSON");
83
+ return JSON.parse(serialized);
84
+ }
85
+ /** Identifies a call independent of state: operation, canonical worktree and canonical arguments. */
86
+ export function repeatCallHash(call) {
87
+ if (!OPERATION.test(call.operation))
88
+ throw new TypeError("Repeat call operation is invalid");
89
+ return sha256Json({
90
+ version: 1,
91
+ operation: call.operation,
92
+ workspace: canonicalWorktreePath(call.workspace),
93
+ arguments: normalizedArguments(call.arguments),
94
+ });
95
+ }
96
+ /** Fingerprint of a call against the run state it applies to. */
97
+ export function repeatFingerprint(callHash, state) {
98
+ return sha256Json({ version: 1, callHash, state });
99
+ }
100
+ /**
101
+ * State token for the repeat window. It changes whenever the selection, the project set, the selected run document,
102
+ * its journal head, its writer lease holder or the content of its mutable files changes; lease expiry renewals do not
103
+ * change it.
104
+ */
105
+ export function repeatStateToken(input) {
106
+ return sha256Json({
107
+ version: 2,
108
+ selection: { projectId: input.selection.projectId, runId: input.selection.runId },
109
+ projects: [...input.projects].sort(),
110
+ runHash: input.runHash,
111
+ journalHead: input.journalHead,
112
+ writer: input.writer,
113
+ files: input.files ?? null,
114
+ });
115
+ }
116
+ function validRecords(value) {
117
+ registerContractFormats();
118
+ return (Value.Check(RepeatGuardRecordsV1Schema, value) &&
119
+ value.records.length <= MAX_REPEAT_RECORDS &&
120
+ value.records.every((record) => Number.isFinite(Date.parse(record.recordedAt)) &&
121
+ Number.isFinite(Date.parse(record.expiresAt)) &&
122
+ Buffer.byteLength(record.result) <= MAX_REPEAT_RESULT_BYTES));
123
+ }
124
+ /**
125
+ * Reads the run's repeat records. An absent, unsafe or malformed file yields no records: losing a record only makes a
126
+ * repeat a new request that normal kernel checks decide, so corruption cannot replay a forged result.
127
+ */
128
+ export async function readRepeatRecords(runDirectory) {
129
+ const path = join(runDirectory, REPEAT_GUARD_FILE);
130
+ try {
131
+ const before = await lstat(path);
132
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_REPEAT_GUARD_FILE_BYTES)
133
+ return [];
134
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
135
+ let bytes;
136
+ try {
137
+ bytes = await handle.readFile();
138
+ }
139
+ finally {
140
+ await handle.close();
141
+ }
142
+ if (bytes.byteLength > MAX_REPEAT_GUARD_FILE_BYTES)
143
+ return [];
144
+ const parsed = JSON.parse(bytes.toString("utf8"));
145
+ return validRecords(parsed) ? parsed.records : [];
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ }
151
+ export async function readRepeatEvents(runDirectory) {
152
+ return new EventJournal(join(runDirectory, REPEAT_JOURNAL_DIRECTORY)).replay();
153
+ }
154
+ function replayable(record) {
155
+ try {
156
+ const value = JSON.parse(record.result);
157
+ return value !== null && typeof value === "object" && !Array.isArray(value)
158
+ ? value
159
+ : undefined;
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ }
165
+ async function appendRepeatEvent(scope, record, call, fingerprint, now, idSource) {
166
+ const journal = new EventJournal(join(scope.runDirectory, REPEAT_JOURNAL_DIRECTORY));
167
+ for (let attempt = 1;; attempt += 1) {
168
+ try {
169
+ await journal.append({
170
+ eventId: idSource(),
171
+ projectId: scope.projectId,
172
+ runId: scope.runId,
173
+ type: REPEAT_EVENT_TYPE,
174
+ timestamp: now.toISOString(),
175
+ ownerEpoch: scope.ownerEpoch,
176
+ expectedHead: await journal.head(),
177
+ payload: {
178
+ operation: record.operation,
179
+ // Record the worktree as the caller sees it; the case-folded comparison key belongs only in callHash.
180
+ workspace: resolve(call.workspace),
181
+ callHash: record.callHash,
182
+ fingerprint,
183
+ originalFingerprint: record.fingerprint,
184
+ state: scope.state,
185
+ originalRecordedAt: record.recordedAt,
186
+ resultHash: sha256Text(record.result),
187
+ },
188
+ });
189
+ return;
190
+ }
191
+ catch (error) {
192
+ if (attempt >= AUDIT_APPEND_ATTEMPTS || !error.message.startsWith("Stale journal head"))
193
+ throw error;
194
+ }
195
+ }
196
+ }
197
+ async function storeRepeatRecord(scope, candidate, callHash, now, maxRecords) {
198
+ // A record is answerable only while the run state equals its post-call state, so any record whose state differs
199
+ // from the current state, or that expired, is dropped. The newest bounded set of same-state calls remains.
200
+ const live = (await readRepeatRecords(scope.runDirectory)).filter((record) => record.stateAfter === scope.state && Date.parse(record.expiresAt) > now.getTime() && record.callHash !== callHash);
201
+ const records = [...live, ...(candidate === undefined ? [] : [candidate])].slice(-maxRecords);
202
+ const file = { schemaVersion: CONTRACT_VERSION, records };
203
+ if (!validRecords(file))
204
+ throw new Error("Repeat records do not satisfy repeat-guard-records-v1");
205
+ await atomicWriteJson(join(scope.runDirectory, REPEAT_GUARD_FILE), file);
206
+ }
207
+ /**
208
+ * Executes a state-changing call at most once per run state. An identical call (same operation, worktree and
209
+ * arguments) made while the selected run is unchanged since the original succeeded, and within the window, returns the
210
+ * original serialized result and appends a `call.repeated` audit event instead of executing again. Any intervening
211
+ * state change, an expired window, a failed original call or a missing record makes the call a new request. A result
212
+ * is stored only if the files the scope binds kept their content from the start of the call, changed only by the
213
+ * call's own writes, so a later repeat is never answered from a result computed on other content. Guarded
214
+ * calls in one workspace are serialized across processes from record lookup through record publication, including
215
+ * calls that create or change the selected run.
216
+ */
217
+ export async function executeRepeatSafe(call, hooks, options = {}) {
218
+ const clock = options.clock ?? (() => new Date());
219
+ const idSource = options.idSource ?? (() => crypto.randomUUID());
220
+ const windowMs = options.windowMs ?? DEFAULT_REPEAT_WINDOW_MS;
221
+ const maxRecords = options.maxRecords ?? MAX_REPEAT_RECORDS;
222
+ if (!Number.isSafeInteger(windowMs) || windowMs < 1)
223
+ throw new RangeError("Repeat window must be positive");
224
+ if (!Number.isSafeInteger(maxRecords) || maxRecords < 1 || maxRecords > MAX_REPEAT_RECORDS)
225
+ throw new RangeError(`Repeat records must be an integer from 1 to ${MAX_REPEAT_RECORDS}`);
226
+ const lockWaitMs = options.lockWaitMs ?? DEFAULT_REPEAT_LOCK_WAIT_MS;
227
+ const callHash = repeatCallHash(call);
228
+ return withRepeatGuardLock(hooks.lockDirectory, () => guardedCall(call, callHash, hooks, clock, idSource, windowMs, maxRecords), { lockWaitMs, ...(options.signal === undefined ? {} : { signal: options.signal }) });
229
+ }
230
+ /**
231
+ * Runs `operation` while holding the workspace repeat lock in `lockDirectory`. Guarded calls take it, and so must every
232
+ * other writer of the same workspace (such as state-changing CLI commands), so that no write lands between a guarded
233
+ * call's state reads, execution and record publication. While the parent of `lockDirectory` does not exist there is no
234
+ * workspace state yet, and `operation` runs unserialized. The lock is not reentrant. `operation` may give the lock up
235
+ * while it waits, for example for interactive input, through {@link WorkspaceLockHold.suspend}; guarded calls never do.
236
+ */
237
+ export async function withRepeatGuardLock(lockDirectory, operation, options = {}) {
238
+ const lockWaitMs = options.lockWaitMs ?? DEFAULT_REPEAT_LOCK_WAIT_MS;
239
+ if (!Number.isSafeInteger(lockWaitMs) || lockWaitMs < 0)
240
+ throw new RangeError("Repeat lock wait must not be negative");
241
+ const { signal } = options;
242
+ const cancelled = () => signal?.aborted === true;
243
+ if (cancelled())
244
+ throw new RepeatGuardCancelledError();
245
+ let held = await acquireRepeatGuardLock(lockDirectory, lockWaitMs, signal);
246
+ let suspended = false;
247
+ const fence = new WriteFence();
248
+ const hold = {
249
+ suspend: async (wait, state) => {
250
+ if (suspended)
251
+ throw new Error("The workspace lock is already suspended");
252
+ suspended = true;
253
+ try {
254
+ // Without a readable state token there is no way to prove the state unchanged afterwards, so keep the lock.
255
+ const before = await state().then((token) => ({ token }), () => undefined);
256
+ if (before === undefined)
257
+ return await wait();
258
+ const releasing = held;
259
+ held = undefined;
260
+ await releasing?.release();
261
+ const value = await wait();
262
+ held = await acquireRepeatGuardLock(lockDirectory, lockWaitMs, signal);
263
+ if (cancelled())
264
+ throw new RepeatGuardCancelledError();
265
+ if ((await state().catch(() => undefined)) !== before.token)
266
+ throw new RepeatGuardStaleError();
267
+ return value;
268
+ }
269
+ finally {
270
+ suspended = false;
271
+ }
272
+ },
273
+ share: async (operation) => {
274
+ if (fence.error !== undefined)
275
+ throw fence.error;
276
+ const sharing = held;
277
+ if (sharing === undefined)
278
+ return operation();
279
+ try {
280
+ // No write of this operation lands while the window is open, even from a flow other than `operation`.
281
+ return await fence.quiet(() => sharing.share(operation));
282
+ }
283
+ catch (error) {
284
+ if (!(error instanceof DirectoryLockShareCloseError))
285
+ throw error;
286
+ // A reader may still trust the open window, so nothing in this operation may write again before release.
287
+ fence.error ??= new RepeatGuardShareCloseError(error);
288
+ throw fence.error;
289
+ }
290
+ },
291
+ };
292
+ try {
293
+ // Cancellation while waiting for the lock is reported by the lock; once it is held, nothing has started yet.
294
+ if (cancelled())
295
+ throw new RepeatGuardCancelledError();
296
+ const value = await withWriteFence(fence, () => operation(hold));
297
+ if (fence.error !== undefined)
298
+ throw fence.error;
299
+ return value;
300
+ }
301
+ catch (error) {
302
+ // An operation that handled the share failure, or failed on a fenced write, still fails with it.
303
+ throw fence.error ?? error;
304
+ }
305
+ finally {
306
+ await held?.release();
307
+ }
308
+ }
309
+ /**
310
+ * Runs a read that may need the workspace lock, for example to finish a pending recovery. It takes the lock like
311
+ * {@link withRepeatGuardLock} when the lock is free. While another process holds the lock but shares it through
312
+ * {@link WorkspaceLockHold.share}, `operation` runs in `snapshot` mode without the lock instead, and its outcome counts
313
+ * only if the holder kept the same share window open throughout, so the holder wrote nothing meanwhile. In `snapshot`
314
+ * mode `operation` must not write workspace state; it throws {@link RepeatGuardSnapshotUnavailableError} instead, and
315
+ * the read waits for the lock. Fails with {@link RepeatGuardBusyError} after `lockWaitMs` without either.
316
+ */
317
+ export async function withRepeatGuardReadLock(lockDirectory, operation, options = {}) {
318
+ const lockWaitMs = options.lockWaitMs ?? DEFAULT_REPEAT_LOCK_WAIT_MS;
319
+ if (!Number.isSafeInteger(lockWaitMs) || lockWaitMs < 0)
320
+ throw new RangeError("Repeat lock wait must not be negative");
321
+ const { signal } = options;
322
+ const cancelled = () => signal?.aborted === true;
323
+ const deadline = Date.now() + lockWaitMs;
324
+ // Only a lock holder can finish a pending recovery, so once a snapshot needed one the read just waits for the lock.
325
+ let snapshots = true;
326
+ for (;;) {
327
+ if (cancelled())
328
+ throw new RepeatGuardCancelledError();
329
+ let held;
330
+ try {
331
+ held = await acquireRepeatGuardLock(lockDirectory, 0, signal);
332
+ }
333
+ catch (error) {
334
+ if (!(error instanceof RepeatGuardBusyError))
335
+ throw error;
336
+ const snapshot = !snapshots
337
+ ? undefined
338
+ : await repeatGuardLock(lockDirectory)
339
+ .readShared(() => operation("snapshot"))
340
+ .catch((readError) => {
341
+ if (!(readError instanceof RepeatGuardSnapshotUnavailableError))
342
+ throw readError;
343
+ snapshots = false;
344
+ return undefined;
345
+ });
346
+ if (snapshot !== undefined)
347
+ return snapshot.value;
348
+ if (Date.now() >= deadline)
349
+ throw error;
350
+ await new Promise((done) => setTimeout(done, READ_LOCK_RETRY_MS));
351
+ continue;
352
+ }
353
+ try {
354
+ if (cancelled())
355
+ throw new RepeatGuardCancelledError();
356
+ return await operation("locked");
357
+ }
358
+ finally {
359
+ await held?.release();
360
+ }
361
+ }
362
+ }
363
+ function repeatGuardLock(lockDirectory) {
364
+ return new DirectoryLock(join(lockDirectory, REPEAT_LOCK_FILE), {
365
+ label: "Repeat guard lock",
366
+ clock: () => new Date(),
367
+ idSource: () => crypto.randomUUID(),
368
+ ttlMs: REPEAT_LOCK_TTL_MS,
369
+ platform: process.platform,
370
+ busyError: () => new RepeatGuardBusyError(),
371
+ });
372
+ }
373
+ /** Acquires the workspace repeat lock, or returns undefined while the workspace has no state to serialize yet. */
374
+ async function acquireRepeatGuardLock(lockDirectory, lockWaitMs, signal) {
375
+ const cancelled = () => (signal?.aborted === true ? new RepeatGuardCancelledError() : undefined);
376
+ try {
377
+ await lstat(dirname(lockDirectory));
378
+ }
379
+ catch (error) {
380
+ if (error.code === "ENOENT")
381
+ return undefined;
382
+ throw error;
383
+ }
384
+ await mkdir(lockDirectory, { recursive: true });
385
+ const directory = await lstat(lockDirectory);
386
+ if (!directory.isDirectory() || directory.isSymbolicLink())
387
+ throw new Error("Repeat guard lock directory is not a regular directory");
388
+ return repeatGuardLock(lockDirectory).acquire({ waitMs: lockWaitMs, onContention: async () => cancelled() });
389
+ }
390
+ async function guardedCall(call, callHash, hooks, clock, idSource, windowMs, maxRecords) {
391
+ // A start state that could not be read cannot bind a result, so such a call is never stored.
392
+ let startRead = true;
393
+ const before = await hooks.scope().catch(() => {
394
+ startRead = false;
395
+ return undefined;
396
+ });
397
+ const fingerprint = repeatFingerprint(callHash, before?.state ?? null);
398
+ if (before !== undefined) {
399
+ const now = clock();
400
+ const records = await readRepeatRecords(before.runDirectory);
401
+ const record = records.findLast((candidate) => candidate.callHash === callHash &&
402
+ candidate.stateAfter === before.state &&
403
+ Date.parse(candidate.expiresAt) > now.getTime());
404
+ const original = record === undefined ? undefined : replayable(record);
405
+ // A bound file that changed since the call identity was taken makes the stored result stale; execute instead.
406
+ // The state is read again so that an edit that bypasses the workspace lock while the record was checked cannot be
407
+ // answered from the stored result; the replay is then current as of that second read.
408
+ if (record !== undefined &&
409
+ original !== undefined &&
410
+ (hooks.identityStable === undefined || (await hooks.identityStable())) &&
411
+ (await hooks.scope().catch(() => undefined))?.state === before.state) {
412
+ await hooks.assertReplayAllowed(before);
413
+ await appendRepeatEvent(before, record, call, fingerprint, now, idSource);
414
+ return { value: original, repeated: true, fingerprint };
415
+ }
416
+ }
417
+ const value = await hooks.execute();
418
+ try {
419
+ if (!startRead)
420
+ return { value, repeated: false, fingerprint };
421
+ if (hooks.identityStable !== undefined && !(await hooks.identityStable()))
422
+ return { value, repeated: false, fingerprint };
423
+ const after = await hooks.scope();
424
+ if (after === undefined)
425
+ return { value, repeated: false, fingerprint };
426
+ // A bound file changed by anything but the operation itself while it ran, for example by an editor that bypasses
427
+ // the workspace lock, would bind this result to content the operation may not have seen.
428
+ if (before?.files !== undefined) {
429
+ const start = before.files;
430
+ const end = after.files !== undefined && start.roots.every((root) => after.files.roots.includes(root))
431
+ ? after.files
432
+ : await snapshotRepeatFiles(start.roots);
433
+ if (!repeatFilesHeld(start, end, hooks.writes))
434
+ return { value, repeated: false, fingerprint };
435
+ }
436
+ const now = clock();
437
+ const result = JSON.stringify(value);
438
+ const limit = hooks.validUntil?.(value);
439
+ const expiresAt = Math.min(now.getTime() + windowMs, limit === undefined || !Number.isFinite(Date.parse(limit)) ? Number.POSITIVE_INFINITY : Date.parse(limit));
440
+ const storable = typeof result === "string" &&
441
+ Buffer.byteLength(result) <= MAX_REPEAT_RESULT_BYTES &&
442
+ replayable({ result }) !== undefined &&
443
+ expiresAt > now.getTime();
444
+ await storeRepeatRecord(after, storable
445
+ ? {
446
+ fingerprint,
447
+ callHash,
448
+ operation: call.operation,
449
+ stateBefore: before?.state ?? null,
450
+ stateAfter: after.state,
451
+ recordedAt: now.toISOString(),
452
+ expiresAt: new Date(expiresAt).toISOString(),
453
+ result,
454
+ }
455
+ : undefined, callHash, now, maxRecords);
456
+ }
457
+ catch {
458
+ // The call has committed. Without a stored record a repeat is a new request decided by normal kernel checks.
459
+ }
460
+ return { value, repeated: false, fingerprint };
461
+ }
462
+ //# sourceMappingURL=repeat-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repeat-guard.js","sourceRoot":"","sources":["../src/repeat-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ import { type ReviewCaptureV1 } from "@apexops/contracts";
2
+ /**
3
+ * Captured rubber-duck reviews (DECISION-031). The kernel issues a review request with a single-use nonce and a prompt;
4
+ * the APEX agent passes the prompt unchanged to the built-in `rubber-duck` agent; the managed `postToolUse` hook saves
5
+ * the prompt and rubber-duck's exact output in a signed capture record outside the workspace. The kernel verifies the
6
+ * record, binds it to the issued prompt and the reviewed artifact, and derives findings only from the captured output.
7
+ *
8
+ * The hook (plugin/hooks/apex-hook.mjs) is dependency-free, so it repeats the record format, canonical JSON, HMAC and
9
+ * location rules below; tools/tests/vnext/plugin-hooks.test.mjs keeps both sides aligned.
10
+ */
11
+ export declare const REVIEW_CAPTURE_SCHEMA: "apex-review-capture-v1";
12
+ export declare const REVIEW_REQUEST_HEADER = "APEX-REVIEW";
13
+ export declare const REVIEW_ANSWER_FENCE = "apex-review";
14
+ export declare const REVIEW_AGENT = "rubber-duck";
15
+ export declare const REVIEW_MAX_ATTEMPTS = 2;
16
+ export declare const REVIEW_HOME_ENV = "APEX_REVIEW_HOME";
17
+ export declare const REVIEW_MAX_FINDINGS = 50;
18
+ /**
19
+ * Budget for all finding titles and details together, measured as doubly JSON-escaped UTF-8 because an MCP result
20
+ * carries its JSON once as structured content and once as escaped text. nextTask returns every open finding in one
21
+ * result capped at 64 KiB, so the findings must fit there with their IDs and actions.
22
+ */
23
+ export declare const REVIEW_MAX_FINDINGS_BYTES = 20480;
24
+ declare const SEVERITIES: readonly ["critical", "high", "medium", "low", "info"];
25
+ declare const PILLARS: readonly ["security", "reliability", "performance-efficiency", "cost-optimization", "operational-excellence"];
26
+ declare const OUTCOMES: readonly ["pass", "finding", "not-applicable"];
27
+ export type ReviewCaptureFailure = "missing" | "ambiguous" | "malformed" | "signature" | "not-rubber-duck" | "nonce-mismatch" | "prompt-mismatch" | "unparseable" | "inputs-changed";
28
+ /** A capture problem. Every reason fails the review closed; only `missing` leaves the request reusable. */
29
+ export declare class ReviewCaptureError extends Error {
30
+ readonly reason: ReviewCaptureFailure;
31
+ constructor(reason: ReviewCaptureFailure, message: string);
32
+ }
33
+ /**
34
+ * The review home (or a folder in it) is not private to this OS user: another user could replace the capture key or
35
+ * inject captures, so nothing in it can be trusted until its owner and mode are fixed.
36
+ */
37
+ export declare class ReviewHomeError extends Error {
38
+ constructor(message: string);
39
+ }
40
+ export type ReviewSeverity = (typeof SEVERITIES)[number];
41
+ export type ReviewPillar = (typeof PILLARS)[number];
42
+ export type ReviewCaptureRecordV1 = ReviewCaptureV1;
43
+ export interface ReviewAnswer {
44
+ findings: Array<{
45
+ id: string;
46
+ severity: ReviewSeverity;
47
+ title: string;
48
+ detail: string;
49
+ }>;
50
+ criteria?: Array<{
51
+ criterionId: ReviewPillar;
52
+ outcome: (typeof OUTCOMES)[number];
53
+ rationale: string;
54
+ findingIds: string[];
55
+ }>;
56
+ }
57
+ export interface ReviewPromptFile {
58
+ label: "instructions" | "subject" | "input";
59
+ kind: string;
60
+ path: string;
61
+ sha256: string;
62
+ }
63
+ export interface ReviewPromptRequest {
64
+ nonce: string;
65
+ gate: number;
66
+ subjectKind: string;
67
+ files: readonly ReviewPromptFile[];
68
+ wellArchitected: boolean;
69
+ }
70
+ /** A fresh nonce, or one derived from a unique seed (the service's ID source) so deterministic runs stay reproducible. */
71
+ export declare function createReviewNonce(seed?: string): string;
72
+ /** Line endings and trailing whitespace do not change a prompt's identity; every other byte does. */
73
+ export declare function normalizeReviewPrompt(prompt: string): string;
74
+ export declare function reviewPromptSha256(prompt: string): string;
75
+ /** The nonce from the first non-empty prompt line `APEX-REVIEW: nonce=<32 hex>`, or undefined. */
76
+ export declare function reviewNonceFromPrompt(prompt: string): string | undefined;
77
+ export declare function buildReviewPrompt(request: ReviewPromptRequest): string;
78
+ /** Reviewer guidance per subject, carried over from the retired APEX Reviewer. */
79
+ export declare function buildReviewInstructions(subjectKind: string): string;
80
+ /**
81
+ * Derives findings from rubber-duck's captured output: exactly one ```apex-review fenced JSON block, checked strictly.
82
+ * Text outside the block is ignored. Findings without an id get F-1, F-2, ... in answer order.
83
+ */
84
+ export declare function parseReviewAnswer(response: string, options: {
85
+ wellArchitected: boolean;
86
+ }): ReviewAnswer;
87
+ /** Folder shared by the hook and the kernel: `$APEX_REVIEW_HOME`, else `~/.apex/reviews`. Outside every workspace. */
88
+ export declare function reviewHome(env?: NodeJS.ProcessEnv): string;
89
+ /** Captures older than this belong to requests that expired (tasks live 24 hours) and can never be ingested. */
90
+ export declare const REVIEW_CAPTURE_RETENTION_MS: number;
91
+ /**
92
+ * Removes captures whose request can no longer be ingested, so abandoned reviews do not keep model output in the
93
+ * shared review home forever: captures older than the retention, and captures whose nonce the caller knows was used
94
+ * (completed, rejected or cancelled) but whose removal failed earlier. Captures for live requests stay. Returns the
95
+ * count removed.
96
+ */
97
+ export declare function sweepReviewCaptures(home?: string, now?: number, maxAgeMs?: number, consumedNonces?: ReadonlySet<string>): Promise<number>;
98
+ /** The per-user HMAC key (32 random bytes, hex) shared with the hook; created with 0600 permissions when absent. */
99
+ export declare function reviewCaptureKey(home?: string): Promise<Buffer>;
100
+ type UnsignedCapture = Omit<ReviewCaptureRecordV1, "signature">;
101
+ export declare function reviewCaptureSignature(record: UnsignedCapture, key: Buffer): string;
102
+ /** Builds a signed record exactly as the hook does. Used by tests and the hook parity check. */
103
+ export declare function signReviewCapture(input: {
104
+ prompt: string;
105
+ response: string;
106
+ sessionId?: string | null;
107
+ toolName?: string;
108
+ agentType?: string;
109
+ capturedAt: string;
110
+ }, key: Buffer): ReviewCaptureRecordV1;
111
+ export declare function reviewCaptureFileName(record: Pick<ReviewCaptureRecordV1, "nonce" | "responseSha256">): string;
112
+ /** Checks one record's shape, signature, hashes and rubber-duck origin. Does not check the issued request. */
113
+ export declare function verifyReviewCapture(value: unknown, key: Buffer): ReviewCaptureRecordV1;
114
+ export interface ReviewCaptureFile {
115
+ path: string;
116
+ /** Parsed JSON, or undefined when the file is unreadable or not JSON. */
117
+ value: unknown;
118
+ /** The bounded raw file bytes, kept so even a malformed capture can be archived as evidence. */
119
+ bytes?: Buffer;
120
+ }
121
+ export declare const REJECTED_REVIEW_CAPTURE_SCHEMA = "apex.rejected-review-capture.v1";
122
+ /**
123
+ * A rejected capture file as JSON audit evidence: the exact UTF-8 text with the hash and size of the original bytes.
124
+ * Run objects must stay JSON so state transfer can carry them and check them for secrets.
125
+ */
126
+ export interface RejectedReviewCaptureV1 {
127
+ schema: typeof REJECTED_REVIEW_CAPTURE_SCHEMA;
128
+ sha256: string;
129
+ bytes: number;
130
+ content: string;
131
+ }
132
+ /** Wraps capture bytes for the object store, or returns undefined when they are not valid UTF-8 (quarantine those). */
133
+ export declare function rejectedReviewCapture(bytes: Buffer): RejectedReviewCaptureV1 | undefined;
134
+ /**
135
+ * Moves captures that could not be archived (oversized, unreadable, not UTF-8 or not regular files) into `quarantine/` in the
136
+ * review home, so the only copy of rejected evidence is kept for inspection rather than deleted.
137
+ */
138
+ export declare function quarantineReviewCaptures(files: readonly ReviewCaptureFile[], home?: string): Promise<{
139
+ quarantined: Array<{
140
+ name: string;
141
+ path: string;
142
+ }>;
143
+ failed: string[];
144
+ }>;
145
+ /** Reads every capture file for a nonce. Unreadable or non-JSON files are returned as malformed values. */
146
+ export declare function loadReviewCaptures(nonce: string, home?: string): Promise<ReviewCaptureFile[]>;
147
+ /**
148
+ * Removes ingested, rejected or cancelled captures, retrying transient errors (a scanner or indexer holding the file on
149
+ * Windows). Returns the paths that still could not be removed; `sweepReviewCaptures` removes them on a later call.
150
+ */
151
+ export declare function removeReviewCaptures(files: readonly ReviewCaptureFile[]): Promise<string[]>;
152
+ /**
153
+ * Rehashes the files a review prompt lists. Rubber-duck reads these files, so a missing, replaced or edited file means
154
+ * its answer may not be about the issued request; that fails closed.
155
+ */
156
+ export declare function verifyReviewFiles(files: readonly Pick<ReviewPromptFile, "path" | "sha256">[]): Promise<void>;
157
+ export interface IssuedReviewRequest {
158
+ nonce: string;
159
+ promptSha256: string;
160
+ }
161
+ /** Missing and ambiguous checks, which need no key, so an unwritable review home still reports a missing capture. */
162
+ export declare function assertSingleReviewCapture(files: readonly ReviewCaptureFile[]): void;
163
+ /**
164
+ * Selects and verifies the capture for an issued request. Exactly one capture may exist for the nonce, and its prompt
165
+ * must be the issued prompt (line endings and trailing whitespace aside). Two captures mean rubber-duck ran twice for
166
+ * one request, so the agent could pick a favourable answer; that fails closed.
167
+ */
168
+ export declare function verifyIssuedReviewCapture(request: IssuedReviewRequest, files: readonly ReviewCaptureFile[], key: Buffer): ReviewCaptureRecordV1;
169
+ export {};
170
+ //# sourceMappingURL=review-capture.d.ts.map