@allocator-one/rcl 4.5.21 → 4.5.22

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.
@@ -0,0 +1,76 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open } from 'node:fs/promises';
3
+ import { z } from 'zod';
4
+ import { readStable, sha256 } from '../telemetry/recovery/files.js';
5
+ /**
6
+ * A pull request's live state, read once by a parent process that holds
7
+ * GitHub credentials and handed to a review process that holds none (RCL-216).
8
+ *
9
+ * Grid hosts strip GitHub credentials from every reviewer process on purpose,
10
+ * yet a review cycle must confirm that the pull request is still open at the
11
+ * head under review. The parent captures that state with `rcl pr-snapshot`
12
+ * shortly before launching, and the review reads it with `--pr-snapshot`
13
+ * instead of calling GitHub. Harness still verifies the reviewed head against
14
+ * the pull request when the evidence arrives; this snapshot only replaces the
15
+ * reviewer's own pre-launch read.
16
+ */
17
+ export const PR_SNAPSHOT_MAX_AGE_MS = 15 * 60 * 1000;
18
+ const MAX_FUTURE_SKEW_MS = 60 * 1000;
19
+ const MAX_SNAPSHOT_BYTES = 16 * 1024;
20
+ export const prSnapshotSchema = z.object({
21
+ kind: z.literal('rcl-pr-snapshot'),
22
+ version: z.literal(1),
23
+ repo: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
24
+ prNumber: z.number().int().positive().safe(),
25
+ headSha: z.string().regex(/^[a-f0-9]{40}$/),
26
+ baseSha: z.string().regex(/^[a-f0-9]{40}$/),
27
+ baseRef: z.string().min(1).max(255),
28
+ merged: z.boolean(),
29
+ state: z.string().min(1).max(32),
30
+ fetchedAt: z.string().datetime({ offset: true }),
31
+ }).strict();
32
+ /** The fields a snapshot keeps from GitHub's pull request object. */
33
+ export function prSnapshotFrom(repo, prNumber, pr, now = new Date()) {
34
+ return prSnapshotSchema.parse({ kind: 'rcl-pr-snapshot', version: 1, repo: repo.toLowerCase(), prNumber,
35
+ headSha: pr.head.sha, baseSha: pr.base.sha, baseRef: pr.base.ref, merged: pr.merged === true, state: pr.state,
36
+ fetchedAt: now.toISOString() });
37
+ }
38
+ /** Write a new snapshot exclusively, readable by its owner only; answer its SHA-256. */
39
+ export async function writePrSnapshot(path, snapshot) {
40
+ const bytes = Buffer.from(`${JSON.stringify(prSnapshotSchema.parse(snapshot), null, 2)}\n`);
41
+ const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
42
+ try {
43
+ await handle.writeFile(bytes);
44
+ await handle.sync();
45
+ }
46
+ finally {
47
+ await handle.close();
48
+ }
49
+ return sha256(bytes);
50
+ }
51
+ /**
52
+ * Read a snapshot for `repo#prNumber` and answer the live state it records.
53
+ * Refuses a file another user could have written, one for another pull
54
+ * request, and one older than {@link PR_SNAPSHOT_MAX_AGE_MS}: an old reading
55
+ * cannot stand in for the live check.
56
+ */
57
+ export async function readPrSnapshot(path, repo, prNumber, now = Date.now()) {
58
+ const stat = await lstat(path);
59
+ if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== 'win32' &&
60
+ (stat.uid !== process.geteuid?.() || (stat.mode & 0o022) !== 0)))
61
+ throw new Error('pr_snapshot_unsafe_file');
62
+ let snapshot;
63
+ try {
64
+ snapshot = prSnapshotSchema.parse(JSON.parse((await readStable(path, MAX_SNAPSHOT_BYTES)).text));
65
+ }
66
+ catch {
67
+ throw new Error('pr_snapshot_invalid');
68
+ }
69
+ if (snapshot.repo.toLowerCase() !== repo.toLowerCase() || snapshot.prNumber !== prNumber)
70
+ throw new Error('pr_snapshot_pull_request_mismatch');
71
+ const age = now - Date.parse(snapshot.fetchedAt);
72
+ if (age > PR_SNAPSHOT_MAX_AGE_MS || age < -MAX_FUTURE_SKEW_MS)
73
+ throw new Error('pr_snapshot_stale');
74
+ return { headSha: snapshot.headSha, merged: snapshot.merged, state: snapshot.state };
75
+ }
76
+ //# sourceMappingURL=pr-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pr-snapshot.js","sourceRoot":"","sources":["../../src/converge/pr-snapshot.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ import { guardedDeliveryState } from './telemetry/deliver.js';
4
4
  import { AmbiguousReviewerIdentityError, assertUnambiguousReviewerIdentities } from './dispatch/reviewer-identity.js';
5
5
  import { retainAssemblyRefusal } from './output/assembly-refusal.js';
6
6
  import { createReviewCycleRemote } from './converge/cycle-remote.js';
7
+ import { prSnapshotFrom, readPrSnapshot, writePrSnapshot } from './converge/pr-snapshot.js';
8
+ import { applyFreshReviewClose, previewFreshReviewClose } from './converge/fresh-review-close.js';
7
9
  import { assertNoPendingFreshReview, freshReviewOutputPaths } from './converge/fresh-review.js';
8
10
  import { previewStaleReport, applyStaleReport } from './converge/stale-report.js';
9
11
  import { previewHistoricalDeliveryReconciliation, applyHistoricalDeliveryReconciliation } from './converge/historical-delivery-reconciliation.js';
@@ -239,6 +241,7 @@ program
239
241
  .option('--round <n>', 'Converge round number (or RCL_CONVERGE_ROUND)')
240
242
  .option('--attempt <n>', 'Converge attempt number (or RCL_CONVERGE_ATTEMPT)')
241
243
  .option('--start-over', 'Start an explicitly requested fresh review with a new normal budget; retain all prior evidence and spending')
244
+ .option('--pr-snapshot <path>', 'Live pull request state from `rcl pr-snapshot`, captured by a parent that holds GitHub credentials; review cycles read it instead of GitHub (RCL-216)')
242
245
  .option('--guarded-converge', 'Validate and claim inside this review process, or consume an exact recovered-v3 --attempt; derive the round from native state')
243
246
  .option('--bound-fix-recovery <run-id>', 'Review unchanged inputs once live Harness evidence proves a retained bound fix obligation after this native dismissal-only run')
244
247
  .option('--launch-intent <intent>', 'Guarded intent: review, stop-upstream, stop-review, or retry-delivery')
@@ -519,6 +522,71 @@ program
519
522
  process.exitCode = 3;
520
523
  }
521
524
  });
525
+ program
526
+ .command('pr-snapshot <pr>')
527
+ .description('Capture a pull request\'s live state (head, base, open/merged) for a review process that holds no GitHub ' +
528
+ 'credentials: run it in the parent, then pass the file to `rcl review --pr-snapshot` within 15 minutes (RCL-216)')
529
+ .requiredOption('--out <path>', 'New file to write (refuses an existing one); readable by its owner only')
530
+ .option('--config <path>', 'Path to config file')
531
+ .option('--json')
532
+ .action(async (pr, opts) => {
533
+ try {
534
+ if (!isGitHubTarget(pr))
535
+ throw new Error('pr_snapshot_requires_pull_request');
536
+ const target = parseGitHubTarget(pr);
537
+ const config = await loadConfig(opts.config);
538
+ const { data } = await getGitHubPullRequest(await createGitHubClient(config.githubToken), target);
539
+ const snapshot = prSnapshotFrom(`${target.owner}/${target.repo}`, target.number, data);
540
+ const digest = await writePrSnapshot(opts.out, snapshot);
541
+ console.log(opts.json ? JSON.stringify({ snapshot, path: opts.out, sha256: digest })
542
+ : `Wrote ${opts.out}: ${snapshot.repo}#${snapshot.prNumber} ${snapshot.state} at ${snapshot.headSha} (sha256 ${digest})`);
543
+ }
544
+ catch (error) {
545
+ console.error(JSON.stringify({ error: { code: 'RCL_PR_SNAPSHOT', message: error instanceof Error ? error.message : String(error) } }));
546
+ process.exitCode = 3;
547
+ }
548
+ });
549
+ program
550
+ .command('converge-close-fresh')
551
+ .description('Preview or apply an audited close of a fresh-review operation whose launch died before writing any report; ' +
552
+ 'never creates a round, finding, verdict or attempt (RCL-216)')
553
+ .option('--preview')
554
+ .option('--apply')
555
+ .option('--target <target>')
556
+ .option('--operation <uuid>', 'The fresh-review operation to close (its native current operation)')
557
+ .requiredOption('--manifest <path>', 'Exclusive preview manifest; the reviewed manifest for apply')
558
+ .option('--manifest-sha256 <sha256>', 'Exact reviewed manifest bytes; required for apply')
559
+ .option('--json')
560
+ .action(async (opts) => {
561
+ try {
562
+ if ([opts.preview, opts.apply].filter(Boolean).length !== 1)
563
+ throw new Error('choose_exactly_one_fresh_close_mode');
564
+ const gitCommonDir = await resolveGitCommonDir();
565
+ let result;
566
+ if (opts.preview) {
567
+ if (opts.manifestSha256 !== undefined)
568
+ throw new Error('preview_does_not_accept_manifest_digest');
569
+ if (typeof opts.target !== 'string' || typeof opts.operation !== 'string')
570
+ throw new Error('fresh_close_preview_arguments_required');
571
+ const manifest = await previewFreshReviewClose({ target: opts.target, operationId: opts.operation }, gitCommonDir);
572
+ await writeExclusive(opts.manifest, manifest, 256 * 1024);
573
+ result = { mode: 'preview', manifest, manifestSha256: sha256(serializeRecoveryDocument(manifest)), accounting: 'unchanged',
574
+ scope: 'local operation close; no admission or approval' };
575
+ }
576
+ else {
577
+ if (opts.target !== undefined || opts.operation !== undefined)
578
+ throw new Error('apply_uses_only_pinned_manifest');
579
+ result = { mode: 'apply', result: await applyFreshReviewClose({ manifest: opts.manifest,
580
+ manifestSha256: opts.manifestSha256 }, gitCommonDir), accounting: 'unchanged',
581
+ scope: 'local operation close; no admission or approval' };
582
+ }
583
+ console.log(JSON.stringify(result));
584
+ }
585
+ catch (error) {
586
+ console.error(JSON.stringify({ error: { code: 'RCL_CONVERGE_CLOSE_FRESH', message: error instanceof Error ? error.message : String(error) } }));
587
+ process.exitCode = 3;
588
+ }
589
+ });
522
590
  program
523
591
  .command('converge-gap')
524
592
  .description('Preview, apply or resume one evidenced missing-terminal-report audit gap; never creates a round, finding, verdict or attempt')
@@ -2022,6 +2090,10 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2022
2090
  throw new Error('Fresh review cycles require full Harness evidence and an actor credential');
2023
2091
  const prTarget = parseGitHubTarget(`${extra.target.repo}#${extra.target.prNumber}`);
2024
2092
  cycleRemote = createReviewCycleRemote(runtime.sink, extra.target.repo, extra.target.prNumber, opts.expectPrHeadSha ?? extra.target.headSha ?? '', async () => {
2093
+ // A parent with GitHub credentials hands the live state over, so this
2094
+ // review process needs none (RCL-216).
2095
+ if (opts.prSnapshot !== undefined)
2096
+ return readPrSnapshot(opts.prSnapshot, extra.target.repo, extra.target.prNumber);
2025
2097
  const client = await createGitHubClient(config.githubToken);
2026
2098
  const auth = await client.auth();
2027
2099
  if (typeof auth !== 'object' || auth === null || !('type' in auth) || auth.type !== 'token') {