review-council 3.1.0 → 3.2.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.
package/README.md CHANGED
@@ -91,6 +91,7 @@ Review a PR, a local diff, or uncommitted work.
91
91
  | `--expect-head-sha <sha>` | Fail fast unless the resolved head commit equals this SHA |
92
92
  | `--spec-source <source>` | Where `--spec` came from: `flag`, `repo_file`, or `harness_issue:<ID>` |
93
93
  | `--converge-target <key>` / `--round <n>` / `--attempt <n>` | Converge context recorded in the report (or `RCL_CONVERGE_TARGET` / `_ROUND` / `_ATTEMPT`) |
94
+ | `--attest` | GitHub Actions gate workflow only: exchange the job's OIDC token for a run-bound Harness credential and record the review as attested (see below) |
94
95
  | `--config <path>` | Path to a config file |
95
96
 
96
97
  `--role`, `--roles`, and `--reviewer` are mutually exclusive. So are a positional target, `--staged`, and `--working-tree` — pick exactly one review source. Untracked files are invisible to `git diff` and therefore not reviewed.
@@ -292,6 +293,59 @@ harness:
292
293
 
293
294
  ---
294
295
 
296
+ ### `--attest`: attested reviews from the gate workflow
297
+
298
+ Only evidence recorded from the organization's own gate workflow on GitHub
299
+ Actions counts for the enforced gate (epic IO-12475, section 4.1). Inside
300
+ such a job — one that grants `id-token: write` — `rcl review owner/repo#N
301
+ --attest` asks the runner for the job's OIDC token with the Harness origin as
302
+ audience, exchanges it at `POST /api/v1/reviews/attest` for a **run-bound
303
+ credential** (`rbc_…`, thirty minutes, one rcl run id, valid while the
304
+ Actions run is in progress), and records the review under it: the envelope,
305
+ its artifacts, the model keys and the model stats all travel with that
306
+ credential and nothing else. Harness verifies the token, requires the
307
+ workflow file to be on the organization's gate allow-list at its default
308
+ branch, re-reads the pull request through its GitHub App and stores the run
309
+ only if the reviewed head is the pull request's current head and the PR is
310
+ not from a fork — the run is then `credential_kind: attested`.
311
+
312
+ `--attest` fails loudly, before any token is requested or any reviewer is
313
+ paid: outside Actions (no `ACTIONS_ID_TOKEN_REQUEST_URL` / `_TOKEN`), without
314
+ `HARNESS_API_URL`, off a pull request target, with a telemetry level other
315
+ than `full` (from `RCL_TELEMETRY` or the project config — an attested run
316
+ carries its full report), or when Harness refuses the exchange (the refusal
317
+ names the reason: `workflow_not_allowed`, `reviews_disabled`,
318
+ `run_not_in_progress`, …). It never falls back to `HARNESS_API_TOKEN` or the
319
+ stored login, it implies `--evidence-required`, and nothing recorded under the
320
+ run-bound credential is ever spooled — the credential does not outlive the
321
+ workflow run. A review that outlasts most of the credential's thirty minutes
322
+ mints it again for the same run id before delivery. Pair it with
323
+ `--expect-head-sha` so a moved pull request fails fast instead of being
324
+ refused at ingest.
325
+
326
+ ```yaml
327
+ # .github/workflows/review_gate.yml (dispatched by Harness for one pull request)
328
+ permissions:
329
+ id-token: write
330
+ contents: read
331
+ jobs:
332
+ review:
333
+ runs-on: ubuntu-latest
334
+ env:
335
+ HARNESS_API_URL: https://harness.infra.one
336
+ steps:
337
+ - run: npm i -g review-council
338
+ # Inputs reach the shell through the environment, never by expression
339
+ # interpolation into the command line.
340
+ - env:
341
+ REPO: ${{ inputs.repo }}
342
+ PR: ${{ inputs.pr }}
343
+ HEAD_SHA: ${{ inputs.head_sha }}
344
+ run: rcl review "$REPO#$PR" --attest --expect-head-sha "$HEAD_SHA" --ci
345
+ ```
346
+
347
+ ---
348
+
295
349
  ### `rcl evidence status` and `rcl evidence show`
296
350
 
297
351
  What Harness holds — never the client's own claim. `rcl evidence status`
@@ -540,7 +594,8 @@ For the full algorithm, see [CONSENSUS_V2_SPEC.md](./CONSENSUS_V2_SPEC.md).
540
594
  | `RCL_NO_HARNESS_KEYS` | Set to any value to disable Harness key distribution (below) |
541
595
  | `RCL_TELEMETRY` | `off` keeps every review on the machine (see `rcl telemetry`) |
542
596
  | `HARNESS_API_TOKEN` | CI credential for evidence delivery; requires `HARNESS_API_URL` — never pairs with the stored login host |
543
- | `HARNESS_API_URL` | The Harness host `HARNESS_API_TOKEN` was minted by |
597
+ | `HARNESS_API_URL` | The Harness host `HARNESS_API_TOKEN` was minted by; under `--attest` the host attested to (no token needed) |
598
+ | `ACTIONS_ID_TOKEN_REQUEST_URL` / `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | Set by the GitHub Actions runner for jobs with `id-token: write`; `--attest` reads them and refuses to run without them |
544
599
 
545
600
  The default blocking council is direct-API only (Anthropic, OpenAI, Google) —
546
601
  no default review round ever waits on an OpenRouter-routed call. The default
@@ -10,6 +10,15 @@ export interface HarnessKeyOptions {
10
10
  env?: Record<string, string | undefined>;
11
11
  credentialsPath?: string;
12
12
  fetchImpl?: typeof fetch;
13
+ /**
14
+ * A credential already in hand — the run-bound one `rcl review --attest`
15
+ * minted (RCL-40). Used instead of the stored login, and the attestation
16
+ * stands in for the repository's `.harness-cli/config.json`.
17
+ */
18
+ credential?: {
19
+ url: string;
20
+ token: string;
21
+ };
13
22
  }
14
23
  /** Walk up from `startDir` looking for `.harness-cli/config.json`. */
15
24
  export declare function findHarnessRepoConfig(startDir: string): string | null;
@@ -121,9 +121,10 @@ export async function applyHarnessModelKeys(options = {}) {
121
121
  const missing = missingProviders(env);
122
122
  if (missing.length === 0)
123
123
  return { injected: [] };
124
- if (findHarnessRepoConfig(options.cwd ?? process.cwd()) === null)
124
+ if (options.credential === undefined && findHarnessRepoConfig(options.cwd ?? process.cwd()) === null)
125
125
  return { injected: [] };
126
- const credentials = await readStoredCredentials(options.credentialsPath ?? defaultCredentialsPath(env));
126
+ const credentials = options.credential ??
127
+ (await readStoredCredentials(options.credentialsPath ?? defaultCredentialsPath(env)));
127
128
  if (credentials === null) {
128
129
  return {
129
130
  injected: [],
@@ -1 +1 @@
1
- {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/config/harness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/config/harness.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -41,8 +41,10 @@ import { assertExpectedHead, resolveReviewTarget } from './resolver/target.js';
41
41
  import { createTelemetryRuntime, deliverRun, evidenceRequirementConflict, emitConvergeEvents, flushOutbox, flushOutboxAtStart, loadHarnessSettings, resolveTelemetryLevel, } from './telemetry/deliver.js';
42
42
  import { sanitizeForDelivery } from './telemetry/envelope.js';
43
43
  import { scrubText } from './telemetry/scrub.js';
44
- import { buildEvent } from './telemetry/events.js';
44
+ import { buildEvent, roundIdentities } from './telemetry/events.js';
45
45
  import { credentialHost } from './telemetry/credentials.js';
46
+ import { attestRun, renewAttestation } from './telemetry/attest.js';
47
+ import { uuidv7 } from './report/uuid.js';
46
48
  import { runEvidenceStatus } from './evidence/status.js';
47
49
  import { runEvidenceShow } from './evidence/show.js';
48
50
  import { fetchServerModelStats, loadMergedWeights, mergeWeights } from './models/server-stats.js';
@@ -96,10 +98,15 @@ function assertEvidenceCanBeRequired(opts, config) {
96
98
  * reviewer is paid: an unmanaged repository or a missing credential makes
97
99
  * delivery impossible, and that is worth knowing at once.
98
100
  */
99
- async function assertEvidenceDeliverable(opts, config) {
101
+ async function assertEvidenceDeliverable(opts, config, credential) {
100
102
  if (!opts.evidenceRequired)
101
103
  return;
102
- const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION, config, noTelemetry: opts.telemetry === false });
104
+ const runtime = await createTelemetryRuntime({
105
+ rclVersion: RCL_VERSION,
106
+ config,
107
+ noTelemetry: opts.telemetry === false,
108
+ ...(credential ? { credential } : {}),
109
+ });
103
110
  if (!runtime.repoManaged) {
104
111
  throw new Error('--evidence-required needs a Harness-managed repository (one carrying .harness-cli/config.json); this one is not.');
105
112
  }
@@ -108,6 +115,34 @@ async function assertEvidenceDeliverable(opts, config) {
108
115
  if (!runtime.sink)
109
116
  throw new Error(`--evidence-required needs a Harness credential: ${runtime.note ?? 'none available'}.`);
110
117
  }
118
+ /** `--attest` records the full report or nothing: the resolved telemetry level must be `full`. */
119
+ function attestLevelMessage(level) {
120
+ return `--attest needs the telemetry level full (resolved: ${level}): an attested run carries its full report as evidence. Check RCL_TELEMETRY and harness.telemetry in the project config.`;
121
+ }
122
+ /**
123
+ * `--attest` (RCL-40), before any key, config or reviewer work: the target
124
+ * must be a pull request, the telemetry level must resolve to `full` from the
125
+ * environment and the project config (the file `--config` names included),
126
+ * and only then is the job's OIDC token requested and exchanged. An attested
127
+ * review is recorded or it does not run; the caller treats evidence as
128
+ * required from here on.
129
+ */
130
+ async function attestBeforeReview(spinner, opts, target, gitMode) {
131
+ if (gitMode || target === undefined || !isGitHubTarget(target)) {
132
+ throw new Error('--attest applies to a pull request target (owner/repo#N or a GitHub PR URL): Harness attests a run of the pull request it re-reads, not a local diff or a patch file.');
133
+ }
134
+ if (opts.telemetry === false) {
135
+ throw new Error('--attest contradicts --no-telemetry: an attested review is recorded or it does not run.');
136
+ }
137
+ const level = resolveTelemetryLevel(await loadHarnessSettings(process.cwd(), opts.config), { noTelemetry: false }, process.env);
138
+ if (level !== 'full')
139
+ throw new Error(attestLevelMessage(level));
140
+ spinner.text = 'Attesting to Harness as this GitHub Actions run...';
141
+ const attestation = await attestRun({ runId: uuidv7(), rclVersion: RCL_VERSION });
142
+ spinner.info(`Attested: run-bound credential from ${credentialHost(attestation.credential)} for run ${attestation.runId} (expires ${attestation.expiresAt})`);
143
+ spinner.start('Loading configuration...');
144
+ return attestation;
145
+ }
111
146
  /** Converge commands report their events fail-soft; nothing they do depends on it. */
112
147
  async function reportConvergeEvents(events) {
113
148
  try {
@@ -154,6 +189,7 @@ program
154
189
  .option('--no-telemetry', 'Do not deliver this review as evidence to Harness')
155
190
  .option('--evidence-required', 'Exit 4 unless Harness acknowledged the evidence (spools first; retry with rcl telemetry flush)')
156
191
  .option('--config <path>', 'Path to config file')
192
+ .option('--attest', 'GitHub Actions gate workflow only: exchange the job OIDC token for a run-bound Harness credential and record this review as attested (needs id-token: write and HARNESS_API_URL; implies --evidence-required; no fallback to another credential)')
157
193
  .action(async (target, opts) => {
158
194
  await runReview(target, opts);
159
195
  });
@@ -361,7 +397,15 @@ program
361
397
  convergeTarget: opts.target,
362
398
  round,
363
399
  ...(runId !== undefined ? { runId } : {}),
364
- payload: { round, round_cap: result.roundCap, counts: result.counts, actionable_gating: actionable.length },
400
+ payload: {
401
+ round,
402
+ round_cap: result.roundCap,
403
+ counts: result.counts,
404
+ actionable_gating: actionable.length,
405
+ // Which identity each sighting was matched to, so the server
406
+ // can apply standing verdicts to keys that moved (IO-12601).
407
+ identities: roundIdentities(result.findings),
408
+ },
365
409
  }),
366
410
  ...(maxRounds !== undefined
367
411
  ? [buildEvent({ kind: 'cap_changed', convergeTarget: opts.target, round, payload: { kind: 'rounds', to: result.roundCap } })]
@@ -901,19 +945,19 @@ rolesCmd
901
945
  * degradation (dropping openrouter models without OPENROUTER_API_KEY) must
902
946
  * see any injected keys.
903
947
  */
904
- async function fetchHarnessKeys(spinner) {
905
- const { note } = await applyHarnessModelKeys();
948
+ async function fetchHarnessKeys(spinner, credential) {
949
+ const { note } = await applyHarnessModelKeys(credential ? { credential } : {});
906
950
  if (note) {
907
951
  spinner.info(note);
908
952
  spinner.start('Loading configuration...');
909
953
  }
910
954
  }
911
- async function prepareCouncil(spinner, opts, fallbackRoles) {
955
+ async function prepareCouncil(spinner, opts, fallbackRoles, attestation) {
912
956
  const startedAt = new Date();
913
957
  // Validate the converge context first: a bad --round must fail before any
914
958
  // model time is spent, not after the council has run.
915
959
  const converge = resolveConvergeContext({ convergeTarget: opts.convergeTarget, round: opts.round, attempt: opts.attempt }, process.env);
916
- await fetchHarnessKeys(spinner);
960
+ await fetchHarnessKeys(spinner, attestation?.credential);
917
961
  const config = await loadConfig(opts.config);
918
962
  // Validate mutually exclusive role options
919
963
  const roleOptionCount = [opts.role, opts.roles, opts.reviewer?.length].filter(Boolean).length;
@@ -1090,11 +1134,24 @@ async function runReview(target, opts) {
1090
1134
  if (opts.evidenceRequired && patchTarget && opts.headSha === undefined) {
1091
1135
  throw new Error('--evidence-required needs --head-sha for a patch file: evidence must bind to the commit it reviewed.');
1092
1136
  }
1137
+ // --attest (RCL-40) runs before any key, config or reviewer work; an
1138
+ // attested review is recorded or it does not run, so evidence is required.
1139
+ let attestation;
1140
+ if (opts.attest) {
1141
+ attestation = await attestBeforeReview(spinner, opts, target, gitMode);
1142
+ opts = { ...opts, evidenceRequired: true };
1143
+ }
1093
1144
  assertEvidenceCanBeRequired(opts);
1094
- const prepared = await prepareCouncil(spinner, opts);
1145
+ const prepared = await prepareCouncil(spinner, opts, undefined, attestation);
1095
1146
  const { config } = prepared;
1147
+ if (attestation) {
1148
+ // The loaded config is the authority; the pre-exchange check read the same file.
1149
+ const level = resolveTelemetryLevel(config, { noTelemetry: opts.telemetry === false }, process.env);
1150
+ if (level !== 'full')
1151
+ throw new Error(attestLevelMessage(level));
1152
+ }
1096
1153
  assertEvidenceCanBeRequired(opts, config);
1097
- await assertEvidenceDeliverable(opts, config);
1154
+ await assertEvidenceDeliverable(opts, config, attestation?.credential);
1098
1155
  spinner.text = `Resolving diff for: ${target ?? `--${gitMode}`}`;
1099
1156
  // Resolve diff. Git modes bracket the read with two HEAD resolutions: a
1100
1157
  // commit landing between them would bind the diff to a commit it was
@@ -1165,6 +1222,7 @@ async function runReview(target, opts) {
1165
1222
  command: 'review',
1166
1223
  target: runTarget,
1167
1224
  asyncTargetLabel,
1225
+ ...(attestation ? { attestation } : {}),
1168
1226
  });
1169
1227
  }
1170
1228
  catch (err) {
@@ -1345,10 +1403,29 @@ async function executeCouncil(spinner, prepared, diff, opts, extra) {
1345
1403
  // Telemetry off means nothing leaves the machine for weights either. The
1346
1404
  // server side is bounded to three seconds; the settings file and the local
1347
1405
  // store are read as local files.
1406
+ // An attested review may outlast its credential: from here on — the
1407
+ // weights read, then delivery — the credential is renewed for the same run
1408
+ // id when little of it remains (RCL-40).
1409
+ let attestation = extra.attestation;
1410
+ if (attestation) {
1411
+ const renewal = await renewAttestation(attestation, { rclVersion: RCL_VERSION });
1412
+ attestation = renewal.attestation;
1413
+ if (renewal.renewed) {
1414
+ process.stderr.write(chalk.dim(`Attestation renewed for run ${attestation.runId} (expires ${attestation.expiresAt})`) + '\n');
1415
+ }
1416
+ else if (renewal.failure !== undefined) {
1417
+ process.stderr.write(chalk.dim(`Attestation not renewed (${renewal.failure}); continuing with the credential in hand`) + '\n');
1418
+ }
1419
+ }
1348
1420
  let modelWeights;
1349
1421
  try {
1350
- const level = resolveTelemetryLevel(await loadHarnessSettings(process.cwd()), { noTelemetry: opts.telemetry === false }, process.env);
1351
- const loaded = await loadMergedWeights({ rclVersion: RCL_VERSION, timeoutMs: 3_000, serverEnabled: level !== 'off' });
1422
+ const level = resolveTelemetryLevel(await loadHarnessSettings(process.cwd(), opts.config), { noTelemetry: opts.telemetry === false }, process.env);
1423
+ const loaded = await loadMergedWeights({
1424
+ rclVersion: RCL_VERSION,
1425
+ timeoutMs: 3_000,
1426
+ serverEnabled: level !== 'off',
1427
+ ...(attestation ? { credential: attestation.credential } : {}),
1428
+ });
1352
1429
  if (loaded.size > 0)
1353
1430
  modelWeights = loaded;
1354
1431
  }
@@ -1442,6 +1519,8 @@ async function executeCouncil(spinner, prepared, diff, opts, extra) {
1442
1519
  // Self-describing run header (IO-12475 section 5.1), built once the body
1443
1520
  // exists so the CI verdict is recorded uniformly — with or without --ci.
1444
1521
  const run = buildRunHeader({
1522
+ // An attested run carries the id its credential was minted for.
1523
+ ...(extra.attestation ? { id: extra.attestation.runId } : {}),
1445
1524
  rclVersion: RCL_VERSION,
1446
1525
  command: extra.command,
1447
1526
  target: extra.target,
@@ -1493,6 +1572,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra) {
1493
1572
  rclVersion: RCL_VERSION,
1494
1573
  config,
1495
1574
  noTelemetry: opts.telemetry === false,
1575
+ ...(attestation ? { credential: attestation.credential } : {}),
1496
1576
  });
1497
1577
  }
1498
1578
  catch (err) {