Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
// One tab, driven through its own webContents.debugger (CDP 1.3). No
// browser-wide DevTools port is involved.
//
// Theseus itself attaches the same debugger to freeze background tabs, so a
// driver attaches only when nothing is attached and detaches only what it
// attached. Page reads run in an isolated world that page scripts cannot see
// or patch.
const { FOCUS _GUARD , isSecretField } = require ( "./policy.cjs" ) ;
const CMD _TIMEOUT _MS = 30000 ;
const WORLD = "theseus-claude-connector" ;
// Keys by DOM `key` name: code, Windows virtual key code, and the text a
// keyDown produces (none for non-printing keys).
const KEYS = {
Enter : { code : "Enter" , vk : 13 , text : "\r" } ,
Tab : { code : "Tab" , vk : 9 } ,
Backspace : { code : "Backspace" , vk : 8 } ,
Delete : { code : "Delete" , vk : 46 } ,
Escape : { code : "Escape" , vk : 27 } ,
ArrowLeft : { code : "ArrowLeft" , vk : 37 } ,
ArrowUp : { code : "ArrowUp" , vk : 38 } ,
ArrowRight : { code : "ArrowRight" , vk : 39 } ,
ArrowDown : { code : "ArrowDown" , vk : 40 } ,
Home : { code : "Home" , vk : 36 } ,
End : { code : "End" , vk : 35 } ,
PageUp : { code : "PageUp" , vk : 33 } ,
PageDown : { code : "PageDown" , vk : 34 } ,
" " : { code : "Space" , vk : 32 , text : " " } ,
} ;
const KEY _ALIASES = { enter : "Enter" , return : "Enter" , tab : "Tab" , backspace : "Backspace" , delete : "Delete" , del : "Delete" , escape : "Escape" , esc : "Escape" , left : "ArrowLeft" , right : "ArrowRight" , up : "ArrowUp" , down : "ArrowDown" , arrowleft : "ArrowLeft" , arrowright : "ArrowRight" , arrowup : "ArrowUp" , arrowdown : "ArrowDown" , home : "Home" , end : "End" , pageup : "PageUp" , pagedown : "PageDown" , space : " " } ;
const MOD _BITS = { alt : 1 , ctrl : 2 , control : 2 , meta : 4 , cmd : 4 , shift : 8 } ;
// Editing shortcuts Chromium only honours from synthetic events when the
// command is named.
const KEY _COMMANDS = { "2:a" : [ "selectAll" ] , "2:z" : [ "undo" ] , "10:z" : [ "redo" ] , "2:y" : [ "redo" ] } ;
function keyInfo ( name ) {
const k = KEYS [ name ] || KEYS [ KEY _ALIASES [ String ( name ) . toLowerCase ( ) ] ] ;
if ( k ) return { key : KEYS [ name ] ? name : KEY _ALIASES [ String ( name ) . toLowerCase ( ) ] , ... k } ;
if ( [ ... String ( name ) ] . length === 1 ) {
const ch = String ( name ) ;
const up = ch . toUpperCase ( ) ;
const code = /[a-z]/i . test ( ch ) ? ` Key ${ up } ` : /[0-9]/ . test ( ch ) ? ` Digit ${ ch } ` : "" ;
return { key : ch , code , vk : /[a-z0-9]/i . test ( ch ) ? up . charCodeAt ( 0 ) : 0 , text : ch } ;
}
throw new Error ( ` unknown key: ${ name } ` ) ;
}
// Interactive roles shown by snapshot({interactive_only: true}), plus
// headings so the client still sees where things are.
const INTERACTIVE = new Set ( [ "button" , "link" , "textbox" , "searchbox" , "combobox" , "checkbox" , "radio" , "menuitem" , "menuitemcheckbox" , "menuitemradio" , "tab" , "switch" , "slider" , "spinbutton" , "option" , "listbox" , "treeitem" , "heading" ] ) ;
const SKIP _ROLES = new Set ( [ "none" , "generic" , "InlineTextBox" , "LineBreak" , "presentation" , "LayoutTable" , "LayoutTableRow" , "LayoutTableCell" ] ) ;
class TabDriver {
constructor ( tabId , wc , { onGone } = { } ) {
this . tabId = tabId ;
this . wc = wc ;
this . attachedByUs = false ;
this . ctx = null ; // isolated world execution context id
this . refs = new Map ( ) ; // "e12" -> backendDOMNodeId
this . refSeq = 0 ;
this . onGone = onGone ;
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
// Approved-origin pin: set once per Controller.use() so each read/input
// method can bail if the page redirected to a different origin between
// approval and the CDP call. See _checkOrigin.
this . expectedOrigin = null ;
this . _originOf = null ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
this . _onDetach = ( ) => { this . attachedByUs = false ; this . ctx = null ; } ;
this . _onDestroyed = ( ) => { if ( this . onGone ) this . onGone ( this . tabId ) ; } ;
wc . debugger . on ( "detach" , this . _onDetach ) ;
wc . once ( "destroyed" , this . _onDestroyed ) ;
this . _onNav = ( ) => { this . ctx = null ; this . refs . clear ( ) ; } ;
wc . on ( "did-navigate" , this . _onNav ) ;
}
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
// Called by Controller.use() right after site approval. Each read/input
// method re-checks the current URL's origin against this before firing
// any CDP command, so a meta-refresh or soft-nav to an unapproved origin
// between approval and the call can't leak content or steer input.
setExpectedOrigin ( origin , originOf ) {
this . expectedOrigin = origin || null ;
this . _originOf = typeof originOf === "function" ? originOf : null ;
}
_checkOrigin ( ) {
if ( ! this . expectedOrigin || ! this . _originOf ) return ;
if ( this . wc . isDestroyed ( ) ) throw new Error ( "the tab was closed" ) ;
const cur = this . _originOf ( this . wc . getURL ( ) ) ;
if ( cur !== this . expectedOrigin ) {
throw new Error ( ` refused: the page navigated from ${ this . expectedOrigin } to ${ cur || "about:blank" } ; call list_tabs/snapshot again — the new site needs its own approval ` ) ;
}
}
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
// Called before every use: the tab's webContents is the same object for
// the tab's life, but the debugger may have been detached (DevTools opened).
async ready ( ) {
const dbg = this . wc . debugger ;
if ( ! dbg . isAttached ( ) ) {
dbg . attach ( "1.3" ) ;
this . attachedByUs = true ;
this . ctx = null ;
}
if ( ! this . _prepared || this . _preparedFor !== this . attachedByUs ) {
// Editors that check document.hasFocus() accept input while Theseus
// is in the background.
try { await this . send ( "Emulation.setFocusEmulationEnabled" , { enabled : true } ) ; } catch { }
this . _prepared = true ;
this . _preparedFor = this . attachedByUs ;
}
}
send ( method , params = { } ) {
const p = this . wc . debugger . sendCommand ( method , params ) ;
let t ;
return Promise . race ( [ p , new Promise ( ( _ , rej ) => { t = setTimeout ( ( ) => rej ( new Error ( ` ${ method } timed out ` ) ) , CMD _TIMEOUT _MS ) ; } ) ] ) . finally ( ( ) => clearTimeout ( t ) ) ;
}
async release ( ) {
try { this . wc . debugger . removeListener ( "detach" , this . _onDetach ) ; } catch { }
try { this . wc . removeListener ( "destroyed" , this . _onDestroyed ) ; } catch { }
try { this . wc . removeListener ( "did-navigate" , this . _onNav ) ; } catch { }
if ( this . wc . isDestroyed ( ) ) return ;
try { if ( this . wc . debugger . isAttached ( ) ) await this . send ( "Emulation.setFocusEmulationEnabled" , { enabled : false } ) ; } catch { }
if ( this . attachedByUs ) { try { this . wc . debugger . detach ( ) ; } catch { } }
this . attachedByUs = false ;
}
// ---- isolated world ----------------------------------------------------
async _context ( ) {
if ( this . ctx ) return this . ctx ;
const { frameTree } = await this . send ( "Page.getFrameTree" ) ;
const { executionContextId } = await this . send ( "Page.createIsolatedWorld" , { frameId : frameTree . frame . id , worldName : WORLD , grantUniveralAccess : false } ) ;
this . ctx = executionContextId ;
return this . ctx ;
}
// Evaluate an expression in the isolated world; the value comes back as JSON.
async isolated ( expression ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
for ( let attempt = 0 ; attempt < 2 ; attempt ++ ) {
const contextId = await this . _context ( ) ;
let r ;
try {
r = await this . send ( "Runtime.evaluate" , { expression , contextId , returnByValue : true , awaitPromise : true } ) ;
} catch ( e ) {
// The document changed under us: a new world on the new document.
if ( /context|Cannot find|not found/i . test ( e ? . message || "" ) && attempt === 0 ) { this . ctx = null ; continue ; }
throw e ;
}
if ( r . exceptionDetails ) {
const d = r . exceptionDetails ;
throw new Error ( ( d . exception && d . exception . description ) || d . text || "script failed" ) ;
}
return r . result ? r . result . value : undefined ;
}
throw new Error ( "page context unavailable" ) ;
}
// ---- reading -----------------------------------------------------------
async pageInfo ( ) {
return this . isolated ( ` ({ url: location.href, title: document.title, readyState: document.readyState }) ` ) ;
}
async text ( maxChars ) {
const r = await this . isolated ( ` (() => { const b = document.body || document.documentElement; return { url: location.href, title: document.title, text: b ? b.innerText : "" }; })() ` ) ;
const n = Math . max ( 500 , Math . min ( Number ( maxChars ) || 40000 , 400000 ) ) ;
const truncated = r . text . length > n ;
return { ... r , text : truncated ? r . text . slice ( 0 , n ) : r . text , truncated , length : r . text . length } ;
}
async snapshot ( { interactiveOnly = false , maxLines = 1500 } = { } ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
await this . send ( "Accessibility.enable" ) . catch ( ( ) => { } ) ;
await this . send ( "DOM.enable" ) . catch ( ( ) => { } ) ;
await this . send ( "DOM.getDocument" , { depth : 0 } ) . catch ( ( ) => { } ) ;
const { nodes } = await this . send ( "Accessibility.getFullAXTree" ) ;
this . refs . clear ( ) ;
this . refSeq = 0 ;
const byId = new Map ( nodes . map ( ( n ) => [ n . nodeId , n ] ) ) ;
const root = nodes . find ( ( n ) => ! n . parentId ) || nodes [ 0 ] ;
const lines = [ ] ;
let truncated = false ;
const prop = ( n , name ) => { const p = ( n . properties || [ ] ) . find ( ( x ) => x . name === name ) ; return p ? p . value && p . value . value : undefined ; } ;
const walk = ( n , depth , parentName ) => {
if ( ! n || truncated ) return ;
const role = n . role && n . role . value ;
const name = ( n . name && n . name . value ) || "" ;
const editable = prop ( n , "editable" ) ;
const show = ! n . ignored && role && ! SKIP _ROLES . has ( role ) && ! ( role === "StaticText" && ( interactiveOnly || name === parentName ) )
&& ( ! interactiveOnly || INTERACTIVE . has ( role ) || editable || prop ( n , "focusable" ) && name ) ;
let nextDepth = depth ;
if ( show ) {
if ( lines . length >= maxLines ) { truncated = true ; return ; }
let line = ` ${ " " . repeat ( Math . min ( depth , 30 ) ) } ${ role === "StaticText" ? "text" : role } ` ;
if ( name ) line += ` " ${ name . length > 160 ? name . slice ( 0 , 160 ) + "…" : name } " ` ;
if ( n . backendDOMNodeId && role !== "StaticText" ) {
const ref = ` e ${ ++ this . refSeq } ` ;
this . refs . set ( ref , n . backendDOMNodeId ) ;
line += ` [ref= ${ ref } ] ` ;
}
const flags = [ ] ;
if ( editable ) flags . push ( ` editable= ${ editable } ` ) ;
for ( const f of [ "focused" , "checked" , "disabled" , "expanded" , "selected" , "required" ] ) { const v = prop ( n , f ) ; if ( v !== undefined && v !== false && v !== "false" ) flags . push ( v === true ? f : ` ${ f } = ${ v } ` ) ; }
const level = prop ( n , "level" ) ; if ( level ) flags . push ( ` level= ${ level } ` ) ;
if ( n . value && n . value . value !== undefined && n . value . value !== "" ) flags . push ( ` value=" ${ String ( n . value . value ) . slice ( 0 , 80 ) } " ` ) ;
if ( flags . length ) line += ` ( ${ flags . join ( ", " ) } ) ` ;
lines . push ( line ) ;
nextDepth = depth + 1 ;
}
for ( const c of n . childIds || [ ] ) walk ( byId . get ( c ) , nextDepth , show ? name : parentName ) ;
} ;
walk ( root , 0 , "" ) ;
return { lines , truncated , refs : this . refSeq } ;
}
backendNode ( ref ) {
const id = this . refs . get ( String ( ref || "" ) ) ;
if ( ! id ) throw new Error ( ` unknown ref " ${ ref } "; take a new snapshot (refs reset on every snapshot and navigation) ` ) ;
return id ;
}
async describe ( backendNodeId ) {
const { node } = await this . send ( "DOM.describeNode" , { backendNodeId } ) ;
return node ;
}
// ---- input -------------------------------------------------------------
async centerOf ( backendNodeId ) {
await this . send ( "DOM.scrollIntoViewIfNeeded" , { backendNodeId } ) . catch ( ( ) => { } ) ;
const { quads } = await this . send ( "DOM.getContentQuads" , { backendNodeId } ) ;
const q = ( quads || [ ] ) . find ( ( x ) => Math . abs ( ( x [ 2 ] - x [ 0 ] ) * ( x [ 5 ] - x [ 1 ] ) ) > 1 ) ;
if ( ! q ) throw new Error ( "the element has no visible box to click" ) ;
return { x : ( q [ 0 ] + q [ 2 ] + q [ 4 ] + q [ 6 ] ) / 4 , y : ( q [ 1 ] + q [ 3 ] + q [ 5 ] + q [ 7 ] ) / 4 } ;
}
async clickAt ( x , y , { button = "left" , clickCount = 1 } = { } ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
const buttons = { left : 1 , right : 2 , middle : 4 } [ button ] || 1 ;
await this . send ( "Input.dispatchMouseEvent" , { type : "mouseMoved" , x , y } ) ;
for ( let i = 1 ; i <= clickCount ; i ++ ) {
await this . send ( "Input.dispatchMouseEvent" , { type : "mousePressed" , x , y , button , buttons , clickCount : i } ) ;
await this . send ( "Input.dispatchMouseEvent" , { type : "mouseReleased" , x , y , button , buttons : 0 , clickCount : i } ) ;
}
}
async focus ( backendNodeId ) {
await this . send ( "DOM.focus" , { backendNodeId } ) ;
}
async assertNotSecret ( backendNodeId ) {
if ( isSecretField ( await this . describe ( backendNodeId ) ) ) throw new Error ( "refused: that element is a password field" ) ;
}
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
// Point-level secret check: for click(x, y) without a ref, resolve the
// element at the pointer and refuse if it's a password / one-time-code
// field. Falls back silently if the point lies on nothing we can resolve.
async assertNotSecretAt ( x , y ) {
this . _checkOrigin ( ) ;
await this . send ( "DOM.enable" ) . catch ( ( ) => { } ) ;
let r ;
try { r = await this . send ( "DOM.getNodeForLocation" , { x : Math . round ( x ) , y : Math . round ( y ) , includeUserAgentShadowDOM : true } ) ; } catch { return ; }
if ( ! r || ! r . backendNodeId ) return ;
let node ;
try { node = await this . describe ( r . backendNodeId ) ; } catch { return ; }
if ( isSecretField ( node ) ) throw new Error ( "refused: that point is on a password field" ) ;
}
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
// Throws if typing now would land in a password field.
async focusGuard ( ) {
const g = await this . isolated ( FOCUS _GUARD ) ;
if ( ! g || ! g . ok ) throw new Error ( ` refused: ${ ( g && g . reason ) || "could not check the focused element" } ` ) ;
return g ;
}
async insertText ( text ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
if ( ! text ) return ;
this . _checkOrigin ( ) ;
await this . send ( "Input.insertText" , { text } ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
}
async press ( name , modifiers = [ ] ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
const k = keyInfo ( name ) ;
const mods = ( Array . isArray ( modifiers ) ? modifiers : String ( modifiers || "" ) . split ( "+" ) ) . reduce ( ( m , x ) => m | ( MOD _BITS [ String ( x ) . trim ( ) . toLowerCase ( ) ] || 0 ) , 0 ) ;
const chord = ( mods & ( 1 | 2 | 4 ) ) !== 0 ;
let text = chord ? undefined : k . text ;
if ( text && mods & 8 && text . length === 1 ) text = text . toUpperCase ( ) ;
const base = { key : text && text . length === 1 && ! KEYS [ k . key ] ? text : k . key , code : k . code , windowsVirtualKeyCode : k . vk , nativeVirtualKeyCode : k . vk , modifiers : mods } ;
const commands = KEY _COMMANDS [ ` ${ mods } : ${ String ( k . key ) . toLowerCase ( ) } ` ] ;
await this . send ( "Input.dispatchKeyEvent" , { type : text ? "keyDown" : "rawKeyDown" , ... base , ... ( text ? { text , unmodifiedText : text } : { } ) , ... ( commands ? { commands } : { } ) } ) ;
await this . send ( "Input.dispatchKeyEvent" , { type : "keyUp" , ... base } ) ;
}
// Text the way rich editors register it: each line as an IME-style insert,
// a real Enter key between lines. mode "keys" sends every character as its
// own key press instead (slower; for fields that only listen to keydown).
async type ( text , { mode = "insert" } = { } ) {
const lines = String ( text ) . replace ( /\r\n?/g , "\n" ) . split ( "\n" ) ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
if ( mode === "keys" ) { for ( const ch of lines [ i ] ) await this . press ( ch ) ; }
else await this . insertText ( lines [ i ] ) ;
if ( i < lines . length - 1 ) await this . press ( "Enter" ) ;
}
}
async setFiles ( backendNodeId , files ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
const node = await this . describe ( backendNodeId ) ;
const attrs = { } ;
const a = node . attributes || [ ] ;
for ( let i = 0 ; i + 1 < a . length ; i += 2 ) attrs [ String ( a [ i ] ) . toLowerCase ( ) ] = String ( a [ i + 1 ] ) ;
if ( String ( node . localName ) . toLowerCase ( ) !== "input" || String ( attrs . type || "" ) . toLowerCase ( ) !== "file" ) throw new Error ( "that ref is not a file input (<input type=file>)" ) ;
await this . send ( "DOM.setFileInputFiles" , { files , backendNodeId } ) ;
}
// ---- screenshots -------------------------------------------------------
// Scaled to CSS pixels, so a point in the image is a point for click(x, y).
async screenshot ( { fullPage = false , format } = { } ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
const dpr = Number ( await this . isolated ( "window.devicePixelRatio" ) ) || 1 ;
const m = await this . send ( "Page.getLayoutMetrics" ) ;
const vv = m . cssVisualViewport || m . visualViewport ;
const fmt = format === "jpeg" || ( fullPage && format !== "png" ) ? "jpeg" : "png" ;
let clip ;
if ( fullPage ) {
const cs = m . cssContentSize || m . contentSize ;
clip = { x : 0 , y : 0 , width : Math . ceil ( cs . width ) , height : Math . min ( Math . ceil ( cs . height ) , 12000 ) , scale : 1 / dpr } ;
} else {
clip = { x : vv . pageX , y : vv . pageY , width : Math . floor ( vv . clientWidth ) , height : Math . floor ( vv . clientHeight ) , scale : 1 / dpr } ;
}
const r = await this . send ( "Page.captureScreenshot" , { format : fmt , ... ( fmt === "jpeg" ? { quality : 75 } : { } ) , clip , captureBeyondViewport : fullPage , fromSurface : true } ) ;
return { data : r . data , mimeType : ` image/ ${ fmt } ` , width : clip . width , height : clip . height , fullPage , truncated : fullPage && clip . height >= 12000 } ;
}
async waitFor ( { selector , text , timeoutMs = 10000 } ) {
const until = Date . now ( ) + Math . max ( 100 , Math . min ( Number ( timeoutMs ) || 10000 , 60000 ) ) ;
const probe = selector
? ` (() => { const el = document.querySelector( ${ JSON . stringify ( String ( selector ) ) } ); if (!el) return false; const r = el.getBoundingClientRect(); return { visible: r.width > 0 && r.height > 0 }; })() `
: ` (() => { const b = document.body; return !!b && b.innerText.includes( ${ JSON . stringify ( String ( text ) ) } ); })() ` ;
for ( ; ; ) {
let hit = false ;
try { hit = await this . isolated ( probe ) ; } catch ( e ) { if ( ! /context/i . test ( e ? . message || "" ) ) throw e ; }
if ( hit ) return { found : true , ... ( typeof hit === "object" ? hit : { } ) } ;
if ( Date . now ( ) > until ) return { found : false } ;
await new Promise ( ( r ) => setTimeout ( r , 250 ) ) ;
}
}
async evaluate ( expression ) {
Theseus 0.3.107: two-section home + Claude connector hardening
Home page splits into two self-contained panels so the left-column
links no longer scale with the Silent Mode cards. The slot grid is
narrower, the icons default closer to Windows desktop-shortcut size,
and each section carries its own subtle panel background and heading.
Claude connector hardening (the opt-in MCP add-on from 0.3.106):
- Pin the approved origin to the TabDriver on every Controller.use()
and re-check the tab's current URL before each CDP read / input
(isolated, snapshot, screenshot, clickAt, insertText, press,
setFiles, evaluate). A meta-refresh or soft-nav to an unapproved
origin between approval and the call now throws instead of leaking
content or steering input to the new site.
- Rotate the bearer token on every stop() so toggling the connector
off (toolbar chip, panel switch, deactivate) invalidates existing
credentials. Re-enabling issues a fresh token; the panel's install
strings update on the next start. Matches the mental model where
"off" means "the keys change" instead of "the server stops".
- click without a ref now resolves the element at (x, y) through
DOM.getNodeForLocation and refuses if it's a password / one-time-
code field. Previously only ref-based click ran isSecretField; raw
coordinates let focus land on a password field before FOCUS_GUARD
kicked in to block typing.
2026-10-23 19:12:52 +03:00
this . _checkOrigin ( ) ;
Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.
- Host: a "tab-control" capability (built-in add-ons only) for listing,
opening, navigating, closing and focusing tabs, handing out web tabs'
webContents (never Settings, extension or home tabs), and a toolbar chip
plus tab outline while a client is connected; the chip's X stops it.
Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
read_page_text, snapshot (accessibility tree with refs), click, type
(insertText + real Enter), press_key, paste_html / paste_text (system
clipboard + webContents.paste, clipboard restored), upload_file
(DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
fields are refused; uploads only from folders the user added; requests
from web pages or foreign Host headers are refused; every action is
logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
throwaway profile that types, pastes HTML with a heading and a link, and
uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
const r = await this . send ( "Runtime.evaluate" , { expression : String ( expression ) , returnByValue : true , awaitPromise : true , userGesture : true } ) ;
if ( r . exceptionDetails ) {
const d = r . exceptionDetails ;
throw new Error ( ( d . exception && d . exception . description ) || d . text || "script failed" ) ;
}
return r . result ;
}
}
module . exports = { TabDriver , keyInfo } ;