machine-bridge-mcp 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-07-12
4
+
5
+ ### Session context and capability selection
6
+
7
+ - Add a global `model_instructions_file` in `~/.config/machine-bridge-mcp/agent.json`. Stdio and remote MCP initialization append the bounded user-designated instructions when the local runtime is reachable; `session_bootstrap` exposes the same content explicitly. Project manifests cannot override the global file.
8
+ - Add `resolve_task_capabilities`, which rescans effective instructions, filesystem skills, and registered commands for every task, returns a refresh fingerprint, ranks relevant capabilities, optionally loads the best skill, and recommends application/browser/file/Git/process tools. Newly added or edited skills are visible without daemon restart or dynamic MCP tool registration. Multilingual tokenization avoids weak single-character Chinese overlap when automatically selecting a skill.
9
+ - Preserve the host boundary: Machine Bridge automates discovery, refresh, ranking, and progressive loading, while the ChatGPT/MCP host still decides whether a tool is exposed, approved, or invoked.
10
+
11
+ ### Existing-profile browser and local applications
12
+
13
+ - Add a packaged Manifest V3 Chromium extension and authenticated loopback machine broker for the user's existing browser profile, windows, tabs, cookies, extensions, and login state. Multiple workspace or stdio runtimes share one extension connection through authenticated broker clients.
14
+ - Add structured browser tools for tab listing, current DOM source, frame-aware and open-Shadow-DOM interactive-element inspection, navigation/actions, complex multi-field forms, visible screenshots, and resource-backed file inputs. Page operations run through a fixed packaged module injected into the target frame rather than caller-provided or service-worker-closure code. Registered local resources can provide sensitive field text or upload bytes without returning those values through MCP results.
15
+ - Add installed-application discovery/opening and structured macOS Accessibility inspection/actions. The implementation uses fixed JXA code and does not accept caller-supplied JavaScript, AppleScript, JXA, or browser-extension source. Application paths are normalized to process names, selector indices apply to filtered matches, Linux desktop launchers use `gio launch`, Windows discovery prioritizes Start Menu launchers, and secure Accessibility fields never return values.
16
+ - Add `machine-mcp browser status|setup|pair|path` for one-time unpacked-extension setup and diagnosis. Pairing tokens remain in owner-only state and non-cacheable loopback HTML rather than MCP output, browser URL fragments, or operational logs. Established pairing is replacement-locked and requires a browser-action click on the active local pairing page before switching broker state. The extension badge reports connection state and its action opens the saved pairing page; signed browser-store packaging is the intended mass-market distribution path.
17
+
18
+ ### Architecture, security, and verification
19
+
20
+ - Split agent context, application automation, and browser broker into dedicated local domain modules while retaining one static catalog shared by Worker and stdio. Bound Worker initialization bootstrap to a short failure-tolerant daemon call.
21
+ - Add loopback Host validation, extension-origin checks, authenticated extension/runtime WebSocket subprotocols, bounded messages/concurrency/source/forms/uploads, proxy timeout/cancellation cleanup, restricted-page handling, secret/value/result non-disclosure, and full-profile gating. Treat the owner-only browser pairing file as a recognized state-root entry so safe uninstall remains available.
22
+ - Extend version synchronization to the browser-extension manifest and add broker owner/client proxy, pairing-token, resource-upload, stdio initialization, live skill refresh, and remote initialization regression coverage. Update architecture, security, operations, logging, privacy, testing, client, release, and user documentation.
23
+
3
24
  ## 0.9.0 - 2026-07-12
4
25
 
5
26
  ### Agent context and local workflows
package/README.md CHANGED
@@ -108,36 +108,37 @@ machine-mcp stdio --workspace /path/to/project
108
108
 
109
109
  The stdio server writes only JSON-RPC messages to stdout and operational logs to stderr. See [docs/CLIENTS.md](docs/CLIENTS.md) for the host/model distinction and transport trade-offs.
110
110
 
111
- ## Agent instructions, local skills, and command registry
111
+ ## Session instructions, local skills, commands, apps, and browser
112
112
 
113
- For substantive workspace tasks, clients can call `agent_context` for the relevant file or directory. Its default instruction discovery mirrors Codex: under unrestricted policy it first checks `CODEX_HOME` or `~/.codex`, then walks from the project root to the target directory, selecting the first non-empty `AGENTS.override.md` or `AGENTS.md` in each scope. Deeper guidance has higher precedence, and the default combined budget is 32 KiB.
114
-
115
- Skill discovery also follows Codex-style progressive disclosure. By default, Machine Bridge scans `.agents/skills` from the target directory through the project root; unrestricted policy additionally enables `~/.agents/skills` and, on Unix-like systems, `/etc/codex/skills`. `agent_context` initially returns bounded metadata, while `load_local_skill` returns the selected `SKILL.md` and file inventory without executing anything implicitly.
116
-
117
- A project can add custom instruction candidates, explicit skill roots, and registered commands with `.machine-bridge/agent.json`:
113
+ Machine Bridge loads a user-selected global instruction file and repository-specific guidance without changing the static MCP catalog. Configure the global file in `~/.config/machine-bridge-mcp/agent.json`:
118
114
 
119
115
  ```json
120
116
  {
121
117
  "version": 1,
122
- "instruction_files": [
123
- "PROJECT.override.md",
124
- "AGENTS.override.md",
125
- "AGENTS.md"
126
- ],
127
- "instruction_max_bytes": 65536,
128
- "commands": {
129
- "check": {
130
- "description": "Run repository validation.",
131
- "argv": ["npm", "run", "check"],
132
- "cwd": ".",
133
- "timeout_seconds": 600,
134
- "allow_extra_args": false
135
- }
136
- }
118
+ "model_instructions_file": "~/.config/machine-bridge-mcp/MODEL.md"
137
119
  }
138
120
  ```
139
121
 
140
- `instruction_files` is a priority list: each directory contributes only its first non-empty candidate. Deeper manifests override earlier settings and can remove an inherited command with `null`. `run_local_command` executes registered commands as direct argv processes without shell parsing and is available only when direct execution is enabled. See [Agent context, local skills, and registered commands](docs/AGENT_CONTEXT.md) for the complete contract.
122
+ The file is prepended to MCP initialization instructions for every new stdio or remote session when the local runtime is reachable. `session_bootstrap` exposes the same material explicitly. `agent_context` adds target-specific `AGENTS.override.md`/`AGENTS.md` precedence, and `resolve_task_capabilities` rescans skills and registered commands for the current task, ranks matches, optionally loads the best skill, and recommends relevant local tools.
123
+
124
+ Skill discovery follows Codex-style progressive disclosure. Default roots are target-to-project `.agents/skills`; unrestricted policy also enables user/admin roots. Newly added or edited skills are found on the next resolver/list call without restarting the daemon. A project can customize instruction candidates, skill roots, and direct-argv registered commands with `.machine-bridge/agent.json`. See [Session instructions, skills, commands, and capability discovery](docs/AGENT_CONTEXT.md).
125
+
126
+ Under canonical `full`, Machine Bridge also exposes structured local automation:
127
+
128
+ - installed application discovery/opening and macOS Accessibility inspection/actions;
129
+ - a packaged Chromium extension that controls the user's existing daily browser profile, active tabs, login state, and windows;
130
+ - current DOM source and frame/open-Shadow-DOM inspection, structured page actions, complex multi-field forms, resource-backed secret fields, resource-backed file uploads, and screenshots.
131
+
132
+ One-time browser setup:
133
+
134
+ ```sh
135
+ machine-mcp browser setup
136
+ machine-mcp browser status
137
+ ```
138
+
139
+ Load the printed unpacked-extension directory once in Chrome, Edge, Brave, Vivaldi, or another compatible Chromium browser. The extension badge reports connection state, and clicking it opens the saved local pairing page. The local pairing token remains in owner-only state and the loopback pairing page; it is not returned through MCP. For a mass-market release, distribute the same extension as a signed browser-store build rather than asking end users to enable Developer mode. See [Local application and browser automation](docs/LOCAL_AUTOMATION.md).
140
+
141
+ Machine Bridge can discover, refresh, rank, and load capabilities automatically. The ChatGPT/MCP host still owns tool selection and approval, so the server cannot force a host to expose or invoke a recommended skill, command, app, or browser operation.
141
142
 
142
143
  ## Policy controls
143
144
 
@@ -264,7 +265,9 @@ The exact `tools/list` response reflects the active local policy. Definitions co
264
265
 
265
266
  - `server_info`
266
267
  - `project_overview`
268
+ - `session_bootstrap` — session/global instructions and capability refresh metadata
267
269
  - `agent_context` — effective instructions, skill summaries, and registered commands for a target path
270
+ - `resolve_task_capabilities` — live skill/command ranking and local automation recommendations
268
271
  - `list_local_skills`
269
272
  - `load_local_skill` — load instructions and file inventory without implicit execution
270
273
  - `list_local_commands`
@@ -275,6 +278,23 @@ The exact `tools/list` response reflects the active local policy. Definitions co
275
278
  - `view_image` — bounded PNG, JPEG, GIF, or WebP as native MCP image content
276
279
  - `search_text`
277
280
 
281
+ ### Local applications and browser (`full`)
282
+
283
+ - `list_local_applications`
284
+ - `open_local_application`
285
+ - `inspect_local_application` — bounded macOS Accessibility tree
286
+ - `operate_local_application` — structured Accessibility action, no arbitrary script source
287
+ - `browser_status`
288
+ - `pair_browser_extension`
289
+ - `browser_list_tabs`
290
+ - `browser_get_source` — bounded current DOM HTML, including selected frames
291
+ - `browser_inspect_page`
292
+ - `browser_action`
293
+ - `browser_fill_form`
294
+ - `browser_upload_files` — registered local resources to file inputs
295
+ - `browser_screenshot`
296
+
297
+
278
298
  ### Mutation
279
299
 
280
300
  - `write_file` — atomic whole-file write with create-only and SHA-256 checks
@@ -335,6 +355,7 @@ machine-mcp status
335
355
  machine-mcp doctor
336
356
  machine-mcp rotate-secrets
337
357
  machine-mcp resource add|list|check|remove
358
+ machine-mcp browser status|setup|pair|path
338
359
  machine-mcp job submit|inspect|approve|list|read|cancel
339
360
  machine-mcp --print-mcp-credentials
340
361
  machine-mcp uninstall [--keep-worker] [--yes]
@@ -386,7 +407,7 @@ npm pack --dry-run
386
407
 
387
408
  `npm run check` covers privacy and release-impact gates, architecture/link invariants, generated Worker types, TypeScript, JavaScript syntax, catalog-to-runtime handler parity, deterministic relay lifecycle and secure-file tests, local path/write/process/state/log/service invariants, Ed25519/RSA generation and key-pair validation, real-machine `full` sandbox acceptance, a clean npm package-manifest/sensitive-artifact check, managed-job integrity/redaction/finally/cancellation/recovery, a live stdio MCP flow, and a live local OAuth/Worker/WebSocket/MCP flow. GitHub Actions runs the suite on Linux, macOS, and Windows with the pinned Node 26/npm 12 baseline; macOS and package-audit jobs also exercise the documented isolated global installation.
388
409
 
389
- See [docs/AGENT_CONTEXT.md](docs/AGENT_CONTEXT.md), [docs/MANAGED_JOBS.md](docs/MANAGED_JOBS.md), [docs/TESTING.md](docs/TESTING.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/ENGINEERING.md](docs/ENGINEERING.md), and [SECURITY.md](SECURITY.md).
410
+ See [docs/AGENT_CONTEXT.md](docs/AGENT_CONTEXT.md), [docs/LOCAL_AUTOMATION.md](docs/LOCAL_AUTOMATION.md), [docs/MANAGED_JOBS.md](docs/MANAGED_JOBS.md), [docs/TESTING.md](docs/TESTING.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/ENGINEERING.md](docs/ENGINEERING.md), and [SECURITY.md](SECURITY.md).
390
411
 
391
412
  ## Uninstall
392
413
 
package/SECURITY.md CHANGED
@@ -59,7 +59,19 @@ Skill loading is non-executing. `load_local_skill` returns an entrypoint and bou
59
59
 
60
60
  Registered commands are available only under direct-execution-capable policy. They use argv spawning rather than shell parsing, reject undeclared caller arguments, and enforce the manifest timeout as a ceiling. They are not an approval or sandbox boundary: a repository-controlled package script, interpreter, compiler, or executable can still run arbitrary code with local-user authority. Deeper manifests can override or remove inherited commands, so callers must obtain context for the actual target path rather than assuming the repository-root registry applies everywhere.
61
61
 
62
- The remote MCP host remains an independent boundary. Machine Bridge can advertise these tools and instruct the model to bootstrap with `agent_context`, but it cannot force a host to expose or invoke them, and it cannot bypass host confirmation or refusal.
62
+ The remote MCP host remains an independent boundary. Machine Bridge can advertise these tools, append `model_instructions_file` during initialization, and recommend capabilities through `resolve_task_capabilities`, but it cannot force a host to preserve instructions, expose tools, approve calls, or invoke them.
63
+
64
+ The global `model_instructions_file` is an explicit user trust decision. It is read for every profile and sent to the authorized host during session initialization; do not place credentials, private records, or content inappropriate for every connected session in it. Project manifests cannot override this global file. Other global manifest fields that would widen local scope are ignored under workspace-confined profiles.
65
+
66
+ ## Browser and application automation
67
+
68
+ Canonical `full` exposes structured browser and desktop UI authority. The browser extension operates the user's existing Chromium profile, including active login state, and has broad host access so it can inspect arbitrary pages and fill complex forms. An authorized client may therefore read page content, click controls, submit forms, upload registered files, and cause transactions with the user's browser identity. macOS Accessibility actions similarly operate applications with the local user's UI authority.
69
+
70
+ The browser loopback broker validates loopback `Host`, requires a random owner-only bearer subprotocol, accepts extension sockets only with a `chrome-extension://` origin, and authenticates additional local runtimes separately. Initial extension pairing is trust-on-first-use; after pairing, a different localhost page cannot replace the stored broker unless the user clicks the extension action while the genuine pairing page is active. The pairing token is never returned by MCP or written to operational logs. These controls defend against casual cross-origin and DNS-rebinding access; they do not protect against malicious code already running as the same OS user or a compromised browser extension profile.
71
+
72
+ Caller-supplied JavaScript, AppleScript, and JXA source are deliberately unsupported. Actions use fixed implementation code and structured selectors. This removes an avoidable arbitrary-evaluation surface but does not make DOM or Accessibility actions safe. Pages can contain prompt injection, misleading labels, hidden consequences, cross-origin frames unavailable to inspection, or state that changes between inspection and submission. High-impact account, financial, legal, medical, publishing, deletion, and purchase actions require contextual review and any host/user confirmation the client provides.
73
+
74
+ Text and file resources can be injected locally so their contents do not appear in MCP arguments or results. The destination page/application still receives the value, and remote tool metadata/results still traverse the Worker and host. Page source and screenshots can themselves contain secrets. Browser/app tool arguments, source, screenshots, field values, and results are intentionally omitted from operational logs.
63
75
 
64
76
  ## Filesystem exposure
65
77
 
@@ -0,0 +1,33 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Machine Bridge Browser",
4
+ "version": "0.10.0",
5
+ "description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
6
+ "permissions": [
7
+ "tabs",
8
+ "scripting",
9
+ "storage",
10
+ "alarms"
11
+ ],
12
+ "host_permissions": [
13
+ "<all_urls>"
14
+ ],
15
+ "background": {
16
+ "service_worker": "service-worker.js"
17
+ },
18
+ "content_scripts": [
19
+ {
20
+ "matches": [
21
+ "http://127.0.0.1/*"
22
+ ],
23
+ "js": [
24
+ "pairing.js"
25
+ ],
26
+ "run_at": "document_idle"
27
+ }
28
+ ],
29
+ "action": {
30
+ "default_title": "Machine Bridge Browser"
31
+ },
32
+ "version_name": "0.10.0"
33
+ }
@@ -0,0 +1,246 @@
1
+ (() => {
2
+ if (globalThis.__machineBridgePageAutomation) return;
3
+
4
+ const INTERACTIVE_SELECTOR = "a,button,input,select,textarea,[role],[contenteditable='true'],summary";
5
+ const MAX_SHADOW_ROOTS = 200;
6
+
7
+ function collectRoots() {
8
+ const roots = [document];
9
+ for (let index = 0; index < roots.length && roots.length <= MAX_SHADOW_ROOTS; index += 1) {
10
+ const root = roots[index];
11
+ for (const element of root.querySelectorAll("*")) {
12
+ if (element.shadowRoot && !roots.includes(element.shadowRoot)) roots.push(element.shadowRoot);
13
+ if (roots.length > MAX_SHADOW_ROOTS) break;
14
+ }
15
+ }
16
+ return roots;
17
+ }
18
+
19
+ function deepQuerySelectorAll(selector) {
20
+ const results = [];
21
+ for (const root of collectRoots()) results.push(...root.querySelectorAll(selector));
22
+ return results;
23
+ }
24
+
25
+ function rootById(element, id) {
26
+ const root = element.getRootNode?.();
27
+ if (root && typeof root.getElementById === "function") return root.getElementById(id);
28
+ return document.getElementById(id);
29
+ }
30
+
31
+ function inspect(params) {
32
+ const maxElements = Number(params.maxElements) || 300;
33
+ const includeValues = params.includeValues === true;
34
+ const all = deepQuerySelectorAll(INTERACTIVE_SELECTOR);
35
+ const candidates = all.slice(0, maxElements);
36
+ return {
37
+ document: {
38
+ title: document.title,
39
+ url: location.href,
40
+ language: document.documentElement.lang || "",
41
+ forms: deepQuerySelectorAll("form").length,
42
+ open_shadow_roots: Math.max(0, collectRoots().length - 1),
43
+ },
44
+ elements: candidates.map((element, index) => describeElement(element, index, includeValues)),
45
+ truncated: all.length > candidates.length,
46
+ };
47
+ }
48
+
49
+ function action(params) {
50
+ const element = findElement(params.selector);
51
+ if (!element) throw new Error("no element matched selector");
52
+ applyOne(element, params.action, params.value, params.key);
53
+ return { ok: true, element: describeElement(element, 0, false) };
54
+ }
55
+
56
+ function fillForm(params) {
57
+ const results = [];
58
+ for (let index = 0; index < params.fields.length; index += 1) {
59
+ const field = params.fields[index];
60
+ const element = findElement(field.selector);
61
+ if (!element) throw new Error(`no element matched fields[${index}] selector`);
62
+ applyOne(element, field.action, field.value, "");
63
+ results.push({ index, action: field.action, sensitive: field.sensitive === true, element: describeElement(element, index, false) });
64
+ }
65
+ if (params.submit) {
66
+ const submitter = params.submitSelector ? findElement(params.submitSelector) : deepQuerySelectorAll("button[type='submit'],input[type='submit']")[0];
67
+ if (submitter) submitter.click();
68
+ else {
69
+ const field = params.fields.length ? findElement(params.fields[0].selector) : null;
70
+ const form = field?.form || field?.closest?.("form") || deepQuerySelectorAll("form")[0];
71
+ if (!form) throw new Error("no form or submit control found");
72
+ if (typeof form.requestSubmit === "function") form.requestSubmit();
73
+ else form.submit();
74
+ }
75
+ }
76
+ return { ok: true, fields: results, submitted: params.submit === true, values_exposed: false };
77
+ }
78
+
79
+ function uploadFiles(params) {
80
+ const input = findElement(params.selector);
81
+ if (!(input instanceof HTMLInputElement) || input.type !== "file") throw new Error("matched element is not a file input");
82
+ const transfer = new DataTransfer();
83
+ for (const item of params.files) {
84
+ const binary = atob(item.data);
85
+ const bytes = new Uint8Array(binary.length);
86
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
87
+ transfer.items.add(new File([bytes], item.filename, { type: item.mime || "application/octet-stream" }));
88
+ }
89
+ input.files = transfer.files;
90
+ dispatchValueEvents(input);
91
+ return { ok: true, file_count: transfer.files.length, values_exposed: false };
92
+ }
93
+
94
+ function describeElement(element, index, includeValues) {
95
+ const tag = element.tagName.toLowerCase();
96
+ const type = String(element.getAttribute("type") || "").toLowerCase();
97
+ const sensitive = isSensitiveElement(element, tag, type);
98
+ const item = {
99
+ index,
100
+ tag,
101
+ type,
102
+ role: element.getAttribute("role") || implicitRole(element),
103
+ name: accessibleName(element),
104
+ id: element.id || "",
105
+ field_name: element.getAttribute("name") || "",
106
+ label: labelText(element),
107
+ placeholder: element.getAttribute("placeholder") || "",
108
+ disabled: Boolean(element.disabled),
109
+ checked: "checked" in element ? Boolean(element.checked) : null,
110
+ selected: tag === "select" ? element.value : null,
111
+ href: tag === "a" ? element.href : "",
112
+ sensitive,
113
+ in_shadow_dom: element.getRootNode?.() instanceof ShadowRoot,
114
+ };
115
+ if (includeValues && !sensitive && "value" in element) item.value = String(element.value || "").slice(0, 2000);
116
+ return item;
117
+ }
118
+
119
+ function isSensitiveElement(element, tag, type) {
120
+ if (tag !== "input" && tag !== "textarea") return false;
121
+ if (["password", "hidden"].includes(type)) return true;
122
+ const autocomplete = String(element.getAttribute("autocomplete") || "").toLowerCase();
123
+ if (/(?:password|one-time-code|cc-number|cc-csc|cc-exp)/.test(autocomplete)) return true;
124
+ const identity = `${element.id || ""} ${element.getAttribute("name") || ""} ${element.getAttribute("aria-label") || ""} ${element.getAttribute("placeholder") || ""}`.toLowerCase();
125
+ return /(?:password|passwd|secret|token|api[-_ ]?key|otp|one[-_ ]?time|verification|cvc|cvv|security[-_ ]?code|card[-_ ]?number)/.test(identity);
126
+ }
127
+
128
+ function applyOne(element, operation, value, key) {
129
+ element.scrollIntoView({ block: "center", inline: "nearest" });
130
+ if (operation === "click") { element.click(); return; }
131
+ if (operation === "focus") { element.focus(); return; }
132
+ if (operation === "submit") {
133
+ const form = element.form || element.closest("form");
134
+ if (!form) throw new Error("matched element is not associated with a form");
135
+ if (typeof form.requestSubmit === "function") form.requestSubmit(); else form.submit();
136
+ return;
137
+ }
138
+ if (operation === "check" || operation === "uncheck") {
139
+ if (!("checked" in element)) throw new Error("matched element is not checkable");
140
+ const wanted = operation === "check";
141
+ if (Boolean(element.checked) !== wanted) element.click();
142
+ return;
143
+ }
144
+ if (operation === "select") {
145
+ if (!(element instanceof HTMLSelectElement)) throw new Error("matched element is not a select control");
146
+ const text = String(value ?? "");
147
+ const option = [...element.options].find((item) => item.value === text || item.text.trim() === text);
148
+ if (!option) throw new Error("select option was not found");
149
+ element.value = option.value;
150
+ dispatchValueEvents(element);
151
+ return;
152
+ }
153
+ if (operation === "fill") {
154
+ if (!("value" in element) && !element.isContentEditable) throw new Error("matched element is not editable");
155
+ element.focus();
156
+ if (element.isContentEditable) element.textContent = String(value ?? "");
157
+ else setNativeValue(element, String(value ?? ""));
158
+ dispatchValueEvents(element);
159
+ return;
160
+ }
161
+ if (operation === "press") {
162
+ const pressed = String(key || value || "Enter");
163
+ element.focus();
164
+ element.dispatchEvent(new KeyboardEvent("keydown", { key: pressed, bubbles: true, composed: true }));
165
+ element.dispatchEvent(new KeyboardEvent("keyup", { key: pressed, bubbles: true, composed: true }));
166
+ return;
167
+ }
168
+ throw new Error(`unsupported element action: ${operation}`);
169
+ }
170
+
171
+ function setNativeValue(element, value) {
172
+ const prototype = element instanceof HTMLTextAreaElement
173
+ ? HTMLTextAreaElement.prototype
174
+ : element instanceof HTMLSelectElement
175
+ ? HTMLSelectElement.prototype
176
+ : HTMLInputElement.prototype;
177
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
178
+ if (descriptor?.set) descriptor.set.call(element, value);
179
+ else element.value = value;
180
+ }
181
+
182
+ function dispatchValueEvents(element) {
183
+ element.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
184
+ element.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
185
+ }
186
+
187
+ function findElement(selector) {
188
+ let elements = [];
189
+ if (selector.css) elements = deepQuerySelectorAll(selector.css);
190
+ else if (selector.id) elements = deepQuerySelectorAll(`[id="${cssEscape(selector.id)}"]`);
191
+ else elements = deepQuerySelectorAll(INTERACTIVE_SELECTOR);
192
+ elements = elements.filter((element) => {
193
+ if (selector.name && element.getAttribute("name") !== selector.name) return false;
194
+ if (selector.label && !sameText(labelText(element), selector.label)) return false;
195
+ if (selector.text && !sameText(element.innerText || element.textContent || "", selector.text)) return false;
196
+ if (selector.role && !sameText(element.getAttribute("role") || implicitRole(element), selector.role)) return false;
197
+ if (selector.placeholder && !sameText(element.getAttribute("placeholder") || "", selector.placeholder)) return false;
198
+ return true;
199
+ });
200
+ return elements[Number.isInteger(selector.index) ? selector.index : 0] || null;
201
+ }
202
+
203
+ function cssEscape(value) {
204
+ if (globalThis.CSS?.escape) return globalThis.CSS.escape(String(value));
205
+ return String(value).replace(/["\\]/g, "\\$&");
206
+ }
207
+
208
+ function sameText(left, right) {
209
+ return String(left || "").trim().replace(/\s+/g, " ").toLowerCase() === String(right || "").trim().replace(/\s+/g, " ").toLowerCase();
210
+ }
211
+
212
+ function labelText(element) {
213
+ if (element.labels?.length) return [...element.labels].map((label) => label.innerText || label.textContent || "").join(" ").trim();
214
+ const parent = element.closest("label");
215
+ if (parent) return (parent.innerText || parent.textContent || "").trim();
216
+ const labelledBy = element.getAttribute("aria-labelledby");
217
+ if (labelledBy) return labelledBy.split(/\s+/).map((id) => rootById(element, id)?.textContent || "").join(" ").trim();
218
+ return "";
219
+ }
220
+
221
+ function accessibleName(element) {
222
+ return (element.getAttribute("aria-label") || labelText(element) || element.getAttribute("title") || element.getAttribute("alt") || element.innerText || element.textContent || "")
223
+ .trim().replace(/\s+/g, " ").slice(0, 500);
224
+ }
225
+
226
+ function implicitRole(element) {
227
+ const tag = element.tagName.toLowerCase();
228
+ if (tag === "a" && element.hasAttribute("href")) return "link";
229
+ if (tag === "button") return "button";
230
+ if (tag === "textarea") return "textbox";
231
+ if (tag === "select") return "combobox";
232
+ if (tag === "input") {
233
+ const type = String(element.type || "text").toLowerCase();
234
+ if (["button", "submit", "reset"].includes(type)) return "button";
235
+ if (type === "checkbox") return "checkbox";
236
+ if (type === "radio") return "radio";
237
+ return "textbox";
238
+ }
239
+ return "";
240
+ }
241
+
242
+ Object.defineProperty(globalThis, "__machineBridgePageAutomation", {
243
+ value: Object.freeze({ inspect, action, fillForm, uploadFiles }),
244
+ configurable: true,
245
+ });
246
+ })();
@@ -0,0 +1,19 @@
1
+ (() => {
2
+ const marker = document.querySelector('meta[name="machine-bridge-browser-pair"]');
3
+ const portMeta = document.querySelector('meta[name="machine-bridge-browser-port"]');
4
+ const tokenMeta = document.querySelector('meta[name="machine-bridge-browser-token"]');
5
+ if (!marker || !portMeta || !tokenMeta) return;
6
+ const token = tokenMeta.content;
7
+ const port = Number(portMeta.content);
8
+ if (!/^[A-Za-z0-9_-]{32,100}$/.test(token) || !Number.isInteger(port) || port < 1024 || port > 65535) return;
9
+ chrome.runtime.sendMessage({ type: "pair", endpoint: `ws://127.0.0.1:${port}/extension`, token }, (response) => {
10
+ const status = document.getElementById("status");
11
+ if (chrome.runtime.lastError || response?.ok !== true) {
12
+ if (status) status.textContent = response?.requires_manual_repair
13
+ ? "This extension is already paired to different local state. Click the Machine Bridge extension icon while this page is active to confirm re-pairing."
14
+ : "Pairing failed. Confirm the local Machine Bridge runtime is running, then reload this page.";
15
+ return;
16
+ }
17
+ if (status) status.textContent = "Paired. You may close this tab.";
18
+ });
19
+ })();