machine-bridge-mcp 0.15.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 (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +12 -2
  3. package/SECURITY.md +7 -0
  4. package/browser-extension/manifest.json +2 -2
  5. package/docs/ARCHITECTURE.md +5 -3
  6. package/docs/AUDIT.md +12 -1
  7. package/docs/LOGGING.md +7 -1
  8. package/docs/OPERATIONS.md +9 -2
  9. package/docs/POLICY_REFERENCE.md +88 -0
  10. package/docs/TESTING.md +7 -0
  11. package/package.json +14 -3
  12. package/scripts/coverage-check.mjs +108 -0
  13. package/scripts/generate-policy-reference.mjs +95 -0
  14. package/src/local/agent-context.mjs +1 -103
  15. package/src/local/app-automation.mjs +7 -9
  16. package/src/local/browser-bridge.mjs +26 -156
  17. package/src/local/browser-extension-protocol.mjs +75 -0
  18. package/src/local/browser-pairing-store.mjs +83 -0
  19. package/src/local/call-registry.mjs +113 -0
  20. package/src/local/capability-ranking.mjs +103 -0
  21. package/src/local/cli-local-admin.mjs +308 -0
  22. package/src/local/cli-options.mjs +151 -0
  23. package/src/local/cli-policy.mjs +77 -0
  24. package/src/local/cli.mjs +16 -521
  25. package/src/local/errors.mjs +122 -0
  26. package/src/local/git-service.mjs +88 -0
  27. package/src/local/lifecycle.mjs +64 -0
  28. package/src/local/log.mjs +50 -19
  29. package/src/local/managed-job-plan.mjs +235 -0
  30. package/src/local/managed-jobs.mjs +16 -220
  31. package/src/local/observability.mjs +83 -0
  32. package/src/local/policy.mjs +148 -0
  33. package/src/local/process-execution.mjs +153 -0
  34. package/src/local/process-sessions.mjs +10 -20
  35. package/src/local/process-tracker.mjs +55 -0
  36. package/src/local/runtime.mjs +154 -672
  37. package/src/local/service.mjs +1 -0
  38. package/src/local/stdio.mjs +3 -11
  39. package/src/local/tool-executor.mjs +102 -0
  40. package/src/local/tools.mjs +21 -104
  41. package/src/local/workspace-file-service.mjs +451 -0
  42. package/src/shared/policy-contract.json +54 -0
  43. package/src/shared/tool-catalog.json +4 -4
  44. package/src/worker/index.ts +69 -524
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 - 2026-07-13
4
+
5
+ ### Runtime boundaries and lifecycle
6
+
7
+ - Replace the monolithic local tool dispatcher with a middleware-based execution pipeline covering policy authorization, bounded call registration, cancellation/deadlines, stable error normalization, structured lifecycle events, and per-tool metrics. Add explicit runtime lifecycle states and a process tracker so stop/cancel paths release calls and child ownership deterministically.
8
+ - Extract workspace filesystem transactions, process/shell execution, Git operations, CLI option/policy parsing, local resource/browser/job administration, capability ranking, managed-job plan/resource validation, browser extension protocol handling, and browser pairing persistence into focused modules. Add executable line-count and dependency-direction limits so these responsibilities cannot silently return to `LocalRuntime`, the CLI entrypoint, or browser manager.
9
+ - Split the Worker Durable Object into HTTP/CORS boundaries, OAuth state/PKCE helpers, shared policy evaluation, structured errors, pending-call indexing, and observability. Replace the linear pending-call scan with an atomic ID/request-key registry so completed, cancelled, timed-out, send-failed, and disconnected calls clear both indexes before settling; JSON-RPC request IDs can be reused immediately after completion.
10
+
11
+ ### Policy, errors, logging, and observability
12
+
13
+ - Define policy revision 4 in one shared contract consumed by the local daemon, Worker, generated documentation, tests, and architecture checks. Fix `start_job` to require both write and direct-execution capabilities, keep `cancel_job` write-gated, and expose read-only resource/job inventory to review-mode clients without granting mutation authority.
14
+ - Introduce typed `BridgeError`/Worker errors with allowlisted stable codes and retryability. Centralize legacy error classification at adapters instead of making transports and tests depend on free-form English messages. Unknown programming errors are no longer silently converted into ordinary managed-resource unavailability; a failing timeout callback still settles and clears its pending indexes.
15
+ - Add structured JSON lifecycle logging and in-memory observability for calls, durations, errors, active processes, pending request indexes, daemon candidates, socket transitions, and per-tool outcomes. Autostart services now use warning-level JSON logs by default while foreground output remains human-readable; local and Worker structured-field redaction is behavior-tested.
16
+
17
+ ### Tests, documentation, and audit follow-up
18
+
19
+ - Add behavior tests for policy parity, compound ACLs, middleware order, runtime lifecycle transitions, process ownership, pending-ID reuse, socket cleanup, Worker/local error serialization, structured log privacy, bilingual capability ranking, and generated policy documentation. Replace brittle source-string and monkeypatched happy-path tests with production-path deadline, process-tree, OAuth/MCP, and resource-reload coverage.
20
+ - Add a critical-module V8 coverage gate with per-module function/branch baselines, rather than a single aggregate percentage that hides weak orchestration coverage. Generate `docs/POLICY_REFERENCE.md` from the shared policy contract and tool catalog; CI rejects stale generated documentation.
21
+ - Correct an extraction defect where a re-exported resource inspector was not locally bound and a broad catch disguised the resulting `ReferenceError` as `resource_unavailable`. File availability now degrades only for classified filesystem conditions; unexpected implementation faults remain visible.
22
+
3
23
  ## 0.15.0 - 2026-07-13
4
24
 
5
25
  ### Daily-profile browser verification and protocol safety
package/README.md CHANGED
@@ -32,7 +32,7 @@ A newly selected workspace starts with the maximum-permission `full` profile for
32
32
  - tool results may return absolute paths;
33
33
  - child processes inherit the complete parent environment.
34
34
 
35
- Policy state records whether it came from the default, an explicit named profile, or custom overrides. Named profiles are canonical contracts: a stored `full` profile is repaired on load to enable writes, shell execution, unrestricted paths, full parent environment, absolute path output, and the complete tool catalog. Any explicit per-capability narrowing is stored as `custom`. Policy revision 3 refreshes default/migrated state and preserves explicit restrictive/custom profiles.
35
+ Policy state records whether it came from the default, an explicit named profile, or custom overrides. Named profiles are canonical contracts: a stored `full` profile is repaired on load to enable writes, shell execution, unrestricted paths, full parent environment, absolute path output, and the complete tool catalog. Any explicit per-capability narrowing is stored as `custom`. Policy revision 4 normalizes named profiles from the shared contract, refreshes default/migrated state, preserves explicit restrictive/custom profiles, and applies compound tool requirements consistently in the local daemon and Worker.
36
36
 
37
37
  | Profile | File edits | Direct argv processes | Shell commands | Filesystem scope | Process environment |
38
38
  |---|---:|---:|---:|---|---|
@@ -199,8 +199,17 @@ Load the printed unpacked-extension directory in the Chromium profile you actual
199
199
 
200
200
  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. Check `server_info.observability.capability_routing` or `project_overview.capabilityRouting` to distinguish “the host never called the resolver” from “the resolver ran but found no relevant capability.”
201
201
 
202
+
203
+ ## Runtime lifecycle and observability
204
+
205
+ Every tool call passes through one execution pipeline: bounded call registration and deadline/cancellation ownership, structured observability, shared policy authorization, then the typed handler. Errors use stable codes and retryability metadata instead of transport-specific message parsing. `server_info` exposes the local lifecycle state, in-flight calls, active process ownership, per-tool outcomes, durations, and error-code counts. The Worker adds pending internal/request-key indexes, daemon candidate/socket counters, and Worker-side per-tool outcomes.
206
+
207
+ Foreground logging defaults to human-readable text. Installed background services use warning-level JSON events by default. Use `--log-format json` for machine-readable foreground logs; arguments, outputs, credentials, resource contents, and local paths remain excluded or redacted.
208
+
202
209
  ## Policy controls
203
210
 
211
+ Policy revision 4 is defined once in `src/shared/policy-contract.json`. The local runtime, Worker advertisement filter, manager-level defense-in-depth checks, tests, and generated [policy reference](docs/POLICY_REFERENCE.md) consume that same contract. A custom policy is authorized by capabilities rather than its display name; compound classes such as `write+direct-exec` require every capability.
212
+
204
213
  The default is `full`. Narrow or customize it with explicit flags:
205
214
 
206
215
  ```text
@@ -212,6 +221,7 @@ The default is `full`. Narrow or customize it with explicit flags:
212
221
  --unrestricted-paths
213
222
  --absolute-paths
214
223
  --log-level error|warn|info|debug
224
+ --log-format text|json
215
225
  --verbose
216
226
  --quiet
217
227
  ```
@@ -432,7 +442,7 @@ Remote mode supports:
432
442
  - Linux `systemd --user`, with best-effort lingering;
433
443
  - Windows Scheduled Task at logon.
434
444
 
435
- The service definition contains neither credentials nor a duplicate policy. It loads the selected policy from owner-only local state and uses the documented `full` default if policy state is absent. launchd/systemd definitions persist a sanitized absolute-only PATH captured during installation, including the stable Node/CLI directories, so background `full` mode does not lose Homebrew or other developer command locations. Background services run at log level `warn`: relay, protocol, and service problems are retained, while all per-tool success/failure/cancellation/timing events remain debug-only. Logs are owner-only where supported and bounded by tail trimming.
445
+ The service definition contains neither credentials nor a duplicate policy. It loads the selected policy from owner-only local state and uses the documented `full` default if policy state is absent. launchd/systemd definitions persist a sanitized absolute-only PATH captured during installation, including the stable Node/CLI directories, so background `full` mode does not lose Homebrew or other developer command locations. Background services run at log level `warn` with JSON output: relay, protocol, and service problems are retained as bounded structured events, while all per-tool success/failure/cancellation/timing events remain debug-only. Logs are owner-only where supported and bounded by tail trimming.
436
446
 
437
447
  ## Secret rotation
438
448
 
package/SECURITY.md CHANGED
@@ -79,6 +79,13 @@ Caller-supplied JavaScript, AppleScript, JXA source, and arbitrary DevTools meth
79
79
 
80
80
  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. Source/inspection budgets are aggregate across frames, page-controlled metadata is bounded, URL userinfo is removed from semantic snapshots, and contenteditable controls with secret-like identity are treated as sensitive; these limits reduce accidental exposure but do not classify every possible secret. Browser/app tool arguments, source, screenshots, field values, and results are intentionally omitted from operational logs.
81
81
 
82
+
83
+ ## Shared authorization contract
84
+
85
+ Policy revision 4 is a single source of truth shared by the local daemon and Worker. The catalog availability class controls both advertisement and execution; manager-level checks invoke the same gate as defense in depth. Custom policies are evaluated by capabilities, so a label cannot impersonate `full`. Persistent job start requires both write and direct-execution authority; read-only job/resource inventory does not imply mutation authority.
86
+
87
+ Transport-visible failures use stable error codes and retryability. Unexpected implementation errors are not downgraded to ordinary resource state, and raw internal exceptions are not returned by the Worker. This reduces both authorization drift and accidental disclosure through error strings.
88
+
82
89
  ## Filesystem exposure
83
90
 
84
91
  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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -30,5 +30,5 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "0.15.0"
33
+ "version_name": "0.16.0"
34
34
  }
@@ -58,7 +58,7 @@ See [Session instructions, skills, commands, and capability discovery](AGENT_CON
58
58
 
59
59
  ### Browser extension and machine broker
60
60
 
61
- `BrowserBridgeManager` owns a loopback HTTP/WebSocket broker. The first runtime for the machine-level state root becomes broker owner; additional workspaces and stdio runtimes authenticate to `/runtime` and proxy through the same extension socket. This preserves one extension pairing while allowing multiple local MCP runtimes.
61
+ `BrowserBridgeManager` owns connection orchestration for a loopback HTTP/WebSocket broker. Extension version/capability parsing lives in `browser-extension-protocol.mjs`; pairing files and local HTML/Host/Origin helpers live in `browser-pairing-store.mjs`. The first runtime for the machine-level state root becomes broker owner; additional workspaces and stdio runtimes authenticate to `/runtime` and proxy through the same extension socket. This preserves one extension pairing while allowing multiple local MCP runtimes.
62
62
 
63
63
  The packaged Manifest V3 extension runs in the user's existing Chromium profile. Its service worker is limited to pairing, transport, acknowledged protocol readiness, cancellation, and response routing. Fixed `browser-operations.js` owns tab lifecycle, aggregate frame/source budgets, waits, screenshots, and input-backend selection; fixed `page-automation.js` is injected into selected frames for snapshot-version-2 semantics, stable refs, bounded DOM/text traversal, actionability checks, open-Shadow-DOM traversal, structured DOM operations, multi-field forms, and resource-backed file inputs. Fixed `devtools-input.js` exposes only bounded mouse, keyboard, and text sequences through the Chromium debugger API; callers cannot select CDP methods. Trusted sessions attach for one action and detach in `finally`; DOM fallback is allowed only before any DevTools Input dispatch, preventing duplicate side effects after an ambiguous command failure. Protocol 3 requires bidirectional `hello`/`hello_ack` plus exact packaged-version and capability equality; pairing state and replacement are committed only after validation, so an invalid candidate cannot displace or overwrite the working configuration. The broker validates loopback hostnames, canonical extension IDs, matching pairing/broker ports, bearer subprotocols, message sizes, concurrency, and deadlines. Pairing material is owner-only and omitted from MCP/log output.
64
64
 
@@ -86,7 +86,9 @@ Local resource registrations remain in owner-only state and are reloaded for eve
86
86
 
87
87
  ### Canonical policy contracts
88
88
 
89
- Named profiles are normalized to their complete capability sets. In particular, `full` always means write enabled, shell execution, unrestricted paths, complete parent environment, absolute-path display, and every catalog tool. CLI flags that alter an individual capability deliberately change the profile identity to `custom`. Policy revision 3 repairs stale or manually edited named-profile fields rather than allowing a misleading partially restricted `full` label.
89
+ Policy revision 4 is loaded from one shared JSON contract by both local and Worker policy evaluators. Tool advertisement and execution authorization use catalog availability classes; managers receive the same authorizer instead of reimplementing profile conditionals. Read-only job/resource inspection is `always`, job cancellation is write-gated, and starting a persistent job requires `write+direct-exec`.
90
+
91
+ Named profiles are normalized to their complete capability sets. In particular, `full` always means write enabled, shell execution, unrestricted paths, complete parent environment, absolute-path display, and every catalog tool. CLI flags that alter an individual capability deliberately change the profile identity to `custom`. Policy revision 4 repairs stale or manually edited named-profile fields rather than allowing a misleading partially restricted `full` label, and applies compound availability requirements from the shared contract.
90
92
 
91
93
  The full-only `generate_ssh_key_resource` operation is implemented locally. The Worker only filters and relays its shared catalog definition. Local generation uses `ssh-keygen`, verifies public/private correspondence, registers the private file through the same owner-only state transaction as the CLI, and rolls back a newly created pair if state persistence fails.
92
94
 
@@ -111,7 +113,7 @@ The daemon attachment deliberately omits workspace path/name/hash and process ID
111
113
 
112
114
  ### Autostart layer
113
115
 
114
- The service layer emits launchd, systemd-user, or Windows Scheduled Task definitions. Credentials are not embedded in service definitions; the daemon loads owner-only state. The exact policy is stored in owner-only state. launchd/systemd definitions contain the workspace/state-root selectors, a `warn` log-level setting, and a sanitized absolute-only PATH captured at installation so background `full` mode can resolve the same developer tools without accepting relative PATH entries.
116
+ The service layer emits launchd, systemd-user, or Windows Scheduled Task definitions. Credentials are not embedded in service definitions; the daemon loads owner-only state. The exact policy is stored in owner-only state. launchd/systemd definitions contain the workspace/state-root selectors, `warn` plus JSON log settings, and a sanitized absolute-only PATH captured at installation so background `full` mode can resolve the same developer tools without accepting relative PATH entries.
115
117
 
116
118
  The platform adapters normalize launchd, systemd, and Windows Scheduled Task operations to one `{ok, provider}` result contract. Removal is not a provider-specific sequence: `service-lifecycle.mjs` first stops the provider, then every verified workspace daemon in scope, and only then removes the definition. A failed stop or unverifiable process prevents definition/state deletion.
117
119
 
package/docs/AUDIT.md CHANGED
@@ -1,6 +1,17 @@
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, plus the 0.15.0 daily-browser and cross-platform hardening review. 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.
4
15
 
5
16
  ## 2026-07-13 daily-browser and cross-platform hardening follow-up
6
17
 
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:
@@ -101,7 +101,14 @@ Uninstall acquires a state-root `maintenance.lock` that blocks new profile/state
101
101
 
102
102
  ## Logs
103
103
 
104
- 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`. 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.
105
112
 
106
113
  Logging is level-based:
107
114
 
@@ -207,7 +214,7 @@ Defense-in-depth limits include:
207
214
 
208
215
  ## Upgrade behavior
209
216
 
210
- 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.
211
218
 
212
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.
213
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
@@ -62,6 +62,13 @@ It opens Calculator, activates it through the fixed JXA helper, verifies structu
62
62
 
63
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
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.
71
+
65
72
  ## Additional release checks
66
73
 
67
74
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.15.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 service-platform:test && npm run catalog:test && npm run agent-context: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",
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",
@@ -78,7 +78,18 @@
78
78
  "browser-devtools-input:test": "node tests/browser-devtools-input-test.mjs",
79
79
  "browser-page-automation:test": "node tests/browser-page-automation-test.mjs",
80
80
  "browser-service-worker:test": "node tests/browser-service-worker-test.mjs",
81
- "service-platform:test": "node tests/service-platform-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"
82
93
  },
83
94
  "dependencies": {
84
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
+ }