coderifts 5.0.1 → 6.0.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/CHANGELOG.md CHANGED
@@ -1,6 +1,52 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 6.0.1
4
+
5
+ ### Changed
6
+ - **Guard floor raised to 13, declared range `^13.0.0`.** ADOPTER-FACING: the guard is a direct
7
+ dependency with no `peerDependencies` and no bundled copy, so this range *is* what a fresh install
8
+ resolves. `coderifts@5.0.1` declared `^12.0.0`, which resolves to 12.0.0 — the version in which
9
+ `wrapWriteWithFsCas` measured its expected CAS token INSIDE `executeFactory`, after preflight and
10
+ after the T2 recheck, and conditioned the write on that self-fetched value. Measured on 12.0.0: a
11
+ writer landing in that window had its state adopted as the legitimate starting point and the CAS
12
+ still returned `committed` with `enforced: true`, clobbering it. 13.0.0 threads the
13
+ authorization-time token, and adds three filesystem defences that were measured absent rather than
14
+ assumed — `..` refused before resolution, a symlinked final component refused (`stat`/`readFile`
15
+ measured the link's TARGET while `rename` replaced the LINK, so check and write named different
16
+ objects), the token read through a single file descriptor, and `dev`+`ino` recorded in the write
17
+ evidence.
18
+
19
+ **Cost measured as zero**, the same way the 11-, 12- and 13-raises were: the CLI imports only
20
+ `deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and `matchGlob`, whose
21
+ implementing modules are byte-identical 12.0.0 → 13.0.0, and all four return identical values
22
+ under both. Suite staged against 13.0.0 returned an identical 539/518/21 with a byte-identical
23
+ 28-item failure set. Nothing in the CLI calls the CAS or FS adapters.
24
+
25
+ ## 5.0.1
26
+
27
+ ### Fixed
28
+ - **A fresh install shipped the guard with the conditional-write hole.**
29
+ `coderifts@5.0.0` declared `@coderifts/agent-guard: ^11.0.0`, which resolved to
30
+ 11.0.1 — precisely the tarball the guard's own 12.0.0 round reproduced the
31
+ defect against, where `requireConditionalWrite` did not fire on an ordinary
32
+ mutation and the side effect **ran** with `enforced: true`. The guard is a
33
+ **direct** dependency with no `peerDependencies` and no bundled copy, so this
34
+ range *is* the adopter's resolution, not advice. Now `^12.0.0`, with
35
+ `MIN_GUARD_MAJOR` raised to 12. Cost measured as zero: the CLI imports only
36
+ `deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and
37
+ `matchGlob`, whose modules are byte-identical 11.0.1 → 12.0.0.
38
+ - **Two defects in the publish gate itself**, both proven by replaying the real
39
+ script. The resolved check only ever compared *downward* — `resolved >= floor`,
40
+ never whether the resolved version sits **inside** the declared range — so
41
+ declaring `^11.0.0` with 12.0.0 staged for a cost measurement printed OK and
42
+ exited 0, which is exactly the shape that makes "we measured 12 and shipped
43
+ ^11" indistinguishable from a correct release. And an unresolvable install was
44
+ a **pass**. Three failure modes added (resolved below the declared range at
45
+ full precision, resolved above it, and not resolvable at all), with no
46
+ opt-out — an `allowUnresolved` flag was written and deliberately removed, and
47
+ a test asserts it stays absent.
48
+
49
+ ## 5.0.0
4
50
 
5
51
  ### Fixed
6
52
  - **P0 — `setup-required-check --apply` no longer reports `APPLIED` over a
@@ -59,6 +105,63 @@
59
105
  template is unchanged and stays non-blocking; it carries the same three lines
60
106
  commented, with a note saying why the two differ.
61
107
 
108
+ ## 4.13.0 — never published (folded into 5.0.0)
109
+
110
+ Bumped in-repo (`917611d`) but never tagged or released to npm; 5.0.0 superseded
111
+ it, so adopters received this content **as part of 5.0.0**. Recorded separately
112
+ because the version number appears in the source history and in
113
+ `assert-guard-major.js`'s floor-raise log.
114
+
115
+ ### Changed
116
+ - **Guard floor raised to 11.** Cost measured as zero the way the 10-raise was —
117
+ the CLI imports only `deployGate`, `asVerifiedDeployReceiptView`,
118
+ `DEPLOY_RECEIPT_VIEW_SPEC` and `matchGlob`, none of which 11.0.0 touched, and
119
+ the suite staged against 11 returned an identical 532/530/2 with a
120
+ byte-identical failure set. It rises anyway, because a semantics floor is about
121
+ what the CLI may **run** against, not what it happens to import today: 10.x's
122
+ `computeBundleFingerprint` disagreed with the server, which is exactly the
123
+ class this gate exists to refuse.
124
+ - The two tests pinning the old floor were updated, and the invariant made
125
+ explicit: **the floor only ever rises**.
126
+
127
+ ### Added
128
+ - `/api/v1/agent/preflight` returns `scorer_version` — the one field that makes
129
+ the single-spec fingerprint reproducible by a consumer holding their own
130
+ before/after/policy. Purely additive; no surface moves and `tools_sha256` is
131
+ unchanged.
132
+
133
+ ## 4.12.0
134
+
135
+ ### Changed
136
+ - **Guard floor raised to 10.** Cost measured as zero: the CLI imports only
137
+ `deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and
138
+ `matchGlob` — never `withCodeRifts` — so 10.0.0's strict-composition break
139
+ cannot reach it. Verified by staging guard 10 into the install and running the
140
+ suite (guard-conformance 6/6, the same two pre-existing failures as the guard-9
141
+ baseline). The gate was satisfied, not weakened: before the install it
142
+ correctly reported FAIL on the stale resolve.
143
+
144
+ ## 4.11.0
145
+
146
+ ### Fixed
147
+ - **P0 — the issuer binding pointed at a check that cannot block.** GitHub passes
148
+ a required status check on `success`, `skipped` **or `neutral`**, so our App's
149
+ advisory check genuinely cannot block; a false comment in
150
+ `webhook-integration.js` claiming otherwise is what produced the regression.
151
+ The enforcing issuer is `app_id 15368` (github-actions), which the contract-gate
152
+ Action posts under, so that binding is now VERIFIED — with the shared-identity
153
+ residual **named** rather than implied: anyone who can edit
154
+ `.github/workflows` can post under it. A binding to our own App id grades
155
+ UNVERIFIABLE rather than NOT_VERIFIED, because `MERGEGATE_ENFORCE` is
156
+ server-side and invisible from here.
157
+ - **P0 — the installer wrote what our own verifier rejects.** It now writes
158
+ `checks[]` with an explicit `app_id`, because an omitted `app_id` silently
159
+ resolves to whoever posted last, which is unauditable. It never re-binds
160
+ another team's existing check, and the success path prints what it does **not**
161
+ achieve. A coupling test asserts the installer's and the verifier's issuer
162
+ constants are equal. The verifier was not weakened — the name-only and
163
+ legacy-contexts paths are byte-identical, asserted by two unchanged tests.
164
+
62
165
  ## 4.10.0
63
166
 
64
167
  ### Added
package/dist/cli.js CHANGED
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
3028
3028
  "package.json"(exports2, module2) {
3029
3029
  module2.exports = {
3030
3030
  name: "coderifts",
3031
- version: "5.0.1",
3031
+ version: "6.0.1",
3032
3032
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3033
3033
  author: "CodeRifts <hello@coderifts.com>",
3034
3034
  license: "MIT",
@@ -3067,13 +3067,13 @@ var require_package = __commonJS({
3067
3067
  },
3068
3068
  scripts: {
3069
3069
  build: "node scripts/copy-corpus.js && node scripts/build.js",
3070
- prepublishOnly: "node scripts/assert-guard-major.js && bash ../../scripts/freeze-gate.sh && npm run build",
3070
+ prepublishOnly: "node scripts/assert-guard-major.js && node scripts/assert-changelog-version.js && bash ../../scripts/freeze-gate.sh && npm run build",
3071
3071
  "assert-guard-major": "node scripts/assert-guard-major.js",
3072
3072
  postinstall: "node scripts/postinstall.js",
3073
3073
  test: "node --test test/*.test.js"
3074
3074
  },
3075
3075
  dependencies: {
3076
- "@coderifts/agent-guard": "^12.0.0",
3076
+ "@coderifts/agent-guard": "^13.0.0",
3077
3077
  chalk: "^4.1.2",
3078
3078
  "cli-table3": "^0.6.4",
3079
3079
  commander: "^12.0.0",
@@ -67189,6 +67189,9 @@ var require_conditional_write = __commonJS({
67189
67189
  write_style,
67190
67190
  mutating
67191
67191
  };
67192
+ if (ctx.guarantee === "SAME_TRANSACTION" || ctx.guarantee === "CONDITIONAL_EXTERNAL" || ctx.guarantee === "NON_ATOMIC") {
67193
+ basis.guarantee = ctx.guarantee;
67194
+ }
67192
67195
  if (conditional_write === true) {
67193
67196
  if (typeof ctx.conditioned_on_token === "string") {
67194
67197
  basis.conditioned_on_token = ctx.conditioned_on_token;
@@ -67324,8 +67327,11 @@ var require_fs = __commonJS({
67324
67327
  };
67325
67328
  })();
67326
67329
  Object.defineProperty(exports2, "__esModule", { value: true });
67327
- exports2.tokensEqual = exports2.FS_ABSENT_TOKEN = exports2.FS_VERSION_TOKEN_PREFIX = void 0;
67330
+ exports2.tokensEqual = exports2.UnsafeCasPathError = exports2.FS_ABSENT_TOKEN = exports2.FS_VERSION_TOKEN_PREFIX = void 0;
67328
67331
  exports2.fsTokenContentHash = fsTokenContentHash;
67332
+ exports2.assertSafeCasPath = assertSafeCasPath;
67333
+ exports2.fsObjectIdentity = fsObjectIdentity;
67334
+ exports2.identitiesEqual = identitiesEqual;
67329
67335
  exports2.createFsVersionToken = createFsVersionToken;
67330
67336
  exports2.readVersionedFile = readVersionedFile;
67331
67337
  exports2.writeFileIfUnchanged = writeFileIfUnchanged;
@@ -67354,23 +67360,90 @@ var require_fs = __commonJS({
67354
67360
  const hash = rest.slice(colon + 1);
67355
67361
  return /^[a-f0-9]{64}$/.test(hash) ? hash : null;
67356
67362
  }
67363
+ var UnsafeCasPathError = class extends Error {
67364
+ reason;
67365
+ casPath;
67366
+ constructor(reason, casPath) {
67367
+ super(`unsafe CAS path (${reason}): ${casPath}`);
67368
+ this.name = "UnsafeCasPathError";
67369
+ this.reason = reason;
67370
+ this.casPath = casPath;
67371
+ }
67372
+ };
67373
+ exports2.UnsafeCasPathError = UnsafeCasPathError;
67374
+ async function assertSafeCasPath(filePath) {
67375
+ const segments = filePath.split(/[\\/]+/);
67376
+ if (segments.includes(".."))
67377
+ throw new UnsafeCasPathError("path_traversal", filePath);
67378
+ const resolved = path.resolve(filePath);
67379
+ const dir = path.dirname(resolved);
67380
+ let canonicalDir = dir;
67381
+ try {
67382
+ canonicalDir = await node_fs_1.promises.realpath(dir);
67383
+ } catch (err) {
67384
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
67385
+ if (code !== "ENOENT")
67386
+ throw err;
67387
+ }
67388
+ const target = path.join(canonicalDir, path.basename(resolved));
67389
+ try {
67390
+ const lst = await node_fs_1.promises.lstat(target);
67391
+ if (lst.isSymbolicLink())
67392
+ throw new UnsafeCasPathError("symlink_target", filePath);
67393
+ } catch (err) {
67394
+ if (err instanceof UnsafeCasPathError)
67395
+ throw err;
67396
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
67397
+ if (code !== "ENOENT")
67398
+ throw err;
67399
+ }
67400
+ return target;
67401
+ }
67402
+ async function fsObjectIdentity(filePath) {
67403
+ let fh;
67404
+ try {
67405
+ fh = await node_fs_1.promises.open(filePath, "r");
67406
+ const st = await fh.stat();
67407
+ return { dev: st.dev, ino: st.ino };
67408
+ } catch (err) {
67409
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
67410
+ if (code === "ENOENT")
67411
+ return null;
67412
+ throw err;
67413
+ } finally {
67414
+ if (fh)
67415
+ await fh.close().catch(() => {
67416
+ });
67417
+ }
67418
+ }
67419
+ function identitiesEqual(a, b) {
67420
+ if (!a || !b)
67421
+ return false;
67422
+ return a.dev === b.dev && a.ino === b.ino;
67423
+ }
67357
67424
  async function createFsVersionToken(filePath) {
67358
- let st;
67425
+ let fh;
67359
67426
  try {
67360
- st = await node_fs_1.promises.stat(filePath);
67427
+ fh = await node_fs_1.promises.open(filePath, "r");
67361
67428
  } catch (err) {
67362
67429
  const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
67363
67430
  if (code === "ENOENT")
67364
67431
  return exports2.FS_ABSENT_TOKEN;
67365
67432
  throw err;
67366
67433
  }
67367
- if (!st.isFile()) {
67368
- throw new Error(`createFsVersionToken: not a regular file: ${filePath}`);
67434
+ try {
67435
+ const st = await fh.stat();
67436
+ if (!st.isFile()) {
67437
+ throw new Error(`createFsVersionToken: not a regular file: ${filePath}`);
67438
+ }
67439
+ const buf = await fh.readFile();
67440
+ const hash = sha256hex(buf);
67441
+ const mtimeMs = Math.trunc(st.mtimeMs);
67442
+ return `${exports2.FS_VERSION_TOKEN_PREFIX}${mtimeMs}:${hash}`;
67443
+ } finally {
67444
+ await fh.close().catch(() => {
67445
+ });
67369
67446
  }
67370
- const buf = await node_fs_1.promises.readFile(filePath);
67371
- const hash = sha256hex(buf);
67372
- const mtimeMs = Math.trunc(st.mtimeMs);
67373
- return `${exports2.FS_VERSION_TOKEN_PREFIX}${mtimeMs}:${hash}`;
67374
67447
  }
67375
67448
  async function readVersionedFile(filePath) {
67376
67449
  const version_token = await createFsVersionToken(filePath);
@@ -67381,9 +67454,23 @@ var require_fs = __commonJS({
67381
67454
  return { content, version_token };
67382
67455
  }
67383
67456
  async function writeFileIfUnchanged(args) {
67384
- const target = path.resolve(args.path);
67457
+ const target = await assertSafeCasPath(args.path);
67385
67458
  const body = typeof args.content === "string" ? Buffer.from(args.content, "utf8") : args.content;
67386
67459
  const writtenHash = sha256hex(body);
67460
+ const checkedIdentity = await fsObjectIdentity(target);
67461
+ const hasT1Identity = Object.prototype.hasOwnProperty.call(args, "expected_identity");
67462
+ if (hasT1Identity) {
67463
+ const t1 = args.expected_identity ?? null;
67464
+ const held = t1 === null ? checkedIdentity === null : identitiesEqual(t1, checkedIdentity);
67465
+ if (!held) {
67466
+ return {
67467
+ status: "refused",
67468
+ reason: "stale_version_token",
67469
+ expected_token: args.expected_token,
67470
+ current_token: await createFsVersionToken(target).catch(() => null)
67471
+ };
67472
+ }
67473
+ }
67387
67474
  return (0, conditional_write_js_1.executeIfUnchanged)({
67388
67475
  expected_token: args.expected_token,
67389
67476
  current_token: () => createFsVersionToken(target),
@@ -67402,6 +67489,15 @@ var require_fs = __commonJS({
67402
67489
  const tmp = path.join(dir, `.coderifts-cas-${path.basename(target)}-${process.pid}-${(0, node_crypto_1.randomBytes)(6).toString("hex")}.tmp`);
67403
67490
  try {
67404
67491
  await node_fs_1.promises.writeFile(tmp, body);
67492
+ const stillIdentity = await fsObjectIdentity(target);
67493
+ const identityHeld = checkedIdentity === null ? stillIdentity === null : identitiesEqual(checkedIdentity, stillIdentity);
67494
+ if (!identityHeld) {
67495
+ try {
67496
+ await node_fs_1.promises.unlink(tmp);
67497
+ } catch {
67498
+ }
67499
+ throw new conditional_write_js_1.StaleVersionTokenAbort(await createFsVersionToken(target).catch(() => null));
67500
+ }
67405
67501
  const still = await createFsVersionToken(target);
67406
67502
  if (!(0, conditional_write_js_1.tokensEqual)(args.expected_token, still)) {
67407
67503
  try {
@@ -67420,7 +67516,13 @@ var require_fs = __commonJS({
67420
67516
  }
67421
67517
  throw err;
67422
67518
  }
67423
- return { path: target, bytes: body.length, written_content_hash: writtenHash };
67519
+ return {
67520
+ path: target,
67521
+ bytes: body.length,
67522
+ written_content_hash: writtenHash,
67523
+ checked_identity: checkedIdentity,
67524
+ committed_identity: await fsObjectIdentity(target)
67525
+ };
67424
67526
  }
67425
67527
  });
67426
67528
  }
@@ -69409,6 +69511,10 @@ var require_cas_attestation = __commonJS({
69409
69511
  const s = x;
69410
69512
  return s.reason === "stale_during_commit" && typeof s.expected_token === "string";
69411
69513
  }
69514
+ if (o.status === "indeterminate") {
69515
+ const i = x;
69516
+ return (i.reason === "response_lost" || i.reason === "ambiguous_provider_reply" || i.reason === "observation_failed") && typeof i.expected_token === "string";
69517
+ }
69412
69518
  return false;
69413
69519
  }
69414
69520
  function freezeExecutionResultHash(h) {
@@ -69431,6 +69537,15 @@ var require_cas_attestation = __commonJS({
69431
69537
  current_token: outcome.current_token == null ? null : outcome.current_token
69432
69538
  });
69433
69539
  }
69540
+ if (outcome.status === "indeterminate") {
69541
+ return Object.freeze({
69542
+ status: "indeterminate",
69543
+ write_ran: "unknown",
69544
+ reason: outcome.reason,
69545
+ expected_token: outcome.expected_token,
69546
+ observed_token: outcome.observed_token == null ? null : outcome.observed_token
69547
+ });
69548
+ }
69434
69549
  return Object.freeze({
69435
69550
  status: "committed_stale_detected",
69436
69551
  write_ran: true,
@@ -69451,6 +69566,7 @@ var require_cas_attestation = __commonJS({
69451
69566
  const write_ran = cas.write_ran === true;
69452
69567
  const stale_during_commit = cas.status === "committed_stale_detected";
69453
69568
  const refused = cas.status === "refused";
69569
+ const indeterminate = cas.status === "indeterminate";
69454
69570
  const cas_evidence = evaluateCasEvidence(outcome, opts);
69455
69571
  let authorized_and_committed = receipt_verified && cas.status === "committed";
69456
69572
  if (opts.profile === "ENFORCING_STRICT") {
@@ -69471,7 +69587,8 @@ var require_cas_attestation = __commonJS({
69471
69587
  authorized_and_committed,
69472
69588
  write_ran,
69473
69589
  stale_during_commit,
69474
- refused
69590
+ refused,
69591
+ indeterminate
69475
69592
  }),
69476
69593
  cas_evidence,
69477
69594
  limits: LIMITS
@@ -72729,6 +72846,8 @@ var require_fs_default_wire = __commonJS({
72729
72846
  exports2.inferFsPathFromArgs = inferFsPathFromArgs;
72730
72847
  exports2.inferFullFileWriteContent = inferFullFileWriteContent;
72731
72848
  exports2.wrapWriteWithFsCas = wrapWriteWithFsCas;
72849
+ exports2.measureFsAuthorization = measureFsAuthorization;
72850
+ exports2.measureFsAuthorizationToken = measureFsAuthorizationToken;
72732
72851
  var cas_attestation_js_1 = require_cas_attestation();
72733
72852
  var fs_js_1 = require_fs();
72734
72853
  function inferFsPathFromArgs(args) {
@@ -72755,20 +72874,16 @@ var require_fs_default_wire = __commonJS({
72755
72874
  }
72756
72875
  return null;
72757
72876
  }
72758
- async function wrapWriteWithFsCas(args, write) {
72877
+ async function wrapWriteWithFsCas(args, write, opts) {
72759
72878
  const filePath = inferFsPathFromArgs(args);
72760
72879
  const content = inferFullFileWriteContent(args);
72761
- if (filePath && content != null) {
72762
- let expected;
72763
- try {
72764
- expected = await (0, fs_js_1.createFsVersionToken)(filePath);
72765
- } catch {
72766
- return write();
72767
- }
72880
+ const expected = opts && typeof opts.expected_token === "string" ? opts.expected_token : null;
72881
+ if (filePath && content != null && expected !== null) {
72768
72882
  return (0, fs_js_1.writeFileIfUnchanged)({
72769
72883
  path: filePath,
72770
72884
  expected_token: expected,
72771
- content
72885
+ content,
72886
+ ...opts && "expected_identity" in opts ? { expected_identity: opts.expected_identity ?? null } : {}
72772
72887
  });
72773
72888
  }
72774
72889
  const raw = await write();
@@ -72776,6 +72891,24 @@ var require_fs_default_wire = __commonJS({
72776
72891
  return raw;
72777
72892
  return raw;
72778
72893
  }
72894
+ async function measureFsAuthorization(args) {
72895
+ const filePath = inferFsPathFromArgs(args);
72896
+ const content = inferFullFileWriteContent(args);
72897
+ if (!filePath || content == null)
72898
+ return null;
72899
+ try {
72900
+ const target = await (0, fs_js_1.assertSafeCasPath)(filePath);
72901
+ const expected_identity = await (0, fs_js_1.fsObjectIdentity)(target);
72902
+ const expected_token = await (0, fs_js_1.createFsVersionToken)(target);
72903
+ return { expected_token, expected_identity };
72904
+ } catch {
72905
+ return null;
72906
+ }
72907
+ }
72908
+ async function measureFsAuthorizationToken(args) {
72909
+ const m = await measureFsAuthorization(args);
72910
+ return m ? m.expected_token : null;
72911
+ }
72779
72912
  }
72780
72913
  });
72781
72914
 
@@ -72960,6 +73093,11 @@ var require_tool_registry = __commonJS({
72960
73093
  lastDerivation = d.derivation;
72961
73094
  return d.call;
72962
73095
  };
73096
+ const rebindAndRemeasure = async () => {
73097
+ const c = await rebind();
73098
+ casAuth = await (0, fs_default_wire_js_1.measureFsAuthorization)(args);
73099
+ return c;
73100
+ };
72963
73101
  const call = await rebind();
72964
73102
  const refreshContext = async (c) => (0, freshness_js_1.collectFreshnessCallContext)({
72965
73103
  call: {
@@ -72970,16 +73108,17 @@ var require_tool_registry = __commonJS({
72970
73108
  resolvePriorContent: guardCfg.resolvePriorContent
72971
73109
  });
72972
73110
  const fctx = await refreshContext(call);
73111
+ let casAuth = await (0, fs_default_wire_js_1.measureFsAuthorization)(args);
72973
73112
  const factory = async (_envelope, redacted, execution) => {
72974
73113
  const rawArgs = redacted ? redacted.arguments : args;
72975
- return (0, fs_default_wire_js_1.wrapWriteWithFsCas)(rawArgs, () => execution ? rawExecute(rawArgs, execution) : rawExecute(rawArgs));
73114
+ return (0, fs_default_wire_js_1.wrapWriteWithFsCas)(rawArgs, () => execution ? rawExecute(rawArgs, execution) : rawExecute(rawArgs), casAuth ? { expected_token: casAuth.expected_token, expected_identity: casAuth.expected_identity } : {});
72976
73115
  };
72977
73116
  const outcome = (0, auto_recheck_js_1.normalizeAutoRecheck)(guardCfg.autoRecheck) ? await (0, auto_recheck_js_1.runAutoRecheckLoop)({
72978
73117
  call,
72979
73118
  factory,
72980
73119
  config: guardCfg,
72981
73120
  callContext: fctx,
72982
- rebind,
73121
+ rebind: rebindAndRemeasure,
72983
73122
  refreshContext
72984
73123
  }) : await (0, guard_js_1.guardToolCall)(call, factory, guardCfg, fctx);
72985
73124
  return (0, auto_derive_js_1.attachDerivation)(outcome, lastDerivation);
@@ -75298,9 +75437,10 @@ var require_cjs4 = __commonJS({
75298
75437
  "use strict";
75299
75438
  Object.defineProperty(exports2, "__esModule", { value: true });
75300
75439
  exports2.monitorAttestSigningInput = exports2.kidFromMonitoringAttestation = exports2.tryIssueMonitoringAttestation = exports2.DEFAULT_MONITORING_SINK_TIMEOUT_MS = exports2.ackBytes = exports2.verifyAckHmac = exports2.monitoringDeliveryFailClosed = exports2.formatMonitoringDeliveryLine = exports2.deliverMonitoring = exports2.hashObservedContent = exports2.observeCommit = exports2.deriveKeySignal = exports2.pathClass = exports2.classifyCommand = exports2.projectState = exports2.emptySessionState = exports2.computeTainted = exports2.evaluate = exports2.updateSession = exports2.SESSION_TAINT_VERSION = exports2.SessionTaintTracker = exports2.readDecision = exports2.EXECUTION_TIME_FP_REASONS = exports2.EXECUTION_STATE_UNMEASURABLE_NOTE = exports2.isUnmeasurableExecutionStateReason = exports2.computeCanonicalBundleFingerprint = exports2.authorizedFingerprintFromEnvelope = exports2.checkExecutionTimeFingerprint = exports2.computeBundleFingerprint = exports2.computeArtifactDigest = exports2.evaluateEnvelope = exports2.RECEIPT_PREV_NULL = exports2.decodeReceiptBodyPrev = exports2.previousReceiptCommitment = exports2.verifyReceiptChainLinkage = exports2.canonicalJson = exports2.computeBodyHash = exports2.bindReceiptToEnvelope = exports2.DETECTOR_VERSION = exports2.builtinDetector = exports2.resetPolicyWarnForTests = exports2.warnPolicyAbsentOnce = exports2.observePolicyPresence = exports2.detectPolicyPresence = exports2.policyPresenceOf = exports2.withPolicy = exports2.POLICY_ABSENT_WARN = exports2.POLICY_MARKER = exports2.CODERIFTS_POLICY = exports2.guardToolCall = void 0;
75301
- exports2.DB_ABSENT_TOKEN = exports2.DB_VERSION_TOKEN_PREFIX = exports2.dbTokenRaw = exports2.writeDbIfUnchanged = exports2.createDbVersionToken = exports2.API_ABSENT_TOKEN = exports2.API_VERSION_TOKEN_PREFIX = exports2.apiTokenRaw = exports2.writeApiIfUnchanged = exports2.createApiVersionToken = exports2.FS_ABSENT_TOKEN = exports2.FS_VERSION_TOKEN_PREFIX = exports2.fsTokenContentHash = exports2.createFsPriorContentResolver = exports2.writeFileIfUnchanged = exports2.readVersionedFile = exports2.createFsVersionToken = exports2.StaleVersionTokenAbort = exports2.executeIfUnchanged = exports2.RESIDUAL_UNCONDITIONAL_WRITE = exports2.conditionalWriteResidual = exports2.tokensEqual = exports2.buildConditionalWriteBasis = exports2.buildFreshnessBasis = exports2.collectFreshnessCallContext = exports2.artifactIdsForResolve = exports2.isMutatingCall = exports2.isWriteStyleCall = exports2.freshnessAllowsEnforce = exports2.computePathSetTreeHash = exports2.contentByteIdentical = exports2.assessWriteStylePrior = exports2.assessFreshness = exports2.deriveProofBanner = exports2.attachProofToAgentResponse = exports2.renderFinalAnswerProof = exports2.EXECUTION_PROOF_SPEC = exports2.assertEnforcedReceiptInvariant = exports2.hashExecutionResult = exports2.buildExecutionProof = exports2.COVERAGE_ATTEST_ENVELOPE_TAG = exports2.COVERAGE_ATTEST_SIGNING_PREFIX = exports2.COVERAGE_ATTEST_VERSION = exports2.coverageAttestSigningInput = exports2.kidFromCoverageAttestation = exports2.tryIssueCoverageAttestation = exports2.MONITOR_ATTEST_ENVELOPE_TAG = exports2.MONITOR_ATTEST_SIGNING_PREFIX = exports2.MONITOR_ATTEST_VERSION = exports2.receiptDigestOfToken = void 0;
75302
- exports2.freezeCoverageObserved = exports2.createCoverageObserver = exports2.readExecutionGrantToken = exports2.isExecutionGrantEnabled = exports2.guardedFractionAmongRoutes = exports2.foldTableSettledCalls = exports2.withCodeRifts = exports2.AUTO_DERIVE_READ_TIMEOUT_MS = exports2.AUTO_DERIVE_SOURCE = exports2.defaultFsReader = exports2.normalizeAutoDerive = exports2.runAutoDerive = exports2.AUTO_RECHECK_MAX_CAP = exports2.clampMaxAttempts = exports2.normalizeAutoRecheck = exports2.runAutoRecheckLoop = exports2.coverageReport = exports2.DEPLOY_REPAIRABLE_REASONS = exports2.bindDeploy = exports2.verifyDeployReceiptToken = exports2.DEPLOY_RECEIPT_VIEW_SPEC = exports2.isVerifiedDeployReceiptView = exports2.asVerifiedDeployReceiptView = exports2.deployGate = exports2.gateDecision = exports2.RegistryConstructionError = exports2.guardToolRegistry = exports2.globToRegExp = exports2.matchGlob = exports2.blobMapKey = exports2.classifyByName = exports2.resolveArtifacts = exports2.REMEDIATION_LOOP_ATTESTATION_SPEC = exports2.isCasAttestation = exports2.readPriorBlockRemediation = exports2.readRemediationTransaction = exports2.buildRemediationLoopAttestation = exports2.COMMIT_EVIDENCE_MISSING = exports2.CAS_ATTESTATION_SPEC = exports2.strictCommitObservation = exports2.extractExecutorAttestationToken = exports2.evaluateCasEvidence = exports2.isExecuteIfUnchangedOutcome = exports2.isGuardExecutionProof = exports2.buildCasAttestation = exports2.REGISTRY_ABSENT_TOKEN = exports2.REGISTRY_VERSION_TOKEN_PREFIX = exports2.registryTokenRaw = exports2.writeRegistryIfUnchanged = exports2.createRegistryVersionToken = void 0;
75303
- exports2.kidFromToolsetAttestation = exports2.tryIssueToolsetAttestation = exports2.toolsetAttestSigningInput = exports2.declarationEntriesFromTools = exports2.computeToolsetDigest = exports2.TOOLSET_ATTEST_STATEMENT = exports2.TOOLSET_ATTEST_ENVELOPE_TAG = exports2.TOOLSET_ATTEST_SIGNING_PREFIX = exports2.TOOLSET_ATTEST_VERSION = exports2.surfaceEnvelopeFields = exports2.isGuardOutcome = exports2.executeLangGraphToolCall = exports2.executeGeminiToolCall = exports2.executeAnthropicToolCall = exports2.executeOpenAIToolCall = exports2.executeProtectedTool = exports2.defaultSerializeGeminiToolResult = exports2.bindGeminiGuardOutcome = exports2.protectedToolToFunctionDeclaration = exports2.toGeminiTools = exports2.geminiToolAdapter = exports2.withCodeRiftsGemini = exports2.wrapWriteWithFsCas = exports2.inferFullFileWriteContent = exports2.inferFsPathFromArgs = exports2.FRESHNESS_RESOLVER_FIX = exports2.freshnessRefusalTeaching = exports2.formatGateRefusalBody = exports2.defaultSerializeLangGraphToolResult = exports2.bindLangGraphGuardOutcome = exports2.LangGraphToolsNotStructuredError = exports2.isLangGraphReactAgentTool = exports2.bindLangGraphTools = exports2.protectedToolToLangGraph = exports2.toLangGraphTools = exports2.langGraphToolAdapter = exports2.withCodeRiftsLangGraph = exports2.defaultSerializeAnthropicToolResult = exports2.bindAnthropicGuardOutcome = exports2.protectedToolToAnthropic = exports2.toAnthropicTools = exports2.anthropicToolAdapter = exports2.withCodeRiftsAnthropic = exports2.defaultSerializeOpenAIToolResult = exports2.bindOpenAIGuardOutcome = exports2.protectedToolToOpenAI = exports2.toOpenAITools = exports2.openAIToolAdapter = exports2.withCodeRiftsOpenAI = exports2.formatCoverageObservedLine = void 0;
75440
+ exports2.createDbVersionToken = exports2.API_ABSENT_TOKEN = exports2.API_VERSION_TOKEN_PREFIX = exports2.apiTokenRaw = exports2.writeApiIfUnchanged = exports2.createApiVersionToken = exports2.identitiesEqual = exports2.fsObjectIdentity = exports2.UnsafeCasPathError = exports2.assertSafeCasPath = exports2.FS_ABSENT_TOKEN = exports2.FS_VERSION_TOKEN_PREFIX = exports2.fsTokenContentHash = exports2.createFsPriorContentResolver = exports2.writeFileIfUnchanged = exports2.readVersionedFile = exports2.createFsVersionToken = exports2.StaleVersionTokenAbort = exports2.executeIfUnchanged = exports2.RESIDUAL_UNCONDITIONAL_WRITE = exports2.conditionalWriteResidual = exports2.tokensEqual = exports2.buildConditionalWriteBasis = exports2.buildFreshnessBasis = exports2.collectFreshnessCallContext = exports2.artifactIdsForResolve = exports2.isMutatingCall = exports2.isWriteStyleCall = exports2.freshnessAllowsEnforce = exports2.computePathSetTreeHash = exports2.contentByteIdentical = exports2.assessWriteStylePrior = exports2.assessFreshness = exports2.deriveProofBanner = exports2.attachProofToAgentResponse = exports2.renderFinalAnswerProof = exports2.EXECUTION_PROOF_SPEC = exports2.assertEnforcedReceiptInvariant = exports2.hashExecutionResult = exports2.buildExecutionProof = exports2.COVERAGE_ATTEST_ENVELOPE_TAG = exports2.COVERAGE_ATTEST_SIGNING_PREFIX = exports2.COVERAGE_ATTEST_VERSION = exports2.coverageAttestSigningInput = exports2.kidFromCoverageAttestation = exports2.tryIssueCoverageAttestation = exports2.MONITOR_ATTEST_ENVELOPE_TAG = exports2.MONITOR_ATTEST_SIGNING_PREFIX = exports2.MONITOR_ATTEST_VERSION = exports2.receiptDigestOfToken = void 0;
75441
+ exports2.guardedFractionAmongRoutes = exports2.foldTableSettledCalls = exports2.withCodeRifts = exports2.AUTO_DERIVE_READ_TIMEOUT_MS = exports2.AUTO_DERIVE_SOURCE = exports2.defaultFsReader = exports2.normalizeAutoDerive = exports2.runAutoDerive = exports2.AUTO_RECHECK_MAX_CAP = exports2.clampMaxAttempts = exports2.normalizeAutoRecheck = exports2.runAutoRecheckLoop = exports2.coverageReport = exports2.DEPLOY_REPAIRABLE_REASONS = exports2.bindDeploy = exports2.verifyDeployReceiptToken = exports2.DEPLOY_RECEIPT_VIEW_SPEC = exports2.isVerifiedDeployReceiptView = exports2.asVerifiedDeployReceiptView = exports2.deployGate = exports2.gateDecision = exports2.RegistryConstructionError = exports2.guardToolRegistry = exports2.globToRegExp = exports2.matchGlob = exports2.blobMapKey = exports2.classifyByName = exports2.resolveArtifacts = exports2.REMEDIATION_LOOP_ATTESTATION_SPEC = exports2.isCasAttestation = exports2.readPriorBlockRemediation = exports2.readRemediationTransaction = exports2.buildRemediationLoopAttestation = exports2.COMMIT_EVIDENCE_MISSING = exports2.CAS_ATTESTATION_SPEC = exports2.strictCommitObservation = exports2.extractExecutorAttestationToken = exports2.evaluateCasEvidence = exports2.isExecuteIfUnchangedOutcome = exports2.isGuardExecutionProof = exports2.buildCasAttestation = exports2.REGISTRY_ABSENT_TOKEN = exports2.REGISTRY_VERSION_TOKEN_PREFIX = exports2.registryTokenRaw = exports2.writeRegistryIfUnchanged = exports2.createRegistryVersionToken = exports2.DB_ABSENT_TOKEN = exports2.DB_VERSION_TOKEN_PREFIX = exports2.dbTokenRaw = exports2.writeDbIfUnchanged = void 0;
75442
+ exports2.TOOLSET_ATTEST_ENVELOPE_TAG = exports2.TOOLSET_ATTEST_SIGNING_PREFIX = exports2.TOOLSET_ATTEST_VERSION = exports2.surfaceEnvelopeFields = exports2.isGuardOutcome = exports2.executeLangGraphToolCall = exports2.executeGeminiToolCall = exports2.executeAnthropicToolCall = exports2.executeOpenAIToolCall = exports2.executeProtectedTool = exports2.defaultSerializeGeminiToolResult = exports2.bindGeminiGuardOutcome = exports2.protectedToolToFunctionDeclaration = exports2.toGeminiTools = exports2.geminiToolAdapter = exports2.withCodeRiftsGemini = exports2.measureFsAuthorizationToken = exports2.measureFsAuthorization = exports2.wrapWriteWithFsCas = exports2.inferFullFileWriteContent = exports2.inferFsPathFromArgs = exports2.FRESHNESS_RESOLVER_FIX = exports2.freshnessRefusalTeaching = exports2.formatGateRefusalBody = exports2.defaultSerializeLangGraphToolResult = exports2.bindLangGraphGuardOutcome = exports2.LangGraphToolsNotStructuredError = exports2.isLangGraphReactAgentTool = exports2.bindLangGraphTools = exports2.protectedToolToLangGraph = exports2.toLangGraphTools = exports2.langGraphToolAdapter = exports2.withCodeRiftsLangGraph = exports2.defaultSerializeAnthropicToolResult = exports2.bindAnthropicGuardOutcome = exports2.protectedToolToAnthropic = exports2.toAnthropicTools = exports2.anthropicToolAdapter = exports2.withCodeRiftsAnthropic = exports2.defaultSerializeOpenAIToolResult = exports2.bindOpenAIGuardOutcome = exports2.protectedToolToOpenAI = exports2.toOpenAITools = exports2.openAIToolAdapter = exports2.withCodeRiftsOpenAI = exports2.formatCoverageObservedLine = exports2.freezeCoverageObserved = exports2.createCoverageObserver = exports2.readExecutionGrantToken = exports2.isExecutionGrantEnabled = void 0;
75443
+ exports2.kidFromToolsetAttestation = exports2.tryIssueToolsetAttestation = exports2.toolsetAttestSigningInput = exports2.declarationEntriesFromTools = exports2.computeToolsetDigest = exports2.TOOLSET_ATTEST_STATEMENT = void 0;
75304
75444
  var guard_js_1 = require_guard();
75305
75445
  Object.defineProperty(exports2, "guardToolCall", { enumerable: true, get: function() {
75306
75446
  return guard_js_1.guardToolCall;
@@ -75589,6 +75729,18 @@ var require_cjs4 = __commonJS({
75589
75729
  Object.defineProperty(exports2, "FS_ABSENT_TOKEN", { enumerable: true, get: function() {
75590
75730
  return fs_js_1.FS_ABSENT_TOKEN;
75591
75731
  } });
75732
+ Object.defineProperty(exports2, "assertSafeCasPath", { enumerable: true, get: function() {
75733
+ return fs_js_1.assertSafeCasPath;
75734
+ } });
75735
+ Object.defineProperty(exports2, "UnsafeCasPathError", { enumerable: true, get: function() {
75736
+ return fs_js_1.UnsafeCasPathError;
75737
+ } });
75738
+ Object.defineProperty(exports2, "fsObjectIdentity", { enumerable: true, get: function() {
75739
+ return fs_js_1.fsObjectIdentity;
75740
+ } });
75741
+ Object.defineProperty(exports2, "identitiesEqual", { enumerable: true, get: function() {
75742
+ return fs_js_1.identitiesEqual;
75743
+ } });
75592
75744
  var api_js_1 = require_api3();
75593
75745
  Object.defineProperty(exports2, "createApiVersionToken", { enumerable: true, get: function() {
75594
75746
  return api_js_1.createApiVersionToken;
@@ -75876,6 +76028,12 @@ var require_cjs4 = __commonJS({
75876
76028
  Object.defineProperty(exports2, "wrapWriteWithFsCas", { enumerable: true, get: function() {
75877
76029
  return fs_default_wire_js_1.wrapWriteWithFsCas;
75878
76030
  } });
76031
+ Object.defineProperty(exports2, "measureFsAuthorization", { enumerable: true, get: function() {
76032
+ return fs_default_wire_js_1.measureFsAuthorization;
76033
+ } });
76034
+ Object.defineProperty(exports2, "measureFsAuthorizationToken", { enumerable: true, get: function() {
76035
+ return fs_default_wire_js_1.measureFsAuthorizationToken;
76036
+ } });
75879
76037
  var gemini_js_1 = require_gemini();
75880
76038
  Object.defineProperty(exports2, "withCodeRiftsGemini", { enumerable: true, get: function() {
75881
76039
  return gemini_js_1.withCodeRiftsGemini;
@@ -253975,6 +254133,16 @@ var require_github_enforcement = __commonJS({
253975
254133
  RULESET_ABSENT: "ruleset_absent",
253976
254134
  /** Rulesets could not be read at all. */
253977
254135
  RULESET_UNREADABLE: "ruleset_unreadable",
254136
+ /** A ruleset requires our context but does not TARGET the branch that matters (1104a). */
254137
+ RULESET_REF_NOT_TARGETED: "ruleset_ref_not_targeted",
254138
+ /** Active, bound, targeted — but strict latest-head is off (1104b). Recorded, not fatal. */
254139
+ RULESET_STRICT_POLICY_OFF: "ruleset_strict_policy_off",
254140
+ /** 1102 — no active targeting ruleset requires a workflow at all. */
254141
+ REQUIRED_WORKFLOW_ABSENT: "required_workflow_absent",
254142
+ /** 1102 — a workflows rule exists but carries no sha, so it names a path and not bytes. */
254143
+ REQUIRED_WORKFLOW_UNPINNED: "required_workflow_unpinned",
254144
+ /** 1102 — a required workflow is pinned to a full commit sha. */
254145
+ REQUIRED_WORKFLOW_SHA_PINNED: "required_workflow_sha_pinned",
253978
254146
  REQUIRED_CHECK_ABSENT: "required_check_absent",
253979
254147
  ACTIVE_JOB: "active_job",
253980
254148
  NO_ACTIVE_JOB: "no_active_job",
@@ -254005,6 +254173,41 @@ var require_github_enforcement = __commonJS({
254005
254173
  const n = Number(value);
254006
254174
  return Number.isInteger(n) && n > 0 ? n : null;
254007
254175
  }
254176
+ function refPatterns(rs, key) {
254177
+ const rn = rs && rs.conditions && rs.conditions.ref_name;
254178
+ if (!rn || !Array.isArray(rn[key])) return null;
254179
+ return rn[key].map((x) => String(x));
254180
+ }
254181
+ function targetsDefaultBranch(entry, defaultBranch) {
254182
+ if (entry.target === "tag") return false;
254183
+ const inc = entry.ref_include;
254184
+ if (!Array.isArray(inc)) return null;
254185
+ const ref = defaultBranch ? `refs/heads/${defaultBranch}` : null;
254186
+ const matches = (pat) => {
254187
+ if (pat === "~ALL" || pat === "~DEFAULT_BRANCH") return true;
254188
+ if (!ref) return null;
254189
+ if (pat === ref) return true;
254190
+ if (!pat.includes("*") && !pat.includes("[") && !pat.includes("?")) return false;
254191
+ const rx = new RegExp(`^${pat.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}$`);
254192
+ return rx.test(ref);
254193
+ };
254194
+ let included = false;
254195
+ for (const pat of inc) {
254196
+ const m = matches(pat);
254197
+ if (m === null) return null;
254198
+ if (m) {
254199
+ included = true;
254200
+ break;
254201
+ }
254202
+ }
254203
+ if (!included) return false;
254204
+ for (const pat of entry.ref_exclude || []) {
254205
+ const m = matches(pat);
254206
+ if (m === null) return null;
254207
+ if (m) return false;
254208
+ }
254209
+ return true;
254210
+ }
254008
254211
  function rulesetRequiredCheckEntries(rulesets) {
254009
254212
  const out = [];
254010
254213
  const list = Array.isArray(rulesets) ? rulesets : [];
@@ -254023,7 +254226,15 @@ var require_github_enforcement = __commonJS({
254023
254226
  enforcement: typeof rs.enforcement === "string" ? rs.enforcement : null,
254024
254227
  integration_id: numericAppId(c && c.integration_id),
254025
254228
  bypass_visible: Object.prototype.hasOwnProperty.call(rs, "bypass_actors"),
254026
- bypass_count: Array.isArray(rs.bypass_actors) ? rs.bypass_actors.length : null
254229
+ bypass_count: Array.isArray(rs.bypass_actors) ? rs.bypass_actors.length : null,
254230
+ // 1104a — a ruleset can be ACTIVE and target nothing that matters. MEASURED against the
254231
+ // live API: `target` and `conditions.ref_name` are both in the detail document we already
254232
+ // fetch, so this is a grading change and needs no new call.
254233
+ target: typeof rs.target === "string" ? rs.target : null,
254234
+ ref_include: refPatterns(rs, "include"),
254235
+ ref_exclude: refPatterns(rs, "exclude"),
254236
+ // 1104b — same document, one boolean.
254237
+ strict_policy: typeof params.strict_required_status_checks_policy === "boolean" ? params.strict_required_status_checks_policy : null
254027
254238
  });
254028
254239
  }
254029
254240
  }
@@ -254031,7 +254242,7 @@ var require_github_enforcement = __commonJS({
254031
254242
  return out;
254032
254243
  }
254033
254244
  var RULESET_BYPASS_CAVEAT = " Bypass actors were not readable on this surface, so this does not establish that nobody can bypass the ruleset.";
254034
- function evaluateRuleset(rulesets) {
254245
+ function evaluateRuleset(rulesets, defaultBranch = null) {
254035
254246
  if (rulesets == null) {
254036
254247
  return {
254037
254248
  status: STATUS.UNVERIFIABLE,
@@ -254062,12 +254273,41 @@ var require_github_enforcement = __commonJS({
254062
254273
  }
254063
254274
  const bound = active.find((e) => e.integration_id === ENFORCING_ISSUER_APP_ID);
254064
254275
  if (bound) {
254276
+ const targeted = targetsDefaultBranch(bound, defaultBranch);
254277
+ if (targeted === false) {
254278
+ return {
254279
+ status: STATUS.NOT_VERIFIED,
254280
+ reason_code: REASON.RULESET_REF_NOT_TARGETED,
254281
+ reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to the enforcing issuer, but its conditions do not target ${defaultBranch ? `refs/heads/${defaultBranch}` : "the default branch"}${bound.target === "tag" ? " (the ruleset targets tags, which never gate a merge to a branch)" : ""} \u2014 include: ${JSON.stringify(bound.ref_include)}, exclude: ${JSON.stringify(bound.ref_exclude || [])}`,
254282
+ ruleset_names: names,
254283
+ app_id: ENFORCING_ISSUER_APP_ID,
254284
+ ref_targeted: false
254285
+ };
254286
+ }
254287
+ if (targeted === null) {
254288
+ return {
254289
+ status: STATUS.UNVERIFIABLE,
254290
+ reason_code: REASON.RULESET_REF_NOT_TARGETED,
254291
+ reason: `active ruleset "${bound.ruleset}" binds "${CHECK_NAME}" to the enforcing issuer, but its ref conditions could not be read, so whether it targets the branch that matters CANNOT BE DETERMINED FROM HERE.${RULESET_BYPASS_CAVEAT}`,
254292
+ ruleset_names: names,
254293
+ app_id: ENFORCING_ISSUER_APP_ID,
254294
+ ref_targeted: null
254295
+ };
254296
+ }
254065
254297
  return {
254066
254298
  status: STATUS.VERIFIED,
254067
254299
  reason_code: REASON.RULESET_ISSUER_BOUND,
254068
- reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to integration_id ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 the contract-gate Action, which concludes failure and blocks the merge.${RULESET_BYPASS_CAVEAT}`,
254300
+ reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to integration_id ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 the contract-gate Action, which concludes failure and blocks the merge. It targets ${defaultBranch ? `refs/heads/${defaultBranch}` : "the default branch"}.` + (bound.strict_policy === true ? " strict_required_status_checks_policy is ON, so a stale head cannot merge." : bound.strict_policy === false ? " RESIDUAL: strict_required_status_checks_policy is OFF, so a branch whose head is behind the base can merge on a check that passed against older bytes." : " strict_required_status_checks_policy was not readable here.") + RULESET_BYPASS_CAVEAT,
254069
254301
  ruleset_names: names,
254070
- app_id: ENFORCING_ISSUER_APP_ID
254302
+ app_id: ENFORCING_ISSUER_APP_ID,
254303
+ ref_targeted: true,
254304
+ strict_policy: bound.strict_policy,
254305
+ strict_policy_residual: bound.strict_policy === false ? REASON.RULESET_STRICT_POLICY_OFF : null,
254306
+ // 1104c — NEVER an absence we cannot see. bypass_actors is in the ruleset schema but was
254307
+ // absent from all six live rulesets read without a credential (measured 2026-08-27), so
254308
+ // "not present in the response" is reported as NOT VISIBLE and never as "none configured".
254309
+ bypass_visible: bound.bypass_visible === true,
254310
+ bypass_count: bound.bypass_visible === true ? bound.bypass_count : null
254071
254311
  };
254072
254312
  }
254073
254313
  const other = active.find((e) => e.integration_id != null);
@@ -254088,9 +254328,66 @@ var require_github_enforcement = __commonJS({
254088
254328
  ruleset_names: names
254089
254329
  };
254090
254330
  }
254091
- function evaluateRequiredCheckAcrossSurfaces(protection, rulesets) {
254331
+ function evaluateRequiredWorkflowPin(rulesets, defaultBranch = null) {
254332
+ if (rulesets == null) {
254333
+ return { status: STATUS.UNVERIFIABLE, reason_code: REASON.RULESET_UNREADABLE, pinned: null, workflows: [] };
254334
+ }
254335
+ const found = [];
254336
+ for (const rs of Array.isArray(rulesets) ? rulesets : []) {
254337
+ if (!rs || typeof rs !== "object") continue;
254338
+ if (rs.enforcement !== "active") continue;
254339
+ const entryShape = {
254340
+ target: typeof rs.target === "string" ? rs.target : null,
254341
+ ref_include: refPatterns(rs, "include"),
254342
+ ref_exclude: refPatterns(rs, "exclude")
254343
+ };
254344
+ if (targetsDefaultBranch(entryShape, defaultBranch) !== true) continue;
254345
+ for (const rule of Array.isArray(rs.rules) ? rs.rules : []) {
254346
+ if (!rule || rule.type !== "workflows") continue;
254347
+ const list = rule.parameters && Array.isArray(rule.parameters.workflows) ? rule.parameters.workflows : [];
254348
+ for (const w of list) {
254349
+ if (!w || typeof w !== "object") continue;
254350
+ found.push({
254351
+ ruleset: String(rs.name || rs.id || "ruleset"),
254352
+ path: typeof w.path === "string" ? w.path : null,
254353
+ repository_id: Number.isInteger(w.repository_id) ? w.repository_id : null,
254354
+ ref: typeof w.ref === "string" ? w.ref : null,
254355
+ // The whole question. sha is optional in the schema, so its ABSENCE is the finding.
254356
+ sha: typeof w.sha === "string" && /^[0-9a-f]{40}$/i.test(w.sha) ? w.sha : null
254357
+ });
254358
+ }
254359
+ }
254360
+ }
254361
+ if (found.length === 0) {
254362
+ return {
254363
+ status: STATUS.NOT_VERIFIED,
254364
+ reason_code: REASON.REQUIRED_WORKFLOW_ABSENT,
254365
+ pinned: false,
254366
+ workflows: [],
254367
+ reason: `no active ruleset targeting this branch requires a workflow, so the check that gates the merge is bound only to the SHARED issuer ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 anyone who can edit .github/workflows can post under it`
254368
+ };
254369
+ }
254370
+ const pinned = found.filter((w) => w.sha != null);
254371
+ if (pinned.length === 0) {
254372
+ return {
254373
+ status: STATUS.NOT_VERIFIED,
254374
+ reason_code: REASON.REQUIRED_WORKFLOW_UNPINNED,
254375
+ pinned: false,
254376
+ workflows: found,
254377
+ reason: `a required workflow is configured (${found.map((w) => w.path || "?").join(", ")}) but NONE carries a sha. \`sha\` is optional in the rule schema, and without it the ruleset names a path rather than bytes, so rewriting that file still changes what runs`
254378
+ };
254379
+ }
254380
+ return {
254381
+ status: STATUS.VERIFIED,
254382
+ reason_code: REASON.REQUIRED_WORKFLOW_SHA_PINNED,
254383
+ pinned: true,
254384
+ workflows: found,
254385
+ reason: `active ruleset "${pinned[0].ruleset}" requires workflow ${pinned[0].path} pinned to sha ${pinned[0].sha} in repository_id ${pinned[0].repository_id}. Moving that pin requires editing the ruleset (repository administration), which is a different permission from workflow write. This does NOT establish that any pull request ran it, nor that bypass actors cannot skip the ruleset.`
254386
+ };
254387
+ }
254388
+ function evaluateRequiredCheckAcrossSurfaces(protection, rulesets, defaultBranch = null) {
254092
254389
  const classic = evaluateRequiredCheck(protection);
254093
- const ruleset = evaluateRuleset(rulesets);
254390
+ const ruleset = evaluateRuleset(rulesets, defaultBranch);
254094
254391
  const merged = (base) => ({
254095
254392
  ...base,
254096
254393
  surfaces_inspected: ["classic_branch_protection", rulesets == null ? "rulesets:unread" : "rulesets"],
@@ -254210,8 +254507,22 @@ var require_github_enforcement = __commonJS({
254210
254507
  var STATUS = Object.freeze({
254211
254508
  VERIFIED: "VERIFIED",
254212
254509
  NOT_VERIFIED: "NOT_VERIFIED",
254213
- UNVERIFIABLE: "UNVERIFIABLE"
254214
- });
254510
+ UNVERIFIABLE: "UNVERIFIABLE",
254511
+ UNSUPPORTED_PLAN: "UNSUPPORTED_PLAN",
254512
+ PROVIDER_ERROR: "PROVIDER_ERROR"
254513
+ });
254514
+ var FRESHNESS = Object.freeze({ FRESH: "FRESH", STALE_EVIDENCE: "STALE_EVIDENCE" });
254515
+ function supportsStrongClaim(status) {
254516
+ return status === STATUS.VERIFIED;
254517
+ }
254518
+ var EVIDENCE_TTL_SECONDS = 3600;
254519
+ function evidenceFreshness(report, nowIso) {
254520
+ if (!report || !report.expires_at) return null;
254521
+ const now = Date.parse(nowIso || (/* @__PURE__ */ new Date()).toISOString());
254522
+ const exp = Date.parse(report.expires_at);
254523
+ if (!Number.isFinite(now) || !Number.isFinite(exp)) return null;
254524
+ return now > exp ? FRESHNESS.STALE_EVIDENCE : FRESHNESS.FRESH;
254525
+ }
254215
254526
  function resolveGitHubAuth(env = process.env) {
254216
254527
  const user = String(env && (env.GITHUB_TOKEN || env.GH_TOKEN) || "").trim();
254217
254528
  if (user) {
@@ -254542,6 +254853,10 @@ var require_github_enforcement = __commonJS({
254542
254853
  NO_TOKEN_INSTRUCTION,
254543
254854
  LAYER_IDS,
254544
254855
  STATUS,
254856
+ FRESHNESS,
254857
+ EVIDENCE_TTL_SECONDS,
254858
+ supportsStrongClaim,
254859
+ evidenceFreshness,
254545
254860
  REASON,
254546
254861
  CHECK_NAME,
254547
254862
  GATE_ACTION,
@@ -254555,6 +254870,8 @@ var require_github_enforcement = __commonJS({
254555
254870
  evaluateLayers,
254556
254871
  evaluateRequiredCheck,
254557
254872
  evaluateRuleset,
254873
+ evaluateRequiredWorkflowPin,
254874
+ targetsDefaultBranch,
254558
254875
  evaluateRequiredCheckAcrossSurfaces,
254559
254876
  verifiedRequiresBinding,
254560
254877
  scanLocalWorkflows,
@@ -254941,7 +255258,7 @@ var require_setup_required_check = __commonJS({
254941
255258
  }
254942
255259
  } catch {
254943
255260
  }
254944
- const rulesetVerdict = evaluateRuleset(rulesetDocs);
255261
+ const rulesetVerdict = evaluateRuleset(rulesetDocs, branch);
254945
255262
  const rulesetNamed = detectRulesetRequiredCheck(rulesetDocs || [], CHECK_NAME);
254946
255263
  const ruleset = {
254947
255264
  enforced: rulesetVerdict.status === STATUS.VERIFIED,
@@ -255314,6 +255631,8 @@ var require_enforce_check = __commonJS({
255314
255631
  var chalk = require_source();
255315
255632
  var {
255316
255633
  EVIDENCE_SPEC,
255634
+ EVIDENCE_TTL_SECONDS,
255635
+ supportsStrongClaim,
255317
255636
  APP_INSTALLATION,
255318
255637
  NO_TOKEN_INSTRUCTION,
255319
255638
  resolveGitHubAuth,
@@ -255325,6 +255644,35 @@ var require_enforce_check = __commonJS({
255325
255644
  var { isValidRepo } = require_status();
255326
255645
  var { resolveOwnerRepo } = require_setup_required_check();
255327
255646
  if (process.env.NO_COLOR) chalk.level = 0;
255647
+ var crypto = require("node:crypto");
255648
+ var EVIDENCE_SCOPE = Object.freeze({
255649
+ grades: "repository configuration \u2014 what the provider would require of any pull request",
255650
+ covers: Object.freeze([
255651
+ "required status check present and named exactly",
255652
+ "the check bound to an issuer that can block (integration_id / app_id)",
255653
+ "ruleset enforcement is active rather than evaluate",
255654
+ "the ruleset targets the branch that matters",
255655
+ "strict latest-head policy recorded",
255656
+ "bypass actors reported as VISIBLE or NOT VISIBLE, never as absent"
255657
+ ]),
255658
+ does_not_cover: Object.freeze([
255659
+ "which GitHub App actually posted a check run on any pull request",
255660
+ "the head_sha a check run executed against",
255661
+ "whether any check run concluded success",
255662
+ "a recomputable external proof hash over a specific run"
255663
+ ]),
255664
+ why_not: "Those are facts about one pull request at one moment, not about configuration. Reading them needs a per-PR check-runs query this command does not make; a passing config is not a passing run."
255665
+ });
255666
+ function responseDigest(read) {
255667
+ if (!read) return null;
255668
+ return {
255669
+ endpoint: read.endpoint ? `GET ${read.endpoint}` : null,
255670
+ http_status: read.status == null ? null : read.status,
255671
+ // Hash of the BODY as we received it. Not the verdict — the INPUT to the verdict, so a
255672
+ // disagreement can be localised to the answer or to our grading of it.
255673
+ body_sha256: read.body === void 0 || read.body === null ? null : `sha256:${crypto.createHash("sha256").update(JSON.stringify(read.body)).digest("hex")}`
255674
+ };
255675
+ }
255328
255676
  var USAGE = [
255329
255677
  "Usage: coderifts enforce --check --repo owner/repo [--env <github-environment>] [--branch <name>] [--json]",
255330
255678
  "",
@@ -255437,7 +255785,12 @@ var require_enforce_check = __commonJS({
255437
255785
  const report2 = {
255438
255786
  command: "enforce --check",
255439
255787
  spec: EVIDENCE_SPEC,
255788
+ // OBSERVED_AT WAS NOT ADDED. `queried_at` already carries exactly that fact, and a
255789
+ // second name for one fact is the defect this round was told to look for.
255440
255790
  queried_at: queriedAt,
255791
+ expires_at: new Date(Date.parse(queriedAt) + EVIDENCE_TTL_SECONDS * 1e3).toISOString(),
255792
+ evidence_ttl_seconds: EVIDENCE_TTL_SECONDS,
255793
+ evidence_scope: EVIDENCE_SCOPE,
255441
255794
  repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
255442
255795
  branch: options.branch || null,
255443
255796
  environment: environmentName,
@@ -255481,14 +255834,33 @@ var require_enforce_check = __commonJS({
255481
255834
  const report = {
255482
255835
  command: "enforce --check",
255483
255836
  spec: EVIDENCE_SPEC,
255837
+ // OBSERVED_AT WAS NOT ADDED. `queried_at` already carries exactly that fact, and a
255838
+ // second name for one fact is the defect this round was told to look for.
255484
255839
  queried_at: queriedAt,
255840
+ expires_at: new Date(Date.parse(queriedAt) + EVIDENCE_TTL_SECONDS * 1e3).toISOString(),
255841
+ evidence_ttl_seconds: EVIDENCE_TTL_SECONDS,
255842
+ evidence_scope: EVIDENCE_SCOPE,
255485
255843
  repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
255486
255844
  branch,
255487
255845
  environment: environmentName,
255488
255846
  provider: "github",
255489
255847
  auth: authPublic,
255848
+ // The raw answers behind the verdict, so a disagreement can be localised to the provider's
255849
+ // response or to our grading of it.
255850
+ responses: {
255851
+ protection: responseDigest(protectionRead),
255852
+ environment: responseDigest(environmentRead)
255853
+ },
255490
255854
  ...evaluated
255491
255855
  };
255856
+ if (report.claim && report.claim.inescapable_deploy === true) {
255857
+ const unsupported = (report.layers || []).filter((l) => !supportsStrongClaim(l.status));
255858
+ if (unsupported.length > 0) {
255859
+ throw new Error(
255860
+ `enforce --check: inescapable_deploy is true with ${unsupported.length} layer(s) not VERIFIED (${unsupported.map((l) => `${l.id}=${l.status}`).join(", ")}) \u2014 refusing to emit`
255861
+ );
255862
+ }
255863
+ }
255492
255864
  if (options.json) log(JSON.stringify(report, null, 2));
255493
255865
  else log(renderHuman(report));
255494
255866
  return { exitCode: 0, report };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "5.0.1",
3
+ "version": "6.0.1",
4
4
  "description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
5
5
  "author": "CodeRifts <hello@coderifts.com>",
6
6
  "license": "MIT",
@@ -39,13 +39,13 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "node scripts/copy-corpus.js && node scripts/build.js",
42
- "prepublishOnly": "node scripts/assert-guard-major.js && bash ../../scripts/freeze-gate.sh && npm run build",
42
+ "prepublishOnly": "node scripts/assert-guard-major.js && node scripts/assert-changelog-version.js && bash ../../scripts/freeze-gate.sh && npm run build",
43
43
  "assert-guard-major": "node scripts/assert-guard-major.js",
44
44
  "postinstall": "node scripts/postinstall.js",
45
45
  "test": "node --test test/*.test.js"
46
46
  },
47
47
  "dependencies": {
48
- "@coderifts/agent-guard": "^12.0.0",
48
+ "@coderifts/agent-guard": "^13.0.0",
49
49
  "chalk": "^4.1.2",
50
50
  "cli-table3": "^0.6.4",
51
51
  "commander": "^12.0.0",
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * CLI release gate (prepublishOnly): refuse to publish a version the CHANGELOG does not name.
6
+ *
7
+ * WHY THIS EXISTS — measured, not hypothetical. On 2026-08-27 packages/cli/CHANGELOG.md listed
8
+ * 4.10.0 and then "Unreleased", while npm carried 4.11.0, 4.12.0, 5.0.0 and 5.0.1. Four published
9
+ * versions were undocumented and a fifth (5.0.0) was published while still labelled Unreleased.
10
+ * Two distinct failure modes produced that, and BOTH are caught by the one check below:
11
+ *
12
+ * 1. The bump commit never touched the changelog at all. Measured: of the last eight CLI version
13
+ * bumps, four omitted it (4.11.0, 4.12.0, 4.13.0, 5.0.1) — and those four are exactly the
14
+ * versions that went missing. The correlation is not partial.
15
+ * 2. The bump commit DID write the entry, but under "## Unreleased", and nobody retitled it at
16
+ * publish. Atomicity alone does not save you: @coderifts/agent-guard 12.0.0 was written
17
+ * atomically and still shipped as "Unreleased", and 11.0.1 before it had to retitle a stale
18
+ * heading by hand.
19
+ *
20
+ * So the invariant is NOT "the bump touched the changelog" — it is that the version being published
21
+ * appears as its own heading. A human step that must be remembered at publish time is the step that
22
+ * gets skipped; this makes it fail the publish instead.
23
+ *
24
+ * Dependency-free on purpose: a publish gate that needs an install to run is a gate that stops
25
+ * running (same reasoning as assert-guard-major.js).
26
+ */
27
+
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+
31
+ const PKG_PATH = path.join(__dirname, '..', 'package.json');
32
+ const CHANGELOG_PATH = path.join(__dirname, '..', 'CHANGELOG.md');
33
+
34
+ function fail(msg) {
35
+ console.error(`assert-changelog-version: FAIL — ${msg}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ /**
40
+ * Headings this file recognises as "this version is documented".
41
+ * A heading may carry a trailing note (e.g. "## 4.13.0 — never published"), so match the version
42
+ * token at a word boundary rather than requiring the line to be exactly the version.
43
+ */
44
+ function headingNamesVersion(line, version) {
45
+ if (!/^##\s+/.test(line)) return false;
46
+ const rest = line.replace(/^##\s+/, '').trim();
47
+ const esc = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
+ return new RegExp(`^v?${esc}(\\b|$)`).test(rest);
49
+ }
50
+
51
+ function main() {
52
+ if (!fs.existsSync(PKG_PATH)) fail(`package.json not found at ${PKG_PATH}`);
53
+ if (!fs.existsSync(CHANGELOG_PATH)) fail(`CHANGELOG.md not found at ${CHANGELOG_PATH}`);
54
+
55
+ let version;
56
+ try {
57
+ version = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8')).version;
58
+ } catch (err) {
59
+ fail(`package.json is not readable JSON: ${err.message}`);
60
+ }
61
+ if (typeof version !== 'string' || !version.trim()) fail('package.json has no version string');
62
+
63
+ const lines = fs.readFileSync(CHANGELOG_PATH, 'utf8').split('\n');
64
+ const headings = lines.filter((l) => /^##\s+/.test(l));
65
+ if (headings.length === 0) fail('CHANGELOG.md has no "## " headings at all');
66
+
67
+ const match = headings.find((l) => headingNamesVersion(l, version));
68
+ if (!match) {
69
+ const listed = headings.slice(0, 4).map((h) => h.replace(/^##\s+/, '').trim());
70
+ fail(
71
+ `package.json is ${version} but CHANGELOG.md has no "## ${version}" heading.\n`
72
+ + ` Top headings are: ${listed.join(' · ')}\n`
73
+ + ' Publishing now is how a version goes undocumented. Retitle the Unreleased section to\n'
74
+ + ` ${version}, or add its section, then publish.`,
75
+ );
76
+ }
77
+
78
+ // "Unreleased" must never be the section that documents the version being published.
79
+ if (/^##\s+unreleased\b/i.test(match)) {
80
+ fail(`the section naming ${version} is the Unreleased heading — retitle it before publishing`);
81
+ }
82
+
83
+ console.log(`assert-changelog-version: OK — ${version} is documented ("${match.trim()}")`);
84
+ }
85
+
86
+ main();
87
+
88
+ module.exports = { headingNamesVersion, PKG_PATH, CHANGELOG_PATH };
@@ -45,13 +45,33 @@
45
45
  * raise, and no product code. The floor rises anyway, because a semantics floor is about
46
46
  * what the CLI may RUN against: a guard that silently skips the atomicity demand on an ordinary
47
47
  * edit is exactly the class of difference this gate exists to refuse.)
48
+ * 6.0.0: floor raised to 13 (the CAS now conditions on the AUTHORIZATION-TIME token. Through 12.0.0
49
+ * wrapWriteWithFsCas measured its expected token INSIDE executeFactory — after preflight and after
50
+ * the T2 recheck — and conditioned the write on that self-fetched value, which asks whether the
51
+ * state changed since a read a microsecond earlier. Measured on 12.0.0: a writer landing in that
52
+ * window had its state adopted as the legitimate starting point and the CAS still returned
53
+ * committed with enforced:true, clobbering it. 13.0.0 threads the T1 token, and adds three FS
54
+ * defences that were measured ABSENT rather than assumed: `..` refused before resolution, a
55
+ * symlinked final component refused (stat/readFile measured the TARGET while rename replaced the
56
+ * LINK — check and write named different objects), the token read through one file descriptor,
57
+ * and dev+ino recorded in the evidence. COST MEASURED AS ZERO, and measured more carefully than
58
+ * the last two raises: the CLI imports only deployGate, asVerifiedDeployReceiptView,
59
+ * DEPLOY_RECEIPT_VIEW_SPEC and matchGlob, whose implementing modules (deploy-gate.js,
60
+ * resolver-glob.js, deploy-receipt-token.js, merge-gate.js) are BYTE-IDENTICAL 12.0.0 -> 13.0.0.
61
+ * Note the honest qualifier the earlier raises did not state: requiring the barrel index.js LOADS
62
+ * every changed module, so the changed code is in the process — it is simply not on the code path
63
+ * of any symbol the CLI calls, and the four symbols return identical values under both versions.
64
+ * Suite staged against 13.0.0 returned an identical 539/518/21 with a byte-identical 28-item
65
+ * failure set. The floor rises anyway, because a semantics floor is about what the CLI may RUN
66
+ * against: a CAS that reports committed after adopting another writer's state is exactly the
67
+ * class this gate exists to refuse.)
48
68
  */
49
69
 
50
70
  const fs = require('fs');
51
71
  const path = require('path');
52
72
 
53
73
  /** Minimum acceptable major for @coderifts/agent-guard in THIS package (the CLI). */
54
- const MIN_GUARD_MAJOR = 12;
74
+ const MIN_GUARD_MAJOR = 13;
55
75
  const DEP = '@coderifts/agent-guard';
56
76
 
57
77
  /**