@openthink/stamp 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/dist/hooks/pre-receive.cjs.map +1 -1
- package/dist/index.js +453 -190
- package/dist/index.js.map +1 -1
- package/dist/server/stamp-review.cjs +4 -1
- package/dist/server/stamp-review.cjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59,8 +59,8 @@ import { Command } from "commander";
|
|
|
59
59
|
|
|
60
60
|
// src/commands/bootstrap.ts
|
|
61
61
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
62
|
-
import { existsSync as existsSync7, readFileSync as
|
|
63
|
-
import { dirname as
|
|
62
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync, statSync, writeFileSync as writeFileSync5 } from "fs";
|
|
63
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
64
64
|
|
|
65
65
|
// src/lib/agentsMd.ts
|
|
66
66
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -1567,11 +1567,31 @@ function parseResponseJson(raw) {
|
|
|
1567
1567
|
`review_server response: top-level verdict (${verdict}) and approval.verdict (${a.verdict}) disagree`
|
|
1568
1568
|
);
|
|
1569
1569
|
}
|
|
1570
|
+
let prAttestationV3 = null;
|
|
1571
|
+
const payloadB64 = obj["pr_attestation_v3_payload_b64"];
|
|
1572
|
+
const sigB64 = obj["pr_attestation_v3_signature_b64"];
|
|
1573
|
+
if (payloadB64 !== void 0 || sigB64 !== void 0) {
|
|
1574
|
+
if (typeof payloadB64 !== "string" || !payloadB64) {
|
|
1575
|
+
throw new Error(
|
|
1576
|
+
`review_server response.pr_attestation_v3_payload_b64 must be a non-empty string when present`
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
if (typeof sigB64 !== "string" || !sigB64) {
|
|
1580
|
+
throw new Error(
|
|
1581
|
+
`review_server response.pr_attestation_v3_signature_b64 must be a non-empty string when present`
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
prAttestationV3 = {
|
|
1585
|
+
payloadBytes: Buffer.from(payloadB64, "base64"),
|
|
1586
|
+
signatureB64: sigB64
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1570
1589
|
return {
|
|
1571
1590
|
verdict,
|
|
1572
1591
|
prose: obj.prose,
|
|
1573
1592
|
approval,
|
|
1574
|
-
signature: obj.signature
|
|
1593
|
+
signature: obj.signature,
|
|
1594
|
+
prAttestationV3
|
|
1575
1595
|
};
|
|
1576
1596
|
}
|
|
1577
1597
|
function verifyServerSignature(opts) {
|
|
@@ -1709,12 +1729,26 @@ async function requestServerReview(input) {
|
|
|
1709
1729
|
`review_server returned a signed approval for diff_sha256 ${parsed.approval.diff_sha256} but we sent diff_sha256 ${diffSha256} \u2014 refusing.`
|
|
1710
1730
|
);
|
|
1711
1731
|
}
|
|
1732
|
+
if (parsed.prAttestationV3) {
|
|
1733
|
+
const localCanonical = canonicalSerializeApproval(parsed.approval);
|
|
1734
|
+
if (!parsed.prAttestationV3.payloadBytes.equals(localCanonical)) {
|
|
1735
|
+
throw new Error(
|
|
1736
|
+
`review_server returned pr_attestation_v3_payload_b64 bytes that do not match canonicalSerializeApproval(approval) recomputed locally \u2014 refusing. This indicates a server/client canonicalizer drift (server stamp version mismatch?). Server bytes (first 80): ${JSON.stringify(parsed.prAttestationV3.payloadBytes.toString("utf8").slice(0, 80))}; client bytes (first 80): ${JSON.stringify(localCanonical.toString("utf8").slice(0, 80))}.`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
if (parsed.prAttestationV3.signatureB64 !== parsed.signature) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`review_server returned pr_attestation_v3_signature_b64 that disagrees with the top-level signature \u2014 refusing.`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1712
1745
|
return {
|
|
1713
1746
|
verdict: parsed.verdict,
|
|
1714
1747
|
prose: parsed.prose,
|
|
1715
1748
|
approval: parsed.approval,
|
|
1716
1749
|
signature: parsed.signature,
|
|
1717
|
-
approvalJson: JSON.stringify(parsed.approval)
|
|
1750
|
+
approvalJson: JSON.stringify(parsed.approval),
|
|
1751
|
+
prAttestationV3: parsed.prAttestationV3
|
|
1718
1752
|
};
|
|
1719
1753
|
}
|
|
1720
1754
|
function exitCodeHint(exitCode) {
|
|
@@ -3132,9 +3166,38 @@ function readLineSync() {
|
|
|
3132
3166
|
return out;
|
|
3133
3167
|
}
|
|
3134
3168
|
|
|
3169
|
+
// src/lib/version.ts
|
|
3170
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3171
|
+
import { dirname, join as join3 } from "path";
|
|
3172
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3173
|
+
function readPackageVersion() {
|
|
3174
|
+
const here = dirname(fileURLToPath2(import.meta.url));
|
|
3175
|
+
for (let dir = here, i = 0; i < 6; i++) {
|
|
3176
|
+
try {
|
|
3177
|
+
const raw = readFileSync4(join3(dir, "package.json"), "utf8");
|
|
3178
|
+
const pkg = JSON.parse(raw);
|
|
3179
|
+
if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
|
|
3180
|
+
} catch {
|
|
3181
|
+
}
|
|
3182
|
+
const parent = dirname(dir);
|
|
3183
|
+
if (parent === dir) break;
|
|
3184
|
+
dir = parent;
|
|
3185
|
+
}
|
|
3186
|
+
throw new Error("could not locate @openthink/stamp package.json to read version");
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3135
3189
|
// src/lib/deprecationNotice.ts
|
|
3136
|
-
function maybePrintDeprecationNotice() {
|
|
3190
|
+
function maybePrintDeprecationNotice(versionOverride) {
|
|
3137
3191
|
if (process.env.STAMP_SUPPRESS_DEPRECATION === "1") return;
|
|
3192
|
+
let major = null;
|
|
3193
|
+
try {
|
|
3194
|
+
const version = versionOverride ?? readPackageVersion();
|
|
3195
|
+
const m = /^(\d+)\./.exec(version);
|
|
3196
|
+
if (m) major = Number.parseInt(m[1], 10);
|
|
3197
|
+
} catch {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
if (major === null || major >= 2) return;
|
|
3138
3201
|
process.stderr.write(
|
|
3139
3202
|
"warning: stamp 1.x is in maintenance \u2014 the server-attested 2.x line is active. See docs/migration-1.x-to-2.x.md. Suppress: STAMP_SUPPRESS_DEPRECATION=1.\n"
|
|
3140
3203
|
);
|
|
@@ -3729,12 +3792,12 @@ ${close}`;
|
|
|
3729
3792
|
import {
|
|
3730
3793
|
existsSync as existsSync3,
|
|
3731
3794
|
mkdirSync,
|
|
3732
|
-
readFileSync as
|
|
3795
|
+
readFileSync as readFileSync5,
|
|
3733
3796
|
renameSync,
|
|
3734
3797
|
unlinkSync,
|
|
3735
3798
|
writeFileSync as writeFileSync2
|
|
3736
3799
|
} from "fs";
|
|
3737
|
-
import { dirname } from "path";
|
|
3800
|
+
import { dirname as dirname2 } from "path";
|
|
3738
3801
|
import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
|
|
3739
3802
|
var DEFAULT_REVIEWER_MODELS = {
|
|
3740
3803
|
security: "claude-sonnet-4-6",
|
|
@@ -3754,7 +3817,7 @@ function loadUserConfig() {
|
|
|
3754
3817
|
if (!existsSync3(path2)) return null;
|
|
3755
3818
|
let raw;
|
|
3756
3819
|
try {
|
|
3757
|
-
raw =
|
|
3820
|
+
raw = readFileSync5(path2, "utf8");
|
|
3758
3821
|
} catch (err) {
|
|
3759
3822
|
throw new Error(
|
|
3760
3823
|
`failed to read ${path2}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -3814,7 +3877,7 @@ function stringifyUserConfig(cfg) {
|
|
|
3814
3877
|
}
|
|
3815
3878
|
function writeUserConfig(cfg) {
|
|
3816
3879
|
const path2 = userConfigPath();
|
|
3817
|
-
const dir =
|
|
3880
|
+
const dir = dirname2(path2);
|
|
3818
3881
|
if (!existsSync3(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3819
3882
|
const tmp = `${path2}.tmp.${process.pid}`;
|
|
3820
3883
|
writeFileSync2(tmp, stringifyUserConfig(cfg), { mode: 384 });
|
|
@@ -4625,7 +4688,7 @@ function stripLastLineVerdict(text) {
|
|
|
4625
4688
|
|
|
4626
4689
|
// src/lib/llmNotice.ts
|
|
4627
4690
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4628
|
-
import { dirname as
|
|
4691
|
+
import { dirname as dirname3 } from "path";
|
|
4629
4692
|
function maybePrintLlmNotice(repoRoot) {
|
|
4630
4693
|
if (process.env.STAMP_SUPPRESS_LLM_NOTICE === "1") return;
|
|
4631
4694
|
const marker = stampLlmNoticeMarkerPath(repoRoot);
|
|
@@ -4638,7 +4701,7 @@ function maybePrintLlmNotice(repoRoot) {
|
|
|
4638
4701
|
`
|
|
4639
4702
|
);
|
|
4640
4703
|
try {
|
|
4641
|
-
mkdirSync3(
|
|
4704
|
+
mkdirSync3(dirname3(marker), { recursive: true });
|
|
4642
4705
|
writeFileSync4(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
4643
4706
|
`);
|
|
4644
4707
|
} catch {
|
|
@@ -4878,7 +4941,7 @@ import { spawnSync as spawnSync4 } from "child_process";
|
|
|
4878
4941
|
import { createInterface } from "readline";
|
|
4879
4942
|
|
|
4880
4943
|
// src/lib/serverConfig.ts
|
|
4881
|
-
import { existsSync as existsSync5, readFileSync as
|
|
4944
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
4882
4945
|
import { parse as parseYaml7 } from "yaml";
|
|
4883
4946
|
var DEFAULT_USER = "git";
|
|
4884
4947
|
var DEFAULT_REPO_ROOT = "/srv/git";
|
|
@@ -4908,7 +4971,7 @@ function loadServerConfig() {
|
|
|
4908
4971
|
if (!existsSync5(path2)) return null;
|
|
4909
4972
|
let raw;
|
|
4910
4973
|
try {
|
|
4911
|
-
raw =
|
|
4974
|
+
raw = readFileSync6(path2, "utf8");
|
|
4912
4975
|
} catch (err) {
|
|
4913
4976
|
throw new Error(
|
|
4914
4977
|
`failed to read ${path2}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -5887,27 +5950,27 @@ function readSeedDir(seedDir) {
|
|
|
5887
5950
|
if (!existsSync7(seedDir) || !statSync(seedDir).isDirectory()) {
|
|
5888
5951
|
throw new Error(`--from path is not a directory: ${seedDir}`);
|
|
5889
5952
|
}
|
|
5890
|
-
const configPath =
|
|
5953
|
+
const configPath = join4(seedDir, "config.yml");
|
|
5891
5954
|
if (!existsSync7(configPath)) {
|
|
5892
5955
|
throw new Error(`--from dir missing config.yml: ${configPath}`);
|
|
5893
5956
|
}
|
|
5894
|
-
const reviewersDir =
|
|
5957
|
+
const reviewersDir = join4(seedDir, "reviewers");
|
|
5895
5958
|
if (!existsSync7(reviewersDir) || !statSync(reviewersDir).isDirectory()) {
|
|
5896
5959
|
throw new Error(`--from dir missing reviewers/ subdirectory: ${reviewersDir}`);
|
|
5897
5960
|
}
|
|
5898
|
-
const yaml =
|
|
5961
|
+
const yaml = readFileSync7(configPath, "utf8");
|
|
5899
5962
|
const config2 = parseConfigFromYaml(yaml);
|
|
5900
5963
|
const reviewerFiles = /* @__PURE__ */ new Map();
|
|
5901
5964
|
for (const entry of readdirSync(reviewersDir)) {
|
|
5902
|
-
const full =
|
|
5965
|
+
const full = join4(reviewersDir, entry);
|
|
5903
5966
|
if (statSync(full).isFile()) {
|
|
5904
|
-
reviewerFiles.set(`.stamp/reviewers/${entry}`,
|
|
5967
|
+
reviewerFiles.set(`.stamp/reviewers/${entry}`, readFileSync7(full, "utf8"));
|
|
5905
5968
|
}
|
|
5906
5969
|
}
|
|
5907
5970
|
let mirrorYml;
|
|
5908
|
-
const mirrorPath =
|
|
5971
|
+
const mirrorPath = join4(seedDir, "mirror.yml");
|
|
5909
5972
|
if (existsSync7(mirrorPath)) {
|
|
5910
|
-
mirrorYml =
|
|
5973
|
+
mirrorYml = readFileSync7(mirrorPath, "utf8");
|
|
5911
5974
|
}
|
|
5912
5975
|
return { config: config2, reviewerFiles, mirrorYml };
|
|
5913
5976
|
}
|
|
@@ -5915,12 +5978,12 @@ function writeBootstrapFiles(repoRoot, plan) {
|
|
|
5915
5978
|
ensureDir(stampConfigDir(repoRoot));
|
|
5916
5979
|
ensureDir(stampReviewersDir(repoRoot));
|
|
5917
5980
|
for (const { path: path2, content } of plan.reviewerFiles.values()) {
|
|
5918
|
-
const full =
|
|
5919
|
-
ensureDir(
|
|
5981
|
+
const full = join4(repoRoot, path2);
|
|
5982
|
+
ensureDir(dirname4(full));
|
|
5920
5983
|
writeFileSync5(full, content);
|
|
5921
5984
|
}
|
|
5922
5985
|
if (plan.mirrorYml !== void 0) {
|
|
5923
|
-
writeFileSync5(
|
|
5986
|
+
writeFileSync5(join4(repoRoot, ".stamp/mirror.yml"), plan.mirrorYml);
|
|
5924
5987
|
}
|
|
5925
5988
|
writeFileSync5(stampConfigFile(repoRoot), stringifyConfig(plan.newConfig));
|
|
5926
5989
|
}
|
|
@@ -5963,8 +6026,8 @@ function branchExists(name, cwd) {
|
|
|
5963
6026
|
}
|
|
5964
6027
|
|
|
5965
6028
|
// src/commands/init.ts
|
|
5966
|
-
import { existsSync as existsSync9, readFileSync as
|
|
5967
|
-
import { dirname as
|
|
6029
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
6030
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
5968
6031
|
|
|
5969
6032
|
// src/lib/ghRuleset.ts
|
|
5970
6033
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -6300,14 +6363,14 @@ function computeDesiredBypassActors(current, deployKeyId, flags) {
|
|
|
6300
6363
|
}
|
|
6301
6364
|
|
|
6302
6365
|
// src/lib/oteamConfig.ts
|
|
6303
|
-
import { existsSync as existsSync8, readFileSync as
|
|
6366
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
6304
6367
|
import { homedir } from "os";
|
|
6305
|
-
import { join as
|
|
6306
|
-
var OTEAM_CONFIG_PATH =
|
|
6368
|
+
import { join as join5 } from "path";
|
|
6369
|
+
var OTEAM_CONFIG_PATH = join5(homedir(), ".open-team", "config.json");
|
|
6307
6370
|
function readOteamConfig(configPath = OTEAM_CONFIG_PATH) {
|
|
6308
6371
|
if (!existsSync8(configPath)) return null;
|
|
6309
6372
|
try {
|
|
6310
|
-
const parsed = JSON.parse(
|
|
6373
|
+
const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
6311
6374
|
if (Array.isArray(parsed)) {
|
|
6312
6375
|
throw new Error("config must be a JSON object, not an array");
|
|
6313
6376
|
}
|
|
@@ -6322,7 +6385,7 @@ function patchStampHost(host, configPath = OTEAM_CONFIG_PATH) {
|
|
|
6322
6385
|
let config2 = {};
|
|
6323
6386
|
if (existsSync8(configPath)) {
|
|
6324
6387
|
try {
|
|
6325
|
-
const parsed = JSON.parse(
|
|
6388
|
+
const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
6326
6389
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
6327
6390
|
config2 = parsed;
|
|
6328
6391
|
}
|
|
@@ -6348,6 +6411,7 @@ function patchStampHost(host, configPath = OTEAM_CONFIG_PATH) {
|
|
|
6348
6411
|
}
|
|
6349
6412
|
|
|
6350
6413
|
// src/commands/init.ts
|
|
6414
|
+
var DEFAULT_ACTION_SOURCE = "OpenThinkAi/stamp-cli";
|
|
6351
6415
|
function runInit(opts = {}) {
|
|
6352
6416
|
maybePrintDeprecationNotice();
|
|
6353
6417
|
const repoRoot = findRepoRoot();
|
|
@@ -6378,26 +6442,26 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
|
|
|
6378
6442
|
if (!alreadyHasConfig) {
|
|
6379
6443
|
if (opts.minimal) {
|
|
6380
6444
|
writeFileSync7(configFile, stringifyConfig(MINIMAL_CONFIG));
|
|
6381
|
-
writeFileSync7(
|
|
6445
|
+
writeFileSync7(join6(reviewersDir, "example.md"), EXAMPLE_REVIEWER_PROMPT);
|
|
6382
6446
|
} else {
|
|
6383
6447
|
writeFileSync7(configFile, stringifyConfig(DEFAULT_CONFIG));
|
|
6384
6448
|
writeFileSync7(
|
|
6385
|
-
|
|
6449
|
+
join6(reviewersDir, "security.md"),
|
|
6386
6450
|
DEFAULT_SECURITY_PROMPT
|
|
6387
6451
|
);
|
|
6388
6452
|
writeFileSync7(
|
|
6389
|
-
|
|
6453
|
+
join6(reviewersDir, "standards.md"),
|
|
6390
6454
|
DEFAULT_STANDARDS_PROMPT
|
|
6391
6455
|
);
|
|
6392
6456
|
writeFileSync7(
|
|
6393
|
-
|
|
6457
|
+
join6(reviewersDir, "product.md"),
|
|
6394
6458
|
DEFAULT_PRODUCT_PROMPT
|
|
6395
6459
|
);
|
|
6396
6460
|
}
|
|
6397
6461
|
}
|
|
6398
6462
|
const { keypair, created: keyCreated } = ensureUserKeypair();
|
|
6399
6463
|
const userCfg = loadOrCreateUserConfig();
|
|
6400
|
-
const pubKeyPath =
|
|
6464
|
+
const pubKeyPath = join6(
|
|
6401
6465
|
trustedKeysDir,
|
|
6402
6466
|
publicKeyFingerprintFilename(keypair.fingerprint)
|
|
6403
6467
|
);
|
|
@@ -6411,7 +6475,8 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
|
|
|
6411
6475
|
const prCheckResult = maybeWriteVerifyWorkflow(
|
|
6412
6476
|
repoRoot,
|
|
6413
6477
|
opts.prCheck,
|
|
6414
|
-
effectiveMode
|
|
6478
|
+
effectiveMode,
|
|
6479
|
+
opts.actionSource ?? DEFAULT_ACTION_SOURCE
|
|
6415
6480
|
);
|
|
6416
6481
|
const prModeResult = opts.prMode ? maybeWritePrModeMirrorWorkflow(repoRoot, { force: opts.prModeForce }) : { action: "skipped", path: PR_MODE_WORKFLOW_PATH, substitution: null };
|
|
6417
6482
|
const agentsMdAction = opts.agentsMd === false ? "skipped" : ensureAgentsMd(repoRoot, effectiveMode);
|
|
@@ -6563,8 +6628,8 @@ function runBootstrapCommit(repoRoot, scaffoldOrSync) {
|
|
|
6563
6628
|
return { kind: "skipped-already-tracked" };
|
|
6564
6629
|
}
|
|
6565
6630
|
const toAdd = [".stamp"];
|
|
6566
|
-
if (existsSync9(
|
|
6567
|
-
if (existsSync9(
|
|
6631
|
+
if (existsSync9(join6(repoRoot, "AGENTS.md"))) toAdd.push("AGENTS.md");
|
|
6632
|
+
if (existsSync9(join6(repoRoot, "CLAUDE.md"))) toAdd.push("CLAUDE.md");
|
|
6568
6633
|
runGit(["add", ...toAdd], repoRoot);
|
|
6569
6634
|
let hasStagedChanges = false;
|
|
6570
6635
|
try {
|
|
@@ -6769,27 +6834,27 @@ function resolveMode(userMode, remoteClass) {
|
|
|
6769
6834
|
}
|
|
6770
6835
|
}
|
|
6771
6836
|
var VERIFY_ACTION_REF = "v1.6.1";
|
|
6772
|
-
function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode) {
|
|
6837
|
+
function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode, actionSource = DEFAULT_ACTION_SOURCE) {
|
|
6773
6838
|
const path2 = ".github/workflows/stamp-verify.yml";
|
|
6774
|
-
const fullPath =
|
|
6839
|
+
const fullPath = join6(repoRoot, path2);
|
|
6775
6840
|
const defaultForMode = effectiveMode !== "server-gated";
|
|
6776
6841
|
const shouldWrite = prCheckOpt ?? defaultForMode;
|
|
6777
6842
|
if (!shouldWrite) return { action: "skipped", path: path2 };
|
|
6778
6843
|
if (existsSync9(fullPath)) {
|
|
6779
6844
|
return { action: "exists", path: path2 };
|
|
6780
6845
|
}
|
|
6781
|
-
ensureDir(
|
|
6782
|
-
writeFileSync7(fullPath, renderVerifyWorkflow());
|
|
6846
|
+
ensureDir(dirname5(fullPath));
|
|
6847
|
+
writeFileSync7(fullPath, renderVerifyWorkflow(actionSource));
|
|
6783
6848
|
return { action: "wrote", path: path2 };
|
|
6784
6849
|
}
|
|
6785
6850
|
var PR_MODE_WORKFLOW_PATH = ".github/workflows/stamp-mirror.yml";
|
|
6786
6851
|
function maybeWritePrModeMirrorWorkflow(repoRoot, opts = {}) {
|
|
6787
|
-
const fullPath =
|
|
6852
|
+
const fullPath = join6(repoRoot, PR_MODE_WORKFLOW_PATH);
|
|
6788
6853
|
const substitution = derivePrModeSubstitution(repoRoot);
|
|
6789
6854
|
if (existsSync9(fullPath) && !opts.force) {
|
|
6790
6855
|
return { action: "exists", path: PR_MODE_WORKFLOW_PATH, substitution };
|
|
6791
6856
|
}
|
|
6792
|
-
ensureDir(
|
|
6857
|
+
ensureDir(dirname5(fullPath));
|
|
6793
6858
|
writeFileSync7(fullPath, renderMirrorWorkflow(substitution));
|
|
6794
6859
|
return { action: "wrote", path: PR_MODE_WORKFLOW_PATH, substitution };
|
|
6795
6860
|
}
|
|
@@ -6797,9 +6862,9 @@ function derivePrModeSubstitution(repoRoot) {
|
|
|
6797
6862
|
let host = null;
|
|
6798
6863
|
let port = null;
|
|
6799
6864
|
try {
|
|
6800
|
-
const configFile =
|
|
6865
|
+
const configFile = join6(repoRoot, ".stamp", "config.yml");
|
|
6801
6866
|
if (existsSync9(configFile)) {
|
|
6802
|
-
const cfg = parseConfigFromYaml(
|
|
6867
|
+
const cfg = parseConfigFromYaml(readFileSync9(configFile, "utf8"));
|
|
6803
6868
|
for (const rule of Object.values(cfg.branches)) {
|
|
6804
6869
|
if (rule.review_server) {
|
|
6805
6870
|
try {
|
|
@@ -7039,7 +7104,7 @@ function printPrModeWalkthrough(result) {
|
|
|
7039
7104
|
);
|
|
7040
7105
|
console.log();
|
|
7041
7106
|
}
|
|
7042
|
-
function renderVerifyWorkflow() {
|
|
7107
|
+
function renderVerifyWorkflow(actionSource = DEFAULT_ACTION_SOURCE) {
|
|
7043
7108
|
return [
|
|
7044
7109
|
"name: stamp verify",
|
|
7045
7110
|
"",
|
|
@@ -7072,18 +7137,18 @@ function renderVerifyWorkflow() {
|
|
|
7072
7137
|
" # would force per-step refetches.",
|
|
7073
7138
|
" fetch-depth: 0",
|
|
7074
7139
|
" - name: stamp/verify-attestation",
|
|
7075
|
-
` uses:
|
|
7140
|
+
` uses: ${actionSource}/.github/actions/verify-attestation@${VERIFY_ACTION_REF}`,
|
|
7076
7141
|
""
|
|
7077
7142
|
].join("\n");
|
|
7078
7143
|
}
|
|
7079
7144
|
|
|
7080
7145
|
// src/commands/migrateServerAttested.ts
|
|
7081
|
-
import { existsSync as existsSync10, readFileSync as
|
|
7082
|
-
import { dirname as
|
|
7146
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
7147
|
+
import { dirname as dirname6, join as join8, relative } from "path";
|
|
7083
7148
|
|
|
7084
7149
|
// src/lib/migrateServerAttested.ts
|
|
7085
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
7086
|
-
import { join as
|
|
7150
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
7151
|
+
import { join as join7 } from "path";
|
|
7087
7152
|
function detectExistingKeys(repoRoot, onSkip) {
|
|
7088
7153
|
const dir = stampTrustedKeysDir(repoRoot);
|
|
7089
7154
|
let entries;
|
|
@@ -7097,7 +7162,7 @@ function detectExistingKeys(repoRoot, onSkip) {
|
|
|
7097
7162
|
const out = [];
|
|
7098
7163
|
for (const filename of entries.sort()) {
|
|
7099
7164
|
if (!filename.endsWith(".pub")) continue;
|
|
7100
|
-
const full =
|
|
7165
|
+
const full = join7(dir, filename);
|
|
7101
7166
|
try {
|
|
7102
7167
|
const st = statSync2(full);
|
|
7103
7168
|
if (!st.isFile()) continue;
|
|
@@ -7106,7 +7171,7 @@ function detectExistingKeys(repoRoot, onSkip) {
|
|
|
7106
7171
|
}
|
|
7107
7172
|
let pem;
|
|
7108
7173
|
try {
|
|
7109
|
-
pem =
|
|
7174
|
+
pem = readFileSync10(full, "utf8");
|
|
7110
7175
|
} catch (err) {
|
|
7111
7176
|
onSkip?.(filename, err instanceof Error ? err.message : String(err));
|
|
7112
7177
|
continue;
|
|
@@ -7294,7 +7359,7 @@ function runMigrateToServerAttested(opts = {}) {
|
|
|
7294
7359
|
console.error(`note: skipping .stamp/trusted-keys/${filename} \u2014 ${reason}`);
|
|
7295
7360
|
})
|
|
7296
7361
|
);
|
|
7297
|
-
const manifestPath =
|
|
7362
|
+
const manifestPath = join8(repoRoot, MANIFEST_RELATIVE_PATH);
|
|
7298
7363
|
const manifestExists = existsSync10(manifestPath);
|
|
7299
7364
|
let adminFingerprints;
|
|
7300
7365
|
if (manifestExists) {
|
|
@@ -7327,7 +7392,7 @@ function runMigrateToServerAttested(opts = {}) {
|
|
|
7327
7392
|
`expected .stamp/config.yml at ${cfgPath} but found none. Run \`stamp init\` first.`
|
|
7328
7393
|
);
|
|
7329
7394
|
}
|
|
7330
|
-
const cfgInput =
|
|
7395
|
+
const cfgInput = readFileSync11(cfgPath, "utf8");
|
|
7331
7396
|
const rewrite = rewriteConfigForMigration(cfgInput);
|
|
7332
7397
|
if (dryRun) {
|
|
7333
7398
|
printDryRun({
|
|
@@ -7351,10 +7416,10 @@ function runMigrateToServerAttested(opts = {}) {
|
|
|
7351
7416
|
console.error(`warning: ${w}`);
|
|
7352
7417
|
}
|
|
7353
7418
|
let manifestAction;
|
|
7354
|
-
if (manifestExists &&
|
|
7419
|
+
if (manifestExists && readFileSync11(manifestPath, "utf8") === manifestText) {
|
|
7355
7420
|
manifestAction = "unchanged";
|
|
7356
7421
|
} else {
|
|
7357
|
-
ensureDir(
|
|
7422
|
+
ensureDir(dirname6(manifestPath));
|
|
7358
7423
|
writeFileSync8(manifestPath, manifestText);
|
|
7359
7424
|
manifestAction = "wrote";
|
|
7360
7425
|
}
|
|
@@ -7420,7 +7485,7 @@ function readExistingAdminFingerprints(manifestPath) {
|
|
|
7420
7485
|
const out = /* @__PURE__ */ new Set();
|
|
7421
7486
|
let text;
|
|
7422
7487
|
try {
|
|
7423
|
-
text =
|
|
7488
|
+
text = readFileSync11(manifestPath, "utf8");
|
|
7424
7489
|
} catch {
|
|
7425
7490
|
return out;
|
|
7426
7491
|
}
|
|
@@ -7495,9 +7560,9 @@ function indent(text, prefix) {
|
|
|
7495
7560
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
7496
7561
|
import { request as httpRequest } from "http";
|
|
7497
7562
|
import { request as httpsRequest } from "https";
|
|
7498
|
-
import { existsSync as existsSync11, readFileSync as
|
|
7563
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
7499
7564
|
import { homedir as homedir2, hostname, userInfo } from "os";
|
|
7500
|
-
import { join as
|
|
7565
|
+
import { join as join9 } from "path";
|
|
7501
7566
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7502
7567
|
|
|
7503
7568
|
// src/lib/inviteUrl.ts
|
|
@@ -7664,10 +7729,10 @@ function runInvitesMint(opts) {
|
|
|
7664
7729
|
}
|
|
7665
7730
|
}
|
|
7666
7731
|
function defaultSshPubkeyPath() {
|
|
7667
|
-
return
|
|
7732
|
+
return join9(homedir2(), ".ssh", "id_ed25519.pub");
|
|
7668
7733
|
}
|
|
7669
7734
|
function defaultStampPubkeyPath() {
|
|
7670
|
-
return
|
|
7735
|
+
return join9(homedir2(), ".stamp", "keys", "ed25519.pub");
|
|
7671
7736
|
}
|
|
7672
7737
|
function defaultShortName() {
|
|
7673
7738
|
const u = userInfo().username || "user";
|
|
@@ -7680,13 +7745,13 @@ function loadSshPubkey(path2) {
|
|
|
7680
7745
|
`SSH pubkey not found at ${path2}. Generate one with \`ssh-keygen -t ed25519\` or pass --ssh-pubkey <path>.`
|
|
7681
7746
|
);
|
|
7682
7747
|
}
|
|
7683
|
-
const raw =
|
|
7748
|
+
const raw = readFileSync12(path2, "utf8");
|
|
7684
7749
|
const parsed = parseSshPubkey(raw);
|
|
7685
7750
|
return { path: path2, full: parsed.full, fingerprint: parsed.fingerprint };
|
|
7686
7751
|
}
|
|
7687
7752
|
function loadStampPubkey(path2) {
|
|
7688
7753
|
if (!existsSync11(path2)) return null;
|
|
7689
|
-
const pem =
|
|
7754
|
+
const pem = readFileSync12(path2, "utf8");
|
|
7690
7755
|
try {
|
|
7691
7756
|
const fp = fingerprintFromPem(pem);
|
|
7692
7757
|
return { path: path2, pem, fingerprint: fp };
|
|
@@ -7831,15 +7896,15 @@ async function runInvitesAccept(opts) {
|
|
|
7831
7896
|
|
|
7832
7897
|
// src/commands/provision.ts
|
|
7833
7898
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
7834
|
-
import { existsSync as existsSync13, mkdtempSync, readFileSync as
|
|
7899
|
+
import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
|
|
7835
7900
|
import { tmpdir } from "os";
|
|
7836
|
-
import { join as
|
|
7901
|
+
import { join as join10, resolve as resolvePath } from "path";
|
|
7837
7902
|
import { parse as parseYaml8 } from "yaml";
|
|
7838
7903
|
|
|
7839
7904
|
// src/commands/server.ts
|
|
7840
7905
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
7841
7906
|
import { existsSync as existsSync12, mkdirSync as mkdirSync4, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
|
|
7842
|
-
import { dirname as
|
|
7907
|
+
import { dirname as dirname7 } from "path";
|
|
7843
7908
|
import { stringify as stringifyYaml2 } from "yaml";
|
|
7844
7909
|
|
|
7845
7910
|
// src/lib/perRepoKey.ts
|
|
@@ -7998,7 +8063,7 @@ function writeConfig(opts) {
|
|
|
7998
8063
|
repoRootPrefix: opts.repoRootPrefix
|
|
7999
8064
|
});
|
|
8000
8065
|
const path2 = userServerConfigPath();
|
|
8001
|
-
const dir =
|
|
8066
|
+
const dir = dirname7(path2);
|
|
8002
8067
|
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
8003
8068
|
const tmp = `${path2}.tmp.${process.pid}`;
|
|
8004
8069
|
writeFileSync9(tmp, yaml, { mode: 384 });
|
|
@@ -8311,9 +8376,9 @@ async function runMigrateExisting(opts, server2) {
|
|
|
8311
8376
|
console.log("\n(dry run \u2014 no changes made)");
|
|
8312
8377
|
return;
|
|
8313
8378
|
}
|
|
8314
|
-
const stagingDir = mkdtempSync(
|
|
8315
|
-
const bareCloneDir =
|
|
8316
|
-
const tarballPath =
|
|
8379
|
+
const stagingDir = mkdtempSync(join10(tmpdir(), "stamp-migrate-"));
|
|
8380
|
+
const bareCloneDir = join10(stagingDir, `${opts.name}.git`);
|
|
8381
|
+
const tarballPath = join10(stagingDir, `${opts.name}.tar.gz`);
|
|
8317
8382
|
try {
|
|
8318
8383
|
console.log(`
|
|
8319
8384
|
Building bare-clone tarball of existing repo`);
|
|
@@ -8349,9 +8414,9 @@ function ensureCwdIsGitRepo(cwd) {
|
|
|
8349
8414
|
}
|
|
8350
8415
|
}
|
|
8351
8416
|
function ensureStampInitDone(cwd) {
|
|
8352
|
-
if (!existsSync13(
|
|
8417
|
+
if (!existsSync13(join10(cwd, ".stamp", "config.yml"))) {
|
|
8353
8418
|
throw new Error(
|
|
8354
|
-
`--migrate-existing expects this repo to already be stamp-init'd (${
|
|
8419
|
+
`--migrate-existing expects this repo to already be stamp-init'd (${join10(cwd, ".stamp/config.yml")} not found). Run \`stamp init --mode local-only\` first, calibrate your reviewers, then re-run with --migrate-existing.`
|
|
8355
8420
|
);
|
|
8356
8421
|
}
|
|
8357
8422
|
}
|
|
@@ -8476,7 +8541,7 @@ mirror.yml was added to .stamp/. Commit it through the normal stamp flow:`);
|
|
|
8476
8541
|
}
|
|
8477
8542
|
}
|
|
8478
8543
|
function readMirrorYmlGithubRepo(repoRoot) {
|
|
8479
|
-
const path2 =
|
|
8544
|
+
const path2 = join10(repoRoot, ".stamp", "mirror.yml");
|
|
8480
8545
|
if (!existsSync13(path2)) {
|
|
8481
8546
|
throw new Error(
|
|
8482
8547
|
`${path2} not found \u2014 --migrate-bypass operates on an already-server-gated repo, but this cwd has no .stamp/mirror.yml. If the repo is not yet server-gated, provision it first with \`stamp provision --migrate-existing\`.`
|
|
@@ -8484,7 +8549,7 @@ function readMirrorYmlGithubRepo(repoRoot) {
|
|
|
8484
8549
|
}
|
|
8485
8550
|
let raw;
|
|
8486
8551
|
try {
|
|
8487
|
-
raw =
|
|
8552
|
+
raw = readFileSync13(path2, "utf8");
|
|
8488
8553
|
} catch (err) {
|
|
8489
8554
|
throw new Error(
|
|
8490
8555
|
`could not read ${path2}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -8724,10 +8789,10 @@ import {
|
|
|
8724
8789
|
import {
|
|
8725
8790
|
existsSync as existsSync14,
|
|
8726
8791
|
readdirSync as readdirSync3,
|
|
8727
|
-
readFileSync as
|
|
8792
|
+
readFileSync as readFileSync14,
|
|
8728
8793
|
writeFileSync as writeFileSync11
|
|
8729
8794
|
} from "fs";
|
|
8730
|
-
import { join as
|
|
8795
|
+
import { join as join11, resolve } from "path";
|
|
8731
8796
|
var USERS_EXIT = {
|
|
8732
8797
|
OK: 0,
|
|
8733
8798
|
CONFIG: 1,
|
|
@@ -8786,10 +8851,10 @@ function findExistingTrustedKey(repoRoot, fingerprint) {
|
|
|
8786
8851
|
if (!existsSync14(dir)) return null;
|
|
8787
8852
|
for (const f of readdirSync3(dir)) {
|
|
8788
8853
|
if (!f.endsWith(".pub")) continue;
|
|
8789
|
-
const fullPath =
|
|
8854
|
+
const fullPath = join11(dir, f);
|
|
8790
8855
|
let pem;
|
|
8791
8856
|
try {
|
|
8792
|
-
pem =
|
|
8857
|
+
pem = readFileSync14(fullPath, "utf8");
|
|
8793
8858
|
} catch {
|
|
8794
8859
|
continue;
|
|
8795
8860
|
}
|
|
@@ -8841,7 +8906,7 @@ function runTrustGrant(opts) {
|
|
|
8841
8906
|
);
|
|
8842
8907
|
}
|
|
8843
8908
|
const repoRoot = resolve(opts.repoPath ?? process.cwd());
|
|
8844
|
-
if (!existsSync14(
|
|
8909
|
+
if (!existsSync14(join11(repoRoot, ".git"))) {
|
|
8845
8910
|
throw new UsageError(`${repoRoot} is not a git repository`);
|
|
8846
8911
|
}
|
|
8847
8912
|
if (!existsSync14(stampConfigDir(repoRoot))) {
|
|
@@ -8883,7 +8948,7 @@ function runTrustGrant(opts) {
|
|
|
8883
8948
|
runGit2(["checkout", "-b", branch], repoRoot);
|
|
8884
8949
|
const keysDir = stampTrustedKeysDir(repoRoot);
|
|
8885
8950
|
ensureDir(keysDir, 493);
|
|
8886
|
-
const keyFile =
|
|
8951
|
+
const keyFile = join11(keysDir, `${opts.shortName}.pub`);
|
|
8887
8952
|
writeFileSync11(keyFile, pem, { mode: 420 });
|
|
8888
8953
|
runGit2(["add", keyFile], repoRoot);
|
|
8889
8954
|
runGit2(
|
|
@@ -9081,8 +9146,8 @@ function runUsersRemove(opts) {
|
|
|
9081
9146
|
}
|
|
9082
9147
|
|
|
9083
9148
|
// src/commands/keys.ts
|
|
9084
|
-
import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as
|
|
9085
|
-
import { basename, join as
|
|
9149
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
|
|
9150
|
+
import { basename, join as join12 } from "path";
|
|
9086
9151
|
function keysGenerate() {
|
|
9087
9152
|
const existing = loadUserKeypair();
|
|
9088
9153
|
if (existing) {
|
|
@@ -9126,7 +9191,7 @@ function keysList() {
|
|
|
9126
9191
|
}
|
|
9127
9192
|
for (const file of pubFiles.sort()) {
|
|
9128
9193
|
try {
|
|
9129
|
-
const pem =
|
|
9194
|
+
const pem = readFileSync15(join12(trustedDir, file), "utf8");
|
|
9130
9195
|
const fp = fingerprintFromPem(pem);
|
|
9131
9196
|
const marker = local && fp === local.fingerprint ? " (you)" : "";
|
|
9132
9197
|
console.log(` ${fp}${marker} [${file}]`);
|
|
@@ -9153,7 +9218,7 @@ function keysTrust(pubFile) {
|
|
|
9153
9218
|
if (!existsSync15(pubFile)) {
|
|
9154
9219
|
throw new Error(`public key file not found: ${pubFile}`);
|
|
9155
9220
|
}
|
|
9156
|
-
const pem =
|
|
9221
|
+
const pem = readFileSync15(pubFile, "utf8");
|
|
9157
9222
|
let fingerprint;
|
|
9158
9223
|
try {
|
|
9159
9224
|
fingerprint = fingerprintFromPem(pem);
|
|
@@ -9163,7 +9228,7 @@ function keysTrust(pubFile) {
|
|
|
9163
9228
|
);
|
|
9164
9229
|
}
|
|
9165
9230
|
const filename = publicKeyFingerprintFilename(fingerprint);
|
|
9166
|
-
const dest =
|
|
9231
|
+
const dest = join12(trustedDir, filename);
|
|
9167
9232
|
if (existsSync15(dest)) {
|
|
9168
9233
|
console.log(`${fingerprint} is already trusted (${basename(dest)})`);
|
|
9169
9234
|
return;
|
|
@@ -9623,14 +9688,14 @@ function gitDiffBetween(repoRoot, base, head) {
|
|
|
9623
9688
|
|
|
9624
9689
|
// src/commands/adminRotate.ts
|
|
9625
9690
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
9626
|
-
import { copyFileSync, existsSync as existsSync16, readFileSync as
|
|
9627
|
-
import { join as
|
|
9691
|
+
import { copyFileSync, existsSync as existsSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
9692
|
+
import { join as join13, relative as relative2 } from "path";
|
|
9628
9693
|
var NAME_PATTERN2 = /^[A-Za-z0-9_.-]+$/;
|
|
9629
9694
|
var FINGERPRINT_PATTERN2 = /^sha256:[0-9a-f]{64}$/;
|
|
9630
9695
|
var KNOWN_CAPABILITIES2 = ["admin", "operator", "server"];
|
|
9631
9696
|
function runAdminAddKey(opts) {
|
|
9632
9697
|
const repoRoot = findRepoRoot();
|
|
9633
|
-
const manifestPath =
|
|
9698
|
+
const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
|
|
9634
9699
|
const trustedDir = stampTrustedKeysDir(repoRoot);
|
|
9635
9700
|
if (!NAME_PATTERN2.test(opts.name)) {
|
|
9636
9701
|
throw new Error(
|
|
@@ -9641,7 +9706,7 @@ function runAdminAddKey(opts) {
|
|
|
9641
9706
|
if (!existsSync16(opts.pubkeyPath)) {
|
|
9642
9707
|
throw new Error(`pubkey file not found: ${opts.pubkeyPath}`);
|
|
9643
9708
|
}
|
|
9644
|
-
const pem =
|
|
9709
|
+
const pem = readFileSync16(opts.pubkeyPath, "utf8");
|
|
9645
9710
|
if (!pem.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
9646
9711
|
throw new Error(
|
|
9647
9712
|
`${opts.pubkeyPath} does not look like a public key PEM (no "-----BEGIN PUBLIC KEY-----" header). Refusing to import \u2014 did you accidentally pass the private key path? Public keys live at ~/.stamp/keys/ed25519.pub (note the .pub suffix).`
|
|
@@ -9660,7 +9725,7 @@ function runAdminAddKey(opts) {
|
|
|
9660
9725
|
`no manifest at ${MANIFEST_RELATIVE_PATH}. Run \`stamp init --migrate-to-server-attested\` to bootstrap one, then re-run \`stamp admin add-key\`.`
|
|
9661
9726
|
);
|
|
9662
9727
|
}
|
|
9663
|
-
const existing = parseManifest(
|
|
9728
|
+
const existing = parseManifest(readFileSync16(manifestPath, "utf8"));
|
|
9664
9729
|
if (!existing) {
|
|
9665
9730
|
throw new Error(
|
|
9666
9731
|
`${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file (see \`stamp admin list-keys\` for the current state) before adding a new key.`
|
|
@@ -9695,7 +9760,7 @@ function runAdminAddKey(opts) {
|
|
|
9695
9760
|
}
|
|
9696
9761
|
writeFileSync13(manifestPath, yamlText);
|
|
9697
9762
|
const pubDestFilename = publicKeyFingerprintFilename(fingerprint);
|
|
9698
|
-
const pubDestPath =
|
|
9763
|
+
const pubDestPath = join13(trustedDir, pubDestFilename);
|
|
9699
9764
|
if (!existsSync16(pubDestPath)) {
|
|
9700
9765
|
copyFileSync(opts.pubkeyPath, pubDestPath);
|
|
9701
9766
|
}
|
|
@@ -9720,7 +9785,7 @@ function runAdminAddKey(opts) {
|
|
|
9720
9785
|
}
|
|
9721
9786
|
function runAdminRevoke(opts) {
|
|
9722
9787
|
const repoRoot = findRepoRoot();
|
|
9723
|
-
const manifestPath =
|
|
9788
|
+
const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
|
|
9724
9789
|
if (!FINGERPRINT_PATTERN2.test(opts.fingerprint)) {
|
|
9725
9790
|
throw new Error(
|
|
9726
9791
|
`fingerprint ${JSON.stringify(opts.fingerprint)} is not in the expected sha256:<64-hex> form. Run \`stamp admin list-keys\` to copy a fingerprint exactly.`
|
|
@@ -9731,7 +9796,7 @@ function runAdminRevoke(opts) {
|
|
|
9731
9796
|
`no manifest at ${MANIFEST_RELATIVE_PATH} \u2014 nothing to revoke.`
|
|
9732
9797
|
);
|
|
9733
9798
|
}
|
|
9734
|
-
const existing = parseManifest(
|
|
9799
|
+
const existing = parseManifest(readFileSync16(manifestPath, "utf8"));
|
|
9735
9800
|
if (!existing) {
|
|
9736
9801
|
throw new Error(
|
|
9737
9802
|
`${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file before revoking.`
|
|
@@ -9792,7 +9857,7 @@ function runAdminRevoke(opts) {
|
|
|
9792
9857
|
}
|
|
9793
9858
|
function runAdminListKeys(opts = {}) {
|
|
9794
9859
|
const repoRoot = findRepoRoot();
|
|
9795
|
-
const manifestPath =
|
|
9860
|
+
const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
|
|
9796
9861
|
if (!existsSync16(manifestPath)) {
|
|
9797
9862
|
if (opts.json) {
|
|
9798
9863
|
process.stdout.write(JSON.stringify({ entries: [] }, null, 2) + "\n");
|
|
@@ -9804,7 +9869,7 @@ function runAdminListKeys(opts = {}) {
|
|
|
9804
9869
|
);
|
|
9805
9870
|
return;
|
|
9806
9871
|
}
|
|
9807
|
-
const manifest = parseManifest(
|
|
9872
|
+
const manifest = parseManifest(readFileSync16(manifestPath, "utf8"));
|
|
9808
9873
|
if (!manifest) {
|
|
9809
9874
|
throw new Error(
|
|
9810
9875
|
`${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file by hand (see \`git show HEAD:${MANIFEST_RELATIVE_PATH}\` for the last good version) before continuing.`
|
|
@@ -10123,6 +10188,7 @@ function patchIdForSpan(base_sha, head_sha, repoRoot) {
|
|
|
10123
10188
|
|
|
10124
10189
|
// src/lib/prAttestation.ts
|
|
10125
10190
|
import { spawnSync as spawnSync13 } from "child_process";
|
|
10191
|
+
var PR_ATTESTATION_SCHEMA_VERSION = 3;
|
|
10126
10192
|
var LEGACY_CLIENT_PR_ATTESTATION_SCHEMA_VERSION = 2;
|
|
10127
10193
|
var MIN_ACCEPTED_PR_ATTESTATION_VERSION = 3;
|
|
10128
10194
|
var MAX_PR_ATTESTATION_BYTES = 64 * 1024;
|
|
@@ -10236,22 +10302,256 @@ function runAttest(opts) {
|
|
|
10236
10302
|
const branchRef = opts.branch ?? "HEAD";
|
|
10237
10303
|
const revspec = `${opts.into}..${branchRef}`;
|
|
10238
10304
|
const resolved = resolveDiff(revspec, repoRoot);
|
|
10239
|
-
const
|
|
10305
|
+
const patch_id = patchIdForSpan(resolved.base_sha, resolved.head_sha, repoRoot);
|
|
10306
|
+
const target_branch_tip_sha = runGit(
|
|
10307
|
+
["rev-parse", `${opts.into}^{commit}`],
|
|
10308
|
+
repoRoot
|
|
10309
|
+
).trim();
|
|
10310
|
+
const { keypair } = ensureUserKeypair();
|
|
10311
|
+
const result = rule.review_server ? buildV3Envelope({
|
|
10312
|
+
repoRoot,
|
|
10313
|
+
revspec,
|
|
10314
|
+
baseSha: resolved.base_sha,
|
|
10315
|
+
headSha: resolved.head_sha,
|
|
10316
|
+
diff: resolved.diff,
|
|
10317
|
+
targetBranch: opts.into,
|
|
10318
|
+
targetBranchTipSha: target_branch_tip_sha,
|
|
10319
|
+
patchId: patch_id,
|
|
10320
|
+
requiredReviewers: rule.required,
|
|
10321
|
+
operatorPrivateKeyPem: keypair.privateKeyPem,
|
|
10322
|
+
operatorFingerprint: keypair.fingerprint
|
|
10323
|
+
}) : buildV2Envelope({
|
|
10324
|
+
repoRoot,
|
|
10325
|
+
revspec,
|
|
10326
|
+
baseSha: resolved.base_sha,
|
|
10327
|
+
headSha: resolved.head_sha,
|
|
10328
|
+
targetBranch: opts.into,
|
|
10329
|
+
targetBranchTipSha: target_branch_tip_sha,
|
|
10330
|
+
patchId: patch_id,
|
|
10331
|
+
requiredReviewers: rule.required,
|
|
10332
|
+
operatorPrivateKeyPem: keypair.privateKeyPem,
|
|
10333
|
+
operatorFingerprint: keypair.fingerprint
|
|
10334
|
+
});
|
|
10335
|
+
const { ref, blob_sha } = writeAttestationRef(
|
|
10336
|
+
{ payload: result.payload, signature: result.signature },
|
|
10337
|
+
repoRoot
|
|
10338
|
+
);
|
|
10339
|
+
const bar = "\u2500".repeat(72);
|
|
10340
|
+
console.log(bar);
|
|
10341
|
+
console.log(`attested ${branchRef} for merge into '${opts.into}'`);
|
|
10342
|
+
console.log(bar);
|
|
10343
|
+
console.log(` patch-id: ${patch_id}`);
|
|
10344
|
+
console.log(
|
|
10345
|
+
` base\u2192head: ${resolved.base_sha.slice(0, 8)} \u2192 ${resolved.head_sha.slice(0, 8)}`
|
|
10346
|
+
);
|
|
10347
|
+
console.log(` signed by: ${keypair.fingerprint}`);
|
|
10348
|
+
console.log(` approvals: ${result.reviewerNames.join(", ")}`);
|
|
10349
|
+
console.log(
|
|
10350
|
+
` schema: v${result.payload.schema_version}${result.payload.schema_version === PR_ATTESTATION_SCHEMA_VERSION ? " (server-attested)" : " (legacy)"}`
|
|
10351
|
+
);
|
|
10352
|
+
console.log(` ref: ${ref}`);
|
|
10353
|
+
console.log(` blob: ${blob_sha.slice(0, 12)}`);
|
|
10354
|
+
console.log(bar);
|
|
10355
|
+
if (opts.pushTo) {
|
|
10356
|
+
pushBranchAndAttestation(opts.pushTo, ref, repoRoot);
|
|
10357
|
+
console.log(
|
|
10358
|
+
`
|
|
10359
|
+
\u2713 pushed branch + attestation ref to ${opts.pushTo}. Open the PR; stamp/verify-attestation@v1 will look up refs/stamp/attestations/<patch-id> from your head SHA's diff against the base.`
|
|
10360
|
+
);
|
|
10361
|
+
} else {
|
|
10362
|
+
console.log(
|
|
10363
|
+
`
|
|
10364
|
+
Next: push the branch + attestation ref to your remote, open a PR, and let stamp/verify-attestation@v1 (the GH Action) confirm it. To do both pushes in one shot:
|
|
10365
|
+
|
|
10366
|
+
git push <remote> HEAD ${ref}
|
|
10367
|
+
|
|
10368
|
+
Or re-run with --push <remote> next time.`
|
|
10369
|
+
);
|
|
10370
|
+
}
|
|
10371
|
+
}
|
|
10372
|
+
function buildV3Envelope(input) {
|
|
10373
|
+
const diffBytes = Buffer.from(input.diff, "utf8");
|
|
10374
|
+
const diffSha256 = createHash12("sha256").update(diffBytes).digest("hex");
|
|
10375
|
+
let manifestYaml;
|
|
10376
|
+
try {
|
|
10377
|
+
manifestYaml = showAtRef(
|
|
10378
|
+
input.baseSha,
|
|
10379
|
+
".stamp/trusted-keys/manifest.yml",
|
|
10380
|
+
input.repoRoot
|
|
10381
|
+
);
|
|
10382
|
+
} catch (err) {
|
|
10383
|
+
throw new Error(
|
|
10384
|
+
`review_server is configured but .stamp/trusted-keys/manifest.yml is missing at base ${input.baseSha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}. Server-attested PR mode requires the manifest in the merge-base tree so each approval's server signature can be checked against the keys the repo trusted at attestation time. Commit a manifest with capabilities: [server] entries for the review server before attesting.`
|
|
10385
|
+
);
|
|
10386
|
+
}
|
|
10387
|
+
const manifest = parseManifest(manifestYaml);
|
|
10388
|
+
if (!manifest) {
|
|
10389
|
+
throw new Error(
|
|
10390
|
+
`.stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)} failed to parse as a valid trusted-keys manifest. Fix the YAML (syntax error, duplicate fingerprint, unknown capability, etc.) and re-run \`stamp attest\`.`
|
|
10391
|
+
);
|
|
10392
|
+
}
|
|
10393
|
+
const pubFilenames = listFilesAtRef(
|
|
10394
|
+
input.baseSha,
|
|
10395
|
+
".stamp/trusted-keys",
|
|
10396
|
+
input.repoRoot
|
|
10397
|
+
);
|
|
10398
|
+
const pubkeyByFingerprint = buildPubkeyMap(
|
|
10399
|
+
pubFilenames,
|
|
10400
|
+
(relPath) => showAtRef(input.baseSha, relPath, input.repoRoot)
|
|
10401
|
+
);
|
|
10402
|
+
const db = openDb(stampStateDbPath(input.repoRoot));
|
|
10403
|
+
let entries;
|
|
10404
|
+
try {
|
|
10405
|
+
const rows = serverApprovalsFor(db, input.baseSha, input.headSha);
|
|
10406
|
+
const byReviewer = new Map(rows.map((r) => [r.reviewer, r]));
|
|
10407
|
+
entries = input.requiredReviewers.map((reviewerName) => {
|
|
10408
|
+
const row = byReviewer.get(reviewerName);
|
|
10409
|
+
if (!row) {
|
|
10410
|
+
throw new Error(
|
|
10411
|
+
`missing server signature for reviewer "${reviewerName}" at base\u2192head ${input.baseSha.slice(0, 8)}\u2192${input.headSha.slice(0, 8)}. Server-attested PR mode requires every required reviewer to have a stamp-server-signed approval in the local DB. Possible causes:
|
|
10412
|
+
\u2022 the stamp-server is older than 2.0.1 and doesn't produce PR-attestation v3 payloads (upgrade the server)
|
|
10413
|
+
\u2022 \`stamp review --diff ${input.revspec}\` hasn't been run against this exact (base, head) pair yet
|
|
10414
|
+
\u2022 the review was run in local-mode (no \`review_server\` configured at review time)
|
|
10415
|
+
Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re-run \`stamp attest\`.`
|
|
10416
|
+
);
|
|
10417
|
+
}
|
|
10418
|
+
let parsedJson;
|
|
10419
|
+
try {
|
|
10420
|
+
parsedJson = JSON.parse(row.approval_json);
|
|
10421
|
+
} catch (err) {
|
|
10422
|
+
throw new Error(
|
|
10423
|
+
`server approval row for reviewer "${reviewerName}" has malformed JSON in server_approval_json \u2014 DB corruption or a writer-side bug. Re-run \`stamp review --diff ${input.revspec}\` to write a fresh row. (parse error: ${err instanceof Error ? err.message : String(err)})`
|
|
10424
|
+
);
|
|
10425
|
+
}
|
|
10426
|
+
if (!parsedJson || typeof parsedJson !== "object" || Array.isArray(parsedJson)) {
|
|
10427
|
+
throw new Error(
|
|
10428
|
+
`server approval row for reviewer "${reviewerName}" parsed to a non-object value \u2014 DB corruption or a writer-side bug. Re-run \`stamp review --diff ${input.revspec}\` to write a fresh row.`
|
|
10429
|
+
);
|
|
10430
|
+
}
|
|
10431
|
+
const obj = parsedJson;
|
|
10432
|
+
for (const field of [
|
|
10433
|
+
"reviewer",
|
|
10434
|
+
"verdict",
|
|
10435
|
+
"prompt_sha256",
|
|
10436
|
+
"diff_sha256",
|
|
10437
|
+
"base_sha",
|
|
10438
|
+
"head_sha",
|
|
10439
|
+
"trusted_keys_snapshot_sha256",
|
|
10440
|
+
"issued_at",
|
|
10441
|
+
"server_key_id"
|
|
10442
|
+
]) {
|
|
10443
|
+
if (typeof obj[field] !== "string") {
|
|
10444
|
+
throw new Error(
|
|
10445
|
+
`server approval row for reviewer "${reviewerName}" is missing required field "${field}" (or it isn't a string) \u2014 DB corruption or a writer-side bug. Re-run \`stamp review --diff ${input.revspec}\` to write a fresh row.`
|
|
10446
|
+
);
|
|
10447
|
+
}
|
|
10448
|
+
}
|
|
10449
|
+
const approval = parsedJson;
|
|
10450
|
+
if (approval.reviewer !== reviewerName) {
|
|
10451
|
+
throw new Error(
|
|
10452
|
+
`server approval row for reviewer "${reviewerName}" carries approval.reviewer="${approval.reviewer}" \u2014 DB row drifted. Re-run \`stamp review --diff ${input.revspec}\`.`
|
|
10453
|
+
);
|
|
10454
|
+
}
|
|
10455
|
+
if (approval.base_sha !== input.baseSha) {
|
|
10456
|
+
throw new Error(
|
|
10457
|
+
`server approval for "${reviewerName}" was signed against base_sha ${approval.base_sha.slice(0, 8)} but we're attesting from ${input.baseSha.slice(0, 8)} \u2014 stale signature. Re-run \`stamp review --diff ${input.revspec}\` to refresh.`
|
|
10458
|
+
);
|
|
10459
|
+
}
|
|
10460
|
+
if (approval.head_sha !== input.headSha) {
|
|
10461
|
+
throw new Error(
|
|
10462
|
+
`server approval for "${reviewerName}" was signed against head_sha ${approval.head_sha.slice(0, 8)} but we're attesting head ${input.headSha.slice(0, 8)} \u2014 stale signature. Re-run \`stamp review --diff ${input.revspec}\` to refresh.`
|
|
10463
|
+
);
|
|
10464
|
+
}
|
|
10465
|
+
if (approval.diff_sha256 !== diffSha256) {
|
|
10466
|
+
throw new Error(
|
|
10467
|
+
`server approval for "${reviewerName}" was signed against diff_sha256 ${approval.diff_sha256.slice(0, 12)}\u2026 but the current diff hashes to ${diffSha256.slice(0, 12)}\u2026 \u2014 stale signature. The diff content drifted between review and attest (rebased base, modified head). Re-run \`stamp review --diff ${input.revspec}\`.`
|
|
10468
|
+
);
|
|
10469
|
+
}
|
|
10470
|
+
if (approval.verdict !== "approved") {
|
|
10471
|
+
throw new Error(
|
|
10472
|
+
`server approval for "${reviewerName}" carries verdict "${approval.verdict}", not "approved". Re-run \`stamp review --diff ${input.revspec}\` so the server signs the current approved verdict.`
|
|
10473
|
+
);
|
|
10474
|
+
}
|
|
10475
|
+
const caps = resolveCapability(manifest, approval.server_key_id);
|
|
10476
|
+
if (caps === null) {
|
|
10477
|
+
throw new Error(
|
|
10478
|
+
`server approval for "${reviewerName}" was signed by ${approval.server_key_id}, but that key isn't listed in .stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)}. Either the server's signing key changed (commit the new fingerprint to the manifest with capabilities: [server]) or this row was written by a server the repo no longer trusts. Re-run \`stamp review --diff ${input.revspec}\` after fixing the manifest.`
|
|
10479
|
+
);
|
|
10480
|
+
}
|
|
10481
|
+
if (!caps.includes("server")) {
|
|
10482
|
+
throw new Error(
|
|
10483
|
+
`server approval for "${reviewerName}" was signed by ${approval.server_key_id}, but that key's capabilities in .stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)} are [${caps.join(", ")}] \u2014 missing the required 'server' capability. Update the manifest entry and re-attest.`
|
|
10484
|
+
);
|
|
10485
|
+
}
|
|
10486
|
+
const serverPubPem = pubkeyByFingerprint.get(approval.server_key_id);
|
|
10487
|
+
if (!serverPubPem) {
|
|
10488
|
+
throw new Error(
|
|
10489
|
+
`server approval for "${reviewerName}" was signed by ${approval.server_key_id}, but no .pub file in .stamp/trusted-keys/ at base ${input.baseSha.slice(0, 8)} matches that fingerprint. Commit the server's public key alongside its manifest entry and re-attest.`
|
|
10490
|
+
);
|
|
10491
|
+
}
|
|
10492
|
+
const sigOk = verifyBytes(
|
|
10493
|
+
serverPubPem,
|
|
10494
|
+
canonicalSerializeApproval(approval),
|
|
10495
|
+
row.signature_b64
|
|
10496
|
+
);
|
|
10497
|
+
if (!sigOk) {
|
|
10498
|
+
throw new Error(
|
|
10499
|
+
`server signature for "${reviewerName}" failed Ed25519 verification against key ${approval.server_key_id}. The DB row's signature does not match the canonical bytes of its approval body \u2014 either the row was tampered with or the writer was buggy. Re-run \`stamp review --diff ${input.revspec}\` to refresh the signed row.`
|
|
10500
|
+
);
|
|
10501
|
+
}
|
|
10502
|
+
return {
|
|
10503
|
+
approval,
|
|
10504
|
+
server_attestation: {
|
|
10505
|
+
server_key_id: approval.server_key_id,
|
|
10506
|
+
signature: row.signature_b64
|
|
10507
|
+
}
|
|
10508
|
+
};
|
|
10509
|
+
});
|
|
10510
|
+
} finally {
|
|
10511
|
+
db.close();
|
|
10512
|
+
}
|
|
10513
|
+
const trustAnchorSignatures = [];
|
|
10514
|
+
const payload = {
|
|
10515
|
+
schema_version: PR_ATTESTATION_SCHEMA_VERSION,
|
|
10516
|
+
patch_id: input.patchId,
|
|
10517
|
+
base_sha: input.baseSha,
|
|
10518
|
+
head_sha: input.headSha,
|
|
10519
|
+
target_branch: input.targetBranch,
|
|
10520
|
+
target_branch_tip_sha: input.targetBranchTipSha,
|
|
10521
|
+
diff_sha256: diffSha256,
|
|
10522
|
+
approvals: entries,
|
|
10523
|
+
checks: [],
|
|
10524
|
+
// Phase-1 deliberate omission — see file-level comment.
|
|
10525
|
+
trust_anchor_signatures: trustAnchorSignatures,
|
|
10526
|
+
signer_key_id: input.operatorFingerprint
|
|
10527
|
+
};
|
|
10528
|
+
const signature = signBytes(
|
|
10529
|
+
input.operatorPrivateKeyPem,
|
|
10530
|
+
serializePayload2(payload)
|
|
10531
|
+
);
|
|
10532
|
+
return {
|
|
10533
|
+
payload,
|
|
10534
|
+
signature,
|
|
10535
|
+
reviewerNames: input.requiredReviewers
|
|
10536
|
+
};
|
|
10537
|
+
}
|
|
10538
|
+
function buildV2Envelope(input) {
|
|
10539
|
+
const db = openDb(stampStateDbPath(input.repoRoot));
|
|
10240
10540
|
let approvals;
|
|
10241
10541
|
try {
|
|
10242
|
-
const reviews = latestReviews(db,
|
|
10542
|
+
const reviews = latestReviews(db, input.baseSha, input.headSha);
|
|
10243
10543
|
const byReviewer = new Map(reviews.map((r) => [r.reviewer, r]));
|
|
10244
10544
|
const missing = [];
|
|
10245
|
-
for (const r of
|
|
10545
|
+
for (const r of input.requiredReviewers) {
|
|
10246
10546
|
const rev = byReviewer.get(r);
|
|
10247
10547
|
if (!rev || rev.verdict !== "approved") missing.push(r);
|
|
10248
10548
|
}
|
|
10249
10549
|
if (missing.length > 0) {
|
|
10250
10550
|
throw new Error(
|
|
10251
|
-
`gate CLOSED: missing approved verdicts for: ${missing.join(", ")}. Run \`stamp status --diff ${revspec}\` to inspect, then \`stamp review --diff ${revspec}\` to review.`
|
|
10551
|
+
`gate CLOSED: missing approved verdicts for: ${missing.join(", ")}. Run \`stamp status --diff ${input.revspec}\` to inspect, then \`stamp review --diff ${input.revspec}\` to review.`
|
|
10252
10552
|
);
|
|
10253
10553
|
}
|
|
10254
|
-
approvals =
|
|
10554
|
+
approvals = input.requiredReviewers.map((name) => {
|
|
10255
10555
|
const rev = byReviewer.get(name);
|
|
10256
10556
|
const toolCalls = redactToolCallsForAttestation(
|
|
10257
10557
|
parseToolCalls(rev.tool_calls)
|
|
@@ -10267,20 +10567,20 @@ function runAttest(opts) {
|
|
|
10267
10567
|
db.close();
|
|
10268
10568
|
}
|
|
10269
10569
|
const baseConfigYaml = showAtRef(
|
|
10270
|
-
|
|
10570
|
+
input.baseSha,
|
|
10271
10571
|
".stamp/config.yml",
|
|
10272
|
-
repoRoot
|
|
10572
|
+
input.repoRoot
|
|
10273
10573
|
);
|
|
10274
10574
|
const baseReviewers = readReviewersFromYaml(baseConfigYaml);
|
|
10275
10575
|
approvals = approvals.map((a) => {
|
|
10276
10576
|
const def = baseReviewers[a.reviewer];
|
|
10277
10577
|
if (!def) {
|
|
10278
10578
|
throw new Error(
|
|
10279
|
-
`reviewer "${a.reviewer}" approved the diff but is not defined in .stamp/config.yml at base ${
|
|
10579
|
+
`reviewer "${a.reviewer}" approved the diff but is not defined in .stamp/config.yml at base ${input.baseSha.slice(0, 8)}. This shouldn't happen \u2014 runReview reads from the same base. File a bug at https://github.com/OpenThinkAi/stamp-cli/issues.`
|
|
10280
10580
|
);
|
|
10281
10581
|
}
|
|
10282
|
-
const promptText = showAtRef(
|
|
10283
|
-
const source = readReviewerSource2(a.reviewer, repoRoot);
|
|
10582
|
+
const promptText = showAtRef(input.baseSha, def.prompt, input.repoRoot);
|
|
10583
|
+
const source = readReviewerSource2(a.reviewer, input.repoRoot);
|
|
10284
10584
|
return {
|
|
10285
10585
|
...a,
|
|
10286
10586
|
prompt_sha256: hashPromptBytes(Buffer.from(promptText, "utf8")),
|
|
@@ -10289,58 +10589,27 @@ function runAttest(opts) {
|
|
|
10289
10589
|
...source ? { reviewer_source: source } : {}
|
|
10290
10590
|
};
|
|
10291
10591
|
});
|
|
10292
|
-
const patch_id = patchIdForSpan(resolved.base_sha, resolved.head_sha, repoRoot);
|
|
10293
|
-
const target_branch_tip_sha = runGit(
|
|
10294
|
-
["rev-parse", `${opts.into}^{commit}`],
|
|
10295
|
-
repoRoot
|
|
10296
|
-
).trim();
|
|
10297
|
-
const { keypair } = ensureUserKeypair();
|
|
10298
10592
|
const payload = {
|
|
10299
10593
|
schema_version: LEGACY_CLIENT_PR_ATTESTATION_SCHEMA_VERSION,
|
|
10300
|
-
patch_id,
|
|
10301
|
-
base_sha:
|
|
10302
|
-
head_sha:
|
|
10303
|
-
target_branch:
|
|
10304
|
-
target_branch_tip_sha,
|
|
10594
|
+
patch_id: input.patchId,
|
|
10595
|
+
base_sha: input.baseSha,
|
|
10596
|
+
head_sha: input.headSha,
|
|
10597
|
+
target_branch: input.targetBranch,
|
|
10598
|
+
target_branch_tip_sha: input.targetBranchTipSha,
|
|
10305
10599
|
approvals,
|
|
10306
10600
|
checks: [],
|
|
10307
10601
|
// Phase-1 deliberate omission — see file-level comment.
|
|
10308
|
-
signer_key_id:
|
|
10602
|
+
signer_key_id: input.operatorFingerprint
|
|
10309
10603
|
};
|
|
10310
|
-
const signature = signBytes(
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
repoRoot
|
|
10604
|
+
const signature = signBytes(
|
|
10605
|
+
input.operatorPrivateKeyPem,
|
|
10606
|
+
serializePayload2(payload)
|
|
10314
10607
|
);
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
console.log(
|
|
10321
|
-
` base\u2192head: ${resolved.base_sha.slice(0, 8)} \u2192 ${resolved.head_sha.slice(0, 8)}`
|
|
10322
|
-
);
|
|
10323
|
-
console.log(` signed by: ${keypair.fingerprint}`);
|
|
10324
|
-
console.log(` approvals: ${approvals.map((a) => a.reviewer).join(", ")}`);
|
|
10325
|
-
console.log(` ref: ${ref}`);
|
|
10326
|
-
console.log(` blob: ${blob_sha.slice(0, 12)}`);
|
|
10327
|
-
console.log(bar);
|
|
10328
|
-
if (opts.pushTo) {
|
|
10329
|
-
pushBranchAndAttestation(opts.pushTo, ref, repoRoot);
|
|
10330
|
-
console.log(
|
|
10331
|
-
`
|
|
10332
|
-
\u2713 pushed branch + attestation ref to ${opts.pushTo}. Open the PR; stamp/verify-attestation@v1 will look up refs/stamp/attestations/<patch-id> from your head SHA's diff against the base.`
|
|
10333
|
-
);
|
|
10334
|
-
} else {
|
|
10335
|
-
console.log(
|
|
10336
|
-
`
|
|
10337
|
-
Next: push the branch + attestation ref to your remote, open a PR, and let stamp/verify-attestation@v1 (the GH Action) confirm it. To do both pushes in one shot:
|
|
10338
|
-
|
|
10339
|
-
git push <remote> HEAD ${ref}
|
|
10340
|
-
|
|
10341
|
-
Or re-run with --push <remote> next time.`
|
|
10342
|
-
);
|
|
10343
|
-
}
|
|
10608
|
+
return {
|
|
10609
|
+
payload,
|
|
10610
|
+
signature,
|
|
10611
|
+
reviewerNames: approvals.map((a) => a.reviewer)
|
|
10612
|
+
};
|
|
10344
10613
|
}
|
|
10345
10614
|
function pushBranchAndAttestation(remote, attestationRef, repoRoot) {
|
|
10346
10615
|
const result = spawnSync14(
|
|
@@ -10375,7 +10644,7 @@ function readReviewerSource2(reviewerName, repoRoot) {
|
|
|
10375
10644
|
|
|
10376
10645
|
// src/commands/prune.ts
|
|
10377
10646
|
import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
10378
|
-
import { join as
|
|
10647
|
+
import { join as join14 } from "path";
|
|
10379
10648
|
|
|
10380
10649
|
// src/lib/duration.ts
|
|
10381
10650
|
function parseRetentionDuration(input) {
|
|
@@ -10404,8 +10673,8 @@ function runPrune(opts) {
|
|
|
10404
10673
|
);
|
|
10405
10674
|
const repoRoot = findRepoRoot();
|
|
10406
10675
|
const dbPath = stampStateDbPath(repoRoot);
|
|
10407
|
-
const parsesDir =
|
|
10408
|
-
const runsDir =
|
|
10676
|
+
const parsesDir = join14(gitCommonDir(repoRoot), "stamp", "failed-parses");
|
|
10677
|
+
const runsDir = join14(gitCommonDir(repoRoot), "stamp", "failed-runs");
|
|
10409
10678
|
const spoolCutoffMs = Date.now() - durationMs;
|
|
10410
10679
|
if (!existsSync18(dbPath) && !existsSync18(parsesDir) && !existsSync18(runsDir)) {
|
|
10411
10680
|
console.log(
|
|
@@ -10486,7 +10755,7 @@ function peekSpools(spoolDir, cutoffMs) {
|
|
|
10486
10755
|
if (!existsSync18(spoolDir)) return [];
|
|
10487
10756
|
const out = [];
|
|
10488
10757
|
for (const entry of readdirSync5(spoolDir)) {
|
|
10489
|
-
const filepath =
|
|
10758
|
+
const filepath = join14(spoolDir, entry);
|
|
10490
10759
|
let stat;
|
|
10491
10760
|
try {
|
|
10492
10761
|
stat = statSync3(filepath);
|
|
@@ -10641,27 +10910,27 @@ function loadOrEmpty() {
|
|
|
10641
10910
|
import { spawnSync as spawnSync15 } from "child_process";
|
|
10642
10911
|
import {
|
|
10643
10912
|
existsSync as existsSync21,
|
|
10644
|
-
readFileSync as
|
|
10913
|
+
readFileSync as readFileSync18,
|
|
10645
10914
|
statSync as statSync4,
|
|
10646
10915
|
unlinkSync as unlinkSync4,
|
|
10647
10916
|
writeFileSync as writeFileSync15
|
|
10648
10917
|
} from "fs";
|
|
10649
|
-
import { join as
|
|
10918
|
+
import { join as join16, relative as relative3, resolve as resolve2 } from "path";
|
|
10650
10919
|
import { parse as parseYaml10, stringify as stringifyYaml3 } from "yaml";
|
|
10651
10920
|
|
|
10652
10921
|
// src/lib/reviewerLock.ts
|
|
10653
|
-
import { existsSync as existsSync20, readFileSync as
|
|
10654
|
-
import { join as
|
|
10922
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "fs";
|
|
10923
|
+
import { join as join15 } from "path";
|
|
10655
10924
|
var LOCK_FILE_VERSION = 1;
|
|
10656
10925
|
var LOCK_DRIFT_EXIT = 3;
|
|
10657
10926
|
function lockFilePath(repoRoot, reviewerName) {
|
|
10658
|
-
return
|
|
10927
|
+
return join15(repoRoot, ".stamp", "reviewers", `${reviewerName}.lock.json`);
|
|
10659
10928
|
}
|
|
10660
10929
|
function readLockFile(repoRoot, reviewerName) {
|
|
10661
10930
|
const path2 = lockFilePath(repoRoot, reviewerName);
|
|
10662
10931
|
if (!existsSync20(path2)) return null;
|
|
10663
10932
|
try {
|
|
10664
|
-
const raw =
|
|
10933
|
+
const raw = readFileSync17(path2, "utf8");
|
|
10665
10934
|
const parsed = JSON.parse(raw);
|
|
10666
10935
|
if (typeof parsed.version !== "number" || typeof parsed.source !== "string" || typeof parsed.ref !== "string" || typeof parsed.reviewer !== "string" || typeof parsed.prompt_sha256 !== "string" || typeof parsed.tools_sha256 !== "string" || typeof parsed.mcp_sha256 !== "string") {
|
|
10667
10936
|
throw new Error(`malformed lock file at ${path2}`);
|
|
@@ -10682,13 +10951,13 @@ function checkReviewerDrift(repoRoot, reviewerName, def) {
|
|
|
10682
10951
|
if (!lock) {
|
|
10683
10952
|
return unpinnedResult();
|
|
10684
10953
|
}
|
|
10685
|
-
const promptPath =
|
|
10954
|
+
const promptPath = join15(repoRoot, def.prompt);
|
|
10686
10955
|
if (!existsSync20(promptPath)) {
|
|
10687
10956
|
throw new Error(
|
|
10688
10957
|
`reviewer "${reviewerName}" has a lock file but its prompt "${def.prompt}" does not exist on disk. Re-run 'stamp reviewers fetch ${reviewerName} --from ${lock.source}@${lock.ref}' to restore it, or delete the lock file to un-pin the reviewer.`
|
|
10689
10958
|
);
|
|
10690
10959
|
}
|
|
10691
|
-
const promptBytes =
|
|
10960
|
+
const promptBytes = readFileSync17(promptPath);
|
|
10692
10961
|
const observedPrompt = hashPromptBytes(promptBytes);
|
|
10693
10962
|
const observedTools = hashTools(def.tools);
|
|
10694
10963
|
const observedMcp = hashMcpServers(def.mcp_servers);
|
|
@@ -10890,8 +11159,8 @@ async function reviewersTest(name, diff) {
|
|
|
10890
11159
|
console.log(` prompt sourced from working tree (test/iteration use case)`);
|
|
10891
11160
|
console.log();
|
|
10892
11161
|
const def = config2.reviewers[name];
|
|
10893
|
-
const promptPath =
|
|
10894
|
-
const systemPrompt =
|
|
11162
|
+
const promptPath = join16(repoRoot, def.prompt);
|
|
11163
|
+
const systemPrompt = readFileSync18(promptPath, "utf8");
|
|
10895
11164
|
const result = await invokeReviewer({
|
|
10896
11165
|
reviewer: name,
|
|
10897
11166
|
config: config2,
|
|
@@ -11029,7 +11298,7 @@ async function reviewersFetch(reviewerName, opts) {
|
|
|
11029
11298
|
mcpServers = validateMcpServersFromSource(parsed.mcp_servers, source, ref);
|
|
11030
11299
|
}
|
|
11031
11300
|
}
|
|
11032
|
-
const promptPath =
|
|
11301
|
+
const promptPath = join16(reviewersDir, `${reviewerName}.md`);
|
|
11033
11302
|
const promptBytes = Buffer.from(promptText, "utf8");
|
|
11034
11303
|
const promptSha = hashPromptBytes(promptBytes);
|
|
11035
11304
|
const toolsSha = hashTools(tools);
|
|
@@ -11367,28 +11636,6 @@ function printGate(result, base_sha, head_sha) {
|
|
|
11367
11636
|
|
|
11368
11637
|
// src/commands/update.ts
|
|
11369
11638
|
import { spawnSync as spawnSync16 } from "child_process";
|
|
11370
|
-
|
|
11371
|
-
// src/lib/version.ts
|
|
11372
|
-
import { readFileSync as readFileSync18 } from "fs";
|
|
11373
|
-
import { dirname as dirname7, join as join16 } from "path";
|
|
11374
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11375
|
-
function readPackageVersion() {
|
|
11376
|
-
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
11377
|
-
for (let dir = here, i = 0; i < 6; i++) {
|
|
11378
|
-
try {
|
|
11379
|
-
const raw = readFileSync18(join16(dir, "package.json"), "utf8");
|
|
11380
|
-
const pkg = JSON.parse(raw);
|
|
11381
|
-
if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
|
|
11382
|
-
} catch {
|
|
11383
|
-
}
|
|
11384
|
-
const parent = dirname7(dir);
|
|
11385
|
-
if (parent === dir) break;
|
|
11386
|
-
dir = parent;
|
|
11387
|
-
}
|
|
11388
|
-
throw new Error("could not locate @openthink/stamp package.json to read version");
|
|
11389
|
-
}
|
|
11390
|
-
|
|
11391
|
-
// src/commands/update.ts
|
|
11392
11639
|
var PKG_NAME = "@openthink/stamp";
|
|
11393
11640
|
function compareSemver(a, b) {
|
|
11394
11641
|
const parse2 = (v) => (v.split("-")[0] ?? "0.0.0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -11990,6 +12237,9 @@ program.command("init").description(
|
|
|
11990
12237
|
).option(
|
|
11991
12238
|
"--pr-mode-force",
|
|
11992
12239
|
"with --pr-mode, overwrite an existing .github/workflows/stamp-mirror.yml (useful when re-running after configuring review_server so the host/port placeholders fill in)"
|
|
12240
|
+
).option(
|
|
12241
|
+
"--action-source <org/repo>",
|
|
12242
|
+
"GitHub repo that hosts the stamp/verify-attestation action used by .github/workflows/stamp-verify.yml. Default 'OpenThinkAi/stamp-cli'. Override when consuming a fork (e.g. 'Anglepoint-Inc/anglepoint-stamp-server') so the workflow tracks your fork's updates instead of the upstream."
|
|
11993
12243
|
).action(
|
|
11994
12244
|
(opts) => {
|
|
11995
12245
|
try {
|
|
@@ -11997,6 +12247,11 @@ program.command("init").description(
|
|
|
11997
12247
|
runMigrateToServerAttested({ dryRun: opts.dryRun === true });
|
|
11998
12248
|
return;
|
|
11999
12249
|
}
|
|
12250
|
+
if (opts.dryRun === true) {
|
|
12251
|
+
throw new Error(
|
|
12252
|
+
"--dry-run is supported only with --migrate-to-server-attested. Re-run with --migrate-to-server-attested to preview the migration scaffold, or drop --dry-run to run the requested init operation."
|
|
12253
|
+
);
|
|
12254
|
+
}
|
|
12000
12255
|
let mode;
|
|
12001
12256
|
if (opts.mode === void 0) {
|
|
12002
12257
|
mode = void 0;
|
|
@@ -12007,6 +12262,13 @@ program.command("init").description(
|
|
|
12007
12262
|
`--mode must be 'server-gated' or 'local-only' (got "${opts.mode}")`
|
|
12008
12263
|
);
|
|
12009
12264
|
}
|
|
12265
|
+
if (opts.actionSource !== void 0) {
|
|
12266
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(opts.actionSource)) {
|
|
12267
|
+
throw new Error(
|
|
12268
|
+
`--action-source must be of the form 'org/repo' (got "${opts.actionSource}")`
|
|
12269
|
+
);
|
|
12270
|
+
}
|
|
12271
|
+
}
|
|
12010
12272
|
runInit({
|
|
12011
12273
|
minimal: opts.minimal,
|
|
12012
12274
|
agentsMd: opts.agentsMd,
|
|
@@ -12022,7 +12284,8 @@ program.command("init").description(
|
|
|
12022
12284
|
// default fires when the operator hasn't opted out.
|
|
12023
12285
|
prCheck: opts.prCheck === false ? false : void 0,
|
|
12024
12286
|
prMode: opts.prMode === true,
|
|
12025
|
-
prModeForce: opts.prModeForce === true
|
|
12287
|
+
prModeForce: opts.prModeForce === true,
|
|
12288
|
+
actionSource: opts.actionSource
|
|
12026
12289
|
});
|
|
12027
12290
|
} catch (err) {
|
|
12028
12291
|
const message = err instanceof Error ? err.message : String(err);
|