@vymalo/opencode-browser 0.7.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 +56 -70
- package/dist/token-file.js +44 -12
- package/dist/token-file.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ flowchart LR
|
|
|
31
31
|
- [Targeting elements](#targeting-elements--prefer-refs)
|
|
32
32
|
- [Screenshots](#screenshots)
|
|
33
33
|
- [Named groups](#named-groups)
|
|
34
|
-
- [Scoping tools
|
|
34
|
+
- [Scoping tools and token cost](#scoping-tools-and-token-cost)
|
|
35
35
|
- [Multiple browsers & agents](#multiple-browsers--agents)
|
|
36
36
|
- [Executors: CDP vs content-script](#executors-cdp-vs-content-script)
|
|
37
37
|
- [Stopping / releasing control](#stopping--releasing-control)
|
|
@@ -184,90 +184,76 @@ map to real `chrome.tabGroups` (titled, colored); on Firefox they're a logical r
|
|
|
184
184
|
keep each task's tabs isolated and inspectable, and are the unit of ownership when multiple
|
|
185
185
|
agents share one bridge.
|
|
186
186
|
|
|
187
|
-
## Scoping tools
|
|
187
|
+
## Scoping tools and token cost
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
Two levers, and they compose:
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
If your team keeps **all groups enabled by default** (`"groups": ["page","control","debug"]`),
|
|
197
|
-
the `groups` option won't narrow anything — every `browser_*` tool is registered. To restrict an
|
|
198
|
-
individual agent, use OpenCode's per-agent **`tools`** map (a `{ "<tool>": true|false }` record on
|
|
199
|
-
the agent). The tool names are stable `browser_*` identifiers (see [the 33 tools](#the-33-tools)),
|
|
200
|
-
so you target them directly. This is OpenCode's own mechanism — nothing plugin-specific.
|
|
201
|
-
|
|
202
|
-
### Denylist — drop the sensitive tools (most common)
|
|
191
|
+
- **`groups` (global)** — which tools are *registered* at all. The ceiling: an agent can only ever
|
|
192
|
+
be granted tools that are registered.
|
|
193
|
+
- **Per-agent `permission` (per agent)** — which registered tools each agent actually gets. **In
|
|
194
|
+
the flat string form this trims the agent's injected tool list, so it genuinely cuts context
|
|
195
|
+
tokens** — not just execution.
|
|
203
196
|
|
|
204
|
-
|
|
197
|
+
Approximate context cost of the full surface (tool name + description + args JSON schema the model
|
|
198
|
+
sees — rough, ≈chars/4):
|
|
205
199
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
200
|
+
| Registered `groups` | Tools | ≈ tokens (an agent granted all of them) |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| `["page"]` | 8 | ~1,000 |
|
|
203
|
+
| `["page","control"]` *(default)* | 27 | ~3,700 |
|
|
204
|
+
| `["page","control","debug"]` | 33 | ~4,560 |
|
|
205
|
+
| `debug` adds | 6 | ~830 |
|
|
206
|
+
|
|
207
|
+
> ### ⚠️ Flat string vs nested — the token footgun
|
|
208
|
+
> Per-agent scoping only reclaims tokens in the **flat string** `permission` form. The other forms
|
|
209
|
+
> gate *execution* but still inject the schema (so they flood context):
|
|
210
|
+
>
|
|
211
|
+
> | Form | Example | Trims context? |
|
|
212
|
+
> | --- | --- | --- |
|
|
213
|
+
> | flat string `permission` | `"browser_eval": "deny"` | ✅ removed from the agent's tools |
|
|
214
|
+
> | nested `permission` | `"browser_eval": { "*": "deny" }` | ❌ execution gate only |
|
|
215
|
+
> | `tools` map (deprecated) | `"browser_eval": false` (≡ nested `{"*":"deny"}`) | ❌ execution gate only |
|
|
216
|
+
>
|
|
217
|
+
> Verified on OpenCode **1.17.6** — confirm on your version by checking an agent's actual context
|
|
218
|
+
> token count with tools denied. (This is exactly the trap behind "registering a group floods every
|
|
219
|
+
> agent": that observation comes from the `tools`-boolean / nested form.)
|
|
220
|
+
|
|
221
|
+
### Recommended: register everything, scope per agent
|
|
222
|
+
|
|
223
|
+
If your org wants the full surface available but **not** flooding every agent's context, register
|
|
224
|
+
all groups once and give each agent only the tools it needs with **flat string** `permission`:
|
|
230
225
|
|
|
231
226
|
```jsonc
|
|
232
227
|
{
|
|
228
|
+
// full surface registered once — global
|
|
229
|
+
"plugin": [["@vymalo/opencode-browser", { "groups": ["page", "control", "debug"] }]],
|
|
233
230
|
"agent": {
|
|
234
231
|
"researcher": {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
232
|
+
// FLAT STRING form → denied tools are dropped from this agent's injected
|
|
233
|
+
// tools, so its context carries only the allowed ones.
|
|
234
|
+
"permission": {
|
|
235
|
+
"browser_*": "deny", // start from nothing…
|
|
236
|
+
"browser_open": "allow", // …allow only what this agent needs
|
|
237
|
+
"browser_navigate": "allow",
|
|
238
|
+
"browser_snapshot": "allow",
|
|
239
|
+
"browser_get_text": "allow",
|
|
240
|
+
"browser_query": "allow",
|
|
241
|
+
"browser_screenshot": "allow"
|
|
245
242
|
}
|
|
246
|
-
}
|
|
243
|
+
},
|
|
244
|
+
"automator": { "permission": { "browser_*": "allow" } } // full drive
|
|
247
245
|
}
|
|
248
246
|
}
|
|
249
247
|
```
|
|
250
248
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
The same `tools` map works in a `.opencode/agent/<name>.md` front-matter file:
|
|
254
|
-
|
|
255
|
-
```markdown
|
|
256
|
-
---
|
|
257
|
-
description: Reads pages, never changes them.
|
|
258
|
-
tools:
|
|
259
|
-
browser_*: false
|
|
260
|
-
browser_snapshot: true
|
|
261
|
-
browser_get_text: true
|
|
262
|
-
browser_screenshot: true
|
|
263
|
-
---
|
|
264
|
-
You are a read-only web researcher. Use browser_snapshot to get refs, then read.
|
|
265
|
-
```
|
|
249
|
+
(Specific keys override the `browser_*` wildcard. If you'd rather not rely on that precedence,
|
|
250
|
+
list explicit `"deny"`s for the tools an agent shouldn't have instead of a wildcard.)
|
|
266
251
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
252
|
+
If you don't need per-agent differences, just register the minimum union globally — `["page"]` for
|
|
253
|
+
read-only research, `["page","control"]` to drive — and skip `debug` unless a workflow needs
|
|
254
|
+
`eval`/`cookies`/`network`. Either way the schemas sit in the cached prefix, so prompt caching
|
|
255
|
+
softens the per-request **dollar** cost; trimming still helps the context window and cache writes.
|
|
256
|
+
See OpenCode's [agent docs](https://opencode.ai/docs/agents/) for the `permission` field.
|
|
271
257
|
|
|
272
258
|
## Multiple browsers & agents
|
|
273
259
|
|
package/dist/token-file.js
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
|
-
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
/**
|
|
5
5
|
* Per-user state file so multiple adapters (plugin + MCP server) on one machine
|
|
6
|
-
* share a bridge token without the operator wiring one up.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* share a bridge token without the operator wiring one up. It lives in the
|
|
7
|
+
* persistent per-OS app-data dir (NOT the temp dir), so a generated token
|
|
8
|
+
* survives reboots — you paste it into the extension once, not every session.
|
|
9
|
+
* Best-effort: a simultaneous cold start can race (each generates its own token
|
|
10
|
+
* before either writes); set an explicit token if you need that guaranteed.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// Only honor an env-provided root if it's a usable absolute path — an empty or
|
|
13
|
+
// relative value (e.g. exported `XDG_STATE_HOME=""`) would otherwise resolve the
|
|
14
|
+
// state file relative to the process cwd, scattering per-directory tokens. Fall
|
|
15
|
+
// back to the home-based default instead.
|
|
16
|
+
function envRoot(value, fallback) {
|
|
17
|
+
return value && isAbsolute(value) ? value : fallback;
|
|
18
|
+
}
|
|
19
|
+
// Resolved lazily (not at module load) so a sandboxed homedir() failure can't
|
|
20
|
+
// break import, and tests can vary process.env / platform between calls.
|
|
21
|
+
function resolveStateRoot() {
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
return envRoot(process.env.APPDATA, join(homedir(), "AppData", "Roaming"));
|
|
24
|
+
}
|
|
25
|
+
if (process.platform === "darwin") {
|
|
26
|
+
return join(homedir(), "Library", "Application Support");
|
|
27
|
+
}
|
|
28
|
+
return envRoot(process.env.XDG_STATE_HOME, join(homedir(), ".local", "state"));
|
|
29
|
+
}
|
|
30
|
+
function bridgeFilePath() {
|
|
31
|
+
return join(resolveStateRoot(), "opencode-browser", "bridge.json");
|
|
32
|
+
}
|
|
33
|
+
// ≤0.7.x kept this in the temp dir, where the OS clears it on reboot (so the
|
|
34
|
+
// token kept changing). Read it once as a fallback so upgrading doesn't force a
|
|
35
|
+
// re-paste; the next write migrates it to the persistent location above.
|
|
36
|
+
function legacyBridgeFilePath() {
|
|
37
|
+
return join(tmpdir(), "opencode-browser-bridge.json");
|
|
38
|
+
}
|
|
39
|
+
function readFileAt(path) {
|
|
13
40
|
try {
|
|
14
|
-
return JSON.parse(readFileSync(
|
|
41
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
42
|
}
|
|
16
43
|
catch {
|
|
17
44
|
return null;
|
|
18
45
|
}
|
|
19
46
|
}
|
|
47
|
+
export function readBridgeFile() {
|
|
48
|
+
return readFileAt(bridgeFilePath()) ?? readFileAt(legacyBridgeFilePath());
|
|
49
|
+
}
|
|
20
50
|
export function writeBridgeFile(port, token) {
|
|
21
51
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
const file = bridgeFilePath();
|
|
53
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
54
|
+
writeFileSync(file, JSON.stringify({ port, token }), { mode: 0o600 });
|
|
55
|
+
chmodSync(file, 0o600);
|
|
24
56
|
}
|
|
25
57
|
catch {
|
|
26
58
|
/* best-effort */
|
package/dist/token-file.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-file.js","sourceRoot":"","sources":["../src/token-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"token-file.js","sourceRoot":"","sources":["../src/token-file.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vymalo/opencode-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "OpenCode plugin that gives the model hands in a real browser: it registers browser_* tools (open, click, type, scroll, screenshot, snapshot, …) and hosts a localhost WebSocket bridge that a companion Chromium/Firefox extension connects to. Tabs are organized into named groups so each task stays isolated and inspectable.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vymalo contributors",
|