@jobshimo/browser-link 0.5.1 → 0.5.3
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/dist/auth/process-identity.d.ts +19 -3
- package/dist/auth/process-identity.js +81 -20
- package/dist/auth/process-identity.js.map +1 -1
- package/dist/bridge/dispatch.d.ts +6 -0
- package/dist/bridge/dispatch.js +2 -2
- package/dist/bridge/dispatch.js.map +1 -1
- package/dist/bridge/events.d.ts +1 -1
- package/dist/bridge/events.js.map +1 -1
- package/dist/bridge/server.d.ts +7 -1
- package/dist/bridge/server.js +42 -5
- package/dist/bridge/server.js.map +1 -1
- package/dist/commands/self-update.d.ts +59 -0
- package/dist/commands/self-update.js +110 -0
- package/dist/commands/self-update.js.map +1 -0
- package/dist/commands/updates.js +27 -1
- package/dist/commands/updates.js.map +1 -1
- package/dist/config.d.ts +12 -10
- package/dist/config.js +55 -20
- package/dist/config.js.map +1 -1
- package/dist/extension/background.js +56 -6
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/manifest.json +2 -2
- package/dist/extension/popup.d.ts +4 -0
- package/dist/extension/popup.html +238 -48
- package/dist/extension/popup.js +37 -24
- package/dist/extension/popup.js.map +1 -1
- package/dist/server.js +50 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +36 -1
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +39 -3
- package/dist/tools/browser-dispatch.js +170 -63
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.d.ts +1 -1
- package/dist/tools/server-instructions.js +32 -0
- package/dist/tools/server-instructions.js.map +1 -1
- package/dist/tools/tab-claims.d.ts +117 -0
- package/dist/tools/tab-claims.js +186 -0
- package/dist/tools/tab-claims.js.map +1 -0
- package/dist/ui/screens.js +57 -6
- package/dist/ui/screens.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,15 +4,37 @@
|
|
|
4
4
|
* runServer().
|
|
5
5
|
*
|
|
6
6
|
* The handlers do not own state: they receive a `BrowserToolDeps` object
|
|
7
|
-
* with the live tab map
|
|
8
|
-
* unit-testable with a fake `callBrowserTool
|
|
7
|
+
* with the live tab map, the call function, and the claim registry. This
|
|
8
|
+
* makes the dispatcher unit-testable with a fake `callBrowserTool` and a
|
|
9
|
+
* fresh `TabClaimRegistry`.
|
|
10
|
+
*
|
|
11
|
+
* Each invocation also receives an `AgentCaller` so claim/ownership
|
|
12
|
+
* decisions can be made per request. The bridge layers (primary's stdio
|
|
13
|
+
* handler + IPC server) are responsible for supplying it.
|
|
9
14
|
*/
|
|
10
15
|
import type { BridgeEvent } from '../bridge/events.js';
|
|
16
|
+
import { type AgentCaller, type TabClaimRegistry } from './tab-claims.js';
|
|
11
17
|
export interface TabSnapshot {
|
|
12
18
|
tab_id: string;
|
|
13
19
|
url: string;
|
|
14
20
|
title: string;
|
|
15
21
|
}
|
|
22
|
+
/** Public view of a TabClaim — what other agents and the user are allowed to see.
|
|
23
|
+
* Keeps the wire payload stable even if the internal `TabClaim` grows fields. */
|
|
24
|
+
export interface PublicClaim {
|
|
25
|
+
tab_id: string;
|
|
26
|
+
agent_id: string;
|
|
27
|
+
pid: number;
|
|
28
|
+
binary: string;
|
|
29
|
+
label?: string;
|
|
30
|
+
claimed_at: number;
|
|
31
|
+
last_activity_at: number;
|
|
32
|
+
ttl_ms: number;
|
|
33
|
+
}
|
|
34
|
+
export interface EnrichedTabSnapshot extends TabSnapshot {
|
|
35
|
+
claimed_by: PublicClaim | null;
|
|
36
|
+
claimed_by_me: boolean;
|
|
37
|
+
}
|
|
16
38
|
export interface BrowserToolDeps {
|
|
17
39
|
listTabs(): TabSnapshot[];
|
|
18
40
|
callBrowserTool(tabId: string, tool: string, params: unknown, timeoutMs?: number): Promise<unknown>;
|
|
@@ -22,6 +44,20 @@ export interface BrowserToolDeps {
|
|
|
22
44
|
sinceId?: number;
|
|
23
45
|
limit?: number;
|
|
24
46
|
}): BridgeEvent[];
|
|
47
|
+
/** Claim registry. Optional so existing test fixtures keep compiling — when
|
|
48
|
+
* absent the dispatcher behaves as before (no enforcement, list_tabs
|
|
49
|
+
* returns claimed_by:null for every tab). */
|
|
50
|
+
tabClaims?: TabClaimRegistry;
|
|
25
51
|
}
|
|
26
52
|
export declare function isBrowserTool(name: string): boolean;
|
|
27
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Dispatch a browser tool call.
|
|
55
|
+
*
|
|
56
|
+
* The switch is intentional: every reachable arm is matched against the
|
|
57
|
+
* closed `BrowserToolName` literal union, so the `name` string can only
|
|
58
|
+
* reach a known handler (or fall through to the `default` throw). This
|
|
59
|
+
* pattern is what CodeQL's `js/unvalidated-dynamic-method-call` accepts as
|
|
60
|
+
* a fixed allowlist — a Map-of-handlers lookup looks identical at runtime
|
|
61
|
+
* but is flagged because the static analyser can't prove the bound.
|
|
62
|
+
*/
|
|
63
|
+
export declare function handleBrowserTool(name: string, args: unknown, deps: BrowserToolDeps, caller: AgentCaller): Promise<unknown>;
|
|
@@ -4,83 +4,190 @@
|
|
|
4
4
|
* runServer().
|
|
5
5
|
*
|
|
6
6
|
* The handlers do not own state: they receive a `BrowserToolDeps` object
|
|
7
|
-
* with the live tab map
|
|
8
|
-
* unit-testable with a fake `callBrowserTool
|
|
7
|
+
* with the live tab map, the call function, and the claim registry. This
|
|
8
|
+
* makes the dispatcher unit-testable with a fake `callBrowserTool` and a
|
|
9
|
+
* fresh `TabClaimRegistry`.
|
|
10
|
+
*
|
|
11
|
+
* Each invocation also receives an `AgentCaller` so claim/ownership
|
|
12
|
+
* decisions can be made per request. The bridge layers (primary's stdio
|
|
13
|
+
* handler + IPC server) are responsible for supplying it.
|
|
9
14
|
*/
|
|
10
15
|
import { requireTabId } from './responses.js';
|
|
16
|
+
import { formatClaimConflict, } from './tab-claims.js';
|
|
17
|
+
/** Closed set of browser tool names. Used both as the discriminant in
|
|
18
|
+
* `handleBrowserTool`'s switch and as the allowlist behind `isBrowserTool`,
|
|
19
|
+
* so adding a tool requires touching one place. The literal-union type lets
|
|
20
|
+
* TypeScript prove the switch is exhaustive at compile time. */
|
|
21
|
+
const BROWSER_TOOL_NAMES = [
|
|
22
|
+
'browser.list_tabs',
|
|
23
|
+
'browser.claim_tab',
|
|
24
|
+
'browser.release_tab',
|
|
25
|
+
'browser.my_tabs',
|
|
26
|
+
'browser.ping',
|
|
27
|
+
'browser.navigate',
|
|
28
|
+
'browser.snapshot',
|
|
29
|
+
'browser.console',
|
|
30
|
+
'browser.network',
|
|
31
|
+
'browser.network_body',
|
|
32
|
+
'browser.click',
|
|
33
|
+
'browser.type',
|
|
34
|
+
'browser.evaluate',
|
|
35
|
+
'browser.events',
|
|
36
|
+
];
|
|
37
|
+
const BROWSER_TOOL_NAME_SET = new Set(BROWSER_TOOL_NAMES);
|
|
11
38
|
export function isBrowserTool(name) {
|
|
12
|
-
return
|
|
39
|
+
return BROWSER_TOOL_NAME_SET.has(name);
|
|
13
40
|
}
|
|
14
41
|
const NAVIGATE_TIMEOUT_MS = 30_000;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
/** Convert an internal TabClaim into the wire-safe `PublicClaim`. */
|
|
43
|
+
function toPublicClaim(claim) {
|
|
44
|
+
const publicClaim = {
|
|
45
|
+
tab_id: claim.tab_id,
|
|
46
|
+
agent_id: claim.agent_id,
|
|
47
|
+
pid: claim.pid,
|
|
48
|
+
binary: claim.binary,
|
|
49
|
+
claimed_at: claim.claimed_at,
|
|
50
|
+
last_activity_at: claim.last_activity_at,
|
|
51
|
+
ttl_ms: claim.ttl_ms,
|
|
52
|
+
};
|
|
53
|
+
if (claim.label !== undefined)
|
|
54
|
+
publicClaim.label = claim.label;
|
|
55
|
+
return publicClaim;
|
|
56
|
+
}
|
|
57
|
+
/** Run an action through the claim registry. Returns the response payload of
|
|
58
|
+
* the action when the agent is allowed, or throws a descriptive Error otherwise.
|
|
59
|
+
* When no registry is wired (test fixtures, or future configs that disable
|
|
60
|
+
* coordination), behaves like the pre-claim dispatcher. */
|
|
61
|
+
async function runAction(tool, tabId, params, deps, caller, timeoutMs) {
|
|
62
|
+
if (deps.tabClaims) {
|
|
63
|
+
const outcome = deps.tabClaims.ensureActionAllowed(tabId, caller);
|
|
64
|
+
if (!outcome.ok) {
|
|
65
|
+
throw new Error(formatClaimConflict(caller, outcome.existing));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Preserve the pre-claim call shape — only forward timeoutMs when set so
|
|
69
|
+
// existing assertions and the bridge's default behaviour stay unchanged.
|
|
70
|
+
return timeoutMs !== undefined
|
|
71
|
+
? deps.callBrowserTool(tabId, tool, params, timeoutMs)
|
|
72
|
+
: deps.callBrowserTool(tabId, tool, params);
|
|
73
|
+
}
|
|
74
|
+
function handleListTabs(deps, caller) {
|
|
75
|
+
return deps.listTabs().map((t) => {
|
|
76
|
+
const claim = deps.tabClaims?.getClaim(t.tab_id) ?? null;
|
|
77
|
+
return {
|
|
78
|
+
...t,
|
|
79
|
+
claimed_by: claim ? toPublicClaim(claim) : null,
|
|
80
|
+
claimed_by_me: claim ? claim.agent_id === caller.agent_id : false,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function handleClaimTab(args, deps, caller) {
|
|
85
|
+
const { tab_id, ttl_minutes, label } = (args ?? {});
|
|
86
|
+
if (!tab_id)
|
|
87
|
+
throw new Error('tab_id required');
|
|
88
|
+
if (!deps.tabClaims) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: 'unsupported',
|
|
92
|
+
message: 'Tab coordination is disabled in this build of browser-link.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const outcome = deps.tabClaims.claim(tab_id, caller, { ttlMinutes: ttl_minutes, label });
|
|
96
|
+
if (!outcome.ok) {
|
|
97
|
+
return { ok: false, reason: outcome.reason, existing: toPublicClaim(outcome.existing) };
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, created: outcome.created, claim: toPublicClaim(outcome.claim) };
|
|
100
|
+
}
|
|
101
|
+
function handleReleaseTab(args, deps, caller) {
|
|
102
|
+
const tab_id = requireTabId(args);
|
|
103
|
+
if (!deps.tabClaims)
|
|
104
|
+
return { ok: true };
|
|
105
|
+
const result = deps.tabClaims.release(tab_id, caller);
|
|
106
|
+
if (result.ok)
|
|
107
|
+
return { ok: true };
|
|
108
|
+
const payload = { ok: false, reason: result.reason };
|
|
109
|
+
if (result.existing)
|
|
110
|
+
payload.existing = toPublicClaim(result.existing);
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
function handleMyTabs(deps, caller) {
|
|
114
|
+
if (!deps.tabClaims)
|
|
115
|
+
return { claims: [] };
|
|
116
|
+
return { claims: deps.tabClaims.myTabs(caller).map(toPublicClaim) };
|
|
117
|
+
}
|
|
118
|
+
function handleEvents(args, deps) {
|
|
119
|
+
const { since_id, limit } = args ?? {};
|
|
120
|
+
if (!deps.recentEvents)
|
|
121
|
+
return { events: [], latest_id: 0 };
|
|
122
|
+
const events = deps.recentEvents({ sinceId: since_id, limit });
|
|
123
|
+
const latest_id = events.length > 0 ? events[events.length - 1].id : (since_id ?? 0);
|
|
124
|
+
return { events, latest_id };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Dispatch a browser tool call.
|
|
128
|
+
*
|
|
129
|
+
* The switch is intentional: every reachable arm is matched against the
|
|
130
|
+
* closed `BrowserToolName` literal union, so the `name` string can only
|
|
131
|
+
* reach a known handler (or fall through to the `default` throw). This
|
|
132
|
+
* pattern is what CodeQL's `js/unvalidated-dynamic-method-call` accepts as
|
|
133
|
+
* a fixed allowlist — a Map-of-handlers lookup looks identical at runtime
|
|
134
|
+
* but is flagged because the static analyser can't prove the bound.
|
|
135
|
+
*/
|
|
136
|
+
export async function handleBrowserTool(name, args, deps, caller) {
|
|
137
|
+
if (!isBrowserTool(name))
|
|
138
|
+
throw new Error(`Unknown browser tool: ${name}`);
|
|
139
|
+
const toolName = name;
|
|
140
|
+
switch (toolName) {
|
|
141
|
+
case 'browser.list_tabs':
|
|
142
|
+
return handleListTabs(deps, caller);
|
|
143
|
+
case 'browser.claim_tab':
|
|
144
|
+
return handleClaimTab(args, deps, caller);
|
|
145
|
+
case 'browser.release_tab':
|
|
146
|
+
return handleReleaseTab(args, deps, caller);
|
|
147
|
+
case 'browser.my_tabs':
|
|
148
|
+
return handleMyTabs(deps, caller);
|
|
149
|
+
case 'browser.ping':
|
|
150
|
+
return deps.callBrowserTool(requireTabId(args), 'ping', {});
|
|
151
|
+
case 'browser.navigate': {
|
|
21
152
|
const { url, wait_for_load = true } = args;
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'browser.console',
|
|
28
|
-
(args, deps) => {
|
|
153
|
+
return runAction('navigate', requireTabId(args), { url, wait_for_load }, deps, caller, NAVIGATE_TIMEOUT_MS);
|
|
154
|
+
}
|
|
155
|
+
case 'browser.snapshot':
|
|
156
|
+
return deps.callBrowserTool(requireTabId(args), 'snapshot', {});
|
|
157
|
+
case 'browser.console': {
|
|
29
158
|
const { level } = args ?? {};
|
|
30
159
|
return deps.callBrowserTool(requireTabId(args), 'console', { level });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
[
|
|
34
|
-
'browser.network',
|
|
35
|
-
(args, deps) => {
|
|
160
|
+
}
|
|
161
|
+
case 'browser.network': {
|
|
36
162
|
const { url_filter } = args ?? {};
|
|
37
163
|
return deps.callBrowserTool(requireTabId(args), 'network', { url_filter });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
[
|
|
41
|
-
'browser.network_body',
|
|
42
|
-
(args, deps) => {
|
|
164
|
+
}
|
|
165
|
+
case 'browser.network_body': {
|
|
43
166
|
const { request_id } = args;
|
|
44
167
|
return deps.callBrowserTool(requireTabId(args), 'network_body', { request_id });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
[
|
|
48
|
-
'browser.click',
|
|
49
|
-
(args, deps) => {
|
|
168
|
+
}
|
|
169
|
+
case 'browser.click': {
|
|
50
170
|
const { selector } = args;
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
[
|
|
55
|
-
'browser.type',
|
|
56
|
-
(args, deps) => {
|
|
171
|
+
return runAction('click', requireTabId(args), { selector }, deps, caller);
|
|
172
|
+
}
|
|
173
|
+
case 'browser.type': {
|
|
57
174
|
const { selector, text, clear = false, } = args;
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
'browser.evaluate',
|
|
63
|
-
(args, deps) => {
|
|
175
|
+
return runAction('type', requireTabId(args), { selector, text, clear }, deps, caller);
|
|
176
|
+
}
|
|
177
|
+
case 'browser.evaluate': {
|
|
64
178
|
const { expression } = args;
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
],
|
|
79
|
-
]);
|
|
80
|
-
export async function handleBrowserTool(name, args, deps) {
|
|
81
|
-
const handler = BROWSER_TOOL_HANDLERS.get(name);
|
|
82
|
-
if (!handler)
|
|
83
|
-
throw new Error(`Unknown browser tool: ${name}`);
|
|
84
|
-
return handler(args, deps);
|
|
179
|
+
return runAction('evaluate', requireTabId(args), { expression }, deps, caller);
|
|
180
|
+
}
|
|
181
|
+
case 'browser.events':
|
|
182
|
+
return handleEvents(args, deps);
|
|
183
|
+
default: {
|
|
184
|
+
// The earlier `isBrowserTool(name)` check makes this branch unreachable
|
|
185
|
+
// for any value within `BrowserToolName`. The exhaustive cast surfaces a
|
|
186
|
+
// compile error if a future tool name is added to the union but missed
|
|
187
|
+
// here.
|
|
188
|
+
const _exhaustive = toolName;
|
|
189
|
+
throw new Error(`Unhandled browser tool: ${String(_exhaustive)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
85
192
|
}
|
|
86
193
|
//# sourceMappingURL=browser-dispatch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-dispatch.js","sourceRoot":"","sources":["../../src/tools/browser-dispatch.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"browser-dispatch.js","sourceRoot":"","sources":["../../src/tools/browser-dispatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Usage protocol pushed to the MCP client on `initialize`. Plain string,
|
|
2
2
|
* intentionally kept short. Edit here when the protocol changes. */
|
|
3
|
-
export declare const SERVER_INSTRUCTIONS = "browser-link bridges Claude Code to the Chrome tabs the user has\nexplicitly connected through the companion extension, and ships a\npersistent UI map backed by a local SQLite DB. The data dir resolves\nper-OS via env-paths ($XDG_DATA_HOME/browser-link on Linux,\n~/Library/Application Support/browser-link on macOS, %APPDATA%/browser-link\non Windows). Override with $BROWSER_LINK_DATA_DIR. The map is private\nand per-machine; never persisted in any repo.\n\n## When operating on a tab\n\n1. Before doing anything on a tab whose URL you don't already know,\n call `browser.map.recall` with { origin } (and optionally url) to load\n selectors, flows and gotchas previously learned for that app.\n2. If recall returns entries with `failed_at` more recent than\n `verified_at`, treat them as suspect: re-verify (snapshot / evaluate)\n before reusing, or replace them.\n3. After every interaction that used a map entry, call\n `browser.map.record_use` with { entry_id, ok }. ok=true updates\n verified_at; ok=false updates failed_at. Keep the map honest.\n4. After a non-trivial flow that worked end-to-end, persist it with\n `browser.map.save`. Three `kind` values:\n - selector: { selector, evidence? } \u2014 a CSS selector tied to a purpose.\n - flow: { steps: [...] } \u2014 an ordered list of actions to reach an outcome.\n - gotcha: { body } \u2014 free-form note about something non-obvious.\n Use `url_pattern` = pathname (exact). Promote to glob only if you have\n evidence of a parametric route. Provide `purpose` as a stable, reusable\n label (\"open task detail dialog\", not \"open IB0311 detail\").\n5. Never save selectors or flows you have not just successfully executed.\n6. Never store domain data (IDs, user names, dates, etc.). The map captures\n UI structure only.\n\n## Identifying the app\n\n- `origin` = scheme://host:port of the tab.\n- `app_key` distinguishes apps that share an origin over time. On first\n save you may omit it; it will be derived from the page title (slugified).\n Use `browser.map.rename_app` if that initial guess is poor.\n\n## When something is wrong\n\n- A selector from recall fails \u2192 record_use({ok:false}), learn the new\n one, save it (upsert on purpose).\n- A whole app got refactored \u2192 `browser.map.forget` the app_id and let\n the map repopulate as you learn the new structure.\n- A tool call fails with \"Tab not connected: tab_X\" \u2192 call\n `browser.events` to see whether the bridge changed primary (the\n Chrome tab probably got a new tab_id after a reconnect). Look for a\n `tab-renamed` event with previous=tab_X and resume on the current id.\n\nThe map is a cache of navigation, not a substitute for `browser.snapshot`.\nThe live snapshot is always the source of truth.";
|
|
3
|
+
export declare const SERVER_INSTRUCTIONS = "browser-link bridges Claude Code to the Chrome tabs the user has\nexplicitly connected through the companion extension, and ships a\npersistent UI map backed by a local SQLite DB. The data dir resolves\nper-OS via env-paths ($XDG_DATA_HOME/browser-link on Linux,\n~/Library/Application Support/browser-link on macOS, %APPDATA%/browser-link\non Windows). Override with $BROWSER_LINK_DATA_DIR. The map is private\nand per-machine; never persisted in any repo.\n\n## When operating on a tab\n\n1. Before doing anything on a tab whose URL you don't already know,\n call `browser.map.recall` with { origin } (and optionally url) to load\n selectors, flows and gotchas previously learned for that app.\n2. If recall returns entries with `failed_at` more recent than\n `verified_at`, treat them as suspect: re-verify (snapshot / evaluate)\n before reusing, or replace them.\n3. After every interaction that used a map entry, call\n `browser.map.record_use` with { entry_id, ok }. ok=true updates\n verified_at; ok=false updates failed_at. Keep the map honest.\n4. After a non-trivial flow that worked end-to-end, persist it with\n `browser.map.save`. Three `kind` values:\n - selector: { selector, evidence? } \u2014 a CSS selector tied to a purpose.\n - flow: { steps: [...] } \u2014 an ordered list of actions to reach an outcome.\n - gotcha: { body } \u2014 free-form note about something non-obvious.\n Use `url_pattern` = pathname (exact). Promote to glob only if you have\n evidence of a parametric route. Provide `purpose` as a stable, reusable\n label (\"open task detail dialog\", not \"open IB0311 detail\").\n5. Never save selectors or flows you have not just successfully executed.\n6. Never store domain data (IDs, user names, dates, etc.). The map captures\n UI structure only.\n\n## Identifying the app\n\n- `origin` = scheme://host:port of the tab.\n- `app_key` distinguishes apps that share an origin over time. On first\n save you may omit it; it will be derived from the page title (slugified).\n Use `browser.map.rename_app` if that initial guess is poor.\n\n## Sharing tabs with other agents\n\nThis primary may be serving several MCP clients at once (multi-agent mode).\nTo stop two agents fighting over the same Chrome tab there is a cooperative\nclaim layer:\n\n- `browser.list_tabs` includes `claimed_by` (null if free, otherwise the\n agent that holds the claim) and `claimed_by_me` (boolean). Use it before\n starting work on a tab whose state you don't already own.\n- `browser.my_tabs` returns YOUR active claims with timestamps. If the\n user asks which tab you are using, this is the answer.\n- Action tools (`browser.click`, `browser.type`, `browser.navigate`,\n `browser.evaluate`) auto-claim a free tab on first use and refresh\n activity on subsequent calls. If another agent holds the tab, they\n return an error naming the owner \u2014 do NOT retry blindly; ask the user\n whose tab it should be, or use a different tab.\n- Read tools (`browser.snapshot`, `browser.console`, `browser.network`,\n `browser.network_body`, `browser.events`, `browser.ping`) ignore claims.\n- `browser.claim_tab({ tab_id, ttl_minutes?, label? })` reserves a tab\n explicitly. Provide a stable `label` (eg \"claude-code\", \"opencode\") so\n other agents and the user see WHO holds the tab. The label is display\n only \u2014 security relies on the IPC session id (kernel-vetted), not on\n what an agent calls itself.\n- `browser.release_tab({ tab_id })` hands a tab back. Claims also auto-\n release when an agent disconnects or after the inactivity TTL elapses\n (default 10 minutes), so explicit release is only needed for early\n hand-off.\n\nWhen you get a claim-conflict error: do NOT spin-retry. Either work on a\ndifferent tab from `list_tabs`, or surface the conflict to the user and\nlet them decide.\n\n## When something is wrong\n\n- A selector from recall fails \u2192 record_use({ok:false}), learn the new\n one, save it (upsert on purpose).\n- A whole app got refactored \u2192 `browser.map.forget` the app_id and let\n the map repopulate as you learn the new structure.\n- A tool call fails with \"Tab not connected: tab_X\" \u2192 call\n `browser.events` to see whether the bridge changed primary (the\n Chrome tab probably got a new tab_id after a reconnect). Look for a\n `tab-renamed` event with previous=tab_X and resume on the current id.\n\nThe map is a cache of navigation, not a substitute for `browser.snapshot`.\nThe live snapshot is always the source of truth.";
|
|
@@ -38,6 +38,38 @@ and per-machine; never persisted in any repo.
|
|
|
38
38
|
save you may omit it; it will be derived from the page title (slugified).
|
|
39
39
|
Use \`browser.map.rename_app\` if that initial guess is poor.
|
|
40
40
|
|
|
41
|
+
## Sharing tabs with other agents
|
|
42
|
+
|
|
43
|
+
This primary may be serving several MCP clients at once (multi-agent mode).
|
|
44
|
+
To stop two agents fighting over the same Chrome tab there is a cooperative
|
|
45
|
+
claim layer:
|
|
46
|
+
|
|
47
|
+
- \`browser.list_tabs\` includes \`claimed_by\` (null if free, otherwise the
|
|
48
|
+
agent that holds the claim) and \`claimed_by_me\` (boolean). Use it before
|
|
49
|
+
starting work on a tab whose state you don't already own.
|
|
50
|
+
- \`browser.my_tabs\` returns YOUR active claims with timestamps. If the
|
|
51
|
+
user asks which tab you are using, this is the answer.
|
|
52
|
+
- Action tools (\`browser.click\`, \`browser.type\`, \`browser.navigate\`,
|
|
53
|
+
\`browser.evaluate\`) auto-claim a free tab on first use and refresh
|
|
54
|
+
activity on subsequent calls. If another agent holds the tab, they
|
|
55
|
+
return an error naming the owner — do NOT retry blindly; ask the user
|
|
56
|
+
whose tab it should be, or use a different tab.
|
|
57
|
+
- Read tools (\`browser.snapshot\`, \`browser.console\`, \`browser.network\`,
|
|
58
|
+
\`browser.network_body\`, \`browser.events\`, \`browser.ping\`) ignore claims.
|
|
59
|
+
- \`browser.claim_tab({ tab_id, ttl_minutes?, label? })\` reserves a tab
|
|
60
|
+
explicitly. Provide a stable \`label\` (eg "claude-code", "opencode") so
|
|
61
|
+
other agents and the user see WHO holds the tab. The label is display
|
|
62
|
+
only — security relies on the IPC session id (kernel-vetted), not on
|
|
63
|
+
what an agent calls itself.
|
|
64
|
+
- \`browser.release_tab({ tab_id })\` hands a tab back. Claims also auto-
|
|
65
|
+
release when an agent disconnects or after the inactivity TTL elapses
|
|
66
|
+
(default 10 minutes), so explicit release is only needed for early
|
|
67
|
+
hand-off.
|
|
68
|
+
|
|
69
|
+
When you get a claim-conflict error: do NOT spin-retry. Either work on a
|
|
70
|
+
different tab from \`list_tabs\`, or surface the conflict to the user and
|
|
71
|
+
let them decide.
|
|
72
|
+
|
|
41
73
|
## When something is wrong
|
|
42
74
|
|
|
43
75
|
- A selector from recall fails → record_use({ok:false}), learn the new
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-instructions.js","sourceRoot":"","sources":["../../src/tools/server-instructions.ts"],"names":[],"mappings":"AAAA;oEACoE;AACpE,MAAM,CAAC,MAAM,mBAAmB,GAAG
|
|
1
|
+
{"version":3,"file":"server-instructions.js","sourceRoot":"","sources":["../../src/tools/server-instructions.ts"],"names":[],"mappings":"AAAA;oEACoE;AACpE,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iDAkFc,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cooperative tab-ownership registry for multi-agent mode.
|
|
3
|
+
*
|
|
4
|
+
* Several MCP clients can share a single browser-link primary. Without
|
|
5
|
+
* coordination they end up racing on the same Chrome tab — clicking,
|
|
6
|
+
* navigating, evaluating in interleaved order. This module tracks who
|
|
7
|
+
* "owns" each tab and lets the dispatcher refuse cross-agent action.
|
|
8
|
+
*
|
|
9
|
+
* Identity model:
|
|
10
|
+
* - `agent_id` is the IPC session id for proxies (UUID minted after the
|
|
11
|
+
* hello/token handshake) and the literal string `"primary"` for the
|
|
12
|
+
* primary's own MCP client.
|
|
13
|
+
* - The auth that the agent passed (peerLookup + Node binary allowlist +
|
|
14
|
+
* rotated token) is the trust anchor. This registry never re-validates
|
|
15
|
+
* identity — it consumes the `AgentCaller` the bridge layer supplies.
|
|
16
|
+
* - Self-declared `label` (eg `"claude-code"`) is display-only and is
|
|
17
|
+
* never used for ownership comparisons.
|
|
18
|
+
*
|
|
19
|
+
* TTL model:
|
|
20
|
+
* - Claims expire after `ttl_minutes` of inactivity. Every action on the
|
|
21
|
+
* tab refreshes `last_activity_at`. Stale claims are dropped by
|
|
22
|
+
* `pruneStale()` (called periodically by the primary).
|
|
23
|
+
* - Sessions ending (proxy disconnect) drop the agent's claims
|
|
24
|
+
* immediately via `onAgentDisconnect()`.
|
|
25
|
+
*/
|
|
26
|
+
export interface AgentCaller {
|
|
27
|
+
agent_id: string;
|
|
28
|
+
pid: number;
|
|
29
|
+
binary: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TabClaim {
|
|
33
|
+
tab_id: string;
|
|
34
|
+
agent_id: string;
|
|
35
|
+
pid: number;
|
|
36
|
+
binary: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
claimed_at: number;
|
|
39
|
+
last_activity_at: number;
|
|
40
|
+
ttl_ms: number;
|
|
41
|
+
}
|
|
42
|
+
export type ClaimEvent = {
|
|
43
|
+
kind: 'tab-claimed';
|
|
44
|
+
tab_id: string;
|
|
45
|
+
agent_id: string;
|
|
46
|
+
pid: number;
|
|
47
|
+
binary: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
ttl_ms: number;
|
|
50
|
+
auto: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'tab-released';
|
|
53
|
+
tab_id: string;
|
|
54
|
+
agent_id: string;
|
|
55
|
+
reason: 'explicit' | 'agent-disconnect' | 'ttl';
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'tab-claim-rejected';
|
|
58
|
+
tab_id: string;
|
|
59
|
+
requester_agent_id: string;
|
|
60
|
+
existing_agent_id: string;
|
|
61
|
+
};
|
|
62
|
+
export type ClaimOutcome = {
|
|
63
|
+
ok: true;
|
|
64
|
+
claim: TabClaim;
|
|
65
|
+
created: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
reason: 'conflict';
|
|
69
|
+
existing: TabClaim;
|
|
70
|
+
};
|
|
71
|
+
export interface TabClaimRegistryOptions {
|
|
72
|
+
/** Default TTL when a claim does not specify one. Defaults to 10 minutes. */
|
|
73
|
+
defaultTtlMinutes?: number;
|
|
74
|
+
/** Upper bound on TTL a caller may request. Defaults to 60 minutes. */
|
|
75
|
+
maxTtlMinutes?: number;
|
|
76
|
+
/** Time source. Tests inject a fake clock to make TTL assertions deterministic. */
|
|
77
|
+
nowMs?: () => number;
|
|
78
|
+
/** Event callback. The primary wires this to its `BridgeEventLog`. */
|
|
79
|
+
onEvent?: (event: ClaimEvent) => void;
|
|
80
|
+
}
|
|
81
|
+
export declare class TabClaimRegistry {
|
|
82
|
+
private claims;
|
|
83
|
+
private readonly defaultTtlMs;
|
|
84
|
+
private readonly maxTtlMs;
|
|
85
|
+
private readonly now;
|
|
86
|
+
private readonly onEvent;
|
|
87
|
+
constructor(opts?: TabClaimRegistryOptions);
|
|
88
|
+
/** Snapshot of the current claim for a tab, or null when free or expired. */
|
|
89
|
+
getClaim(tab_id: string): TabClaim | null;
|
|
90
|
+
/** Explicit claim. Returns conflict if another agent owns the tab. Same-agent re-claims refresh activity and update the label/TTL. */
|
|
91
|
+
claim(tab_id: string, caller: AgentCaller, opts?: {
|
|
92
|
+
ttlMinutes?: number;
|
|
93
|
+
label?: string;
|
|
94
|
+
}): ClaimOutcome;
|
|
95
|
+
/** For action tools. Auto-claims a free tab for the caller, refreshes when the caller already owns it, conflicts otherwise. */
|
|
96
|
+
ensureActionAllowed(tab_id: string, caller: AgentCaller): ClaimOutcome;
|
|
97
|
+
/** Explicit release. Only the owner may release. */
|
|
98
|
+
release(tab_id: string, caller: AgentCaller): {
|
|
99
|
+
ok: true;
|
|
100
|
+
} | {
|
|
101
|
+
ok: false;
|
|
102
|
+
reason: 'not-claimed' | 'not-owner';
|
|
103
|
+
existing?: TabClaim;
|
|
104
|
+
};
|
|
105
|
+
/** Claims owned by this caller, sorted by `claimed_at`. */
|
|
106
|
+
myTabs(caller: AgentCaller): TabClaim[];
|
|
107
|
+
/** Drop every claim held by `agent_id` (called when a proxy disconnects). */
|
|
108
|
+
onAgentDisconnect(agent_id: string): TabClaim[];
|
|
109
|
+
/** Sweep claims past their TTL. Returns the dropped claims. */
|
|
110
|
+
pruneStale(): TabClaim[];
|
|
111
|
+
/** Test helper. Production callers should not need this. */
|
|
112
|
+
size(): number;
|
|
113
|
+
private isExpired;
|
|
114
|
+
private claimInternal;
|
|
115
|
+
}
|
|
116
|
+
/** User-facing error string for action tools when another agent holds the tab. Kept here so the wording is consistent across handlers. */
|
|
117
|
+
export declare function formatClaimConflict(caller: AgentCaller, existing: TabClaim, nowMs?: number): string;
|