@inobit/pi-instabox 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +214 -2
- package/README.zh-CN.md +215 -0
- package/package.json +64 -3
- package/src/boxes.ts +158 -0
- package/src/cli.ts +440 -0
- package/src/config.ts +310 -0
- package/src/devtools.ts +257 -0
- package/src/entry-render.ts +42 -0
- package/src/index.ts +1141 -0
- package/src/log.ts +203 -0
- package/src/pool.ts +651 -0
- package/src/remote.ts +678 -0
- package/src/resolve.ts +216 -0
- package/src/schema.ts +370 -0
- package/src/session-target.ts +100 -0
- package/src/template.ts +449 -0
- package/src/tools.ts +322 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-11-09
|
|
4
|
+
|
|
5
|
+
- feat: nine `instabox_desktop_*` tools generated from the target box's remote `tools/list`, each taking an optional `box` parameter and returning `[box=<name>]` as the first line of every result
|
|
6
|
+
- feat: five-level target resolution (call parameter → box name in the last user message → session target → `defaultBox` → first `instabox ls` box) with hard errors instead of silent fallbacks, and an explicit "no fallback, nothing retried" note on remote errors
|
|
7
|
+
- feat: `/instabox-use` and `/instabox-status` commands; the session target is a branch-scoped custom entry rendered in the transcript and never injected into the LLM context; `/instabox-use` also retries registration (wake → fetch → register) after a cold start
|
|
8
|
+
- feat: per-box template cache at `<agentDir>/pi-instabox/templates.json` (remote `serverInfo.version` + 7-day TTL) so the load-time declarations need no ssh; a background fetch that never wakes a paused box re-validates the version once per session start and refetches only when it changed or the TTL expired
|
|
9
|
+
- feat: silent session-start tool declaration (supersedes the earlier “never wakes a paused box” wording) — cache hit: silent registration, then one handshake to re-validate the version (a failed check falls back to the cached tools without a word); no cache: the first box in `state === "running"` and never woken; nothing running: the first listed box is woken in the background (no progress UI, non-blocking) and its template fetched. Success never notifies; only “no usable sandbox” (`ls` empty or failing) and a failed sync warn once. Internal `[pi-instabox] …` logs are silent unless `PI_INSTABOX_DEBUG=1`
|
|
10
|
+
- feat: per-box stdio MCP pool with a `desiredState`-aware wake chain (`start` / `wake` / `resume`), abort propagation and failure eviction, driven by a JSON-RPC client that tolerates both newline-delimited and `Content-Length` framing
|
|
11
|
+
- feat: optional remote `chrome-devtools-mcp` — when `devToolsBox` is set, a generated `bin/devtools-connect.sh` helper plus an `instabox-devtools` MCP server with `codemode` exposure; config comes from `<agentDir>/extensions/pi-instabox/config.json` layered over defaults and `INSTABOX_MCP_*` environment variables, and the extension never calls `setActiveTools`
|
|
12
|
+
- feat: gate devtools MCP behind --instabox-devtools with a runtime toggle
|
|
13
|
+
- feat: write structured JSONL logs under `<agentDir>/logs/pi-instabox` (one record per event, `info` by default and `debug` with `PI_INSTABOX_DEBUG=1`, 1 MiB × 3 rotation, sensitive field names redacted, write failures swallowed)
|
|
14
|
+
- feat: bind devtools box to the session and add /instabox-devtools reset
|
|
15
|
+
- fix: cap the prepare phase at 60s
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 inobit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,215 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @inobit/pi-instabox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** | [中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Drive instabox sandboxes from Pi sessions: nine `instabox_desktop_*` computer-use tools whose every call names the box it targets, plus a remote `chrome-devtools-mcp` server bound to one fixed box that stays off until you pass `--instabox-devtools`.
|
|
6
|
+
|
|
7
|
+
- **Every call names its box**: an optional `box` parameter (or the five-level target resolution chain) decides which sandbox runs, and the first line of every successful result is `[box=<name>]`
|
|
8
|
+
- **No silent fallback**: a mistyped or ambiguous target is an error listing candidates; only a stale `defaultBox` degrades (with one warning per session)
|
|
9
|
+
- **Schemas come from the remote**: tool definitions are generated from the target box's `tools/list` and cached per box, so the load-time declarations need no ssh
|
|
10
|
+
- **Never touches your tool selection**: desktop tools are `exposure: "codemode"` and the extension never calls `setActiveTools`
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- **`@earendil-works/pi-coding-agent >= 0.99.0`** — a hard floor. `exposure`, `namespace`, `registerMcpServer` and the codemode/MCP support this package builds on landed in 0.99.0; older hosts cannot load it.
|
|
15
|
+
- Node.js **>= 24**.
|
|
16
|
+
- The `instabox` CLI on `PATH` (`instabox version`, `instabox ls --json`, `instabox ssh`, `instabox wake|start|resume`, `instabox exec`). A missing CLI means the extension registers nothing — no tools, no commands, no MCP server, no runtime files (no template cache, no helper) — and shows one warning per session; the only thing written is the single `cli.probe` record in the event log. A first `instabox version` probe that merely times out is retried once at session start with a larger budget before that verdict.
|
|
17
|
+
- Sandboxes whose template ships `instabox-computer-use` (the remote MCP server this extension drives over `instabox ssh <box> -- instabox-computer-use mcp`).
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install npm:@inobit/pi-instabox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Restart the Pi session to take effect.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
**Desktop tools.** Each entry of the target box's remote `tools/list` becomes one Pi tool named `instabox_desktop_<remote_name>` (nine for the `instabox-computer-use` build this package targets, e.g. `instabox_desktop_click`), labelled `instabox desktop <remote_name>`, grouped in the `instabox-desktop` namespace (alias `instabox_desktop`) with `exposure: "codemode"`.
|
|
30
|
+
|
|
31
|
+
Every tool takes an optional `box` parameter. When it is omitted, the target is resolved in this order, first match wins:
|
|
32
|
+
|
|
33
|
+
1. the `box` parameter of this call;
|
|
34
|
+
2. the only box name mentioned in the last user message of the current branch;
|
|
35
|
+
3. the session target (set with `/instabox-use`);
|
|
36
|
+
4. `defaultBox` from config;
|
|
37
|
+
5. the first box returned by `instabox ls`.
|
|
38
|
+
|
|
39
|
+
Levels 1–3 are strict: an unknown name is an error and a message naming several boxes is `ambiguous` — none of them guesses, and none falls through to the next level. Only level 4 degrades (to level 5, with one warning). Every successful result starts with `[box=<name>]`; remote content (text and images) is passed through unchanged, and a remote error keeps its `isError` flag plus one line stating that nothing was retried and no other box was used (with two remedies).
|
|
40
|
+
|
|
41
|
+
**`/instabox-use [box]`** — set or show this session's target box. Without arguments it prints the current target and the candidate boxes. An unknown name is refused and the previous target is kept. The target is stored as a custom session entry (never injected into the LLM context, scoped to the current branch, so `/tree` switches and `--continue` restore it). If no desktop tool is registered yet (cold start, no cache), the command also wakes the box, fetches `tools/list` and registers the tools — this is the retry path when the box was not running at session start.
|
|
42
|
+
|
|
43
|
+
**`/instabox-status`** — one-shot diagnostics: config summary (`defaultBox`, `devToolsBox`, `autoInstallDevTools`, `remotePath` — `devToolsBox` there is only the DevTools default box, not the switch), session target, resolved target and how it was resolved, registered `instabox_desktop_*` tools with their exposure, which box's cached template the declarations came from (`tool declarations: from the cached template of box "…"`), active tools, codemode state, and the MCP servers registered by extensions — `instabox-devtools` appears in that last list only while the DevTools server is on; the switch itself is reported by `/instabox-devtools status` (see [DevTools MCP](#devtools-mcp)).
|
|
44
|
+
|
|
45
|
+
**`/instabox-devtools [status|on [box]|off|reset]`** — the runtime toggle for the remote DevTools MCP server: bare (or `status`) shows the switch, the name of the server registered in this session, the box bound to this session and the box the next `on` would use with where that box comes from (`session` / `flag` / `config`); `on [box]` registers or replaces it and, when a box is given, binds it to this session; `off` unregisters it for the rest of the session without clearing that binding; `reset` clears the session binding only. Details in [DevTools MCP](#devtools-mcp).
|
|
46
|
+
|
|
47
|
+
**Where the tools come from.** On load, a cache hit registers the desktop tools silently — no ssh, no notification. Otherwise the session start decides in the background: with a cache it re-validates the box the declarations came from (session target → `defaultBox` → most recently fetched box, each only if that box has a cache entry); with no cache it lists boxes and takes the first box in `state === "running"`, or — when nothing is running — the first box in the list. **Connecting wakes the box:** every connection, desktop tool calls and this background sync alike, first makes sure the box is running (`instabox wake`, then `instabox start`, then `instabox resume`), so a paused box is woken in the background — no progress UI, the session is not blocked — instead of being skipped. Success is silent: the tools simply appear. You only see a warning when there is no usable sandbox at all (`instabox ls` empty or failing) or when the sync itself fails; both are once per session and the latter names `/instabox-use <box>`.
|
|
48
|
+
|
|
49
|
+
**codemode prerequisite.** `codemode` exposure means the tools are registered and callable, but they are not declared to the model directly — they are listed in codemode's description with their TypeScript declarations (inside codemode's shared 3000-token budget), so a codemode script calls them by name (`tools.instabox_desktop_click({ … })`). `describeTool` / `searchTools` / `describeNamespace` are needed for the DevTools MCP tools only, whose names never enter that description (see [DevTools MCP](#devtools-mcp)). The extension never activates codemode for you, and it only judges the state once desktop tools are registered (at session start, and on every `/instabox-status`). If codemode is inactive and no `codemode`-exposure MCP server exists, you get one warning per session with the fix: start with `pi --tools +codemode`, or set `"defaultTools": ["+codemode"]` in settings. If the session has no `codemode` tool at all (filtered out by `--tools` / `--exclude-tools`, or a built-in extension replaced), a different one-time warning says the desktop tools are not callable in this session. With the DevTools server enabled (`--instabox-devtools`, or `/instabox-devtools on`), Pi activates codemode by itself (see below), so neither warning appears. `/instabox-use` and `/instabox-status` keep working either way.
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
Optional `config.json` (global `<agentDir>/extensions/pi-instabox/config.json`, or project `<cwd>/.pi/extensions/pi-instabox/config.json` when the project is trusted):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"defaultBox": "",
|
|
58
|
+
"devToolsBox": "",
|
|
59
|
+
"autoInstallDevTools": true,
|
|
60
|
+
"remotePath": "/opt/npm/bin:/usr/local/bin:/usr/bin:/bin",
|
|
61
|
+
"devToolsArgs": [
|
|
62
|
+
"--headless",
|
|
63
|
+
"--userDataDir=/data/home/.agent-chrome",
|
|
64
|
+
"--executablePath=/usr/bin/chromium",
|
|
65
|
+
"--viewport=1280x800",
|
|
66
|
+
"--no-usage-statistics"
|
|
67
|
+
],
|
|
68
|
+
"timeouts": {
|
|
69
|
+
"prepareMs": 60000,
|
|
70
|
+
"wakeMs": 20000,
|
|
71
|
+
"startMs": 30000,
|
|
72
|
+
"connectMs": 15000,
|
|
73
|
+
"initializeMs": 15000,
|
|
74
|
+
"listToolsMs": 15000,
|
|
75
|
+
"callMs": 120000
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Key | Default | Meaning |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `defaultBox` | `""` | Late fallback target; empty means unset (falls back to the first `instabox ls` box) |
|
|
83
|
+
| `devToolsBox` | `""` | Default box for `chrome-devtools-mcp` once the DevTools server is switched on; it never enables the server by itself, and both `--instabox-devtools-box` and the session binding (`/instabox-devtools on <box>`) override it |
|
|
84
|
+
| `autoInstallDevTools` | `true` | Try `sudo -n env "PATH=<remotePath>" npm i -g chrome-devtools-mcp` on the box once when it is missing |
|
|
85
|
+
| `remotePath` | `/opt/npm/bin:/usr/local/bin:/usr/bin:/bin` | PATH used inside the box; the defaults cover the npm prefixes seen so far on box templates |
|
|
86
|
+
| `devToolsArgs` | see above | Passed verbatim to `chrome-devtools-mcp` |
|
|
87
|
+
| `timeouts.prepareMs` | `60000` | Deadline for the whole prepare phase of one tool call: state check → wake chain → confirm `running` → connect |
|
|
88
|
+
| `timeouts.wakeMs` | `20000` | Per-step cap for `instabox wake` (self-paused boxes) |
|
|
89
|
+
| `timeouts.startMs` | `30000` | Per-step cap for `instabox start` / `instabox resume` |
|
|
90
|
+
| `timeouts.connectMs` | `15000` | Per-step cap from spawn until the first byte from the remote MCP server |
|
|
91
|
+
| `timeouts.initializeMs` | `15000` | `initialize` request |
|
|
92
|
+
| `timeouts.listToolsMs` | `15000` | `tools/list` request |
|
|
93
|
+
| `timeouts.callMs` | `120000` | `tools/call` request (desktop actions, screenshots included); not part of the prepare budget |
|
|
94
|
+
|
|
95
|
+
`prepareMs` is a deadline, the other fields are single-step caps: every step's timeout is `min(step cap, remaining budget)`, and polling/backoff waits shrink the same way, so a step that hangs cannot stretch the total into the sum of the caps. When the budget runs out the call fails with `prepare timed out after 60000ms (box "<name>", step: wake|start|resume|confirm|connect)` plus that step's `stderr` tail; nothing is retried on another box. `timeouts.callMs` starts after the prepare phase, so it is not contained in `prepareMs`.
|
|
96
|
+
|
|
97
|
+
Layers are applied in order, later wins: built-in defaults → the global `config.json` → the project `config.json` (trusted projects only) → environment variables. Invalid values are ignored, so that layer simply keeps the value below it; `timeouts` merges field by field, and an explicit `"devToolsArgs": []` clears the list.
|
|
98
|
+
|
|
99
|
+
Environment variables (empty means unset): `INSTABOX_MCP_DEFAULT_BOX`, `INSTABOX_MCP_DEVTOOLS_BOX`, `INSTABOX_MCP_AUTO_INSTALL` (`0` or `1`), `INSTABOX_MCP_PATH` (the remote PATH).
|
|
100
|
+
|
|
101
|
+
## Paths
|
|
102
|
+
|
|
103
|
+
`<agentDir>` is Pi's agent directory: `~/.pi/agent` by default, or the value of `PI_CODING_AGENT_DIR` (`~` is expanded).
|
|
104
|
+
|
|
105
|
+
| Path | What |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `<agentDir>/extensions/pi-instabox/config.json` | Configuration (the global layer above) |
|
|
108
|
+
| `<agentDir>/pi-instabox/templates.json` | Per-box template cache: each box's `tools/list` plus the remote `serverInfo.version` and `fetchedAt`. Load-time declarations come from this file alone (no ssh, no notification), and the box they come from is picked cache-first: the session target (only if that box has a cache entry) → `defaultBox` (same condition) → the most recently fetched box in the cache. With an empty cache the session start does not use the resolution chain: it takes the first box in `state === "running"` or, when no box is running, the first box in the list; a running box needs no wake, a paused one is woken in the background first (no progress UI). Each session start then validates the chosen box with one handshake (waking it first if it is paused): the same `serverInfo.version` reuses the cache untouched, a different version refetches `tools/list` and rewrites that box's entry, a 168-hour TTL refetches regardless of version, and a failed validation silently falls back to the cached tools. The version compared is whatever that handshake returns — there is nothing to configure |
|
|
109
|
+
| `<agentDir>/pi-instabox/bin/devtools-connect.sh` | Generated (mode `0755`) helper that connects the remote `chrome-devtools-mcp`. Only written when the DevTools server is actually enabled (`--instabox-devtools` or `/instabox-devtools on`); safe to delete, it comes back with `chmod 0755` on the next load or the next `on` |
|
|
110
|
+
| `<agentDir>/logs/pi-instabox/pi-instabox.jsonl` | Event log (`0700` directory, `0600` file): one JSON record per event, rotated at 1 MiB into `.1` … `.3`. Logging starts at extension load (config and paths resolved, before the CLI probe), so a failed probe is on record too; safe to delete. See [Logs](#logs) |
|
|
111
|
+
|
|
112
|
+
## Logs
|
|
113
|
+
|
|
114
|
+
Every session appends structured events to `<agentDir>/logs/pi-instabox/pi-instabox.jsonl` (directory `0700`, file `0600`) from the moment the extension loads — config and paths are resolved first, then logging is wired, and only then does the `instabox version` probe run. A probe that fails or times out is therefore on record too; a session with no CLI writes exactly one `cli.probe` record and nothing else. Assembly failures also write log records only: no template cache, no helper, no `bin/`.
|
|
115
|
+
|
|
116
|
+
- **Format:** one JSON record per line — `{"ts":"2026-11-09T12:00:00.000Z","level":"info","event":"session.start","fields":{…}}`. Event data lives under `fields` rather than being flattened into the record: `undefined` values are dropped, string values are truncated at 2000 characters, and a field whose key matches `token|secret|password|api[-_]?key` is written as `"[redacted]"`. Messages and payloads are never logged — events carry names, states, counts and timings (`box`, `source`, `mentions`, `candidates`, `ms`, …), never prompt text or tool arguments.
|
|
117
|
+
- **Levels:** `info` by default (lifecycle plus one `resolve` record per tool call, so box selection can be audited after the fact without any switch). `PI_INSTABOX_DEBUG=1` switches the file to `debug`, which additionally records pool state samples, spawn argv, template cache hits and per-tree target changes. `warn`/`error` records are written at both levels, so failures never disappear because you left debug off. `off` exists only in code (`createFileLogger({ level: "off" })`); there is no switch for it.
|
|
118
|
+
- **Rotation:** at 1 MiB the file is rolled into `pi-instabox.jsonl.1` … `pi-instabox.jsonl.3` (`.1` is the newest). The size check runs after the write, so a freshly rolled main file is empty and reappears on the next event.
|
|
119
|
+
- **Write failures never reach the session:** a directory or append failure is reported once through the internal debug channel and swallowed; logging is never a precondition for the tools working.
|
|
120
|
+
|
|
121
|
+
| Event | Level | Fields |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `cli.probe` | info / warn | `ok`, `reason` (`missing` / `timeout` / `error`), `version`, `ms` |
|
|
124
|
+
| `cli.ls` | info | `ok`, `count`, `reason`, `ms` |
|
|
125
|
+
| `cli.lifecycle` | info | `command` (`start` / `wake` / `resume`), `box`, `ok`, `reason`, `ms` |
|
|
126
|
+
| `cli.exec` | info | `box`, `timeoutSec`, `ok`, `ms` |
|
|
127
|
+
| `load.assembled` | info | `cacheHit`, `box`, `tools` (declared at load time) |
|
|
128
|
+
| `session.start` | info | `target`, `source` (session start reason), `mode` |
|
|
129
|
+
| `session.tree` | debug | `target` |
|
|
130
|
+
| `session.shutdown` | info | `open` (connections disposed) |
|
|
131
|
+
| `resolve` | info | `param`, `source` (`param` / `message` / `session` / `config` / `first`), `box`, `mentions`, `candidates` (first 10), `degraded` (a missing `defaultBox` was skipped) |
|
|
132
|
+
| `resolve.fail` | warn | `kind` (`no-boxes` / `invalid-param` / `invalid-session` / `ambiguous` / `invalid-config`), `message`, `candidates` (first 10) |
|
|
133
|
+
| `pool.connect` | info | `box`, `ok`, `ms`, `error` |
|
|
134
|
+
| `pool.prepare` | info / warn | `box`, `ok`, `ms` (the whole prepare phase), `step` (`wake` / `start` / `resume` / `confirm` / `connect`; only when the budget ran out), `timeoutMs` (the budget that step got) |
|
|
135
|
+
| `pool.wake` | info / warn | `box`, `step` (`wake` / `start` / `resume`), `ok`, `ms`, `timeoutMs` (that step's budget) |
|
|
136
|
+
| `pool.state` | debug | `box`, `state`, `desiredState` |
|
|
137
|
+
| `pool.reuse` | debug | `box` |
|
|
138
|
+
| `pool.dispose` | debug | `box` |
|
|
139
|
+
| `remote.spawn` | debug | `command`, `args` |
|
|
140
|
+
| `remote.initialize` | info | `ok`, `ms`, `protocolVersion`, `serverInfoVersion` |
|
|
141
|
+
| `remote.listTools` | info | `ok`, `count`, `ms` |
|
|
142
|
+
| `remote.call` | info | `tool`, `ok`, `ms`, `isError` |
|
|
143
|
+
| `template.hit` | debug | `box`, `serverInfoVersion`, `stale` (TTL expired) |
|
|
144
|
+
| `template.refetch` | info | `box`, `reason`, `ms`, `tools` |
|
|
145
|
+
| `template.fail` | warn | `box`, `error`, `stale` (a cached template was kept) |
|
|
146
|
+
| `tool.call` | info | `tool`, `box`, `ok`, `ms`, `isError` |
|
|
147
|
+
| `codemode` | debug | `state` (`active` / `inactive` / `missing`), `server` (a codemode-exposure MCP server exists) |
|
|
148
|
+
| `devtools.register` | info | `box`, `ok` |
|
|
149
|
+
| `devtools.enabled` | info | `box`, `source` (`session` / `flag` / `config` / `command`) |
|
|
150
|
+
| `devtools.disabled` | info | — (no fields) |
|
|
151
|
+
| `devtools.reset` | info | — (no fields) |
|
|
152
|
+
| `devtools.helper` | info | `ok`, `path`, `error` |
|
|
153
|
+
| `error` | warn | `where`, `message` (a swallowed failure: session branch, `session_start` background task, `ui.notify`) |
|
|
154
|
+
|
|
155
|
+
**Diagnosing a wrong box.** The successful `resolve` records are `info`-level, so they are on disk at the default level — no switch needed, just filter:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
grep '"event":"resolve"' ~/.pi/agent/logs/pi-instabox/pi-instabox.jsonl
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each record says which of the five levels won (`source`), which box that produced (`box`), every box name found in the last user message (`mentions` — recorded even when it is empty, and empty is exactly what sends the call to the next level), the names that were in play (`candidates`) and whether a stale `defaultBox` was skipped (`degraded`). Hard failures land right next to them as `resolve.fail` with `kind` and `candidates`: an unknown `box` parameter, an unknown session target, a message naming two boxes, or an empty box list. `PI_INSTABOX_DEBUG=1` adds the detail around the decision (pool state samples, spawn argv, template hits), not the decision itself.
|
|
162
|
+
|
|
163
|
+
## DevTools MCP
|
|
164
|
+
|
|
165
|
+
The remote `chrome-devtools-mcp` server is **off by default**. Three inputs decide whether it exists in a session:
|
|
166
|
+
|
|
167
|
+
| Input | Meaning |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `--instabox-devtools` | Switch the capability on (boolean, default `false`) |
|
|
170
|
+
| `--instabox-devtools-box <name>` | The box that hosts the server; beats `devToolsBox`, loses to the session binding (string, default `""`) |
|
|
171
|
+
| `/instabox-devtools [status\|on [box]\|off\|reset]` | The runtime toggle and the session binding (see below) |
|
|
172
|
+
|
|
173
|
+
The box is resolved in this order: the **session binding** written by `/instabox-devtools on <box>` (a branch-scoped session entry, so it survives `--continue` and follows `/tree`), then `--instabox-devtools-box`, then `config.devToolsBox`. With none of the three — and the switch on — one warning says the box is unspecified and **no server is registered**. A box that `instabox ls` does not list is refused with one warning naming the candidates — never silently swapped for another box. Passing only `--instabox-devtools-box` without the switch warns once and enables nothing; `config.devToolsBox` alone is silent, because the config only supplies a default box.
|
|
174
|
+
|
|
175
|
+
With the switch on, the extension writes the helper and registers one MCP server named `instabox-devtools` whose `command` is that helper, so the remote `chrome-devtools-mcp` runs inside that fixed box over `instabox ssh`. Its `exposure` is `codemode` with a 180-second per-request timeout; before connecting, the helper makes sure the box is running (`instabox wake`, falling back to `instabox start`) — the MCP client never wakes a box itself, so a paused box would otherwise cost the whole session. If the binary is missing on the box and `autoInstallDevTools` is on, the helper then tries one `sudo -n env "PATH=<remotePath>" npm i -g chrome-devtools-mcp` (sudo rewrites PATH from `secure_path`, so the remote PATH is passed through explicitly), and all of its diagnostics go to stderr so the JSON-RPC framing on stdout stays clean. The budgets sit inside that timeout: each `wake`/`start` gets `timeout 60` when the box has the `timeout` command (the helper's own copy of the 60 s prepare budget), the presence check gets 10 s and the install 40 s, all thrown away in `/dev/null`.
|
|
176
|
+
|
|
177
|
+
**`/instabox-devtools`** with no argument is `status`, and it prints five lines: the switch (`instabox-devtools: on (--instabox-devtools)` / `instabox-devtools: off (--instabox-devtools)`), the server registered in this session (`registration: <name> (via <source>)`, or `registration: (none)` when nothing is registered; `<source>` is `command` for a server registered by this command, otherwise the level that supplied the box — `session` / `flag` / `config`), the box bound to this session (`session binding: <name>` / `session binding: (unset)`), the box the next `on` or session start would use together with its source (`effective box: <name> (<source>)` / `effective box: (unset)`; the levels resolve `session` → `flag` → `config`), and the candidate boxes (`candidates: …`). The three middle lines stay separate on purpose: `reset` clears the session binding only, so `registration` keeps naming the server that is still running while `effective box` already shows where the next `on` would land. `on [box]` registers the server for that box (argument first, then the session binding, `--instabox-devtools-box`, `devToolsBox`) and, when a box argument is given, stores it as the session binding; re-running `on` unregisters the old registration first, so the connection is new instead of reused. An unknown box name is refused and the current state is kept. `off` unregisters the server for the rest of the session: its tools become `exposure: "hidden"` (Pi cannot unregister a tool) and therefore disappear from codemode's `ALL_TOOLS` and `searchTools`, calls that are already in flight fail, earlier transcript messages do not change, and a later `on` opens a fresh connection. `off` does **not** clear the session binding. `reset` clears the session binding only: the next `on` (and the next session start with the switch on) falls back to `--instabox-devtools-box` → `config.devToolsBox`, the switch keeps its state, and an already registered server stays registered.
|
|
178
|
+
|
|
179
|
+
Its tools — DOM/accessibility snapshots, element screenshots, CSS cascade, network, console, traces — are not declared to the model. The model discovers them from a codemode script:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const ns = describeNamespace("mcp__instabox_devtools"); // { name, tools }
|
|
183
|
+
// or: searchTools("devtools")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Because this server has `codemode` exposure, Pi activates codemode automatically while it is registered, which is also why desktop tools become reachable in the same session.
|
|
187
|
+
|
|
188
|
+
**If the first connection fails, Pi does not retry it.** The MCP server stays dead for the rest of the session, so the namespace yields nothing until you reconnect it with `/mcp`, run `/instabox-devtools on <box>` again (that registration is fresh), or restart the session. The helper therefore does its own recovery before handing over to `instabox ssh`: `wake` first, `start` if that fails. A wake/start that fails too is not fatal — the following step reports the real error on stderr.
|
|
189
|
+
|
|
190
|
+
## Compatibility & Limitations
|
|
191
|
+
|
|
192
|
+
- **Host floor:** `pi-coding-agent >= 0.99.0` (see Requirements). The package pins the types it is compiled against, so an upgrade shows up as a `check` failure rather than a runtime surprise.
|
|
193
|
+
- **Your tool selection is yours:** the extension never calls `setActiveTools`, never writes `settings.json` and never edits `defaultTools`. It only reports the codemode state and the command that fixes it.
|
|
194
|
+
- **codemode inactive means uncallable:** the tools are registered but unreachable while codemode is off, and the model cannot see their names outside codemode. See the prerequisite in Usage.
|
|
195
|
+
- **Tool sets follow the template, not the extension:** definitions come from the cached `tools/list` of the box they came from. Different boxes may run different `instabox-computer-use` versions, so the set and the schemas can differ per box. Validation is one handshake per session start: the same remote version keeps the cached tools as they are, a different version refetches `tools/list` and rewrites the entry, and a 168-hour TTL refetches on its own. Names are stable (`instabox_desktop_<remote_name>`), so a codemode script that names a tool keeps working across versions.
|
|
196
|
+
- **Quiet by design:** a successful background sync says nothing at all — the tools simply show up, and the transcript carries no notification about the declaration. The only session-start warnings are "no usable sandbox" (`ls` empty or failing), a failed sync naming `/instabox-use <box>`, and a misconfigured DevTools switch (box missing, box not in `ls`, or `--instabox-devtools-box` without `--instabox-devtools`). Internal `[pi-instabox] …` stderr output and the `debug`-level half of the JSONL log (see [Logs](#logs)) are off by default; set `PI_INSTABOX_DEBUG=1` to see wakes, spawn argv and handshake internals. Box selection needs no switch: each tool call writes its `resolve` record at `info`.
|
|
197
|
+
- **A fully paused fleet is still woken:** when no box is running, the first box from `instabox ls` is woken in the background and its template fetched — the wake happens on connect, so a cached box that is paused gets woken for the version check too; a wake failure degrades to that one warning, and the session is never blocked or held up.
|
|
198
|
+
- **Desktop screenshots are expensive:** for browser work prefer the DevTools server when one is enabled; the namespace instructions tell the model the same.
|
|
199
|
+
- **`box` covers desktop tools only:** it selects the sandbox for `instabox_desktop_*` calls. The DevTools server is bound to one box — session binding first, then `--instabox-devtools-box`, then `devToolsBox`; switch it at runtime with `/instabox-devtools on <box>` (which also updates the session binding), no reload needed, and `/instabox-devtools reset` drops the binding again.
|
|
200
|
+
- **`off` leaves names behind:** `/instabox-devtools off` only hides the server's tools. Tool names that already appear in the transcript can still be tried by the model and then fail, because the connection is gone and Pi does not reload history.
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pnpm --filter @inobit/pi-instabox check # tsc --noEmit
|
|
206
|
+
pnpm --filter @inobit/pi-instabox test # vitest
|
|
207
|
+
pnpm --filter @inobit/pi-instabox pack:check # tarball contents
|
|
208
|
+
pi -ne -e builtin:codemode -e builtin:mcp -e ./packages/pi-instabox
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The smoke command has to bring the built-ins back: `-ne` / `--no-extensions` disables `codemode` and `mcp` along with every installed extension, and without them the desktop tools cannot be called and `registerMcpServer` fails. Unit tests stay offline — every external edge (`exec`, `spawn`, filesystem, clock, `Text`) is injectable, so no test spawns `instabox`, connects to a box or touches the network.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @inobit/pi-instabox
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | **中文**
|
|
4
|
+
|
|
5
|
+
在 Pi 会话里驱动 instabox 沙箱:九个 `instabox_desktop_*` 桌面工具,每次调用都明说自己打到哪台箱;另有一个绑定单台箱的远端 `chrome-devtools-mcp` server,**默认关闭**,需 `--instabox-devtools` 才开启。
|
|
6
|
+
|
|
7
|
+
- **每次调用都点名箱**:可选的 `box` 参数(或五级目标解析链)决定哪台沙箱执行,且每条成功结果首行固定为 `[box=<name>]`
|
|
8
|
+
- **不静默降级**:箱名写错或有歧义就是错误并列出候选;唯一会降级的是失效的 `defaultBox`(每会话一条 warning)
|
|
9
|
+
- **schema 来自远端**:工具定义由目标箱的 `tools/list` 生成并按箱缓存,加载期的工具声明不需要 ssh
|
|
10
|
+
- **不碰你的工具选择集**:桌面工具是 `exposure: "codemode"`,扩展从不调用 `setActiveTools`
|
|
11
|
+
|
|
12
|
+
## 环境要求
|
|
13
|
+
|
|
14
|
+
- **`@earendil-works/pi-coding-agent >= 0.99.0`**——硬门槛。本包依赖的 `exposure`、`namespace`、`registerMcpServer` 与 codemode/MCP 支持自 0.99.0 起才有,更早的宿主无法加载。
|
|
15
|
+
- Node.js **>= 24**。
|
|
16
|
+
- `PATH` 上有 `instabox` CLI(`instabox version`、`instabox ls --json`、`instabox ssh`、`instabox wake|start|resume`、`instabox exec`)。没有 CLI 时扩展零注册——无工具、无命令、无 MCP server、不写运行期文件(无模板缓存、无 helper)——只每会话给一条 warning;唯一的落盘产物是事件日志里那一条 `cli.probe`。若只是首次 `instabox version` 探测超时,会在会话启动时用更大预算重试一次再下这个结论。
|
|
17
|
+
- 沙箱模板带 `instabox-computer-use`(本扩展通过 `instabox ssh <box> -- instabox-computer-use mcp` 驱动这个远端 MCP server)。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install npm:@inobit/pi-instabox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
重启 Pi 会话生效。
|
|
26
|
+
|
|
27
|
+
## 用法
|
|
28
|
+
|
|
29
|
+
**桌面工具。** 目标箱远端 `tools/list` 的每个条目变成一个 pi 工具,名为 `instabox_desktop_<远端名>`(本包面向的 `instabox-computer-use` 是九个,如 `instabox_desktop_click`),label 为 `instabox desktop <远端名>`,统一挂在 `instabox-desktop` 命名空间(别名 `instabox_desktop`)下,`exposure: "codemode"`。
|
|
30
|
+
|
|
31
|
+
每个工具都有一个可选 `box` 参数。不传时按下面的顺序解析目标,先命中先停:
|
|
32
|
+
|
|
33
|
+
1. 本次调用的 `box` 参数;
|
|
34
|
+
2. 当前分支里最后一条 user 消息提到的唯一箱名;
|
|
35
|
+
3. 会话目标(由 `/instabox-use` 设置);
|
|
36
|
+
4. 配置里的 `defaultBox`;
|
|
37
|
+
5. `instabox ls` 返回的第一台。
|
|
38
|
+
|
|
39
|
+
第 1–3 级都是严格的:写错的名字是硬错误,消息里出现多个箱名是 `ambiguous`——都不猜、也都不退到下一级。只有第 4 级会降级(退到第 5 级,带一条 warning)。每条成功结果首行是 `[box=<name>]`;远端内容(文本与图片)原样透传,远端报错时保留 `isError`,并追加一行说明「未重试、未换箱」及两条补救。
|
|
40
|
+
|
|
41
|
+
**`/instabox-use [box]`**——设置或查看本会话的目标箱。不带参数时打印当前目标与候选箱;箱名不存在则拒绝,原目标保留。目标以一条自定义会话条目存储(不进 LLM 上下文,按分支生效,因此 `/tree` 切分支与 `--continue` 都能恢复)。若当时还没注册任何桌面工具(冷启动、无缓存),这条命令还会唤醒该箱、拉取 `tools/list` 并注册工具——这就是会话启动时箱子没在跑的重试路径。
|
|
42
|
+
|
|
43
|
+
**`/instabox-status`**——一次性诊断:配置摘要(`defaultBox`、`devToolsBox`、`autoInstallDevTools`、`remotePath`——那里出现的 `devToolsBox` 只是 DevTools 的默认箱,不是开关)、会话目标、解析出的目标及其来源、已注册的 `instabox_desktop_*` 工具与 exposure、当前声明来自哪台箱的缓存模板(`tool declarations: from the cached template of box "…"`)、active tools、codemode 状态,以及扩展注册的 MCP server——`instabox-devtools` 只在 DevTools server 开着时出现在最后这一段;开关本身由 `/instabox-devtools status` 报告(见 [DevTools MCP](#devtools-mcp))。
|
|
44
|
+
|
|
45
|
+
**`/instabox-devtools [status|on [box]|off|reset]`**——远端 DevTools MCP server 的运行期开关:不带参数(或 `status`)显示开关、本会话已注册的 server、会话绑定的箱,以及下一次将使用的箱及其来源(`session` / `flag` / `config`);`on [box]` 注册或替换,给了箱名就同时把它绑到本会话;`off` 在本会话内注销它、但**不清除**该绑定;`reset` 只清会话绑定。细节见 [DevTools MCP](#devtools-mcp)。
|
|
46
|
+
|
|
47
|
+
**工具声明的来源。**加载期命中缓存就静默注册桌面工具——零 ssh、零提示。否则会话启动在后台替你决定:有缓存时对「声明来源」那台箱做一次版本校验(会话目标 → `defaultBox` → 缓存里最近拉取的那台,均要求该箱有缓存条目);没有缓存时先 `ls`,取**第一台 `state === "running"`** 的箱,一台 running 都没有时取列表第一台。**连接即唤醒**:无论桌面工具调用还是这个后台同步,建连前都先确保箱子在跑(`instabox wake`、再 `instabox start`、再 `instabox resume`),因此暂停的箱会在后台被唤醒——不显示进度、不阻塞会话——而不是被跳过。成功一律静默:工具出现就是唯一的成功信号。只有真的同步不了才提示:`instabox ls` 为空或失败 → 一条「没有可用沙箱」;同步本身失败 → 一条带 `/instabox-use <box>` 的提示,都是每会话一次。
|
|
48
|
+
|
|
49
|
+
**codemode 前提。** `codemode` exposure 的含义是:工具已注册、可调用,但不直接声明给模型——它们会带 TypeScript 声明列在 codemode 的描述里(与其他 codemode 工具共享那 3000 token 预算),脚本按名字直接调用即可(`tools.instabox_desktop_click({ … })`)。`describeTool`、`searchTools`、`describeNamespace` 只用于 DevTools 的 MCP 工具,那些工具的名字不进这段描述(见 [DevTools MCP](#devtools-mcp))。扩展不会替你激活 codemode,且只在**已注册桌面工具**时才判定状态(会话启动一次,`/instabox-status` 每次实时读)。若 codemode 未激活、且会话里没有 `codemode` exposure 的 MCP server,你会收到每会话一条 warning 并附带补救命令:用 `pi --tools +codemode` 启动,或在 settings 里设 `"defaultTools": ["+codemode"]`。若本会话根本没有 `codemode` 工具条目(被 `--tools` / `--exclude-tools` 剔除,或内置扩展被替换),则用另一条一次性文案明说桌面工具在本会话不可调用。配置了 DevTools server 时(`--instabox-devtools` 或 `/instabox-devtools on`),pi 会自动激活 codemode(见下文),因此两条提示都不会出现。两种情况下 `/instabox-use` 与 `/instabox-status` 都可用。
|
|
50
|
+
|
|
51
|
+
## 配置
|
|
52
|
+
|
|
53
|
+
可选 `config.json`(全局 `<agentDir>/extensions/pi-instabox/config.json`,或 trusted 时项目 `<cwd>/.pi/extensions/pi-instabox/config.json`):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"defaultBox": "",
|
|
58
|
+
"devToolsBox": "",
|
|
59
|
+
"autoInstallDevTools": true,
|
|
60
|
+
"remotePath": "/opt/npm/bin:/usr/local/bin:/usr/bin:/bin",
|
|
61
|
+
"devToolsArgs": [
|
|
62
|
+
"--headless",
|
|
63
|
+
"--userDataDir=/data/home/.agent-chrome",
|
|
64
|
+
"--executablePath=/usr/bin/chromium",
|
|
65
|
+
"--viewport=1280x800",
|
|
66
|
+
"--no-usage-statistics"
|
|
67
|
+
],
|
|
68
|
+
"timeouts": {
|
|
69
|
+
"prepareMs": 60000,
|
|
70
|
+
"wakeMs": 20000,
|
|
71
|
+
"startMs": 30000,
|
|
72
|
+
"connectMs": 15000,
|
|
73
|
+
"initializeMs": 15000,
|
|
74
|
+
"listToolsMs": 15000,
|
|
75
|
+
"callMs": 120000
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| 键 | 默认值 | 含义 |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `defaultBox` | `""` | 解析链靠后的兜底目标;空串=未配置(退到 `instabox ls` 第一台) |
|
|
83
|
+
| `devToolsBox` | `""` | DevTools server 开启后 `chrome-devtools-mcp` 的默认箱;它本身不开启该能力,且 `--instabox-devtools-box` 与会话绑定(`/instabox-devtools on <box>`)都会覆盖它 |
|
|
84
|
+
| `autoInstallDevTools` | `true` | 远端缺 `chrome-devtools-mcp` 时尝试一次 `sudo -n env "PATH=<remotePath>" npm i -g chrome-devtools-mcp` |
|
|
85
|
+
| `remotePath` | `/opt/npm/bin:/usr/local/bin:/usr/bin:/bin` | 箱内使用的 PATH;默认值覆盖目前已在箱模板上见到的 npm 前缀 |
|
|
86
|
+
| `devToolsArgs` | 见上 | 原样透传给 `chrome-devtools-mcp` |
|
|
87
|
+
| `timeouts.prepareMs` | `60000` | 一次工具调用准备阶段的截止时间:查状态 → 唤醒链 → 确认 `running` → 建连 |
|
|
88
|
+
| `timeouts.wakeMs` | `20000` | `instabox wake` 的单步上限(自动暂停的箱) |
|
|
89
|
+
| `timeouts.startMs` | `30000` | `instabox start` / `instabox resume` 的单步上限 |
|
|
90
|
+
| `timeouts.connectMs` | `15000` | 从 spawn 到远端 MCP server 吐出第一个字节的单步上限 |
|
|
91
|
+
| `timeouts.initializeMs` | `15000` | `initialize` 请求 |
|
|
92
|
+
| `timeouts.listToolsMs` | `15000` | `tools/list` 请求 |
|
|
93
|
+
| `timeouts.callMs` | `120000` | `tools/call` 请求(含截图的桌面操作);不在准备预算里 |
|
|
94
|
+
|
|
95
|
+
`prepareMs` 是截止时间,其余字段只是单步上限:每一步的超时取 `min(单步上限, 剩余预算)`,轮询与退避的等待也按剩余预算收窄,所以一步卡住不会把总时长顶成各步上限之和。预算耗尽时调用报 `prepare timed out after 60000ms (box "<名称>", step: wake|start|resume|confirm|connect)` 并带上该步 `stderr` 尾部,不换箱、不重试。`timeouts.callMs` 从准备阶段之后开始计时,因此不包含在 `prepareMs` 里。
|
|
96
|
+
|
|
97
|
+
叠加顺序后者覆盖前者:内置默认值 → 全局 `config.json` → 项目 `config.json`(仅 trusted 项目)→ 环境变量。非法值被忽略,即该层保留下一层的值;`timeouts` 逐字段合并,显式写 `"devToolsArgs": []` 表示清空。
|
|
98
|
+
|
|
99
|
+
环境变量(空串=未设置):`INSTABOX_MCP_DEFAULT_BOX`、`INSTABOX_MCP_DEVTOOLS_BOX`、`INSTABOX_MCP_AUTO_INSTALL`(`0` / `1`)、`INSTABOX_MCP_PATH`(远端 PATH)。
|
|
100
|
+
|
|
101
|
+
## 路径
|
|
102
|
+
|
|
103
|
+
`<agentDir>` 是 pi 的 agent 目录:默认 `~/.pi/agent`,可用 `PI_CODING_AGENT_DIR` 覆盖(`~` 会展开)。
|
|
104
|
+
|
|
105
|
+
| 路径 | 内容 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `<agentDir>/extensions/pi-instabox/config.json` | 配置文件(即上文的全局层) |
|
|
108
|
+
| `<agentDir>/pi-instabox/templates.json` | per-box 模板缓存:每台箱的 `tools/list` 加远端 `serverInfo.version` 与 `fetchedAt`。加载期的工具声明只读这个文件(零 ssh、零提示),声明来自哪台箱是**缓存优先**的:会话目标(该箱有缓存条目才算)→ `config.defaultBox`(同上)→ 缓存里最近拉取的那台。**缓存为空时后台不走解析链**:取第一台 `state === "running"` 的箱,一台 running 都没有时取列表第一台;running 的箱不需要唤醒,暂停的箱会先在后台被唤醒(不显示进度)。随后每次会话启动用一次握手校验该箱(暂停时会先唤醒它):`serverInfo.version` 相同就复用缓存、不动磁盘,不同则重拉 `tools/list` 并重写该箱条目;另有 168 小时 TTL 兜底(超期无论版本是否相同都会重拉),校验失败则静默沿用缓存。比对用的就是这次握手返回的 `serverInfo.version`,无需额外配置 |
|
|
109
|
+
| `<agentDir>/pi-instabox/bin/devtools-connect.sh` | 生成的 helper(权限 `0755`),用于连接远端 `chrome-devtools-mcp`。只在 DevTools server 真的开启时(`--instabox-devtools` 或 `/instabox-devtools on`)写入;可以删,下次加载或下次 `on` 会重新生成并 `chmod 0755` |
|
|
110
|
+
| `<agentDir>/logs/pi-instabox/pi-instabox.jsonl` | 事件日志(目录 `0700`、文件 `0600`):一行一事件,超 1 MiB 轮转为 `.1` … `.3`。**扩展加载时**(配置与路径解析完、CLI 探测前)就开始记,因此探测失败也有记录;可以删。见[日志](#日志) |
|
|
111
|
+
|
|
112
|
+
## 日志
|
|
113
|
+
|
|
114
|
+
每个会话从**扩展加载时**起就往 `<agentDir>/logs/pi-instabox/pi-instabox.jsonl` 追加结构化事件(目录 `0700`、文件 `0600`):先解析配置与路径,随即接上日志,然后才跑 `instabox version` 探测——所以探测失败/超时也有记录;没有 CLI 的会话只有 `cli.probe` 那一条,别的不写。装配失败同样只写日志,不建模板缓存、不写 helper、不建 `bin/`。
|
|
115
|
+
|
|
116
|
+
- **格式:**一行一条 JSON —— `{"ts":"2026-11-09T12:00:00.000Z","level":"info","event":"session.start","fields":{…}}`。事件数据在 `fields` 子对象里,不摊平到顶层:`undefined` 字段被丢掉,字符串截断到 2000 字符,键名命中 `token|secret|password|api[-_]?key` 的字段整体写成 `"[redacted]"`。**消息正文与调用参数从不写盘**——事件只带名字、状态、计数与耗时(`box`、`source`、`mentions`、`candidates`、`ms` 等)。
|
|
117
|
+
- **级别:**默认 `info`(生命周期 + 每一次工具调用一条 `resolve`,选箱可事后复盘,不需要任何开关)。`PI_INSTABOX_DEBUG=1` 把文件切到 `debug`,额外记池状态采样、spawn 参数、模板缓存命中与 `session_tree` 的目标切换。`warn` / `error` 记录在两个级别下都写,所以「忘了开 debug」不会让失败消失。`off` 只存在于代码层(`createFileLogger({ level: "off" })`),没有对外开关。
|
|
118
|
+
- **轮转:**到 1 MiB 就滚动成 `pi-instabox.jsonl.1` … `pi-instabox.jsonl.3`(`.1` 最新)。大小判定在**写入之后**,所以刚滚动完的主文件是空的,下一条事件写入时重新出现。
|
|
119
|
+
- **写盘失败不进会话:**建目录或追加失败只经内部 debug 渠道报一次然后吞掉;日志永远不是工具可用的前提。
|
|
120
|
+
|
|
121
|
+
| 事件 | 级别 | 字段 |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `cli.probe` | info / warn | `ok`、`reason`(`missing` / `timeout` / `error`)、`version`、`ms` |
|
|
124
|
+
| `cli.ls` | info | `ok`、`count`、`reason`、`ms` |
|
|
125
|
+
| `cli.lifecycle` | info | `command`(`start` / `wake` / `resume`)、`box`、`ok`、`reason`、`ms` |
|
|
126
|
+
| `cli.exec` | info | `box`、`timeoutSec`、`ok`、`ms` |
|
|
127
|
+
| `load.assembled` | info | `cacheHit`、`box`、`tools`(加载期声明的工具数) |
|
|
128
|
+
| `session.start` | info | `target`、`source`(会话启动原因)、`mode` |
|
|
129
|
+
| `session.tree` | debug | `target` |
|
|
130
|
+
| `session.shutdown` | info | `open`(收掉的连接数) |
|
|
131
|
+
| `resolve` | info | `param`、`source`(`param` / `message` / `session` / `config` / `first`)、`box`、`mentions`、`candidates`(前 10 个)、`degraded`(跳过了失效的 `defaultBox`) |
|
|
132
|
+
| `resolve.fail` | warn | `kind`(`no-boxes` / `invalid-param` / `invalid-session` / `ambiguous` / `invalid-config`)、`message`、`candidates`(前 10 个) |
|
|
133
|
+
| `pool.connect` | info | `box`、`ok`、`ms`、`error` |
|
|
134
|
+
| `pool.prepare` | info / warn | `box`、`ok`、`ms`(整个准备阶段)、`step`(`wake` / `start` / `resume` / `confirm` / `connect`;只在预算耗尽时出现)、`timeoutMs`(该步实际拿到的预算) |
|
|
135
|
+
| `pool.wake` | info / warn | `box`、`step`(`wake` / `start` / `resume`)、`ok`、`ms`、`timeoutMs`(该步实际拿到的预算) |
|
|
136
|
+
| `pool.state` | debug | `box`、`state`、`desiredState` |
|
|
137
|
+
| `pool.reuse` | debug | `box` |
|
|
138
|
+
| `pool.dispose` | debug | `box` |
|
|
139
|
+
| `remote.spawn` | debug | `command`、`args` |
|
|
140
|
+
| `remote.initialize` | info | `ok`、`ms`、`protocolVersion`、`serverInfoVersion` |
|
|
141
|
+
| `remote.listTools` | info | `ok`、`count`、`ms` |
|
|
142
|
+
| `remote.call` | info | `tool`、`ok`、`ms`、`isError` |
|
|
143
|
+
| `template.hit` | debug | `box`、`serverInfoVersion`、`stale`(TTL 已过) |
|
|
144
|
+
| `template.refetch` | info | `box`、`reason`、`ms`、`tools` |
|
|
145
|
+
| `template.fail` | warn | `box`、`error`、`stale`(沿用了缓存模板) |
|
|
146
|
+
| `tool.call` | info | `tool`、`box`、`ok`、`ms`、`isError` |
|
|
147
|
+
| `codemode` | debug | `state`(`active` / `inactive` / `missing`)、`server`(存在 codemode exposure 的 MCP server) |
|
|
148
|
+
| `devtools.register` | info | `box`、`ok` |
|
|
149
|
+
| `devtools.enabled` | info | `box`、`source`(`session` / `flag` / `config` / `command`) |
|
|
150
|
+
| `devtools.disabled` | info | ——(无字段) |
|
|
151
|
+
| `devtools.reset` | info | ——(无字段) |
|
|
152
|
+
| `devtools.helper` | info | `ok`、`path`、`error` |
|
|
153
|
+
| `error` | warn | `where`、`message`(被吞掉的失败:会话分支、`session_start` 后台任务、`ui.notify`) |
|
|
154
|
+
|
|
155
|
+
**排查一次选错箱。**成功的 `resolve` 是 `info` 级,默认级别下就已经落盘——不用切级别,直接过滤:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
grep '"event":"resolve"' ~/.pi/agent/logs/pi-instabox/pi-instabox.jsonl
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
每条记录说明:五级里哪一级命中(`source`)、命中的是哪台箱(`box`)、本轮 user 消息里出现了哪些箱名(`mentions`——**零命中也记**,而零命中正是「落到下一级」的原因)、当时备选的名字(`candidates`)、是否跳过了失效的 `defaultBox`(`degraded`)。硬失败就在旁边,以 `resolve.fail` 落盘,带 `kind` 与 `candidates`:`box` 参数不存在、会话目标不存在、消息里提到两台箱、或箱列表为空。`PI_INSTABOX_DEBUG=1` 打开的是决策周边的细节(池状态采样、spawn 参数、模板命中),不是决策本身。
|
|
162
|
+
|
|
163
|
+
## DevTools MCP
|
|
164
|
+
|
|
165
|
+
远端 `chrome-devtools-mcp` server **默认关闭**。三个入口决定它在一次会话里存不存在:
|
|
166
|
+
|
|
167
|
+
| 入口 | 含义 |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `--instabox-devtools` | 打开该能力(boolean,默认 `false`) |
|
|
170
|
+
| `--instabox-devtools-box <name>` | 承载该 server 的箱;覆盖 `devToolsBox`、但低于会话绑定(string,默认 `""`) |
|
|
171
|
+
| `/instabox-devtools [status\|on [box]\|off\|reset]` | 运行期开关与会话绑定(见下文) |
|
|
172
|
+
|
|
173
|
+
箱子按三级取,先命中先用:`/instabox-devtools on <box>` 写下的**会话绑定**(按分支生效的 session entry,`--continue` 能恢复、`/tree` 切分支会跟着变)→ `--instabox-devtools-box` → `config.devToolsBox`。三者都没配时(且开关开着)给一条 warning 说箱名缺失,**不注册**。箱不在 `instabox ls` 里也是 warning + 不注册并列出候选——绝不静默换一台。只给 `--instabox-devtools-box` 而不给开关,会有一条 warning 提示加开关、什么都不启用;只配 `config.devToolsBox` 则完全静默,因为配置只负责提供默认箱。
|
|
174
|
+
|
|
175
|
+
开关打开后,扩展写 helper,并注册一个名为 `instabox-devtools` 的 MCP server,其 `command` 就是该 helper,于是远端 `chrome-devtools-mcp` 在这台固定箱里经 `instabox ssh` 运行。它的 `exposure` 是 `codemode`,每请求超时 180 秒;连接前 helper 会先把目标箱拉起来(`instabox wake`,失败再 `instabox start`)——MCP 客户端自己从不唤醒箱,一台暂停的箱否则会赔上整场会话。箱上缺该二进制且 `autoInstallDevTools` 开启时,helper 接着尝试一次 `sudo -n env "PATH=<remotePath>" npm i -g chrome-devtools-mcp`(sudo 会按 `secure_path` 重写 PATH,所以远端 PATH 显式再传一遍),其全部诊断信息只写 stderr,避免污染 stdout 上的 JSON-RPC 帧。预算都在那个超时之内:每条 `wake`/`start` 在有 `timeout` 命令时包一层 `timeout 60`(helper 自己那份 60 秒准备预算),检查 10 秒、安装 40 秒,输出一律丢进 `/dev/null`。
|
|
176
|
+
|
|
177
|
+
**`/instabox-devtools`** 不带参数就是 `status`,打印五行:开关(`instabox-devtools: on (--instabox-devtools)` / `instabox-devtools: off (--instabox-devtools)`)、本会话已注册的 server(`registration: <名称> (via <来源>)`,未注册时为 `registration: (none)`;`<来源>` 本命令注册的取 `command`,其余取提供箱名的那一级 `session` / `flag` / `config`)、本会话绑定的箱(`session binding: <名称>` / `session binding: (unset)`)、下一次 `on` 或会话启动将使用的箱及其来源(`effective box: <名称> (<来源>)` / `effective box: (unset)`;三级按 `session` → `flag` → `config` 解析)、候选箱(`candidates: …`)。中间三段分开报是有意的:`reset` 只清会话绑定,所以 `registration` 仍指向那台还在跑的 server,而 `effective box` 已经回退到下一次会用的箱。`on [box]` 为该箱注册 server(箱名依次取参数、会话绑定、`--instabox-devtools-box`、`devToolsBox`),给了箱名参数时同时把它写成会话绑定;重复执行 `on` 会先注销旧注册再重注,所以拿到的是新连接而不是复用旧连接。箱名不存在则拒绝且保持现状。`off` 在本会话内注销该 server:它的工具变成 `exposure: "hidden"`(pi 不支持反注册工具),因此从 codemode 的 `ALL_TOOLS` 与 `searchTools` 里消失,已经发出的调用会报错,但转录里已有的消息不变;之后再 `on` 会建立新连接。`off` **不清除**会话绑定。`reset` 只清会话绑定:下一次 `on`(以及开关开着时的下一次会话启动)回退到 `--instabox-devtools-box` → `config.devToolsBox`,开关状态与已注册的 server 都不动。
|
|
178
|
+
|
|
179
|
+
它的工具——DOM/无障碍快照、元素截图、CSS 级联、网络、console、性能 trace——不会声明给模型。模型在 codemode 脚本里发现它们:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const ns = describeNamespace("mcp__instabox_devtools"); // { name, tools }
|
|
183
|
+
// 或: searchTools("devtools")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
正因为这个 server 是 `codemode` exposure,它在注册期间会让 pi 自动激活 codemode,这也是同一会话里桌面工具变得可调用的原因。
|
|
187
|
+
|
|
188
|
+
**首次连接失败时 pi 不会自己重试。**该 MCP server 会一直处于失败状态直到你用 `/mcp` 重连、再执行一次 `/instabox-devtools on <box>`(这是一次全新注册)或重启会话,期间命名空间里拿不到任何工具。所以 helper 在交给 `instabox ssh` 之前先自己补救一次:先 `wake`,失败再 `start`;这一步也失败不阻断,真正的原因由后续步骤原样报到 stderr。
|
|
189
|
+
|
|
190
|
+
## 兼容与限制
|
|
191
|
+
|
|
192
|
+
- **宿主门槛:**`pi-coding-agent >= 0.99.0`(见环境要求)。本包钉死编译所用类型,升级只会体现为 `check` 失败,而不是运行期意外。
|
|
193
|
+
- **工具选择集归你:**扩展从不调用 `setActiveTools`,不写 `settings.json`,也不改 `defaultTools`;它只报告 codemode 状态与补救命令。
|
|
194
|
+
- **codemode 未激活=不可调用:**工具已注册,但 codemode 关掉时模型够不到它们,在 codemode 之外也看不到工具名。见「用法」里的前提说明。
|
|
195
|
+
- **工具集跟随模板而非扩展:**定义来自该箱缓存的 `tools/list`。不同箱可能跑不同版本的 `instabox-computer-use`,因此工具集与 schema 可能逐箱不同。校验口径是每次会话启动一次握手:远端版本相同则沿用缓存的工具、不动磁盘,不同则重拉并重写该箱条目,另有 168 小时 TTL 单独触发重拉。工具名稳定(`instabox_desktop_<远端名>`),所以写死了名字的 codemode 脚本跨版本仍可用。
|
|
196
|
+
- **默认安静:**后台声明成功不提示——成功路径没有任何通知,工具直接出现。会话启动只有三种提示:`ls` 空/失败的「没有可用沙箱」、同步失败并给出 `/instabox-use <box>`,以及 DevTools 开关配置有误(箱名缺失、箱不在 `ls` 里、或只给了 `--instabox-devtools-box` 没给 `--instabox-devtools`,见 [DevTools MCP](#devtools-mcp))。内部 `[pi-instabox] …` 的 stderr 输出与 JSONL 日志的 `debug` 级记录(见[日志](#日志))默认关闭,设 `PI_INSTABOX_DEBUG=1` 才能看到唤醒、spawn 参数与握手内部细节;**选箱不需要开关**:每次工具调用都会以 `info` 落一条 `resolve`。
|
|
197
|
+
- **全部暂停也会被拉起:**没有 running 的箱时,取 `instabox ls` 第一台在后台唤醒并拉模板——唤醒发生在建连这一步,所以缓存命中但箱已暂停时也会为版本校验把它唤醒;唤醒失败降级为那一条提示,会话不会被阻塞或拖住。
|
|
198
|
+
- **桌面截图成本高:**DevTools server 开启时,浏览器相关工作优先走它;命名空间说明里也对模型传达了同一条。
|
|
199
|
+
- **`box` 只作用于桌面工具:**它为 `instabox_desktop_*` 调用选择沙箱。DevTools server 固定绑定一台箱——会话绑定优先,其次 `--instabox-devtools-box`,最后 `devToolsBox`;换箱用 `/instabox-devtools on <box>`(同时更新会话绑定),不需要重新加载扩展;`/instabox-devtools reset` 可以撤掉这条会话绑定。
|
|
200
|
+
- **`off` 会留下旧工具名:**`/instabox-devtools off` 只是把它们隐藏。转录里已经出现过的工具名仍可能被模型尝试,然后以报错告终——连接已关,而 pi 不会因为隐藏而改写历史消息。
|
|
201
|
+
|
|
202
|
+
## 开发
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pnpm --filter @inobit/pi-instabox check # tsc --noEmit
|
|
206
|
+
pnpm --filter @inobit/pi-instabox test # vitest
|
|
207
|
+
pnpm --filter @inobit/pi-instabox pack:check # tarball 内容
|
|
208
|
+
pi -ne -e builtin:codemode -e builtin:mcp -e ./packages/pi-instabox
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
冒烟命令必须把内置扩展带回来:`-ne` / `--no-extensions` 会连同已安装的扩展一起禁用 `codemode` 与 `mcp`,缺了它们桌面工具无法调用、`registerMcpServer` 也会失败。单测全程离线——每处外部边界(`exec`、`spawn`、文件系统、时钟、`Text`)都可注入,因此没有测试会 spawn `instabox`、连箱子或访问网络。
|
|
212
|
+
|
|
213
|
+
## 许可证
|
|
214
|
+
|
|
215
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inobit/pi-instabox",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drive instabox sandboxes from Pi: `instabox_desktop_*` computer-use tools with per-call sandbox targeting, plus a config-driven remote `chrome-devtools-mcp` server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"default": "./src/index.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"README.zh-CN.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi-package",
|
|
21
|
+
"pi",
|
|
22
|
+
"pi-extension",
|
|
23
|
+
"pi-coding-agent",
|
|
24
|
+
"instabox",
|
|
25
|
+
"sandbox",
|
|
26
|
+
"computer-use",
|
|
27
|
+
"mcp",
|
|
28
|
+
"chrome-devtools"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/inobit/pi-packages.git",
|
|
34
|
+
"directory": "packages/pi-instabox"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"pi": {
|
|
43
|
+
"extensions": [
|
|
44
|
+
"./src/index.ts"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
49
|
+
"@earendil-works/pi-tui": "*",
|
|
50
|
+
"typebox": "*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
54
|
+
"@earendil-works/pi-tui": "1.5.0",
|
|
55
|
+
"@types/node": "^24.0.0",
|
|
56
|
+
"typebox": "1.3.27",
|
|
57
|
+
"typescript": "^7.0.0",
|
|
58
|
+
"vitest": "^4.1.8"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest",
|
|
64
|
+
"lint": "tsc --noEmit -p tsconfig.json",
|
|
65
|
+
"pack:check": "pnpm pack --pack-destination /tmp"
|
|
66
|
+
}
|
|
6
67
|
}
|