border-customs 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Border-Customs contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # border
2
+
3
+ A fail-closed pre-push gate for git, npm, and PyPI. It scans your repo for secrets and
4
+ supply-chain risk, then refuses to let anything leave the machine unless a fresh, unbroken
5
+ check passed for exactly the state you are about to publish.
6
+
7
+ ```bash
8
+ npm install -g border-customs
9
+ cd /path/to/your/repo
10
+ border check
11
+ ```
12
+
13
+ Requires Node >= 22 and the `gitleaks` binary on `PATH` (border vendors the rule config for
14
+ gitleaks 8.30.1; the binary itself is yours to install, same as `git`). secretlint runs
15
+ in-process as a bundled dependency. Optional engines are listed in
16
+ [Configuration](#configuration).
17
+
18
+ ## The problem it solves
19
+
20
+ The usual secret workflow is leak-then-repent: `git push`, notice the AWS key in the diff
21
+ review (or not), then `git filter-repo`, force-push, revoke the credential, and email the
22
+ security list. Post-hoc scrubbing fails for two mechanical reasons. Force-pushing a public
23
+ branch is history rewriting: clones, forks, CI caches, and package mirrors already hold the
24
+ bad objects. And published package versions are effectively forever: npm's unpublish policy
25
+ forbids reusing a `name@version` once it was ever used, and PyPI yanking is non-destructive.
26
+ The only cheap fix is to not cross the boundary, and the only boundary that matters is the
27
+ one between this machine and the world.
28
+
29
+ Git's own `pre-commit` and `pre-push` hooks help, but they gate the *command*, not the
30
+ *state*, and they trust whatever ran last. border inverts that: a push is only allowed when
31
+ a check passed for the exact fingerprint of what would be pushed, where "what would be
32
+ pushed" includes the six things people forget:
33
+
34
+ - **Untracked and dirty files are inputs too.** History scanning alone misses the `.env`
35
+ that was never committed but will ride along in a `git add -A`. border fingerprints the
36
+ full `git status --porcelain` digest and scans the working tree directly.
37
+ - **Deleted-but-pushed blobs.** A secret removed in a later commit still ships in the
38
+ history of the ref being pushed. The history leg scans everything the push would
39
+ transmit, not just HEAD.
40
+ - **Archives.** Tarballs and zips inside the repo are unpacked and scanned with
41
+ `<archive>!<inner-path>` attribution.
42
+ - **The published bytes, not the source tree.** `npm pack` and `python -m build` rewrite
43
+ what ships. border packs/builds once into `.border/dist/`, scans those exact bytes, and
44
+ re-hashes them again at publish time.
45
+ - **The registry.** Pushing `package.json` at version `1.2.3` when `1.2.3` already exists
46
+ on the registry is a supply-chain event (silent stale artifact, or squatted name). border
47
+ fails the check on "version exists, bump required" and on foreign ownership of your name.
48
+ - **Identity.** A repo whose pushed commits are authored by an identity outside your
49
+ allow-list is on fire, and `PASS` from an earlier state does not vouch for it at push
50
+ time; border re-scans identity on the transmit set before every real push.
51
+
52
+ ## Principle
53
+
54
+ One sentence: **a PASS is a statement about a fingerprint, not about the past.**
55
+
56
+ ```
57
+ checkKey = sha256( headSha
58
+ ⊕ porcelainDigest # every tracked/untracked/staged byte-change
59
+ ⊕ rulesHash # config digest + vendored rules + engine versions + prompt template
60
+ ⊕ exposureSet # sanitized remote URLs + npm/pypi name@version
61
+ ⊕ refSet # branch being pushed + every local tag
62
+ ⊕ effectiveTargets ) # git, npm, pypi as configured for this run
63
+ ```
64
+
65
+ Move *any* of those six and the key moves, the stored PASS no longer covers the state, and
66
+ border makes you re-check. Committing, editing an untracked file, renaming a remote,
67
+ bumping a version, adding a tag, editing `border.yaml`, or an engine version bump all
68
+ invalidate. There is no time-based expiry and no "looks the same": the check either covers
69
+ this exact state or it does not.
70
+
71
+ Everything else in the design follows from making that statement unbreakable: the ledger is
72
+ append-only and corruption-tolerant, degraded tools poison the verdict instead of hiding
73
+ it, and the two registry-facing legs (pre-flight and publish) both have to agree.
74
+
75
+ ## Architecture: what `border check` runs, in order
76
+
77
+ 1. **Config load.** `border.yaml` parsed with zod in strict mode. Unknown keys, bad types,
78
+ or unset `${ENV}` placeholders are hard errors (exit 2), never defaults. If no config
79
+ exists but git remotes do, border infers a `git`-only config and says so on stderr. A
80
+ hidden local overlay `.border/config.local.yaml` is deep-merged for machine-local pins.
81
+ 2. **Engine policy probe.** Every required engine (default `gitleaks` + `secretlint`,
82
+ overridable with `--require-engine`) must answer a version probe. A missing or broken
83
+ probe yields a `DEGRADED-ENGINE` CRITICAL finding and forces **exit 2 with the verdict
84
+ marked UNTRUSTWORTHY**: border refuses to emit a PASS it cannot mechanically justify.
85
+ An engine whose version output cannot be parsed also exits 2 rather than assuming a
86
+ current tool.
87
+ 3. **Hostile-config guard.** Before trusting any scan, the HEAD tree is checked for a
88
+ committed `.gitleaksignore` or `.gitleaks.toml`. Those files make gitleaks itself
89
+ obey the repo it should be auditing (verified empirically: one committed ignore file
90
+ turned 1 finding into 0). Their mere presence is a CRITICAL `repo-self-ignores-findings`
91
+ finding, because refusing to trust them is the only fail-closed option. border always
92
+ passes its own vendored config explicitly, so repo-shipped rules cannot replace it.
93
+ 4. **gitleaks legs.** History scan over the refs a push would transmit (secrets in
94
+ commits unreachable from HEAD still count), working-tree directory scan (archive depth
95
+ 2, plus border's own extractor for formats gitleaks misses such as `.tgz`), and a tag
96
+ *message* leg (annotation bodies are pushed with the tag and are not covered by file
97
+ scans). Findings inside extracted archives are re-attributed to `archive!<inner>`.
98
+ 5. **secretlint leg.** Runs in-process on the git-listed file set with the recommended
99
+ preset, `no-homedir`, `no-dotenv`, plus `border.yaml` rules turned into patterns
100
+ (hosts, IPs, paths). The upstream AWS access-key ID rule ships force-enabled, and
101
+ every secretlint hit's echoed raw value is stripped from messages before persistence.
102
+ 6. **Native rules.** AI-session artifact detection on a *closed* matcher list
103
+ (`.omo/**`, `**/transcripts/**`, `*.session.jsonl`, `opencode.json(c)`, `.opencode/**`,
104
+ env files except `.env.example`/`.env.sample`, junk like `probe*`/`*.rej`/
105
+ `node_modules/**`, plus your `rules.pathPatterns` through the same compiler). Identity
106
+ allow-listing over commit authors, merge committers, and annotated-tag taggers, checked
107
+ against the history of every ref *and* against the object set a remote does not already
108
+ have (the transmit set). Oversized-file, checked-in-binary, and notebook-output rules
109
+ round it out.
110
+ 7. **Registry pre-flight** (npm/PyPI targets only). Three outcomes per target: version
111
+ already published ⇒ CRITICAL `version-exists` ("bump version required"); name owned by
112
+ someone outside your `rules.authors` allow-list ⇒ CRITICAL `name-foreign-owner`;
113
+ ambiguous ownership signals ⇒ also CRITICAL, because guessing wrong is how squats ship.
114
+ Critically, **silence never means absent**: an empty stdout from `npm view`, a non-200
115
+ non-404 PyPI response, a timeout, or unparseable JSON all fail closed as exit 2. An
116
+ unreachable registry blocks the push instead of letting it run blind.
117
+ 8. **Artifact stage.** If npm/PyPI targets are configured, packages are built **once** into
118
+ `.border/dist/` (`npm pack --ignore-scripts`, `python -m build --no-isolation`), that
119
+ exact byte-stream is scanned (gitleaks + secretlint over the extracted contents),
120
+ manifest-diffed (lifecycle `preinstall`/`install`/`postinstall`/`prepare` hooks are
121
+ CRITICAL; entries outside the `files` whitelist are HIGH; PyPI sdists get a
122
+ `sdist-unexpected-file` HIGH because setuptools builds from the working tree and is
123
+ `.gitignore`-blind), checked with `publint` / `twine check --strict`, and recorded as
124
+ `{file, sha256, bytes}` in the ledger.
125
+ At publish time the *same bytes* are re-hashed: any mismatch is exit 2, before the wire.
126
+ 9. **Report.** Findings carry `valueDigest` (sha256 of the matched value) and a masked
127
+ snippet: fully blocked for short values, else `first4…last4`. Raw secret bytes never
128
+ leave process memory. A per-run `TextSanitizer` holds the digest-to-value registry and
129
+ replaces every flagged literal in *all* rendered text, including agent-written strings in
130
+ the LLM layer, so a secret cannot reappear inside a message about itself. `report.json`
131
+ (canonical, byte-stable for machines) and `report.md` land under `.border/runs/`.
132
+ 10. **Ledger record.** A PASS is appended to `.border/ledger.jsonl` together with the key
133
+ and artifact digests. Degraded and NO-OP runs can never write a PASS.
134
+
135
+ ```bash
136
+ $ border check # full scan, ~seconds on a mid-size repo
137
+ $ border check # identical state: no re-scan
138
+ SKIP 35ffb3cb — PASS 2026-09-04T18:11:44.976Z report .border/runs/35ffb3cb-.../report.json
139
+ ```
140
+
141
+ The **skip-ledger** is what makes the gate cheap enough to run on every push: a repeat
142
+ `border check` on an unchanged fingerprint replays the recorded verdict in well under a
143
+ second. The replay is not blind trust. Before honoring a skip border re-derives the live
144
+ fingerprint (including engine probes), refuses skips recorded in the other LLM mode, and
145
+ re-packs npm artifacts to prove the recorded tarball digests still match fresh byte-for-byte
146
+ (a dirty tree, a drifted `package.json`, or a changed npm version all force a full re-check;
147
+ PyPI builds are not byte-reproducible, so their skip proof is head+porcelain equality, and
148
+ publish still re-hashes the exact dist files). A corrupt ledger line is dropped with a loud
149
+ warning, never a crash, and never a silently empty history.
150
+
151
+ ## Architecture: `border push`
152
+
153
+ `border push` is a state machine over per-target states, recomputed from live git queries,
154
+ never from cache:
155
+
156
+ | State | Meaning |
157
+ | --- | --- |
158
+ | `PUSHED` | remote already holds every ref at the local value (checked via `git ls-remote`, full-ref compare, tags compared *peeled*) |
159
+ | `PENDING` | this target needs to move, and the current fingerprint has a PASS behind it |
160
+ | `BLOCKED` | no PASS for this state (run `border check`), or a registry finding says stop |
161
+
162
+ - Bare `border push` is a **DRY-RUN**: it prints the exact `git push --dry-run` /
163
+ `npm publish` / `twine upload` lines it would run, refuses to act, and exits with the
164
+ gate's verdict. `--yes` is what executes.
165
+ - Multi-remote git push is **all-or-nothing before anything moves**: every remote × every
166
+ ref must be a fast-forward, checked up front. If any remote diverged, border prints
167
+ `DIVERGED` with the offending shas and pushes nothing. **It never force-pushes; there is
168
+ no flag for it.**
169
+ - Registry legs go through the same gate as push: a PASS record for the current key, then
170
+ the artifact re-hash match against `.border/dist/`, then an immediate version-exists
171
+ re-probe (the registry is allowed to change between check and publish), then the upload
172
+ with `stdio: inherit` so `npm`/`twine` OTP and credential prompts reach you untouched.
173
+ border never reads, stores, or handles registry tokens. Published versions are not
174
+ retried on failure because a half-published version can never be republished.
175
+ - If one remote of several succeeded before a failure, border says so explicitly (PARTIAL)
176
+ and exits 1; a rerun of `border push --yes` picks up only the still-PENDING targets.
177
+
178
+ ## Architecture: the LLM layer (optional, agent-executed)
179
+
180
+ border itself **never calls any model API**. The `--llm` layer is a two-file handoff with
181
+ the operator's own agent (Claude, Codex, a human with a chat tab):
182
+
183
+ ```bash
184
+ border check # must pass for the current state first
185
+ border llm-request # writes .border/runs/<key8>-.../llm-request.json
186
+ # → your agent reads the bundle, follows the embedded prompt template,
187
+ # and authors findings.json (a strict array; [] is a valid PASS statement)
188
+ border llm-ingest findings.json # validates, re-scrubs, recomputes the verdict
189
+ ```
190
+
191
+ The request bundle is a *masked* review context: the diff versus the remote tip (or a
192
+ stated full-tree diff on first push, truncated over 10 MiB with an explicit marker), the
193
+ deterministic findings, artifact digests, and the review prompt with its sha256. Values the
194
+ deterministic engines flagged appear only as `[REDACTED:<sha8>]`. The ingest side validates
195
+ every agent finding against the schema, rejects any finding whose `path` is not in the
196
+ bundle (an agent inventing locations is a hard error, exit 2), forces `engine: "agent"`,
197
+ computes digests itself rather than trusting agent-supplied ones, re-runs the sanitizer over
198
+ agent free text, and records an `llm: true` PASS that plain checks cannot ride and vice
199
+ versa. The residual risk is stated in the bundle itself: masking only covers values the
200
+ deterministic layers already found, so an undiscovered secret in diff context *can* reach
201
+ your LLM endpoint. That is the accepted boundary of this optional layer; the deterministic
202
+ layers remain the actual gate.
203
+
204
+ ## Commands
205
+
206
+ ```
207
+ border <command> [options]
208
+ ```
209
+
210
+ | Command | Purpose |
211
+ | --- | --- |
212
+ | `check` | run the secret + supply-chain gate on the pending scope |
213
+ | `push` | gate-verified push; DRY-RUN unless `--yes` |
214
+ | `status` | newest gate records per target |
215
+ | `llm-request` | emit the masked review bundle for LLM-authored commits |
216
+ | `llm-ingest <findings.json>` | validate agent findings and record the combined verdict |
217
+
218
+ Every subcommand accepts the same global flags (verified against `border <cmd> --help`):
219
+
220
+ | Flag | Effect |
221
+ | --- | --- |
222
+ | `--config <path>` | config file; default `./border.yaml` |
223
+ | `--targets <list>` | comma-separated `git,npm,pypi` subset restricting this run's scope; a named target that is not configured is exit 2 |
224
+ | `--force` | ignore the skip-ledger, re-run the full check |
225
+ | `--yes` | execute mutations (push only); without it a push is always DRY-RUN |
226
+ | `--require-engine <list>` | replaces the required-engine set from config; unknown or unprobeable names degrade the run (exit 2) |
227
+ | `--llm` | opt this check into the LLM review layer (a plain check can never satisfy an llm-recorded skip) |
228
+ | `--json` | machine-readable report on stdout (check) |
229
+ | `--help, -h` | usage table |
230
+
231
+ ### Exit codes (the contract)
232
+
233
+ | Code | Meaning |
234
+ | --- | --- |
235
+ | `0` | PASS (or no-op): MEDIUM / LOW / INFO findings are allowed and listed |
236
+ | `1` | gate-blocked: HIGH or CRITICAL findings; or a push refused (BLOCKED targets); or a partial push |
237
+ | `2` | the gate could not answer: config error, missing/degraded engine, unreachable registry, malformed input, concurrent lock holder |
238
+
239
+ The `2` class matters as much as `1`: a tool error never exits 0, and no exit-0 run ever
240
+ rests on a leg that silently skipped. Engine exit codes are translated against a closed
241
+ matrix (`gitleaks` 0/1, `trufflehog` 0/183, `secretlint` 0/1); *anything* else, including a
242
+ 126 from gitleaks, is exit 2, never "clean".
243
+
244
+ ## Configuration
245
+
246
+ Everything border can be told lives in `border.yaml` at the repo root. Unknown keys are
247
+ rejected at load (a typo in a gate config is a gate you did not ask for). `${VAR}`
248
+ expansion applies only to remote URLs and registry/repository values, and an unset variable
249
+ is a hard error, never an empty string.
250
+
251
+ Minimal, a git-only repo:
252
+
253
+ ```yaml
254
+ version: 1
255
+ targets:
256
+ git:
257
+ remotes:
258
+ - name: origin
259
+ url: git@github.com:acme/widgets.git
260
+ rules:
261
+ authors:
262
+ emails: [devs@acme.example] # identity allow-list: commits authored by
263
+ names: [Acme Dev] # anyone else are CRITICAL findings
264
+ hosts: [] # extra detection patterns (internal hostnames…)
265
+ ips: []
266
+ pathPatterns: []
267
+ ```
268
+
269
+ `rules` is required by design: a gate with silent defaults is a gate you did not ask for.
270
+
271
+ A repo that publishes a package, with the full surface:
272
+
273
+ ```yaml
274
+ version: 1
275
+ targets:
276
+ git:
277
+ remotes:
278
+ - name: origin
279
+ url: https://github.com/acme/widgets.git
280
+ npm:
281
+ registry: https://registry.npmjs.org # optional; any npm-protocol registry works
282
+ pypi:
283
+ repository: https://pypi.org # optional; TestPyPI or private indexes work
284
+ rules:
285
+ authors: # identity allow-list
286
+ emails: [devs@acme.example]
287
+ names: [Acme CI]
288
+ allowBots: true
289
+ hosts: ["git.internal.acme.example"] # turned into detection patterns
290
+ ips: ["10.20.30.40"]
291
+ pathPatterns: ["/Users/*", "*.pfx"]
292
+ maxFileKB: 500 # oversized-file threshold (default)
293
+ allow: # enumerated suppressions, never blanket
294
+ - { rule: "junk-artifact", match: "*", file: "test/fixtures/**" }
295
+ engines:
296
+ require: [gitleaks, secretlint] # trufflehog: true adds the third-party engine
297
+ ```
298
+
299
+ Notes that change behavior:
300
+
301
+ - `targets.git.remotes: []` (explicit, empty) is a *deliberate* repo-local scope: all
302
+ history/tree/identity/artifact legs still run, only the exposure set is empty. A
303
+ completely missing config with real remotes falls back to config-from-`git remote` with
304
+ a loud stderr warning.
305
+ - Every `allow` entry must be scoped (a `file` pin); `{rule: "*", match: "*"}` with no
306
+ file is rejected at load. Suppressed findings are enumerated in the report's
307
+ `allow-hits` section, so an exit 0 never hides what it hid.
308
+ - `rules.hosts/ips/pathPatterns` feed the same matcher pipeline as the built-ins, so your
309
+ patterns get the same archive attribution and the same allow-listing semantics.
310
+
311
+ ## Integration
312
+
313
+ **Git hook.** The gate is one command, so the classic one-liner is exactly:
314
+
315
+ ```bash
316
+ printf '#!/bin/sh\nexec border check --force\n' > .git/hooks/pre-push && chmod +x .git/hooks/pre-push
317
+ ```
318
+
319
+ `check` exits nonzero whenever the push it would gate is not covered, so any push through
320
+ the hook must first pass. Drop `--force` to get sub-second skip-ledger replay instead of a
321
+ re-scan on every `git push`.
322
+
323
+ **CI.** `border check --json` emits the stable report object; gate on the exit code, parse
324
+ `verdict`/`counts` for dashboards. In CI you typically want `--force` on a cache-less
325
+ runner, plus your `gitleaks` binary provisioned in the image (a degraded run exits 2, which
326
+ most CI maps to red; that is the point).
327
+
328
+ **Agents.** `skills/border/SKILL.md` ships with the repo: it teaches an operator agent (the
329
+ same one that wrote the commit) how to run the five subcommands, how to produce a valid
330
+ `llm-ingest` findings file, and the two standing rules for agents driving pushes: never pass
331
+ `--yes` without a visible human go-ahead, and never push over an exit-2 gate.
332
+
333
+ ## What this is not
334
+
335
+ - **Not a vault or secret manager.** It detects credentials on their way out; it does not
336
+ store, rotate, or inject them.
337
+ - **Not a history rewriter.** If a secret is already in a pushed commit, the fix
338
+ (`git filter-repo` plus credential rotation) is explicitly out of scope and named in the
339
+ finding text. border refuses rather than rewrites.
340
+ - **Not an escape hatch.** There is no force-push flag, no `--i-know-what-im-doing`, no
341
+ per-run expiry of a missing PASS. The refusal paths are the product. If a check cannot be
342
+ trusted (engine gone, registry unreachable), the result is stop, not proceed.
343
+ - **Not a replacement for review or branch protection.** It gates *this machine's* push
344
+ surface; server-side controls still own everything else.
345
+
346
+ ## Security posture
347
+
348
+ - **Redaction is the default output channel.** Findings, reports, status lines, and agent
349
+ text all flow through masked snippets (`valueDigest` + `first4…last4`, full block for
350
+ short values) and the run-scoped sanitizer; raw matched bytes never leave memory.
351
+ - **Fail-closed doctrine.** "Silence never means absent" is applied mechanically: empty
352
+ registry stdout, unparsed versions, non-translatable exit codes, unresolvable ownership,
353
+ corrupt-but-present rules inputs, all become exit 2 or a CRITICAL, never a pass.
354
+ - **No telemetry, no callbacks.** `border check` touches the network only for the
355
+ registry pre-flight, against URLs you configured (`npmjs.org`/`pypi.org` defaults, fully
356
+ overridable to private indexes).
357
+ - **No credential handling.** Subprocess publishes inherit stdio so tokens and OTP prompts
358
+ go between you and `npm`/`twine`, never through border.
359
+ - **State is inert.** Everything border writes lives in `.border/` (ledger, run archives,
360
+ dist, lock), which carries its own `.gitignore` with `*` so a `git add -A` cannot stage
361
+ it, and which every scan leg refuses to treat as a finding source or an allow-list
362
+ target. A repo that *tracks* `.border/**` is itself a CRITICAL finding. A single-writer
363
+ lock makes concurrent runs exit 2 instead of racing.
364
+
365
+ ## License
366
+
367
+ MIT, see [LICENSE](LICENSE).
368
+
369
+ ## 中文概要
370
+
371
+ border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install -g border-customs` 安装,在仓库里跑 `border check`。它扫描 git 历史、工作区(未跟踪文件同样是一等输入)、归档、tag 注释和将要发布的 npm/PyPI 字节,检出密钥与供应链风险;只有当"当前状态指纹"存在新鲜且完整的 PASS 记录时才允许 `border push --yes` 放行。指纹是 sha256(head、porcelain 摘要、规则哈希、暴露面、ref 集合、有效目标)六元组,任何一处变动,旧的 PASS 立即失效,必须重查。流水线:gitleaks(历史+工作区+tag,内置 8.30.1 规则,仓库自带的 ignore 文件直接判 CRITICAL)+ secretlint(进程内,AWS Key 规则强制开启)+ 原生规则(AI 会话产物闭集、提交身份白名单含传输对象检查)+ 注册表预检(版本已存在=必须 bump,名称被外人占有=拒绝;空响应/超时/解析失败一律 exit 2,沉默绝不等于不存在)。构件只构建一次进 `.border/dist/`,扫描的就是发布的字节,发布时再哈希比对,不一致直接拒发。跳过台账让重复检查不到 1 秒,但回放前先重算指纹并重新 pack 验证新鲜度。报告只输出掩码片段(sha256 摘要 + 前4…后4)。push 是多目标状态机,多 remote 先做全有或全无的 fast-forward 预检,永不 force-push;npm/twine 的凭据经 stdio 透传,border 从不触碰。可选 LLM 层 border 自身从不调用模型 API:`llm-request` 导出掩码审阅包,`llm-ingest` 严格校验 agent 结论并重算裁决。退出码即合同:0 通过、1 拦截、2 门禁无法作答,任何"工具不健康"都不可能被误读为干净。MIT 许可,无遥测,除你配置的注册表预检外不联网。