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 +56 -1
- package/dist/config/harness.d.ts +9 -0
- package/dist/config/harness.js +3 -2
- package/dist/config/harness.js.map +1 -1
- package/dist/index.js +92 -12
- package/dist/index.js.map +1 -1
- package/dist/telemetry/attest.d.ts +79 -0
- package/dist/telemetry/attest.js +269 -0
- package/dist/telemetry/attest.js.map +1 -0
- package/dist/telemetry/credentials.d.ts +2 -1
- package/dist/telemetry/credentials.js.map +1 -1
- package/dist/telemetry/deliver.d.ts +15 -2
- package/dist/telemetry/deliver.js +32 -4
- package/dist/telemetry/deliver.js.map +1 -1
- package/dist/telemetry/events.d.ts +25 -0
- package/dist/telemetry/events.js +26 -0
- package/dist/telemetry/events.js.map +1 -1
- package/dist/telemetry/read-sink.d.ts +3 -0
- package/dist/telemetry/read-sink.js +8 -6
- package/dist/telemetry/read-sink.js.map +1 -1
- package/dist/telemetry/sink.d.ts +6 -0
- package/dist/telemetry/sink.js +2 -1
- package/dist/telemetry/sink.js.map +1 -1
- package/package.json +1 -1
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
|
package/dist/config/harness.d.ts
CHANGED
|
@@ -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;
|
package/dist/config/harness.js
CHANGED
|
@@ -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 =
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAExD;;;;;;;;;;;;;;;;;GAiBG;AAEH,6EAA6E;AAC7E,MAAM,iBAAiB,GAA2B;IAChD,SAAS,EAAE,mBAAmB;IAC9B,MAAM,EAAE,gBAAgB;IACxB,MAAM,EAAE,gBAAgB;IACxB,UAAU,EAAE,oBAAoB;CACjC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,KAAK,CAAC;
|
|
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({
|
|
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: {
|
|
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({
|
|
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) {
|