border-customs 0.7.1 → 0.8.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/README.md CHANGED
@@ -118,7 +118,18 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
118
118
  against the history of every ref *and* against the object set a remote does not already
119
119
  have (the transmit set). Oversized-file, checked-in-binary, and notebook-output rules
120
120
  round it out.
121
- 7. **Registry pre-flight** (npm, PyPI, crates.io, and RubyGems targets). Three outcomes per
121
+ 7. **Landing-grade tip lens.** The exact machinery `border push` uses to verify a landing
122
+ (`scanTipTree`: the tip tree materialized from git objects into a throwaway directory,
123
+ scanned by the native exfil family plus both twins filtered to `exfil-*` ids) also runs
124
+ here, against local HEAD — the tree a push would transmit. This closes the
125
+ disk-versus-tip gap: content that exists only in HEAD (introduced by a merge commit's
126
+ resolution and since deleted from disk, say) is invisible to the history and working-tree
127
+ legs above but cannot hide from the gate. Tip findings merge before the allow-list, so a
128
+ config `allow` pin exempts them by rule+file+digest exactly like every other finding, and
129
+ the post-push landing check honors the *same* pins — a green packet means green landing, in
130
+ both directions. The tip leg's git plumbing is fail-closed: a git failure aborts the run
131
+ (exit 2), never a silent clean.
132
+ 8. **Registry pre-flight** (npm, PyPI, crates.io, and RubyGems targets). Three outcomes per
122
133
  target: version already published ⇒ CRITICAL `version-exists` ("bump version required");
123
134
  name owned by someone outside your `rules.authors` allow-list ⇒ CRITICAL
124
135
  `name-foreign-owner`; ambiguous ownership signals ⇒ also CRITICAL, because guessing wrong
@@ -129,7 +140,7 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
129
140
  one of that API's exact *absent* messages (its 404s are text, not JSON, and an
130
141
  unrecognized one is never guessed), a timeout, or unparseable JSON all fail closed as
131
142
  exit 2. An unreachable registry blocks the push instead of letting it run blind.
132
- 8. **Artifact stage.** If registry targets are configured, packages are built **once** into
143
+ 9. **Artifact stage.** If registry targets are configured, packages are built **once** into
133
144
  `.border/dist/` (`npm pack --ignore-scripts`, `python -m build --no-isolation`,
134
145
  `cargo package --allow-dirty --no-verify`, `gem build <name>.gemspec -o .border/dist/`),
135
146
  that exact byte-stream is scanned (gitleaks + secretlint over the extracted contents),
@@ -145,14 +156,16 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
145
156
  re-runs the check's exact `cargo package` into a throwaway directory and refuses to
146
157
  publish if the fresh digest differs from the certified `.crate`. `gem push` uploads the
147
158
  recorded `.gem` byte-for-byte, so it needs no such re-assertion.
148
- 9. **Report.** Findings carry `valueDigest` (sha256 of the matched value) and a masked
159
+ 10. **Report.** Findings carry `valueDigest` (sha256 of the matched value) and a masked
149
160
  snippet: fully blocked for short values, else `first4…last4`. Raw secret bytes never
150
161
  leave process memory. A per-run `TextSanitizer` holds the digest-to-value registry and
151
162
  replaces every flagged literal in *all* rendered text, including agent-written strings in
152
163
  the LLM layer, so a secret cannot reappear inside a message about itself. `report.json`
153
164
  (canonical, byte-stable for machines) and `report.md` land under `.border/runs/`.
154
- 10. **Ledger record.** A PASS is appended to `.border/ledger.jsonl` together with the key
155
- and artifact digests. Degraded and NO-OP runs can never write a PASS.
165
+ 11. **Ledger record.** A PASS is appended to `.border/ledger.jsonl` together with the key
166
+ and artifact digests. Degraded and NO-OP runs can never write a PASS. The key folds in a
167
+ *lens identity*, so a PASS recorded under an older, weaker set of legs never certifies a
168
+ push under the current lens — upgrading the gate forces one fresh full check.
156
169
 
157
170
  ```bash
158
171
  $ border check # full scan, ~seconds on a mid-size repo
@@ -251,6 +264,7 @@ lives inside. The exfil lens adds five rules over the surfaces about to become p
251
264
  | `exfil-home-path` | `/home/<user>` and `C:\Users\<user>` shapes |
252
265
  | `exfil-cred-location` | `~/.pypirc`, `SSHPASS`, `*.env` credential-location shapes |
253
266
  | `exfil-host-profile` | multi-signal host fingerprints (os+version / no-docker / no-passwordless-sudo co-occurrences in one paragraph) |
267
+ | `exfil-machine-binding` | vendor inference-model ids pinned on portable surfaces next to model/default/provider context (L-MACHINE-LOCAL; born 2026-10-02) |
254
268
 
255
269
  The severity a reader **observes** is the (rule × facet × channel) matrix, and its single
256
270
  home is [src/exfil/severity.ts](src/exfil/severity.ts) — the check pipeline, the
@@ -264,11 +278,13 @@ home is [src/exfil/severity.ts](src/exfil/severity.ts) — the check pipeline, t
264
278
  | `exfil-home-path` | tree | native | MEDIUM |
265
279
  | `exfil-cred-location` | tree | native | MEDIUM |
266
280
  | `exfil-host-profile` | tree | native | MEDIUM |
281
+ | `exfil-machine-binding` | tree | native | MEDIUM |
267
282
  | `exfil-rfc1918` | message | native | HIGH |
268
283
  | `exfil-ssh-target` | message | native | HIGH |
269
284
  | `exfil-home-path` | message | native | MEDIUM |
270
285
  | `exfil-cred-location` | message | native | MEDIUM |
271
286
  | `exfil-host-profile` | message | native | MEDIUM |
287
+ | `exfil-machine-binding` | message | native | MEDIUM |
272
288
 
273
289
  Why an observed `CRITICAL` stands where the intent table says HIGH: the twin channels have
274
290
  no HIGH rung — every ingested gitleaks finding carries CRITICAL, and secretlint's `error`
@@ -411,7 +427,10 @@ never from cache:
411
427
  and exits 1; a rerun of `border push --yes` picks up only the still-PENDING targets.
412
428
  - After every git leg that moved, border **verifies the landing**: it re-observes the remote
413
429
  tip (`git ls-remote`, then a fetch of exactly that tip when it is not local) and scans the
414
- landed tree with the same exfil machinery as `border exfil`. Three honest outcomes: clean
430
+ landed tree with the same exfil machinery as `border exfil` — the same lens `border check`
431
+ now runs pre-push, and the same config `allow` pins apply: blocking counts are computed
432
+ after the allow-list, and any exemption is echoed loudly (rule, entry index, count), never
433
+ silent. Three honest outcomes: clean
415
434
  appends a `t:"landing"` ledger record and preserves the push exit; a blocking hit exits 1
416
435
  with `ALREADY PUBLIC; border detects, never erases` — the bytes are out, and border's job
417
436
  there is detection, recording the fact, never rewriting history; an unobservable tip exits
@@ -491,7 +510,7 @@ border <command> [options]
491
510
  | `llm-ingest <findings.json>` | validate agent findings and record the combined verdict |
492
511
  | `scan <[ecosystem:]name@version>` | inspect a third-party published artifact for residue before installing it |
493
512
  | `exfil <rev\|url>` | read-only exfil lens over a local rev or a remote URL (tip tree by default) |
494
- | `opencode <install\|status\|uninstall>` | install/status/uninstall the official OpenCode plugin adapter |
513
+ | `opencode <install\|status\|uninstall\|inspect>` | install/status/uninstall the official OpenCode plugin adapter; `inspect` audits this machine's V1 load surface |
495
514
 
496
515
  Every subcommand accepts the same global flags (verified against `border <cmd> --help`):
497
516
 
@@ -717,7 +736,7 @@ plugin entry ships as `exports["./server"]`):
717
736
  ```jsonc
718
737
  {
719
738
  // pin the exact version — an @latest channel hits the registry on every cold start
720
- "plugin": ["border-customs@0.7.1"]
739
+ "plugin": ["border-customs@0.8.1"]
721
740
  }
722
741
  ```
723
742
 
@@ -752,7 +771,7 @@ The `/border` slash command stays V1-only until the V2 command-delivery API sett
752
771
 
753
772
  What loads is one agent tool `border` plus a `/border` slash command. The tool spawns the CLI
754
773
  argv-only (no shell; 300 s cap, 64 KiB per stream) restricted to the closed command list
755
- `check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`, `roundtrip`, `--help`, and
774
+ `check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`, `roundtrip`, `exfil`, `--help`, and
756
775
  renders the exit-code contract unchanged: 0 pass, 1 gate-blocked or partial push, 2 the gate
757
776
  could not answer — which since 0.5.1 also covers "no border CLI found or verified": the tool
758
777
  answers exit 2 cannot-answer and spawns nothing else. The tool REFUSES `--yes` — a real push
@@ -769,6 +788,18 @@ HTTP assertions — the `border` tool id present, `/border` present exactly once
769
788
  fails); plugin load failures are silent, so judge by the HTTP responses, never by the exit
770
789
  code.
771
790
 
791
+ `border opencode inspect [--json]` audits this machine's **V1 load surface** as one gate-owned
792
+ command: six read-only aspects — cache self-consistency (package.json vs plugin marker line),
793
+ spec freshness (exact pin offline; dist-tag against the registry), the 0.5.1 identity handshake
794
+ replayed against the CLI resolver's candidate order, Route A byte parity, Route A∧B dual-registration
795
+ collision, and the scope declaration — aggregated fail-closed: any FAIL exits 1, else any CANNOT
796
+ exits 2, all PASS exits 0. The certificate boundary is printed, not implied: `--json` carries
797
+ `scope:"v1"`, and V2 load surfaces (project-local `.opencode/plugins/`, npm-prefix layouts, lazy
798
+ activation) are a DECLARED BLIND SPOT pending V2 stabilization — a PASS is never evidence that
799
+ "the whole machine is clean". The tool never writes: `--purge` lives in the drift-watch cron shell,
800
+ which is now a thin consumer of `inspect --json` with the rc contract unchanged
801
+ (FRESH 0 / DRIFT 1 / NOT-CACHED 0 / CANNOT 2).
802
+
772
803
  ## What this is not
773
804
 
774
805
  - **Not a vault or secret manager.** It detects credentials on their way out; it does not
@@ -815,6 +846,27 @@ code.
815
846
 
816
847
  ## Changelog
817
848
 
849
+ ### 0.8.1 (2026-10-06)
850
+ - Maintenance release: certificate-chain rebuild off the scrubbed lineage (INCIDENT-20261006
851
+ identity-exposure cleanup). No functional change vs 0.8.0; 0.8.0's tarball is byte-intact but
852
+ its OIDC provenance pin moved with the history rewrite, so a fresh tag + publish restores a
853
+ verifiable chain. Rotation: package.json + lock roots + plugin marker + scan UA + README pins +
854
+ this entry.
855
+
856
+ ### 0.8.0 (2026-09-30)
857
+ - Add: **`border opencode inspect [--json]`** — the load-surface self-audit becomes a gate-owned
858
+ surface: six read-only aspects (cache self-consistency, spec freshness vs pin/registry, identity
859
+ handshake replay, Route A byte parity, Route A∧B dual-route collision, scope declaration),
860
+ fail-closed aggregation FAIL 1 > CANNOT 2 > PASS 0. `--json` pins `scope:"v1"` — V2 load surfaces
861
+ are a declared blind spot, not silently covered. Zero writes; `--purge` stays in the cron shell.
862
+ - Change: **`exfil` joins the plugin's closed command list** (owner ruling R1 under the four-question
863
+ eligibility gate — no mutation, no credentials, no foreign-byte execution, no local egress;
864
+ packet: `.omo/evidence/F-EXFIL-ALLOWLIST-RULING-2026-09-30.md`). `border opencode …` verbs stay
865
+ terminal-only: they mutate `$XDG_CONFIG_HOME` and fail the gate's first question.
866
+ - Change: `tools/plugin-drift-watch.mjs` retires to a thin shell over `border opencode inspect --json`
867
+ (single source of verdicts); the cron rc contract is byte-stable — FRESH 0, DRIFT 1, NOT-CACHED 0
868
+ (special-cased per ruling B2), CANNOT 2 — and the crontab line is unchanged.
869
+
818
870
  ### 0.7.1 (2026-09-30)
819
871
  - Fix: **release-gate test ruler race** (`test/push.integration.test.ts` I4) — the
820
872
  "dry-run made ZERO verdaccio requests" invariant counted lines in verdaccio's stdout
@@ -842,7 +894,7 @@ code.
842
894
 
843
895
  ### 0.6.0 (2026-09-25)
844
896
  - Add: **the exfil lens** — five rules (`exfil-rfc1918`, `exfil-ssh-target`,
845
- `exfil-home-path`, `exfil-cred-location`, `exfil-host-profile`) that flag
897
+ `exfil-home-path`, `exfil-cred-location`, `exfil-host-profile`, `exfil-machine-binding`) that flag
846
898
  exfil-shaped literals on about-to-be-public surfaces, the gap classic secret
847
899
  scanners leave open because an internal IP is not a credential. The observed
848
900
  severity of every (rule × facet × channel) cell lives in one machine-readable
@@ -1000,6 +1052,8 @@ MIT, see [LICENSE](LICENSE).
1000
1052
 
1001
1053
  ## 中文概要
1002
1054
 
1055
+ **0.8.0(2026-09-30)装载面自审计**:`border opencode inspect [--json]` 把散在插件、cron 脚本与安装器里的装载检查制度化为门禁自有表面——六个只读方面(缓存自洽:package.json 对插件 marker 行;spec 新鲜度:exact pin 离线比、dist-tag 问注册表;身份握手按 CLI 候选序重放;Route A 字节 parity;A∧B 双注册相撞;scope 声明),fail-closed 聚合:任一 FAIL 退 1,否则任一 CANNOT 退 2,全 PASS 退 0。`--json` 面值钉死 `scope:"v1"`:V2 装载面(项目本地 `.opencode/plugins/`、npm-prefix 布局、懒激活态)是**声明的盲区**而非被覆盖——PASS 永远不等于"全机干净"。工具零写入;`--purge` 留在 cron 薄壳,壳的 rc 合同原样保持(FRESH 0 / DRIFT 1 / NOT-CACHED 0(裁定 B2 特判)/ CANNOT 2),crontab 一行未动。exfil 依主人裁定(R1)进插件命令闭集——四问判据全过:不变异、不碰凭据、不执行外来字节、不外传本地数据;`border opencode` 动词组因写 `$XDG_CONFIG_HOME` 留在终端侧。裁决包:`.omo/evidence/F-EXFIL-ALLOWLIST-RULING-2026-09-30.md`。
1056
+
1003
1057
  **0.7.1(2026-09-30)发布门禁尺伤修复**:I4 集成测试的"dry-run 对 registry 零请求"断言按 verdaccio stdout 日志行计数,而日志是最终一致视图——CI 满载 runner 上更早阶段的合法 probe 行晚于快照落盘,把差额错记到 dry-run 头上(v0.7.0 run 36671401844 红 `6 !== 2`,同字节本机绿)。修复=尺子不松语义:所有快照前后加 FIFO drain 屏障(哨兵请求行可见⇒先前请求行必已落盘;哨兵流量不计入计数)。dry-run 零网络契约本身未动,两侧实测请求为零。
1004
1058
 
1005
1059
  **0.7.0(2026-09-28)OpenCode 2(beta)兼容**:插件模块双形状——V1 宿主调 `server()`(对象入口需 ≥1.18.29),V2 宿主(`opencode2`)调 `setup(ctx)` 经 `ctx.tool.transform` 注册同一个工具;spawn 前门(allowlist/`--yes`)与 0.5.1 身份握手两形状共享,双边渲染逐字 parity 由测试钉死。V2 懒激活(断言前须 `POST /api/plugin/await-activation`);`/border` 命令暂仅 V1(V2 投递 API 未定形);已在 beta-19271 项目本地两形态活体验证。V1 活体探针反真空化:opt-in 缺前提即 FAIL 点名补救。
@@ -1035,6 +1089,6 @@ border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install
1035
1089
 
1036
1090
  发布一致性(0.4.1):起因是 aihr 事故——PyPI 上标为 0.2.2 的 wheel 里 `__init__.__version__` 却写着 0.2.1,构件名、元数据与实际模块行为三者不一致,所有 `==0.2.2` 的用户静默装上了旧行为;同类漂移在本仓库也出现过两次(npm tarball 的 User-Agent 停在 0.3.0 而 package.json 是 0.3.1;package-lock.json 根版本两个 release 一直躺在 0.1.0)。发布阶段扫描器因此在构件内部逐源交叉核对版本号:`release-coherence-version-drift` 为 CRITICAL(package.json 与被强制打包的 package-lock、wheel 文件名对 dist-info 目录对 METADATA `Version:` 对 `__init__` 字面量、sdist 的 pyproject 对 PKG-INFO/setup 系字面量、Cargo.toml 对已打包的 Cargo.lock、.gemspec 对 metadata.gz、dist 文件名对 sdist 内 PKG-INFO);版本源存在但无法静态解析(动态 `__version__`、`attr:`、`dynamic = ["version"]`、坏 lockfile)判 MEDIUM `release-coherence-unverifiable-source`——不可核查绝不静默算干净;第二版本源根本没被打包时不发现在内,缺源不是漂移,这条边界是承重的,误报的门禁会把用户训练成 `--force`。唯一可静态强制的跨包管理器声明是 opt-in `release.twin`(`{pypi, npm}` 严格 zod 对列表,未知键 exit 2):同一 run 的 dist 里孪生版本不等 ⇒ CRITICAL `release-coherence-twin-drift`,消息点名两个构件。规则源文件与 residue 指纹表一样并入 rulesHash,改一个匹配器即令全部缓存 PASS 失效。
1037
1091
 
1038
- OpenCode 插件适配层(0.5.0):零手工装载两条路线——主路是在 `opencode.jsonc` 声明 `"plugin": ["border-customs@0.5.0"]`(建议钉具体版本,@latest 每次冷启动都打注册表),opencode 自行下载构件,插件入口走 `exports["./server"]`;插件自带 CLI 解析,顺序为 `BORDER_BIN` 覆盖(独占,坏了就报错、绝不静默回退)> 包内同侧 `dist/index.js`(shebang 直接执行,不可执行则回退 PATH 上的 `node` 承载)> PATH 上的 `border`,免全局安装、免 PATH 配置;0.5.1 起任何候选都必须先过**身份握手**(以 `--help` 应答出 border 横幅与 `usage: border` 才算数),通过之前绝不投递用户 argv,握手不过即 cannot-answer exit 2、不运行任何其他二进制——0.5.0 曾用 `process.execPath` 承载包内 dist,而在插件宿主里 execPath 是 opencode 本体,边检门被静默冒充为宿主(2026-09-22 事故,见 0.5.1 变更日志)。备路是 `border opencode install|status|uninstall` 文件投递,落盘 `$XDG_CONFIG_HOME/opencode` 下的 `plugins/border.ts` 与 `commands/border.md`(无 XDG 回退 HOME/.config):受管文件凭 marker 行识别身份,同名外人文件 exit 2 拒绝、绝不覆盖,卸载只删自己带 marker 的文件,字节相同即 `up to date`。装载面只有一个 `border` 工具(argv-only 生成、无 shell、300 秒与每流 64 KiB 上限,命令闭集 check/push/status/llm-request/llm-ingest/scan/roundtrip/--help)与一条 `/border` 斜杠命令(config 钩子以 `??=` 自注册,用户同名文件命令保持权威)。边界如实说:allowlist 是 UX/防呆而非安全边界——插件进程与 opencode 进程同权,门禁的牙齿在 CLI 与账本,不在适配层;`push --yes` 按设计留在终端人审,工具端机械拒绝,会话内裸 push 本就是 CLI 合同的 DRY-RUN。装载验证走 opt-in 探针 `BORDER_OPENCODE_PROBE=1`(临时 HOME/XDG 起 `opencode serve` 对照组,断言 `border` 工具 id 与恰好一条 border 命令,重复即 FAIL;插件加载失败是静默的,只认 HTTP 响应不认退出码)。两条路线装载后都需重启 opencode:工具与命令在启动时扫描。
1092
+ OpenCode 插件适配层(0.5.0):零手工装载两条路线——主路是在 `opencode.jsonc` 声明 `"plugin": ["border-customs@0.5.0"]`(建议钉具体版本,@latest 每次冷启动都打注册表),opencode 自行下载构件,插件入口走 `exports["./server"]`;插件自带 CLI 解析,顺序为 `BORDER_BIN` 覆盖(独占,坏了就报错、绝不静默回退)> 包内同侧 `dist/index.js`(shebang 直接执行,不可执行则回退 PATH 上的 `node` 承载)> PATH 上的 `border`,免全局安装、免 PATH 配置;0.5.1 起任何候选都必须先过**身份握手**(以 `--help` 应答出 border 横幅与 `usage: border` 才算数),通过之前绝不投递用户 argv,握手不过即 cannot-answer exit 2、不运行任何其他二进制——0.5.0 曾用 `process.execPath` 承载包内 dist,而在插件宿主里 execPath 是 opencode 本体,边检门被静默冒充为宿主(2026-09-22 事故,见 0.5.1 变更日志)。备路是 `border opencode install|status|uninstall` 文件投递,落盘 `$XDG_CONFIG_HOME/opencode` 下的 `plugins/border.ts` 与 `commands/border.md`(无 XDG 回退 HOME/.config):受管文件凭 marker 行识别身份,同名外人文件 exit 2 拒绝、绝不覆盖,卸载只删自己带 marker 的文件,字节相同即 `up to date`。装载面只有一个 `border` 工具(argv-only 生成、无 shell、300 秒与每流 64 KiB 上限,命令闭集 check/push/status/llm-request/llm-ingest/scan/roundtrip/exfil/--help)与一条 `/border` 斜杠命令(config 钩子以 `??=` 自注册,用户同名文件命令保持权威)。边界如实说:allowlist 是 UX/防呆而非安全边界——插件进程与 opencode 进程同权,门禁的牙齿在 CLI 与账本,不在适配层;`push --yes` 按设计留在终端人审,工具端机械拒绝,会话内裸 push 本就是 CLI 合同的 DRY-RUN。装载验证走 opt-in 探针 `BORDER_OPENCODE_PROBE=1`(临时 HOME/XDG 起 `opencode serve` 对照组,断言 `border` 工具 id 与恰好一条 border 命令,重复即 FAIL;插件加载失败是静默的,只认 HTTP 响应不认退出码)。两条路线装载后都需重启 opencode:工具与命令在启动时扫描。
1039
1093
 
1040
- 外泄透镜(0.6.0):经典密钥扫描器看不见内网 IP、`user@内网主机`、家目录、凭据文件位置、主机画像这类"外泄形状"字面量——它们不是密钥,却如实描述公网仓库身后的私网。0.6.0 加五条规则 `exfil-rfc1918/exfil-ssh-target/exfil-home-path/exfil-cred-location/exfil-host-profile`,每个(规则×分面×通道)格子的**观察严重度**只有单一事实源 `src/exfil/severity.ts`:树面 HIGH 族走双生规则(gitleaks TOML + secretlint pattern,id 逐字节相等、三通道判决对等性有测试钉死),因两引擎都没有 HIGH 档而如实记为 CRITICAL(不硬掰,如实表);树面 MEDIUM 族走原生腿;提交消息面原生独占、id 带 `:message` 后缀,allow 条目按 id 精确匹配、跨面豁免在结构上不可能;tag note 分面归既有引擎腿 `tag-message-secret`,核心绝不重复发。规则源文件并入 rulesHash,改一个匹配器即令全部缓存 PASS 失效。独立只读入口 `border exfil <rev|url>`(默认树尖,`--deep` 补全史 blob 与全部可达提交消息;URL 模式落临时克隆、退出即销毁)。push 之后新增**落地核验**:ls-remote 复读远端尖、必要时精确 fetch 该对象,用同一套机制扫已公开树——干净则追加 `t:"landing"` 账本行并保留原退出码;有阻断发现则 exit 1 并明说 ALREADY PUBLIC;border 只检测、从不抹除;远端不可观测则 exit 2、一条记录都不造(没核验就没事实),已执行的 push 记录原样矗立——账本 append-only,任何情况下不改写历史。测试语料遵守夹具纪律:10.200.0.0/16 为预留夹具段(树面全通道静默、双生规则原生镜像该豁免,消息面例外照红),其余泄漏形状一律由 `assembleOctet`/`assembleHost` 运行时拼装,签出字节里没有完整泄漏 token——border 的透镜对自己的仓库绿,红故事照常武装。真值通道 `BORDER_EXFIL_TRUTH=1` 为 opt-in,红锚点带生命周期条款:上游作者清理后按设计转绿、留档不判败。
1094
+ 外泄透镜(0.6.0):经典密钥扫描器看不见内网 IP、`user@内网主机`、家目录、凭据文件位置、主机画像这类"外泄形状"字面量——它们不是密钥,却如实描述公网仓库身后的私网。0.6.0 加五条规则 `exfil-rfc1918/exfil-ssh-target/exfil-home-path/exfil-cred-location/exfil-host-profile`,每个(规则×分面×通道)格子的**观察严重度**只有单一事实源 `src/exfil/severity.ts`:树面 HIGH 族走双生规则(gitleaks TOML + secretlint pattern,id 逐字节相等、三通道判决对等性有测试钉死),因两引擎都没有 HIGH 档而如实记为 CRITICAL(不硬掰,如实表);树面 MEDIUM 族走原生腿;提交消息面原生独占、id 带 `:message` 后缀,allow 条目按 id 精确匹配、跨面豁免在结构上不可能;tag note 分面归既有引擎腿 `tag-message-secret`,核心绝不重复发。规则源文件并入 rulesHash,改一个匹配器即令全部缓存 PASS 失效。独立只读入口 `border exfil <rev|url>`(默认树尖,`--deep` 补全史 blob 与全部可达提交消息;URL 模式落临时克隆、退出即销毁)。push 之后新增**落地核验**:ls-remote 复读远端尖、必要时精确 fetch 该对象,用同一套机制扫已公开树——干净则追加 `t:"landing"` 账本行并保留原退出码;有阻断发现则 exit 1 并明说 ALREADY PUBLIC;border 只检测、从不抹除;远端不可观测则 exit 2、一条记录都不造(没核验就没事实),已执行的 push 记录原样矗立——账本 append-only,任何情况下不改写历史。2026-10-04 补上透镜对等:check 流水线在原生规则之后、注册表预检之前多跑一条**树尖腿**,用与落地核验完全同一套 scanTipTree 机制预扫本地 HEAD(push 将传输的那棵树),历史与工作面腿看不见的"只在树尖存在"内容(如合并提交解析引入、随后从磁盘删除的文件)从此无处藏;树尖发现汇入 allow 前合并点,按 rule+file+digest 与所有发现同权豁免——落地核验同样消费 cfg allow 并把每一次豁免大声列出(规则、条目号、条数),绝不沉默;绿色交接单与绿色落地双向一致。ledger key 折入透镜身份,旧一代(无树尖腿)的缓存 PASS 不再为新透镜背书,升级后强制一次全新全扫。树尖腿 git 故障即 exit 2,没有静默清白。测试语料遵守夹具纪律:10.200.0.0/16 为预留夹具段(树面全通道静默、双生规则原生镜像该豁免,消息面例外照红),其余泄漏形状一律由 `assembleOctet`/`assembleHost` 运行时拼装,签出字节里没有完整泄漏 token——border 的透镜对自己的仓库绿,红故事照常武装。真值通道 `BORDER_EXFIL_TRUTH=1` 为 opt-in,红锚点带生命周期条款:上游作者清理后按设计转绿、留档不判败。
@@ -176,4 +176,4 @@ export function matchSshTarget(text: string, o: RuleOptions): RuleHit[] {
176
176
  // matchHomePath / matchCredLocation / matchHostProfile live in rulesPath.ts
177
177
  // and are re-exported here so every consumer keeps importing the five
178
178
  // predicates from one module. Type-only imports keep that pair acyclic.
179
- export { matchCredLocation, matchHomePath, matchHostProfile } from "./rulesPath.ts";
179
+ export { matchCredLocation, matchHomePath, matchHostProfile, matchMachineBinding } from "./rulesPath.ts";
@@ -110,3 +110,31 @@ export function matchHostProfile(text: string, _o: RuleOptions): RuleHit[] {
110
110
  flush();
111
111
  return hits;
112
112
  }
113
+
114
+ // ---------------------------------------------------------------- exfil-machine-binding
115
+
116
+ /**
117
+ * L-MACHINE-LOCAL (owner ruling 2026-10-01; rule born 2026-10-02 after a
118
+ * vendor model id sat in another repo's portable README+code default for six
119
+ * releases unnoticed). Vendor inference-model identifiers pinned on portable
120
+ * surfaces couple a package to ONE operator's device authorization. Context
121
+ * gate: the line must ALSO speak of model/default/provider — prose mentions
122
+ * of a model name stay green (location, not vocabulary — mirrors the
123
+ * cred-location "position, not value" discipline).
124
+ * Self-stealth: literals are built in fragments so THIS file never trips
125
+ * its own regex.
126
+ */
127
+ const BINDING_TOKEN = String.raw`\b(?:q` + "wen" + String.raw`|deep` + "seek" + String.raw`|kimi|g` + "lm" + String.raw`|ernie|dou` + "bao" + String.raw`|hunyuan|sp` + "ark" + String.raw`)[-\.\d][\w.-]*\b`;
128
+ const BINDING_VENDOR_RE = new RegExp(BINDING_TOKEN, "gi");
129
+ const BINDING_CONTEXT_RE = /(?:model|模型|default|默认|provider)/i;
130
+
131
+ export function matchMachineBinding(text: string, _o: RuleOptions): RuleHit[] {
132
+ const hits: RuleHit[] = [];
133
+ text.split("\n").forEach((body, idx) => {
134
+ if (!BINDING_CONTEXT_RE.test(body)) return;
135
+ for (const m of body.matchAll(BINDING_VENDOR_RE)) {
136
+ hits.push({ rule: "exfil-machine-binding", matched: m[0] ?? "", line: idx + 1, message: "Machine binding: a vendor inference-model id pinned next to model/default/provider context — device authorization belongs in operator-local config, not portable surfaces (L-MACHINE-LOCAL)." });
137
+ }
138
+ });
139
+ return hits;
140
+ }
@@ -40,6 +40,7 @@ import {
40
40
  import {
41
41
  matchCredLocation,
42
42
  matchHomePath,
43
+ matchMachineBinding,
43
44
  matchHostProfile,
44
45
  matchRfc1918,
45
46
  matchSshTarget,
@@ -99,6 +100,7 @@ const PREDICATES: Readonly<Record<ExfilRuleId, (text: string, o: RuleOptions) =>
99
100
  "exfil-home-path": matchHomePath,
100
101
  "exfil-cred-location": matchCredLocation,
101
102
  "exfil-host-profile": matchHostProfile,
103
+ "exfil-machine-binding": matchMachineBinding,
102
104
  };
103
105
 
104
106
  /**
@@ -44,13 +44,14 @@
44
44
  export const EXFIL_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] as const;
45
45
  export type ExfilSeverity = (typeof EXFIL_SEVERITIES)[number];
46
46
 
47
- /** The five exfil-lens rule ids, verbatim; twins must ship these ids byte-equal (T2 guard). */
47
+ /** The exfil-lens rule ids, verbatim; twins must ship these ids byte-equal (T2 guard). */
48
48
  export const EXFIL_RULE_IDS = [
49
49
  "exfil-rfc1918",
50
50
  "exfil-ssh-target",
51
51
  "exfil-home-path",
52
52
  "exfil-cred-location",
53
53
  "exfil-host-profile",
54
+ "exfil-machine-binding",
54
55
  ] as const;
55
56
  export type ExfilRuleId = (typeof EXFIL_RULE_IDS)[number];
56
57
 
@@ -71,6 +72,7 @@ export const EXFIL_TABLE_SEVERITY: Readonly<Record<ExfilRuleId, ExfilSeverity>>
71
72
  "exfil-home-path": "MEDIUM",
72
73
  "exfil-cred-location": "MEDIUM",
73
74
  "exfil-host-profile": "MEDIUM",
75
+ "exfil-machine-binding": "MEDIUM",
74
76
  };
75
77
 
76
78
  /** HIGH family: the two identity-carrying location rules; tree face belongs to the twins. */
@@ -85,7 +87,7 @@ export const EXFIL_HIGH_FAMILY: readonly ExfilRuleId[] = ["exfil-rfc1918", "exfi
85
87
  */
86
88
  export const EXFIL_SECRET_VALUE_RULES: readonly ExfilRuleId[] = EXFIL_HIGH_FAMILY;
87
89
  /** MEDIUM family: privacy-shape rules; native carries them on both scanned facets. */
88
- export const EXFIL_MEDIUM_FAMILY: readonly ExfilRuleId[] = ["exfil-home-path", "exfil-cred-location", "exfil-host-profile"];
90
+ export const EXFIL_MEDIUM_FAMILY: readonly ExfilRuleId[] = ["exfil-home-path", "exfil-cred-location", "exfil-host-profile", "exfil-machine-binding"];
89
91
 
90
92
  /** null = FORBIDDEN cell: that channel must never emit that rule on that facet. */
91
93
  export type ObservedSeverity = ExfilSeverity | null;
@@ -96,6 +98,7 @@ const TWIN_HIGH: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
96
98
  "exfil-home-path": null,
97
99
  "exfil-cred-location": null,
98
100
  "exfil-host-profile": null,
101
+ "exfil-machine-binding": null,
99
102
  };
100
103
  const NULL_ROW: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
101
104
  "exfil-rfc1918": null,
@@ -103,13 +106,14 @@ const NULL_ROW: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
103
106
  "exfil-home-path": null,
104
107
  "exfil-cred-location": null,
105
108
  "exfil-host-profile": null,
109
+ "exfil-machine-binding": null,
106
110
  };
107
111
 
108
112
  /** The full (facet × channel × rule) truth table — the plan's 分面契约, machine-readable. */
109
113
  export const EXFIL_MATRIX: Readonly<Record<ExfilFacet, Readonly<Record<ExfilChannel, Readonly<Record<ExfilRuleId, ObservedSeverity>>>>>> = {
110
114
  tree: {
111
115
  // native tree leg = MEDIUM family补发 only (v2 double-emission revoked).
112
- native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM" },
116
+ native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM", "exfil-machine-binding": "MEDIUM" },
113
117
  // gitleaks.ts:81 hardcodes CRITICAL for every ingested finding.
114
118
  "gitleaks-twin": TWIN_HIGH,
115
119
  // secretlint.ts:299-309 has no HIGH rung — error maps to CRITICAL as-is.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "border-customs",
9
- "version": "0.7.1",
9
+ "version": "0.8.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@opencode-ai/plugin": "^1.17.4",