border-customs 0.8.4 → 0.8.5

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 CHANGED
@@ -40,6 +40,10 @@ source checkout. Every check and push record carries `engineBuild` — the ident
40
40
  `border --version` prints. A `.dirty` suffix means the verdict came from uncommitted code, and
41
41
  records from one repo carrying different `engineBuild` values mean the audit trail is mixed:
42
42
  both are findings an auditor should chase, which is exactly why the identity is on the record face.
43
+ Freshness telemetry (the `global-resolution` WARN rung, drift-watch rc 3, and heartbeat lines) is
44
+ read-only information. Pinned consumption governs the install and execution surfaces: running an
45
+ older, deliberately chosen version is a legitimate steady state, not a defect. The decision to
46
+ update always stays with the human operator. (Owner-approved clarification, 2026-11-06.)
43
47
 
44
48
  ## The problem it solves
45
49
 
@@ -59,7 +63,11 @@ pushed" includes the six things people forget:
59
63
 
60
64
  - **Untracked and dirty files are inputs too.** History scanning alone misses the `.env`
61
65
  that was never committed but will ride along in a `git add -A`. border fingerprints the
62
- full `git status --porcelain` digest and scans the working tree directly.
66
+ full `git status --porcelain` digest and scans the working tree directly. One carve-out is
67
+ provably unpushable and demoted to INFO (never silently dropped): paths that are
68
+ `.gitignore`'d **and** untracked — `git add` honours `.gitignore`, so they can only reach a
69
+ commit through a deliberate `git add -f`, which moves them into the tracked surface where
70
+ the full gate applies. Blocking on them would train operators to mute the gate.
63
71
  - **Deleted-but-pushed blobs.** A secret removed in a later commit still ships in the
64
72
  history of the ref being pushed. The history leg scans everything the push would
65
73
  transmit, not just HEAD.
@@ -129,9 +137,13 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
129
137
  (`.omo/**`, `**/transcripts/**`, `*.session.jsonl`, `opencode.json(c)`, `.opencode/**`,
130
138
  env files except `.env.example`/`.env.sample`, junk like `probe*`/`*.rej`/
131
139
  `node_modules/**`, plus your `rules.pathPatterns` through the same compiler). Identity
132
- allow-listing over commit authors, merge committers, and annotated-tag taggers, checked
133
- against the history of every ref *and* against the object set a remote does not already
134
- have (the transmit set). Oversized-file, checked-in-binary, and notebook-output rules
140
+ allow-listing over commit authors, merge committers, and annotated-tag taggers, split by
141
+ *exposure class* against each configured remote's **live** `git ls-remote` (never a stale
142
+ local tracking ref): objects leaving the machine for the first time are CRITICAL
143
+ `identity-not-allowlisted`; offenders the remote already carries everywhere are recorded as
144
+ INFO `identity-already-public` without remediation — border never advises rewriting
145
+ published history. Offenders plus an unreachable remote exit 2 (the exposure class is
146
+ never guessed); no configured remote means first-push semantics (all fresh). Oversized-file, checked-in-binary, and notebook-output rules
135
147
  round it out.
136
148
  7. **Landing-grade tip lens.** The exact machinery `border push` uses to verify a landing
137
149
  (`scanTipTree`: the tip tree materialized from git objects into a throwaway directory,
@@ -811,7 +823,7 @@ plugin entry ships as `exports["./server"]`):
811
823
  ```jsonc
812
824
  {
813
825
  // pin the exact version — an @latest channel hits the registry on every cold start
814
- "plugin": ["border-customs@0.8.4"]
826
+ "plugin": ["border-customs@0.8.5"]
815
827
  }
816
828
  ```
817
829
 
@@ -849,7 +861,12 @@ argv-only (no shell; 300 s cap, 64 KiB per stream) restricted to the closed comm
849
861
  `check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`, `roundtrip`, `exfil`, `--help`, and
850
862
  renders the exit-code contract unchanged: 0 pass, 1 gate-blocked or partial push, 2 the gate
851
863
  could not answer — which since 0.5.1 also covers "no border CLI found or verified": the tool
852
- answers exit 2 cannot-answer and spawns nothing else. The tool REFUSES `--yes` — a real push
864
+ answers exit 2 cannot-answer and spawns nothing else. Every result carries a
865
+ `face: <resolved candidate> version=<--version stamp>` line: the identity handshake proves *which
866
+ binary*, the stamp line makes *which version* observable on every call (a PATH-fallback to an older
867
+ published CLI is loud, never silent). `BORDER_REQUIRE_PKG_VERSION=1` escalates that to a gate: the
868
+ resolved CLI must report the plugin package's own version or the call is refused as cannot-answer
869
+ with nothing spawned. The tool REFUSES `--yes` — a real push
853
870
  is the human gate, terminal-only, and a bare `border push` from the session is DRY-RUN by the
854
871
  CLI's own contract anyway. `/border` self-registers via the plugin `config` hook with `??=`, so
855
872
  a user file command of the same name — including a Route-A `commands/border.md` — stays
@@ -864,16 +881,24 @@ fails); plugin load failures are silent, so judge by the HTTP responses, never b
864
881
  code.
865
882
 
866
883
  `border opencode inspect [--json]` audits this machine's **V1 load surface** as one gate-owned
867
- command: six read-only aspects — cache self-consistency (package.json vs plugin marker line),
884
+ command: seven read-only aspects — cache self-consistency (package.json vs plugin marker line),
868
885
  spec freshness (exact pin offline; dist-tag against the registry), the 0.5.1 identity handshake
869
886
  replayed against the CLI resolver's candidate order, Route A byte parity, Route A∧B dual-registration
870
- collision, and the scope declaration — aggregated fail-closed: any FAIL exits 1, else any CANNOT
871
- exits 2, all PASS exits 0. The certificate boundary is printed, not implied: `--json` carries
887
+ collision, the scope declaration, and global-resolution (which CLI build answers on which face —
888
+ `BORDER_BIN`, packaged dist, PATH — and whether the active face lags the registry's latest) —
889
+ aggregated fail-closed: any FAIL exits 1, else any CANNOT exits 2, else 0. The seventh aspect's lag
890
+ rung is **WARN — informational**: lag under pinned consumption is a legitimate steady state, so WARN
891
+ never moves the aggregate or any purge trigger; registry-unreachable for this aspect stays named.
892
+ `BORDER_INSPECT_OFFLINE=1` skips the freshness leg explicitly on air-gapped machines — the aspect
893
+ then reports WARN "skipped", never fresh (an unevaluated face is never reported as fresh).
894
+ The certificate boundary is printed, not implied: `--json` carries
872
895
  `scope:"v1"`, and V2 load surfaces (project-local `.opencode/plugins/`, npm-prefix layouts, lazy
873
896
  activation) are a DECLARED BLIND SPOT pending V2 stabilization — a PASS is never evidence that
874
897
  "the whole machine is clean". The tool never writes: `--purge` lives in the drift-watch cron shell,
875
- which is now a thin consumer of `inspect --json` with the rc contract unchanged
876
- (FRESH 0 / DRIFT 1 / NOT-CACHED 0 / CANNOT 2).
898
+ which is now a thin consumer of `inspect --json` with rc contract FRESH 0 / DRIFT 1 / NOT-CACHED 0 /
899
+ CANNOT 2 / **GLOBAL-WARN or GLOBAL-UNREACHABLE 3** (warn-only by construction: the cron purge guard
900
+ acts on rc 1 only, and rc 3 can never reach it), plus an optional `--heartbeat <path>` that appends
901
+ one `ISO / verdict / rc / bin` line per run.
877
902
 
878
903
  ## What this is not
879
904
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "border-customs",
9
- "version": "0.8.4",
9
+ "version": "0.8.5",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@secretlint/core": "13.0.5",
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
44
44
  var define_BORDER_BUILD_default;
45
45
  var init_define_BORDER_BUILD = __esm({
46
46
  "<define:__BORDER_BUILD__>"() {
47
- define_BORDER_BUILD_default = { version: "0.8.4", commit: "bc8bb704aa17da327ab8c710e458f6ae4b2f368c", dirty: false, builtAt: "2026-11-04T15:24:46.596Z" };
47
+ define_BORDER_BUILD_default = { version: "0.8.5", commit: "56c5aa3959e18fa143e49904608f4564798ff0a6", dirty: false, builtAt: "2026-11-08T08:34:50.133Z" };
48
48
  }
49
49
  });
50
50
 
@@ -30368,8 +30368,8 @@ function sandboxBindingJoin(images) {
30368
30368
  return [...new Set(images)].sort().join("\n");
30369
30369
  }
30370
30370
  function sandboxRun(o) {
30371
- const probe = o.laneProbes.map((p) => `{ printf '%s ' '${p}'; ${p} --version 2>&1 | head -n 1; }`).join("\n");
30372
- const script = `${probe} > /audit/out/toolchain.txt
30371
+ const probe2 = o.laneProbes.map((p) => `{ printf '%s ' '${p}'; ${p} --version 2>&1 | head -n 1; }`).join("\n");
30372
+ const script = `${probe2} > /audit/out/toolchain.txt
30373
30373
  ${o.laneScript}`;
30374
30374
  const r = runSandboxedBuild({
30375
30375
  spec: o.spec,
@@ -37531,8 +37531,8 @@ function pypiArtifactCoherenceFindings(o) {
37531
37531
  for (const initRel of o.files.filter((f) => /(?:^|\/)__init__\.py$/.test(f)).sort()) {
37532
37532
  const body = o.read(initRel);
37533
37533
  if (body === null) continue;
37534
- const probe = probeDunderVersion(body);
37535
- if (probe !== null) sources.push({ label: `__version__ literal in ${initRel}`, path: initRel, probe });
37534
+ const probe2 = probeDunderVersion(body);
37535
+ if (probe2 !== null) sources.push({ label: `__version__ literal in ${initRel}`, path: initRel, probe: probe2 });
37536
37536
  }
37537
37537
  const hits = [];
37538
37538
  analyze(sources, `${o.archiveName}!`, hits);
@@ -37978,6 +37978,29 @@ init_define_BORDER_BUILD();
37978
37978
  import { spawnSync as spawnSync6 } from "node:child_process";
37979
37979
  import { dirname as dirname6, resolve as resolve6 } from "node:path";
37980
37980
 
37981
+ // src/lsremote.ts
37982
+ init_define_BORDER_BUILD();
37983
+ var PushStateError = class extends Error {
37984
+ exitCode = 2;
37985
+ name = "PushStateError";
37986
+ };
37987
+ var SHA_SHAPE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
37988
+ function parseLsRemote(text, targetId) {
37989
+ const out = /* @__PURE__ */ new Map();
37990
+ for (const line of text.split("\n")) {
37991
+ if (line.trim() === "") continue;
37992
+ const fields = line.split(" ");
37993
+ const sha = fields[0];
37994
+ const ref = fields[1];
37995
+ const shapeOk = fields.length === 2 && sha !== void 0 && SHA_SHAPE.test(sha) && (ref === "HEAD" || ref !== void 0 && ref.startsWith("refs/"));
37996
+ if (!shapeOk) {
37997
+ throw new PushStateError(`target '${targetId}': malformed 'git ls-remote' output line '${line.slice(0, 72)}' \u2014 refusing to interpret (fail closed)`);
37998
+ }
37999
+ if (ref !== "HEAD") out.set(ref, sha);
38000
+ }
38001
+ return out;
38002
+ }
38003
+
37981
38004
  // src/rules/allowlist.ts
37982
38005
  init_define_BORDER_BUILD();
37983
38006
  var BOT_EMAIL_PATTERN = "*[bot]@users.noreply.github.com";
@@ -38055,7 +38078,6 @@ function runGit2(repoDir, args) {
38055
38078
  }
38056
38079
  var LOG_FIELDS = 6;
38057
38080
  var LOG_FORMAT = "--format=%H%x1f%an%x1f%ae%x1f%cn%x1f%ce%x1f%P";
38058
- var REV_BATCH = 200;
38059
38081
  function collectCommits(s, revs) {
38060
38082
  const out = runGit2(s.repoDir, ["log", LOG_FORMAT, ...revs]);
38061
38083
  for (const line of out.split("\n")) {
@@ -38094,30 +38116,66 @@ function revExists(repoDir, rev) {
38094
38116
  });
38095
38117
  return r.status === 0;
38096
38118
  }
38097
- function trackingEndpoint(repoDir, remoteName, ref) {
38098
- const candidates = [`refs/remotes/${remoteName}/${ref}`];
38099
- const short = ref.replace(/^refs\/(?:heads|tags)\//, "");
38100
- if (short !== ref) candidates.push(`refs/remotes/${remoteName}/${short}`);
38101
- return candidates.find((c) => revExists(repoDir, c)) ?? null;
38119
+ function revParse(repoDir, rev) {
38120
+ return runGit2(repoDir, ["rev-parse", rev]).trim();
38102
38121
  }
38103
- function scanTransmitted(s, refSet, remotes) {
38104
- const names = remotes.map((r) => r.name).filter((n) => n !== void 0);
38105
- if (names.length === 0) return;
38106
- for (const ref of refSet) {
38107
- if (!revExists(s.repoDir, ref)) continue;
38108
- for (const name of names) {
38109
- const endpoint = trackingEndpoint(s.repoDir, name, ref);
38110
- if (endpoint === null) continue;
38111
- const out = runGit2(s.repoDir, ["rev-list", `${endpoint}..${ref}`]);
38112
- const fresh = [];
38113
- for (const sha of out.split("\n")) {
38114
- if (sha !== "" && !s.seen.has(sha)) fresh.push(sha);
38122
+ var LS_REMOTE_TIMEOUT_MS = 3e4;
38123
+ function urlForMessage(url2) {
38124
+ try {
38125
+ const u = new URL(url2);
38126
+ if (u.protocol === "file:") return url2;
38127
+ return `${u.protocol}//${u.host}/***`;
38128
+ } catch {
38129
+ return "***";
38130
+ }
38131
+ }
38132
+ function lsRemoteFetch(repoDir, url2, env2) {
38133
+ const r = spawnSync6("git", ["ls-remote", url2], {
38134
+ cwd: repoDir,
38135
+ encoding: "utf8",
38136
+ env: { ...process.env, ...env2, GIT_CEILING_DIRECTORIES: dirname6(resolve6(repoDir)) },
38137
+ timeout: LS_REMOTE_TIMEOUT_MS
38138
+ });
38139
+ if (r.error !== void 0 || r.status !== 0) {
38140
+ const why = r.error !== void 0 ? r.error.message : (r.stderr ?? "").trim().slice(-200);
38141
+ throw new Error(
38142
+ `identity: remote ${urlForMessage(url2)} is unreachable via git ls-remote (${why}) \u2014 the exposure class of the offending identities cannot be decided and border never guesses it; restore connectivity or scope the check and retry`
38143
+ );
38144
+ }
38145
+ return parseLsRemote(r.stdout ?? "", urlForMessage(url2));
38146
+ }
38147
+ function remoteTruth(o, walkShas) {
38148
+ const truth = { published: /* @__PURE__ */ new Set(), publishedTags: /* @__PURE__ */ new Set(), remoteLabels: [] };
38149
+ const resolvable = o.refSet.filter((ref) => revExists(o.repoDir, ref));
38150
+ const urls = [...new Set(o.cfg.targets.git.remotes.map((r) => r.url).filter((u) => u !== void 0 && u.length > 0))];
38151
+ if (resolvable.length === 0 || urls.length === 0) return truth;
38152
+ const env2 = o.env ?? process.env;
38153
+ const fetch2 = o.lsRemote ?? ((url2) => lsRemoteFetch(o.repoDir, url2, env2));
38154
+ for (const url2 of urls) {
38155
+ const map2 = fetch2(url2);
38156
+ truth.remoteLabels.push(urlForMessage(url2));
38157
+ for (const ref of resolvable) {
38158
+ const ep = map2.get(ref) ?? map2.get(`${ref}^{}`);
38159
+ if (ep === void 0) continue;
38160
+ if (!/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(ep)) {
38161
+ throw new Error(`identity: remote ${urlForMessage(url2)} reports a malformed endpoint sha for ${ref} \u2014 refusing to interpret (fail closed)`);
38162
+ }
38163
+ if (ref.startsWith("refs/tags/")) {
38164
+ const localObj = revParse(o.repoDir, ref);
38165
+ const localPeel = revParse(o.repoDir, `${ref}^{}`);
38166
+ if (map2.get(ref) === localObj || map2.get(`${ref}^{}`) === localPeel) truth.publishedTags.add(ref);
38167
+ continue;
38115
38168
  }
38116
- for (let i = 0; i < fresh.length; i += REV_BATCH) {
38117
- collectCommits(s, ["--no-walk", ...fresh.slice(i, i + REV_BATCH)]);
38169
+ if (!revExists(o.repoDir, ep)) {
38170
+ throw new Error(
38171
+ `identity: remote ${urlForMessage(url2)} points ${ref} at ${ep.slice(0, 12)}, which is not an object in this repository \u2014 the transmitted set cannot be computed; run 'git fetch ${urlForMessage(url2)}' and retry (fail closed)`
38172
+ );
38118
38173
  }
38174
+ const fresh = new Set(runGit2(o.repoDir, ["rev-list", `${ep}..${ref}`]).split("\n").filter((s) => s !== ""));
38175
+ for (const sha of walkShas) if (!fresh.has(sha)) truth.published.add(sha);
38119
38176
  }
38120
38177
  }
38178
+ return truth;
38121
38179
  }
38122
38180
  function scanTags(repoDir, authors) {
38123
38181
  const entries = /* @__PURE__ */ new Map();
@@ -38155,35 +38213,61 @@ function mergeEntries(target, extra) {
38155
38213
  }
38156
38214
  }
38157
38215
  }
38216
+ var IDENTITY_PUBLIC_RULE = "identity-already-public";
38217
+ function locationsSuffix(objects) {
38218
+ const locations = objects.map(([obj, roles]) => `${roles} of ${obj}`);
38219
+ const shownList = locations.slice(0, MAX_LISTED_LOCATIONS);
38220
+ const more = locations.length - shownList.length;
38221
+ return { shown: shownList.join("; "), suffix: more > 0 ? ` (+${String(more)} more)` : "" };
38222
+ }
38158
38223
  function scanIdentity(o) {
38159
38224
  const authors = o.cfg.rules.authors;
38160
38225
  const s = { repoDir: o.repoDir, authors, entries: /* @__PURE__ */ new Map(), seen: /* @__PURE__ */ new Set() };
38161
- if (o.refSet.length > 0) {
38162
- collectCommits(s, o.refSet);
38163
- scanTransmitted(s, o.refSet, o.cfg.targets.git.remotes);
38164
- }
38226
+ if (o.refSet.length > 0) collectCommits(s, o.refSet);
38165
38227
  mergeEntries(s.entries, scanTags(o.repoDir, authors));
38228
+ const truth = s.entries.size > 0 ? remoteTruth(o, s.seen) : { published: /* @__PURE__ */ new Set(), publishedTags: /* @__PURE__ */ new Set(), remoteLabels: [] };
38166
38229
  const findings = [];
38167
38230
  for (const entry of s.entries.values()) {
38168
- const locations = [...entry.objects].map(([obj, roles]) => `${roles} of ${obj}`);
38169
- const shown = locations.slice(0, MAX_LISTED_LOCATIONS);
38170
- const more = locations.length - shown.length;
38171
- const suffix = more > 0 ? ` (+${String(more)} more)` : "";
38231
+ const freshObjects = [];
38232
+ const publicObjects = [];
38233
+ for (const [obj, roles] of entry.objects) {
38234
+ const isCommit = /^[0-9a-f]{40}$/.test(obj);
38235
+ const published = isCommit ? truth.published.has(obj) : truth.publishedTags.has(obj);
38236
+ (published ? publicObjects : freshObjects).push([obj, roles]);
38237
+ }
38172
38238
  const identity = `${entry.name} <${entry.email}>`;
38173
- const firstCommit = [...entry.objects.entries()].find(([obj]) => /^[0-9a-f]{40}$/.test(obj));
38174
38239
  const { valueDigest, snippet } = redact(identity);
38175
- const raw = `'${identity}' is not allowlisted in rules.authors \u2014 push would expose this git identity forever: ${shown.join("; ")}${suffix}. Remediation: use git filter-repo manually (out of scope for border; border never rewrites history).`;
38176
- const finding = {
38177
- rule: IDENTITY_RULE,
38178
- severity: "CRITICAL",
38179
- target: IDENTITY_TARGET,
38180
- engine: IDENTITY_ENGINE,
38181
- message: o.sanitizer?.sanitize(raw) ?? raw,
38182
- valueDigest,
38183
- snippet
38184
- };
38185
- if (firstCommit !== void 0) finding.commit = firstCommit[0];
38186
- findings.push(finding);
38240
+ const firstCommit = [...freshObjects, ...publicObjects].find(([obj]) => /^[0-9a-f]{40}$/.test(obj));
38241
+ if (freshObjects.length > 0) {
38242
+ const { shown, suffix } = locationsSuffix(freshObjects);
38243
+ const raw = `'${identity}' is not allowlisted in rules.authors \u2014 push would expose this git identity forever: ${shown}${suffix}. Remediation: use git filter-repo manually (out of scope for border; border never rewrites history).`;
38244
+ const finding = {
38245
+ rule: IDENTITY_RULE,
38246
+ severity: "CRITICAL",
38247
+ target: IDENTITY_TARGET,
38248
+ engine: IDENTITY_ENGINE,
38249
+ message: o.sanitizer?.sanitize(raw) ?? raw,
38250
+ valueDigest,
38251
+ snippet
38252
+ };
38253
+ if (firstCommit !== void 0) finding.commit = firstCommit[0];
38254
+ findings.push(finding);
38255
+ }
38256
+ if (publicObjects.length > 0) {
38257
+ const { shown, suffix } = locationsSuffix(publicObjects);
38258
+ const raw = `'${identity}' is not allowlisted in rules.authors \u2014 but every object carrying it is already public on the configured remote(s) (${truth.remoteLabels.join(", ") || "remotes probed"}): ${shown}${suffix}. Nothing new leaves this machine with this push; recorded as fact for housekeeping (add it to rules.authors, or keep it out of new commits). border detects, never erases \u2014 rewriting published history is not offered.`;
38259
+ const publishedFinding = {
38260
+ rule: IDENTITY_PUBLIC_RULE,
38261
+ severity: "INFO",
38262
+ target: IDENTITY_TARGET,
38263
+ engine: IDENTITY_ENGINE,
38264
+ message: o.sanitizer?.sanitize(raw) ?? raw,
38265
+ valueDigest: redact(`${identity}:already-public`).valueDigest,
38266
+ snippet
38267
+ };
38268
+ if (firstCommit !== void 0) publishedFinding.commit = firstCommit[0];
38269
+ findings.push(publishedFinding);
38270
+ }
38187
38271
  }
38188
38272
  return findings;
38189
38273
  }
@@ -39404,12 +39488,12 @@ function pruneRunDirs(repoDir, key8, keep = KEEP_RUN_DIRS_PER_KEY8) {
39404
39488
  async function computeFingerprint(repoDir, cfg, configDigest, effectiveTargets, o = {}) {
39405
39489
  const envOpt = o.env !== void 0 ? { env: o.env } : {};
39406
39490
  const ctx = await gatherContext(repoDir, envOpt);
39407
- const probe = await probeEngines(cfg, {
39491
+ const probe2 = await probeEngines(cfg, {
39408
39492
  ...envOpt,
39409
39493
  ...o.requireOverride !== void 0 ? { requireOverride: o.requireOverride } : {}
39410
39494
  });
39411
39495
  const rulesHash = await computeCheckRulesHash({
39412
- engineVersions: probe.engineVersions,
39496
+ engineVersions: probe2.engineVersions,
39413
39497
  configDigest,
39414
39498
  ...envOpt
39415
39499
  });
@@ -39425,7 +39509,7 @@ async function computeFingerprint(repoDir, cfg, configDigest, effectiveTargets,
39425
39509
  rulesHash,
39426
39510
  exposureSet: [...exposureSet(cfg, { cwd: repoDir })]
39427
39511
  };
39428
- return { ctx, fp, probeDegraded: probe.degraded };
39512
+ return { ctx, fp, probeDegraded: probe2.degraded };
39429
39513
  }
39430
39514
  async function consultSkipLedger(q) {
39431
39515
  let ctx;
@@ -40156,26 +40240,6 @@ function gitTargetId(remote, index) {
40156
40240
  }
40157
40241
 
40158
40242
  // src/pushstate.ts
40159
- var PushStateError = class extends Error {
40160
- exitCode = 2;
40161
- name = "PushStateError";
40162
- };
40163
- var SHA_SHAPE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
40164
- function parseLsRemote(text, targetId) {
40165
- const out = /* @__PURE__ */ new Map();
40166
- for (const line of text.split("\n")) {
40167
- if (line.trim() === "") continue;
40168
- const fields = line.split(" ");
40169
- const sha = fields[0];
40170
- const ref = fields[1];
40171
- const shapeOk = fields.length === 2 && sha !== void 0 && SHA_SHAPE.test(sha) && (ref === "HEAD" || ref !== void 0 && ref.startsWith("refs/"));
40172
- if (!shapeOk) {
40173
- throw new PushStateError(`target '${targetId}': malformed 'git ls-remote' output line '${line.slice(0, 72)}' \u2014 refusing to interpret (fail closed)`);
40174
- }
40175
- if (ref !== "HEAD") out.set(ref, sha);
40176
- }
40177
- return out;
40178
- }
40179
40243
  function localRefCommits(repoDir, refSet, env2) {
40180
40244
  const commits = /* @__PURE__ */ new Map();
40181
40245
  if (refSet.length === 0) return commits;
@@ -42332,6 +42396,46 @@ function resolveBuildConsent(o) {
42332
42396
  return { allowed: false, reason: WITHHELD_REASON };
42333
42397
  }
42334
42398
 
42399
+ // src/check/ignoredSurface.ts
42400
+ init_define_BORDER_BUILD();
42401
+ var IGNORED_SURFACE_NOTE = " \u2014 path is .gitignore'd and untracked: git add honours .gitignore, so it cannot reach a push; informational surface, never blocking";
42402
+ function nulList(stdout) {
42403
+ return new Set(stdout.split("\0").filter((p) => p !== ""));
42404
+ }
42405
+ function ignoredHit(rel, ignored) {
42406
+ if (ignored.has(rel)) return true;
42407
+ let cut = rel.indexOf("/");
42408
+ while (cut > 0) {
42409
+ if (ignored.has(`${rel.slice(0, cut)}/`)) return true;
42410
+ cut = rel.indexOf("/", cut + 1);
42411
+ }
42412
+ return false;
42413
+ }
42414
+ function relFacetPath(f, repoDir) {
42415
+ if (f.path === void 0 || f.commit !== void 0) return null;
42416
+ if (f.target !== "git" && f.target !== "tree") return null;
42417
+ const outer = f.path.split("!")[0] ?? f.path;
42418
+ const prefix = `${repoDir}/`;
42419
+ return outer.startsWith(prefix) ? outer.slice(prefix.length) : outer;
42420
+ }
42421
+ function demoteIgnoredUntracked(findings, repoDir, o = {}) {
42422
+ const candidates = findings.filter((f) => f.severity !== "INFO" && relFacetPath(f, repoDir) !== null);
42423
+ if (candidates.length === 0) return 0;
42424
+ const tracked = nulList(runGitChecked(repoDir, ["ls-files", "-z"], o));
42425
+ const ignored = nulList(runGitChecked(repoDir, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"], o));
42426
+ if (ignored.size === 0) return 0;
42427
+ let demoted = 0;
42428
+ for (const f of findings) {
42429
+ if (f.severity === "INFO") continue;
42430
+ const rel = relFacetPath(f, repoDir);
42431
+ if (rel === null || !ignoredHit(rel, ignored) || tracked.has(rel)) continue;
42432
+ f.severity = "INFO";
42433
+ f.message = `${f.message}${IGNORED_SURFACE_NOTE}`;
42434
+ demoted += 1;
42435
+ }
42436
+ return demoted;
42437
+ }
42438
+
42335
42439
  // src/check/exfilTreeScan.ts
42336
42440
  init_define_BORDER_BUILD();
42337
42441
  import { readFileSync as readFileSync17, statSync as statSync2 } from "node:fs";
@@ -42583,7 +42687,7 @@ function revExists2(run2, rev) {
42583
42687
  return false;
42584
42688
  }
42585
42689
  }
42586
- function trackingEndpoint2(run2, remoteName, ref) {
42690
+ function trackingEndpoint(run2, remoteName, ref) {
42587
42691
  const candidates = [`refs/remotes/${remoteName}/${ref}`];
42588
42692
  const short = ref.replace(/^refs\/(?:heads|tags)\//, "");
42589
42693
  if (short !== ref) candidates.push(`refs/remotes/${remoteName}/${short}`);
@@ -42606,7 +42710,7 @@ function willPublishShas(run2, refSet, remotes) {
42606
42710
  for (const remote of remotes) {
42607
42711
  const name = remote.name;
42608
42712
  if (name === void 0) continue;
42609
- const endpoint = trackingEndpoint2(run2, name, ref);
42713
+ const endpoint = trackingEndpoint(run2, name, ref);
42610
42714
  if (endpoint !== null) ranges.push(`${endpoint}..${ref}`);
42611
42715
  }
42612
42716
  if (ranges.length === 0) collect([ref]);
@@ -43136,13 +43240,13 @@ async function runPipeline(o, ctx, lockWarning) {
43136
43240
  const envOpt = o.env !== void 0 ? { env: o.env } : {};
43137
43241
  const findings = [];
43138
43242
  const guard = trackedBorderStateFinding(repoDir, ctx.headSha, o.env);
43139
- const probe = await probeEngines(o.cfg, {
43243
+ const probe2 = await probeEngines(o.cfg, {
43140
43244
  ...envOpt,
43141
43245
  ...o.requireOverride !== void 0 ? { requireOverride: o.requireOverride } : {}
43142
43246
  });
43143
- findings.push(...probe.findings);
43247
+ findings.push(...probe2.findings);
43144
43248
  if (guard !== null) findings.push(guard);
43145
- const broken = new Set(probe.findings.map((f) => f.engine));
43249
+ const broken = new Set(probe2.findings.map((f) => f.engine));
43146
43250
  const sanitizer = new TextSanitizer();
43147
43251
  findings.push(...filterBorderStateFindings(detectHostileConfig({ repoDir, target: "git", ...envOpt }), repoDir));
43148
43252
  if (!broken.has("gitleaks")) {
@@ -43187,6 +43291,7 @@ async function runPipeline(o, ctx, lockWarning) {
43187
43291
  findings.push(f);
43188
43292
  }
43189
43293
  }
43294
+ demoteIgnoredUntracked(findings, repoDir, o.env !== void 0 ? { env: o.env } : {});
43190
43295
  const ledgerArtifacts = [];
43191
43296
  const buildToolchains = [];
43192
43297
  for (const channel of publishChannels()) {
@@ -43235,7 +43340,7 @@ async function runPipeline(o, ctx, lockWarning) {
43235
43340
  }
43236
43341
  }
43237
43342
  const rulesHash = await computeCheckRulesHash({
43238
- engineVersions: probe.engineVersions,
43343
+ engineVersions: probe2.engineVersions,
43239
43344
  configDigest: o.configDigest,
43240
43345
  ...o.env !== void 0 ? { env: o.env } : {}
43241
43346
  });
@@ -43295,7 +43400,7 @@ async function runPipeline(o, ctx, lockWarning) {
43295
43400
  return {
43296
43401
  report,
43297
43402
  ctx,
43298
- degraded: probe.degraded,
43403
+ degraded: probe2.degraded,
43299
43404
  sanitizedSummary: sanitizeSummary(report, sanitizer),
43300
43405
  lockWarning,
43301
43406
  artifacts: ledgerArtifacts.length > 0 ? ledgerArtifacts : null,
@@ -43326,12 +43431,12 @@ async function runCheck(ctx) {
43326
43431
  effective = { kind: "loaded", config: load.explicit.config, warnings: [], source: load.explicit.source, security: load.explicit.security };
43327
43432
  } else {
43328
43433
  if (ctx.flags.requireEngine !== void 0 && ctx.flags.requireEngine.length > 0) {
43329
- const probe = await probeEngines(NO_TARGETS_PROBE_CFG, {
43434
+ const probe2 = await probeEngines(NO_TARGETS_PROBE_CFG, {
43330
43435
  requireOverride: ctx.flags.requireEngine,
43331
43436
  ...env2 !== void 0 ? { env: env2 } : {}
43332
43437
  });
43333
- if (probe.degraded) {
43334
- for (const finding of probe.findings) ctx.stderr(`border: ${finding.message}`);
43438
+ if (probe2.degraded) {
43439
+ for (const finding of probe2.findings) ctx.stderr(`border: ${finding.message}`);
43335
43440
  return EXIT_ERROR;
43336
43441
  }
43337
43442
  }
@@ -43849,13 +43954,13 @@ async function runLlmRequest(ctx) {
43849
43954
 
43850
43955
  // src/commands/opencode.ts
43851
43956
  init_define_BORDER_BUILD();
43852
- import { existsSync as existsSync23, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync23, rmSync as rmSync14, writeFileSync as writeFileSync8 } from "node:fs";
43957
+ import { existsSync as existsSync24, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync23, rmSync as rmSync14, writeFileSync as writeFileSync8 } from "node:fs";
43853
43958
  import { dirname as dirname17, join as join36 } from "node:path";
43854
43959
  import { fileURLToPath as fileURLToPath7 } from "node:url";
43855
43960
 
43856
43961
  // src/opencode/inspect.ts
43857
43962
  init_define_BORDER_BUILD();
43858
- import { existsSync as existsSync22, readFileSync as readFileSync22 } from "node:fs";
43963
+ import { existsSync as existsSync23, readFileSync as readFileSync22 } from "node:fs";
43859
43964
  import { join as join35 } from "node:path";
43860
43965
 
43861
43966
  // src/opencode/aspects/cacheVersion.ts
@@ -44105,6 +44210,10 @@ function verifyDualRoute(facts) {
44105
44210
  return { id, verdict: "PASS", detail: "neither route registered \u2014 no border plugin load claimed on the V1 surface" };
44106
44211
  }
44107
44212
 
44213
+ // src/opencode/aspects/globalResolution.ts
44214
+ init_define_BORDER_BUILD();
44215
+ import { existsSync as existsSync22 } from "node:fs";
44216
+
44108
44217
  // src/opencode/aspects/identity.ts
44109
44218
  init_define_BORDER_BUILD();
44110
44219
  import { execFile } from "node:child_process";
@@ -44161,11 +44270,11 @@ async function verifyIdentity(deps) {
44161
44270
  const reasons = [];
44162
44271
  let anySpawned = false;
44163
44272
  for (const candidate of candidates) {
44164
- const probe = await runner(candidate, HANDSHAKE_ARGV);
44165
- if (passesHandshake(probe)) {
44273
+ const probe2 = await runner(candidate, HANDSHAKE_ARGV);
44274
+ if (passesHandshake(probe2)) {
44166
44275
  return { id, verdict: "PASS", detail: `${candidate.label} passed the identity handshake` };
44167
44276
  }
44168
- if (probe.status !== null) {
44277
+ if (probe2.status !== null) {
44169
44278
  anySpawned = true;
44170
44279
  reasons.push(`${candidate.label}: spawned but --help did not answer as border (impostor?)`);
44171
44280
  } else {
@@ -44178,51 +44287,6 @@ async function verifyIdentity(deps) {
44178
44287
  return { id, verdict: "CANNOT", detail: `no border binary reachable \u2014 tried: ${reasons.join("; ")}` };
44179
44288
  }
44180
44289
 
44181
- // src/opencode/aspects/routeA.ts
44182
- init_define_BORDER_BUILD();
44183
- function verifyRouteAParity(facts) {
44184
- const id = "route-a-parity";
44185
- const { routeAPlugin, routeAInstalled } = facts;
44186
- if (!routeAInstalled) {
44187
- return { id, verdict: "PASS", detail: `${routeAPlugin}: absent \u2014 Route A not installed, nothing to be stale` };
44188
- }
44189
- if (facts.packagedPlugin === null) {
44190
- return { id, verdict: "CANNOT", detail: "the packaged plugin asset is unreadable \u2014 the border install itself looks broken, parity cannot be judged" };
44191
- }
44192
- const installed = facts.routeABytes;
44193
- if (installed === null) {
44194
- return { id, verdict: "CANNOT", detail: `${routeAPlugin}: present but unreadable \u2014 parity cannot be judged` };
44195
- }
44196
- const installedSha = sha256Hex3(installed);
44197
- const packagedSha = sha256Hex3(facts.packagedPlugin);
44198
- if (installedSha === packagedSha) {
44199
- return { id, verdict: "PASS", detail: `${routeAPlugin}: up-to-date installed=${installedSha} packaged=${packagedSha}` };
44200
- }
44201
- const ours = firstLine(installed).startsWith(PLUGIN_MARKER);
44202
- const shape = ours ? "outdated (marker ours, bytes differ)" : "foreign (first line lacks the marker)";
44203
- return { id, verdict: "FAIL", detail: `${routeAPlugin}: ${shape} installed=${installedSha} packaged=${packagedSha}` };
44204
- }
44205
-
44206
- // src/opencode/aspects/scope.ts
44207
- init_define_BORDER_BUILD();
44208
- var SCOPE_ID = "scope";
44209
- var SCOPE_NAME = "v1-load-surface";
44210
- var SCOPE_UNCOVERED = [
44211
- "V2 project-local .opencode/plugins/",
44212
- "V2 npm-prefix/node_modules installs",
44213
- "V2 lazy activation state"
44214
- ];
44215
- function verifyScope() {
44216
- return {
44217
- id: SCOPE_ID,
44218
- verdict: "PASS",
44219
- detail: `verdict scope = ${SCOPE_NAME}; NOT covered (declared blind, separate ledger item): ${SCOPE_UNCOVERED.join(", ")}`
44220
- };
44221
- }
44222
-
44223
- // src/opencode/aspects/specFreshness.ts
44224
- init_define_BORDER_BUILD();
44225
-
44226
44290
  // src/opencode/registry.ts
44227
44291
  init_define_BORDER_BUILD();
44228
44292
  var isRecord3 = (v) => typeof v === "object" && v !== null;
@@ -44276,7 +44340,110 @@ async function fetchRegistryVersion(fetcher, url2, timeoutMs) {
44276
44340
  return parsed["version"];
44277
44341
  }
44278
44342
 
44343
+ // src/opencode/aspects/globalResolution.ts
44344
+ var VERSION_ARGV = ["--version"];
44345
+ function parseVersionStamp(stdout) {
44346
+ const m = /^border\s+(\d[^\s]*)/m.exec(stdout);
44347
+ return m?.[1] ?? null;
44348
+ }
44349
+ async function probe(runner, target) {
44350
+ const out = await runner(target, VERSION_ARGV);
44351
+ if (out.status === null) return "unspawnable";
44352
+ return parseVersionStamp(out.stdout) ?? "no version stamp";
44353
+ }
44354
+ async function verifyGlobalResolution(deps) {
44355
+ const id = "global-resolution";
44356
+ if ((deps.env["BORDER_INSPECT_OFFLINE"] ?? "") === "1") {
44357
+ return { id, verdict: "WARN", detail: "skipped (BORDER_INSPECT_OFFLINE=1) \u2014 freshness not evaluated; an unevaluated face is never reported as fresh" };
44358
+ }
44359
+ const runner = deps.runner ?? defaultInspectRunner;
44360
+ const override = deps.env["BORDER_BIN"];
44361
+ const hasOverride = override !== void 0 && override.length > 0;
44362
+ const faces = [];
44363
+ let active = null;
44364
+ if (hasOverride) {
44365
+ const stamp = await probe(runner, { bin: override, prefix: [], label: `BORDER_BIN=${override}` });
44366
+ faces.push({ label: `env BORDER_BIN=${override}`, stamp });
44367
+ active = stamp;
44368
+ } else {
44369
+ if (existsSync22(deps.distEntry)) {
44370
+ const packaged = { bin: deps.distEntry, prefix: [], label: "packaged dist" };
44371
+ let stamp = await probe(runner, packaged);
44372
+ if (stamp === "unspawnable") {
44373
+ stamp = await probe(runner, { bin: "node", prefix: [deps.distEntry], label: "packaged dist" });
44374
+ }
44375
+ faces.push({ label: "packaged dist", stamp });
44376
+ }
44377
+ const pathStamp = await probe(runner, { bin: "border", prefix: [], label: "border on PATH" });
44378
+ faces.push({ label: "border on PATH", stamp: pathStamp });
44379
+ active = pathStamp === "unspawnable" ? null : pathStamp;
44380
+ }
44381
+ const faceLine = faces.map((f) => `${f.label}: ${f.stamp}`).join("; ");
44382
+ const fetcher = deps.fetcher ?? defaultRegistryFetcher;
44383
+ let latest;
44384
+ try {
44385
+ latest = await fetchRegistryVersion(fetcher, npmTagUrl(deps.pkg, "latest", deps.registryBase), deps.timeoutMs ?? REGISTRY_TIMEOUT_MS2);
44386
+ } catch (err) {
44387
+ const message = err instanceof Error ? err.message : String(err);
44388
+ return { id, verdict: "CANNOT", detail: `registry unreachable for global-resolution: ${message}; ${faceLine}` };
44389
+ }
44390
+ if (active === null) {
44391
+ return { id, verdict: "PASS", detail: `no active CLI face (no BORDER_BIN, no PATH border); registry latest ${latest}; ${faceLine}` };
44392
+ }
44393
+ const activeCore = active.split("+")[0];
44394
+ if (activeCore === latest) {
44395
+ return { id, verdict: "PASS", detail: `active face matches registry latest ${latest}; ${faceLine}` };
44396
+ }
44397
+ if (active === "unspawnable" || active === "no version stamp") {
44398
+ return { id, verdict: "WARN", detail: `active CLI face carries no version stamp (registry latest ${latest}) \u2014 informational: identity may be fine, age is unknown; ${faceLine}` };
44399
+ }
44400
+ return { id, verdict: "WARN", detail: `active CLI face is ${active}, registry latest is ${latest} \u2014 informational only: pinned consumption is a legitimate steady state, the decision to update stays with the human; ${faceLine}` };
44401
+ }
44402
+
44403
+ // src/opencode/aspects/routeA.ts
44404
+ init_define_BORDER_BUILD();
44405
+ function verifyRouteAParity(facts) {
44406
+ const id = "route-a-parity";
44407
+ const { routeAPlugin, routeAInstalled } = facts;
44408
+ if (!routeAInstalled) {
44409
+ return { id, verdict: "PASS", detail: `${routeAPlugin}: absent \u2014 Route A not installed, nothing to be stale` };
44410
+ }
44411
+ if (facts.packagedPlugin === null) {
44412
+ return { id, verdict: "CANNOT", detail: "the packaged plugin asset is unreadable \u2014 the border install itself looks broken, parity cannot be judged" };
44413
+ }
44414
+ const installed = facts.routeABytes;
44415
+ if (installed === null) {
44416
+ return { id, verdict: "CANNOT", detail: `${routeAPlugin}: present but unreadable \u2014 parity cannot be judged` };
44417
+ }
44418
+ const installedSha = sha256Hex3(installed);
44419
+ const packagedSha = sha256Hex3(facts.packagedPlugin);
44420
+ if (installedSha === packagedSha) {
44421
+ return { id, verdict: "PASS", detail: `${routeAPlugin}: up-to-date installed=${installedSha} packaged=${packagedSha}` };
44422
+ }
44423
+ const ours = firstLine(installed).startsWith(PLUGIN_MARKER);
44424
+ const shape = ours ? "outdated (marker ours, bytes differ)" : "foreign (first line lacks the marker)";
44425
+ return { id, verdict: "FAIL", detail: `${routeAPlugin}: ${shape} installed=${installedSha} packaged=${packagedSha}` };
44426
+ }
44427
+
44428
+ // src/opencode/aspects/scope.ts
44429
+ init_define_BORDER_BUILD();
44430
+ var SCOPE_ID = "scope";
44431
+ var SCOPE_NAME = "v1-load-surface";
44432
+ var SCOPE_UNCOVERED = [
44433
+ "V2 project-local .opencode/plugins/",
44434
+ "V2 npm-prefix/node_modules installs",
44435
+ "V2 lazy activation state"
44436
+ ];
44437
+ function verifyScope() {
44438
+ return {
44439
+ id: SCOPE_ID,
44440
+ verdict: "PASS",
44441
+ detail: `verdict scope = ${SCOPE_NAME}; NOT covered (declared blind, separate ledger item): ${SCOPE_UNCOVERED.join(", ")}`
44442
+ };
44443
+ }
44444
+
44279
44445
  // src/opencode/aspects/specFreshness.ts
44446
+ init_define_BORDER_BUILD();
44280
44447
  var isExactVersion2 = (spec) => /^\d+\.\d+\.\d+([-+].*)?$/.test(spec);
44281
44448
  async function verifySpecFreshness(facts, deps = {}) {
44282
44449
  const id = "spec-freshness";
@@ -44333,6 +44500,7 @@ function inspectExit(verdict) {
44333
44500
  case "CANNOT":
44334
44501
  return EXIT_ERROR;
44335
44502
  case "PASS":
44503
+ case "WARN":
44336
44504
  return EXIT_PASS;
44337
44505
  }
44338
44506
  }
@@ -44348,7 +44516,7 @@ function gatherFacts(env2, cwd, deps) {
44348
44516
  const pluginDrop = routeAPluginPath(env2);
44349
44517
  const cacheRoot = deps.cacheRoot ?? opencodeCacheRoot(env2);
44350
44518
  const globalPath = deps.globalConfigPath ?? join35(opencodeConfigRoot(env2), "opencode", "opencode.jsonc");
44351
- const routeAInstalled = existsSync22(pluginDrop);
44519
+ const routeAInstalled = existsSync23(pluginDrop);
44352
44520
  return {
44353
44521
  pkg,
44354
44522
  env: env2,
@@ -44363,6 +44531,7 @@ function gatherFacts(env2, cwd, deps) {
44363
44531
  }
44364
44532
  async function runInspectCore(ctx, deps = {}) {
44365
44533
  const facts = gatherFacts(ctx.env, ctx.cwd, deps);
44534
+ const distEntry = deps.distEntry ?? packagedDistEntry(import.meta.url);
44366
44535
  const aspects = [
44367
44536
  verifyCacheVersion(facts),
44368
44537
  await verifySpecFreshness(facts, {
@@ -44371,12 +44540,20 @@ async function runInspectCore(ctx, deps = {}) {
44371
44540
  }),
44372
44541
  await verifyIdentity({
44373
44542
  ...deps.runner !== void 0 ? { runner: deps.runner } : {},
44374
- distEntry: deps.distEntry ?? packagedDistEntry(import.meta.url),
44543
+ distEntry,
44375
44544
  env: facts.env
44376
44545
  }),
44377
44546
  verifyRouteAParity(facts),
44378
44547
  verifyDualRoute(facts),
44379
- verifyScope()
44548
+ verifyScope(),
44549
+ await verifyGlobalResolution({
44550
+ ...deps.runner !== void 0 ? { runner: deps.runner } : {},
44551
+ distEntry,
44552
+ env: facts.env,
44553
+ pkg: facts.pkg,
44554
+ ...deps.registryFetcher !== void 0 ? { fetcher: deps.registryFetcher } : {},
44555
+ ...deps.registryBase !== void 0 ? { registryBase: deps.registryBase } : {}
44556
+ })
44380
44557
  ];
44381
44558
  const verdict = aggregateVerdict(aspects);
44382
44559
  const report = { schemaVersion: 1, scope: "v1", verdict, aspects };
@@ -44400,7 +44577,7 @@ function resolvePluginAsset(moduleUrl, fileName) {
44400
44577
  join36(here, "..", "..", "plugin", fileName)
44401
44578
  // src mode: <root>/plugin/<f>
44402
44579
  ];
44403
- return candidates.find((p) => existsSync23(p)) ?? candidates[1];
44580
+ return candidates.find((p) => existsSync24(p)) ?? candidates[1];
44404
44581
  }
44405
44582
  function targetsFor(env2) {
44406
44583
  const root = join36(opencodeConfigRoot(env2), "opencode");
@@ -44972,7 +45149,7 @@ import { createHash as createHash20 } from "node:crypto";
44972
45149
  import { basename as basename5 } from "node:path";
44973
45150
  var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
44974
45151
  var SCAN_REQUEST_TIMEOUT_MS = 6e4;
44975
- var SCAN_USER_AGENT = "border-customs/0.8.4 (+https://github.com/TachikomaGundam/Border-Customs; border scan)";
45152
+ var SCAN_USER_AGENT = "border-customs/0.8.5 (+https://github.com/TachikomaGundam/Border-Customs; border scan)";
44976
45153
  var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
44977
45154
  var defaultScanFetcher = async (url2, init) => {
44978
45155
  if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
@@ -45143,7 +45320,7 @@ function parseScanSpec(raw) {
45143
45320
 
45144
45321
  // src/roundtrip/localArtifact.ts
45145
45322
  init_define_BORDER_BUILD();
45146
- import { createReadStream, existsSync as existsSync24, statSync as statSync3 } from "node:fs";
45323
+ import { createReadStream, existsSync as existsSync25, statSync as statSync3 } from "node:fs";
45147
45324
  import { createHash as createHash21 } from "node:crypto";
45148
45325
  import { basename as basename6, resolve as resolve20 } from "node:path";
45149
45326
  var EXT_TABLE = /* @__PURE__ */ new Map([
@@ -45192,7 +45369,7 @@ var GRAMMAR_HINT = {
45192
45369
  };
45193
45370
  function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
45194
45371
  const abs = resolve20(cwd, raw);
45195
- if (existsSync24(abs)) {
45372
+ if (existsSync25(abs)) {
45196
45373
  const st = statSync3(abs);
45197
45374
  if (st.isDirectory()) {
45198
45375
  bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
@@ -45742,13 +45919,13 @@ async function recordRoundtripProof(ctx, artifactSha256, rows, verdict) {
45742
45919
  return;
45743
45920
  }
45744
45921
  const repoDir = resolveRepoDir(ctx.cwd, { ...env2 !== void 0 ? { env: env2 } : {} });
45745
- const probe = await probeEngines(effective.config, { ...env2 !== void 0 ? { env: env2 } : {} });
45746
- if (probe.degraded) {
45922
+ const probe2 = await probeEngines(effective.config, { ...env2 !== void 0 ? { env: env2 } : {} });
45923
+ if (probe2.degraded) {
45747
45924
  notice("engine probe degraded \u2014 cannot mint the rulesHash this proof must match; proof NOT recorded");
45748
45925
  return;
45749
45926
  }
45750
45927
  const rulesHash = await computeCheckRulesHash({
45751
- engineVersions: probe.engineVersions,
45928
+ engineVersions: probe2.engineVersions,
45752
45929
  configDigest: computeConfigDigest(effective),
45753
45930
  ...env2 !== void 0 ? { env: env2 } : {}
45754
45931
  });
@@ -45773,7 +45950,7 @@ var runRoundtrip = (ctx) => runRoundtripCore(ctx);
45773
45950
  init_define_BORDER_BUILD();
45774
45951
  var import_yaml2 = __toESM(require_dist(), 1);
45775
45952
  import { spawnSync as spawnSync20 } from "node:child_process";
45776
- import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync24, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
45953
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync24, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
45777
45954
  import { dirname as dirname19, join as join39 } from "node:path";
45778
45955
  var realIo = {
45779
45956
  read: (p) => readFileSync24(p, "utf8"),
@@ -45785,7 +45962,7 @@ var realIo = {
45785
45962
  mkdir0700: (p) => {
45786
45963
  let cur = p;
45787
45964
  const stack = [];
45788
- while (!existsSync25(cur) && cur !== "/" && cur !== ".") {
45965
+ while (!existsSync26(cur) && cur !== "/" && cur !== ".") {
45789
45966
  stack.push(cur);
45790
45967
  cur = dirname19(cur);
45791
45968
  }
@@ -45793,7 +45970,7 @@ var realIo = {
45793
45970
  mkdirSync13(dir, { mode: 448 });
45794
45971
  }
45795
45972
  },
45796
- exists: (p) => existsSync25(p),
45973
+ exists: (p) => existsSync26(p),
45797
45974
  remove: (p) => unlinkSync2(p),
45798
45975
  remotes: (cwd) => {
45799
45976
  const r = spawnSync20("git", ["-C", cwd, "remote", "-v"], { encoding: "utf8" });
@@ -45973,7 +46150,7 @@ function synthesizeScanConfig(eco) {
45973
46150
 
45974
46151
  // src/scan/materialize.ts
45975
46152
  init_define_BORDER_BUILD();
45976
- import { existsSync as existsSync26, mkdirSync as mkdirSync14, readdirSync as readdirSync13, readFileSync as readFileSync25, rmSync as rmSync17, writeFileSync as writeFileSync11 } from "node:fs";
46153
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readdirSync as readdirSync13, readFileSync as readFileSync25, rmSync as rmSync17, writeFileSync as writeFileSync11 } from "node:fs";
45977
46154
  import { join as join40 } from "node:path";
45978
46155
  var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
45979
46156
  var fail3 = (why) => {
@@ -45996,7 +46173,7 @@ function unwrapSingleDir(dir) {
45996
46173
  return dir;
45997
46174
  }
45998
46175
  function requireManifest(repoDir, ecosystem, candidates) {
45999
- if (!candidates.some((rel) => existsSync26(join40(repoDir, rel)))) {
46176
+ if (!candidates.some((rel) => existsSync27(join40(repoDir, rel)))) {
46000
46177
  fail3(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
46001
46178
  }
46002
46179
  }
@@ -46016,7 +46193,7 @@ function materializePackage(o) {
46016
46193
  const outer = repoDir;
46017
46194
  const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
46018
46195
  const inner = dataCandidates[0] ?? join40(outer, "data.tar.gz");
46019
- if (!existsSync26(inner)) fail3(".gem archive contains no data.tar.gz");
46196
+ if (!existsSync27(inner)) fail3(".gem archive contains no data.tar.gz");
46020
46197
  const gemTree = join40(o.baseDir, "gem-tree");
46021
46198
  mkdirSync14(gemTree, { recursive: true });
46022
46199
  extractFlat(inner, gemTree);
@@ -46029,7 +46206,7 @@ function materializePackage(o) {
46029
46206
  requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
46030
46207
  for (const file2 of CRATES_ENVELOPE_FILES) {
46031
46208
  const path4 = join40(repoDir, file2);
46032
- if (!existsSync26(path4)) continue;
46209
+ if (!existsSync27(path4)) continue;
46033
46210
  const original = readFileSync25(path4);
46034
46211
  rmSync17(path4, { force: true });
46035
46212
  note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex4(original)}`);
@@ -46142,6 +46319,7 @@ function runStatus(ctx) {
46142
46319
  return EXIT_ERROR;
46143
46320
  }
46144
46321
  for (const w of ledger.warnings) ctx.stderr(`border: WARNING ${w}`);
46322
+ ctx.stdout(`engine: ${engineBuild()}`);
46145
46323
  try {
46146
46324
  const drift = remoteTrackingDrift(repoDir);
46147
46325
  const names = drift.configured.length > 0 ? drift.configured.join(", ") : "(none configured \u2014 nothing can ever be pushed)";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "//": "publint is a runtime dep (exact pin): the npm-stage engine is invoked via bin resolution against the installed layout, so consumers get the publint-fail leg out of the box; the dev suite resolves the same node_modules/.bin/publint — no devDependencies entry is needed for it to stay green.",
5
5
  "description": "Fail-closed git push gate: secret scanning + supply-chain checks before any remote leaves the machine",
6
6
  "type": "module",
package/plugin/border.ts CHANGED
@@ -1,4 +1,4 @@
1
- // border-opencode-plugin v0.8.4
1
+ // border-opencode-plugin v0.8.5
2
2
  // Official opencode plugin adapter for the border fail-closed push gate.
3
3
  // Registers ONE agent tool, `border`, that drives the border CLI — argv-only
4
4
  // (node:child_process execFile, never a shell), top-level commands restricted
@@ -46,7 +46,7 @@
46
46
  // cannot-answer, never a fallback to some other binary.
47
47
 
48
48
  import { execFile } from "node:child_process";
49
- import { existsSync } from "node:fs";
49
+ import { existsSync, readFileSync } from "node:fs";
50
50
  import { fileURLToPath } from "node:url";
51
51
  import { z } from "zod";
52
52
 
@@ -209,25 +209,47 @@ function isBorderHelp(result: CliResult): boolean {
209
209
  }
210
210
 
211
211
  type Resolution =
212
- | { readonly ok: true; readonly resolution: BinResolution }
212
+ | { readonly ok: true; readonly resolution: BinResolution; readonly stamp: string }
213
213
  | { readonly ok: false; readonly reasons: readonly string[] };
214
214
 
215
+ /** Twin of parseVersionStamp in src/opencode/aspects/globalResolution.ts; the
216
+ * digit anchor keeps the banner (`border — fail-closed…`) from parsing as a
217
+ * version. Duplicated on purpose: this file must stay import-free (Route A
218
+ * drops it as a single standalone file). */
219
+ function parseVersion(stdout: string): string | null {
220
+ const m = /^border\s+(\d[^\s]*)/m.exec(stdout);
221
+ return m?.[1] ?? null;
222
+ }
223
+
224
+ /** The version this plugin module was packaged with (the ./package.json export).
225
+ * Route A drops (bare file in plugins/) have no sibling package.json: null. */
226
+ function packagedVersion(): string | null {
227
+ try {
228
+ const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")) as { version?: unknown };
229
+ return typeof pkg.version === "string" ? pkg.version : null;
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+
215
235
  /** Positive handshakes are cached per BORDER_BIN key for the host process's
216
- * life; failures are never cached, so a mid-session install recovers on the
217
- * next call. */
218
- let verified: { readonly key: string; readonly resolution: BinResolution } | null = null;
236
+ * life (stamp included); failures are never cached, so a mid-session install
237
+ * recovers on the next call. */
238
+ let verified: { readonly key: string; readonly resolution: BinResolution; readonly stamp: string } | null = null;
219
239
 
220
240
  async function resolveVerified(): Promise<Resolution> {
221
241
  const key = process.env["BORDER_BIN"] ?? "";
222
242
  if (verified !== null && verified.key === key) {
223
- return { ok: true, resolution: verified.resolution };
243
+ return { ok: true, resolution: verified.resolution, stamp: verified.stamp };
224
244
  }
225
245
  const reasons: string[] = [];
226
246
  for (const candidate of candidateList()) {
227
247
  const probe = await runCli(candidate, ["--help"], PROBE_TIMEOUT_MS);
228
248
  if (isBorderHelp(probe)) {
229
- verified = { key, resolution: candidate };
230
- return { ok: true, resolution: candidate };
249
+ const version = await runCli(candidate, ["--version"], PROBE_TIMEOUT_MS);
250
+ const stamp = version.status === 0 ? (parseVersion(version.stdout) ?? "unknown") : "unknown";
251
+ verified = { key, resolution: candidate, stamp };
252
+ return { ok: true, resolution: candidate, stamp };
231
253
  }
232
254
  reasons.push(
233
255
  probe.status === 0
@@ -367,11 +389,26 @@ async function buildToolResult(argv: readonly string[]): Promise<string> {
367
389
  "--- stderr ---\n(empty)",
368
390
  ].join("\n");
369
391
  }
392
+ const shell = `$ border ${argv.join(" ")}`;
393
+ if ((process.env["BORDER_REQUIRE_PKG_VERSION"] ?? "") === "1") {
394
+ const own = packagedVersion();
395
+ const stampCore = resolution.stamp.split("+")[0];
396
+ if (own === null || own !== stampCore) {
397
+ return [
398
+ shell,
399
+ "exit: 2",
400
+ `note: cannot-answer — BORDER_REQUIRE_PKG_VERSION=1 demands the resolved CLI report this package's own version (${own ?? "unreadable"}); the resolved face (${resolution.resolution.label}) reported '${resolution.stamp}'. Nothing was run.`,
401
+ "--- stdout ---\n(empty)",
402
+ "--- stderr ---\n(empty)",
403
+ ].join("\n");
404
+ }
405
+ }
370
406
  const result = await runCli(resolution.resolution, argv, TIMEOUT_MS);
371
407
  if (result.spawnFailed) verified = null;
372
408
  const sections = [
373
- `$ border ${argv.join(" ")}`,
409
+ shell,
374
410
  `exit: ${String(result.status)}`,
411
+ `face: ${resolution.resolution.label} version=${resolution.stamp}`,
375
412
  ];
376
413
  if (result.note.length > 0) sections.push(`note: ${result.note}`);
377
414
  sections.push(`--- stdout ---\n${result.stdout.length === 0 ? "(empty)" : result.stdout}`);