machine-bridge-mcp 0.14.0 → 0.16.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +18 -8
  3. package/SECURITY.md +10 -3
  4. package/browser-extension/browser-operations.js +515 -0
  5. package/browser-extension/devtools-input.js +17 -2
  6. package/browser-extension/manifest.json +2 -2
  7. package/browser-extension/page-automation.js +245 -60
  8. package/browser-extension/pairing.js +1 -1
  9. package/browser-extension/service-worker.js +151 -354
  10. package/docs/ARCHITECTURE.md +6 -4
  11. package/docs/AUDIT.md +24 -1
  12. package/docs/LOCAL_AUTOMATION.md +10 -10
  13. package/docs/LOGGING.md +7 -1
  14. package/docs/OPERATIONS.md +13 -5
  15. package/docs/POLICY_REFERENCE.md +88 -0
  16. package/docs/TESTING.md +9 -2
  17. package/package.json +16 -3
  18. package/scripts/coverage-check.mjs +108 -0
  19. package/scripts/generate-policy-reference.mjs +95 -0
  20. package/src/local/agent-context.mjs +1 -103
  21. package/src/local/app-automation.mjs +7 -9
  22. package/src/local/browser-bridge.mjs +69 -143
  23. package/src/local/browser-extension-protocol.mjs +75 -0
  24. package/src/local/browser-pairing-store.mjs +83 -0
  25. package/src/local/call-registry.mjs +113 -0
  26. package/src/local/capability-ranking.mjs +103 -0
  27. package/src/local/cli-local-admin.mjs +308 -0
  28. package/src/local/cli-options.mjs +151 -0
  29. package/src/local/cli-policy.mjs +77 -0
  30. package/src/local/cli.mjs +16 -507
  31. package/src/local/errors.mjs +122 -0
  32. package/src/local/git-service.mjs +88 -0
  33. package/src/local/lifecycle.mjs +64 -0
  34. package/src/local/log.mjs +50 -19
  35. package/src/local/managed-job-plan.mjs +235 -0
  36. package/src/local/managed-jobs.mjs +16 -220
  37. package/src/local/observability.mjs +83 -0
  38. package/src/local/policy.mjs +148 -0
  39. package/src/local/process-execution.mjs +153 -0
  40. package/src/local/process-sessions.mjs +10 -20
  41. package/src/local/process-tracker.mjs +55 -0
  42. package/src/local/runtime.mjs +154 -672
  43. package/src/local/service.mjs +8 -3
  44. package/src/local/stdio.mjs +3 -11
  45. package/src/local/tool-executor.mjs +102 -0
  46. package/src/local/tools.mjs +21 -104
  47. package/src/local/workspace-file-service.mjs +451 -0
  48. package/src/shared/policy-contract.json +54 -0
  49. package/src/shared/tool-catalog.json +11 -11
  50. package/src/worker/index.ts +69 -524
package/docs/AUDIT.md CHANGED
@@ -1,6 +1,29 @@
1
1
  # Engineering and security audit
2
2
 
3
- This document records the cross-cutting audit initiated for version 0.12.0, the 0.12.2 cross-platform CI follow-up, and the 0.13.0 architecture/automatic-routing follow-up. It complements, but does not replace, the continuously enforced contracts in `SECURITY.md`, `docs/ENGINEERING.md`, and the test suite.
3
+ This document records the cross-cutting audit initiated for version 0.12.0, the 0.12.2 cross-platform CI follow-up, and the 0.13.0 architecture/automatic-routing follow-up, the 0.15.0 daily-browser and cross-platform hardening review, and the 0.16.0 runtime-boundary review. It complements, but does not replace, the continuously enforced contracts in `SECURITY.md`, `docs/ENGINEERING.md`, and the test suite.
4
+
5
+
6
+ ## 2026-07-13 runtime-boundary and lifecycle follow-up
7
+
8
+ This review confirmed that earlier safety controls existed but were distributed across duplicated policy checks, transport-specific pending maps, message-regex errors, and oversized composition files. It reproduced a production-visible Worker defect where a completed client JSON-RPC request key could remain indexed and cause unrelated later calls to fail as duplicate in-flight IDs. Pending calls now use one atomic double-index registry whose terminal `take` operation clears the internal ID, request key, and timer before resolution or rejection; success, daemon error, cancellation, timeout, send failure, and socket loss have behavior coverage.
9
+
10
+ Policy revision 4 replaces local/Worker/manager interpretations with one contract. The audit found a concrete compound-ACL error: `start_job` was advertised by direct-exec alone even though it creates persistent files. It now requires write plus direct execution; read-only job/resource inspection no longer inherits mutation requirements. Typed errors, shared middleware, explicit runtime lifecycle, process ownership, structured JSON logs, and local/Worker metrics replace ad hoc message parsing and invisible pending state.
11
+
12
+ The composition roots were reduced by extracting filesystem, process, Git, CLI parsing/admin, ranking, managed-job validation, browser protocol/pairing, Worker HTTP, OAuth, policy, errors, pending calls, and observability. Executable architecture limits prevent these responsibilities from returning. During extraction, a missing local ESM binding was initially hidden by a broad catch and surfaced only as `resource_unavailable`; the catch now rethrows unknown faults, and the stdio generated-resource test exercises the complete state reload path.
13
+
14
+ Coverage review showed that a large test count had hidden weak orchestration coverage. CI now reports and enforces per-module baselines, while pure policy/CLI/ranking/state modules receive direct branch tests and Worker behavior still runs through workerd. Generated policy documentation eliminates a second hand-maintained ACL description.
15
+
16
+ ## 2026-07-13 daily-browser and cross-platform hardening follow-up
17
+
18
+ The review first distinguished installation from actual connectivity. The user identified the target as their ordinary Chrome profile and manually loaded the unpacked extension there; it was initially unpaired. After opening the genuine loopback pairing page, a localhost-only live fixture verified that this connected Chrome instance could create and close a real tab, return semantic snapshot refs, traverse an open Shadow DOM, fill a multi-field form, issue trusted text and pointer input, wait for final DOM state, and capture a screenshot without reading unrelated tabs. The product now states the narrower machine-verifiable fact: Machine Bridge does not launch a browser, but an extension cannot infer whether its host profile is the user's daily or isolated profile.
19
+
20
+ Fault injection then found several state-machine issues that a success-only smoke test could not reveal. WebSocket `open` was treated as extension readiness before protocol validation; pairing state was written before the candidate authenticated; `input_mode: auto` replayed a DOM action after any DevTools failure even when an Input command might already have taken effect; screenshot capture focused the browser window and did not restore the previous active tab; navigation wait used a hidden 30-second deadline independent of the request; and replacement could leave direct/proxied requests pending. Protocol 3 now requires `hello`/`hello_ack` with exact packaged-version/capability equality, persists pairing only after acknowledgement, validates a canonical extension ID plus matching loopback ports, propagates the request deadline, rejects ambiguous post-dispatch retries, restores tab state without stealing focus, and fails interrupted routes with retry guidance.
21
+
22
+ Hostile-page analysis found that previous limits were result limits rather than work limits: every frame received the full source/element allowance, `outerHTML` was built before truncation, interactive elements and Shadow roots were fully enumerated before slicing, page-controlled strings were not uniformly bounded, and whole-page `innerText` was materialized for waits. Version 0.15.0 applies aggregate budgets across at most 64 frames, uses iterative bounded DOM serialization, limits each page scan to 100,000 nodes, reusable refs to a 10,000-entry LRU, and text search to 2 MiB, bounds metadata, removes URL userinfo, treats secret-like contenteditable controls as sensitive, and reports truncation explicitly. Multi-field failures now identify possible earlier mutations without returning values.
23
+
24
+ The extension architecture was also corrected: the service worker now owns only pairing/transport/readiness/cancellation/response routing, while fixed `browser-operations.js` owns tab and page orchestration. An executable line-count invariant prevents those concerns from silently merging again. Outside the browser path, the review found that Windows Scheduled Task command construction did not implement CRT backslash-before-quote rules; drive-root or trailing-backslash arguments could be misparsed. A dedicated quoting primitive and platform regression test correct that boundary.
25
+
26
+ The same pass rechecked Worker OAuth/PKCE, registration/code/token limits, process sessions, managed jobs, state deletion, log redaction/rotation, package contents, dependencies, open issues, and current CI. No additional high-impact code change was justified in those areas. Their documented residuals remain: canonical `full` intentionally exposes the local user's authority, service logs are trimmed at startup rather than continuously, and same-user malicious code or an MCP host can still operate outside Machine Bridge's control.
4
27
 
5
28
  ## 2026-07-13 architecture and automatic-routing follow-up
6
29
 
@@ -15,7 +15,7 @@ The extension works with the current profile and supports:
15
15
 
16
16
  - listing, creating, activating, and closing tabs;
17
17
  - reading the current serialized DOM HTML from the main frame or accessible subframes;
18
- - discovering interactive controls, stable per-document element references, actionability state, geometry, labels, roles, placeholders, and field metadata across accessible frames and open Shadow DOM roots;
18
+ - discovering interactive controls, bounded per-document element references, actionability state, geometry, labels, roles, placeholders, and field metadata across accessible frames and open Shadow DOM roots;
19
19
  - waiting for bounded combinations of URL, load state, page text, and element state;
20
20
  - structured navigation, click, double-click, hover, focus, fill, type-text, select, check, uncheck, key press, scroll-into-view, and form submission;
21
21
  - automatic existence/visibility/enabled/editable/stability/hit-target checks before actions, with fixed DevTools mouse/keyboard input for top-frame interactions and a controlled DOM fallback;
@@ -23,7 +23,7 @@ The extension works with the current profile and supports:
23
23
  - populating file inputs from registered local resource files;
24
24
  - visible-tab screenshots.
25
25
 
26
- Current support targets Chrome, Chromium, Microsoft Edge, Brave, Vivaldi, and compatible Chromium browsers. After upgrading Machine Bridge, reload the unpacked extension from the browser extensions page so its packaged scripts and permissions match the new runtime. Version 0.14.0 adds the `debugger` permission for fixed trusted-input commands; the extension attaches only for the action and detaches afterward. The broker requires a versioned extension hello with the expected capability set, keeps the last compatible connection active until a replacement completes its handshake, and reports `extension_reload_required` instead of silently routing new tools to stale extension code. Browser-internal pages, extension stores, some PDF/plugin viewers, cross-origin frames denied to the extension, and pages restricted by enterprise policy may not be scriptable.
26
+ Current support targets Chrome, Chromium, Microsoft Edge, Brave, Vivaldi, and compatible Chromium browsers. The extension must be loaded into the user profile to be controlled; it is not installed into Playwright or an isolated automation profile. After every Machine Bridge upgrade, reload the unpacked extension so packaged scripts, protocol, and permissions match the runtime. Version 0.15.0 requires protocol 3 with broker `hello_ack`; the badge and pairing page report success only after protocol/version/capability validation. Pairing material is persisted only after that acknowledgement, an invalid candidate cannot overwrite the previous pairing, and `browser status` reports the expected packaged build, authenticated connected build/protocol/capabilities, and `extension_reload_required`; exact version equality is required in addition to protocol and capabilities. Browser-internal pages, extension stores, some PDF/plugin viewers, inaccessible cross-origin frames, and pages restricted by enterprise policy remain unscriptable.
27
27
 
28
28
  ## One-time browser setup
29
29
 
@@ -55,17 +55,17 @@ The broker is machine-global rather than workspace-global. One local owner liste
55
55
 
56
56
  ## Browser tools
57
57
 
58
- - `browser_status` reports broker role, extension connection, supported operations, pairing URL, and extension path without returning the pairing token.
58
+ - `browser_status` reports broker role, authenticated extension protocol/version/capabilities, reload state, supported operations, pairing URL, and extension path without returning the pairing token.
59
59
  - `pair_browser_extension` opens the local pairing page and returns setup steps.
60
60
  - `browser_list_tabs` lists current tabs.
61
61
  - `browser_manage_tabs` creates, activates, or closes a selected tab.
62
- - `browser_get_source` returns bounded current DOM HTML for selected frames.
63
- - `browser_inspect_page` returns a bounded semantic snapshot. Each control includes a stable `ref` for the lifetime of that document/frame plus visibility, enabled/editable state, and viewport geometry.
62
+ - `browser_get_source` returns a bounded iterative DOM serialization. `max_bytes` is one aggregate request budget across at most 64 accessible frames; omitted frames and node/byte truncation are explicit.
63
+ - `browser_inspect_page` returns snapshot version 2 with one aggregate `max_elements` budget, at most 64 accessible frames, a 100,000-node per-frame scan ceiling, bounded page-controlled strings, URL-userinfo redaction, and explicit scan/frame truncation. Each control includes a reusable `ref` plus visibility, enabled/editable state, and viewport geometry. References are held in a 10,000-entry per-frame LRU; navigation, element replacement, or bounded eviction makes an older ref stale and requires a new inspection.
64
64
  - `browser_wait` waits until all supplied URL, load-state, page-text, and element-state conditions are true.
65
- - `browser_action` performs one structured navigation or page action. Navigation accepts absolute `http`, `https`, or `file` URLs; script/data schemes are rejected. Pointer and keyboard actions default to `input_mode: auto`, which attempts fixed trusted DevTools input in the top frame and falls back to DOM behavior. `trusted` forbids fallback; `dom` never attaches the debugger.
66
- - `browser_fill_form` fills up to 200 fields and can submit once.
65
+ - `browser_action` performs one structured navigation or page action. Navigation accepts absolute `http`, `https`, or `file` URLs; script/data schemes are rejected. Pointer and keyboard actions default to `input_mode: auto`, which uses fixed trusted DevTools input in the top frame and falls back to DOM only when no `Input` command has started. Once dispatch begins, failure has an unknown outcome and must be inspected before retrying; automatic replay is forbidden. `trusted` always forbids fallback; `dom` never attaches the debugger.
66
+ - `browser_fill_form` fills up to 200 fields and can submit once. If a later field fails, the error states how many earlier fields may already have changed without returning their values.
67
67
  - `browser_upload_files` sets a file input from up to eight registered local resources. Caller filenames must be safe single-component names; derived names have controls and separators removed. MIME overrides must be canonical media types.
68
- - `browser_screenshot` returns native MCP image content.
68
+ - `browser_screenshot` returns native MCP image content, temporarily activates the selected tab only when required, restores the previous active tab when safe, and never focuses another browser window.
69
69
 
70
70
  Selectors can use the `ref` returned by inspection, CSS, ID, field name, label text, visible text, ARIA/implicit role, placeholder, and a zero-based match index. `ref` cannot be combined with other fields and becomes stale after navigation or element replacement. Non-ref selectors that match multiple controls fail explicitly unless `index` disambiguates them. The fixed page module traverses open Shadow DOM roots; closed shadow roots remain inaccessible. For frame-specific work, inspect all frames first and then pass `frame_id` to an action. Trusted input currently targets the top frame because DevTools coordinates are top-level; use `input_mode: dom` for an explicitly selected subframe.
71
71
 
@@ -105,12 +105,12 @@ The implementation reduces avoidable risk as follows:
105
105
 
106
106
  - all browser/app tools are `full`-only;
107
107
  - no arbitrary evaluation, caller-provided script source, or caller-selected DevTools method is accepted; trusted input exposes only fixed `Input.dispatchMouseEvent`, `Input.dispatchKeyEvent`, and `Input.insertText` sequences;
108
- - loopback HTTP validates `Host`; extension WebSockets require a random bearer subprotocol and a `chrome-extension://` origin;
108
+ - loopback HTTP validates `Host`; extension WebSockets require a random bearer subprotocol and a canonical `chrome-extension://` origin whose host is a 32-character Chromium extension ID; pairing-page and broker ports must match;
109
109
  - runtime-to-broker connections require a separate authenticated subprotocol;
110
110
  - the pairing token is stored owner-only, embedded only in the non-cacheable local pairing page, and omitted from MCP results and logs;
111
111
  - an established extension pairing cannot be silently replaced by another localhost page; replacement requires clicking the extension action on the active pairing page;
112
112
  - arguments, form values, page source, screenshots, and results are not operational log data;
113
- - message, source, form-field, upload, traversal, concurrency, proxy-route, actionability, and timeout limits are enforced; ambiguous selectors, stale refs, hidden/disabled/edit-blocked controls, moving targets, and obscured pointer targets fail explicitly;
113
+ - message, aggregate source/element/frame, form-field, upload, DOM-node/text, concurrency, proxy-route, actionability, and request-deadline limits are enforced; ambiguous selectors, stale refs, hidden/disabled/edit-blocked controls, moving targets, and obscured pointer targets fail explicitly;
114
114
  - MCP cancellation clears local and broker pending state and propagates a cancellation signal to the extension; an action already delivered to a page or application may still have completed;
115
115
  - resource-backed text and files are not returned after injection.
116
116
 
package/docs/LOGGING.md CHANGED
@@ -24,7 +24,7 @@ The CLI accepts:
24
24
  --verbose # alias for debug
25
25
  ```
26
26
 
27
- Foreground mode defaults to `info`. Platform autostart services use `warn`.
27
+ Foreground mode defaults to `info` and human-readable text. Platform autostart services use `warn` with newline-delimited JSON. Foreground operators can select JSON explicitly with `--log-format json`.
28
28
 
29
29
  | Level | Intended events |
30
30
  |---|---|
@@ -101,6 +101,12 @@ The logger exposes two intentional plain-output boundaries. `safePlain` sanitize
101
101
 
102
102
  This is defense in depth, not content classification. Unknown, split, transformed, encrypted, or application-specific secret formats can evade pattern matching, which is why tool arguments and outputs are omitted rather than merely filtered.
103
103
 
104
+ ## Structured lifecycle events
105
+
106
+ The local execution middleware emits bounded events such as `tool.call.started`, `tool.call.completed`, `tool.call.failed`, `tool.call.slow`, and `tool.call.cancel_requested`. Stable fields include a shortened call ID, tool name, origin, duration, error code, and retryability. The Worker emits JSON events for HTTP failures and daemon socket errors. Structured values still pass through field-name and value redaction; JSON format is not permission to log arguments or results.
107
+
108
+ `server_info` is the operational metrics surface. Local metrics include lifecycle state, active and maximum calls, oldest-call age, active-process ownership, per-tool duration buckets, and error-code counts. Worker metrics include HTTP status classes, pending internal/request-key indexes, per-tool outcomes, daemon candidate/authenticated/disconnected counts, and protocol-error counts. Metrics contain counts and bounded identifiers, not request arguments or result contents.
109
+
104
110
  ## Files
105
111
 
106
112
  Autostart logs are stored below the owner-only state root:
@@ -44,14 +44,15 @@ machine-mcp browser setup
44
44
  machine-mcp browser status
45
45
  ```
46
46
 
47
- `browser setup` prints the unpacked-extension path and opens the local pairing page. Load that directory from the Chromium extensions page with Developer mode enabled. The pairing page contains the token only in non-cacheable loopback HTML; the CLI and MCP result do not print it. A first pairing is automatic. When local pairing state was reset or moved, click the extension icon on the active pairing page to authorize replacing the extension's previous broker configuration.
47
+ `browser setup` prints the unpacked-extension path and opens the local pairing page. Load that directory into the Chromium profile you actually use; it is not installed into Playwright or a separate automation profile. `browser status` prints the authenticated extension build/protocol and the existing-profile backend. The pairing page contains the token only in non-cacheable loopback HTML; the CLI and MCP result do not print it. A first pairing is automatic. When local pairing state was reset or moved, click the extension icon on the active pairing page to authorize replacing the extension's previous broker configuration.
48
48
 
49
49
  Diagnosis:
50
50
 
51
51
  | Result | Interpretation |
52
52
  |---|---|
53
53
  | broker not reachable | no full-profile runtime owns the machine broker, state is stale, or a local security product blocked loopback listening |
54
- | broker running, extension disconnected | extension not loaded/enabled, pairing not completed, browser profile not running, or MV3 worker reconnect failed |
54
+ | broker running, extension disconnected | extension not loaded/enabled in the intended Chromium profile, pairing not completed, browser profile not running, or MV3 worker reconnect failed |
55
+ | extension reload required | unpacked files, permissions, protocol, or capability set differ from the running extension; reload it from the extensions page and revisit the pairing page |
55
56
  | tab list works but page action fails | restricted browser-internal page, inaccessible frame, stale selector, page navigation, enterprise policy, or page script interference |
56
57
  | `frame_id` action fails | frame was replaced or is not accessible; inspect frames again |
57
58
  | resource-backed upload fails | resource unavailable/too large, input is not `type=file`, page replaced the input, or the site rejects the file |
@@ -100,7 +101,14 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
100
101
 
101
102
  ## Logs
102
103
 
103
- Remote autostart definitions prefer a stable PATH alias that resolves to the currently running Node executable and persist a sanitized absolute-only service `PATH` containing the Node/CLI directories, the installer's absolute PATH entries, and platform defaults. This avoids versioned Homebrew-style paths becoming invalid after upgrades and prevents launchd/systemd from falling back to a minimal system-only PATH. Re-run `machine-mcp service install` after changing Node installation families or PATH layout. A service-style `--daemon-only` start that finds the same workspace daemon already running is an idempotent no-op: it exits successfully without repeating warnings or readiness output; explicit policy/secret/change requests still report that changes were not applied. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Files are owner-only where supported and tail-trimmed before daemon startup. On a logging-schema upgrade, bounded prior content is moved into `daemon.out.legacy.log` and `daemon.err.legacy.log`; use the active filenames when diagnosing current behavior.
104
+ ### Lifecycle and pending-call diagnosis
105
+
106
+ `server_info.runtime.lifecycle` reports `ready`, `starting`, `running`, `failed`, `stopping`, or `stopped`. `runtime.observability.in_flight_calls` and `runtime.observability.processes` distinguish a blocked call from a surviving process. Worker `server_info.worker.pending_calls` reports both the internal-call index and client request-key index; they must return to zero after a terminal result, cancellation, timeout, send failure, or daemon disconnect. Nonzero request-key counts after active calls reach zero indicate a lifecycle defect rather than normal load.
107
+
108
+ Stable errors include `policy_denied`, `invalid_request`, `timeout`, `cancelled`, `network_error`, `unavailable`, `limit_exceeded`, and `integrity_error`, with retryability metadata. Diagnose by code first; free-form messages are guidance, not an API contract.
109
+
110
+
111
+ Windows Scheduled Task command lines use CRT-compatible quoting, including trailing backslashes and drive-root paths. Remote autostart definitions prefer a stable PATH alias that resolves to the currently running Node executable and persist a sanitized absolute-only service `PATH` containing the Node/CLI directories, the installer's absolute PATH entries, and platform defaults. This avoids versioned Homebrew-style paths becoming invalid after upgrades and prevents launchd/systemd from falling back to a minimal system-only PATH. Re-run `machine-mcp service install` after changing Node installation families or PATH layout. A service-style `--daemon-only` start that finds the same workspace daemon already running is an idempotent no-op: it exits successfully without repeating warnings or readiness output; explicit policy/secret/change requests still report that changes were not applied. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Installed services pass `--log-level warn --log-format json`, so each active line is a bounded JSON event suitable for ingestion. Files are owner-only where supported and tail-trimmed before daemon startup. On a logging-schema upgrade, bounded prior content is moved into `daemon.out.legacy.log` and `daemon.err.legacy.log`; use the active filenames when diagnosing current behavior.
104
112
 
105
113
  Logging is level-based:
106
114
 
@@ -199,14 +207,14 @@ Defense-in-depth limits include:
199
207
  - managed-job timeout: 1–3,600 seconds per step;
200
208
  - managed-job output: 64 KiB per stream and 256 KiB total captured across one job;
201
209
  - registered resources: 64, 1 MiB each, 8 MiB referenced per job;
202
- - browser source: 4 MiB; browser WebSocket messages: 8 MiB; pending browser requests: 32 per runtime;
210
+ - browser source: 4 MiB aggregate across at most 64 accessible frames; semantic inspection: 1,000 elements aggregate, 100,000 scanned nodes, 10,000 reusable refs, and 2 MiB bounded page-text search per frame; browser WebSocket messages: 8 MiB; pending browser requests: 32 per runtime;
203
211
  - browser forms: 200 fields, 128 KiB per text value, and 4 MiB aggregate text; upload: eight resources and 5 MiB total;
204
212
  - application Accessibility inspection: 500 elements and depth 12; action text: 4,000 characters;
205
213
  - job-scoped temporary files: 16 files, 512 KiB total content.
206
214
 
207
215
  ## Upgrade behavior
208
216
 
209
- Policy revision 3 makes named profiles canonical. A state entry labelled `full` is repaired to writes, direct processes, process sessions, shell execution, unrestricted direct filesystem paths, absolute path output, the complete parent environment, and the complete tool catalog. CLI capability overrides are stored as `custom`. The exact pre-0.4 implicit-default shape is still migrated to the current `full` default; explicit restrictive and identified custom profiles remain preserved.
217
+ Policy revision 4 makes named profiles canonical and evaluates compound tool requirements from the shared contract. A state entry labelled `full` is repaired to writes, direct processes, process sessions, shell execution, unrestricted direct filesystem paths, absolute path output, the complete parent environment, and the complete tool catalog. CLI capability overrides are stored as `custom`. The exact pre-0.4 implicit-default shape is still migrated to the current `full` default; explicit restrictive and identified custom profiles remain preserved.
210
218
 
211
219
  `full` removes Machine Bridge's own profile/path/environment/shell denials and makes the complete catalog available to the relay. It does not force a connector host to expose every relayed tool, and the server cannot see the host's final subset. It also does not override operating-system access controls, endpoint security, remote authentication, cloud IAM, `sudo`, or independent MCP-host/platform policy.
212
220
 
@@ -0,0 +1,88 @@
1
+ # Policy reference
2
+
3
+ > Generated from `src/shared/policy-contract.json` and `src/shared/tool-catalog.json`. Do not edit this file manually.
4
+
5
+ Policy revision: **4**. Default profile: **full**.
6
+
7
+ ## Canonical profiles
8
+
9
+ | Capability | review | edit | agent | full |
10
+ |---|---|---|---|---|
11
+ | Write | no | yes | yes | yes |
12
+ | Execution mode | off | off | direct | shell |
13
+ | Unrestricted paths | no | no | no | yes |
14
+ | Minimal environment | yes | yes | yes | no |
15
+ | Absolute paths | no | no | no | yes |
16
+
17
+ ## Availability classes
18
+
19
+ | Class | Requirements |
20
+ |---|---|
21
+ | `always` | none |
22
+ | `write` | write |
23
+ | `direct-exec` | exec in direct/shell |
24
+ | `write+direct-exec` | write, exec in direct/shell |
25
+ | `shell-exec` | exec in shell |
26
+ | `full` | profile = full, write, exec in shell, unrestricted paths = true, minimal env = false, absolute paths = true |
27
+
28
+ ## Tool access
29
+
30
+ | Tool | Availability | review | edit | agent | full |
31
+ |---|---|---|---|---|---|
32
+ | `agent_context` | `always` | yes | yes | yes | yes |
33
+ | `apply_patch` | `write` | no | yes | yes | yes |
34
+ | `browser_action` | `full` | no | no | no | yes |
35
+ | `browser_fill_form` | `full` | no | no | no | yes |
36
+ | `browser_get_source` | `full` | no | no | no | yes |
37
+ | `browser_inspect_page` | `full` | no | no | no | yes |
38
+ | `browser_list_tabs` | `full` | no | no | no | yes |
39
+ | `browser_manage_tabs` | `full` | no | no | no | yes |
40
+ | `browser_screenshot` | `full` | no | no | no | yes |
41
+ | `browser_status` | `full` | no | no | no | yes |
42
+ | `browser_upload_files` | `full` | no | no | no | yes |
43
+ | `browser_wait` | `full` | no | no | no | yes |
44
+ | `cancel_job` | `write` | no | yes | yes | yes |
45
+ | `diagnose_runtime` | `always` | yes | yes | yes | yes |
46
+ | `edit_file` | `write` | no | yes | yes | yes |
47
+ | `exec_command` | `shell-exec` | no | no | no | yes |
48
+ | `generate_ssh_key_resource` | `full` | no | no | no | yes |
49
+ | `git_diff` | `always` | yes | yes | yes | yes |
50
+ | `git_log` | `always` | yes | yes | yes | yes |
51
+ | `git_show` | `always` | yes | yes | yes | yes |
52
+ | `git_status` | `always` | yes | yes | yes | yes |
53
+ | `inspect_local_application` | `full` | no | no | no | yes |
54
+ | `kill_process` | `direct-exec` | no | no | yes | yes |
55
+ | `list_dir` | `always` | yes | yes | yes | yes |
56
+ | `list_files` | `always` | yes | yes | yes | yes |
57
+ | `list_jobs` | `always` | yes | yes | yes | yes |
58
+ | `list_local_applications` | `full` | no | no | no | yes |
59
+ | `list_local_commands` | `always` | yes | yes | yes | yes |
60
+ | `list_local_resources` | `always` | yes | yes | yes | yes |
61
+ | `list_local_skills` | `always` | yes | yes | yes | yes |
62
+ | `list_roots` | `always` | yes | yes | yes | yes |
63
+ | `load_local_skill` | `always` | yes | yes | yes | yes |
64
+ | `open_local_application` | `full` | no | no | no | yes |
65
+ | `operate_local_application` | `full` | no | no | no | yes |
66
+ | `pair_browser_extension` | `full` | no | no | no | yes |
67
+ | `project_overview` | `always` | yes | yes | yes | yes |
68
+ | `read_file` | `always` | yes | yes | yes | yes |
69
+ | `read_job` | `always` | yes | yes | yes | yes |
70
+ | `read_process` | `direct-exec` | no | no | yes | yes |
71
+ | `resolve_task_capabilities` | `always` | yes | yes | yes | yes |
72
+ | `run_local_command` | `direct-exec` | no | no | yes | yes |
73
+ | `run_process` | `direct-exec` | no | no | yes | yes |
74
+ | `search_text` | `always` | yes | yes | yes | yes |
75
+ | `server_info` | `always` | yes | yes | yes | yes |
76
+ | `session_bootstrap` | `always` | yes | yes | yes | yes |
77
+ | `stage_job` | `write` | no | yes | yes | yes |
78
+ | `start_job` | `write+direct-exec` | no | no | yes | yes |
79
+ | `start_process` | `direct-exec` | no | no | yes | yes |
80
+ | `view_image` | `always` | yes | yes | yes | yes |
81
+ | `write_file` | `write` | no | yes | yes | yes |
82
+ | `write_process` | `direct-exec` | no | no | yes | yes |
83
+
84
+ ## Enforcement contract
85
+
86
+ The local runtime and Cloudflare Worker both load the same policy contract. Tool advertisement and execution authorization use the catalog availability class; manager-level checks call the same gate rather than reimplementing profile logic.
87
+
88
+ A custom policy is evaluated by capabilities, not by its display name. Compound classes such as `write+direct-exec` require every listed capability.
package/docs/TESTING.md CHANGED
@@ -52,7 +52,7 @@ The suite includes:
52
52
 
53
53
  ## Opt-in live desktop and browser validation
54
54
 
55
- The normal suite uses deterministic mocks for macOS Accessibility and an authenticated in-process extension peer for browser routing. Browser contract tests cover versioned extension handshakes, stale-extension rejection, compatible replacement without premature displacement, clean rejection of in-flight direct and proxied requests during replacement, keepalive handling, strict action/value validation, stable-ref validation, combined waits, tab commands, fixed trusted-input CDP sequences, attach/detach cleanup, semantic-ref stability, deterministic scrolling, obscured-target waiting, and stale-reference failure. Before a release that changes local UI automation, run the macOS live smoke test on a machine where the invoking Node/terminal process has Accessibility permission:
55
+ The normal suite uses deterministic mocks for macOS Accessibility and an authenticated in-process extension peer for browser routing. Browser contract tests cover acknowledged protocol readiness, provisional pairing commit/rollback boundaries, strict extension IDs and matching loopback ports, stale-extension rejection, compatible replacement without premature displacement, clean rejection of in-flight direct and proxied requests, keepalive handling, trusted-input replay prevention after partial dispatch, focus-safe screenshots, aggregate 64-frame/source/element budgets, bounded hostile DOM/text processing, contenteditable-secret suppression, partial-form failure reporting, stable refs, combined waits, and actionability failures. Before a release that changes local UI automation, run the macOS live smoke test on a machine where the invoking Node/terminal process has Accessibility permission:
56
56
 
57
57
  ```sh
58
58
  npm run app-automation:live-test
@@ -60,7 +60,14 @@ npm run app-automation:live-test
60
60
 
61
61
  It opens Calculator, activates it through the fixed JXA helper, verifies structured JSON output, inspects main-window Accessibility controls with menu recursion disabled, clicks the `One` control, and quits the application. It is intentionally excluded from CI because macOS TCC permission and a graphical login session are host state.
62
62
 
63
- For browser changes, perform an isolated-profile smoke test with the packaged unpacked extension: pair it to a temporary source runtime, navigate a local no-store page, inspect controls and reuse a returned `ref`, wait for an element state, fill multiple fields, exercise `input_mode: trusted` for click/key/text, verify the live DOM, create/activate/close a tab, and capture a screenshot. A Playwright persistent Chromium context is an acceptable harness because it loads the real MV3 service worker while keeping the user's daily browser profile untouched.
63
+ For deterministic release validation, perform an isolated-profile smoke test with the packaged unpacked extension; a Playwright persistent Chromium context is acceptable only as that isolated harness. When the requirement is specifically to prove control of the user's ordinary browser, the user must load the same unpacked directory into that known daily Chromium profile; status can verify the extension version/protocol and that Machine Bridge did not launch a browser, but cannot infer profile identity. Then use a localhost no-store fixture in a newly created tab. Do not enumerate, read, or mutate unrelated existing tabs. In both modes, inspect and reuse refs, exercise waits/forms/trusted input/open Shadow DOM/screenshots, verify final live DOM, and close the fixture tab.
64
+
65
+
66
+ ## Critical-module coverage gate
67
+
68
+ `npm run coverage:test` runs selected in-process and lightweight entrypoint fixtures under V8 coverage and enforces per-module baselines for policy, typed errors, call registration, execution middleware, lifecycle/observability, logging, Runtime/CLI orchestration, and Worker pending/policy/error modules. The full stdio and Worker OAuth/MCP integrations run separately in the main suite and are not duplicated inside coverage collection. The gate deliberately reports each module rather than one aggregate percentage. JavaScript modules enforce function and branch minima; Node's direct TypeScript stripping does not expose trustworthy branch ranges, so TypeScript modules enforce function coverage plus explicit state-machine tests and the real workerd integration suite.
69
+
70
+ The current CLI entrypoint remains the weakest covered orchestration surface; its baseline is therefore reported and locked independently, while extracted `cli-options` and `cli-policy` pure modules carry substantially higher thresholds. A refactor may raise a threshold, but must not lower one merely to make CI green without an explicit audit explanation.
64
71
 
65
72
  ## Additional release checks
66
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "worker:types": "wrangler types src/worker/worker-configuration.d.ts",
44
44
  "typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
45
45
  "syntax": "node scripts/syntax-check.mjs",
46
- "check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run catalog:test && npm run agent-context:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
46
+ "check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run policy:test && npm run policy-docs:check && npm run runtime-infrastructure:test && npm run runtime-handlers:test && npm run cli-entrypoint:test && npm run lifecycle:test && npm run logging-structure:test && npm run coverage:test && npm run worker-runtime-infrastructure:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run service-platform:test && npm run catalog:test && npm run agent-context:test && npm run capability-ranking:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-service-worker:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
47
47
  "worker:dry-run": "wrangler deploy --dry-run",
48
48
  "worker:integration-test": "node tests/worker-integration-test.mjs",
49
49
  "release:check": "node scripts/github-release.mjs --check",
@@ -76,7 +76,20 @@
76
76
  "app-automation:live-test": "node tests/app-automation-test.mjs --live-macos",
77
77
  "browser-command:test": "node tests/browser-command-test.mjs",
78
78
  "browser-devtools-input:test": "node tests/browser-devtools-input-test.mjs",
79
- "browser-page-automation:test": "node tests/browser-page-automation-test.mjs"
79
+ "browser-page-automation:test": "node tests/browser-page-automation-test.mjs",
80
+ "browser-service-worker:test": "node tests/browser-service-worker-test.mjs",
81
+ "service-platform:test": "node tests/service-platform-test.mjs",
82
+ "policy:test": "node tests/policy-test.mjs",
83
+ "runtime-infrastructure:test": "node tests/runtime-infrastructure-test.mjs",
84
+ "worker-runtime-infrastructure:test": "node tests/worker-runtime-infrastructure-test.mjs",
85
+ "logging-structure:test": "node tests/logging-structure-test.mjs",
86
+ "policy-docs:generate": "node scripts/generate-policy-reference.mjs",
87
+ "policy-docs:check": "node scripts/generate-policy-reference.mjs --check",
88
+ "coverage:test": "node scripts/coverage-check.mjs",
89
+ "capability-ranking:test": "node tests/capability-ranking-test.mjs",
90
+ "lifecycle:test": "node tests/lifecycle-test.mjs",
91
+ "runtime-handlers:test": "node tests/runtime-handler-matrix-test.mjs",
92
+ "cli-entrypoint:test": "node tests/cli-entrypoint-test.mjs"
80
93
  },
81
94
  "dependencies": {
82
95
  "https-proxy-agent": "9.1.0",
@@ -0,0 +1,108 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const coverageDir = mkdtempSync(resolve(tmpdir(), "machine-bridge-coverage-"));
9
+ const tests = [
10
+ "tests/policy-test.mjs",
11
+ "tests/runtime-infrastructure-test.mjs",
12
+ "tests/worker-runtime-infrastructure-test.mjs",
13
+ "tests/logging-structure-test.mjs",
14
+ "tests/runtime-handler-matrix-test.mjs",
15
+ "tests/cli-entrypoint-test.mjs",
16
+ "tests/local-self-test.mjs",
17
+ ];
18
+
19
+ try {
20
+ for (const test of tests) {
21
+ const run = spawnSync(process.execPath, [test], {
22
+ cwd: root,
23
+ env: { ...process.env, NODE_V8_COVERAGE: coverageDir },
24
+ encoding: "utf8",
25
+ maxBuffer: 16 * 1024 * 1024,
26
+ });
27
+ if (run.status !== 0) {
28
+ process.stdout.write(run.stdout || "");
29
+ process.stderr.write(run.stderr || "");
30
+ throw new Error(`coverage fixture failed: ${test}`);
31
+ }
32
+ }
33
+
34
+ const coverage = collectCoverage(coverageDir);
35
+ const thresholds = {
36
+ "src/local/policy.mjs": [75, 45],
37
+ "src/local/errors.mjs": [70, 50],
38
+ "src/local/call-registry.mjs": [80, 50],
39
+ "src/local/tool-executor.mjs": [90, 25],
40
+ "src/local/observability.mjs": [65, 35],
41
+ "src/local/process-tracker.mjs": [65, 35],
42
+ "src/local/log.mjs": [60, 40],
43
+ "src/local/runtime.mjs": [60, 35],
44
+ "src/local/cli.mjs": [40, 5],
45
+ "src/local/cli-options.mjs": [65, 35],
46
+ "src/local/cli-policy.mjs": [70, 35],
47
+ "src/worker/pending-calls.ts": [85, null],
48
+ "src/worker/policy.ts": [90, null],
49
+ "src/worker/errors.ts": [100, null],
50
+ };
51
+ const failures = [];
52
+ for (const [file, [minimumFunctions, minimumBlocks]] of Object.entries(thresholds)) {
53
+ const metric = coverage.get(file);
54
+ if (!metric) {
55
+ failures.push(`${file}: no coverage data`);
56
+ continue;
57
+ }
58
+ const functionPercent = percent(metric.functionsCovered, metric.functionsTotal);
59
+ const blockPercent = percent(metric.blocksCovered, metric.blocksTotal);
60
+ console.log(`${file}: functions ${functionPercent.toFixed(1)}% (${metric.functionsCovered}/${metric.functionsTotal}), blocks ${blockPercent.toFixed(1)}% (${metric.blocksCovered}/${metric.blocksTotal})`);
61
+ if (functionPercent < minimumFunctions) failures.push(`${file}: function coverage ${functionPercent.toFixed(1)}% < ${minimumFunctions}%`);
62
+ if (minimumBlocks !== null && blockPercent < minimumBlocks) failures.push(`${file}: block coverage ${blockPercent.toFixed(1)}% < ${minimumBlocks}%`);
63
+ }
64
+ if (failures.length) throw new Error(`coverage thresholds failed:\n- ${failures.join("\n- ")}`);
65
+ console.log("critical-module coverage thresholds passed");
66
+ } finally {
67
+ rmSync(coverageDir, { recursive: true, force: true });
68
+ }
69
+
70
+ function collectCoverage(directory) {
71
+ const scripts = new Map();
72
+ for (const filename of readdirSync(directory)) {
73
+ const report = JSON.parse(readFileSync(resolve(directory, filename), "utf8"));
74
+ for (const script of report.result || []) {
75
+ if (!String(script.url).startsWith("file://")) continue;
76
+ const absolute = fileURLToPath(script.url);
77
+ const repositoryRelative = relative(root, absolute);
78
+ if (!repositoryRelative || repositoryRelative === ".." || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) continue;
79
+ const file = repositoryRelative.split(sep).join("/");
80
+ if (!file.startsWith("src/")) continue;
81
+ let entry = scripts.get(file);
82
+ if (!entry) {
83
+ entry = { functions: new Map(), blocks: new Map() };
84
+ scripts.set(file, entry);
85
+ }
86
+ for (const fn of script.functions || []) {
87
+ if (!fn.ranges?.length) continue;
88
+ const outer = fn.ranges[0];
89
+ const functionKey = `${outer.startOffset}:${outer.endOffset}:${fn.functionName}`;
90
+ entry.functions.set(functionKey, (entry.functions.get(functionKey) || false) || outer.count > 0);
91
+ for (const range of fn.ranges.slice(1)) {
92
+ const blockKey = `${range.startOffset}:${range.endOffset}`;
93
+ entry.blocks.set(blockKey, (entry.blocks.get(blockKey) || false) || range.count > 0);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return new Map([...scripts].map(([file, entry]) => [file, {
99
+ functionsTotal: entry.functions.size,
100
+ functionsCovered: [...entry.functions.values()].filter(Boolean).length,
101
+ blocksTotal: entry.blocks.size,
102
+ blocksCovered: [...entry.blocks.values()].filter(Boolean).length,
103
+ }]));
104
+ }
105
+
106
+ function percent(covered, total) {
107
+ return total ? covered * 100 / total : 100;
108
+ }
@@ -0,0 +1,95 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import process from "node:process";
3
+
4
+ const root = new URL("../", import.meta.url);
5
+ const contract = JSON.parse(await readFile(new URL("src/shared/policy-contract.json", root), "utf8"));
6
+ const catalog = JSON.parse(await readFile(new URL("src/shared/tool-catalog.json", root), "utf8"));
7
+ const target = new URL("docs/POLICY_REFERENCE.md", root);
8
+ const generated = render(contract, catalog);
9
+
10
+ if (process.argv.includes("--check")) {
11
+ const current = await readFile(target, "utf8").catch(() => "");
12
+ if (normalizeLineEndings(current) !== generated) {
13
+ console.error("docs/POLICY_REFERENCE.md is stale; run npm run policy-docs:generate");
14
+ process.exitCode = 1;
15
+ } else {
16
+ console.log("policy reference is up to date");
17
+ }
18
+ } else {
19
+ await writeFile(target, generated, "utf8");
20
+ console.log("generated docs/POLICY_REFERENCE.md");
21
+ }
22
+
23
+ function render(value, tools) {
24
+ const profileNames = Object.keys(value.profiles);
25
+ const lines = [
26
+ "# Policy reference",
27
+ "",
28
+ "> Generated from `src/shared/policy-contract.json` and `src/shared/tool-catalog.json`. Do not edit this file manually.",
29
+ "",
30
+ `Policy revision: **${value.revision}**. Default profile: **${value.defaultProfile}**.`,
31
+ "",
32
+ "## Canonical profiles",
33
+ "",
34
+ `| Capability | ${profileNames.join(" | ")} |`,
35
+ `|---|${profileNames.map(() => "---").join("|")}|`,
36
+ ];
37
+ for (const [label, key] of [
38
+ ["Write", "allowWrite"],
39
+ ["Execution mode", "execMode"],
40
+ ["Unrestricted paths", "unrestrictedPaths"],
41
+ ["Minimal environment", "minimalEnv"],
42
+ ["Absolute paths", "exposeAbsolutePaths"],
43
+ ]) {
44
+ lines.push(`| ${label} | ${profileNames.map((name) => display(value.profiles[name][key])).join(" | ")} |`);
45
+ }
46
+ lines.push("", "## Availability classes", "", "| Class | Requirements |", "|---|---|");
47
+ for (const [name, requirement] of Object.entries(value.availability)) {
48
+ lines.push(`| \`${name}\` | ${requirementText(requirement)} |`);
49
+ }
50
+ lines.push("", "## Tool access", "", `| Tool | Availability | ${profileNames.join(" | ")} |`, `|---|---|${profileNames.map(() => "---").join("|")}|`);
51
+ for (const tool of [...tools].sort((left, right) => left.name.localeCompare(right.name))) {
52
+ const cells = profileNames.map((name) => allows(value.profiles[name], value.availability[tool.availability]) ? "yes" : "no");
53
+ lines.push(`| \`${tool.name}\` | \`${tool.availability}\` | ${cells.join(" | ")} |`);
54
+ }
55
+ lines.push(
56
+ "",
57
+ "## Enforcement contract",
58
+ "",
59
+ "The local runtime and Cloudflare Worker both load the same policy contract. Tool advertisement and execution authorization use the catalog availability class; manager-level checks call the same gate rather than reimplementing profile logic.",
60
+ "",
61
+ "A custom policy is evaluated by capabilities, not by its display name. Compound classes such as `write+direct-exec` require every listed capability.",
62
+ "",
63
+ );
64
+ return `${lines.join("\n").trimEnd()}\n`;
65
+ }
66
+
67
+ function allows(policy, requirement = {}) {
68
+ if (requirement.profile && policy.profile !== requirement.profile) return false;
69
+ if (requirement.allowWrite === true && policy.allowWrite !== true) return false;
70
+ if (Array.isArray(requirement.execModes) && !requirement.execModes.includes(policy.execMode)) return false;
71
+ if (typeof requirement.unrestrictedPaths === "boolean" && policy.unrestrictedPaths !== requirement.unrestrictedPaths) return false;
72
+ if (typeof requirement.minimalEnv === "boolean" && policy.minimalEnv !== requirement.minimalEnv) return false;
73
+ if (typeof requirement.exposeAbsolutePaths === "boolean" && policy.exposeAbsolutePaths !== requirement.exposeAbsolutePaths) return false;
74
+ return true;
75
+ }
76
+
77
+ function requirementText(requirement) {
78
+ const parts = [];
79
+ if (requirement.profile) parts.push(`profile = ${requirement.profile}`);
80
+ if (requirement.allowWrite === true) parts.push("write");
81
+ if (requirement.execModes) parts.push(`exec in ${requirement.execModes.join("/")}`);
82
+ if (typeof requirement.unrestrictedPaths === "boolean") parts.push(`unrestricted paths = ${requirement.unrestrictedPaths}`);
83
+ if (typeof requirement.minimalEnv === "boolean") parts.push(`minimal env = ${requirement.minimalEnv}`);
84
+ if (typeof requirement.exposeAbsolutePaths === "boolean") parts.push(`absolute paths = ${requirement.exposeAbsolutePaths}`);
85
+ return parts.length ? parts.join(", ") : "none";
86
+ }
87
+
88
+ function display(value) {
89
+ if (typeof value === "boolean") return value ? "yes" : "no";
90
+ return String(value);
91
+ }
92
+
93
+ function normalizeLineEndings(value) {
94
+ return String(value).replace(/\r\n/g, "\n");
95
+ }