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 +32 -4
- package/dist/assets/package-lock.json +2 -2
- package/dist/index.js +594 -82
- package/package.json +1 -1
- package/plugin/border-command.md +1 -1
- package/plugin/border.ts +4 -3
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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",
|
|
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:
|
|
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
|
|
40357
|
+
const sha2563 = createHash6("sha256").update(bytes).digest("hex");
|
|
40358
40358
|
const findings = [];
|
|
40359
|
-
const extractDir = join12(stateDir, "tmp",
|
|
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:
|
|
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
|
|
41999
|
+
const sha2563 = createHash11("sha256").update(bytes).digest("hex");
|
|
42000
42000
|
const findings = [];
|
|
42001
|
-
const extractDir = join18(stateDir, "tmp",
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
44509
|
-
const
|
|
44510
|
-
|
|
44511
|
-
|
|
44512
|
-
|
|
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
|
-
|
|
44516
|
-
|
|
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
|
|
44519
|
-
|
|
44520
|
-
|
|
44521
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 ? "-" :
|
|
44624
|
-
ctx.stdout(`${target.destination}: ${state} installed=${installedSha} 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, ...
|
|
44652
|
-
|
|
44653
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
45159
|
-
const files = urls.filter(
|
|
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
|
|
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
|
|
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 (
|
|
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-${
|
|
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:
|
|
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(
|
|
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 :
|
|
45799
|
-
const hostPath =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
45930
|
-
import { join as
|
|
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 =
|
|
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
|
|
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) =>
|
|
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 =
|
|
46474
|
+
const archive = join34(o.baseDir, safe);
|
|
45963
46475
|
writeFileSync10(archive, o.bytes);
|
|
45964
|
-
const extractDir =
|
|
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] ??
|
|
45972
|
-
if (!
|
|
45973
|
-
const gemTree =
|
|
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 =
|
|
45985
|
-
if (!
|
|
45986
|
-
const original =
|
|
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=${
|
|
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(
|
|
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:
|
|
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:
|
|
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(
|
|
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.
|
|
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",
|
package/plugin/border-command.md
CHANGED
|
@@ -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.
|
|
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 " +
|