machine-bridge-mcp 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/LOGGING.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # Logging and observability
2
2
 
3
+ Operational logs are a user-facing diagnosis surface. They report meaningful service state without becoming a transcript of user activity or a dump of transport callbacks.
4
+
3
5
  ## Goals
4
6
 
5
- Operational logs should answer four questions without becoming a transcript of user activity:
7
+ Logs should answer:
6
8
 
7
- 1. Did deployment and relay connection succeed?
8
- 2. Is the daemon reconnecting, superseded, or unhealthy?
9
- 3. Is an infrastructure, protocol, deployment, or relay failure occurring?
10
- 4. When debug logging is explicitly enabled, which bounded per-tool event should be correlated?
9
+ 1. Did deployment and authenticated relay readiness succeed?
10
+ 2. Is a service persistently degraded, and is recovery automatic?
11
+ 3. Did the service recover, and how long was it unavailable?
12
+ 4. Is an infrastructure, protocol, deployment, or local service problem requiring action?
13
+ 5. When debug logging is explicitly enabled, which bounded implementation event should be correlated?
11
14
 
12
15
  Logs are not a command history, content audit trail, or substitute for MCP client approvals.
13
16
 
@@ -21,53 +24,89 @@ The CLI accepts:
21
24
  --verbose # alias for debug
22
25
  ```
23
26
 
24
- The default foreground level is `info`. Platform autostart services use `warn`.
27
+ Foreground mode defaults to `info`. Platform autostart services use `warn`.
25
28
 
26
29
  | Level | Intended events |
27
30
  |---|---|
28
- | `error` | Broken request handlers, relay transport errors, unrecoverable local failures |
29
- | `warn` | Relay disconnections/send failures, malformed or oversized messages, superseded daemons, service/autostart problems |
30
- | `info` | Deployment/startup transitions and successful relay connection |
31
- | `debug` | Every per-tool start/success/failure/cancellation/timing event, shortened correlation IDs, reconnect timing |
31
+ | `error` | The operation or service cannot continue without intervention |
32
+ | `warn` | Persistent relay outage, malformed/oversized protocol data, supersession, or service/autostart degradation |
33
+ | `info` | Deployment/startup transitions, authenticated relay readiness, and recovery after a visible outage |
34
+ | `debug` | Transport open/close codes and reasons, brief self-healing interruptions, retry timing, per-tool outcomes, and shortened correlation IDs |
35
+
36
+ ## Relay connection event policy
37
+
38
+ A TCP/WebSocket `open` event is only transport availability. The daemon is reported as connected only after the Worker returns `hello_ack` for the authenticated `hello` message.
39
+
40
+ Brief network interruptions are expected on laptop network changes, Worker deployment, proxy rotation, and ordinary internet transport. They are handled as follows:
32
41
 
33
- All per-tool outcomes are deliberately absent from `info`, `warn`, and `error`. The MCP response already reports success or failure to the caller; duplicating ordinary failures in daemon logs creates noise without adding operational information. A normal sequence of hundreds of successful or failed tool invocations should not fill the user's terminal or service logs.
42
+ - the raw close code, close reason, error class, connected duration, and retry delay are debug-only;
43
+ - a brief interruption that reconnects within the grace period produces no `info` or `warn` line;
44
+ - an outage that remains unresolved for 10 seconds produces one rate-limited warning stating that automatic reconnection is in progress;
45
+ - relay identity/version mismatch and authentication rejection are non-transient: they produce an immediate actionable error and terminate that daemon instead of entering the reconnect loop;
46
+ - repeated warnings are rate-limited;
47
+ - recovery after a visible outage produces one information summary with outage duration and attempt count;
48
+ - an authenticated replacement is a distinct warning and permanently stops the older daemon;
49
+ - failure to receive `hello_ack` within the handshake deadline terminates the candidate socket and retries;
50
+ - lack of inbound heartbeat activity terminates a half-open socket and reconnects.
34
51
 
35
- ## Event fields
52
+ A WebSocket close code such as `1006` means the transport ended without a normal close handshake. It is useful for debug diagnosis but not useful as the default user message. Default logs therefore describe the effect and recovery behavior rather than printing `{"code":1006,"reason":""}`.
53
+
54
+ Examples:
55
+
56
+ ```text
57
+ [info] daemon: remote relay connected
58
+ [warn] daemon: remote relay is unavailable; automatic reconnection is still in progress {"outage_seconds":12,"attempts":3,"cause":"connection interrupted"}
59
+ [info] daemon: remote relay connection restored {"outage_seconds":18,"attempts":4}
60
+ ```
36
61
 
37
- Default operational events may include component, relay close code, bounded reconnect delay, provider name, or a coarse infrastructure error class. Per-tool fields—tool name, duration, outcome class, and shortened call identifier—are debug-only. Call identifiers are useful only for correlating adjacent local events and are not stable audit identifiers.
62
+ With `--verbose`, the same incident may additionally include bounded transport diagnostics.
38
63
 
39
- The implementation intentionally omits:
64
+ ## Tool events
40
65
 
41
- - tool arguments;
42
- - command text or argv;
66
+ All per-tool starts, successes, failures, cancellations, and timing are debug-only. The MCP response already reports the outcome to the caller; duplicating routine tool traffic at default levels creates noise and can reveal activity patterns.
67
+
68
+ Debug per-tool fields may include tool name, duration, coarse outcome class, and a shortened random call identifier. The identifier is for correlating adjacent local events and is not a stable audit identifier.
69
+
70
+ ## Data that is never logged
71
+
72
+ The implementation omits:
73
+
74
+ - tool arguments and command text or argv;
43
75
  - stdin, stdout, and stderr;
44
- - file, patch, or image content;
76
+ - file, patch, image, and temporary-file content;
45
77
  - OAuth request bodies;
46
- - connection passwords, daemon secrets, authorization codes, and access tokens.
78
+ - connection passwords, daemon secrets, authorization codes, and access tokens;
79
+ - registered resource values and source paths.
47
80
 
48
- Unexpected daemon, service, and Worker infrastructure failures are reduced to coarse error classes in normal logs. Client-facing tool errors may contain more detail according to the active path-display policy, but ordinary tool failures and those details are not copied into normal operational logs.
81
+ Unexpected infrastructure failures are reduced to coarse error classes in normal logs. Client-facing tool errors may contain more detail according to the active path-display policy, but those details are not copied into operational logs.
49
82
 
50
83
  ## Bounding and redaction
51
84
 
52
- Messages, strings, object depth, object key counts, array item counts, and serialized field payloads are bounded. Control characters are neutralized. Fields with secret-like names and known token formats are recursively redacted.
85
+ Messages, strings, object depth, object key counts, array item counts, and serialized field payloads are bounded. Control characters and Unicode display controls are neutralized. Fields with secret-like names, path-like keys, and known token formats are recursively redacted.
53
86
 
54
- This is defense in depth, not content classification. A previously unknown secret format embedded in an ordinary non-secret-named field may evade pattern redaction. For that reason, tool arguments and outputs are omitted rather than merely filtered.
87
+ This is defense in depth, not content classification. Unknown secret formats can evade pattern matching, which is why tool arguments and outputs are omitted rather than merely filtered.
55
88
 
56
89
  ## Files
57
90
 
58
- Autostart logs are stored below the state root:
91
+ Autostart logs are stored below the owner-only state root:
59
92
 
60
93
  ```text
61
94
  logs/daemon.out.log
62
95
  logs/daemon.err.log
63
96
  ```
64
97
 
65
- They are owner-only where the platform supports Unix-style permissions. Existing files are tail-trimmed on UTF-8/line boundaries before daemon startup. Because background services use `warn` and per-tool events are debug-only, neither successful nor failed ordinary tool traffic should cause sustained log growth.
98
+ Existing files are opened without following symbolic links where supported and tail-trimmed on UTF-8/line boundaries before startup. Background services use `warn`, so ordinary tool traffic and brief relay interruptions do not cause sustained growth.
66
99
 
67
- Each managed job also has owner-only `runner.out.log` and `runner.err.log` files for runner startup/crash diagnostics. Child-step stdout and stderr are captured into bounded job results according to `capture_output`; they are not copied into runner or daemon operational logs. Runner logs are retained with the bounded job directory and removed by job retention cleanup.
100
+ Each managed job has owner-only runner diagnostic logs. Child-step output is retained only in bounded, redacted job results according to `capture_output`; it is not copied into daemon or runner operational logs.
68
101
 
69
102
  ## MCP host boundary
70
103
 
71
- Machine Bridge does not classify filenames as sensitive and does not block `.env`, password, key, or credential-looking names. The active local policy, local OS permissions, and endpoint-security controls determine what the server itself can read or execute.
104
+ Machine Bridge does not classify filenames as sensitive and does not block credential-looking names under canonical `full`. An MCP host, connector, model provider, desktop application, operating system, or endpoint-security layer may independently reject a request before it reaches Machine Bridge.
105
+
106
+ Use `server_info`, `machine-mcp status`, `machine-mcp doctor`, and `diagnose_runtime` to distinguish local policy from host-side enforcement. Changing the Machine Bridge profile cannot override another layer.
107
+
108
+ ## Adding or changing logs
109
+
110
+ A default-level message must be actionable, privacy-preserving, and resistant to repetition. Add a regression test for severity and field visibility. Raw protocol values belong at debug unless a user can act on them without external documentation.
72
111
 
73
- An MCP host, model provider, desktop application, or platform execution layer may independently deny a request involving credentials or sensitive files. Such a denial occurs before or outside Machine Bridge's file resolver and cannot be disabled by selecting the local `full` profile. Use `server_info`, `machine-mcp status`, and `machine-mcp doctor` to distinguish the active local policy from host-side enforcement.
112
+ See [ENGINEERING.md](ENGINEERING.md) for the project-wide review rules.
@@ -46,10 +46,11 @@ A successful diagnostic response proves that particular request reached the loca
46
46
  Secrets and other local-only files should be registered from the user's terminal, not read into the model context:
47
47
 
48
48
  ```sh
49
- chmod 600 ~/.ssh/racknerd_ed25519
50
- machine-mcp resource add racknerd-key ~/.ssh/racknerd_ed25519
51
- machine-mcp resource list
52
- machine-mcp resource check racknerd-key
49
+ chmod 600 ~/.ssh/example_maintenance_ed25519
50
+ machine-mcp resource add maintenance-key ~/.ssh/example_maintenance_ed25519
51
+ machine-mcp resource list # paths omitted by default
52
+ machine-mcp resource list --show-paths # explicit local-only disclosure
53
+ machine-mcp resource check maintenance-key
53
54
  ```
54
55
 
55
56
  Registration stores only canonical path and bounded metadata in owner-only state. It reads the file locally to validate accessibility and size but does not print or send its contents. New jobs see registry changes immediately; the daemon does not need a restart.
@@ -59,7 +60,7 @@ Unix-like systems reject group/other-readable resource files by default. `--allo
59
60
  Remove an alias with:
60
61
 
61
62
  ```sh
62
- machine-mcp resource remove racknerd-key
63
+ machine-mcp resource remove maintenance-key
63
64
  ```
64
65
 
65
66
  Removing an alias affects new jobs. A job already accepted has an owner-only resource snapshot specification and continues independently.
@@ -67,10 +68,10 @@ Removing an alias affects new jobs. A job already accepted has an owner-only res
67
68
  Generate a new Ed25519 resource rather than exposing a private-key creation command through a model-generated shell string:
68
69
 
69
70
  ```sh
70
- machine-mcp resource generate-ssh-key racknerd-key ~/.ssh/ai-racknerd-maint-ed25519
71
+ machine-mcp resource generate-ssh-key maintenance-key ~/.ssh/machine-mcp-example-maint-ed25519
71
72
  ```
72
73
 
73
- Canonical-full MCP clients also receive `generate_ssh_key_resource`. It generates or reuses the pair locally, verifies correspondence, registers the private file, and returns no private bytes. Public-key installation in a cloud or remote account should be a separate reviewed step or managed job.
74
+ Canonical-full MCP clients also receive `generate_ssh_key_resource`. It generates or reuses the pair locally, verifies correspondence, registers the private file, and returns neither private bytes nor local paths by default. Set `expose_paths=true` only for an explicit path-retrieval operation. Public-key installation in a cloud or remote account should be a separate reviewed step or managed job.
74
75
 
75
76
  ## Resource injection modes
76
77
 
@@ -80,7 +81,7 @@ A managed step is argv-based and supports three local resource modes:
80
81
 
81
82
  ```json
82
83
  {
83
- "argv": ["ssh", "-i", "{{resource:racknerd-key}}", "root@example", "true"]
84
+ "argv": ["ssh", "-i", "{{resource:maintenance-key}}", "admin@server.example", "true"]
84
85
  }
85
86
  ```
86
87
 
@@ -152,8 +153,8 @@ For a remote shell program, avoid a remote helper entirely when possible:
152
153
  "argv": [
153
154
  "ssh",
154
155
  "-i",
155
- "{{resource:racknerd-key}}",
156
- "root@example",
156
+ "{{resource:maintenance-key}}",
157
+ "admin@server.example",
157
158
  "sh",
158
159
  "-s"
159
160
  ],
@@ -174,12 +175,12 @@ A job can contain ordered main steps and ordered `finally_steps`:
174
175
  "name": "remote maintenance",
175
176
  "steps": [
176
177
  {
177
- "argv": ["ssh", "root@example", "perform-maintenance"]
178
+ "argv": ["ssh", "admin@server.example", "perform-maintenance"]
178
179
  }
179
180
  ],
180
181
  "finally_steps": [
181
182
  {
182
- "argv": ["ssh", "root@example", "rm", "-f", "/tmp/maintenance-helper"],
183
+ "argv": ["ssh", "admin@server.example", "rm", "-f", "/tmp/maintenance-helper"],
183
184
  "allow_failure": true
184
185
  }
185
186
  ]
@@ -207,7 +208,7 @@ machine-mcp job approve JOB_ID
207
208
  machine-mcp job approve JOB_ID --yes
208
209
  ```
209
210
 
210
- `job inspect` displays the reviewable plan, including argv, ordinary environment overrides, stdin, temporary helper content, and finally steps, while omitting registered resource source paths and per-resource hashes. The overall `plan_sha256` can be compared with the value returned by `stage_job`.
211
+ `job inspect` displays the reviewable plan, including argv, ordinary environment overrides, stdin, temporary helper content, and finally steps, while omitting registered resource source paths and per-resource hashes. The overall `plan_sha256` is displayed for review and is revalidated atomically during approval and again by the runner before execution. A modified staged plan is rejected.
211
212
 
212
213
  Local approval is a new operator authorization. It intentionally does not depend on the current MCP execution profile: a plan staged under a write-capable profile can be reviewed and approved from the terminal even when the connector is not allowed to execute. The plan retains the filesystem scope and environment mode captured when it was staged.
213
214
 
@@ -14,6 +14,7 @@ machine-mcp service status
14
14
 
15
15
  | Result | Interpretation |
16
16
  |---|---|
17
+ | `server_info` reports full and all relay tools, but the current session UI exposes fewer tools | Host/connector post-relay filtering; Machine Bridge cannot enumerate or override that subset |
17
18
  | No structured result because the host rejects the call | Host/connector approval or safety layer, or transport before daemon delivery |
18
19
  | `mcp-host-to-daemon` passes but `local-filesystem` fails | Local state/runtime permissions, disk policy, sandbox, or endpoint security |
19
20
  | Filesystem passes but `local-process-spawn` fails | Local executable policy, endpoint security, OS permissions, or damaged Node runtime |
@@ -21,11 +22,17 @@ machine-mcp service status
21
22
  | `managed-job-storage` fails | Owner-only profile/job directory cannot be used |
22
23
  | Registered resource is unavailable | File moved, permissions changed, size exceeded, or local access denied |
23
24
 
24
- A successful diagnostic result applies only to that probe. An MCP host can still deny a later call based on its own request context.
25
+ A successful diagnostic result applies only to that probe. An MCP host can still deny a later call based on its own request context. This is expected layering, not a defect in the `full` profile: `full` removes Machine Bridge's own denials, while host delivery remains independent.
26
+
27
+ ### Relay interruption messages
28
+
29
+ A brief relay interruption is retried automatically and is visible only with `--verbose`. Default logs do not print raw WebSocket values such as `code=1006` with an empty reason. If a transient outage persists for 10 seconds, one warning is emitted and repeated warnings are rate-limited; a later recovery produces one summary with outage duration and attempt count. Identity/version mismatch and authentication rejection are not retried as ordinary network faults: the daemon emits an immediate actionable error and exits, requiring upgrade/redeployment or credential repair.
30
+
31
+ Use `--verbose` only when close codes, close reasons, heartbeat timeouts, and retry delays are needed for diagnosis. A close code of 1006 means the transport ended without a normal close handshake; it does not by itself identify the cause.
25
32
 
26
33
  ## Logs
27
34
 
28
- Remote 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.
35
+ 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. 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.
29
36
 
30
37
  Logging is level-based:
31
38
 
@@ -58,7 +65,7 @@ Generate and register an operator key locally:
58
65
  machine-mcp resource generate-ssh-key NAME [PRIVATE_KEY_PATH]
59
66
  ```
60
67
 
61
- An authorized canonical-full MCP client can use `generate_ssh_key_resource`. Both paths validate the key pair and return only metadata and a public fingerprint. They do not install the public key in Google, modify `authorized_keys`, or grant remote `sudo`; those remain explicit managed-job/local-operator operations.
68
+ An authorized canonical-full MCP client can use `generate_ssh_key_resource`. Both paths validate the key pair and return only metadata and the bare public fingerprint. Local paths are omitted unless `--show-paths` or `expose_paths=true` is explicitly requested; public-key comments are not included in the returned fingerprint. They do not install the public key in Google, modify `authorized_keys`, or grant remote `sudo`; those remain explicit managed-job/local-operator operations.
62
69
 
63
70
  ## Managed jobs and local recovery
64
71
 
@@ -66,7 +73,8 @@ Register local-only resources from the terminal:
66
73
 
67
74
  ```sh
68
75
  machine-mcp resource add NAME FILE_PATH
69
- machine-mcp resource list
76
+ machine-mcp resource list # paths omitted by default
77
+ machine-mcp resource list --show-paths # explicit local-only disclosure
70
78
  machine-mcp resource check NAME
71
79
  machine-mcp resource remove NAME
72
80
  ```
@@ -102,8 +110,9 @@ Pending calls are bound to the socket that received them. Results from another s
102
110
  Defense-in-depth limits include:
103
111
 
104
112
  - Worker MCP body: 8 MiB by default, hard cap 16 MiB;
113
+ - stdio JSON-RPC line: 8 MiB, enforced incrementally while reading;
105
114
  - OAuth body: 64 KiB;
106
- - daemon WebSocket message: 8 MiB;
115
+ - daemon WebSocket message: 8 MiB, enforced by the local WebSocket parser before string conversion;
107
116
  - text writes and patch envelopes: 5 MiB;
108
117
  - images: 4 MiB before base64 encoding;
109
118
  - shell/argv envelope: 64 KiB;
@@ -128,7 +137,7 @@ Defense-in-depth limits include:
128
137
 
129
138
  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.
130
139
 
131
- `full` removes Machine Bridge's own profile/path/environment/shell denials. It does not override operating-system access controls, endpoint security, remote authentication, cloud IAM, `sudo`, or independent MCP-host/platform policy.
140
+ `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.
132
141
 
133
142
  Inspect effective policy with:
134
143
 
@@ -0,0 +1,43 @@
1
+ # Repository privacy hygiene
2
+
3
+ Source code, tests, examples, release notes, and documentation are publication surfaces. Examples must use synthetic identifiers such as `maintenance-key`, `admin@server.example`, reserved example domains, and generic filesystem paths. Do not copy a real server alias, username, hostname, account name, workspace path, key filename, customer name, or internal codename into a fixture merely because the value is not itself a credential.
4
+
5
+ ## Automated check
6
+
7
+ Run:
8
+
9
+ ```sh
10
+ npm run privacy:check
11
+ ```
12
+
13
+ The check scans tracked and unignored new UTF-8 files and their relative names for private-key material, common live-token forms, user-home paths, non-example `user@host` identifiers, and locally configured private identifiers. Publication-surface symbolic links are rejected rather than followed. Binary, invalid UTF-8, and files above the bounded scanner limit fail closed and require explicit manual review instead of being silently skipped. It reports only the file, line, and rule; it does not print the matched value.
14
+
15
+ Maintain machine-specific names in an ignored owner-only file:
16
+
17
+ ```sh
18
+ cp .privacy-denylist.example .privacy-denylist
19
+ chmod 600 .privacy-denylist
20
+ ```
21
+
22
+ Add one identifier per line. The denylist is deliberately local and must never be committed. CI still runs the built-in generic checks; a developer's local check adds their private vocabulary.
23
+
24
+ ## Local maintenance notes
25
+
26
+ Machine-specific operational notes may be kept under the ignored `.project-local/` directory. Use it for temporary environment state and one-machine recovery observations, not for reusable engineering decisions. General lessons belong in tracked documentation such as `ENGINEERING.md`.
27
+
28
+ Ignored does not mean safe for secrets: do not store passwords, tokens, private keys, authorization URLs, or copied secret-bearing logs there. `.privacy-denylist` remains the dedicated local vocabulary gate.
29
+
30
+ ## Review rules
31
+
32
+ Before committing or publishing:
33
+
34
+ - inspect the complete staged diff, including tests, snapshots, examples, release notes, and generated metadata;
35
+ - use reserved example domains and neutral aliases;
36
+ - run `npm run privacy:check`, `npm run check`, and `npm pack --dry-run`;
37
+ - treat paths, host aliases, usernames, and codenames as private metadata even when they are not authentication secrets.
38
+
39
+ The scanner is heuristic. It cannot identify every personal or organizational name, transformed value, image, archive, binary fixture, or data already present in Git history.
40
+
41
+ ## Incident response
42
+
43
+ For an accidental publication, remove the value from the current tree and release artifacts, determine whether it is merely identifying metadata or an active credential, and rotate/revoke any credential immediately. Public Git and npm history are immutable in ordinary workflows: replacing the current file does not erase old commits or a published package. A coordinated history rewrite, cache invalidation request, or replacement release may be appropriate, but those actions are disruptive and require an explicit repository-owner decision.
package/docs/RELEASING.md CHANGED
@@ -7,6 +7,8 @@ The release invariant is:
7
7
  - A final GitHub Release exists for the tag.
8
8
  - The GitHub Release contains the npm tarball generated from that commit.
9
9
  - `package.json`, `package-lock.json`, and the Worker-reported version agree.
10
+ - Every release-relevant change since the prior version tag has a higher package version and matching CHANGELOG section.
11
+ - The same reviewed change is present on GitHub and in a new npm version.
10
12
 
11
13
  `npm publish` runs `release:check` through `prepublishOnly`, so npm publication is blocked until all GitHub state is synchronized.
12
14
 
@@ -18,8 +20,11 @@ The release invariant is:
18
20
  npm version <version> --no-git-tag-version
19
21
  ```
20
22
 
21
- 2. Add the matching `CHANGELOG.md` section.
22
- 3. Run `npm run check`, review the diff, and commit all release changes to `main`.
23
+ 2. Add the matching dated `CHANGELOG.md` section.
24
+ 3. Run `npm run release-impact:check`, `npm run privacy:check`, `npm run check`, both dependency audits, `npm audit signatures`, and generate a CycloneDX `npm sbom`.
25
+ 4. Inspect the complete diff and `npm pack --dry-run`, then commit and push all release changes to `main`.
26
+
27
+ A privacy/security documentation correction is not “docs only” for release purposes. It requires a replacement npm version and, when appropriate, deprecation or unpublication of the affected version.
23
28
 
24
29
  ## Publish GitHub source and release
25
30
 
@@ -29,7 +34,7 @@ From a clean `main` worktree:
29
34
  npm run release:publish
30
35
  ```
31
36
 
32
- The command validates the project, fast-forwards `origin/main`, creates or verifies the annotated version tag, pushes it, builds the npm tarball, creates or updates the GitHub Release, uploads the tarball, and verifies the resulting state.
37
+ The command validates the project, including repository privacy checks, fast-forwards `origin/main`, creates or verifies the annotated version tag, pushes it, builds the npm tarball, creates or updates the GitHub Release, uploads the tarball, and verifies the resulting state. Read-only GitHub operations, Git pushes, and idempotent release updates use bounded retries only for classified transient network failures; ambiguous Release creation responses are resolved by querying server state before continuing.
33
38
 
34
39
  To verify without changing anything:
35
40
 
@@ -51,7 +56,7 @@ Only after `release:check` succeeds:
51
56
  npm publish --access public
52
57
  ```
53
58
 
54
- The npm lifecycle repeats the full project checks and the GitHub synchronization check before upload.
59
+ The npm lifecycle repeats the full project checks and the GitHub synchronization check before upload. Do not leave a code or documentation fix only on GitHub; the corresponding npm version must be published, and the operator should be explicitly reminded until registry publication is confirmed.
55
60
 
56
61
  ## Authentication requirements
57
62
 
package/docs/TESTING.md CHANGED
@@ -8,8 +8,11 @@ The project treats transport, authorization, local authority, and state removal
8
8
  npm run check
9
9
  ```
10
10
 
11
+ The repository requires Node.js 26 and npm 12. `.node-version`, `.nvmrc`, `packageManager`, `devEngines`, and strict engine checks keep local and CI execution on the same baseline.
12
+
11
13
  The suite includes:
12
14
 
15
+ - release-impact enforcement requiring a new package version and CHANGELOG section for release-relevant changes;
13
16
  - generated Cloudflare Worker types and strict TypeScript checking;
14
17
  - syntax validation for every shipped JavaScript entry point;
15
18
  - shared tool-catalog schema, annotation, and profile-inventory checks;
@@ -34,6 +37,8 @@ The suite includes:
34
37
  - guarded state-root removal, schema migration, policy-origin persistence, and legacy implicit-default migration;
35
38
  - no filename-based sensitive-file denial under unrestricted policy;
36
39
  - log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, and service warning-level configuration;
40
+ - deterministic relay connection lifecycle coverage for transport construction/error, authenticated `hello_ack` readiness, identity/version mismatch, handshake and heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
41
+ - shared no-follow bounded-file reads for normal files, over-limit data, directories, and symbolic links;
37
42
  - CLI parsing, policy profiles, and client configuration boundaries;
38
43
  - live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
39
44
  - live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, dynamic tool advertisement, rich content, daemon replacement, and cancellation.
@@ -44,11 +49,14 @@ The suite includes:
44
49
  npm run worker:dry-run
45
50
  npm audit --audit-level=high
46
51
  npm audit --omit=dev --audit-level=high
52
+ npm audit signatures
53
+ npm sbom --sbom-format cyclonedx
47
54
  npm pack --dry-run
48
55
  npm run version:check
56
+ npm run release-impact:check
49
57
  ```
50
58
 
51
- GitHub Actions executes the main suite on Linux, macOS, and Windows. Node 22 and 24 are covered on Linux; Node 22 is covered on macOS and Windows. A separate package-audit job audits both the complete dependency graph and the production-only graph, then performs a dry-run package build. Dependency and GitHub Actions updates are monitored by Dependabot.
59
+ GitHub Actions executes the main suite on Linux, macOS, and Windows using the pinned Node 26/npm 12 baseline. Because Node 26 currently bundles npm 11, CI explicitly disables setup-node's automatic package-manager cache and upgrades npm from the runner temporary directory before any project-local npm command can trigger strict `devEngines`. Checkout fetches version tags so the release-impact gate can compare the branch with the latest release. A separate package-audit job audits both the complete dependency graph and the production-only graph, verifies registry signatures and attestations, validates a CycloneDX SBOM written under the runner temporary directory, exercises the documented isolated global installation, then performs a dry-run package build. The macOS matrix job also runs the installation smoke test because Wrangler's optional `fsevents` resolution is platform-specific. Dependency and GitHub Actions updates are monitored by Dependabot.
52
60
 
53
61
  ## Test design rules
54
62
 
@@ -61,3 +69,17 @@ GitHub Actions executes the main suite on Linux, macOS, and Windows. Node 22 and
61
69
  - Secret-bearing resource tests must assert absence of raw, path, base64, and hex forms from MCP-visible results.
62
70
  - Logs and public metadata should be tested for absence of sensitive fields, arguments, outputs, and routine success noise—not only presence of expected fields.
63
71
  - Cross-platform tests must avoid shell syntax, URL-path conversion, and executable-shim assumptions specific to one operating system.
72
+
73
+ ## Privacy hygiene
74
+
75
+ Run `npm run privacy:check` before committing and before packaging. Developers should maintain an ignored owner-only `.privacy-denylist` for machine aliases, usernames, internal codenames, and other private identifiers that a generic scanner cannot know. See [Repository privacy hygiene](PRIVACY.md).
76
+
77
+ ## Package manifest
78
+
79
+ `npm run package:test` executes a real silent `npm pack --dry-run --json`, requires clean parseable JSON, rejects sensitive local artifacts and credential-like file classes, and verifies that privacy/engineering guidance, the runtime/relay/secure-file modules, contribution discipline, and privacy/release-impact checkers are present in the published package. `npm run install:test` packs the real tarball, installs it into an isolated global prefix with the documented npm 12 options, rejects blocked-script warnings, confirms optional `fsevents` is absent, and runs the installed CLI.
80
+
81
+ The stdio integration test also sends an oversized line, verifies bounded rejection, and confirms that the next valid request is still processed.
82
+
83
+ ## Architecture and documentation regression checks
84
+
85
+ `npm run architecture:test` rejects local-module dependency cycles, missing relative imports, obsolete `LocalDaemon`/`daemon.mjs` naming, broken relative Markdown links, invisible ASCII control bytes in repository text, removal of the owner-required default-`full` engineering invariant, and accidental publication of `.project-local/` notes.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Cross-client MCP bridge for canonical local automation, files, Git, SSH resources, managed jobs, and processes over stdio or an OAuth-protected remote relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": {
8
- "node": ">=22.0.0"
8
+ "node": ">=26.0.0"
9
9
  },
10
10
  "bin": {
11
11
  "machine-bridge-mcp": "bin/machine-mcp.mjs",
@@ -20,11 +20,15 @@
20
20
  "src/shared",
21
21
  "src/worker/index.ts",
22
22
  "scripts/sync-worker-version.mjs",
23
+ "scripts/privacy-check.mjs",
24
+ "scripts/release-impact-check.mjs",
25
+ "scripts/network-retry.mjs",
23
26
  "wrangler.jsonc",
24
27
  "tsconfig.json",
25
28
  "README.md",
26
29
  "SECURITY.md",
27
30
  "CHANGELOG.md",
31
+ "CONTRIBUTING.md",
28
32
  "docs",
29
33
  "LICENSE"
30
34
  ],
@@ -35,12 +39,12 @@
35
39
  "version:sync": "node scripts/sync-worker-version.mjs",
36
40
  "version:check": "node scripts/sync-worker-version.mjs --check",
37
41
  "version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts",
38
- "prepack": "npm run version:check",
42
+ "prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
39
43
  "prepublishOnly": "npm run check && npm run version:check && npm run release:check",
40
44
  "worker:types": "wrangler types src/worker/worker-configuration.d.ts",
41
45
  "typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
42
- "syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs",
43
- "check": "npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run atomic-fs: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
+ "syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/runtime.mjs && node --check src/local/relay-connection.mjs && node --check src/local/secure-file.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/privacy-check.mjs && node --check scripts/release-impact-check.mjs && node --check scripts/network-retry.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/runtime-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs && node --check tests/package-test.mjs && node --check tests/release-impact-test.mjs && node --check tests/privacy-test.mjs && node --check tests/network-retry-test.mjs && node --check tests/relay-connection-test.mjs && node --check tests/install-smoke-test.mjs && node --check tests/secure-file-test.mjs && node --check tests/architecture-test.mjs",
47
+ "check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact: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 catalog: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",
44
48
  "worker:dry-run": "wrangler deploy --dry-run",
45
49
  "worker:integration-test": "node tests/worker-integration-test.mjs",
46
50
  "release:check": "node scripts/github-release.mjs --check",
@@ -51,15 +55,25 @@
51
55
  "managed-jobs:test": "node tests/managed-jobs-test.mjs",
52
56
  "ssh-key:test": "node tests/ssh-key-test.mjs",
53
57
  "full-access:test": "node tests/full-access-test.mjs",
54
- "atomic-fs:test": "node tests/atomic-fs-test.mjs"
58
+ "atomic-fs:test": "node tests/atomic-fs-test.mjs",
59
+ "privacy:check": "node scripts/privacy-check.mjs",
60
+ "package:test": "node tests/package-test.mjs",
61
+ "release-impact:check": "node scripts/release-impact-check.mjs",
62
+ "release-impact:test": "node tests/release-impact-test.mjs",
63
+ "privacy:test": "node tests/privacy-test.mjs",
64
+ "network-retry:test": "node tests/network-retry-test.mjs",
65
+ "relay:test": "node tests/relay-connection-test.mjs",
66
+ "install:test": "node tests/install-smoke-test.mjs",
67
+ "secure-file:test": "node tests/secure-file-test.mjs",
68
+ "architecture:test": "node tests/architecture-test.mjs"
55
69
  },
56
70
  "dependencies": {
57
71
  "wrangler": "4.110.0",
58
72
  "ws": "8.21.0"
59
73
  },
60
74
  "devDependencies": {
61
- "@types/node": "^22.20.1",
62
- "typescript": "^7.0.2"
75
+ "@types/node": "26.1.1",
76
+ "typescript": "7.0.2"
63
77
  },
64
78
  "keywords": [
65
79
  "mcp",
@@ -89,5 +103,18 @@
89
103
  "sharp@0.34.5": true,
90
104
  "workerd@1.20260708.1": true,
91
105
  "fsevents": false
106
+ },
107
+ "packageManager": "npm@12.0.1",
108
+ "devEngines": {
109
+ "runtime": {
110
+ "name": "node",
111
+ "version": ">=26.0.0",
112
+ "onFail": "error"
113
+ },
114
+ "packageManager": {
115
+ "name": "npm",
116
+ "version": ">=12.0.0",
117
+ "onFail": "error"
118
+ }
92
119
  }
93
120
  }
@@ -0,0 +1,47 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
4
+ const TRANSIENT_NETWORK_FAILURE = /(?:SSL_ERROR_SYSCALL|SSL_connect|TLS handshake timeout|Client network socket disconnected before secure TLS connection was established|unexpected EOF|unexpected eof while reading|\bEOF\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ECONNREFUSED|Could not resolve host|Failed to connect|connection (?:was )?reset|remote end hung up|unexpected disconnect|HTTP (?:429|5\d\d)|(?:502|503|504) (?:Bad Gateway|Service Unavailable|Gateway Timeout))/i;
5
+
6
+ export function runNetworkCommand(command, args, options = {}) {
7
+ const attempts = clampInteger(options.attempts, 3, 1, 5);
8
+ const baseDelayMs = clampInteger(options.baseDelayMs, 750, 0, 10_000);
9
+ const actualArgs = networkCommandArgs(command, args);
10
+ let result;
11
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
12
+ result = spawnSync(command, actualArgs, {
13
+ cwd: options.cwd,
14
+ encoding: "utf8",
15
+ stdio: "pipe",
16
+ env: options.env,
17
+ windowsHide: true,
18
+ });
19
+ result.attempts = attempt;
20
+ if (!isTransientNetworkFailure(result) || attempt === attempts) return result;
21
+ sleepSync(Math.min(baseDelayMs * attempt, 5_000));
22
+ }
23
+ return result;
24
+ }
25
+
26
+ export function networkCommandArgs(command, args) {
27
+ return command === "git" ? ["-c", "http.version=HTTP/1.1", ...args] : [...args];
28
+ }
29
+
30
+ export function isTransientNetworkFailure(result) {
31
+ if (result?.error) {
32
+ const code = String(result.error.code || "");
33
+ if (["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH", "ECONNREFUSED"].includes(code)) return true;
34
+ }
35
+ const detail = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}\n${result?.error?.message ?? ""}`;
36
+ return TRANSIENT_NETWORK_FAILURE.test(detail);
37
+ }
38
+
39
+ function sleepSync(milliseconds) {
40
+ if (milliseconds > 0) Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
41
+ }
42
+
43
+ function clampInteger(value, fallback, minimum, maximum) {
44
+ const parsed = Number.parseInt(String(value ?? ""), 10);
45
+ const number = Number.isFinite(parsed) ? parsed : fallback;
46
+ return Math.min(Math.max(number, minimum), maximum);
47
+ }