diff options
| author | amankumarsingh77 <amankumarsingh7702@gmal.com> | 2026-05-05 23:23:59 +0530 |
|---|---|---|
| committer | amankumarsingh77 <amankumarsingh7702@gmal.com> | 2026-05-05 23:23:59 +0530 |
| commit | 20ceaa1c6ade4950ed7d16caa5269ad2f4805db4 (patch) | |
| tree | df4459a67b08533704f0dd2a1c85010578471917 /src/cdp/ownership.ts | |
| parent | e42d9538ad5dafd4e11cf8f5a3912ebf0763d770 (diff) | |
feat: tab ownership — isolate harness tabs from the user's browsing
Track which page targets this harness session opened so the agent can never
attach to, switch to, or close a tab the user opened themselves. New tabs
are spawned in a dedicated Chrome window (newWindow:true on first open,
openerId chaining after) so harness activity stays visually separate.
- new OwnershipRegistry (src/cdp/ownership.ts) tracks owned targetIds and
the harness window seed; mutations notify a single listener that persists
to BrowserState (ownedTargetIds, harnessWindowTargetId)
- attachFirstPage prefers an already-owned live target; otherwise opens a
fresh harness-owned tab in its own window instead of grabbing whatever
page the user is on
- session subscribes Target.setDiscoverTargets and reaps targetDestroyed
events so the registry stays in sync with reality
- listTabs returns owned:boolean per tab and reconciles dead ids
- browser_list_tabs defaults to scope:"owned"; pass scope:"all" to inspect
the user's other tabs (read-only — they cannot be switched to or closed)
- browser_switch_tab refuses non-owned tabs, with a clear remediation hint
- browser_new_tab opens inside the harness window via openerId, falling
back to a fresh window when the seed has been closed
- new browser_close_tab tool — owned tabs only, supports unique hex prefix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/cdp/ownership.ts')
| -rw-r--r-- | src/cdp/ownership.ts | 72 |
1 files changed, 72 insertions, 0 deletions
diff --git a/src/cdp/ownership.ts b/src/cdp/ownership.ts new file mode 100644 index 0000000..f221d6a --- /dev/null +++ b/src/cdp/ownership.ts @@ -0,0 +1,72 @@ +/** + * Tracks which tab targetIds belong to this harness session. + * + * The harness attaches to the user's running Chrome, which is also full of + * the user's own tabs. Without an ownership boundary the agent could attach + * to (or close) personal tabs by accident. The registry is the single source + * of truth for "did this session open that tab?". + * + * Mutations notify a single change listener so the orchestrator can persist + * to disk via the existing BrowserState flow without the registry needing to + * know about the pi storage API. + */ + +export type OwnershipRegistry = { + add(targetId: string): void; + remove(targetId: string): void; + has(targetId: string): boolean; + list(): ReadonlyArray<string>; + /** Replace the full set — used on restoreState to re-hydrate from disk. */ + replaceAll(ids: ReadonlyArray<string>): void; + setHarnessWindow(targetId: string | undefined): void; + harnessWindow(): string | undefined; + onChange(cb: () => void): void; +}; + +export const createOwnershipRegistry = ( + initial?: { readonly ownedTargetIds?: ReadonlyArray<string>; readonly harnessWindowTargetId?: string }, +): OwnershipRegistry => { + const owned = new Set<string>(initial?.ownedTargetIds ?? []); + let harnessWindow: string | undefined = initial?.harnessWindowTargetId; + let listener: (() => void) | null = null; + + const notify = (): void => { + if (listener) listener(); + }; + + return { + add(targetId) { + if (owned.has(targetId)) return; + owned.add(targetId); + notify(); + }, + remove(targetId) { + if (!owned.has(targetId) && harnessWindow !== targetId) return; + owned.delete(targetId); + if (harnessWindow === targetId) harnessWindow = undefined; + notify(); + }, + has(targetId) { + return owned.has(targetId); + }, + list() { + return [...owned]; + }, + replaceAll(ids) { + owned.clear(); + for (const id of ids) owned.add(id); + notify(); + }, + setHarnessWindow(targetId) { + if (harnessWindow === targetId) return; + harnessWindow = targetId; + notify(); + }, + harnessWindow() { + return harnessWindow; + }, + onChange(cb) { + listener = cb; + }, + }; +}; |
