border-customs 0.6.0 → 0.7.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 +42 -1
- package/dist/assets/package-lock.json +2 -2
- package/dist/index.js +1 -1
- package/index.ts +7 -0
- package/package.json +3 -2
- package/plugin/border.ts +169 -58
package/README.md
CHANGED
|
@@ -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.7.1"]
|
|
721
721
|
}
|
|
722
722
|
```
|
|
723
723
|
|
|
@@ -738,6 +738,18 @@ exit 2 and never overwritten, uninstall removes only marker-bearing files, and i
|
|
|
738
738
|
report `up to date`. Either route, restart opencode afterwards — tools and commands are
|
|
739
739
|
scanned at startup.
|
|
740
740
|
|
|
741
|
+
**OpenCode 2 (beta) compatibility (0.7.0).** The same module is dual-shaped: V1 hosts call
|
|
742
|
+
`server()` (object entry form, opencode ≥ 1.18.29 required); V2 hosts (`@opencode-ai/cli@beta`,
|
|
743
|
+
bin `opencode2`) call `setup(ctx)` and register the identical tool through
|
|
744
|
+
`ctx.tool.transform` — hand-written `{id, setup}` (the SDK's `define()` is identity), plain
|
|
745
|
+
JSON Schema input, zero V2-SDK runtime dependency. Live-verified on beta-19271 via
|
|
746
|
+
project-local `.opencode/plugins/` (both single-file and package-directory shapes; directory
|
|
747
|
+
discovery resolves the root `index.ts` re-export, which is why the package ships it). V2
|
|
748
|
+
activates plugins lazily — listing them requires `POST /api/plugin/await-activation` first.
|
|
749
|
+
The `/border` slash command stays V1-only until the V2 command-delivery API settles; the
|
|
750
|
+
`border` tool is the supported V2 surface. Config keys are shared (V2 normalizes
|
|
751
|
+
`plugin`→`plugins`). Global file-drop discovery under V2 is untested and not claimed.
|
|
752
|
+
|
|
741
753
|
What loads is one agent tool `border` plus a `/border` slash command. The tool spawns the CLI
|
|
742
754
|
argv-only (no shell; 300 s cap, 64 KiB per stream) restricted to the closed command list
|
|
743
755
|
`check`, `push`, `status`, `llm-request`, `llm-ingest`, `scan`, `roundtrip`, `--help`, and
|
|
@@ -803,6 +815,31 @@ code.
|
|
|
803
815
|
|
|
804
816
|
## Changelog
|
|
805
817
|
|
|
818
|
+
### 0.7.1 (2026-09-30)
|
|
819
|
+
- Fix: **release-gate test ruler race** (`test/push.integration.test.ts` I4) — the
|
|
820
|
+
"dry-run made ZERO verdaccio requests" invariant counted lines in verdaccio's stdout
|
|
821
|
+
log, an eventually-consistent view: on a saturated CI runner the gate-leg probe lines
|
|
822
|
+
of an earlier phase land after the snapshot, inflating the dry-run delta (v0.7.0 CI
|
|
823
|
+
run 36671401844 failed `6 !== 2` while the identical bytes pass locally). The suite
|
|
824
|
+
now takes every snapshot through a FIFO drain barrier (sentinel request whose log line
|
|
825
|
+
must be visible before counting; sentinel traffic is excluded from the count). The
|
|
826
|
+
zero-network dry-run contract itself is unchanged — verified request-free on both sides
|
|
827
|
+
of the barrier; only the measuring instrument was repaired.
|
|
828
|
+
|
|
829
|
+
### 0.7.0 (2026-09-28)
|
|
830
|
+
- Add: **OpenCode 2 (beta) plugin compatibility** — the plugin module is dual-shaped
|
|
831
|
+
(`{id, setup}` for V2 hosts, frozen V1 `server()`), sharing the pre-spawn
|
|
832
|
+
allowlist/`--yes` gates and the 0.5.1 identity handshake across both shapes;
|
|
833
|
+
refusal/handshake/rendering byte-parity between edges is pinned by
|
|
834
|
+
`test/opencode.v2shape.test.ts` while the V1 path stays 16/16 on unmodified
|
|
835
|
+
tests. Root `index.ts` re-export shipped for V2 directory discovery (which
|
|
836
|
+
bypasses the exports map; live-proven on `@opencode-ai/cli` beta-19271).
|
|
837
|
+
- Fix: the live V1 probe is anti-vacuous — opting in (`BORDER_OPENCODE_PROBE=1`)
|
|
838
|
+
without a resolvable binary or built dist now FAILS naming the remedy instead
|
|
839
|
+
of skipping green-adjacent (`BORDER_OPENCODE_BIN` override added).
|
|
840
|
+
- Docs: support matrix (V1 object entry ≥ 1.18.29; V2 project-local verified;
|
|
841
|
+
`/border` command V1-only pending the V2 delivery API).
|
|
842
|
+
|
|
806
843
|
### 0.6.0 (2026-09-25)
|
|
807
844
|
- Add: **the exfil lens** — five rules (`exfil-rfc1918`, `exfil-ssh-target`,
|
|
808
845
|
`exfil-home-path`, `exfil-cred-location`, `exfil-host-profile`) that flag
|
|
@@ -963,6 +1000,10 @@ MIT, see [LICENSE](LICENSE).
|
|
|
963
1000
|
|
|
964
1001
|
## 中文概要
|
|
965
1002
|
|
|
1003
|
+
**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
|
+
|
|
1005
|
+
**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 点名补救。
|
|
1006
|
+
|
|
966
1007
|
border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install -g border-customs` 安装,在仓库里跑 `border check`。它扫描 git 历史、工作区(未跟踪文件同样是一等输入)、归档、tag 注释和将要发布的 npm/PyPI/crates/RubyGems 字节,检出密钥与供应链风险;只有当"当前状态指纹"存在新鲜且完整的 PASS 记录时才允许 `border push --yes` 放行。指纹是 sha256(head、porcelain 摘要、规则哈希、暴露面、ref 集合、有效目标)六元组,任何一处变动,旧的 PASS 立即失效,必须重查。流水线:gitleaks(历史+工作区+tag,内置 8.30.1 规则,仓库自带的 ignore 文件直接判 CRITICAL)+ secretlint(进程内,AWS Key 规则强制开启)+ 原生规则(AI 会话产物闭集、提交身份白名单含传输对象检查)+ 注册表预检(版本已存在=必须 bump,名称被外人占有=拒绝;空响应/超时/解析失败一律 exit 2,沉默绝不等于不存在)。构件只构建一次进 `.border/dist/`,扫描的就是发布的字节,发布时再哈希比对,不一致直接拒发。跳过台账让重复检查不到 1 秒,但回放前先重算指纹并重新 pack 验证新鲜度。报告只输出掩码片段(sha256 摘要 + 前4…后4)。push 是多目标状态机,多 remote 先做全有或全无的 fast-forward 预检,永不 force-push;npm/twine/cargo/gem 的凭据经 stdio 透传,border 从不触碰。0.2.0 新增 crates.io 与 RubyGems 通道(公开 crates.io 固定、rubygems 可用 host 覆盖私有镜像),既有配置行为不变。0.3.0 新增残留扫描(residue gate):发布字节里的安装期钩子按 T0-T4 闭集签名表分类(闭集 T1 树内钩子降为 MEDIUM,未知形态原样保留 CRITICAL),外加跨包管理器写入、配对标记缺失(`# BEGIN` 块只检测不豁免)与 gem 不可解析扩展共七条 `residue-*` 规则;`residue.enabled` 是唯一豁免开关且改动指纹使旧 PASS 失效,规则表与分类器源码进入 rulesHash,改一个签名即强制重查。静态分析只证明"能力"不证明"事实",border 不是恶意软件沙箱,从不执行被读代码。0.4.0 新增证明阀
|
|
967
1008
|
`residue.requireProof`(默认关,strict):开启后,发布构件若触发阻断级 `residue-*` 发现,通道 PASS
|
|
968
1009
|
还要求账本里存在按其 sha256 索引且 rulesHash 新鲜的 `border roundtrip` 实测记录——check 自身从不
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "border-customs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "border-customs",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.7.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": "^1.17.4",
|
package/dist/index.js
CHANGED
|
@@ -45059,7 +45059,7 @@ import { createHash as createHash18 } from "node:crypto";
|
|
|
45059
45059
|
import { basename as basename5 } from "node:path";
|
|
45060
45060
|
var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
45061
45061
|
var SCAN_REQUEST_TIMEOUT_MS = 6e4;
|
|
45062
|
-
var SCAN_USER_AGENT = "border-customs/0.
|
|
45062
|
+
var SCAN_USER_AGENT = "border-customs/0.7.1 (+https://github.com/sumteclab/border; border scan)";
|
|
45063
45063
|
var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
|
|
45064
45064
|
var defaultScanFetcher = async (url2, init) => {
|
|
45065
45065
|
if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
|
package/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// provenance: .omo/plans/border-opencode-v2-plugin.md T1 — V2 project-directory plugin
|
|
2
|
+
// discovery resolves <dir>/index directly (bypassing the package exports map, live-proven
|
|
3
|
+
// on cli beta-19271); this re-export lets the unpacked package directory itself serve as a
|
|
4
|
+
// V2 local plugin. Name-based imports continue to enter through exports["./server"].
|
|
5
|
+
import plugin from "./plugin/border.ts";
|
|
6
|
+
|
|
7
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "border-customs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"plugin"
|
|
17
|
+
"plugin",
|
|
18
|
+
"index.ts"
|
|
18
19
|
],
|
|
19
20
|
"engines": {
|
|
20
21
|
"node": ">=22.0.0"
|
package/plugin/border.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// border-opencode-plugin v0.
|
|
1
|
+
// border-opencode-plugin v0.7.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
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
// the human gate: nothing here can push for real (a bare `border push` is a
|
|
7
7
|
// DRY-RUN by the CLI's own contract, and `--yes` is refused outright).
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// Dual-shape default export — one module, two host generations:
|
|
10
|
+
// V1 plugin format (opencode >= 1.18.29, object entry): { id, server }, where
|
|
10
11
|
// server(input, options) resolves to Hooks; Hooks.tool is a
|
|
11
|
-
// { [name]: ToolDefinition } record (see @opencode-ai/plugin).
|
|
12
|
+
// { [name]: ToolDefinition } record (see @opencode-ai/plugin). The V1 host
|
|
13
|
+
// ignores the extra `setup` key (this is the official dual-shape form).
|
|
14
|
+
// V2 plugin format (@opencode-ai/cli beta line): the loader accepts only
|
|
15
|
+
// { id, setup } (or { id, effect }) and silently drops anything else, so the
|
|
16
|
+
// SAME tool registers through setup(ctx) -> ctx.tool.transform ->
|
|
17
|
+
// editor.add(Info). Both edges share the pre-spawn gate (gateArgv) and the
|
|
18
|
+
// execute substance (buildToolResult) below, so behavior stays identical;
|
|
19
|
+
// test/opencode.v2shape.test.ts pins V1↔V2 rendering and refusal parity.
|
|
12
20
|
// This file ships as-is (package.json "files") and reaches a session by two
|
|
13
21
|
// routes, neither compiling it through border's tsc:
|
|
14
22
|
// (A) copied into <CONFIG>/plugins/ by `border opencode install`, which also
|
|
@@ -234,8 +242,160 @@ contract: it reports the verdict the gate would produce without touching any
|
|
|
234
242
|
remote.
|
|
235
243
|
`;
|
|
236
244
|
|
|
245
|
+
// --- V2 loader surface: local structural types ---------------------------------
|
|
246
|
+
// shape proven from @opencode-ai/plugin beta-19271 .d.ts; live-gated by
|
|
247
|
+
// test/opencode.v2.probe (plan T3). Deliberately NO V2 SDK import here (wave
|
|
248
|
+
// plan D1/R3 pending): only the two members the setup() seam touches are
|
|
249
|
+
// typed — ctx.tool.transform and editor.add — everything else stays opaque.
|
|
250
|
+
|
|
251
|
+
/** Parsed tool input, mirroring the V1 args schema (command required, extra optional). */
|
|
252
|
+
interface V2ToolArgs {
|
|
253
|
+
readonly command: string;
|
|
254
|
+
readonly extra?: readonly string[];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The V2 promise-entry result envelope: the rendered text in `content`. */
|
|
258
|
+
interface V2ToolResult {
|
|
259
|
+
readonly content: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** One tool registration handed to ToolEditor.add by the transform callback. */
|
|
263
|
+
export interface V2ToolInfo {
|
|
264
|
+
readonly name: string;
|
|
265
|
+
readonly description: string;
|
|
266
|
+
/** Pure JSON Schema — ValueSchema accepts it without a schema-library import. */
|
|
267
|
+
readonly input: Record<string, unknown>;
|
|
268
|
+
readonly execute: (args: V2ToolArgs, context?: unknown) => Promise<V2ToolResult>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface V2ToolEditor {
|
|
272
|
+
add(info: V2ToolInfo): void;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface V2ToolRegistry {
|
|
276
|
+
transform(callback: (editor: V2ToolEditor) => void): Promise<unknown>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface V2PluginContext {
|
|
280
|
+
readonly tool: V2ToolRegistry;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The ONE tool description, byte-shared by both edges: the V1 tool() schema
|
|
285
|
+
* and the V2 Info both render exactly this text (single source of truth).
|
|
286
|
+
*/
|
|
287
|
+
const TOOL_DESCRIPTION =
|
|
288
|
+
"Run the border fail-closed push-gate CLI on this machine. " +
|
|
289
|
+
"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
|
+
"token in `extra`. The call is spawned argv-only (no shell) with a 300s " +
|
|
292
|
+
"timeout; the result always ends with the CLI exit code: 0 pass, 1 " +
|
|
293
|
+
"gate-blocked or partial push, 2 gate could not answer. Before running your " +
|
|
294
|
+
"argv the tool handshakes its binary candidate with `--help` and refuses " +
|
|
295
|
+
"everything (exit 2, cannot-answer) unless the responder provably is the " +
|
|
296
|
+
"border CLI — it never falls back to another binary. Honest privilege " +
|
|
297
|
+
"note: this tool runs the border CLI, and the CLI IS the gate that decides " +
|
|
298
|
+
"what leaves this machine — it carries the same power as the binary, so an " +
|
|
299
|
+
"agent may only *ask* the gate, never bypass it. `push --yes` is " +
|
|
300
|
+
"deliberately NOT reachable here: real pushes are the human gate and belong " +
|
|
301
|
+
"to a terminal. A bare `border push` through this tool is a DRY-RUN by the " +
|
|
302
|
+
"CLI's own contract.";
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The pre-spawn gate — the deliberate seam split, load-bearing for security:
|
|
306
|
+
* gateArgv(argv) decides WHETHER the argv may run at all (allowlist
|
|
307
|
+
* membership, `--yes` refusal) and returns the refusal text or null;
|
|
308
|
+
* buildToolResult(argv) does the actual work (handshake, spawn, render)
|
|
309
|
+
* and NEVER re-gates — it trusts its caller.
|
|
310
|
+
* Every edge (today: V1 tool.execute and V2 Info.execute) MUST call gateArgv
|
|
311
|
+
* BEFORE buildToolResult. A future edge that skips the gate silently gets no
|
|
312
|
+
* gating; test/opencode.v2shape.test.ts pins refusal parity on both edges.
|
|
313
|
+
*/
|
|
314
|
+
function gateArgv(argv: readonly string[]): string | null {
|
|
315
|
+
const command = argv[0] ?? "";
|
|
316
|
+
if (!ALLOWED_COMMANDS.includes(command)) {
|
|
317
|
+
return (
|
|
318
|
+
`border: refused '${command}' — not an allowed command. ` +
|
|
319
|
+
`Allowed: ${ALLOWED_COMMANDS.join(", ")}. Put the command word in 'command' ` +
|
|
320
|
+
`and every other token in 'extra'.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (argv.includes("--yes")) {
|
|
324
|
+
return (
|
|
325
|
+
`border: refused '${command}' with '--yes' — a real push is the human gate ` +
|
|
326
|
+
`and happens in a terminal. Run \`border push --yes\` there if a visible ` +
|
|
327
|
+
`human go-ahead exists. A bare \`border push\` through this tool is a ` +
|
|
328
|
+
`DRY-RUN by the CLI's own contract.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The tool-execute substance shared by both edges (wave plan D2): resolve the
|
|
336
|
+
* CLI via resolveVerified (identity handshake included), spawn it argv-only
|
|
337
|
+
* via runCli, render the sectioned result. No gates and no V1-only
|
|
338
|
+
* context.metadata here — those stay at each edge, see gateArgv above.
|
|
339
|
+
*/
|
|
340
|
+
async function buildToolResult(argv: readonly string[]): Promise<string> {
|
|
341
|
+
const resolution = await resolveVerified();
|
|
342
|
+
if (!resolution.ok) {
|
|
343
|
+
return [
|
|
344
|
+
`$ border ${argv.join(" ")}`,
|
|
345
|
+
"exit: 2",
|
|
346
|
+
`note: cannot-answer — no border CLI passed the identity handshake, so nothing was run. Tried: ${resolution.reasons.join("; ")}. ` +
|
|
347
|
+
"Remedies: point BORDER_BIN at a real border CLI, run 'border opencode install' (route A), or npm i -g border-customs (PATH entry).",
|
|
348
|
+
"--- stdout ---\n(empty)",
|
|
349
|
+
"--- stderr ---\n(empty)",
|
|
350
|
+
].join("\n");
|
|
351
|
+
}
|
|
352
|
+
const result = await runCli(resolution.resolution, argv, TIMEOUT_MS);
|
|
353
|
+
if (result.spawnFailed) verified = null;
|
|
354
|
+
const sections = [
|
|
355
|
+
`$ border ${argv.join(" ")}`,
|
|
356
|
+
`exit: ${String(result.status)}`,
|
|
357
|
+
];
|
|
358
|
+
if (result.note.length > 0) sections.push(`note: ${result.note}`);
|
|
359
|
+
sections.push(`--- stdout ---\n${result.stdout.length === 0 ? "(empty)" : result.stdout}`);
|
|
360
|
+
sections.push(`--- stderr ---\n${result.stderr.length === 0 ? "(empty)" : result.stderr}`);
|
|
361
|
+
return sections.join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The V2-side tool registration: same gate + same substance as the V1 tool. */
|
|
365
|
+
function makeV2ToolInfo(): V2ToolInfo {
|
|
366
|
+
return {
|
|
367
|
+
name: "border",
|
|
368
|
+
description: TOOL_DESCRIPTION,
|
|
369
|
+
input: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {
|
|
372
|
+
command: { type: "string" },
|
|
373
|
+
extra: { type: "array", items: { type: "string" } },
|
|
374
|
+
},
|
|
375
|
+
required: ["command"],
|
|
376
|
+
additionalProperties: false,
|
|
377
|
+
},
|
|
378
|
+
execute: async (args: V2ToolArgs): Promise<V2ToolResult> => {
|
|
379
|
+
const argv: readonly string[] = [args.command.trim(), ...(args.extra ?? [])];
|
|
380
|
+
const refusal = gateArgv(argv);
|
|
381
|
+
if (refusal !== null) return { content: refusal };
|
|
382
|
+
return { content: await buildToolResult(argv) };
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
237
387
|
export default {
|
|
388
|
+
// Shared plugin id: the V1 host reads it for the {id, server} object entry
|
|
389
|
+
// and the V2 loader reads this same id for its {id, setup} schema check.
|
|
238
390
|
id: "border",
|
|
391
|
+
setup: async (ctx: V2PluginContext): Promise<void> => {
|
|
392
|
+
// transform's return is awaited (Promise<Registration>), but the callback
|
|
393
|
+
// body itself must stay SYNCHRONOUS — only editor.add inside, any async
|
|
394
|
+
// work belongs in Info.execute (contract E3; pinned by v2shape test (d)).
|
|
395
|
+
await ctx.tool.transform((editor: V2ToolEditor): void => {
|
|
396
|
+
editor.add(makeV2ToolInfo());
|
|
397
|
+
});
|
|
398
|
+
},
|
|
239
399
|
server: async () => ({
|
|
240
400
|
// Self-registering /border: opencode calls hook.config(cfg) once per
|
|
241
401
|
// instance after ALL config sources are merged — file-based commands are
|
|
@@ -251,22 +411,7 @@ export default {
|
|
|
251
411
|
},
|
|
252
412
|
tool: {
|
|
253
413
|
border: tool({
|
|
254
|
-
description:
|
|
255
|
-
"Run the border fail-closed push-gate CLI on this machine. " +
|
|
256
|
-
"Pass the top-level command word in `command` (one of: check, push, status, " +
|
|
257
|
-
"llm-request, llm-ingest, scan, roundtrip, --help) and every remaining argv " +
|
|
258
|
-
"token in `extra`. The call is spawned argv-only (no shell) with a 300s " +
|
|
259
|
-
"timeout; the result always ends with the CLI exit code: 0 pass, 1 " +
|
|
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 " +
|
|
264
|
-
"note: this tool runs the border CLI, and the CLI IS the gate that decides " +
|
|
265
|
-
"what leaves this machine — it carries the same power as the binary, so an " +
|
|
266
|
-
"agent may only *ask* the gate, never bypass it. `push --yes` is " +
|
|
267
|
-
"deliberately NOT reachable here: real pushes are the human gate and belong " +
|
|
268
|
-
"to a terminal. A bare `border push` through this tool is a DRY-RUN by the " +
|
|
269
|
-
"CLI's own contract.",
|
|
414
|
+
description: TOOL_DESCRIPTION,
|
|
270
415
|
args: {
|
|
271
416
|
command: tool.schema
|
|
272
417
|
.string()
|
|
@@ -276,47 +421,13 @@ export default {
|
|
|
276
421
|
.optional()
|
|
277
422
|
.describe("remaining argv tokens, e.g. ['--force']"),
|
|
278
423
|
},
|
|
424
|
+
// V1 edge: gate → V1-only metadata → shared substance (see gateArgv).
|
|
279
425
|
execute: async (args, context) => {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
`border: refused '${command}' — not an allowed command. ` +
|
|
284
|
-
`Allowed: ${ALLOWED_COMMANDS.join(", ")}. Put the command word in 'command' ` +
|
|
285
|
-
`and every other token in 'extra'.`
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
const extra = args.extra ?? [];
|
|
289
|
-
if (extra.includes("--yes")) {
|
|
290
|
-
return (
|
|
291
|
-
`border: refused '${command}' with '--yes' — a real push is the human gate ` +
|
|
292
|
-
`and happens in a terminal. Run \`border push --yes\` there if a visible ` +
|
|
293
|
-
`human go-ahead exists. A bare \`border push\` through this tool is a ` +
|
|
294
|
-
`DRY-RUN by the CLI's own contract.`
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
const argv: readonly string[] = [command, ...extra];
|
|
426
|
+
const argv: readonly string[] = [args.command.trim(), ...(args.extra ?? [])];
|
|
427
|
+
const refusal = gateArgv(argv);
|
|
428
|
+
if (refusal !== null) return refusal;
|
|
298
429
|
context.metadata({ title: `border ${argv.join(" ")}` });
|
|
299
|
-
|
|
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;
|
|
312
|
-
const sections = [
|
|
313
|
-
`$ border ${argv.join(" ")}`,
|
|
314
|
-
`exit: ${String(result.status)}`,
|
|
315
|
-
];
|
|
316
|
-
if (result.note.length > 0) sections.push(`note: ${result.note}`);
|
|
317
|
-
sections.push(`--- stdout ---\n${result.stdout.length === 0 ? "(empty)" : result.stdout}`);
|
|
318
|
-
sections.push(`--- stderr ---\n${result.stderr.length === 0 ? "(empty)" : result.stderr}`);
|
|
319
|
-
return sections.join("\n");
|
|
430
|
+
return buildToolResult(argv);
|
|
320
431
|
},
|
|
321
432
|
}),
|
|
322
433
|
},
|