machine-bridge-mcp 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2 - 2026-07-10
4
+
5
+ ### Fixed
6
+
7
+ - Retry atomic JSON file replacement with bounded backoff for transient Windows sharing failures (`EPERM`, `EACCES`, `EBUSY`, and `ENOTEMPTY`). State, managed-job manager, and detached runner commits now use one shared implementation.
8
+ - Prevent an intermittent Windows `runner_failed` result caused by a transient status-file replacement failure after the same commit had passed the protected pull-request checks.
9
+ - Preserve immediate failure for non-transient filesystem errors and keep temporary-file cleanup behavior unchanged.
10
+ - Restore corrupt-state backup creation through the shared replacement primitive, add collision-resistant backup names, and verify that recovery preserves the original corrupt bytes.
11
+
12
+ ### Tests and diagnostics
13
+
14
+ - Add deterministic injected transient/non-transient atomic replacement tests.
15
+ - Run the full real-machine sandbox three consecutive times on Windows, exercising repeated detached runner, status, result, and finally-step commits.
16
+ - Include managed-job error and cleanup classifications in `full-test` output when a lifecycle check fails.
17
+
18
+ ## 0.6.1 - 2026-07-10
19
+
20
+ ### Full-profile contract and operator workflows
21
+
22
+ - Make named profiles canonical capability contracts. A stored `full` label is repaired to writes, shell execution, unrestricted paths, full parent environment, absolute path output, and the complete tool catalog; deliberate individual overrides remain `custom`. Advance the policy revision to 3 and expose the contract in `server_info` and `doctor`.
23
+ - Add `machine-mcp full-test`, a real local-machine acceptance suite using disposable directories. It verifies outside-workspace I/O, direct and shell execution, parent-environment inheritance, SSH key generation, sandbox `authorized_keys`, SSH client parsing, Google OS Login command availability, a non-mutating sudo probe, and detached finally cleanup without changing cloud or remote state.
24
+ - Add `machine-mcp resource generate-ssh-key NAME [PATH]` and the canonical-full-only MCP tool `generate_ssh_key_resource`. Both generate or reuse an Ed25519 pair locally, register the private file as a resource, and return only paths, modes, key type, and public fingerprint.
25
+
26
+ ### Logging
27
+
28
+ - Remove every ordinary per-tool event from default `info`/`warn` logs. Starts, successes, failures, cancellations, durations, slow calls, tool names, coarse outcome classes, and call correlation are now debug-only in both remote daemon and stdio transports.
29
+ - Keep default logs focused on deployment, connection, protocol, relay, service, and infrastructure health. Tool arguments, command text, inputs, outputs, and results remain omitted at all levels.
30
+
31
+ ### Security, correctness, and portability
32
+
33
+ - Verify that an existing SSH public key matches its private key and that the private key is usable non-interactively before reuse. Reject incomplete pairs and symbolic links, enforce owner-only private modes where supported, and never return private bytes.
34
+ - Fix the previously untested RSA key-generation argument order and add real RSA coverage.
35
+ - Centralize CLI and MCP key generation/registration in one locked state transaction. Roll back a newly created pair if state persistence fails, handle canonical path aliases, and reapply permissions after cross-filesystem installation.
36
+ - Validate the resource state layout before deriving its root and restrict the MCP key generator to the complete canonical `full` profile.
37
+ - Avoid reading user SSH configuration during `full-test`, canonicalize temporary path aliases, and report sudo/cloud prerequisites separately from core Machine Bridge capability.
38
+
39
+ ### Tests and documentation
40
+
41
+ - Add real Ed25519/RSA generation, reuse, mismatch, incomplete-pair, symlink, mode, private-content, CLI, stdio, Worker-policy, and real-machine full-profile regression coverage.
42
+ - Update logging, architecture, operations, client, managed-job, security, and testing documentation to match the canonical-full and debug-only per-tool event contracts.
43
+
3
44
  ## 0.6.0 - 2026-07-10
4
45
 
5
46
  ### Added
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. Version 0.5 migrates the exact pre-0.4 implicit-default shape to `full` once; explicit restrictive profiles and identified custom policies remain unchanged.
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.
36
36
 
37
37
  | Profile | File edits | Direct argv processes | Shell commands | Filesystem scope | Process environment |
38
38
  |---|---:|---:|---:|---|---|
@@ -152,6 +152,14 @@ machine-mcp doctor
152
152
 
153
153
  A successful `diagnose_runtime` response proves that request reached the local daemon. It then reports fixed probes for Machine Bridge policy, private filesystem access, direct process spawning, shell execution, managed-job storage, and registered resources. If the host blocks the tool call before any structured response, the server cannot diagnose that request because it never received it.
154
154
 
155
+ Run a real local acceptance test for the canonical `full` contract:
156
+
157
+ ```sh
158
+ machine-mcp full-test --workspace /path/to/project
159
+ ```
160
+
161
+ The test uses a temporary sandbox to perform an outside-workspace read/write, direct process, shell command, parent-environment inheritance, Ed25519 generation, temporary `authorized_keys` write, SSH client check, Google Cloud OS Login command availability check, non-mutating `sudo -n true` probe, and detached managed-job/finally lifecycle. It does not add a cloud key, contact a remote maintenance host, modify a user account, or retain the generated key.
162
+
155
163
  ## Managed jobs and local resources
156
164
 
157
165
  Long, remote, multi-step, or cleanup-sensitive work should not depend on a sequence of later MCP calls remaining available. `start_job` durably accepts ordered argv steps plus `finally_steps`, then launches an independent local runner. It continues after MCP disconnects or later host-side tool refusals.
@@ -166,6 +174,14 @@ machine-mcp resource add racknerd-key ~/.ssh/racknerd_ed25519
166
174
  machine-mcp resource list
167
175
  ```
168
176
 
177
+ Generate an Ed25519 key and register its private file in one operation:
178
+
179
+ ```sh
180
+ machine-mcp resource generate-ssh-key racknerd-key ~/.ssh/ai-racknerd-maint-ed25519
181
+ ```
182
+
183
+ Under canonical `full`, an authorized MCP host can invoke `generate_ssh_key_resource` with the same semantics. The tool is idempotent, verifies that existing public/private files match, enforces local file modes where supported, and returns only paths, public fingerprint, key type, and registration status—not private key bytes.
184
+
169
185
  A job refers to the alias rather than the value:
170
186
 
171
187
  ```json
@@ -242,6 +258,7 @@ Repository-configured external diff, text conversion, and filesystem-monitor hel
242
258
 
243
259
  - `diagnose_runtime` — fixed layered probes; no user-controlled command input
244
260
  - `list_local_resources` — aliases and validation status without paths or values
261
+ - `generate_ssh_key_resource` — canonical-full-only Ed25519 generation and private-file resource registration without returning private content
245
262
  - `stage_job` — persist a validated plan for later local approval without executing it
246
263
  - `start_job` — detached ordered argv steps, private temporary files, and finally steps
247
264
  - `list_jobs`
@@ -296,7 +313,7 @@ Remote mode supports:
296
313
  - Linux `systemd --user`, with best-effort lingering;
297
314
  - Windows Scheduled Task at logon.
298
315
 
299
- 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. Background services run at log level `warn`: failures and connection problems are retained, while routine successful tool calls are omitted. Logs are owner-only where supported and bounded by tail trimming.
316
+ 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. 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.
300
317
 
301
318
  ## Secret rotation
302
319
 
@@ -317,7 +334,7 @@ Default state roots:
317
334
 
318
335
  State/config writes use owner-only temporary files, flushes, and atomic rename. Malformed state is retained as a bounded corrupt backup before reconstruction. Resource source paths are redacted from `status` output. Active managed-job plans are owner-only and are deleted after a terminal result; bounded redacted results are retained temporarily. Uninstall validates markers, canonical paths, active locks, workspace/source exclusions, and known contents before recursive deletion.
319
336
 
320
- Default foreground logs show startup/deployment transitions, relay connectivity, warnings/errors, and successful calls that exceed 30 seconds. Routine successful tool calls and correlation IDs appear only at `--log-level debug` or `--verbose`. Background services use `warn`. Log messages and structured fields are bounded, secret-like keys and known token formats are redacted, and tool arguments/results are not written. See [docs/LOGGING.md](docs/LOGGING.md) and [docs/OPERATIONS.md](docs/OPERATIONS.md).
337
+ Default foreground logs show startup/deployment transitions, relay connectivity, and infrastructure warnings/errors. Every per-tool event—including success, failure, cancellation, and slow-call timing—appears only at `--log-level debug` or `--verbose`. Background services use `warn`, so ordinary tool outcomes never fill daemon logs. Log messages and structured fields are bounded, secret-like keys and known token formats are redacted, and tool arguments/results are not written. See [docs/LOGGING.md](docs/LOGGING.md) and [docs/OPERATIONS.md](docs/OPERATIONS.md).
321
338
 
322
339
  ## Development and verification
323
340
 
@@ -330,7 +347,7 @@ npm audit --omit=dev --audit-level=high
330
347
  npm pack --dry-run
331
348
  ```
332
349
 
333
- `npm run check` covers generated Worker types, TypeScript, JavaScript syntax, the shared tool catalog, local path/write/process/state/log/service invariants, managed-job detachment/resource redaction/finally/cancellation/recovery, a live stdio MCP flow, and a live local OAuth/Worker/WebSocket/MCP flow. GitHub Actions runs the suite on Linux, macOS, and Windows with supported Node versions.
350
+ `npm run check` covers generated Worker types, TypeScript, JavaScript syntax, the shared tool catalog, local path/write/process/state/log/service invariants, Ed25519/RSA generation and key-pair validation, real-machine `full` sandbox acceptance, managed-job detachment/resource redaction/finally/cancellation/recovery, a live stdio MCP flow, and a live local OAuth/Worker/WebSocket/MCP flow. GitHub Actions runs the suite on Linux, macOS, and Windows with supported Node versions.
334
351
 
335
352
  See [docs/MANAGED_JOBS.md](docs/MANAGED_JOBS.md), [docs/TESTING.md](docs/TESTING.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), and [SECURITY.md](SECURITY.md).
336
353
 
package/SECURITY.md CHANGED
@@ -32,9 +32,9 @@ The Worker is a remote authentication and relay boundary. The local runtime is t
32
32
 
33
33
  ## Profiles are capability sets, not sandboxes
34
34
 
35
- The default for newly selected workspaces is `full`, which prioritizes ease of use over least privilege.
35
+ The default for newly selected workspaces is `full`, which prioritizes ease of use over least privilege. Named profiles are canonical capability contracts. A stored `full` label is repaired on load to the complete maximum-permission field set and tool catalog; a deliberate per-capability override is represented as `custom`, not as a partially restricted `full`.
36
36
 
37
- - `full` exposes all tools, unrestricted direct filesystem paths, absolute path output, and the complete parent process environment.
37
+ - `full` exposes all tools, unrestricted direct filesystem paths, absolute path output, shell execution, and the complete parent process environment.
38
38
  - `agent` exposes file mutation, direct argv execution, and process sessions while keeping direct filesystem tools workspace-confined and the process environment isolated.
39
39
  - `edit` exposes read and mutation tools without process execution.
40
40
  - `review` exposes read-only workspace/Git/image tools.
@@ -57,6 +57,12 @@ Maximum local policy does not bypass Unix permissions, Windows ACLs, macOS TCC/S
57
57
 
58
58
  Processes are not confined by the filesystem-tool resolver. They can access paths, networks, processes, credential stores, and devices available to the local user.
59
59
 
60
+ ## Full-profile verification
61
+
62
+ `machine-mcp full-test` performs real local operations inside disposable temporary directories: unrestricted file access, direct and shell processes, inherited environment, SSH key generation/matching, a sandbox `authorized_keys` write, SSH client parsing, Google Cloud OS Login command discovery, a non-mutating sudo availability probe, and detached job cleanup. It makes no cloud or remote-server change and cannot prove that an MCP host will deliver a future request. The command verifies the Machine Bridge and local-machine portion of the authority chain only.
63
+
64
+ A canonical `full` profile guarantees that Machine Bridge itself will not reject a catalogued tool because of its local profile, path scope, environment mode, or shell mode. It cannot override the MCP host/connector, operating-system access controls, endpoint-security policy, remote authentication, cloud IAM, `sudo`, or service-side authorization.
65
+
60
66
  ## Mutation integrity
61
67
 
62
68
  Writes are bounded, reject symbolic-link/non-regular destinations, use same-directory staging, and commit atomically per file. Create-only commit fails if a concurrent destination appears. Expected hashes and exact edits reduce accidental stale overwrites.
@@ -75,7 +81,7 @@ The default `full` profile passes the complete parent environment. Narrower prof
75
81
 
76
82
  ## Local resources and managed jobs
77
83
 
78
- Local resources are registered only through the operator-controlled CLI. State stores canonical paths and metadata, not file contents. Unix-like registration rejects group/other-readable files unless explicitly overridden. Portable mode checks do not fully describe Windows ACLs or extended Unix ACLs; the operator remains responsible for platform permissions.
84
+ Local resources are registered through the operator-controlled CLI or, only under canonical `full`, through `generate_ssh_key_resource`. State stores canonical paths and metadata, not file contents. The SSH generator creates or reuses an unencrypted automation key, verifies that the public and private files match, rejects symbolic links/incomplete pairs, applies `0600`/`0644` modes where supported, and returns no private bytes. Unix-like registration rejects group/other-readable private resource files unless explicitly overridden. Portable mode checks do not fully describe Windows ACLs or extended Unix ACLs; the operator remains responsible for platform permissions.
79
85
 
80
86
  At job acceptance, referenced resources are bounded and hashed. The detached runner reopens and verifies each hash before copying it to a private `0600` runtime file. Resource copies are removed after the finally phase. A changed or unavailable resource fails closed.
81
87
 
@@ -131,7 +137,7 @@ Process sessions are interactive and intentionally die with runtime disconnect/r
131
137
 
132
138
  ## Logs and privacy
133
139
 
134
- Default operational logs record startup/connection transitions, failed calls, slow-call duration, and coarse error classes. Routine successful calls and shortened correlation IDs are debug-only. Tool arguments, command text, stdin, file/patch contents, and outputs are omitted. Messages and fields are bounded; unexpected daemon and Worker exceptions are reduced to error classes. Git author email is omitted from `git_log` unless explicitly requested.
140
+ Default operational logs record startup/deployment, relay, protocol, service, and infrastructure transitions. Every ordinary per-tool event—start, success, failure, cancellation, and duration—is debug-only. Tool arguments, command text, stdin, file/patch contents, and outputs are omitted. Messages and fields are bounded; unexpected daemon and Worker infrastructure exceptions are reduced to error classes. Git author email is omitted from `git_log` unless explicitly requested.
135
141
 
136
142
  No logging policy can prevent data from being returned to a client that explicitly invokes an enabled tool. The Worker necessarily relays remote tool arguments and results; this is not end-to-end encryption against the user's Cloudflare execution environment. Managed-job result files may contain remote command output and are owner-only local data, not operational logs.
137
143
 
@@ -58,6 +58,14 @@ Running managed jobs do not belong to an MCP socket or daemon call ID and snapsh
58
58
 
59
59
  Local resource registrations remain in owner-only state and are reloaded for every new job. MCP-visible resource inventory includes aliases and validation status but never source paths, hashes, or contents.
60
60
 
61
+ ### Canonical policy contracts
62
+
63
+ 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.
64
+
65
+ 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.
66
+
67
+ `full-test` constructs a local runtime with canonical full policy and executes real operations in disposable directories. It is an acceptance test for the local implementation, not a probe that changes remote systems or bypasses host policy.
68
+
61
69
  ### Stdio MCP server
62
70
 
63
71
  The stdio server implements newline-delimited JSON-RPC over stdin/stdout. It negotiates supported MCP versions, advertises policy-filtered tools, returns text plus structured content, supports native image content, maps cancellation notifications to runtime call IDs, and sends level-filtered logs only to stderr.
@@ -175,7 +183,7 @@ Pending calls retain their originating socket reference. A stale socket cannot c
175
183
 
176
184
  ## Persistence
177
185
 
178
- Local state and global config are owner-only, versioned, size-bounded, and written through temporary files, flushes, and atomic rename. Reads reject symbolic links and use no-follow descriptors where supported. Malformed or oversized state becomes a bounded-count `.corrupt-*` backup. Resource paths are omitted from redacted status output. Custom roots are adopted only when empty or recognizable as legacy Machine Bridge state.
186
+ Local state and global config are owner-only, versioned, size-bounded, and written through temporary files, flushes, and atomic rename. State, managed-job manager, and detached runner commits share a bounded retrying atomic-replace primitive for transient Windows `EPERM`, `EACCES`, `EBUSY`, and `ENOTEMPTY` sharing failures. Reads reject symbolic links and use no-follow descriptors where supported. Malformed or oversized state becomes a bounded-count `.corrupt-*` backup. Resource paths are omitted from redacted status output. Custom roots are adopted only when empty or recognizable as legacy Machine Bridge state.
179
187
 
180
188
  Active managed jobs persist an owner-only plan, status, runner PID, and bounded runner diagnostics. Terminal jobs delete the full plan and retain only bounded status/redacted results for up to seven days. This balances crash cleanup with minimization of scripts, stdin, argv, environment overrides, and resource source paths.
181
189
 
@@ -187,7 +195,7 @@ OAuth metadata is pruned on access. Expired codes/tokens, old throttling records
187
195
 
188
196
  Public health exposes only server identity and version. Authenticated `server_info` exposes bounded runtime status, policy origin/revision, managed-job counts, and resource alias names without paths or values. `diagnose_runtime` runs fixed local probes and explicitly reports that its own request reached the daemon.
189
197
 
190
- Foreground logging defaults to `info`; autostart uses `warn`. Routine successful calls and shortened random correlation IDs are debug-only. `info` retains deployment/connection transitions and successful calls slower than 30 seconds. Failures log tool name, duration, and coarse error class without arguments or outputs. Unexpected local and Worker errors are reduced to classes in normal logs. Messages, strings, arrays, object depth/key counts, and serialized fields are bounded.
198
+ Foreground logging defaults to `info`; autostart uses `warn`. All per-tool starts, successes, failures, cancellations, durations, and shortened random correlation IDs are debug-only. Normal logs retain deployment, connection, protocol, service, and infrastructure events rather than duplicating MCP tool outcomes. Unexpected local and Worker infrastructure errors are reduced to classes. Messages, strings, arrays, object depth/key counts, and serialized fields are bounded.
191
199
 
192
200
  Cloudflare sampling is size control rather than an audit log. The project intentionally does not claim complete forensic logging. See [LOGGING.md](LOGGING.md).
193
201
 
package/docs/CLIENTS.md CHANGED
@@ -118,13 +118,23 @@ Enter the printed `/mcp` URL in the remote MCP connector. During OAuth authoriza
118
118
 
119
119
  ## Profile guidance
120
120
 
121
- - `full` is the default and prioritizes immediate usability. It exposes shell execution, unrestricted direct filesystem paths, absolute path output, and the full parent environment.
121
+ - `full` is the default and prioritizes immediate usability. It is a canonical contract exposing every catalog tool, shell execution, unrestricted direct filesystem paths, absolute path output, and the full parent environment. Any individual narrowing is represented as `custom`.
122
122
  - `agent` retains file mutation and direct process execution but removes shell parsing, confines direct filesystem tools to the workspace, and isolates the process environment.
123
123
  - `edit` permits deterministic file mutation without process execution.
124
124
  - `review` is read-only and workspace-confined.
125
125
 
126
126
  A client configuration is an authorization decision. Anyone who controls an authorized host can invoke every tool exposed by the selected profile.
127
127
 
128
+ Before connecting a host, verify the local implementation directly:
129
+
130
+ ```sh
131
+ machine-mcp full-test --workspace /path/to/project
132
+ ```
133
+
134
+ A passing result proves that Machine Bridge and the local OS allowed its temporary file/process/shell/key/job probes at that time. It does not prove that a hosted MCP connector will deliver a later request or that a cloud/remote account will authorize it.
135
+
136
+ For SSH automation, prefer `generate_ssh_key_resource` under canonical full, or `machine-mcp resource generate-ssh-key` from the terminal. The private key remains a local resource; only paths and the public fingerprint are returned. Installation of the public key into Google OS Login or a remote `authorized_keys` file remains an explicit external action.
137
+
128
138
  ## Host-side safety rules
129
139
 
130
140
  The local `full` profile controls Machine Bridge's own tool catalog, path resolver, path display, process environment, and shell availability. It does not control the MCP host's model policy, approval UI, connector gateway, or platform execution filters.
package/docs/LOGGING.md CHANGED
@@ -6,8 +6,8 @@ Operational logs should answer four questions without becoming a transcript of u
6
6
 
7
7
  1. Did deployment and relay connection succeed?
8
8
  2. Is the daemon reconnecting, superseded, or unhealthy?
9
- 3. Did a tool call fail, time out, or become unusually slow?
10
- 4. Which component and coarse error class should be investigated?
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?
11
11
 
12
12
  Logs are not a command history, content audit trail, or substitute for MCP client approvals.
13
13
 
@@ -26,24 +26,15 @@ The default foreground level is `info`. Platform autostart services use `warn`.
26
26
  | Level | Intended events |
27
27
  |---|---|
28
28
  | `error` | Broken request handlers, relay transport errors, unrecoverable local failures |
29
- | `warn` | Failed tool calls, disconnections, malformed/oversized relay messages, superseded daemons |
30
- | `info` | Deployment/startup transitions, successful relay connection, successful calls slower than 30 seconds |
31
- | `debug` | Routine successful calls, shortened correlation IDs, reconnect timing, cancellation correlation |
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 |
32
32
 
33
- Routine successful calls are deliberately absent from `info`. A normal sequence of hundreds of tool invocations should not fill the user's terminal or service logs.
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.
34
34
 
35
35
  ## Event fields
36
36
 
37
- Default warning/error and slow-call events may include:
38
-
39
- - component;
40
- - tool name;
41
- - duration in milliseconds;
42
- - coarse error class;
43
- - relay close code;
44
- - bounded reconnect delay.
45
-
46
- Shortened random call identifiers are debug-only. They are useful only for correlating adjacent local events and are not stable audit identifiers.
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.
47
38
 
48
39
  The implementation intentionally omits:
49
40
 
@@ -54,7 +45,7 @@ The implementation intentionally omits:
54
45
  - OAuth request bodies;
55
46
  - connection passwords, daemon secrets, authorization codes, and access tokens.
56
47
 
57
- Unexpected daemon and Worker 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 normal operational logs.
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.
58
49
 
59
50
  ## Bounding and redaction
60
51
 
@@ -71,7 +62,7 @@ logs/daemon.out.log
71
62
  logs/daemon.err.log
72
63
  ```
73
64
 
74
- 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`, normal successful tool traffic should not cause sustained log growth.
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.
75
66
 
76
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.
77
68
 
@@ -64,6 +64,14 @@ machine-mcp resource remove racknerd-key
64
64
 
65
65
  Removing an alias affects new jobs. A job already accepted has an owner-only resource snapshot specification and continues independently.
66
66
 
67
+ Generate a new Ed25519 resource rather than exposing a private-key creation command through a model-generated shell string:
68
+
69
+ ```sh
70
+ machine-mcp resource generate-ssh-key racknerd-key ~/.ssh/ai-racknerd-maint-ed25519
71
+ ```
72
+
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
+
67
75
  ## Resource injection modes
68
76
 
69
77
  A managed step is argv-based and supports three local resource modes:
@@ -30,10 +30,10 @@ Remote autostart logs are stored under the state root in `logs/daemon.out.log` a
30
30
  Logging is level-based:
31
31
 
32
32
  ```text
33
- error unrecoverable local/transport failures
34
- warn failed calls, disconnects, malformed relay events
35
- info startup/deploy/connect transitions and calls slower than 30 seconds
36
- debug routine successful calls, shortened correlation IDs, cancellation/reconnect details
33
+ error unrecoverable local/transport/service failures
34
+ warn relay disconnect/send failures, malformed relay events, supersession and service problems
35
+ info startup/deploy/connect transitions
36
+ debug all per-tool starts/successes/failures/cancellations/timing, correlation and reconnect details
37
37
  ```
38
38
 
39
39
  Foreground mode defaults to `info`; autostart uses `warn`. Use `--verbose` or `--log-level debug` only for diagnosis. `--quiet` is an alias for `--log-level error`.
@@ -42,6 +42,24 @@ Normal logs intentionally omit tool arguments, file/patch/image content, command
42
42
 
43
43
  See [LOGGING.md](LOGGING.md) for the event contract and MCP-host boundary. Cloudflare observability is sampled and is not a complete audit log.
44
44
 
45
+ ## Full capability acceptance
46
+
47
+ Run:
48
+
49
+ ```sh
50
+ machine-mcp full-test --workspace /path/to/project
51
+ ```
52
+
53
+ The command uses disposable local directories and performs actual read/write, process, shell, environment, SSH-key, sandbox authorized-key, SSH-client, managed-job and finally-cleanup operations. It also checks whether the Google Cloud OS Login command exists and whether `sudo -n true` is currently permitted, without changing either system. `ok` covers core Machine Bridge functionality; `operator_workflow_ready` additionally reports the local SSH/Google CLI prerequisites. No external cloud, account, or server change is made.
54
+
55
+ Generate and register an operator key locally:
56
+
57
+ ```sh
58
+ machine-mcp resource generate-ssh-key NAME [PRIVATE_KEY_PATH]
59
+ ```
60
+
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.
62
+
45
63
  ## Managed jobs and local recovery
46
64
 
47
65
  Register local-only resources from the terminal:
@@ -108,9 +126,9 @@ Defense-in-depth limits include:
108
126
 
109
127
  ## Upgrade behavior
110
128
 
111
- Version 0.5 records policy origin and revision. A state entry matching the exact legacy implicit-default shape—write enabled, shell enabled, workspace-confined paths, isolated environment, and relative output—is migrated once to the current `full` default. Explicit named profiles and identified custom policies are preserved.
129
+ 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.
112
130
 
113
- `full` enables writes, direct processes, process sessions, shell execution, unrestricted direct filesystem paths, absolute path output, and the complete parent environment. It does not override operating-system access controls or independent MCP-host/platform policy.
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.
114
132
 
115
133
  Inspect effective policy with:
116
134
 
package/docs/TESTING.md CHANGED
@@ -25,11 +25,15 @@ The suite includes:
25
25
  - one-shot timeout, descendant termination, cancellation, and process-session interaction;
26
26
  - layered fixed runtime diagnostics for filesystem, direct process, shell, managed-job storage, and resource availability;
27
27
  - local resource CLI registration, permission checks, dynamic reload, state-path redaction, and content non-disclosure;
28
+ - real Ed25519 and RSA generation, idempotent reuse, public/private correspondence, mode enforcement, incomplete/mismatched/symlink rejection, and private-content non-disclosure;
29
+ - real-machine canonical-full sandbox acceptance for outside-workspace I/O, direct/shell execution, full environment inheritance, SSH prerequisites, temporary authorized-key writing, and detached cleanup without external state changes;
30
+ - deterministic injected atomic-replace failures and repeated Windows full-sandbox runs to catch transient file-sharing races;
31
+ - canonical named-profile repair and full-only tool exposure parity between local and Worker policy filters;
28
32
  - managed-job staging/local approval/cancel-before-start, detachment, job-scoped temporary files, resource hash verification/redaction, discard capture, finally execution, cancellation escalation, plan scrubbing, and dead-runner recovery;
29
33
  - daemon/startup locking and state corruption recovery;
30
34
  - guarded state-root removal, schema migration, policy-origin persistence, and legacy implicit-default migration;
31
35
  - no filename-based sensitive-file denial under unrestricted policy;
32
- - log redaction, control-character handling, message/field bounds, default success-log suppression, and service warning-level configuration;
36
+ - log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, and service warning-level configuration;
33
37
  - CLI parsing, policy profiles, and client configuration boundaries;
34
38
  - live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
35
39
  - live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, dynamic tool advertisement, rich content, daemon replacement, and cancellation.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.6.0",
4
- "description": "Cross-client MCP bridge for policy-controlled local files, Git, images, patches, and processes over stdio or OAuth-protected remote relay.",
3
+ "version": "0.6.2",
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": {
@@ -39,8 +39,8 @@
39
39
  "prepublishOnly": "npm run check && npm run version:check && npm run release:check",
40
40
  "worker:types": "wrangler types src/worker/worker-configuration.d.ts",
41
41
  "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/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",
43
- "check": "npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
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",
44
44
  "worker:dry-run": "wrangler deploy --dry-run",
45
45
  "worker:integration-test": "node tests/worker-integration-test.mjs",
46
46
  "release:check": "node scripts/github-release.mjs --check",
@@ -48,7 +48,10 @@
48
48
  "release:backfill": "node scripts/github-release.mjs --backfill",
49
49
  "stdio:integration-test": "node tests/stdio-integration-test.mjs",
50
50
  "catalog:test": "node tests/catalog-test.mjs",
51
- "managed-jobs:test": "node tests/managed-jobs-test.mjs"
51
+ "managed-jobs:test": "node tests/managed-jobs-test.mjs",
52
+ "ssh-key:test": "node tests/ssh-key-test.mjs",
53
+ "full-access:test": "node tests/full-access-test.mjs",
54
+ "atomic-fs:test": "node tests/atomic-fs-test.mjs"
52
55
  },
53
56
  "dependencies": {
54
57
  "wrangler": "4.110.0",
@@ -68,7 +71,10 @@
68
71
  "stdio",
69
72
  "coding-agent",
70
73
  "oauth",
71
- "cross-platform"
74
+ "cross-platform",
75
+ "ssh",
76
+ "automation",
77
+ "managed-jobs"
72
78
  ],
73
79
  "repository": {
74
80
  "type": "git",
@@ -0,0 +1,37 @@
1
+ import { renameSync } from "node:fs";
2
+
3
+ const TRANSIENT_REPLACE_ERRORS = new Set(["EACCES", "EBUSY", "EPERM", "ENOTEMPTY"]);
4
+ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
5
+
6
+ export function replaceFileSync(source, target, options = {}) {
7
+ const rename = typeof options.rename === "function" ? options.rename : renameSync;
8
+ const attempts = clampInteger(options.attempts, 8, 1, 32);
9
+ const baseDelayMs = clampInteger(options.baseDelayMs, 15, 0, 1000);
10
+ let lastError;
11
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
12
+ try {
13
+ rename(source, target);
14
+ return { attempts: attempt };
15
+ } catch (error) {
16
+ lastError = error;
17
+ if (!isTransientReplaceError(error) || attempt === attempts) throw error;
18
+ sleepSync(Math.min(baseDelayMs * attempt, 250));
19
+ }
20
+ }
21
+ throw lastError || new Error("atomic file replacement failed");
22
+ }
23
+
24
+ export function isTransientReplaceError(error) {
25
+ return TRANSIENT_REPLACE_ERRORS.has(String(error?.code || ""));
26
+ }
27
+
28
+ function sleepSync(milliseconds) {
29
+ if (milliseconds <= 0) return;
30
+ Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
31
+ }
32
+
33
+ function clampInteger(value, fallback, minimum, maximum) {
34
+ const parsed = Number.parseInt(String(value ?? ""), 10);
35
+ const number = Number.isFinite(parsed) ? parsed : fallback;
36
+ return Math.min(Math.max(number, minimum), maximum);
37
+ }
package/src/local/cli.mjs CHANGED
@@ -5,10 +5,12 @@ import process from "node:process";
5
5
  import readline from "node:readline/promises";
6
6
  import { LocalDaemon } from "./daemon.mjs";
7
7
  import { runStdioServer } from "./stdio.mjs";
8
- import { DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile } from "./tools.mjs";
8
+ import { assertCanonicalFullPolicy, DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile, toolsForPolicy } from "./tools.mjs";
9
9
  import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
10
10
  import { activeManagedJobs, inspectResourceFile, loadManagedJobPlan, ManagedJobManager, publicResourceRegistry, validateResourceName } from "./managed-jobs.mjs";
11
11
  import { runWrangler } from "./shell.mjs";
12
+ import { generateRegisteredSshKey } from "./resource-operations.mjs";
13
+ import { runFullAccessTest } from "./full-access-test.mjs";
12
14
  import {
13
15
  acquireDaemonLock,
14
16
  acquireStartupLock,
@@ -60,6 +62,7 @@ export async function main(argv = process.argv.slice(2)) {
60
62
  case "client-config": return clientConfigCommand(args);
61
63
  case "status": return statusCommand(args);
62
64
  case "doctor": return doctorCommand(args);
65
+ case "full-test": return fullTestCommand(args);
63
66
  case "workspace": return workspaceCommand(args);
64
67
  case "service":
65
68
  case "autostart": return serviceCommand(args);
@@ -84,6 +87,7 @@ const COMMAND_OPTIONS = {
84
87
  "client-config": new Set(["workspace", "stateDir", "profile", "client", "json"]),
85
88
  status: new Set(["workspace", "stateDir"]),
86
89
  doctor: new Set(["workspace", "stateDir"]),
90
+ "full-test": new Set(["workspace", "stateDir", "json"]),
87
91
  "rotate-secrets": new Set(["workspace", "stateDir", "workerName", "noPrintCredentials", "printMcpCredentials", "printCredentials", "quiet"]),
88
92
  workspace: new Set(["workspace", "stateDir"]),
89
93
  service: new Set(["workspace", "stateDir", "quiet"]),
@@ -123,7 +127,7 @@ function toKebab(value) {
123
127
 
124
128
  export function validatePositionals(command, args) {
125
129
  const count = args._.length;
126
- if (["start", "stdio", "status", "doctor", "rotate-secrets"].includes(command)) {
130
+ if (["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"].includes(command)) {
127
131
  if (count > 1) throw new Error(`${command} accepts at most one positional workspace path`);
128
132
  if (args.workspace && count) throw new Error("workspace path was provided both positionally and with --workspace");
129
133
  return;
@@ -148,7 +152,7 @@ export function validatePositionals(command, args) {
148
152
  }
149
153
  if (command === "resource") {
150
154
  const action = String(args._[0] || "list");
151
- const max = action === "add" ? 3 : action === "remove" || action === "check" ? 2 : 1;
155
+ const max = action === "add" || action === "generate-ssh-key" ? 3 : action === "remove" || action === "check" ? 2 : 1;
152
156
  if (count > max) throw new Error(`resource ${action} received too many positional arguments`);
153
157
  return;
154
158
  }
@@ -510,6 +514,42 @@ async function resourceCommand(args) {
510
514
  return;
511
515
  }
512
516
 
517
+ if (action === "generate-ssh-key") {
518
+ const name = validateResourceName(args._[1]);
519
+ const home = process.env.HOME || process.env.USERPROFILE;
520
+ if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
521
+ const requestedPath = args._[2] ? expandHome(args._[2]) : join(home, ".ssh", `machine-mcp-${name}-ed25519`);
522
+ const key = await generateRegisteredSshKey({
523
+ workspace,
524
+ stateDir: args.stateDir,
525
+ name,
526
+ targetPath: requestedPath,
527
+ comment: `machine-mcp:${name}`,
528
+ });
529
+ const result = {
530
+ name: key.name,
531
+ created: key.created,
532
+ private_key_path: key.privateKeyPath,
533
+ public_key_path: key.publicKeyPath,
534
+ fingerprint: key.fingerprint,
535
+ key_type: key.keyType,
536
+ private_mode: key.privateMode,
537
+ public_mode: key.publicMode,
538
+ private_key_content_exposed: key.privateKeyContentExposed,
539
+ registered: key.registered,
540
+ available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
541
+ };
542
+ if (args.json) console.log(JSON.stringify(result, null, 2));
543
+ else {
544
+ console.log(`${key.created ? "Generated and registered" : "Reused and registered"} SSH key resource: ${name}`);
545
+ console.log(`Private key: ${key.privateKeyPath}`);
546
+ console.log(`Public key: ${key.publicKeyPath}`);
547
+ console.log(`Fingerprint: ${key.fingerprint}`);
548
+ console.log("Private key content was not printed or sent through MCP.");
549
+ }
550
+ return;
551
+ }
552
+
513
553
  if (action === "remove") {
514
554
  const name = validateResourceName(args._[1]);
515
555
  const lock = acquireStartupLock(state);
@@ -884,6 +924,14 @@ async function doctorCommand(args) {
884
924
  const storedPolicyOrigin = state.policy?.origin;
885
925
  state.policy = resolvePolicy({}, state.policy);
886
926
  checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
927
+ if (state.policy.profile === "full") {
928
+ try {
929
+ assertCanonicalFullPolicy(state.policy);
930
+ checks.push({ name: "full-policy-contract", ok: true, detail: `${toolsForPolicy(state.policy).length} tools exposed` });
931
+ } catch (error) {
932
+ checks.push({ name: "full-policy-contract", ok: false, detail: sanitizeLines(error?.message || error) });
933
+ }
934
+ }
887
935
  const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
888
936
  checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
889
937
  const diagnosticRuntime = new LocalDaemon({
@@ -917,6 +965,15 @@ async function doctorCommand(args) {
917
965
  }, null, 2));
918
966
  }
919
967
 
968
+ async function fullTestCommand(args) {
969
+ const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
970
+ const state = loadState(workspace, { stateDir: args.stateDir });
971
+ const policy = resolvePolicy({}, state.policy);
972
+ const result = await runFullAccessTest({ workspace, policy });
973
+ console.log(JSON.stringify(result, null, 2));
974
+ if (!result.ok) process.exitCode = 1;
975
+ }
976
+
920
977
  async function rotateSecretsCommand(args) {
921
978
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
922
979
  const state = loadState(workspace, { stateDir: args.stateDir });
@@ -1188,7 +1245,10 @@ Commands:
1188
1245
  service uninstall Remove only the autostart entry
1189
1246
  status Print redacted local profile state and Worker health
1190
1247
  doctor Check Node, Wrangler, Cloudflare login, Worker health
1248
+ full-test Run real local full-profile capability tests in a temporary sandbox
1191
1249
  rotate-secrets Rotate MCP password and daemon secret in local state
1250
+ resource generate-ssh-key NAME [PATH]
1251
+ Generate/reuse an Ed25519 key locally and register its private file by alias
1192
1252
  uninstall Delete known Worker(s), remove autostart and local state
1193
1253
 
1194
1254
  Start options: