@openthink/stamp 2.1.0 → 2.1.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.
- package/dist/index.js +963 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10140,8 +10140,9 @@ function printReviewHistory(repoRoot, limit, diff) {
|
|
|
10140
10140
|
}
|
|
10141
10141
|
|
|
10142
10142
|
// src/commands/attest.ts
|
|
10143
|
-
import { spawnSync as
|
|
10143
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
10144
10144
|
import { createHash as createHash12 } from "crypto";
|
|
10145
|
+
import { parse as parseYaml11 } from "yaml";
|
|
10145
10146
|
|
|
10146
10147
|
// src/lib/patchId.ts
|
|
10147
10148
|
import { spawnSync as spawnSync12 } from "child_process";
|
|
@@ -10218,6 +10219,15 @@ function parseEnvelope(bytes) {
|
|
|
10218
10219
|
if (typeof p.manifest_snapshot_sha256 !== "string") return null;
|
|
10219
10220
|
if (!Array.isArray(p.trust_anchor_signatures)) return null;
|
|
10220
10221
|
if (typeof p.target_branch_tip_sha !== "string") return null;
|
|
10222
|
+
if (p.migration_bootstrap !== void 0) {
|
|
10223
|
+
const m = p.migration_bootstrap;
|
|
10224
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) return null;
|
|
10225
|
+
if (!Array.isArray(m.activated_paths)) return null;
|
|
10226
|
+
if (m.activated_paths.length === 0) return null;
|
|
10227
|
+
for (const ap of m.activated_paths) {
|
|
10228
|
+
if (typeof ap !== "string" || ap.length === 0) return null;
|
|
10229
|
+
}
|
|
10230
|
+
}
|
|
10221
10231
|
return env;
|
|
10222
10232
|
}
|
|
10223
10233
|
function peekSchemaVersion(bytes) {
|
|
@@ -10290,6 +10300,414 @@ function readAttestationBlobBytes(patch_id, repoRoot) {
|
|
|
10290
10300
|
return cat.stdout;
|
|
10291
10301
|
}
|
|
10292
10302
|
|
|
10303
|
+
// src/lib/migrationBootstrap.ts
|
|
10304
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
10305
|
+
import { parse as parseYaml10 } from "yaml";
|
|
10306
|
+
function validateShape4ActivationDiff(input) {
|
|
10307
|
+
const { repoRoot, baseSha, headSha } = input;
|
|
10308
|
+
const changedFilesResult = spawnSync14(
|
|
10309
|
+
"git",
|
|
10310
|
+
["diff", "-z", "--name-status", `${baseSha}...${headSha}`],
|
|
10311
|
+
{ cwd: repoRoot, encoding: "utf8" }
|
|
10312
|
+
);
|
|
10313
|
+
if (changedFilesResult.status !== 0) {
|
|
10314
|
+
return {
|
|
10315
|
+
ok: false,
|
|
10316
|
+
reason: `could not enumerate diff between ${baseSha.slice(0, 8)} and ${headSha.slice(0, 8)}: ${(changedFilesResult.stderr ?? "").trim()}`
|
|
10317
|
+
};
|
|
10318
|
+
}
|
|
10319
|
+
const tokens = (changedFilesResult.stdout ?? "").split("\0").filter((s) => s.length > 0);
|
|
10320
|
+
const entries = [];
|
|
10321
|
+
for (let i = 0; i < tokens.length; ) {
|
|
10322
|
+
const status = tokens[i];
|
|
10323
|
+
if (/^[RC]\d*$/.test(status)) {
|
|
10324
|
+
return {
|
|
10325
|
+
ok: false,
|
|
10326
|
+
reason: `bootstrap diff contains a rename/copy change (status "${status}") \u2014 only add/modify is permitted in a Shape 4 activation`
|
|
10327
|
+
};
|
|
10328
|
+
}
|
|
10329
|
+
const path2 = tokens[i + 1];
|
|
10330
|
+
if (path2 === void 0) {
|
|
10331
|
+
return {
|
|
10332
|
+
ok: false,
|
|
10333
|
+
reason: `bootstrap diff: malformed git output (status "${status}" with no path)`
|
|
10334
|
+
};
|
|
10335
|
+
}
|
|
10336
|
+
entries.push({ status, path: path2 });
|
|
10337
|
+
i += 2;
|
|
10338
|
+
}
|
|
10339
|
+
if (entries.length === 0) {
|
|
10340
|
+
return {
|
|
10341
|
+
ok: false,
|
|
10342
|
+
reason: "bootstrap diff is empty \u2014 nothing to activate"
|
|
10343
|
+
};
|
|
10344
|
+
}
|
|
10345
|
+
for (const e of entries) {
|
|
10346
|
+
if (!e.path.startsWith(".stamp/")) {
|
|
10347
|
+
return {
|
|
10348
|
+
ok: false,
|
|
10349
|
+
reason: `bootstrap diff touches "${e.path}" outside .stamp/ \u2014 the bootstrap flag accepts only Shape-4-activation changes under .stamp/**. Use the normal flow for non-bootstrap changes.`
|
|
10350
|
+
};
|
|
10351
|
+
}
|
|
10352
|
+
}
|
|
10353
|
+
let sawReviewServerAdd = false;
|
|
10354
|
+
for (const e of entries) {
|
|
10355
|
+
if (e.path === ".stamp/config.yml") {
|
|
10356
|
+
if (e.status !== "M" && e.status !== "A") {
|
|
10357
|
+
return {
|
|
10358
|
+
ok: false,
|
|
10359
|
+
reason: `bootstrap diff has unexpected status "${e.status}" on .stamp/config.yml \u2014 only modification (M) or initial add (A) is permitted`
|
|
10360
|
+
};
|
|
10361
|
+
}
|
|
10362
|
+
const cfgResult = validateConfigYamlChange({
|
|
10363
|
+
repoRoot,
|
|
10364
|
+
baseSha,
|
|
10365
|
+
headSha,
|
|
10366
|
+
statusAdd: e.status === "A"
|
|
10367
|
+
});
|
|
10368
|
+
if (!cfgResult.ok) return cfgResult;
|
|
10369
|
+
sawReviewServerAdd = sawReviewServerAdd || cfgResult.activatesReviewServer;
|
|
10370
|
+
continue;
|
|
10371
|
+
}
|
|
10372
|
+
if (e.path === ".stamp/trusted-keys/manifest.yml") {
|
|
10373
|
+
if (e.status !== "M" && e.status !== "A") {
|
|
10374
|
+
return {
|
|
10375
|
+
ok: false,
|
|
10376
|
+
reason: `bootstrap diff has unexpected status "${e.status}" on .stamp/trusted-keys/manifest.yml \u2014 only modification (M) or initial add (A) is permitted`
|
|
10377
|
+
};
|
|
10378
|
+
}
|
|
10379
|
+
const manifestResult = validateManifestChange({
|
|
10380
|
+
repoRoot,
|
|
10381
|
+
baseSha,
|
|
10382
|
+
headSha,
|
|
10383
|
+
statusAdd: e.status === "A"
|
|
10384
|
+
});
|
|
10385
|
+
if (!manifestResult.ok) return manifestResult;
|
|
10386
|
+
continue;
|
|
10387
|
+
}
|
|
10388
|
+
if (e.path.startsWith(".stamp/trusted-keys/") && e.path.endsWith(".pub")) {
|
|
10389
|
+
if (e.status !== "A") {
|
|
10390
|
+
return {
|
|
10391
|
+
ok: false,
|
|
10392
|
+
reason: `bootstrap diff modifies or removes "${e.path}" (status "${e.status}") \u2014 the bootstrap flag only permits adding new .pub files, never modifying or removing existing ones`
|
|
10393
|
+
};
|
|
10394
|
+
}
|
|
10395
|
+
continue;
|
|
10396
|
+
}
|
|
10397
|
+
return {
|
|
10398
|
+
ok: false,
|
|
10399
|
+
reason: `bootstrap diff touches "${e.path}" (status "${e.status}") which is not in the Shape-4-activation whitelist (.stamp/config.yml, .stamp/trusted-keys/manifest.yml, or new .stamp/trusted-keys/*.pub files)`
|
|
10400
|
+
};
|
|
10401
|
+
}
|
|
10402
|
+
if (!sawReviewServerAdd) {
|
|
10403
|
+
return {
|
|
10404
|
+
ok: false,
|
|
10405
|
+
reason: `bootstrap diff does not add review_server: to any branch rule in .stamp/config.yml \u2014 a Shape 4 activation must add review_server. If your diff only adds trust-anchor keys, use the normal admin-sign flow.`
|
|
10406
|
+
};
|
|
10407
|
+
}
|
|
10408
|
+
const activatedPaths = entries.map((e) => e.path).sort();
|
|
10409
|
+
return { ok: true, activatedPaths };
|
|
10410
|
+
}
|
|
10411
|
+
function validateConfigYamlChange(args) {
|
|
10412
|
+
if (args.statusAdd) {
|
|
10413
|
+
return {
|
|
10414
|
+
ok: false,
|
|
10415
|
+
reason: `.stamp/config.yml does not exist at base ${args.baseSha.slice(0, 8)} \u2014 the bootstrap flag is for activating Shape 4 on an existing stamp-gated repo, not for first-time stamp init`,
|
|
10416
|
+
activatesReviewServer: false
|
|
10417
|
+
};
|
|
10418
|
+
}
|
|
10419
|
+
const baseYaml = readAtRef(args.repoRoot, args.baseSha, ".stamp/config.yml");
|
|
10420
|
+
const headYaml = readAtRef(args.repoRoot, args.headSha, ".stamp/config.yml");
|
|
10421
|
+
if (baseYaml === null || headYaml === null) {
|
|
10422
|
+
return {
|
|
10423
|
+
ok: false,
|
|
10424
|
+
reason: `.stamp/config.yml is unreadable at base ${args.baseSha.slice(0, 8)} or head ${args.headSha.slice(0, 8)}`,
|
|
10425
|
+
activatesReviewServer: false
|
|
10426
|
+
};
|
|
10427
|
+
}
|
|
10428
|
+
let baseParsed;
|
|
10429
|
+
let headParsed;
|
|
10430
|
+
try {
|
|
10431
|
+
baseParsed = parseYaml10(baseYaml);
|
|
10432
|
+
headParsed = parseYaml10(headYaml);
|
|
10433
|
+
} catch (err) {
|
|
10434
|
+
return {
|
|
10435
|
+
ok: false,
|
|
10436
|
+
reason: `.stamp/config.yml parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
10437
|
+
activatesReviewServer: false
|
|
10438
|
+
};
|
|
10439
|
+
}
|
|
10440
|
+
if (!baseParsed || typeof baseParsed !== "object" || Array.isArray(baseParsed) || !headParsed || typeof headParsed !== "object" || Array.isArray(headParsed)) {
|
|
10441
|
+
return {
|
|
10442
|
+
ok: false,
|
|
10443
|
+
reason: `.stamp/config.yml at base or head did not parse to an object`,
|
|
10444
|
+
activatesReviewServer: false
|
|
10445
|
+
};
|
|
10446
|
+
}
|
|
10447
|
+
const baseObj = baseParsed;
|
|
10448
|
+
const headObj = headParsed;
|
|
10449
|
+
const allowedDiffKeys = /* @__PURE__ */ new Set(["branches", "reviewers"]);
|
|
10450
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(baseObj), ...Object.keys(headObj)]);
|
|
10451
|
+
for (const k of allKeys) {
|
|
10452
|
+
if (allowedDiffKeys.has(k)) continue;
|
|
10453
|
+
if (!deepEqual(baseObj[k], headObj[k])) {
|
|
10454
|
+
return {
|
|
10455
|
+
ok: false,
|
|
10456
|
+
reason: `.stamp/config.yml: bootstrap diff modifies "${k}" outside the allowed (branches, reviewers) set \u2014 refused`,
|
|
10457
|
+
activatesReviewServer: false
|
|
10458
|
+
};
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10461
|
+
const baseBranches = baseObj.branches ?? {};
|
|
10462
|
+
const headBranches = headObj.branches ?? {};
|
|
10463
|
+
if (typeof baseBranches !== "object" || baseBranches === null || Array.isArray(baseBranches) || typeof headBranches !== "object" || headBranches === null || Array.isArray(headBranches)) {
|
|
10464
|
+
return {
|
|
10465
|
+
ok: false,
|
|
10466
|
+
reason: `.stamp/config.yml: branches must be objects at both base and head`,
|
|
10467
|
+
activatesReviewServer: false
|
|
10468
|
+
};
|
|
10469
|
+
}
|
|
10470
|
+
const baseBranchKeys = Object.keys(baseBranches);
|
|
10471
|
+
const headBranchKeys = Object.keys(headBranches);
|
|
10472
|
+
if (baseBranchKeys.length !== headBranchKeys.length || !baseBranchKeys.every((k) => headBranchKeys.includes(k))) {
|
|
10473
|
+
return {
|
|
10474
|
+
ok: false,
|
|
10475
|
+
reason: `.stamp/config.yml: bootstrap diff adds or removes branch entries (base: [${baseBranchKeys.join(", ")}], head: [${headBranchKeys.join(", ")}]) \u2014 only review_server addition on existing branches is permitted`,
|
|
10476
|
+
activatesReviewServer: false
|
|
10477
|
+
};
|
|
10478
|
+
}
|
|
10479
|
+
let anyBranchActivatesReviewServer = false;
|
|
10480
|
+
for (const name of baseBranchKeys) {
|
|
10481
|
+
const baseRule = baseBranches[name];
|
|
10482
|
+
const headRule = headBranches[name];
|
|
10483
|
+
if (!baseRule || !headRule) {
|
|
10484
|
+
return {
|
|
10485
|
+
ok: false,
|
|
10486
|
+
reason: `.stamp/config.yml: branch "${name}" is missing at base or head`,
|
|
10487
|
+
activatesReviewServer: false
|
|
10488
|
+
};
|
|
10489
|
+
}
|
|
10490
|
+
if (Array.isArray(baseRule) || Array.isArray(headRule)) {
|
|
10491
|
+
return {
|
|
10492
|
+
ok: false,
|
|
10493
|
+
reason: `.stamp/config.yml: branch "${name}" must be an object`,
|
|
10494
|
+
activatesReviewServer: false
|
|
10495
|
+
};
|
|
10496
|
+
}
|
|
10497
|
+
const baseKeys = Object.keys(baseRule);
|
|
10498
|
+
const headKeys = Object.keys(headRule);
|
|
10499
|
+
for (const k of baseKeys) {
|
|
10500
|
+
if (!(k in headRule)) {
|
|
10501
|
+
return {
|
|
10502
|
+
ok: false,
|
|
10503
|
+
reason: `.stamp/config.yml: branch "${name}" removes field "${k}" \u2014 bootstrap diff cannot remove branch-rule fields`,
|
|
10504
|
+
activatesReviewServer: false
|
|
10505
|
+
};
|
|
10506
|
+
}
|
|
10507
|
+
if (!deepEqual(baseRule[k], headRule[k])) {
|
|
10508
|
+
return {
|
|
10509
|
+
ok: false,
|
|
10510
|
+
reason: `.stamp/config.yml: branch "${name}" modifies field "${k}" \u2014 bootstrap diff can only ADD review_server, not modify existing fields`,
|
|
10511
|
+
activatesReviewServer: false
|
|
10512
|
+
};
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
for (const k of headKeys) {
|
|
10516
|
+
if (k in baseRule) continue;
|
|
10517
|
+
if (k !== "review_server") {
|
|
10518
|
+
return {
|
|
10519
|
+
ok: false,
|
|
10520
|
+
reason: `.stamp/config.yml: branch "${name}" adds field "${k}" \u2014 bootstrap diff can only add "review_server"`,
|
|
10521
|
+
activatesReviewServer: false
|
|
10522
|
+
};
|
|
10523
|
+
}
|
|
10524
|
+
if (typeof headRule.review_server !== "string" || !headRule.review_server) {
|
|
10525
|
+
return {
|
|
10526
|
+
ok: false,
|
|
10527
|
+
reason: `.stamp/config.yml: branch "${name}".review_server must be a non-empty string`,
|
|
10528
|
+
activatesReviewServer: false
|
|
10529
|
+
};
|
|
10530
|
+
}
|
|
10531
|
+
anyBranchActivatesReviewServer = true;
|
|
10532
|
+
}
|
|
10533
|
+
}
|
|
10534
|
+
const baseReviewers = baseObj.reviewers ?? {};
|
|
10535
|
+
const headReviewers = headObj.reviewers ?? {};
|
|
10536
|
+
if (typeof baseReviewers !== "object" || baseReviewers === null || Array.isArray(baseReviewers) || typeof headReviewers !== "object" || headReviewers === null || Array.isArray(headReviewers)) {
|
|
10537
|
+
return {
|
|
10538
|
+
ok: false,
|
|
10539
|
+
reason: `.stamp/config.yml: reviewers must be objects at both base and head`,
|
|
10540
|
+
activatesReviewServer: false
|
|
10541
|
+
};
|
|
10542
|
+
}
|
|
10543
|
+
const baseRevKeys = Object.keys(baseReviewers).sort();
|
|
10544
|
+
const headRevKeys = Object.keys(headReviewers).sort();
|
|
10545
|
+
if (baseRevKeys.length !== headRevKeys.length || !baseRevKeys.every((k, i) => k === headRevKeys[i])) {
|
|
10546
|
+
return {
|
|
10547
|
+
ok: false,
|
|
10548
|
+
reason: `.stamp/config.yml: bootstrap diff adds or removes reviewer entries (base: [${baseRevKeys.join(", ")}], head: [${headRevKeys.join(", ")}]) \u2014 Shape 4 activation may only cleanup prompt paths, not add or remove reviewers`,
|
|
10549
|
+
activatesReviewServer: false
|
|
10550
|
+
};
|
|
10551
|
+
}
|
|
10552
|
+
for (const name of baseRevKeys) {
|
|
10553
|
+
const baseRev = baseReviewers[name];
|
|
10554
|
+
const headRev = headReviewers[name];
|
|
10555
|
+
if (deepEqual(baseRev, headRev)) continue;
|
|
10556
|
+
if (headRev !== null && typeof headRev === "object" && !Array.isArray(headRev) && Object.keys(headRev).length === 0) {
|
|
10557
|
+
continue;
|
|
10558
|
+
}
|
|
10559
|
+
return {
|
|
10560
|
+
ok: false,
|
|
10561
|
+
reason: `.stamp/config.yml: reviewer "${name}" modified outside the Shape-4 cleanup pattern (HEAD must equal BASE or be {}); refused`,
|
|
10562
|
+
activatesReviewServer: false
|
|
10563
|
+
};
|
|
10564
|
+
}
|
|
10565
|
+
return { ok: true, activatedPaths: [], activatesReviewServer: anyBranchActivatesReviewServer };
|
|
10566
|
+
}
|
|
10567
|
+
function validateManifestChange(args) {
|
|
10568
|
+
const headYaml = readAtRef(args.repoRoot, args.headSha, ".stamp/trusted-keys/manifest.yml");
|
|
10569
|
+
if (headYaml === null) {
|
|
10570
|
+
return {
|
|
10571
|
+
ok: false,
|
|
10572
|
+
reason: `.stamp/trusted-keys/manifest.yml unreadable at head ${args.headSha.slice(0, 8)}`
|
|
10573
|
+
};
|
|
10574
|
+
}
|
|
10575
|
+
let headParsed;
|
|
10576
|
+
try {
|
|
10577
|
+
headParsed = parseYaml10(headYaml);
|
|
10578
|
+
} catch (err) {
|
|
10579
|
+
return {
|
|
10580
|
+
ok: false,
|
|
10581
|
+
reason: `.stamp/trusted-keys/manifest.yml parse error at head: ${err instanceof Error ? err.message : String(err)}`
|
|
10582
|
+
};
|
|
10583
|
+
}
|
|
10584
|
+
if (!headParsed || typeof headParsed !== "object" || Array.isArray(headParsed) || !headParsed.keys || typeof headParsed.keys !== "object") {
|
|
10585
|
+
return {
|
|
10586
|
+
ok: false,
|
|
10587
|
+
reason: `.stamp/trusted-keys/manifest.yml: missing or malformed top-level "keys" map at head`
|
|
10588
|
+
};
|
|
10589
|
+
}
|
|
10590
|
+
const headKeys = headParsed.keys;
|
|
10591
|
+
let baseKeys = {};
|
|
10592
|
+
if (!args.statusAdd) {
|
|
10593
|
+
const baseYaml = readAtRef(args.repoRoot, args.baseSha, ".stamp/trusted-keys/manifest.yml");
|
|
10594
|
+
if (baseYaml === null) {
|
|
10595
|
+
return {
|
|
10596
|
+
ok: false,
|
|
10597
|
+
reason: `.stamp/trusted-keys/manifest.yml unreadable at base ${args.baseSha.slice(0, 8)}`
|
|
10598
|
+
};
|
|
10599
|
+
}
|
|
10600
|
+
let baseParsed;
|
|
10601
|
+
try {
|
|
10602
|
+
baseParsed = parseYaml10(baseYaml);
|
|
10603
|
+
} catch (err) {
|
|
10604
|
+
return {
|
|
10605
|
+
ok: false,
|
|
10606
|
+
reason: `.stamp/trusted-keys/manifest.yml parse error at base: ${err instanceof Error ? err.message : String(err)}`
|
|
10607
|
+
};
|
|
10608
|
+
}
|
|
10609
|
+
if (!baseParsed || typeof baseParsed !== "object" || Array.isArray(baseParsed) || !baseParsed.keys || typeof baseParsed.keys !== "object") {
|
|
10610
|
+
return {
|
|
10611
|
+
ok: false,
|
|
10612
|
+
reason: `.stamp/trusted-keys/manifest.yml: missing or malformed top-level "keys" map at base`
|
|
10613
|
+
};
|
|
10614
|
+
}
|
|
10615
|
+
baseKeys = baseParsed.keys;
|
|
10616
|
+
}
|
|
10617
|
+
for (const name of Object.keys(baseKeys)) {
|
|
10618
|
+
if (!(name in headKeys)) {
|
|
10619
|
+
return {
|
|
10620
|
+
ok: false,
|
|
10621
|
+
reason: `.stamp/trusted-keys/manifest.yml: bootstrap diff removes entry "${name}" \u2014 refused (only additions of [server]+role_source:server entries are permitted)`
|
|
10622
|
+
};
|
|
10623
|
+
}
|
|
10624
|
+
if (!deepEqual(baseKeys[name], headKeys[name])) {
|
|
10625
|
+
return {
|
|
10626
|
+
ok: false,
|
|
10627
|
+
reason: `.stamp/trusted-keys/manifest.yml: bootstrap diff modifies existing entry "${name}" \u2014 refused (only additions are permitted)`
|
|
10628
|
+
};
|
|
10629
|
+
}
|
|
10630
|
+
}
|
|
10631
|
+
for (const name of Object.keys(headKeys)) {
|
|
10632
|
+
if (name in baseKeys) continue;
|
|
10633
|
+
const entry = headKeys[name];
|
|
10634
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
10635
|
+
return {
|
|
10636
|
+
ok: false,
|
|
10637
|
+
reason: `.stamp/trusted-keys/manifest.yml: new entry "${name}" is not a YAML object`
|
|
10638
|
+
};
|
|
10639
|
+
}
|
|
10640
|
+
const e = entry;
|
|
10641
|
+
if (!Array.isArray(e.capabilities)) {
|
|
10642
|
+
return {
|
|
10643
|
+
ok: false,
|
|
10644
|
+
reason: `.stamp/trusted-keys/manifest.yml: new entry "${name}" missing capabilities array`
|
|
10645
|
+
};
|
|
10646
|
+
}
|
|
10647
|
+
const caps = e.capabilities.filter((c) => typeof c === "string");
|
|
10648
|
+
if (caps.length !== 1 || caps[0] !== "server") {
|
|
10649
|
+
return {
|
|
10650
|
+
ok: false,
|
|
10651
|
+
reason: `.stamp/trusted-keys/manifest.yml: new entry "${name}" has capabilities [${caps.join(", ")}] \u2014 bootstrap diff only permits adding entries with capabilities: [server] exactly`
|
|
10652
|
+
};
|
|
10653
|
+
}
|
|
10654
|
+
if (e.role_source !== "server") {
|
|
10655
|
+
return {
|
|
10656
|
+
ok: false,
|
|
10657
|
+
reason: `.stamp/trusted-keys/manifest.yml: new entry "${name}" must have role_source: server (got ${JSON.stringify(e.role_source)}). This invariant flags entries auto-published by stamp-server.`
|
|
10658
|
+
};
|
|
10659
|
+
}
|
|
10660
|
+
if (typeof e.fingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(e.fingerprint)) {
|
|
10661
|
+
return {
|
|
10662
|
+
ok: false,
|
|
10663
|
+
reason: `.stamp/trusted-keys/manifest.yml: new entry "${name}" has invalid fingerprint`
|
|
10664
|
+
};
|
|
10665
|
+
}
|
|
10666
|
+
}
|
|
10667
|
+
return { ok: true, activatedPaths: [] };
|
|
10668
|
+
}
|
|
10669
|
+
var MIGRATION_BOOTSTRAP_KEY = "migration_bootstrap";
|
|
10670
|
+
function bootstrapAdminSigningBytes(args) {
|
|
10671
|
+
const augmented = {
|
|
10672
|
+
...args.payloadV4,
|
|
10673
|
+
[MIGRATION_BOOTSTRAP_KEY]: args.marker,
|
|
10674
|
+
trust_anchor_signatures: []
|
|
10675
|
+
};
|
|
10676
|
+
return canonicalSerializePayload(augmented);
|
|
10677
|
+
}
|
|
10678
|
+
function readAtRef(repoRoot, ref, relPath) {
|
|
10679
|
+
const result = spawnSync14("git", ["show", `${ref}:${relPath}`], {
|
|
10680
|
+
cwd: repoRoot,
|
|
10681
|
+
encoding: "utf8"
|
|
10682
|
+
});
|
|
10683
|
+
if (result.status !== 0) return null;
|
|
10684
|
+
return result.stdout ?? "";
|
|
10685
|
+
}
|
|
10686
|
+
function deepEqual(a, b) {
|
|
10687
|
+
if (a === b) return true;
|
|
10688
|
+
if (a === null || b === null) return false;
|
|
10689
|
+
if (typeof a !== typeof b) return false;
|
|
10690
|
+
if (typeof a !== "object") return false;
|
|
10691
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
10692
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
10693
|
+
if (a.length !== b.length) return false;
|
|
10694
|
+
for (let i = 0; i < a.length; i++) {
|
|
10695
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
10696
|
+
}
|
|
10697
|
+
return true;
|
|
10698
|
+
}
|
|
10699
|
+
const ao = a;
|
|
10700
|
+
const bo = b;
|
|
10701
|
+
const aKeys = Object.keys(ao).sort();
|
|
10702
|
+
const bKeys = Object.keys(bo).sort();
|
|
10703
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
10704
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
10705
|
+
if (aKeys[i] !== bKeys[i]) return false;
|
|
10706
|
+
if (!deepEqual(ao[aKeys[i]], bo[bKeys[i]])) return false;
|
|
10707
|
+
}
|
|
10708
|
+
return true;
|
|
10709
|
+
}
|
|
10710
|
+
|
|
10293
10711
|
// src/commands/attest.ts
|
|
10294
10712
|
function runAttest(opts) {
|
|
10295
10713
|
const repoRoot = findRepoRoot();
|
|
@@ -10309,7 +10727,19 @@ function runAttest(opts) {
|
|
|
10309
10727
|
repoRoot
|
|
10310
10728
|
).trim();
|
|
10311
10729
|
const { keypair } = ensureUserKeypair();
|
|
10312
|
-
const result =
|
|
10730
|
+
const result = opts.migrateExisting ? buildBootstrapEnvelope({
|
|
10731
|
+
repoRoot,
|
|
10732
|
+
revspec,
|
|
10733
|
+
baseSha: resolved.base_sha,
|
|
10734
|
+
headSha: resolved.head_sha,
|
|
10735
|
+
diff: resolved.diff,
|
|
10736
|
+
targetBranch: opts.into,
|
|
10737
|
+
targetBranchTipSha: target_branch_tip_sha,
|
|
10738
|
+
patchId: patch_id,
|
|
10739
|
+
operatorPrivateKeyPem: keypair.privateKeyPem,
|
|
10740
|
+
operatorPublicKeyPem: keypair.publicKeyPem,
|
|
10741
|
+
operatorFingerprint: keypair.fingerprint
|
|
10742
|
+
}) : rule.review_server ? buildV3Envelope({
|
|
10313
10743
|
repoRoot,
|
|
10314
10744
|
revspec,
|
|
10315
10745
|
baseSha: resolved.base_sha,
|
|
@@ -10346,10 +10776,9 @@ function runAttest(opts) {
|
|
|
10346
10776
|
` base\u2192head: ${resolved.base_sha.slice(0, 8)} \u2192 ${resolved.head_sha.slice(0, 8)}`
|
|
10347
10777
|
);
|
|
10348
10778
|
console.log(` signed by: ${keypair.fingerprint}`);
|
|
10349
|
-
console.log(` approvals: ${result.reviewerNames.join(", ")}`);
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
);
|
|
10779
|
+
console.log(` approvals: ${result.reviewerNames.length > 0 ? result.reviewerNames.join(", ") : "(none \u2014 bootstrap envelope)"}`);
|
|
10780
|
+
const schemaLabel = result.payload.migration_bootstrap ? " (Shape 4 migration bootstrap)" : result.payload.schema_version === PR_ATTESTATION_SCHEMA_VERSION ? " (server-attested)" : " (legacy)";
|
|
10781
|
+
console.log(` schema: v${result.payload.schema_version}${schemaLabel}`);
|
|
10353
10782
|
console.log(` ref: ${ref}`);
|
|
10354
10783
|
console.log(` blob: ${blob_sha.slice(0, 12)}`);
|
|
10355
10784
|
console.log(bar);
|
|
@@ -10608,18 +11037,197 @@ function buildV2Envelope(input) {
|
|
|
10608
11037
|
// Phase-1 deliberate omission — see file-level comment.
|
|
10609
11038
|
signer_key_id: input.operatorFingerprint
|
|
10610
11039
|
};
|
|
10611
|
-
const signature = signBytes(
|
|
10612
|
-
input.operatorPrivateKeyPem,
|
|
10613
|
-
serializePayload2(payload)
|
|
10614
|
-
);
|
|
11040
|
+
const signature = signBytes(
|
|
11041
|
+
input.operatorPrivateKeyPem,
|
|
11042
|
+
serializePayload2(payload)
|
|
11043
|
+
);
|
|
11044
|
+
return {
|
|
11045
|
+
payload,
|
|
11046
|
+
signature,
|
|
11047
|
+
reviewerNames: approvals.map((a) => a.reviewer)
|
|
11048
|
+
};
|
|
11049
|
+
}
|
|
11050
|
+
function buildBootstrapEnvelope(input) {
|
|
11051
|
+
const validation = validateShape4ActivationDiff({
|
|
11052
|
+
repoRoot: input.repoRoot,
|
|
11053
|
+
baseSha: input.baseSha,
|
|
11054
|
+
headSha: input.headSha
|
|
11055
|
+
});
|
|
11056
|
+
if (!validation.ok) {
|
|
11057
|
+
throw new Error(
|
|
11058
|
+
`--migrate-existing refused: ${validation.reason}
|
|
11059
|
+
|
|
11060
|
+
The bootstrap flag accepts ONLY a narrow Shape 4 activation diff: adding \`review_server:\` to a branch rule in .stamp/config.yml, adding new [server]+role_source:server entries to .stamp/trusted-keys/manifest.yml, and adding the corresponding new .pub files. Land any unrelated changes through the normal \`stamp attest\` flow AFTER the bootstrap PR merges.`
|
|
11061
|
+
);
|
|
11062
|
+
}
|
|
11063
|
+
const activatedPaths = validation.activatedPaths;
|
|
11064
|
+
const baseConfigYaml = readAtBaseSha(
|
|
11065
|
+
input.repoRoot,
|
|
11066
|
+
input.baseSha,
|
|
11067
|
+
".stamp/config.yml"
|
|
11068
|
+
);
|
|
11069
|
+
if (baseConfigYaml === null) {
|
|
11070
|
+
throw new Error(
|
|
11071
|
+
`--migrate-existing refused: .stamp/config.yml is missing at base ${input.baseSha.slice(0, 8)}. Bootstrap requires the config file in the merge-base tree (the activation diff modifies it).`
|
|
11072
|
+
);
|
|
11073
|
+
}
|
|
11074
|
+
const baseRules = extractPathRulesFromYaml(baseConfigYaml);
|
|
11075
|
+
if (baseRules.length === 0) {
|
|
11076
|
+
throw new Error(
|
|
11077
|
+
`--migrate-existing refused: no \`path_rules\` configured at base ${input.baseSha.slice(0, 8)}. The bootstrap path needs a path_rules entry covering the activated paths with \`bypass_review_cycle: true\` so the verifier knows the reviewer cycle is intentionally skipped for this PR. Add a \`path_rules\` entry for \`.stamp/**\` in a separate prior PR before bootstrapping.`
|
|
11078
|
+
);
|
|
11079
|
+
}
|
|
11080
|
+
const matchedRule = matchAnyCoveringRule(activatedPaths, baseRules);
|
|
11081
|
+
if (!matchedRule) {
|
|
11082
|
+
throw new Error(
|
|
11083
|
+
`--migrate-existing refused: \`path_rules\` at base ${input.baseSha.slice(0, 8)} does not cover every activated path. Activated: ${activatedPaths.join(", ")}. Configured patterns: ${baseRules.map((r) => `"${r.pattern}"`).join(", ")}. Add or widen a path_rules entry that matches these paths (in a separate prior PR \u2014 the bootstrap diff cannot modify path_rules).`
|
|
11084
|
+
);
|
|
11085
|
+
}
|
|
11086
|
+
if (!matchedRule.bypass_review_cycle) {
|
|
11087
|
+
throw new Error(
|
|
11088
|
+
`--migrate-existing refused: matched path_rule "${matchedRule.pattern}" at base has \`bypass_review_cycle: false\` \u2014 bootstrap requires the rule to \`bypass_review_cycle: true\` (otherwise the reviewer cycle is still nominally required and the bootstrap envelope is structurally invalid).`
|
|
11089
|
+
);
|
|
11090
|
+
}
|
|
11091
|
+
if (matchedRule.minimum_signatures > 1) {
|
|
11092
|
+
throw new Error(
|
|
11093
|
+
`--migrate-existing refused: matched path_rule "${matchedRule.pattern}" at base requires \`minimum_signatures: ${matchedRule.minimum_signatures}\` admin signatures, but the bootstrap path only collects a single operator-self admin signature today. Options: (a) temporarily lower the rule to \`minimum_signatures: 1\` in a prior PR; (b) for the migration commit only, use the standard \`stamp admin sign\` flow against the eventual landed commit.`
|
|
11094
|
+
);
|
|
11095
|
+
}
|
|
11096
|
+
const workingTreeManifestYaml = readWorkingTreeManifestYaml(input.repoRoot);
|
|
11097
|
+
if (workingTreeManifestYaml === null) {
|
|
11098
|
+
throw new Error(
|
|
11099
|
+
`--migrate-existing refused: .stamp/trusted-keys/manifest.yml is missing from the working tree. Bootstrap requires the working-tree manifest to bind the operator's local stamp key to the \`admin\` capability.`
|
|
11100
|
+
);
|
|
11101
|
+
}
|
|
11102
|
+
const workingTreeManifest = parseManifest(workingTreeManifestYaml);
|
|
11103
|
+
if (!workingTreeManifest) {
|
|
11104
|
+
throw new Error(
|
|
11105
|
+
`--migrate-existing refused: .stamp/trusted-keys/manifest.yml in the working tree failed to parse (bad YAML, duplicate fingerprint, unknown capability, etc.).`
|
|
11106
|
+
);
|
|
11107
|
+
}
|
|
11108
|
+
const operatorCaps = resolveCapability(workingTreeManifest, input.operatorFingerprint);
|
|
11109
|
+
if (operatorCaps === null) {
|
|
11110
|
+
throw new Error(
|
|
11111
|
+
`--migrate-existing refused: your local stamp key (${input.operatorFingerprint}) is not listed in the working-tree .stamp/trusted-keys/manifest.yml. Add your key with \`capabilities: [admin]\` (in a separate prior PR) before bootstrapping the Shape 4 migration.`
|
|
11112
|
+
);
|
|
11113
|
+
}
|
|
11114
|
+
if (!operatorCaps.includes("admin")) {
|
|
11115
|
+
throw new Error(
|
|
11116
|
+
`--migrate-existing refused: your local stamp key (${input.operatorFingerprint}) has capabilities [${operatorCaps.join(", ")}] in the working-tree manifest \u2014 needs \`admin\` for bootstrap. Either grant your key admin capability (in a separate prior PR) or have a different admin run \`stamp attest --migrate-existing\`.`
|
|
11117
|
+
);
|
|
11118
|
+
}
|
|
11119
|
+
const baseManifestYaml = readAtBaseSha(
|
|
11120
|
+
input.repoRoot,
|
|
11121
|
+
input.baseSha,
|
|
11122
|
+
".stamp/trusted-keys/manifest.yml"
|
|
11123
|
+
);
|
|
11124
|
+
if (baseManifestYaml === null) {
|
|
11125
|
+
throw new Error(
|
|
11126
|
+
`--migrate-existing refused: .stamp/trusted-keys/manifest.yml is missing at base ${input.baseSha.slice(0, 8)}. Bootstrap requires the manifest in the merge-base tree (the operator's outer signature commits to its snapshot hash). Run \`stamp init --migrate-to-server-attested\` first to seed the manifest.`
|
|
11127
|
+
);
|
|
11128
|
+
}
|
|
11129
|
+
const baseManifest = parseManifest(baseManifestYaml);
|
|
11130
|
+
if (!baseManifest) {
|
|
11131
|
+
throw new Error(
|
|
11132
|
+
`--migrate-existing refused: .stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)} failed to parse.`
|
|
11133
|
+
);
|
|
11134
|
+
}
|
|
11135
|
+
const manifestSnapshot = snapshotSha256(baseManifest);
|
|
11136
|
+
const diffBytes = Buffer.from(input.diff, "utf8");
|
|
11137
|
+
const diffSha256 = createHash12("sha256").update(diffBytes).digest("hex");
|
|
11138
|
+
const marker = {
|
|
11139
|
+
activated_paths: activatedPaths
|
|
11140
|
+
};
|
|
11141
|
+
const payloadV4Shape = {
|
|
11142
|
+
schema_version: PR_ATTESTATION_SCHEMA_VERSION,
|
|
11143
|
+
base_sha: input.baseSha,
|
|
11144
|
+
head_sha: input.headSha,
|
|
11145
|
+
target_branch: input.targetBranch,
|
|
11146
|
+
diff_sha256: diffSha256,
|
|
11147
|
+
manifest_snapshot_sha256: manifestSnapshot,
|
|
11148
|
+
approvals: [],
|
|
11149
|
+
// bootstrap: no server signatures
|
|
11150
|
+
checks: [],
|
|
11151
|
+
trust_anchor_signatures: [],
|
|
11152
|
+
signer_key_id: input.operatorFingerprint
|
|
11153
|
+
};
|
|
11154
|
+
const adminSigningBytes = bootstrapAdminSigningBytes({
|
|
11155
|
+
payloadV4: payloadV4Shape,
|
|
11156
|
+
marker
|
|
11157
|
+
});
|
|
11158
|
+
const adminSignatureB64 = signBytes(input.operatorPrivateKeyPem, adminSigningBytes);
|
|
11159
|
+
const selfOk = verifyBytes(
|
|
11160
|
+
input.operatorPublicKeyPem,
|
|
11161
|
+
adminSigningBytes,
|
|
11162
|
+
adminSignatureB64
|
|
11163
|
+
);
|
|
11164
|
+
if (!selfOk) {
|
|
11165
|
+
throw new Error(
|
|
11166
|
+
`internal error: just-produced bootstrap admin signature failed self-verification. Refusing to write a bad envelope. File a bug at https://github.com/OpenThinkAi/stamp-cli/issues.`
|
|
11167
|
+
);
|
|
11168
|
+
}
|
|
11169
|
+
const trustAnchorSignatures = [
|
|
11170
|
+
{
|
|
11171
|
+
signer_key_id: input.operatorFingerprint,
|
|
11172
|
+
signature: adminSignatureB64
|
|
11173
|
+
}
|
|
11174
|
+
];
|
|
11175
|
+
const payload = {
|
|
11176
|
+
schema_version: PR_ATTESTATION_SCHEMA_VERSION,
|
|
11177
|
+
patch_id: input.patchId,
|
|
11178
|
+
base_sha: input.baseSha,
|
|
11179
|
+
head_sha: input.headSha,
|
|
11180
|
+
target_branch: input.targetBranch,
|
|
11181
|
+
target_branch_tip_sha: input.targetBranchTipSha,
|
|
11182
|
+
diff_sha256: diffSha256,
|
|
11183
|
+
manifest_snapshot_sha256: manifestSnapshot,
|
|
11184
|
+
approvals: [],
|
|
11185
|
+
checks: [],
|
|
11186
|
+
trust_anchor_signatures: trustAnchorSignatures,
|
|
11187
|
+
signer_key_id: input.operatorFingerprint,
|
|
11188
|
+
migration_bootstrap: marker
|
|
11189
|
+
};
|
|
11190
|
+
const signature = signBytes(input.operatorPrivateKeyPem, serializePayload2(payload));
|
|
10615
11191
|
return {
|
|
10616
11192
|
payload,
|
|
10617
11193
|
signature,
|
|
10618
|
-
reviewerNames:
|
|
11194
|
+
reviewerNames: []
|
|
10619
11195
|
};
|
|
10620
11196
|
}
|
|
11197
|
+
function readWorkingTreeManifestYaml(repoRoot) {
|
|
11198
|
+
try {
|
|
11199
|
+
return showAtRef("HEAD", ".stamp/trusted-keys/manifest.yml", repoRoot);
|
|
11200
|
+
} catch {
|
|
11201
|
+
return null;
|
|
11202
|
+
}
|
|
11203
|
+
}
|
|
11204
|
+
function readAtBaseSha(repoRoot, baseSha, relPath) {
|
|
11205
|
+
try {
|
|
11206
|
+
return showAtRef(baseSha, relPath, repoRoot);
|
|
11207
|
+
} catch {
|
|
11208
|
+
return null;
|
|
11209
|
+
}
|
|
11210
|
+
}
|
|
11211
|
+
function extractPathRulesFromYaml(yamlText) {
|
|
11212
|
+
let parsed;
|
|
11213
|
+
try {
|
|
11214
|
+
parsed = parseYaml11(yamlText);
|
|
11215
|
+
} catch {
|
|
11216
|
+
return [];
|
|
11217
|
+
}
|
|
11218
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
11219
|
+
const { rules } = parsePathRules(parsed.path_rules);
|
|
11220
|
+
return rules;
|
|
11221
|
+
}
|
|
11222
|
+
function matchAnyCoveringRule(activatedPaths, rules) {
|
|
11223
|
+
for (const rule of rules) {
|
|
11224
|
+
const allMatch = activatedPaths.every((p) => pathMatchesAny(p, [rule.pattern]));
|
|
11225
|
+
if (allMatch) return rule;
|
|
11226
|
+
}
|
|
11227
|
+
return null;
|
|
11228
|
+
}
|
|
10621
11229
|
function pushBranchAndAttestation(remote, attestationRef, repoRoot) {
|
|
10622
|
-
const result =
|
|
11230
|
+
const result = spawnSync15(
|
|
10623
11231
|
"git",
|
|
10624
11232
|
["push", "--atomic", remote, "HEAD", attestationRef],
|
|
10625
11233
|
{ cwd: repoRoot, stdio: "inherit" }
|
|
@@ -10914,7 +11522,7 @@ function loadOrEmpty() {
|
|
|
10914
11522
|
}
|
|
10915
11523
|
|
|
10916
11524
|
// src/commands/reviewers.ts
|
|
10917
|
-
import { spawnSync as
|
|
11525
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
10918
11526
|
import {
|
|
10919
11527
|
existsSync as existsSync21,
|
|
10920
11528
|
readFileSync as readFileSync18,
|
|
@@ -10923,7 +11531,7 @@ import {
|
|
|
10923
11531
|
writeFileSync as writeFileSync15
|
|
10924
11532
|
} from "fs";
|
|
10925
11533
|
import { join as join16, relative as relative3, resolve as resolve2 } from "path";
|
|
10926
|
-
import { parse as
|
|
11534
|
+
import { parse as parseYaml12, stringify as stringifyYaml3 } from "yaml";
|
|
10927
11535
|
|
|
10928
11536
|
// src/lib/reviewerLock.ts
|
|
10929
11537
|
import { existsSync as existsSync20, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "fs";
|
|
@@ -11322,7 +11930,7 @@ async function reviewersFetch(reviewerName, opts) {
|
|
|
11322
11930
|
let tools;
|
|
11323
11931
|
let mcpServers;
|
|
11324
11932
|
if (configYaml !== null) {
|
|
11325
|
-
const parsed =
|
|
11933
|
+
const parsed = parseYaml12(configYaml) ?? {};
|
|
11326
11934
|
if (Array.isArray(parsed.tools)) {
|
|
11327
11935
|
tools = parseToolsLoose(parsed.tools);
|
|
11328
11936
|
}
|
|
@@ -11583,7 +12191,7 @@ function buildConfigYamlHint(reviewerName, tools, mcpServers) {
|
|
|
11583
12191
|
}
|
|
11584
12192
|
function launchEditor(path2) {
|
|
11585
12193
|
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? (process.platform === "win32" ? "notepad" : "vi");
|
|
11586
|
-
const result =
|
|
12194
|
+
const result = spawnSync16(editor, [path2], { stdio: "inherit" });
|
|
11587
12195
|
if (result.error) {
|
|
11588
12196
|
throw new Error(
|
|
11589
12197
|
`failed to launch editor "${editor}": ${result.error.message}`
|
|
@@ -11667,7 +12275,7 @@ function printGate(result, base_sha, head_sha) {
|
|
|
11667
12275
|
}
|
|
11668
12276
|
|
|
11669
12277
|
// src/commands/update.ts
|
|
11670
|
-
import { spawnSync as
|
|
12278
|
+
import { spawnSync as spawnSync17 } from "child_process";
|
|
11671
12279
|
var PKG_NAME = "@openthink/stamp";
|
|
11672
12280
|
function compareSemver(a, b) {
|
|
11673
12281
|
const parse2 = (v) => (v.split("-")[0] ?? "0.0.0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -11686,7 +12294,7 @@ function runUpdate() {
|
|
|
11686
12294
|
`);
|
|
11687
12295
|
process.stdout.write(`checking npm registry for latest...
|
|
11688
12296
|
`);
|
|
11689
|
-
const viewResult =
|
|
12297
|
+
const viewResult = spawnSync17("npm", ["view", PKG_NAME, "version"], {
|
|
11690
12298
|
encoding: "utf8"
|
|
11691
12299
|
});
|
|
11692
12300
|
if (viewResult.error || viewResult.status !== 0) {
|
|
@@ -11720,7 +12328,7 @@ function runUpdate() {
|
|
|
11720
12328
|
}
|
|
11721
12329
|
process.stdout.write(`installing ${PKG_NAME}@${latest}...
|
|
11722
12330
|
`);
|
|
11723
|
-
const installResult =
|
|
12331
|
+
const installResult = spawnSync17(
|
|
11724
12332
|
"npm",
|
|
11725
12333
|
["install", "-g", `${PKG_NAME}@${latest}`],
|
|
11726
12334
|
{ stdio: "inherit" }
|
|
@@ -11741,9 +12349,9 @@ through that tool instead \u2014 this command only uses 'npm install -g'.`
|
|
|
11741
12349
|
}
|
|
11742
12350
|
|
|
11743
12351
|
// src/commands/verify.ts
|
|
11744
|
-
import { execFileSync as execFileSync7, spawnSync as
|
|
12352
|
+
import { execFileSync as execFileSync7, spawnSync as spawnSync18 } from "child_process";
|
|
11745
12353
|
function loadConfigAtSha(sha, repoRoot) {
|
|
11746
|
-
const result =
|
|
12354
|
+
const result = spawnSync18(
|
|
11747
12355
|
"git",
|
|
11748
12356
|
["show", `${sha}:.stamp/config.yml`],
|
|
11749
12357
|
{ cwd: repoRoot, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }
|
|
@@ -11960,8 +12568,9 @@ function git2(args, cwd) {
|
|
|
11960
12568
|
}
|
|
11961
12569
|
|
|
11962
12570
|
// src/commands/verifyPr.ts
|
|
11963
|
-
import { spawnSync as
|
|
11964
|
-
import {
|
|
12571
|
+
import { spawnSync as spawnSync19 } from "child_process";
|
|
12572
|
+
import { createHash as createHash13 } from "crypto";
|
|
12573
|
+
import { parse as parseYaml13 } from "yaml";
|
|
11965
12574
|
function runVerifyPr(opts) {
|
|
11966
12575
|
const repoRoot = opts.repoPath ?? findRepoRoot();
|
|
11967
12576
|
const resolved = resolveDiff(`${opts.base}..${opts.head}`, repoRoot);
|
|
@@ -12002,6 +12611,10 @@ function runVerifyPr(opts) {
|
|
|
12002
12611
|
);
|
|
12003
12612
|
}
|
|
12004
12613
|
if (envelope.payload.schema_version >= MIN_ACCEPTED_PR_ATTESTATION_VERSION) {
|
|
12614
|
+
if (envelope.payload.migration_bootstrap !== void 0) {
|
|
12615
|
+
verifyBootstrapEnvelope(envelope, opts, resolved, patch_id, repoRoot);
|
|
12616
|
+
return;
|
|
12617
|
+
}
|
|
12005
12618
|
verifyV3Envelope(envelope, opts, resolved, patch_id, repoRoot);
|
|
12006
12619
|
return;
|
|
12007
12620
|
}
|
|
@@ -12130,6 +12743,310 @@ function verifyV3Envelope(envelope, opts, resolved, patch_id, repoRoot) {
|
|
|
12130
12743
|
schema_version: envelope.payload.schema_version
|
|
12131
12744
|
});
|
|
12132
12745
|
}
|
|
12746
|
+
function verifyBootstrapEnvelope(envelope, opts, resolved, patch_id, repoRoot) {
|
|
12747
|
+
const payload = envelope.payload;
|
|
12748
|
+
const marker = payload.migration_bootstrap;
|
|
12749
|
+
if (!marker) {
|
|
12750
|
+
fail2(
|
|
12751
|
+
`internal error: bootstrap dispatch with no marker (should be unreachable)`,
|
|
12752
|
+
patch_id,
|
|
12753
|
+
resolved.base_sha,
|
|
12754
|
+
resolved.head_sha
|
|
12755
|
+
);
|
|
12756
|
+
}
|
|
12757
|
+
if (payload.approvals.length !== 0) {
|
|
12758
|
+
fail2(
|
|
12759
|
+
`bootstrap envelope has ${payload.approvals.length} approvals \u2014 bootstrap envelopes MUST have approvals: []. Non-empty server signatures should go through the normal (non-bootstrap) attest flow.`,
|
|
12760
|
+
patch_id,
|
|
12761
|
+
resolved.base_sha,
|
|
12762
|
+
resolved.head_sha
|
|
12763
|
+
);
|
|
12764
|
+
}
|
|
12765
|
+
const validation = validateShape4ActivationDiff({
|
|
12766
|
+
repoRoot,
|
|
12767
|
+
baseSha: payload.base_sha,
|
|
12768
|
+
headSha: payload.head_sha
|
|
12769
|
+
});
|
|
12770
|
+
if (!validation.ok) {
|
|
12771
|
+
fail2(
|
|
12772
|
+
`bootstrap envelope rejected: diff does not match the Shape 4 activation whitelist \u2014 ${validation.reason}`,
|
|
12773
|
+
patch_id,
|
|
12774
|
+
resolved.base_sha,
|
|
12775
|
+
resolved.head_sha
|
|
12776
|
+
);
|
|
12777
|
+
}
|
|
12778
|
+
const claimed = [...marker.activated_paths].sort();
|
|
12779
|
+
const actual = [...validation.activatedPaths].sort();
|
|
12780
|
+
if (claimed.length !== actual.length || !claimed.every((p, i) => p === actual[i])) {
|
|
12781
|
+
fail2(
|
|
12782
|
+
`bootstrap envelope's migration_bootstrap.activated_paths (${claimed.join(", ")}) does not match the actual changed-files set (${actual.join(", ")}). The marker must declare exactly the paths the bootstrap PR activates.`,
|
|
12783
|
+
patch_id,
|
|
12784
|
+
resolved.base_sha,
|
|
12785
|
+
resolved.head_sha
|
|
12786
|
+
);
|
|
12787
|
+
}
|
|
12788
|
+
const baseManifest = loadManifestAtBase(payload.base_sha, repoRoot);
|
|
12789
|
+
if (!baseManifest) {
|
|
12790
|
+
fail2(
|
|
12791
|
+
`bootstrap envelope rejected: .stamp/trusted-keys/manifest.yml is missing or malformed at base ${payload.base_sha.slice(0, 8)}. Bootstrap requires the manifest in the merge-base tree (it's the trust root for the operator's outer signature and the admin counter-signature).`,
|
|
12792
|
+
patch_id,
|
|
12793
|
+
resolved.base_sha,
|
|
12794
|
+
resolved.head_sha
|
|
12795
|
+
);
|
|
12796
|
+
}
|
|
12797
|
+
if (!payload.manifest_snapshot_sha256) {
|
|
12798
|
+
fail2(
|
|
12799
|
+
`bootstrap envelope is missing manifest_snapshot_sha256 \u2014 required for v3+ envelopes`,
|
|
12800
|
+
patch_id,
|
|
12801
|
+
resolved.base_sha,
|
|
12802
|
+
resolved.head_sha
|
|
12803
|
+
);
|
|
12804
|
+
}
|
|
12805
|
+
const computedSnapshot = snapshotSha256(baseManifest);
|
|
12806
|
+
if (payload.manifest_snapshot_sha256 !== computedSnapshot) {
|
|
12807
|
+
fail2(
|
|
12808
|
+
`bootstrap envelope manifest_snapshot_sha256 (${payload.manifest_snapshot_sha256.slice(0, 16)}\u2026) does not match the manifest at base ${payload.base_sha.slice(0, 8)} (${computedSnapshot.slice(0, 16)}\u2026)`,
|
|
12809
|
+
patch_id,
|
|
12810
|
+
resolved.base_sha,
|
|
12811
|
+
resolved.head_sha
|
|
12812
|
+
);
|
|
12813
|
+
}
|
|
12814
|
+
const pubkeyByFingerprint = loadPubkeysAtBase(payload.base_sha, repoRoot);
|
|
12815
|
+
const operatorCaps = resolveCapability(baseManifest, payload.signer_key_id);
|
|
12816
|
+
if (operatorCaps === null) {
|
|
12817
|
+
fail2(
|
|
12818
|
+
`bootstrap envelope signer ${payload.signer_key_id} is not listed in the manifest at base ${payload.base_sha.slice(0, 8)}`,
|
|
12819
|
+
patch_id,
|
|
12820
|
+
resolved.base_sha,
|
|
12821
|
+
resolved.head_sha
|
|
12822
|
+
);
|
|
12823
|
+
}
|
|
12824
|
+
if (!operatorCaps.includes("operator") && !operatorCaps.includes("admin")) {
|
|
12825
|
+
fail2(
|
|
12826
|
+
`bootstrap envelope signer ${payload.signer_key_id} has capabilities [${operatorCaps.join(", ")}] at base ${payload.base_sha.slice(0, 8)} \u2014 needs operator or admin`,
|
|
12827
|
+
patch_id,
|
|
12828
|
+
resolved.base_sha,
|
|
12829
|
+
resolved.head_sha
|
|
12830
|
+
);
|
|
12831
|
+
}
|
|
12832
|
+
const operatorPem = pubkeyByFingerprint.get(payload.signer_key_id);
|
|
12833
|
+
if (!operatorPem) {
|
|
12834
|
+
fail2(
|
|
12835
|
+
`bootstrap envelope signer ${payload.signer_key_id} has no matching .pub file in .stamp/trusted-keys/ at base ${payload.base_sha.slice(0, 8)}`,
|
|
12836
|
+
patch_id,
|
|
12837
|
+
resolved.base_sha,
|
|
12838
|
+
resolved.head_sha
|
|
12839
|
+
);
|
|
12840
|
+
}
|
|
12841
|
+
const operatorBytes = serializePayload2(payload);
|
|
12842
|
+
let operatorSigOk = false;
|
|
12843
|
+
try {
|
|
12844
|
+
operatorSigOk = verifyBytes(operatorPem, operatorBytes, envelope.signature);
|
|
12845
|
+
} catch (err) {
|
|
12846
|
+
fail2(
|
|
12847
|
+
`bootstrap envelope operator-signature verification threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
12848
|
+
patch_id,
|
|
12849
|
+
resolved.base_sha,
|
|
12850
|
+
resolved.head_sha
|
|
12851
|
+
);
|
|
12852
|
+
}
|
|
12853
|
+
if (!operatorSigOk) {
|
|
12854
|
+
fail2(
|
|
12855
|
+
`bootstrap envelope operator signature does not verify against the operator's pubkey ${payload.signer_key_id}`,
|
|
12856
|
+
patch_id,
|
|
12857
|
+
resolved.base_sha,
|
|
12858
|
+
resolved.head_sha
|
|
12859
|
+
);
|
|
12860
|
+
}
|
|
12861
|
+
const sigs = payload.trust_anchor_signatures ?? [];
|
|
12862
|
+
if (sigs.length !== 1) {
|
|
12863
|
+
fail2(
|
|
12864
|
+
`bootstrap envelope must carry exactly one trust_anchor_signatures entry (got ${sigs.length}) \u2014 multi-admin bootstrap collection is not supported by the bootstrap path. Lower path_rules minimum_signatures or use the standard admin-sign flow.`,
|
|
12865
|
+
patch_id,
|
|
12866
|
+
resolved.base_sha,
|
|
12867
|
+
resolved.head_sha
|
|
12868
|
+
);
|
|
12869
|
+
}
|
|
12870
|
+
const adminSig = sigs[0];
|
|
12871
|
+
const adminCaps = resolveCapability(baseManifest, adminSig.signer_key_id);
|
|
12872
|
+
if (adminCaps === null) {
|
|
12873
|
+
fail2(
|
|
12874
|
+
`bootstrap envelope admin signer ${adminSig.signer_key_id} is not listed in the manifest at base ${payload.base_sha.slice(0, 8)}`,
|
|
12875
|
+
patch_id,
|
|
12876
|
+
resolved.base_sha,
|
|
12877
|
+
resolved.head_sha
|
|
12878
|
+
);
|
|
12879
|
+
}
|
|
12880
|
+
if (!adminCaps.includes("admin")) {
|
|
12881
|
+
fail2(
|
|
12882
|
+
`bootstrap envelope admin signer ${adminSig.signer_key_id} has capabilities [${adminCaps.join(", ")}] at base ${payload.base_sha.slice(0, 8)} \u2014 needs admin`,
|
|
12883
|
+
patch_id,
|
|
12884
|
+
resolved.base_sha,
|
|
12885
|
+
resolved.head_sha
|
|
12886
|
+
);
|
|
12887
|
+
}
|
|
12888
|
+
const adminPem = pubkeyByFingerprint.get(adminSig.signer_key_id);
|
|
12889
|
+
if (!adminPem) {
|
|
12890
|
+
fail2(
|
|
12891
|
+
`bootstrap envelope admin signer ${adminSig.signer_key_id} has no matching .pub file in .stamp/trusted-keys/ at base ${payload.base_sha.slice(0, 8)}`,
|
|
12892
|
+
patch_id,
|
|
12893
|
+
resolved.base_sha,
|
|
12894
|
+
resolved.head_sha
|
|
12895
|
+
);
|
|
12896
|
+
}
|
|
12897
|
+
const v4View = {
|
|
12898
|
+
schema_version: payload.schema_version,
|
|
12899
|
+
base_sha: payload.base_sha,
|
|
12900
|
+
head_sha: payload.head_sha,
|
|
12901
|
+
target_branch: payload.target_branch,
|
|
12902
|
+
diff_sha256: payload.diff_sha256,
|
|
12903
|
+
manifest_snapshot_sha256: payload.manifest_snapshot_sha256,
|
|
12904
|
+
approvals: [],
|
|
12905
|
+
checks: payload.checks,
|
|
12906
|
+
trust_anchor_signatures: [],
|
|
12907
|
+
signer_key_id: payload.signer_key_id
|
|
12908
|
+
};
|
|
12909
|
+
const adminBytes = bootstrapAdminSigningBytes({ payloadV4: v4View, marker });
|
|
12910
|
+
let adminSigOk = false;
|
|
12911
|
+
try {
|
|
12912
|
+
adminSigOk = verifyBytes(adminPem, adminBytes, adminSig.signature);
|
|
12913
|
+
} catch (err) {
|
|
12914
|
+
fail2(
|
|
12915
|
+
`bootstrap envelope admin-signature verification threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
12916
|
+
patch_id,
|
|
12917
|
+
resolved.base_sha,
|
|
12918
|
+
resolved.head_sha
|
|
12919
|
+
);
|
|
12920
|
+
}
|
|
12921
|
+
if (!adminSigOk) {
|
|
12922
|
+
fail2(
|
|
12923
|
+
`bootstrap envelope admin signature by ${adminSig.signer_key_id} does not verify against the bootstrap signing-bytes (payload-with-marker, trust_anchor_signatures: []).`,
|
|
12924
|
+
patch_id,
|
|
12925
|
+
resolved.base_sha,
|
|
12926
|
+
resolved.head_sha
|
|
12927
|
+
);
|
|
12928
|
+
}
|
|
12929
|
+
const pathRules = loadPathRulesAtBase(payload.base_sha, repoRoot);
|
|
12930
|
+
if (pathRules.length === 0) {
|
|
12931
|
+
fail2(
|
|
12932
|
+
`bootstrap envelope rejected: no path_rules configured at base ${payload.base_sha.slice(0, 8)}. The bootstrap path requires path_rules covering the activated paths with bypass_review_cycle: true \u2014 typically already in place from a prior Shape 2 migration.`,
|
|
12933
|
+
patch_id,
|
|
12934
|
+
resolved.base_sha,
|
|
12935
|
+
resolved.head_sha
|
|
12936
|
+
);
|
|
12937
|
+
}
|
|
12938
|
+
for (const p of marker.activated_paths) {
|
|
12939
|
+
let coveredBy = null;
|
|
12940
|
+
for (const rule2 of pathRules) {
|
|
12941
|
+
if (pathMatchesAny(p, [rule2.pattern])) {
|
|
12942
|
+
coveredBy = rule2;
|
|
12943
|
+
break;
|
|
12944
|
+
}
|
|
12945
|
+
}
|
|
12946
|
+
if (!coveredBy) {
|
|
12947
|
+
fail2(
|
|
12948
|
+
`bootstrap envelope rejected: path_rules at base ${payload.base_sha.slice(0, 8)} does not cover activated path "${p}". Configured patterns: ${pathRules.map((r) => `"${r.pattern}"`).join(", ")}.`,
|
|
12949
|
+
patch_id,
|
|
12950
|
+
resolved.base_sha,
|
|
12951
|
+
resolved.head_sha
|
|
12952
|
+
);
|
|
12953
|
+
}
|
|
12954
|
+
if (!coveredBy.bypass_review_cycle) {
|
|
12955
|
+
fail2(
|
|
12956
|
+
`bootstrap envelope rejected: path_rule "${coveredBy.pattern}" at base ${payload.base_sha.slice(0, 8)} has bypass_review_cycle: false. Bootstrap requires the matched rule to opt out of the reviewer cycle.`,
|
|
12957
|
+
patch_id,
|
|
12958
|
+
resolved.base_sha,
|
|
12959
|
+
resolved.head_sha
|
|
12960
|
+
);
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
if (payload.target_branch !== opts.into) {
|
|
12964
|
+
fail2(
|
|
12965
|
+
`bootstrap envelope target_branch ("${payload.target_branch}") does not match --into ("${opts.into}")`,
|
|
12966
|
+
patch_id,
|
|
12967
|
+
resolved.base_sha,
|
|
12968
|
+
resolved.head_sha
|
|
12969
|
+
);
|
|
12970
|
+
}
|
|
12971
|
+
if (!payload.diff_sha256) {
|
|
12972
|
+
fail2(
|
|
12973
|
+
`bootstrap envelope is missing diff_sha256 \u2014 required for v3+ envelopes`,
|
|
12974
|
+
patch_id,
|
|
12975
|
+
resolved.base_sha,
|
|
12976
|
+
resolved.head_sha
|
|
12977
|
+
);
|
|
12978
|
+
}
|
|
12979
|
+
const actualDiff = spawnSync19(
|
|
12980
|
+
"git",
|
|
12981
|
+
["diff", `${payload.base_sha}...${payload.head_sha}`],
|
|
12982
|
+
{ cwd: repoRoot, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
|
|
12983
|
+
);
|
|
12984
|
+
if (actualDiff.status !== 0) {
|
|
12985
|
+
fail2(
|
|
12986
|
+
`bootstrap envelope rejected: could not compute base...head diff for diff_sha256 check`,
|
|
12987
|
+
patch_id,
|
|
12988
|
+
resolved.base_sha,
|
|
12989
|
+
resolved.head_sha
|
|
12990
|
+
);
|
|
12991
|
+
}
|
|
12992
|
+
const diffText = actualDiff.stdout ?? "";
|
|
12993
|
+
const actualDiffSha256 = createHash13("sha256").update(Buffer.from(diffText, "utf8")).digest("hex");
|
|
12994
|
+
if (actualDiffSha256 !== payload.diff_sha256) {
|
|
12995
|
+
fail2(
|
|
12996
|
+
`bootstrap envelope diff_sha256 mismatch \u2014 payload claims ${payload.diff_sha256.slice(0, 12)}\u2026 but base...head hashes to ${actualDiffSha256.slice(0, 12)}\u2026. The operator signed against a different diff.`,
|
|
12997
|
+
patch_id,
|
|
12998
|
+
resolved.base_sha,
|
|
12999
|
+
resolved.head_sha
|
|
13000
|
+
);
|
|
13001
|
+
}
|
|
13002
|
+
let configYaml;
|
|
13003
|
+
try {
|
|
13004
|
+
configYaml = showAtRef(payload.base_sha, ".stamp/config.yml", repoRoot);
|
|
13005
|
+
} catch (e) {
|
|
13006
|
+
fail2(
|
|
13007
|
+
`bootstrap envelope rejected: could not read .stamp/config.yml at base ${payload.base_sha.slice(0, 8)}: ${e.message}`,
|
|
13008
|
+
patch_id,
|
|
13009
|
+
resolved.base_sha,
|
|
13010
|
+
resolved.head_sha
|
|
13011
|
+
);
|
|
13012
|
+
}
|
|
13013
|
+
const config2 = parseConfigFromYaml(configYaml);
|
|
13014
|
+
const rule = findBranchRule(config2.branches, opts.into);
|
|
13015
|
+
if (!rule) {
|
|
13016
|
+
fail2(
|
|
13017
|
+
`bootstrap envelope rejected: no branch rule for "${opts.into}" in .stamp/config.yml at base ${payload.base_sha.slice(0, 8)}`,
|
|
13018
|
+
patch_id,
|
|
13019
|
+
resolved.base_sha,
|
|
13020
|
+
resolved.head_sha
|
|
13021
|
+
);
|
|
13022
|
+
}
|
|
13023
|
+
if (rule.strict_base) {
|
|
13024
|
+
const currentTip = runGit(
|
|
13025
|
+
["rev-parse", `${opts.into}^{commit}`],
|
|
13026
|
+
repoRoot
|
|
13027
|
+
).trim();
|
|
13028
|
+
if (envelope.payload.target_branch_tip_sha !== currentTip) {
|
|
13029
|
+
fail2(
|
|
13030
|
+
`bootstrap strict_base check failed: attestation was signed when ${opts.into} was at ${envelope.payload.target_branch_tip_sha?.slice(0, 8)}, but ${opts.into} is now at ${currentTip.slice(0, 8)}`,
|
|
13031
|
+
patch_id,
|
|
13032
|
+
resolved.base_sha,
|
|
13033
|
+
resolved.head_sha
|
|
13034
|
+
);
|
|
13035
|
+
}
|
|
13036
|
+
}
|
|
13037
|
+
printSuccess3({
|
|
13038
|
+
patch_id,
|
|
13039
|
+
base_sha: resolved.base_sha,
|
|
13040
|
+
head_sha: resolved.head_sha,
|
|
13041
|
+
target_branch: opts.into,
|
|
13042
|
+
signer_key_id: payload.signer_key_id,
|
|
13043
|
+
approvals: [],
|
|
13044
|
+
strict_base: rule.strict_base ?? false,
|
|
13045
|
+
schema_version: envelope.payload.schema_version,
|
|
13046
|
+
bootstrap: true,
|
|
13047
|
+
activated_paths: marker.activated_paths
|
|
13048
|
+
});
|
|
13049
|
+
}
|
|
12133
13050
|
function loadManifestAtBase(base_sha, repoRoot) {
|
|
12134
13051
|
let yaml;
|
|
12135
13052
|
try {
|
|
@@ -12140,7 +13057,7 @@ function loadManifestAtBase(base_sha, repoRoot) {
|
|
|
12140
13057
|
return parseManifest(yaml);
|
|
12141
13058
|
}
|
|
12142
13059
|
function loadPubkeysAtBase(base_sha, repoRoot) {
|
|
12143
|
-
const lsResult =
|
|
13060
|
+
const lsResult = spawnSync19(
|
|
12144
13061
|
"git",
|
|
12145
13062
|
["ls-tree", "--name-only", base_sha, ".stamp/trusted-keys/"],
|
|
12146
13063
|
{ cwd: repoRoot, encoding: "utf8" }
|
|
@@ -12152,7 +13069,7 @@ function loadPubkeysAtBase(base_sha, repoRoot) {
|
|
|
12152
13069
|
return l.startsWith(prefix) ? l.slice(prefix.length) : l;
|
|
12153
13070
|
}).filter((n) => n.endsWith(".pub"));
|
|
12154
13071
|
return buildPubkeyMap(pubFiles, (relPath) => {
|
|
12155
|
-
const show =
|
|
13072
|
+
const show = spawnSync19(
|
|
12156
13073
|
"git",
|
|
12157
13074
|
["show", `${base_sha}:${relPath}`],
|
|
12158
13075
|
{ cwd: repoRoot, encoding: "utf8" }
|
|
@@ -12170,7 +13087,7 @@ function loadPathRulesAtBase(base_sha, repoRoot) {
|
|
|
12170
13087
|
}
|
|
12171
13088
|
let raw;
|
|
12172
13089
|
try {
|
|
12173
|
-
raw =
|
|
13090
|
+
raw = parseYaml13(yaml);
|
|
12174
13091
|
} catch {
|
|
12175
13092
|
return [];
|
|
12176
13093
|
}
|
|
@@ -12183,7 +13100,7 @@ function loadPathRulesAtBase(base_sha, repoRoot) {
|
|
|
12183
13100
|
return parsedRules.rules;
|
|
12184
13101
|
}
|
|
12185
13102
|
function loadChangedFiles(base_sha, head_sha, repoRoot) {
|
|
12186
|
-
const result =
|
|
13103
|
+
const result = spawnSync19(
|
|
12187
13104
|
"git",
|
|
12188
13105
|
["diff", "-z", "--name-only", `${base_sha}...${head_sha}`],
|
|
12189
13106
|
{ cwd: repoRoot, encoding: "utf8" }
|
|
@@ -12199,12 +13116,18 @@ function printSuccess3(s) {
|
|
|
12199
13116
|
);
|
|
12200
13117
|
console.log(bar);
|
|
12201
13118
|
console.log(` patch-id: ${s.patch_id}`);
|
|
12202
|
-
console.log(
|
|
13119
|
+
console.log(
|
|
13120
|
+
` schema: v${s.schema_version}${s.bootstrap ? " (Shape 4 migration bootstrap)" : " (v4-trust)"}`
|
|
13121
|
+
);
|
|
12203
13122
|
console.log(` signer: ${s.signer_key_id}`);
|
|
12204
13123
|
console.log(` base mode: ${s.strict_base ? "strict" : "loose"}`);
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
13124
|
+
if (s.bootstrap && s.activated_paths) {
|
|
13125
|
+
console.log(` activated: ${s.activated_paths.join(", ")}`);
|
|
13126
|
+
} else {
|
|
13127
|
+
for (const a of s.approvals) {
|
|
13128
|
+
const mark = a.verdict === "approved" ? "\u2713" : "\u2717";
|
|
13129
|
+
console.log(` ${mark} ${a.reviewer.padEnd(16)} ${a.verdict}`);
|
|
13130
|
+
}
|
|
12208
13131
|
}
|
|
12209
13132
|
console.log(bar);
|
|
12210
13133
|
console.log("result: VERIFIED");
|
|
@@ -12601,11 +13524,19 @@ program.command("attest [branch]").description(
|
|
|
12601
13524
|
).requiredOption("--into <target>", "target branch whose rule the gate is checked against").option(
|
|
12602
13525
|
"--push [remote]",
|
|
12603
13526
|
"after attesting locally, push the current branch + the attestation ref to <remote> in one atomic git push (default remote: origin)"
|
|
13527
|
+
).option(
|
|
13528
|
+
"--migrate-existing",
|
|
13529
|
+
"Shape 4 migration bootstrap (AGT-398): attest a narrowly-scoped diff that ADDS `review_server:` + `[server]`-capability trust-anchor entries (the chicken-and-egg PR that activates server-attested reviews on an existing repo). Produces a v3 envelope with empty server_signatures, a migration-bootstrap marker in the operator-signed payload, and exactly one operator-self admin counter-signature in `trust_anchor_signatures`. The flag is REFUSED on any diff outside the narrow Shape-4-activation whitelist (no files outside .stamp/, no modifications to existing trust-anchor entries, no removals). Requires the operator's local key to have `admin` capability in the working-tree manifest and `path_rules` to cover the activated paths with `bypass_review_cycle: true` and `minimum_signatures: 1`. See docs/migration-1.x-to-2.x.md for the full Shape 4 bootstrap walkthrough."
|
|
12604
13530
|
).action(
|
|
12605
13531
|
(branch, opts) => {
|
|
12606
13532
|
try {
|
|
12607
13533
|
const pushTo = opts.push === true ? "origin" : typeof opts.push === "string" ? opts.push : void 0;
|
|
12608
|
-
runAttest({
|
|
13534
|
+
runAttest({
|
|
13535
|
+
branch,
|
|
13536
|
+
into: opts.into,
|
|
13537
|
+
pushTo,
|
|
13538
|
+
migrateExisting: opts.migrateExisting === true
|
|
13539
|
+
});
|
|
12609
13540
|
} catch (err) {
|
|
12610
13541
|
handleCliError(err);
|
|
12611
13542
|
}
|