machine-bridge-mcp 0.8.2 → 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,40 @@
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
+
24
+ ## 0.9.0 - 2026-07-12
25
+
26
+ ### Agent context and local workflows
27
+
28
+ - Add `agent_context`, a bounded bootstrap tool with Codex-compatible instruction precedence: unrestricted `CODEX_HOME`/`~/.codex` guidance, then project root-to-target scopes, selecting the first non-empty `AGENTS.override.md` or `AGENTS.md` candidate per directory under a default 32 KiB combined budget. Add hierarchical `.machine-bridge/agent.json` manifests plus an optional unrestricted user manifest at `~/.config/machine-bridge-mcp/agent.json` for custom candidate priority and bounds.
29
+ - Add `list_local_skills` and `load_local_skill` with Codex-style progressive disclosure. Default discovery scans target-to-root `.agents/skills`, unrestricted user/admin roots, and canonicalized symlinked skill directories; invalid metadata is skipped with bounded warnings. Skill loading returns instructions and a relative file inventory without implicitly executing scripts.
30
+ - Add `list_local_commands` and direct-argv `run_local_command`. Nearest manifests can override or remove inherited commands, caller arguments require manifest opt-in, and callers cannot increase the manifest timeout ceiling.
31
+
32
+ ### Architecture, security, and tests
33
+
34
+ - Extract agent discovery into `AgentContextManager` rather than expanding transport or runtime dispatch responsibilities. Keep a static MCP catalog across Worker and stdio transports so host-side caching and filtering do not require dynamic per-skill tools.
35
+ - Reject escaping instruction/config paths, unknown manifest fields, out-of-policy skill symlink targets, symbolic-link skill entrypoints, ambiguous skill names, oversized content/argv, and execution attempts under non-execution profiles. Document that repository instructions and skills are untrusted content and that registered commands are convenience aliases, not a sandbox or approval boundary.
36
+ - Add regression coverage for global/project override selection, empty-candidate fallback, custom priority, instruction-byte ceilings, target-to-root skill discovery, symlinked skill folders, invalid metadata warnings, command override/removal, literal argument handling without shell parsing, timeout ceilings, path escape denial, and execution-profile denial. Update server instructions, architecture, security, testing, and operator documentation for the new bootstrap workflow.
37
+
3
38
  ## 0.8.2 - 2026-07-11
4
39
 
5
40
  ### Relay reliability and protocol correctness
package/README.md CHANGED
@@ -108,6 +108,38 @@ 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
+ ## Session instructions, local skills, commands, apps, and browser
112
+
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`:
114
+
115
+ ```json
116
+ {
117
+ "version": 1,
118
+ "model_instructions_file": "~/.config/machine-bridge-mcp/MODEL.md"
119
+ }
120
+ ```
121
+
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.
142
+
111
143
  ## Policy controls
112
144
 
113
145
  The default is `full`. Narrow or customize it with explicit flags:
@@ -233,6 +265,12 @@ The exact `tools/list` response reflects the active local policy. Definitions co
233
265
 
234
266
  - `server_info`
235
267
  - `project_overview`
268
+ - `session_bootstrap` — session/global instructions and capability refresh metadata
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
271
+ - `list_local_skills`
272
+ - `load_local_skill` — load instructions and file inventory without implicit execution
273
+ - `list_local_commands`
236
274
  - `list_roots`
237
275
  - `list_dir`
238
276
  - `list_files`
@@ -240,6 +278,23 @@ The exact `tools/list` response reflects the active local policy. Definitions co
240
278
  - `view_image` — bounded PNG, JPEG, GIF, or WebP as native MCP image content
241
279
  - `search_text`
242
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
+
243
298
  ### Mutation
244
299
 
245
300
  - `write_file` — atomic whole-file write with create-only and SHA-256 checks
@@ -270,6 +325,7 @@ Managed jobs are non-interactive and persist independently of the MCP connection
270
325
 
271
326
  ### Processes
272
327
 
328
+ - `run_local_command` — direct argv execution of a manifest-registered command
273
329
  - `run_process` — one-shot argv execution without a shell
274
330
  - `start_process`
275
331
  - `read_process`
@@ -299,6 +355,7 @@ machine-mcp status
299
355
  machine-mcp doctor
300
356
  machine-mcp rotate-secrets
301
357
  machine-mcp resource add|list|check|remove
358
+ machine-mcp browser status|setup|pair|path
302
359
  machine-mcp job submit|inspect|approve|list|read|cancel
303
360
  machine-mcp --print-mcp-credentials
304
361
  machine-mcp uninstall [--keep-worker] [--yes]
@@ -350,7 +407,7 @@ npm pack --dry-run
350
407
 
351
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.
352
409
 
353
- See [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).
354
411
 
355
412
  ## Uninstall
356
413
 
package/SECURITY.md CHANGED
@@ -51,6 +51,28 @@ The default for newly selected workspaces is `full`, which prioritizes ease of u
51
51
 
52
52
  For untrusted repositories or instructions, run the bridge inside a disposable VM/container or under a dedicated low-privilege OS account. On macOS and Windows, this external isolation is especially important. The project does not claim an in-process OS sandbox.
53
53
 
54
+ ## Agent instructions, skills, and command manifests
55
+
56
+ Repository and user instruction files are untrusted content from the model's perspective. `agent_context` returns their text in deterministic precedence order but does not certify correctness or safety. A malicious `AGENTS.md`, custom instruction file, or `SKILL.md` can attempt prompt injection or recommend destructive operations. Use only trusted repositories and skill roots, or isolate the bridge externally.
57
+
58
+ Skill loading is non-executing. `load_local_skill` returns an entrypoint and bounded file inventory; scripts remain inert until a separate process or command tool is called. Symlinked skill directories are followed only after canonical path-policy validation, while symbolic-link `SKILL.md` entrypoints are rejected. Traversal, cycles, content, summaries, and inventory are bounded.
59
+
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
+
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.
75
+
54
76
  ## Filesystem exposure
55
77
 
56
78
  Direct filesystem scope is profile-dependent. The default `full` profile is unrestricted. The `agent`, `edit`, and `review` profiles confine direct filesystem tools to the canonical selected workspace, including symbolic-link resolution.
@@ -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
+ })();