border-customs 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -8
- package/dist/assets/package-lock.json +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/plugin/border.ts +110 -25
package/README.md
CHANGED
|
@@ -638,13 +638,20 @@ plugin entry ships as `exports["./server"]`):
|
|
|
638
638
|
```jsonc
|
|
639
639
|
{
|
|
640
640
|
// pin the exact version — an @latest channel hits the registry on every cold start
|
|
641
|
-
"plugin": ["border-customs@0.5.
|
|
641
|
+
"plugin": ["border-customs@0.5.1"]
|
|
642
642
|
}
|
|
643
643
|
```
|
|
644
644
|
|
|
645
645
|
No global install and no PATH entry are required for it: the plugin resolves its CLI from the
|
|
646
|
-
package's own `dist/index.js` sitting next to the plugin module
|
|
647
|
-
|
|
646
|
+
package's own `dist/index.js` sitting next to the plugin module — exec'd directly through its
|
|
647
|
+
shebang, or under `node` from `PATH` if that is not runnable — with `BORDER_BIN` as an explicit
|
|
648
|
+
override that wins alone (a broken override is a loud error, never a silent fallback) and
|
|
649
|
+
`border` on `PATH` as the file-drop route's fallback. Every candidate must first pass an
|
|
650
|
+
**identity handshake**: the adapter spawns `--help` and requires the border banner and
|
|
651
|
+
`usage: border` line before any user argv is handed over; the host binary is never used as a
|
|
652
|
+
runtime (0.5.0 spawned `process.execPath`, which inside a plugin host is opencode itself —
|
|
653
|
+
see the 0.5.1 changelog). Nothing that fails the handshake runs, and no fallback to a
|
|
654
|
+
different binary exists.
|
|
648
655
|
The alternative is the file-drop installer, `border opencode install|status|uninstall`, which
|
|
649
656
|
writes `plugins/border.ts` and `commands/border.md` into `$XDG_CONFIG_HOME/opencode` (with an
|
|
650
657
|
`HOME/.config` fallback) under marker identity: a foreign file at either path is refused with
|
|
@@ -656,10 +663,12 @@ What loads is one agent tool `border` plus a `/border` slash command. The tool s
|
|
|
656
663
|
argv-only (no shell; 300 s cap, 64 KiB per stream) restricted to the closed command list
|
|
657
664
|
`check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`, `roundtrip`, `--help`, and
|
|
658
665
|
renders the exit-code contract unchanged: 0 pass, 1 gate-blocked or partial push, 2 the gate
|
|
659
|
-
could not answer
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
666
|
+
could not answer — which since 0.5.1 also covers "no border CLI found or verified": the tool
|
|
667
|
+
answers exit 2 cannot-answer and spawns nothing else. The tool REFUSES `--yes` — a real push
|
|
668
|
+
is the human gate, terminal-only, and a bare `border push` from the session is DRY-RUN by the
|
|
669
|
+
CLI's own contract anyway. `/border` self-registers via the plugin `config` hook with `??=`, so
|
|
670
|
+
a user file command of the same name — including a Route-A `commands/border.md` — stays
|
|
671
|
+
authoritative.
|
|
663
672
|
|
|
664
673
|
**Boundary honesty.** The command allowlist is fool-proofing (UX), not a security boundary: an
|
|
665
674
|
opencode plugin runs with the opencode process's rights, and the gate's teeth are the CLI and
|
|
@@ -715,6 +724,24 @@ code.
|
|
|
715
724
|
|
|
716
725
|
## Changelog
|
|
717
726
|
|
|
727
|
+
### 0.5.1 (2026-09-24)
|
|
728
|
+
- Fix: **plugin impersonation — the `border` tool could spawn the host binary instead of the
|
|
729
|
+
gate.** On Route B the 0.5.0 adapter resolved its packaged CLI as `process.execPath +
|
|
730
|
+
dist/index.js`, but inside the opencode plugin host `process.execPath` is the opencode
|
|
731
|
+
(bun-compiled) binary — sessions saw opencode's banner for `--help` and opencode usage
|
|
732
|
+
errors for every other command, while the tool's schema still promised border's
|
|
733
|
+
fail-closed contract. The resolution order is now: `BORDER_BIN` (wins alone; a broken
|
|
734
|
+
override never silently falls back) → packaged `dist/index.js` exec'd directly via its
|
|
735
|
+
shebang, else under `PATH` `node` → `border` on `PATH`. `process.execPath` is removed from
|
|
736
|
+
the adapter, pinned by a source-level regression test, and **every candidate must pass an
|
|
737
|
+
identity handshake** (`--help` answering with border's banner + `usage: border`, exit 0,
|
|
738
|
+
20 s cap) before any user argv is spawned — failure reports `cannot-answer` at exit 2 and
|
|
739
|
+
runs nothing else (spawn failures now map to 2, not 127, aligning the tool with the CLI's
|
|
740
|
+
exit-code contract). Regression tests cover the unspawnable-override and the
|
|
741
|
+
opencode-style-impostor fixtures; the impostor logs prove user argv never reaches it.
|
|
742
|
+
Honest boundary: the handshake catches mistaken identity (host binaries, PATH accidents),
|
|
743
|
+
not a deliberately forged banner.
|
|
744
|
+
|
|
718
745
|
### 0.5.0 (2026-09-12)
|
|
719
746
|
- Add: OpenCode plugin adapter (`exports["./server"]` + `/border` + `border` tool) loadable
|
|
720
747
|
two ways with zero manual install — config-declared `"plugin": ["border-customs@0.5.0"]`
|
|
@@ -855,4 +882,4 @@ border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install
|
|
|
855
882
|
|
|
856
883
|
发布一致性(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 失效。
|
|
857
884
|
|
|
858
|
-
OpenCode 插件适配层(0.5.0):零手工装载两条路线——主路是在 `opencode.jsonc` 声明 `"plugin": ["border-customs@0.5.0"]`(建议钉具体版本,@latest 每次冷启动都打注册表),opencode 自行下载构件,插件入口走 `exports["./server"]`;插件自带 CLI 解析,顺序为 `BORDER_BIN` 覆盖
|
|
885
|
+
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:工具与命令在启动时扫描。
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "border-customs",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "border-customs",
|
|
9
|
-
"version": "0.5.
|
|
9
|
+
"version": "0.5.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": "^1.17.4",
|
package/dist/index.js
CHANGED
|
@@ -44162,7 +44162,7 @@ import { createHash as createHash16 } from "node:crypto";
|
|
|
44162
44162
|
import { basename as basename5 } from "node:path";
|
|
44163
44163
|
var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
44164
44164
|
var SCAN_REQUEST_TIMEOUT_MS = 6e4;
|
|
44165
|
-
var SCAN_USER_AGENT = "border-customs/0.5.
|
|
44165
|
+
var SCAN_USER_AGENT = "border-customs/0.5.1 (+https://github.com/sumteclab/border; border scan)";
|
|
44166
44166
|
var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
|
|
44167
44167
|
var defaultScanFetcher = async (url2, init) => {
|
|
44168
44168
|
if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "border-customs",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"//": "publint is a runtime dep (exact pin): the npm-stage engine is invoked via bin resolution against the installed layout, so consumers get the publint-fail leg out of the box; the dev suite resolves the same node_modules/.bin/publint — no devDependencies entry is needed for it to stay green.",
|
|
5
5
|
"description": "Fail-closed git push gate: secret scanning + supply-chain checks before any remote leaves the machine",
|
|
6
6
|
"type": "module",
|
package/plugin/border.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// border-opencode-plugin v0.5.
|
|
1
|
+
// border-opencode-plugin v0.5.1
|
|
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
|
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
// On this route the CLI is spawned FROM THE PACKAGE ITSELF (../dist/index.js
|
|
23
23
|
// lives inside the same tarball), so no global install or PATH entry is
|
|
24
24
|
// needed — that is what makes Route B genuinely install-free.
|
|
25
|
+
//
|
|
26
|
+
// NEVER spawn the host: this module runs inside the opencode process, whose
|
|
27
|
+
// process.execPath is the opencode (bun-compiled) binary, not node. The 0.5.0
|
|
28
|
+
// packaged-sibling route used process.execPath + dist/index.js as argv — on
|
|
29
|
+
// Route B that spawned opencode with the dist path as a subcommand: the gate
|
|
30
|
+
// was silently replaced by the host (impersonation incident, 2026-09-22; the
|
|
31
|
+
// node --test seam was blind because there execPath really is node). Since
|
|
32
|
+
// 0.5.1 the packaged dist is exec'd DIRECTLY (shebang) or under PATH `node`,
|
|
33
|
+
// and every candidate must pass an identity handshake (`--help` answering as
|
|
34
|
+
// border) before any user argv is ever spawned. Unverifiable ⇒ exit 2
|
|
35
|
+
// cannot-answer, never a fallback to some other binary.
|
|
25
36
|
|
|
26
37
|
import { execFile } from "node:child_process";
|
|
27
38
|
import { existsSync } from "node:fs";
|
|
@@ -30,6 +41,8 @@ import { tool, type Config } from "@opencode-ai/plugin";
|
|
|
30
41
|
|
|
31
42
|
/** Spawn cap: a CLI call that outlives this is killed and reported, never awaited forever. */
|
|
32
43
|
const TIMEOUT_MS = 300_000;
|
|
44
|
+
/** Identity-handshake cap: a `--help` needing longer than this is not a border CLI. */
|
|
45
|
+
const PROBE_TIMEOUT_MS = 20_000;
|
|
33
46
|
/** Per-stream output cap fed back into the session; keeps one tool call from flooding context. */
|
|
34
47
|
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
35
48
|
|
|
@@ -47,24 +60,36 @@ const ALLOWED_COMMANDS: readonly string[] = [
|
|
|
47
60
|
"--help",
|
|
48
61
|
];
|
|
49
62
|
|
|
50
|
-
/** How to reach the CLI: a binary name/absolute path, optionally
|
|
63
|
+
/** How to reach the CLI: a binary name/absolute path, optionally a script prefix. */
|
|
51
64
|
interface BinResolution {
|
|
52
65
|
readonly bin: string;
|
|
53
|
-
/** argv to prepend before the CLI's own argv (the dist entry when hosted by node). */
|
|
66
|
+
/** argv to prepend before the CLI's own argv (the dist entry when hosted by PATH node). */
|
|
54
67
|
readonly prefix: readonly string[];
|
|
68
|
+
/** Human-readable candidate identity, named in the cannot-answer note. */
|
|
69
|
+
readonly label: string;
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
73
|
+
* Candidate order: BORDER_BIN (non-empty) wins alone on every route — a broken
|
|
74
|
+
* explicit override is a loud error, never a silent fallback; else the dist
|
|
75
|
+
* bundle sitting next to this module, exec'd directly through its shebang and,
|
|
76
|
+
* if that is not runnable, under PATH `node` (Route B + dev checkout); else the
|
|
77
|
+
* `border` bin from PATH (Route A). process.execPath is deliberately absent:
|
|
78
|
+
* inside a plugin host it is the HOST binary, not a JS runtime.
|
|
61
79
|
*/
|
|
62
|
-
function
|
|
80
|
+
function candidateList(): readonly BinResolution[] {
|
|
63
81
|
const configured = process.env["BORDER_BIN"];
|
|
64
|
-
if (configured !== undefined && configured.length > 0)
|
|
82
|
+
if (configured !== undefined && configured.length > 0) {
|
|
83
|
+
return [{ bin: configured, prefix: [], label: `BORDER_BIN=${configured}` }];
|
|
84
|
+
}
|
|
65
85
|
const distEntry = fileURLToPath(new URL("../dist/index.js", import.meta.url));
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
const candidates: BinResolution[] = [];
|
|
87
|
+
if (existsSync(distEntry)) {
|
|
88
|
+
candidates.push({ bin: distEntry, prefix: [], label: "packaged dist/index.js (shebang exec)" });
|
|
89
|
+
candidates.push({ bin: "node", prefix: [distEntry], label: "packaged dist/index.js under PATH node" });
|
|
90
|
+
}
|
|
91
|
+
candidates.push({ bin: "border", prefix: [], label: "border on PATH" });
|
|
92
|
+
return candidates;
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
interface CliResult {
|
|
@@ -72,6 +97,8 @@ interface CliResult {
|
|
|
72
97
|
readonly stdout: string;
|
|
73
98
|
readonly stderr: string;
|
|
74
99
|
readonly note: string;
|
|
100
|
+
/** The child could not be spawned at all (ENOENT/EACCES/ENOEXEC) — never a gate verdict. */
|
|
101
|
+
readonly spawnFailed: boolean;
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
/** The subset of the execFile error shape this adapter reads (node sets `code` on failures). */
|
|
@@ -82,29 +109,27 @@ interface SpawnError extends Error {
|
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
/** Never rejects: every spawn failure becomes a structured result the agent can read. */
|
|
85
|
-
function runCli(
|
|
112
|
+
function runCli(resolution: BinResolution, argv: readonly string[], timeoutMs: number): Promise<CliResult> {
|
|
86
113
|
return new Promise((resolve) => {
|
|
87
114
|
execFile(
|
|
88
|
-
bin,
|
|
89
|
-
[...prefix, ...argv],
|
|
90
|
-
{ timeout:
|
|
115
|
+
resolution.bin,
|
|
116
|
+
[...resolution.prefix, ...argv],
|
|
117
|
+
{ timeout: timeoutMs, maxBuffer: MAX_OUTPUT_BYTES, shell: false },
|
|
91
118
|
(error, stdout, stderr) => {
|
|
92
119
|
const out = stdout.slice(0, MAX_OUTPUT_BYTES);
|
|
93
120
|
const errOut = stderr.slice(0, MAX_OUTPUT_BYTES);
|
|
94
121
|
if (error === null) {
|
|
95
|
-
resolve({ status: 0, stdout: out, stderr: errOut, note: "" });
|
|
122
|
+
resolve({ status: 0, stdout: out, stderr: errOut, note: "", spawnFailed: false });
|
|
96
123
|
return;
|
|
97
124
|
}
|
|
98
125
|
const failure = error as SpawnError;
|
|
99
|
-
if (failure.code === "ENOENT") {
|
|
126
|
+
if (failure.code === "ENOENT" || failure.code === "EACCES" || failure.code === "ENOEXEC") {
|
|
100
127
|
resolve({
|
|
101
|
-
status:
|
|
128
|
+
status: 2,
|
|
102
129
|
stdout: out,
|
|
103
130
|
stderr: errOut,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"real border binary, or run 'border opencode install' (route A), or " +
|
|
107
|
-
"npm i -g border-customs (route A, PATH entry).",
|
|
131
|
+
spawnFailed: true,
|
|
132
|
+
note: `cannot-answer: the border CLI could not be spawned (${String(failure.code)}).`,
|
|
108
133
|
});
|
|
109
134
|
return;
|
|
110
135
|
}
|
|
@@ -114,6 +139,7 @@ function runCli(bin: string, prefix: readonly string[], argv: readonly string[])
|
|
|
114
139
|
stdout: out,
|
|
115
140
|
stderr: errOut,
|
|
116
141
|
note: `output exceeded ${MAX_OUTPUT_BYTES} bytes per stream and was truncated.`,
|
|
142
|
+
spawnFailed: false,
|
|
117
143
|
});
|
|
118
144
|
return;
|
|
119
145
|
}
|
|
@@ -123,9 +149,10 @@ function runCli(bin: string, prefix: readonly string[], argv: readonly string[])
|
|
|
123
149
|
stdout: out,
|
|
124
150
|
stderr: errOut,
|
|
125
151
|
note:
|
|
126
|
-
`killed after ${
|
|
152
|
+
`killed after ${timeoutMs / 1000}s timeout (signal ${failure.signal ?? "SIGTERM"}). ` +
|
|
127
153
|
"border check and roundtrip can be long — if you need a long roundtrip, " +
|
|
128
154
|
"prefer running it in a terminal.",
|
|
155
|
+
spawnFailed: false,
|
|
129
156
|
});
|
|
130
157
|
return;
|
|
131
158
|
}
|
|
@@ -135,12 +162,56 @@ function runCli(bin: string, prefix: readonly string[], argv: readonly string[])
|
|
|
135
162
|
stderr: errOut,
|
|
136
163
|
note:
|
|
137
164
|
typeof failure.code === "number" ? "" : `spawn failed: ${failure.message}`,
|
|
165
|
+
spawnFailed: false,
|
|
138
166
|
});
|
|
139
167
|
},
|
|
140
168
|
);
|
|
141
169
|
});
|
|
142
170
|
}
|
|
143
171
|
|
|
172
|
+
/**
|
|
173
|
+
* border's --help contract (src/cli.ts): exit 0, banner line starts with
|
|
174
|
+
* `border`, usage line names `usage: border`. An impostor answering the spawn
|
|
175
|
+
* slot (the 2026-09-22 incident: the opencode host banner) fails this test, so
|
|
176
|
+
* user argv is never handed to a binary that cannot prove it is the gate.
|
|
177
|
+
* Honest boundary: this catches mistaken identity, not a forged banner.
|
|
178
|
+
*/
|
|
179
|
+
function isBorderHelp(result: CliResult): boolean {
|
|
180
|
+
if (result.status !== 0) return false;
|
|
181
|
+
const firstLine = (result.stdout.match(/^\s*(.*)/) ?? ["", ""])[1] ?? "";
|
|
182
|
+
return firstLine.startsWith("border") && result.stdout.includes("usage: border");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type Resolution =
|
|
186
|
+
| { readonly ok: true; readonly resolution: BinResolution }
|
|
187
|
+
| { readonly ok: false; readonly reasons: readonly string[] };
|
|
188
|
+
|
|
189
|
+
/** Positive handshakes are cached per BORDER_BIN key for the host process's
|
|
190
|
+
* life; failures are never cached, so a mid-session install recovers on the
|
|
191
|
+
* next call. */
|
|
192
|
+
let verified: { readonly key: string; readonly resolution: BinResolution } | null = null;
|
|
193
|
+
|
|
194
|
+
async function resolveVerified(): Promise<Resolution> {
|
|
195
|
+
const key = process.env["BORDER_BIN"] ?? "";
|
|
196
|
+
if (verified !== null && verified.key === key) {
|
|
197
|
+
return { ok: true, resolution: verified.resolution };
|
|
198
|
+
}
|
|
199
|
+
const reasons: string[] = [];
|
|
200
|
+
for (const candidate of candidateList()) {
|
|
201
|
+
const probe = await runCli(candidate, ["--help"], PROBE_TIMEOUT_MS);
|
|
202
|
+
if (isBorderHelp(probe)) {
|
|
203
|
+
verified = { key, resolution: candidate };
|
|
204
|
+
return { ok: true, resolution: candidate };
|
|
205
|
+
}
|
|
206
|
+
reasons.push(
|
|
207
|
+
probe.status === 0
|
|
208
|
+
? `${candidate.label}: spawned but --help did not answer as border (impostor?)`
|
|
209
|
+
: `${candidate.label}: probe exit ${String(probe.status)}${probe.note === "" ? "" : ` — ${probe.note}`}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return { ok: false, reasons };
|
|
213
|
+
}
|
|
214
|
+
|
|
144
215
|
/**
|
|
145
216
|
* The /border slash-command template. ALSO published verbatim (after the marker
|
|
146
217
|
* line) as plugin/border-command.md — test/opencode.test.ts pins the byte-mirror.
|
|
@@ -186,7 +257,10 @@ export default {
|
|
|
186
257
|
"llm-request, llm-ingest, scan, roundtrip, --help) and every remaining argv " +
|
|
187
258
|
"token in `extra`. The call is spawned argv-only (no shell) with a 300s " +
|
|
188
259
|
"timeout; the result always ends with the CLI exit code: 0 pass, 1 " +
|
|
189
|
-
"gate-blocked or partial push, 2 gate could not answer.
|
|
260
|
+
"gate-blocked or partial push, 2 gate could not answer. Before running your " +
|
|
261
|
+
"argv the tool handshakes its binary candidate with `--help` and refuses " +
|
|
262
|
+
"everything (exit 2, cannot-answer) unless the responder provably is the " +
|
|
263
|
+
"border CLI — it never falls back to another binary. Honest privilege " +
|
|
190
264
|
"note: this tool runs the border CLI, and the CLI IS the gate that decides " +
|
|
191
265
|
"what leaves this machine — it carries the same power as the binary, so an " +
|
|
192
266
|
"agent may only *ask* the gate, never bypass it. `push --yes` is " +
|
|
@@ -222,8 +296,19 @@ export default {
|
|
|
222
296
|
}
|
|
223
297
|
const argv: readonly string[] = [command, ...extra];
|
|
224
298
|
context.metadata({ title: `border ${argv.join(" ")}` });
|
|
225
|
-
const resolution =
|
|
226
|
-
|
|
299
|
+
const resolution = await resolveVerified();
|
|
300
|
+
if (!resolution.ok) {
|
|
301
|
+
return [
|
|
302
|
+
`$ border ${argv.join(" ")}`,
|
|
303
|
+
"exit: 2",
|
|
304
|
+
`note: cannot-answer — no border CLI passed the identity handshake, so nothing was run. Tried: ${resolution.reasons.join("; ")}. ` +
|
|
305
|
+
"Remedies: point BORDER_BIN at a real border CLI, run 'border opencode install' (route A), or npm i -g border-customs (PATH entry).",
|
|
306
|
+
"--- stdout ---\n(empty)",
|
|
307
|
+
"--- stderr ---\n(empty)",
|
|
308
|
+
].join("\n");
|
|
309
|
+
}
|
|
310
|
+
const result = await runCli(resolution.resolution, argv, TIMEOUT_MS);
|
|
311
|
+
if (result.spawnFailed) verified = null;
|
|
227
312
|
const sections = [
|
|
228
313
|
`$ border ${argv.join(" ")}`,
|
|
229
314
|
`exit: ${String(result.status)}`,
|