border-customs 0.7.1 → 0.8.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/README.md CHANGED
@@ -491,7 +491,7 @@ border <command> [options]
491
491
  | `llm-ingest <findings.json>` | validate agent findings and record the combined verdict |
492
492
  | `scan <[ecosystem:]name@version>` | inspect a third-party published artifact for residue before installing it |
493
493
  | `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 |
494
+ | `opencode <install\|status\|uninstall\|inspect>` | install/status/uninstall the official OpenCode plugin adapter; `inspect` audits this machine's V1 load surface |
495
495
 
496
496
  Every subcommand accepts the same global flags (verified against `border <cmd> --help`):
497
497
 
@@ -717,7 +717,7 @@ plugin entry ships as `exports["./server"]`):
717
717
  ```jsonc
718
718
  {
719
719
  // pin the exact version — an @latest channel hits the registry on every cold start
720
- "plugin": ["border-customs@0.7.1"]
720
+ "plugin": ["border-customs@0.8.0"]
721
721
  }
722
722
  ```
723
723
 
@@ -752,7 +752,7 @@ The `/border` slash command stays V1-only until the V2 command-delivery API sett
752
752
 
753
753
  What loads is one agent tool `border` plus a `/border` slash command. The tool spawns the CLI
754
754
  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
755
+ `check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`, `roundtrip`, `exfil`, `--help`, and
756
756
  renders the exit-code contract unchanged: 0 pass, 1 gate-blocked or partial push, 2 the gate
757
757
  could not answer — which since 0.5.1 also covers "no border CLI found or verified": the tool
758
758
  answers exit 2 cannot-answer and spawns nothing else. The tool REFUSES `--yes` — a real push
@@ -769,6 +769,18 @@ HTTP assertions — the `border` tool id present, `/border` present exactly once
769
769
  fails); plugin load failures are silent, so judge by the HTTP responses, never by the exit
770
770
  code.
771
771
 
772
+ `border opencode inspect [--json]` audits this machine's **V1 load surface** as one gate-owned
773
+ command: six read-only aspects — cache self-consistency (package.json vs plugin marker line),
774
+ spec freshness (exact pin offline; dist-tag against the registry), the 0.5.1 identity handshake
775
+ replayed against the CLI resolver's candidate order, Route A byte parity, Route A∧B dual-registration
776
+ collision, and the scope declaration — aggregated fail-closed: any FAIL exits 1, else any CANNOT
777
+ exits 2, all PASS exits 0. The certificate boundary is printed, not implied: `--json` carries
778
+ `scope:"v1"`, and V2 load surfaces (project-local `.opencode/plugins/`, npm-prefix layouts, lazy
779
+ activation) are a DECLARED BLIND SPOT pending V2 stabilization — a PASS is never evidence that
780
+ "the whole machine is clean". The tool never writes: `--purge` lives in the drift-watch cron shell,
781
+ which is now a thin consumer of `inspect --json` with the rc contract unchanged
782
+ (FRESH 0 / DRIFT 1 / NOT-CACHED 0 / CANNOT 2).
783
+
772
784
  ## What this is not
773
785
 
774
786
  - **Not a vault or secret manager.** It detects credentials on their way out; it does not
@@ -815,6 +827,20 @@ code.
815
827
 
816
828
  ## Changelog
817
829
 
830
+ ### 0.8.0 (2026-09-30)
831
+ - Add: **`border opencode inspect [--json]`** — the load-surface self-audit becomes a gate-owned
832
+ surface: six read-only aspects (cache self-consistency, spec freshness vs pin/registry, identity
833
+ handshake replay, Route A byte parity, Route A∧B dual-route collision, scope declaration),
834
+ fail-closed aggregation FAIL 1 > CANNOT 2 > PASS 0. `--json` pins `scope:"v1"` — V2 load surfaces
835
+ are a declared blind spot, not silently covered. Zero writes; `--purge` stays in the cron shell.
836
+ - Change: **`exfil` joins the plugin's closed command list** (owner ruling R1 under the four-question
837
+ eligibility gate — no mutation, no credentials, no foreign-byte execution, no local egress;
838
+ packet: `.omo/evidence/F-EXFIL-ALLOWLIST-RULING-2026-09-30.md`). `border opencode …` verbs stay
839
+ terminal-only: they mutate `$XDG_CONFIG_HOME` and fail the gate's first question.
840
+ - Change: `tools/plugin-drift-watch.mjs` retires to a thin shell over `border opencode inspect --json`
841
+ (single source of verdicts); the cron rc contract is byte-stable — FRESH 0, DRIFT 1, NOT-CACHED 0
842
+ (special-cased per ruling B2), CANNOT 2 — and the crontab line is unchanged.
843
+
818
844
  ### 0.7.1 (2026-09-30)
819
845
  - Fix: **release-gate test ruler race** (`test/push.integration.test.ts` I4) — the
820
846
  "dry-run made ZERO verdaccio requests" invariant counted lines in verdaccio's stdout
@@ -1000,6 +1026,8 @@ MIT, see [LICENSE](LICENSE).
1000
1026
 
1001
1027
  ## 中文概要
1002
1028
 
1029
+ **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`。
1030
+
1003
1031
  **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
1032
 
1005
1033
  **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 +1063,6 @@ border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install
1035
1063
 
1036
1064
  发布一致性(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
1065
 
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:工具与命令在启动时扫描。
1066
+ 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
1067
 
1040
1068
  外泄透镜(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,红锚点带生命周期条款:上游作者清理后按设计转绿、留档不判败。
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
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.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@opencode-ai/plugin": "^1.17.4",
package/dist/index.js CHANGED
@@ -13029,7 +13029,7 @@ var require_micromatch = __commonJS({
13029
13029
 
13030
13030
  // src/index.ts
13031
13031
  import { realpathSync } from "node:fs";
13032
- import { fileURLToPath as fileURLToPath6, pathToFileURL } from "node:url";
13032
+ import { fileURLToPath as fileURLToPath7, pathToFileURL } from "node:url";
13033
13033
 
13034
13034
  // src/cli.ts
13035
13035
  import { parseArgs } from "node:util";
@@ -38093,12 +38093,12 @@ function packOnce(o) {
38093
38093
  throw new EngineRunError(`npm pack reported '${report.filename}' but no tarball landed in ${o.distDir}`, null);
38094
38094
  }
38095
38095
  const data = readFileSync3(tarballAbs);
38096
- const sha2564 = createHash3("sha256").update(data).digest("hex");
38096
+ const sha2563 = createHash3("sha256").update(data).digest("hex");
38097
38097
  const sha1 = createHash3("sha1").update(data).digest("hex");
38098
38098
  if (sha1 !== report.shasum || data.byteLength !== report.size) {
38099
38099
  throw new EngineRunError(`npm pack report disagrees with the tarball bytes for ${report.filename} \u2014 corrupted or swapped artifact, failing closed`, null);
38100
38100
  }
38101
- return { filename: report.filename, sha256: sha2564, bytes: data.byteLength, tarballAbs };
38101
+ return { filename: report.filename, sha256: sha2563, bytes: data.byteLength, tarballAbs };
38102
38102
  }
38103
38103
  function publintFinding(o, tarballAbs, identity) {
38104
38104
  const candidates = o.publintBinPath !== void 0 ? [o.publintBinPath] : [
@@ -39141,7 +39141,7 @@ async function runNpmArtifactStage(o) {
39141
39141
  const stateDir = join10(repoDir, BORDER_STATE_DIR);
39142
39142
  const distDir = join10(stateDir, "dist");
39143
39143
  mkdirSync3(distDir, { recursive: true });
39144
- const { filename, sha256: sha2564, bytes, tarballAbs } = packOnce({
39144
+ const { filename, sha256: sha2563, bytes, tarballAbs } = packOnce({
39145
39145
  repoDir,
39146
39146
  distDir,
39147
39147
  ...o.env !== void 0 ? { env: o.env } : {},
@@ -39149,7 +39149,7 @@ async function runNpmArtifactStage(o) {
39149
39149
  ...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
39150
39150
  });
39151
39151
  const findings = [];
39152
- const extractDir = join10(stateDir, "tmp", sha2564.slice(0, 8));
39152
+ const extractDir = join10(stateDir, "tmp", sha2563.slice(0, 8));
39153
39153
  rmSync3(extractDir, { recursive: true, force: true });
39154
39154
  mkdirSync3(extractDir, { recursive: true });
39155
39155
  try {
@@ -39254,7 +39254,7 @@ async function runNpmArtifactStage(o) {
39254
39254
  if (publint !== null) findings.push(publint);
39255
39255
  return {
39256
39256
  findings,
39257
- artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256: sha2564, bytes }
39257
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256: sha2563, bytes }
39258
39258
  };
39259
39259
  }
39260
39260
 
@@ -40354,9 +40354,9 @@ async function runRubygemsArtifactStage(o) {
40354
40354
  throw new EngineRunError(`gem build produced no ${gemName} in ${relative3(repoDir, distDir)} \u2014 failing closed (was the gemspec's literal name/version re-read?)`, null);
40355
40355
  }
40356
40356
  const bytes = readFileSync7(dist);
40357
- const sha2564 = createHash6("sha256").update(bytes).digest("hex");
40357
+ const sha2563 = createHash6("sha256").update(bytes).digest("hex");
40358
40358
  const findings = [];
40359
- const extractDir = join12(stateDir, "tmp", sha2564.slice(0, 8));
40359
+ const extractDir = join12(stateDir, "tmp", sha2563.slice(0, 8));
40360
40360
  rmSync4(extractDir, { recursive: true, force: true });
40361
40361
  mkdirSync4(extractDir, { recursive: true });
40362
40362
  const dataDir = `${extractDir}-data`;
@@ -40434,7 +40434,7 @@ async function runRubygemsArtifactStage(o) {
40434
40434
  }
40435
40435
  return {
40436
40436
  findings,
40437
- artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2564, bytes: bytes.length }
40437
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2563, bytes: bytes.length }
40438
40438
  };
40439
40439
  }
40440
40440
 
@@ -41996,9 +41996,9 @@ async function runCargoArtifactStage(o) {
41996
41996
  const dist = join18(distDir, crateName);
41997
41997
  renameSync(packed, dist);
41998
41998
  const bytes = readFileSync11(dist);
41999
- const sha2564 = createHash11("sha256").update(bytes).digest("hex");
41999
+ const sha2563 = createHash11("sha256").update(bytes).digest("hex");
42000
42000
  const findings = [];
42001
- const extractDir = join18(stateDir, "tmp", sha2564.slice(0, 8));
42001
+ const extractDir = join18(stateDir, "tmp", sha2563.slice(0, 8));
42002
42002
  rmSync8(extractDir, { recursive: true, force: true });
42003
42003
  mkdirSync8(extractDir, { recursive: true });
42004
42004
  try {
@@ -42051,7 +42051,7 @@ async function runCargoArtifactStage(o) {
42051
42051
  }
42052
42052
  return {
42053
42053
  findings,
42054
- artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256: sha2564, bytes: bytes.length }
42054
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256: sha2563, bytes: bytes.length }
42055
42055
  };
42056
42056
  } finally {
42057
42057
  rmSync8(targetDir, { recursive: true, force: true });
@@ -44482,43 +44482,551 @@ async function runLlmRequest(ctx) {
44482
44482
  }
44483
44483
 
44484
44484
  // src/commands/opencode.ts
44485
+ import { existsSync as existsSync22, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync20, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
44486
+ import { dirname as dirname16, join as join31 } from "node:path";
44487
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
44488
+
44489
+ // src/opencode/inspect.ts
44490
+ import { existsSync as existsSync21, readFileSync as readFileSync19 } from "node:fs";
44491
+ import { join as join30 } from "node:path";
44492
+
44493
+ // src/opencode/jsonconfig.ts
44494
+ import { existsSync as existsSync19, readdirSync as readdirSync10, readFileSync as readFileSync18 } from "node:fs";
44495
+ import { join as join29 } from "node:path";
44496
+
44497
+ // src/opencode/contract.ts
44485
44498
  import { createHash as createHash17 } from "node:crypto";
44486
- import { existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync18, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
44499
+ import { existsSync as existsSync18 } from "node:fs";
44487
44500
  import os3 from "node:os";
44488
44501
  import { dirname as dirname15, join as join28 } from "node:path";
44489
44502
  import { fileURLToPath as fileURLToPath5 } from "node:url";
44490
44503
  var PLUGIN_MARKER = "// border-opencode-plugin v";
44491
44504
  var COMMAND_MARKER = "<!-- border-opencode-command -->";
44505
+ var BORDER_PACKAGE = "border-customs";
44506
+ var DEFAULT_REGISTRY_BASE = "https://registry.npmjs.org";
44507
+ var REGISTRY_TIMEOUT_MS2 = 15e3;
44508
+ function sha256Hex3(bytes) {
44509
+ return createHash17("sha256").update(bytes).digest("hex");
44510
+ }
44511
+ function firstLine(bytes) {
44512
+ const text = bytes.toString("utf8");
44513
+ const newline = text.indexOf("\n");
44514
+ return newline < 0 ? text : text.slice(0, newline);
44515
+ }
44492
44516
  function opencodeConfigRoot(env2) {
44493
44517
  const xdg = env2["XDG_CONFIG_HOME"] ?? "";
44494
44518
  if (xdg.length > 0) return xdg;
44495
44519
  const home = env2["HOME"] ?? "";
44496
44520
  return join28(home.length > 0 ? home : os3.homedir(), ".config");
44497
44521
  }
44498
- function resolvePluginAsset(moduleUrl, fileName) {
44522
+ function opencodeCacheRoot(env2) {
44523
+ const xdg = env2["XDG_CACHE_HOME"] ?? "";
44524
+ const base = xdg.length > 0 ? xdg : join28(env2["HOME"] ?? os3.homedir(), ".cache");
44525
+ return join28(base, "opencode", "packages");
44526
+ }
44527
+ function routeAPluginPath(env2) {
44528
+ return join28(opencodeConfigRoot(env2), "opencode", "plugins", "border.ts");
44529
+ }
44530
+ function packagedAssetPath(moduleUrl, fileName) {
44499
44531
  const here = dirname15(fileURLToPath5(moduleUrl));
44500
44532
  const candidates = [
44501
44533
  join28(here, "..", "plugin", fileName),
44502
- // dist mode (bundled): <root>/plugin/<f>
44534
+ // dist (bundled) mode: <root>/plugin/<f>
44503
44535
  join28(here, "..", "..", "plugin", fileName)
44504
44536
  // src mode: <root>/plugin/<f>
44505
44537
  ];
44506
44538
  return candidates.find((p) => existsSync18(p)) ?? candidates[1];
44507
44539
  }
44508
- function targetsFor(env2) {
44509
- const root = join28(opencodeConfigRoot(env2), "opencode");
44510
- return [
44511
- { fileName: "border.ts", destination: join28(root, "plugins", "border.ts"), marker: PLUGIN_MARKER },
44512
- { fileName: "border-command.md", destination: join28(root, "commands", "border.md"), marker: COMMAND_MARKER }
44540
+ function packagedDistEntry(moduleUrl) {
44541
+ const here = dirname15(fileURLToPath5(moduleUrl));
44542
+ const candidates = [
44543
+ join28(here, "index.js"),
44544
+ // dist (bundled) mode: <root>/dist/index.js
44545
+ join28(here, "..", "dist", "index.js"),
44546
+ // src/opencode mode one level under root
44547
+ join28(here, "..", "..", "dist", "index.js")
44548
+ // src mode: <root>/dist/index.js
44513
44549
  ];
44550
+ return candidates.find((p) => existsSync18(p)) ?? candidates[2];
44514
44551
  }
44515
- function sha2563(bytes) {
44516
- return createHash17("sha256").update(bytes).digest("hex");
44552
+
44553
+ // src/opencode/jsonconfig.ts
44554
+ function stripJsoncComments(text) {
44555
+ let out = "";
44556
+ let inString = false;
44557
+ let inLine = false;
44558
+ let inBlock = false;
44559
+ for (let i = 0; i < text.length; i += 1) {
44560
+ const ch = text[i];
44561
+ const next = text[i + 1] ?? "";
44562
+ if (inLine) {
44563
+ if (ch === "\n") {
44564
+ inLine = false;
44565
+ out += ch;
44566
+ }
44567
+ continue;
44568
+ }
44569
+ if (inBlock) {
44570
+ if (ch === "*" && next === "/") {
44571
+ inBlock = false;
44572
+ i += 1;
44573
+ out += " ";
44574
+ }
44575
+ continue;
44576
+ }
44577
+ if (inString) {
44578
+ if (ch === "\\") {
44579
+ out += ch + next;
44580
+ i += 1;
44581
+ continue;
44582
+ }
44583
+ if (ch === '"') inString = false;
44584
+ out += ch;
44585
+ continue;
44586
+ }
44587
+ if (ch === '"') {
44588
+ inString = true;
44589
+ out += ch;
44590
+ continue;
44591
+ }
44592
+ if (ch === "/" && next === "/") {
44593
+ inLine = true;
44594
+ i += 1;
44595
+ continue;
44596
+ }
44597
+ if (ch === "/" && next === "*") {
44598
+ inBlock = true;
44599
+ i += 1;
44600
+ continue;
44601
+ }
44602
+ out += ch;
44603
+ }
44604
+ return out;
44517
44605
  }
44518
- function firstLine(bytes) {
44519
- const text = bytes.toString("utf8");
44520
- const newline = text.indexOf("\n");
44521
- return newline < 0 ? text : text.slice(0, newline);
44606
+ function parseJsonc(text) {
44607
+ return JSON.parse(stripJsoncComments(text).replace(/,(\s*[}\]])/g, "$1"));
44608
+ }
44609
+ var isRecord2 = (v) => typeof v === "object" && v !== null;
44610
+ function splitNpmSpec(entry) {
44611
+ const at = entry.lastIndexOf("@");
44612
+ if (at <= 0) return { name: entry, spec: null };
44613
+ return { name: entry.slice(0, at), spec: entry.slice(at + 1) };
44614
+ }
44615
+ function readPluginDeclarations(path4, pkg) {
44616
+ const none = { path: path4, status: "absent", specs: [] };
44617
+ let text;
44618
+ try {
44619
+ if (!existsSync19(path4)) return none;
44620
+ text = readFileSync18(path4, "utf8");
44621
+ } catch {
44622
+ return { path: path4, status: "error", specs: [] };
44623
+ }
44624
+ let parsed;
44625
+ try {
44626
+ parsed = parseJsonc(text);
44627
+ } catch {
44628
+ return { path: path4, status: "error", specs: [] };
44629
+ }
44630
+ if (!isRecord2(parsed)) return { path: path4, status: "error", specs: [] };
44631
+ const plugin = Array.isArray(parsed["plugin"]) ? parsed["plugin"] : [];
44632
+ const specs = plugin.filter((e) => typeof e === "string" && splitNpmSpec(e).name === pkg);
44633
+ return { path: path4, status: "ok", specs };
44634
+ }
44635
+ function listCacheDirs(cacheRoot, pkg) {
44636
+ let names;
44637
+ try {
44638
+ names = readdirSync10(cacheRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith(`${pkg}@`)).map((e) => e.name).sort();
44639
+ } catch {
44640
+ return [];
44641
+ }
44642
+ return names.map((name) => {
44643
+ const pkgRoot = join29(cacheRoot, name, "node_modules", pkg);
44644
+ let packageVersion = null;
44645
+ try {
44646
+ const meta3 = parseJsonc(readFileSync18(join29(pkgRoot, "package.json"), "utf8"));
44647
+ if (isRecord2(meta3) && typeof meta3["version"] === "string") packageVersion = meta3["version"];
44648
+ } catch {
44649
+ packageVersion = null;
44650
+ }
44651
+ let markerLine = null;
44652
+ try {
44653
+ markerLine = firstLine(readFileSync18(join29(pkgRoot, "plugin", "border.ts")));
44654
+ } catch {
44655
+ markerLine = null;
44656
+ }
44657
+ const markerVersion = markerLine !== null && markerLine.startsWith(PLUGIN_MARKER) ? markerLine.slice(PLUGIN_MARKER.length).trim() : null;
44658
+ return { dir: join29(cacheRoot, name), tag: name.slice(pkg.length + 1), packageVersion, markerVersion, markerLine };
44659
+ });
44660
+ }
44661
+
44662
+ // src/opencode/aspects/cacheVersion.ts
44663
+ var isExactVersion = (spec) => /^\d+\.\d+\.\d+([-+].*)?$/.test(spec);
44664
+ function verifyCacheVersion(facts) {
44665
+ const id = "cache-version";
44666
+ const { cacheDirs, configs, cacheRoot, pkg } = facts;
44667
+ const errored = configs.filter((c) => c.status === "error").map((c) => c.path);
44668
+ const declared = configs.some((c) => c.specs.length > 0);
44669
+ const where = configs.map((c) => `${c.path} [${c.status}]`).join(", ");
44670
+ if (cacheDirs.length === 0) {
44671
+ if (errored.length > 0) {
44672
+ return { id, verdict: "CANNOT", detail: `no cache directory under ${cacheRoot} and the configuration is unreadable (${errored.join(", ")}) \u2014 cannot tell whether a Route-B load is claimed` };
44673
+ }
44674
+ if (declared) {
44675
+ return { id, verdict: "CANNOT", detail: `NOT-CACHED: ${pkg} is declared (${configs.flatMap((c) => [...c.specs]).join(", ")}) but no cache directory exists under ${cacheRoot} \u2014 the load claim has no bytes behind it` };
44676
+ }
44677
+ return { id, verdict: "PASS", detail: `no ${pkg} cache directory under ${cacheRoot} and none declared (${where}) \u2014 Route B not in use, nothing to be stale` };
44678
+ }
44679
+ const pins = new Set(
44680
+ configs.flatMap((c) => [...c.specs]).map((s) => splitNpmSpec(s).spec).filter((s) => s !== null && isExactVersion(s))
44681
+ );
44682
+ const problems = [];
44683
+ for (const d of cacheDirs) {
44684
+ if (d.packageVersion === null) {
44685
+ problems.push(`${d.dir}: package.json missing or unreadable`);
44686
+ }
44687
+ if (d.markerLine === null) {
44688
+ problems.push(`${d.dir}: plugin/border.ts missing or unreadable in the cached package`);
44689
+ } else if (d.markerVersion === null) {
44690
+ problems.push(`${d.dir}: plugin/border.ts first line lacks the marker '${PLUGIN_MARKER}'`);
44691
+ } else if (d.packageVersion !== null && d.markerVersion !== d.packageVersion) {
44692
+ problems.push(`${d.dir}: marker says ${d.markerVersion}, package.json says ${d.packageVersion}`);
44693
+ }
44694
+ if (pins.has(d.tag) && d.packageVersion !== null && d.packageVersion !== d.tag) {
44695
+ problems.push(`${d.dir}: cached under exact spec '${d.tag}' but the package reports ${d.packageVersion}`);
44696
+ }
44697
+ }
44698
+ if (problems.length > 0) {
44699
+ return { id, verdict: "FAIL", detail: problems.join("; ") };
44700
+ }
44701
+ return {
44702
+ id,
44703
+ verdict: "PASS",
44704
+ detail: `cache self-consistent: ${cacheDirs.map((d) => `${d.tag}->${d.packageVersion ?? "?"}`).join(", ")}`
44705
+ };
44706
+ }
44707
+
44708
+ // src/opencode/aspects/dualRoute.ts
44709
+ function verifyDualRoute(facts) {
44710
+ const id = "dual-route";
44711
+ const { configs, routeAInstalled, routeAPlugin, pkg } = facts;
44712
+ const errored = configs.filter((c) => c.status === "error").map((c) => c.path);
44713
+ if (errored.length > 0) {
44714
+ return { id, verdict: "CANNOT", detail: `configuration unreadable (${errored.join(", ")}) \u2014 Route B presence cannot be counted` };
44715
+ }
44716
+ const declared = configs.flatMap((c) => [...c.specs]);
44717
+ const hasB = declared.length > 0;
44718
+ if (routeAInstalled && hasB) {
44719
+ return {
44720
+ id,
44721
+ verdict: "FAIL",
44722
+ detail: `both routes registered: Route A drop at ${routeAPlugin} AND Route B declaration ${declared.map((d) => `'${d}'`).join(", ")} \u2014 pick exactly one route (the dual-registration window blueprint \xA77 forbids)`
44723
+ };
44724
+ }
44725
+ if (routeAInstalled) {
44726
+ return { id, verdict: "PASS", detail: `Route A only: drop at ${routeAPlugin}, no ${pkg} config declaration` };
44727
+ }
44728
+ if (hasB) {
44729
+ return { id, verdict: "PASS", detail: `Route B only: ${declared.map((d) => `'${d}'`).join(", ")}, no file drop at ${routeAPlugin}` };
44730
+ }
44731
+ return { id, verdict: "PASS", detail: "neither route registered \u2014 no border plugin load claimed on the V1 surface" };
44732
+ }
44733
+
44734
+ // src/opencode/aspects/identity.ts
44735
+ import { execFile } from "node:child_process";
44736
+ import { existsSync as existsSync20 } from "node:fs";
44737
+ var HANDSHAKE_ARGV = ["--help"];
44738
+ var HANDSHAKE_BANNER_PREFIX = "border";
44739
+ var HANDSHAKE_USAGE_NEEDLE = "usage: border";
44740
+ var HANDSHAKE_TIMEOUT_MS = 2e4;
44741
+ var MAX_OUTPUT_BYTES = 64 * 1024;
44742
+ var defaultInspectRunner = (target, argv) => new Promise((resolve20) => {
44743
+ execFile(
44744
+ target.bin,
44745
+ [...target.prefix, ...argv],
44746
+ { timeout: HANDSHAKE_TIMEOUT_MS, maxBuffer: MAX_OUTPUT_BYTES, shell: false },
44747
+ (error61, stdout, stderr) => {
44748
+ const out = stdout.slice(0, MAX_OUTPUT_BYTES);
44749
+ const err = stderr.slice(0, MAX_OUTPUT_BYTES);
44750
+ if (error61 === null) {
44751
+ resolve20({ status: 0, stdout: out, stderr: err });
44752
+ return;
44753
+ }
44754
+ const failure = error61;
44755
+ if (failure.code === "ENOENT" || failure.code === "EACCES" || failure.code === "ENOEXEC") {
44756
+ resolve20({ status: null, stdout: out, stderr: err });
44757
+ return;
44758
+ }
44759
+ const status = typeof failure.code === "number" ? failure.code : failure.killed === true || failure.signal != null ? 124 : -1;
44760
+ resolve20({ status, stdout: out, stderr: err });
44761
+ }
44762
+ );
44763
+ });
44764
+ function passesHandshake(outcome) {
44765
+ if (outcome.status !== 0) return false;
44766
+ const firstLine2 = (outcome.stdout.match(/^\s*(.*)/) ?? ["", ""])[1] ?? "";
44767
+ return firstLine2.startsWith(HANDSHAKE_BANNER_PREFIX) && outcome.stdout.includes(HANDSHAKE_USAGE_NEEDLE);
44768
+ }
44769
+ function identityCandidates(env2, distEntry) {
44770
+ const configured = env2["BORDER_BIN"];
44771
+ if (configured !== void 0 && configured.length > 0) {
44772
+ return [{ bin: configured, prefix: [], label: `BORDER_BIN=${configured}` }];
44773
+ }
44774
+ const candidates = [];
44775
+ if (existsSync20(distEntry)) {
44776
+ candidates.push({ bin: distEntry, prefix: [], label: "packaged dist/index.js (shebang exec)" });
44777
+ candidates.push({ bin: "node", prefix: [distEntry], label: "packaged dist/index.js under PATH node" });
44778
+ }
44779
+ candidates.push({ bin: "border", prefix: [], label: "border on PATH" });
44780
+ return candidates;
44781
+ }
44782
+ async function verifyIdentity(deps) {
44783
+ const id = "identity-handshake";
44784
+ const runner = deps.runner ?? defaultInspectRunner;
44785
+ const candidates = identityCandidates(deps.env, deps.distEntry);
44786
+ const reasons = [];
44787
+ let anySpawned = false;
44788
+ for (const candidate of candidates) {
44789
+ const probe = await runner(candidate, HANDSHAKE_ARGV);
44790
+ if (passesHandshake(probe)) {
44791
+ return { id, verdict: "PASS", detail: `${candidate.label} passed the identity handshake` };
44792
+ }
44793
+ if (probe.status !== null) {
44794
+ anySpawned = true;
44795
+ reasons.push(`${candidate.label}: spawned but --help did not answer as border (impostor?)`);
44796
+ } else {
44797
+ reasons.push(`${candidate.label}: unspawnable (ENOENT/EACCES/ENOEXEC)`);
44798
+ }
44799
+ }
44800
+ if (anySpawned) {
44801
+ return { id, verdict: "FAIL", detail: `a border binary was reachable but failed the identity handshake \u2014 impersonation shape: ${reasons.join("; ")}` };
44802
+ }
44803
+ return { id, verdict: "CANNOT", detail: `no border binary reachable \u2014 tried: ${reasons.join("; ")}` };
44804
+ }
44805
+
44806
+ // src/opencode/aspects/routeA.ts
44807
+ function verifyRouteAParity(facts) {
44808
+ const id = "route-a-parity";
44809
+ const { routeAPlugin, routeAInstalled } = facts;
44810
+ if (!routeAInstalled) {
44811
+ return { id, verdict: "PASS", detail: `${routeAPlugin}: absent \u2014 Route A not installed, nothing to be stale` };
44812
+ }
44813
+ if (facts.packagedPlugin === null) {
44814
+ return { id, verdict: "CANNOT", detail: "the packaged plugin asset is unreadable \u2014 the border install itself looks broken, parity cannot be judged" };
44815
+ }
44816
+ const installed = facts.routeABytes;
44817
+ if (installed === null) {
44818
+ return { id, verdict: "CANNOT", detail: `${routeAPlugin}: present but unreadable \u2014 parity cannot be judged` };
44819
+ }
44820
+ const installedSha = sha256Hex3(installed);
44821
+ const packagedSha = sha256Hex3(facts.packagedPlugin);
44822
+ if (installedSha === packagedSha) {
44823
+ return { id, verdict: "PASS", detail: `${routeAPlugin}: up-to-date installed=${installedSha} packaged=${packagedSha}` };
44824
+ }
44825
+ const ours = firstLine(installed).startsWith(PLUGIN_MARKER);
44826
+ const shape = ours ? "outdated (marker ours, bytes differ)" : "foreign (first line lacks the marker)";
44827
+ return { id, verdict: "FAIL", detail: `${routeAPlugin}: ${shape} installed=${installedSha} packaged=${packagedSha}` };
44828
+ }
44829
+
44830
+ // src/opencode/aspects/scope.ts
44831
+ var SCOPE_ID = "scope";
44832
+ var SCOPE_NAME = "v1-load-surface";
44833
+ var SCOPE_UNCOVERED = [
44834
+ "V2 project-local .opencode/plugins/",
44835
+ "V2 npm-prefix/node_modules installs",
44836
+ "V2 lazy activation state"
44837
+ ];
44838
+ function verifyScope() {
44839
+ return {
44840
+ id: SCOPE_ID,
44841
+ verdict: "PASS",
44842
+ detail: `verdict scope = ${SCOPE_NAME}; NOT covered (declared blind, separate ledger item): ${SCOPE_UNCOVERED.join(", ")}`
44843
+ };
44844
+ }
44845
+
44846
+ // src/opencode/registry.ts
44847
+ var isRecord3 = (v) => typeof v === "object" && v !== null;
44848
+ var defaultRegistryFetcher = async (url2, init) => {
44849
+ if (!/^https?:\/\//.test(url2)) throw new Error(`border opencode inspect refuses non-http(s) URL: ${url2}`);
44850
+ const res = await fetch(url2, {
44851
+ redirect: "follow",
44852
+ signal: init.signal,
44853
+ headers: { "user-agent": "border-customs (+https://github.com/TachikomaGundam/Border-Customs; border opencode inspect)" }
44854
+ });
44855
+ const raw = res.headers.get("content-length");
44856
+ const parsed = raw === null ? Number.NaN : Number(raw);
44857
+ return {
44858
+ status: res.status,
44859
+ contentLength: Number.isFinite(parsed) && parsed >= 0 ? parsed : null,
44860
+ chunks: () => {
44861
+ if (res.body === null) {
44862
+ return {
44863
+ async *[Symbol.asyncIterator]() {
44864
+ }
44865
+ };
44866
+ }
44867
+ return res.body;
44868
+ }
44869
+ };
44870
+ };
44871
+ function npmTagUrl(pkg, tag, base = DEFAULT_REGISTRY_BASE) {
44872
+ const enc = pkg.startsWith("@") ? pkg.replace("/", "%2F") : encodeURIComponent(pkg);
44873
+ return `${base}/${enc}/${encodeURIComponent(tag)}`;
44874
+ }
44875
+ var REGISTRY_BODY_CAP = 8 * 1024 * 1024;
44876
+ async function fetchRegistryVersion(fetcher, url2, timeoutMs) {
44877
+ const res = await fetcher(url2, { signal: AbortSignal.timeout(timeoutMs) });
44878
+ if (res.status !== 200) throw new Error(`HTTP ${String(res.status)} from ${url2}`);
44879
+ const parts = [];
44880
+ let total = 0;
44881
+ for await (const chunk of res.chunks()) {
44882
+ total += chunk.byteLength;
44883
+ if (total > REGISTRY_BODY_CAP) throw new Error(`${url2} exceeds the ${String(REGISTRY_BODY_CAP)} B metadata cap`);
44884
+ parts.push(Buffer.from(chunk));
44885
+ }
44886
+ let parsed;
44887
+ try {
44888
+ parsed = JSON.parse(Buffer.concat(parts).toString("utf8"));
44889
+ } catch {
44890
+ throw new Error(`metadata response from ${url2} is not valid JSON`);
44891
+ }
44892
+ if (!isRecord3(parsed) || typeof parsed["version"] !== "string" || parsed["version"].length === 0) {
44893
+ throw new Error(`metadata response from ${url2} carries no "version" string`);
44894
+ }
44895
+ return parsed["version"];
44896
+ }
44897
+
44898
+ // src/opencode/aspects/specFreshness.ts
44899
+ var isExactVersion2 = (spec) => /^\d+\.\d+\.\d+([-+].*)?$/.test(spec);
44900
+ async function verifySpecFreshness(facts, deps = {}) {
44901
+ const id = "spec-freshness";
44902
+ const { configs, cacheDirs, pkg } = facts;
44903
+ const errored = configs.filter((c) => c.status === "error").map((c) => c.path);
44904
+ if (errored.length > 0) {
44905
+ return { id, verdict: "CANNOT", detail: `configuration unreadable (${errored.join(", ")}) \u2014 the expected version cannot be determined` };
44906
+ }
44907
+ const specs = [...new Set(configs.flatMap((c) => [...c.specs]).map((s) => splitNpmSpec(s).spec).filter((s) => s !== null))];
44908
+ if (specs.length === 0) {
44909
+ return { id, verdict: "PASS", detail: `no ${pkg} spec declared in opencode config \u2014 nothing to be fresh` };
44910
+ }
44911
+ const expected = /* @__PURE__ */ new Map();
44912
+ const fetcher = deps.fetcher ?? defaultRegistryFetcher;
44913
+ const timeoutMs = deps.timeoutMs ?? REGISTRY_TIMEOUT_MS2;
44914
+ for (const spec of specs) {
44915
+ if (isExactVersion2(spec)) {
44916
+ expected.set(spec, spec);
44917
+ continue;
44918
+ }
44919
+ try {
44920
+ expected.set(spec, await fetchRegistryVersion(fetcher, npmTagUrl(pkg, spec, deps.registryBase), timeoutMs));
44921
+ } catch (err) {
44922
+ const message = err instanceof Error ? err.message : String(err);
44923
+ return { id, verdict: "CANNOT", detail: `registry unreachable for spec '${pkg}@${spec}': ${message} \u2014 border never reads silence as fresh` };
44924
+ }
44925
+ }
44926
+ if (cacheDirs.length === 0) {
44927
+ return { id, verdict: "PASS", detail: `nothing cached to compare (see cache-version); spec resolves to ${[...new Set(expected.values())].join(", ")}` };
44928
+ }
44929
+ const wanted = new Set(expected.values());
44930
+ const judged = cacheDirs.filter((d) => d.packageVersion !== null);
44931
+ const stale = judged.filter((d) => !wanted.has(d.packageVersion));
44932
+ if (judged.length > 0 && stale.length > 0) {
44933
+ return {
44934
+ id,
44935
+ verdict: "FAIL",
44936
+ detail: `cache holds ${stale.map((d) => `${d.tag}->${d.packageVersion}`).join(",")}, spec expects ${[...wanted].join(",")}`
44937
+ };
44938
+ }
44939
+ return { id, verdict: "PASS", detail: `cache matches spec: ${[...wanted].join(",")}` };
44940
+ }
44941
+
44942
+ // src/opencode/inspect.ts
44943
+ function aggregateVerdict(aspects) {
44944
+ if (aspects.some((a) => a.verdict === "FAIL")) return "FAIL";
44945
+ if (aspects.some((a) => a.verdict === "CANNOT")) return "CANNOT";
44946
+ return "PASS";
44947
+ }
44948
+ function inspectExit(verdict) {
44949
+ switch (verdict) {
44950
+ case "FAIL":
44951
+ return EXIT_BLOCKED;
44952
+ case "CANNOT":
44953
+ return EXIT_ERROR;
44954
+ case "PASS":
44955
+ return EXIT_PASS;
44956
+ }
44957
+ }
44958
+ function readPackagedPlugin(path4) {
44959
+ try {
44960
+ return readFileSync19(path4);
44961
+ } catch {
44962
+ return null;
44963
+ }
44964
+ }
44965
+ function gatherFacts(env2, cwd, deps) {
44966
+ const pkg = deps.package ?? BORDER_PACKAGE;
44967
+ const pluginDrop = routeAPluginPath(env2);
44968
+ const cacheRoot = deps.cacheRoot ?? opencodeCacheRoot(env2);
44969
+ const globalPath = deps.globalConfigPath ?? join30(opencodeConfigRoot(env2), "opencode", "opencode.jsonc");
44970
+ const routeAInstalled = existsSync21(pluginDrop);
44971
+ return {
44972
+ pkg,
44973
+ env: env2,
44974
+ cacheRoot,
44975
+ cacheDirs: listCacheDirs(cacheRoot, pkg),
44976
+ configs: [readPluginDeclarations(globalPath, pkg), readPluginDeclarations(join30(cwd, "opencode.jsonc"), pkg)],
44977
+ packagedPlugin: readPackagedPlugin(packagedAssetPath(import.meta.url, "border.ts")),
44978
+ routeAPlugin: pluginDrop,
44979
+ routeAInstalled,
44980
+ routeABytes: routeAInstalled ? readPackagedPlugin(pluginDrop) : null
44981
+ };
44982
+ }
44983
+ async function runInspectCore(ctx, deps = {}) {
44984
+ const facts = gatherFacts(ctx.env, ctx.cwd, deps);
44985
+ const aspects = [
44986
+ verifyCacheVersion(facts),
44987
+ await verifySpecFreshness(facts, {
44988
+ ...deps.registryFetcher !== void 0 ? { fetcher: deps.registryFetcher } : {},
44989
+ ...deps.registryBase !== void 0 ? { registryBase: deps.registryBase } : {}
44990
+ }),
44991
+ await verifyIdentity({
44992
+ ...deps.runner !== void 0 ? { runner: deps.runner } : {},
44993
+ distEntry: deps.distEntry ?? packagedDistEntry(import.meta.url),
44994
+ env: facts.env
44995
+ }),
44996
+ verifyRouteAParity(facts),
44997
+ verifyDualRoute(facts),
44998
+ verifyScope()
44999
+ ];
45000
+ const verdict = aggregateVerdict(aspects);
45001
+ const report = { schemaVersion: 1, scope: "v1", verdict, aspects };
45002
+ if (ctx.flags.json) {
45003
+ ctx.stdout(JSON.stringify(report));
45004
+ } else {
45005
+ ctx.stdout(`border opencode inspect [${report.scope}] ${verdict}`);
45006
+ for (const a of aspects) {
45007
+ ctx.stdout(` ${a.verdict.padEnd(6)} ${a.id}: ${a.detail}`);
45008
+ }
45009
+ }
45010
+ return inspectExit(verdict);
45011
+ }
45012
+
45013
+ // src/commands/opencode.ts
45014
+ function resolvePluginAsset(moduleUrl, fileName) {
45015
+ const here = dirname16(fileURLToPath6(moduleUrl));
45016
+ const candidates = [
45017
+ join31(here, "..", "plugin", fileName),
45018
+ // dist mode (bundled): <root>/plugin/<f>
45019
+ join31(here, "..", "..", "plugin", fileName)
45020
+ // src mode: <root>/plugin/<f>
45021
+ ];
45022
+ return candidates.find((p) => existsSync22(p)) ?? candidates[1];
45023
+ }
45024
+ function targetsFor(env2) {
45025
+ const root = join31(opencodeConfigRoot(env2), "opencode");
45026
+ return [
45027
+ { fileName: "border.ts", destination: join31(root, "plugins", "border.ts"), marker: PLUGIN_MARKER },
45028
+ { fileName: "border-command.md", destination: join31(root, "commands", "border.md"), marker: COMMAND_MARKER }
45029
+ ];
44522
45030
  }
44523
45031
  function carriesMarker(bytes, marker) {
44524
45032
  return firstLine(bytes).startsWith(marker);
@@ -44530,7 +45038,7 @@ function fail(ctx, message) {
44530
45038
  function readPackagedAsset(ctx, fileName) {
44531
45039
  const source = resolvePluginAsset(import.meta.url, fileName);
44532
45040
  try {
44533
- return readFileSync18(source);
45041
+ return readFileSync20(source);
44534
45042
  } catch {
44535
45043
  ctx.stderr(`border: opencode: packaged asset is missing (${source})`);
44536
45044
  ctx.stderr("border: opencode: the border install looks broken \u2014 `npm i -g border-customs` or a fresh checkout should carry plugin/.");
@@ -44539,7 +45047,7 @@ function readPackagedAsset(ctx, fileName) {
44539
45047
  }
44540
45048
  function readTarget(destination) {
44541
45049
  try {
44542
- return readFileSync18(destination);
45050
+ return readFileSync20(destination);
44543
45051
  } catch {
44544
45052
  return null;
44545
45053
  }
@@ -44606,7 +45114,7 @@ function opencodeInstall(ctx) {
44606
45114
  ctx.stdout(`${target.destination}: up to date`);
44607
45115
  continue;
44608
45116
  }
44609
- mkdirSync12(dirname15(target.destination), { recursive: true });
45117
+ mkdirSync12(dirname16(target.destination), { recursive: true });
44610
45118
  writeFileSync8(target.destination, packaged);
44611
45119
  ctx.stdout(`${target.destination}: ${state === "outdated" ? "updated" : "installed"}`);
44612
45120
  }
@@ -44620,8 +45128,8 @@ function opencodeStatus(ctx) {
44620
45128
  for (const { target, packaged } of prepared) {
44621
45129
  const current = readTarget(target.destination);
44622
45130
  const state = classify(target, current, packaged);
44623
- const installedSha = current === null ? "-" : sha2563(current);
44624
- ctx.stdout(`${target.destination}: ${state} installed=${installedSha} packaged=${sha2563(packaged)}`);
45131
+ const installedSha = current === null ? "-" : sha256Hex3(current);
45132
+ ctx.stdout(`${target.destination}: ${state} installed=${installedSha} packaged=${sha256Hex3(packaged)}`);
44625
45133
  }
44626
45134
  return EXIT_PASS;
44627
45135
  }
@@ -44648,23 +45156,27 @@ function opencodeUninstall(ctx) {
44648
45156
  return EXIT_PASS;
44649
45157
  }
44650
45158
  function runOpencode(ctx) {
44651
- const [sub, ...rest] = ctx.positionals;
44652
- if (rest.length > 0) {
44653
- throw new UnknownArgError(
44654
- `opencode ${sub ?? "<none>"}: unexpected argument '${rest[0] ?? ""}' \u2014 usage: border opencode install | status | uninstall`
44655
- );
44656
- }
45159
+ const [sub, ...restAll] = ctx.positionals;
45160
+ const rest = restAll.filter((arg) => arg !== "--json");
45161
+ const USAGE = "usage: border opencode install | status | uninstall | inspect";
44657
45162
  switch (sub) {
44658
45163
  case "install":
44659
- return opencodeInstall(ctx);
44660
45164
  case "status":
44661
- return opencodeStatus(ctx);
44662
45165
  case "uninstall":
44663
- return opencodeUninstall(ctx);
45166
+ case "inspect": {
45167
+ if (rest.length > 0) {
45168
+ throw new UnknownArgError(`opencode ${sub}: unexpected argument '${rest[0] ?? ""}' \u2014 ${USAGE}`);
45169
+ }
45170
+ const table2 = {
45171
+ install: opencodeInstall,
45172
+ status: opencodeStatus,
45173
+ uninstall: opencodeUninstall,
45174
+ inspect: runInspectCore
45175
+ };
45176
+ return table2[sub](ctx);
45177
+ }
44664
45178
  default:
44665
- throw new UnknownArgError(
44666
- `opencode: unknown subcommand '${sub ?? "<none>"}' \u2014 usage: border opencode install | status | uninstall`
44667
- );
45179
+ throw new UnknownArgError(`opencode: unknown subcommand '${sub ?? "<none>"}' \u2014 ${USAGE}`);
44668
45180
  }
44669
45181
  }
44670
45182
 
@@ -44768,7 +45280,7 @@ function confirmRemoteBranch(i) {
44768
45280
  import { spawnSync as spawnSync18 } from "node:child_process";
44769
45281
  import { mkdtempSync as mkdtempSync6, rmSync as rmSync14 } from "node:fs";
44770
45282
  import { tmpdir as tmpdir4 } from "node:os";
44771
- import { dirname as dirname16, join as join29, resolve as resolve18 } from "node:path";
45283
+ import { dirname as dirname17, join as join32, resolve as resolve18 } from "node:path";
44772
45284
  var LANDINGS_REF_PREFIX = "refs/border-landings/";
44773
45285
  async function runLandingVerification(o) {
44774
45286
  if (o.legs.length === 0) return null;
@@ -44818,7 +45330,7 @@ function gitPlain(repoDir, env2, args) {
44818
45330
  for (const [name, value] of Object.entries(env2)) {
44819
45331
  if (!name.startsWith("GIT_")) clean[name] = value;
44820
45332
  }
44821
- clean.GIT_CEILING_DIRECTORIES = dirname16(resolve18(repoDir));
45333
+ clean.GIT_CEILING_DIRECTORIES = dirname17(resolve18(repoDir));
44822
45334
  clean.GIT_OPTIONAL_LOCKS = "0";
44823
45335
  clean.GIT_TERMINAL_PROMPT = "0";
44824
45336
  const r = spawnSync18("git", ["-C", resolve18(repoDir), ...args], { encoding: "utf8", env: clean, timeout: 55e3, maxBuffer: 32 * 1024 * 1024 });
@@ -44843,7 +45355,7 @@ async function defaultScanRemoteTip(o, remote, branch, remoteSha) {
44843
45355
  ownedTempRef = landingRef;
44844
45356
  rev = git.run(["rev-parse", landingRef]).trim();
44845
45357
  }
44846
- const treeRoot = mkdtempSync6(join29(tmpdir4(), "border-landing-tree-"));
45358
+ const treeRoot = mkdtempSync6(join32(tmpdir4(), "border-landing-tree-"));
44847
45359
  try {
44848
45360
  return await scanTipTree(git, treeRoot, rev, o.env);
44849
45361
  } finally {
@@ -45052,14 +45564,14 @@ async function executeYesPush(ctx, loaded) {
45052
45564
  import { mkdtempSync as mkdtempSync7, rmSync as rmSync15, writeFileSync as writeFileSync9 } from "node:fs";
45053
45565
  import { randomBytes } from "node:crypto";
45054
45566
  import { tmpdir as tmpdir5 } from "node:os";
45055
- import { join as join30 } from "node:path";
45567
+ import { join as join33 } from "node:path";
45056
45568
 
45057
45569
  // src/scan/fetch.ts
45058
45570
  import { createHash as createHash18 } from "node:crypto";
45059
45571
  import { basename as basename5 } from "node:path";
45060
45572
  var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
45061
45573
  var SCAN_REQUEST_TIMEOUT_MS = 6e4;
45062
- var SCAN_USER_AGENT = "border-customs/0.7.1 (+https://github.com/sumteclab/border; border scan)";
45574
+ var SCAN_USER_AGENT = "border-customs/0.8.0 (+https://github.com/sumteclab/border; border scan)";
45063
45575
  var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
45064
45576
  var defaultScanFetcher = async (url2, init) => {
45065
45577
  if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
@@ -45113,7 +45625,7 @@ async function fetchJson(fetcher, url2, timeoutMs) {
45113
45625
  fail2(`metadata response from ${url2} is not valid JSON`);
45114
45626
  }
45115
45627
  }
45116
- var isRecord2 = (v) => typeof v === "object" && v !== null;
45628
+ var isRecord4 = (v) => typeof v === "object" && v !== null;
45117
45629
  function channelBaseUrl(eco) {
45118
45630
  const ch = publishChannels().find((c) => c.id === eco);
45119
45631
  if (ch === void 0 || ch.defaultUrl.length === 0) {
@@ -45144,7 +45656,7 @@ async function fetchArtifact(spec, o) {
45144
45656
  switch (spec.ecosystem) {
45145
45657
  case "npm": {
45146
45658
  const meta3 = await fetchJson(o.fetcher, npmMetadataUrl(spec.name, spec.version), timeoutMs);
45147
- const dist = isRecord2(meta3) && isRecord2(meta3["dist"]) ? meta3["dist"] : {};
45659
+ const dist = isRecord4(meta3) && isRecord4(meta3["dist"]) ? meta3["dist"] : {};
45148
45660
  const tarball = dist["tarball"];
45149
45661
  if (typeof tarball !== "string" || tarball.length === 0) fail2(`npm metadata for ${label} has no dist.tarball`);
45150
45662
  if (!/^https?:\/\//.test(tarball)) fail2(`npm dist.tarball for ${label} is not an http(s) URL`);
@@ -45155,8 +45667,8 @@ async function fetchArtifact(spec, o) {
45155
45667
  }
45156
45668
  case "pypi": {
45157
45669
  const meta3 = await fetchJson(o.fetcher, pypiMetadataUrl(spec.name, spec.version), timeoutMs);
45158
- const urls = isRecord2(meta3) && Array.isArray(meta3["urls"]) ? meta3["urls"] : [];
45159
- const files = urls.filter(isRecord2);
45670
+ const urls = isRecord4(meta3) && Array.isArray(meta3["urls"]) ? meta3["urls"] : [];
45671
+ const files = urls.filter(isRecord4);
45160
45672
  const sdists = files.filter((f) => f["packagetype"] === "sdist");
45161
45673
  const sdist = sdists.find((f) => typeof f["filename"] === "string" && f["filename"].endsWith(".tar.gz"));
45162
45674
  if (sdist === void 0) {
@@ -45184,7 +45696,7 @@ async function fetchArtifact(spec, o) {
45184
45696
  }
45185
45697
  }
45186
45698
  }
45187
- function sha256Hex3(data) {
45699
+ function sha256Hex4(data) {
45188
45700
  return createHash18("sha256").update(data).digest("hex");
45189
45701
  }
45190
45702
 
@@ -45228,7 +45740,7 @@ function parseScanSpec(raw) {
45228
45740
  }
45229
45741
 
45230
45742
  // src/roundtrip/localArtifact.ts
45231
- import { createReadStream, existsSync as existsSync19, statSync as statSync3 } from "node:fs";
45743
+ import { createReadStream, existsSync as existsSync23, statSync as statSync3 } from "node:fs";
45232
45744
  import { createHash as createHash19 } from "node:crypto";
45233
45745
  import { basename as basename6, resolve as resolve19 } from "node:path";
45234
45746
  var EXT_TABLE = /* @__PURE__ */ new Map([
@@ -45277,7 +45789,7 @@ var GRAMMAR_HINT = {
45277
45789
  };
45278
45790
  function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
45279
45791
  const abs = resolve19(cwd, raw);
45280
- if (existsSync19(abs)) {
45792
+ if (existsSync23(abs)) {
45281
45793
  const st = statSync3(abs);
45282
45794
  if (st.isDirectory()) {
45283
45795
  bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
@@ -45636,7 +46148,7 @@ function classifyResidue(label, diff, attribution = null) {
45636
46148
 
45637
46149
  // src/roundtrip/orchestrate.ts
45638
46150
  function containerNameFor(label, rand = randomBytes(3).toString("hex")) {
45639
- return `rt-${sha256Hex3(label).slice(0, 8)}-${rand}`;
46151
+ return `rt-${sha256Hex4(label).slice(0, 8)}-${rand}`;
45640
46152
  }
45641
46153
  var shArgv = (name) => {
45642
46154
  if (!/^[A-Za-z0-9@._+/-]+$/.test(name)) {
@@ -45757,7 +46269,7 @@ function runPipeline2(exec, profile, spec, label, hostPath, container, closure,
45757
46269
  return {
45758
46270
  findings: classifyResidue(label, diffManifests(m1, m3), attribution),
45759
46271
  installDelta,
45760
- baselineDigest: sha256Hex3([...m1.keys()].join("\n"))
46272
+ baselineDigest: sha256Hex4([...m1.keys()].join("\n"))
45761
46273
  };
45762
46274
  } finally {
45763
46275
  try {
@@ -45791,12 +46303,12 @@ async function runRoundtripCore(ctx, deps = {}) {
45791
46303
  ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
45792
46304
  return EXIT_ERROR;
45793
46305
  }
45794
- const baseDir = mkdtempSync7(join30(tmpdir5(), "border-roundtrip-"));
46306
+ const baseDir = mkdtempSync7(join33(tmpdir5(), "border-roundtrip-"));
45795
46307
  try {
45796
46308
  const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
45797
46309
  const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
45798
- const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex3(bytes);
45799
- const hostPath = join30(baseDir, "artifact");
46310
+ const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex4(bytes);
46311
+ const hostPath = join33(baseDir, "artifact");
45800
46312
  writeFileSync9(hostPath, bytes);
45801
46313
  const container = containerNameFor(label);
45802
46314
  const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
@@ -45804,12 +46316,12 @@ async function runRoundtripCore(ctx, deps = {}) {
45804
46316
  const verdict = computeVerdict(findings);
45805
46317
  const report = {
45806
46318
  schemaVersion: 1,
45807
- key: sha256Hex3(`roundtrip:${label}:${baselineDigest}:${verdict}`),
46319
+ key: sha256Hex4(`roundtrip:${label}:${baselineDigest}:${verdict}`),
45808
46320
  head: baselineDigest.slice(0, 40),
45809
46321
  dirty: false,
45810
46322
  exposureSet: [label],
45811
46323
  refSet: [],
45812
- rulesHash: sha256Hex3(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
46324
+ rulesHash: sha256Hex4(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
45813
46325
  verdict,
45814
46326
  counts: countFindings(findings),
45815
46327
  findings,
@@ -45899,7 +46411,7 @@ var runRoundtrip = (ctx) => runRoundtripCore(ctx);
45899
46411
  // src/scan/scan.ts
45900
46412
  import { mkdtempSync as mkdtempSync8 } from "node:fs";
45901
46413
  import { tmpdir as tmpdir6 } from "node:os";
45902
- import { join as join32 } from "node:path";
46414
+ import { join as join35 } from "node:path";
45903
46415
 
45904
46416
  // src/scan/config.ts
45905
46417
  function buildScanConfigYaml(eco) {
@@ -45926,8 +46438,8 @@ function synthesizeScanConfig(eco) {
45926
46438
  }
45927
46439
 
45928
46440
  // src/scan/materialize.ts
45929
- import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync10, readFileSync as readFileSync19, rmSync as rmSync16, writeFileSync as writeFileSync10 } from "node:fs";
45930
- import { join as join31 } from "node:path";
46441
+ import { existsSync as existsSync24, mkdirSync as mkdirSync13, readdirSync as readdirSync11, readFileSync as readFileSync21, rmSync as rmSync16, writeFileSync as writeFileSync10 } from "node:fs";
46442
+ import { join as join34 } from "node:path";
45931
46443
  var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
45932
46444
  var fail3 = (why) => {
45933
46445
  throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
@@ -45941,15 +46453,15 @@ function extractFlat(archive, destDir) {
45941
46453
  }
45942
46454
  }
45943
46455
  function unwrapSingleDir(dir) {
45944
- const entries = readdirSync10(dir, { withFileTypes: true });
46456
+ const entries = readdirSync11(dir, { withFileTypes: true });
45945
46457
  const dirs = entries.filter((e) => e.isDirectory());
45946
46458
  if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
45947
- return join31(dir, dirs[0].name);
46459
+ return join34(dir, dirs[0].name);
45948
46460
  }
45949
46461
  return dir;
45950
46462
  }
45951
46463
  function requireManifest(repoDir, ecosystem, candidates) {
45952
- if (!candidates.some((rel) => existsSync20(join31(repoDir, rel)))) {
46464
+ if (!candidates.some((rel) => existsSync24(join34(repoDir, rel)))) {
45953
46465
  fail3(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
45954
46466
  }
45955
46467
  }
@@ -45959,18 +46471,18 @@ function materializePackage(o) {
45959
46471
  `);
45960
46472
  });
45961
46473
  const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
45962
- const archive = join31(o.baseDir, safe);
46474
+ const archive = join34(o.baseDir, safe);
45963
46475
  writeFileSync10(archive, o.bytes);
45964
- const extractDir = join31(o.baseDir, "extract");
46476
+ const extractDir = join34(o.baseDir, "extract");
45965
46477
  mkdirSync13(extractDir, { recursive: true });
45966
46478
  extractFlat(archive, extractDir);
45967
46479
  let repoDir = unwrapSingleDir(extractDir);
45968
46480
  if (o.ecosystem === "rubygems") {
45969
46481
  const outer = repoDir;
45970
46482
  const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
45971
- const inner = dataCandidates[0] ?? join31(outer, "data.tar.gz");
45972
- if (!existsSync20(inner)) fail3(".gem archive contains no data.tar.gz");
45973
- const gemTree = join31(o.baseDir, "gem-tree");
46483
+ const inner = dataCandidates[0] ?? join34(outer, "data.tar.gz");
46484
+ if (!existsSync24(inner)) fail3(".gem archive contains no data.tar.gz");
46485
+ const gemTree = join34(o.baseDir, "gem-tree");
45974
46486
  mkdirSync13(gemTree, { recursive: true });
45975
46487
  extractFlat(inner, gemTree);
45976
46488
  repoDir = unwrapSingleDir(gemTree);
@@ -45981,11 +46493,11 @@ function materializePackage(o) {
45981
46493
  } else {
45982
46494
  requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
45983
46495
  for (const file2 of CRATES_ENVELOPE_FILES) {
45984
- const path4 = join31(repoDir, file2);
45985
- if (!existsSync20(path4)) continue;
45986
- const original = readFileSync19(path4);
46496
+ const path4 = join34(repoDir, file2);
46497
+ if (!existsSync24(path4)) continue;
46498
+ const original = readFileSync21(path4);
45987
46499
  rmSync16(path4, { force: true });
45988
- note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex3(original)}`);
46500
+ note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex4(original)}`);
45989
46501
  }
45990
46502
  }
45991
46503
  const env2 = o.env;
@@ -46011,7 +46523,7 @@ async function runScanCore(ctx, deps = {}) {
46011
46523
  }
46012
46524
  const cfg = synthesizeScanConfig(spec.ecosystem);
46013
46525
  const fetcher = deps.fetcher ?? defaultScanFetcher;
46014
- const baseDir = mkdtempSync8(join32(tmpdir6(), "border-scan-"));
46526
+ const baseDir = mkdtempSync8(join35(tmpdir6(), "border-scan-"));
46015
46527
  try {
46016
46528
  const { bytes, filename } = await fetchArtifact(spec, { fetcher });
46017
46529
  const env2 = { ...ctx.env };
@@ -46022,12 +46534,12 @@ async function runScanCore(ctx, deps = {}) {
46022
46534
  const verdict = computeVerdict(findings);
46023
46535
  const report = {
46024
46536
  schemaVersion: 1,
46025
- key: sha256Hex3(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
46537
+ key: sha256Hex4(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
46026
46538
  head,
46027
46539
  dirty: false,
46028
46540
  exposureSet: [`${spec.ecosystem}:${spec.name}@${spec.version}`],
46029
46541
  refSet: [],
46030
- rulesHash: sha256Hex3(buildScanConfigYaml(spec.ecosystem)),
46542
+ rulesHash: sha256Hex4(buildScanConfigYaml(spec.ecosystem)),
46031
46543
  verdict,
46032
46544
  counts: countFindings(findings),
46033
46545
  findings,
@@ -46168,7 +46680,7 @@ function usage() {
46168
46680
  " llm-ingest record provenance captured by the harness",
46169
46681
  " scan inspect a third-party package ([ecosystem:]name@version) for residue",
46170
46682
  " roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
46171
- " opencode install|status|uninstall the official opencode plugin adapter (files under $XDG_CONFIG_HOME/opencode)",
46683
+ " opencode install|status|uninstall|inspect the official opencode plugin adapter (files under $XDG_CONFIG_HOME/opencode; inspect = read-only V1 load-surface audit)",
46172
46684
  " exfil read-only exfil-lens scan of <ref|url> [ref...]: tip tree + tag notes (--deep adds reachable-history blobs + commit messages)",
46173
46685
  "",
46174
46686
  "flags:",
@@ -46311,7 +46823,7 @@ function isMainModule({ argv1, moduleUrl, realpath }) {
46311
46823
  return p;
46312
46824
  }
46313
46825
  };
46314
- return pathToFileURL(canonical(argv1)).href === pathToFileURL(canonical(fileURLToPath6(moduleUrl))).href;
46826
+ return pathToFileURL(canonical(argv1)).href === pathToFileURL(canonical(fileURLToPath7(moduleUrl))).href;
46315
46827
  }
46316
46828
  if (isMainModule({ argv1: process.argv[1], moduleUrl: import.meta.url, realpath: realpathSync.native })) {
46317
46829
  const outcome = run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
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",
@@ -5,7 +5,7 @@ User request: $ARGUMENTS
5
5
 
6
6
  Interpret the request as ONE `border` CLI invocation: the first word is the
7
7
  command (`check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`,
8
- `roundtrip`, or `--help`) and the rest are argv tokens. Call the `border` tool
8
+ `roundtrip`, `exfil`, or `--help`) and the rest are argv tokens. Call the `border` tool
9
9
  with `command` set to the first word and `extra` set to the remaining tokens,
10
10
  then report the CLI exit code (0 pass / 1 gate-blocked or partial push / 2 gate
11
11
  could not answer) and the relevant lines of its output. If no request was given,
package/plugin/border.ts CHANGED
@@ -1,4 +1,4 @@
1
- // border-opencode-plugin v0.7.1
1
+ // border-opencode-plugin v0.8.0
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
@@ -65,6 +65,7 @@ const ALLOWED_COMMANDS: readonly string[] = [
65
65
  "llm-ingest",
66
66
  "scan",
67
67
  "roundtrip",
68
+ "exfil",
68
69
  "--help",
69
70
  ];
70
71
 
@@ -230,7 +231,7 @@ User request: $ARGUMENTS
230
231
 
231
232
  Interpret the request as ONE \`border\` CLI invocation: the first word is the
232
233
  command (\`check\`, \`push\`, \`status\`, \`llm-request\`, \`llm-ingest\`, \`scan\`,
233
- \`roundtrip\`, or \`--help\`) and the rest are argv tokens. Call the \`border\` tool
234
+ \`roundtrip\`, \`exfil\`, or \`--help\`) and the rest are argv tokens. Call the \`border\` tool
234
235
  with \`command\` set to the first word and \`extra\` set to the remaining tokens,
235
236
  then report the CLI exit code (0 pass / 1 gate-blocked or partial push / 2 gate
236
237
  could not answer) and the relevant lines of its output. If no request was given,
@@ -287,7 +288,7 @@ export interface V2PluginContext {
287
288
  const TOOL_DESCRIPTION =
288
289
  "Run the border fail-closed push-gate CLI on this machine. " +
289
290
  "Pass the top-level command word in `command` (one of: check, push, status, " +
290
- "llm-request, llm-ingest, scan, roundtrip, --help) and every remaining argv " +
291
+ "llm-request, llm-ingest, scan, roundtrip, exfil, --help) and every remaining argv " +
291
292
  "token in `extra`. The call is spawned argv-only (no shell) with a 300s " +
292
293
  "timeout; the result always ends with the CLI exit code: 0 pass, 1 " +
293
294
  "gate-blocked or partial push, 2 gate could not answer. Before running your " +