@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 CHANGED
@@ -10140,8 +10140,9 @@ function printReviewHistory(repoRoot, limit, diff) {
10140
10140
  }
10141
10141
 
10142
10142
  // src/commands/attest.ts
10143
- import { spawnSync as spawnSync14 } from "child_process";
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 = rule.review_server ? buildV3Envelope({
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
- console.log(
10351
- ` schema: v${result.payload.schema_version}${result.payload.schema_version === PR_ATTESTATION_SCHEMA_VERSION ? " (server-attested)" : " (legacy)"}`
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: approvals.map((a) => a.reviewer)
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 = spawnSync14(
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 spawnSync15 } from "child_process";
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 parseYaml10, stringify as stringifyYaml3 } from "yaml";
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 = parseYaml10(configYaml) ?? {};
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 = spawnSync15(editor, [path2], { stdio: "inherit" });
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 spawnSync16 } from "child_process";
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 = spawnSync16("npm", ["view", PKG_NAME, "version"], {
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 = spawnSync16(
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 spawnSync17 } from "child_process";
12352
+ import { execFileSync as execFileSync7, spawnSync as spawnSync18 } from "child_process";
11745
12353
  function loadConfigAtSha(sha, repoRoot) {
11746
- const result = spawnSync17(
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 spawnSync18 } from "child_process";
11964
- import { parse as parseYaml11 } from "yaml";
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 = spawnSync18(
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 = spawnSync18(
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 = parseYaml11(yaml);
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 = spawnSync18(
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(` schema: v${s.schema_version} (v4-trust)`);
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
- for (const a of s.approvals) {
12206
- const mark = a.verdict === "approved" ? "\u2713" : "\u2717";
12207
- console.log(` ${mark} ${a.reviewer.padEnd(16)} ${a.verdict}`);
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({ branch, into: opts.into, pushTo });
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
  }