machine-bridge-mcp 0.5.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 +68 -0
- package/README.md +113 -8
- package/SECURITY.md +42 -6
- package/docs/ARCHITECTURE.md +50 -9
- package/docs/CLIENTS.md +24 -3
- package/docs/LOGGING.md +12 -19
- package/docs/MANAGED_JOBS.md +282 -0
- package/docs/OPERATIONS.md +79 -10
- package/docs/TESTING.md +11 -2
- package/package.json +13 -6
- package/src/local/atomic-fs.mjs +37 -0
- package/src/local/cli.mjs +254 -7
- package/src/local/daemon.mjs +168 -6
- package/src/local/full-access-test.mjs +206 -0
- package/src/local/job-runner.mjs +471 -0
- package/src/local/managed-jobs.mjs +855 -0
- package/src/local/resource-operations.mjs +66 -0
- package/src/local/ssh-key.mjs +125 -0
- package/src/local/state.mjs +56 -12
- package/src/local/stdio.mjs +4 -6
- package/src/local/tools.mjs +37 -3
- package/src/shared/server-metadata.json +10 -1
- package/src/shared/tool-catalog.json +531 -0
- package/src/worker/index.ts +7 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Managed jobs and local resources
|
|
2
|
+
|
|
3
|
+
## Problem addressed
|
|
4
|
+
|
|
5
|
+
An MCP conversation is not a durable execution coordinator. A host or connector may reject a later tool call, an approval may be withdrawn, the network may disconnect, or local endpoint-security software may begin denying process creation. A sequence such as:
|
|
6
|
+
|
|
7
|
+
1. create a helper script;
|
|
8
|
+
2. connect to a remote server;
|
|
9
|
+
3. repair the service;
|
|
10
|
+
4. remove the helper;
|
|
11
|
+
|
|
12
|
+
can therefore stop after step 2 or 3 and leave local or remote temporary state behind.
|
|
13
|
+
|
|
14
|
+
Machine Bridge managed jobs reduce this failure mode by accepting the complete execution and cleanup plan in one call. After acceptance, an independent local runner owns the lifecycle. It does not depend on the MCP socket remaining connected.
|
|
15
|
+
|
|
16
|
+
This mechanism does **not** bypass host, operating-system, or endpoint-security policy. A job snapshots its execution policy/environment at acceptance; later profile changes affect new jobs, while accepted jobs continue until completion or explicit cancellation. The initial `start_job` request must still be permitted, and every local child process remains subject to local security controls.
|
|
17
|
+
|
|
18
|
+
## Diagnose the blocking layer
|
|
19
|
+
|
|
20
|
+
Use the MCP tool:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
diagnose_runtime
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or the local terminal:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
machine-mcp doctor
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Interpretation:
|
|
33
|
+
|
|
34
|
+
| Observation | Likely boundary |
|
|
35
|
+
|---|---|
|
|
36
|
+
| The tool call is rejected before any structured response | MCP host, connector gateway, approval system, or transport |
|
|
37
|
+
| `diagnose_runtime` responds, but `local-process-spawn` fails | Local OS permissions, endpoint security, executable policy, or broken runtime |
|
|
38
|
+
| Process spawn passes but `local-shell` fails | Shell configuration or shell-specific local policy |
|
|
39
|
+
| Managed-job storage fails | State-root permissions, disk, filesystem policy, or endpoint security |
|
|
40
|
+
| A job was accepted and later MCP calls are rejected | The detached job continues; inspect it through local CLI |
|
|
41
|
+
|
|
42
|
+
A successful diagnostic response proves that particular request reached the local daemon. It cannot retrospectively identify a request that the host blocked before delivery.
|
|
43
|
+
|
|
44
|
+
## Register local resources
|
|
45
|
+
|
|
46
|
+
Secrets and other local-only files should be registered from the user's terminal, not read into the model context:
|
|
47
|
+
|
|
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
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
Unix-like systems reject group/other-readable resource files by default. `--allow-insecure-permissions` is an explicit override. Windows ACLs and extended Unix ACLs cannot be fully represented by the portable mode check, so operators must still inspect platform permissions.
|
|
58
|
+
|
|
59
|
+
Remove an alias with:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
machine-mcp resource remove racknerd-key
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Removing an alias affects new jobs. A job already accepted has an owner-only resource snapshot specification and continues independently.
|
|
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
|
+
|
|
75
|
+
## Resource injection modes
|
|
76
|
+
|
|
77
|
+
A managed step is argv-based and supports three local resource modes:
|
|
78
|
+
|
|
79
|
+
### File-path placeholder
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"argv": ["ssh", "-i", "{{resource:racknerd-key}}", "root@example", "true"]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The runner copies the resource into the private job runtime with mode `0600`, substitutes that copy's path, and removes the runtime after cleanup.
|
|
88
|
+
|
|
89
|
+
### Standard input
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"argv": ["some-program", "--password-stdin"],
|
|
94
|
+
"stdin_resource": "service-password",
|
|
95
|
+
"capture_output": "discard"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Environment variable
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"argv": ["some-program"],
|
|
104
|
+
"env_resources": {
|
|
105
|
+
"SERVICE_TOKEN": "service-token"
|
|
106
|
+
},
|
|
107
|
+
"capture_output": "discard"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Environment injection is convenient but may expose a value to same-user process inspection or child processes. Prefer a file path or stdin when the target program supports it.
|
|
112
|
+
|
|
113
|
+
At job acceptance, referenced resources are reopened, bounded, hashed, and recorded in the owner-only active plan. The runner reopens and verifies the hash before copying. A changed resource causes the job to fail rather than silently using different content.
|
|
114
|
+
|
|
115
|
+
## Job-scoped temporary files
|
|
116
|
+
|
|
117
|
+
Do not create ad hoc helpers in the workspace when the file exists only for one operation. Include it in the job:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"temporary_files": [
|
|
122
|
+
{
|
|
123
|
+
"name": "repair.js",
|
|
124
|
+
"content": "console.log('repair')"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"steps": [
|
|
128
|
+
{
|
|
129
|
+
"argv": ["node", "{{temp:repair.js}}"]
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Available placeholders are:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
{{temp:name}}
|
|
139
|
+
{{resource:name}}
|
|
140
|
+
{{job:runtime}}
|
|
141
|
+
{{job:workspace}}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Temporary files live only below the private job runtime and are removed in the runner's final cleanup path. This is preferable to writing into `Codex/daily/ops`, `/tmp`, or a remote home directory and relying on a later MCP call to delete the file.
|
|
145
|
+
|
|
146
|
+
For a remote shell program, avoid a remote helper entirely when possible:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"steps": [
|
|
151
|
+
{
|
|
152
|
+
"argv": [
|
|
153
|
+
"ssh",
|
|
154
|
+
"-i",
|
|
155
|
+
"{{resource:racknerd-key}}",
|
|
156
|
+
"root@example",
|
|
157
|
+
"sh",
|
|
158
|
+
"-s"
|
|
159
|
+
],
|
|
160
|
+
"stdin": "set -eu\n# repair commands here\n"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The script travels through the SSH process stdin and is not stored as a remote file.
|
|
167
|
+
|
|
168
|
+
## Finally steps
|
|
169
|
+
|
|
170
|
+
A job can contain ordered main steps and ordered `finally_steps`:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"name": "remote maintenance",
|
|
175
|
+
"steps": [
|
|
176
|
+
{
|
|
177
|
+
"argv": ["ssh", "root@example", "perform-maintenance"]
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"finally_steps": [
|
|
181
|
+
{
|
|
182
|
+
"argv": ["ssh", "root@example", "rm", "-f", "/tmp/maintenance-helper"],
|
|
183
|
+
"allow_failure": true
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Finally steps are attempted after success, failure, timeout, or cancellation. Cancellation is delivered through an owner-only marker polled by the runner; the runner terminates the current child process tree and remains alive to execute cleanup. This avoids platform-specific signal behavior killing the coordinator itself. If a runner is interrupted, the next daemon start or local `machine-mcp job ...` command detects the dead runner, removes stale private resource copies, and runs the finally phase in recovery mode.
|
|
190
|
+
|
|
191
|
+
Automatic dead-runner recovery is attempted at most three times; persistent failure becomes `recovery_exhausted` to avoid an endless restart loop. Cleanup is best effort, not mathematically guaranteed. Power loss, disk failure, permanent account loss, or a local security product that denies the cleanup executable can still prevent it. Finally steps should therefore be idempotent and safe to run more than once.
|
|
192
|
+
|
|
193
|
+
## Two-phase local approval
|
|
194
|
+
|
|
195
|
+
If the MCP host rejects execution-class tools but still allows state changes, use:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
stage_job
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This performs the same schema, cwd, resource, size, and permission validation as `start_job`, but records status `staged` and launches no process. The response includes the local approval command:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
machine-mcp job inspect JOB_ID
|
|
205
|
+
machine-mcp job approve JOB_ID
|
|
206
|
+
# or after a separate review, non-interactively:
|
|
207
|
+
machine-mcp job approve JOB_ID --yes
|
|
208
|
+
```
|
|
209
|
+
|
|
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
|
+
|
|
212
|
+
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
|
+
Before approval:
|
|
215
|
+
|
|
216
|
+
- no main step runs;
|
|
217
|
+
- no finally step runs;
|
|
218
|
+
- no resource is copied into a runtime directory;
|
|
219
|
+
- cancelling produces `cancelled_before_start` and deletes the plan.
|
|
220
|
+
|
|
221
|
+
Staged plans count toward the 50-item retention limit and expire with the seven-day job retention policy. They are not considered active processes and do not independently block uninstall after the normal uninstall confirmation.
|
|
222
|
+
|
|
223
|
+
## Submit and inspect
|
|
224
|
+
|
|
225
|
+
Through MCP:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
stage_job
|
|
229
|
+
start_job
|
|
230
|
+
list_jobs
|
|
231
|
+
read_job
|
|
232
|
+
cancel_job
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
From the local terminal, including after an MCP host blocks further execution calls:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
machine-mcp job list
|
|
239
|
+
machine-mcp job inspect JOB_ID
|
|
240
|
+
machine-mcp job approve JOB_ID [--yes]
|
|
241
|
+
machine-mcp job cancel JOB_ID
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A local JSON fallback is also available:
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
machine-mcp job submit plan.json
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The plan format is the same object accepted by `start_job`.
|
|
251
|
+
|
|
252
|
+
## Output and secret handling
|
|
253
|
+
|
|
254
|
+
Step output is bounded to 64 KiB per stream and a shared 256 KiB capture budget per job, then stored in owner-only job results. Exact registered resource paths and exact resource content are replaced before results are returned. Common exact base64 and hexadecimal forms are also replaced for bounded resources.
|
|
255
|
+
|
|
256
|
+
This redaction is defense in depth, not a data-loss-prevention guarantee. It cannot reliably detect:
|
|
257
|
+
|
|
258
|
+
- partial secrets;
|
|
259
|
+
- encrypted, compressed, hashed, or otherwise transformed values;
|
|
260
|
+
- values copied from the full inherited environment rather than registered resources;
|
|
261
|
+
- application-specific encodings.
|
|
262
|
+
|
|
263
|
+
Use:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{ "capture_output": "discard" }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
for any step that may echo a credential. In discard mode stdout and stderr are consumed but not retained or returned.
|
|
270
|
+
|
|
271
|
+
Never place a secret directly in `argv`, `env`, `stdin`, a temporary file's `content`, or a plan JSON file. Use a registered resource alias.
|
|
272
|
+
|
|
273
|
+
## Persistence and privacy
|
|
274
|
+
|
|
275
|
+
Per-workspace jobs are stored below the owner-only profile directory. Active jobs retain an owner-only plan for crash recovery. After a terminal status is committed, the full plan is deleted, including argv, stdin, embedded temporary-file content, and resource source paths.
|
|
276
|
+
|
|
277
|
+
Retained public job data contains bounded status and redacted results. Up to 50 jobs are retained for up to seven days. Private runtime copies are removed after the finally phase. Runner stdout/stderr log files contain only runner-level diagnostics; step output is not written to those operational logs.
|
|
278
|
+
|
|
279
|
+
Process sessions and managed jobs have different semantics:
|
|
280
|
+
|
|
281
|
+
- process sessions are interactive, memory-only, and die with the daemon connection;
|
|
282
|
+
- managed jobs are non-interactive, persistent, detached, and designed to survive MCP disconnection.
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -8,7 +8,20 @@ machine-mcp doctor
|
|
|
8
8
|
machine-mcp service status
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`status` prints redacted profile state and verifies the deployed Worker version. `doctor` checks Node.js, the package-installed Wrangler binary, Cloudflare login,
|
|
11
|
+
`status` prints redacted profile state and verifies the deployed Worker version. Resource source paths remain redacted. `doctor` checks Node.js, the package-installed Wrangler binary, Cloudflare login, Worker health, and the same fixed local filesystem/process/shell/job-storage/resource probes exposed by `diagnose_runtime`. Public `/healthz` output contains only server identity and version; daemon details require an authenticated `server_info` call.
|
|
12
|
+
|
|
13
|
+
### Blocking-layer decision table
|
|
14
|
+
|
|
15
|
+
| Result | Interpretation |
|
|
16
|
+
|---|---|
|
|
17
|
+
| No structured result because the host rejects the call | Host/connector approval or safety layer, or transport before daemon delivery |
|
|
18
|
+
| `mcp-host-to-daemon` passes but `local-filesystem` fails | Local state/runtime permissions, disk policy, sandbox, or endpoint security |
|
|
19
|
+
| Filesystem passes but `local-process-spawn` fails | Local executable policy, endpoint security, OS permissions, or damaged Node runtime |
|
|
20
|
+
| Direct spawn passes but `local-shell` fails | Shell path/profile/policy problem |
|
|
21
|
+
| `managed-job-storage` fails | Owner-only profile/job directory cannot be used |
|
|
22
|
+
| Registered resource is unavailable | File moved, permissions changed, size exceeded, or local access denied |
|
|
23
|
+
|
|
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.
|
|
12
25
|
|
|
13
26
|
## Logs
|
|
14
27
|
|
|
@@ -17,10 +30,10 @@ Remote autostart logs are stored under the state root in `logs/daemon.out.log` a
|
|
|
17
30
|
Logging is level-based:
|
|
18
31
|
|
|
19
32
|
```text
|
|
20
|
-
error unrecoverable local/transport failures
|
|
21
|
-
warn
|
|
22
|
-
info startup/deploy/connect transitions
|
|
23
|
-
debug
|
|
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
|
|
24
37
|
```
|
|
25
38
|
|
|
26
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`.
|
|
@@ -29,6 +42,55 @@ Normal logs intentionally omit tool arguments, file/patch/image content, command
|
|
|
29
42
|
|
|
30
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.
|
|
31
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
|
+
|
|
63
|
+
## Managed jobs and local recovery
|
|
64
|
+
|
|
65
|
+
Register local-only resources from the terminal:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
machine-mcp resource add NAME FILE_PATH
|
|
69
|
+
machine-mcp resource list
|
|
70
|
+
machine-mcp resource check NAME
|
|
71
|
+
machine-mcp resource remove NAME
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Inspect detached jobs even when the MCP host no longer permits execution tools:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
machine-mcp job list
|
|
78
|
+
machine-mcp job inspect JOB_ID
|
|
79
|
+
machine-mcp job approve JOB_ID [--yes]
|
|
80
|
+
machine-mcp job cancel JOB_ID
|
|
81
|
+
machine-mcp job submit plan.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Registry changes apply to newly submitted jobs without restarting the daemon. Active jobs use the resource snapshot accepted with their plan.
|
|
85
|
+
|
|
86
|
+
Policy changes affect new direct submissions. Cancel accepted running jobs explicitly when revoking execution authority. A staged plan launches only after local `job approve`, which is an independent operator authorization. A managed job transitions through `queued`, `running`, `cleaning`, and a terminal status such as `succeeded`, `failed`, or `cancelled`. Cleanup-specific terminal variants report a failed finally phase. If a runner PID dies, the next daemon/job-CLI start marks the job interrupted, removes stale private runtime copies, and runs the finally phase in recovery mode. Automatic recovery is capped at three attempts; persistent failure becomes `recovery_exhausted`.
|
|
87
|
+
|
|
88
|
+
Use job-scoped `temporary_files` for local helpers. For remote maintenance, prefer `ssh ... sh -s` with the remote script in step `stdin`; this avoids remote temporary scripts. Explicit remote cleanup belongs in idempotent `finally_steps`.
|
|
89
|
+
|
|
90
|
+
Uninstall refuses to remove local state while any managed job remains active. Active plans are needed for recovery and are owner-only. Terminal jobs delete their full plans. Bounded redacted results and runner-level diagnostics remain for up to seven days/50 jobs. Step output is never copied to ordinary daemon logs.
|
|
91
|
+
|
|
92
|
+
See [MANAGED_JOBS.md](MANAGED_JOBS.md).
|
|
93
|
+
|
|
32
94
|
## Reconnect and replacement
|
|
33
95
|
|
|
34
96
|
The daemon sends heartbeats and reconnects with bounded exponential backoff and jitter. A new socket remains a candidate until it authenticates and sends a valid `hello`; only then does it replace the previous daemon.
|
|
@@ -54,13 +116,19 @@ Defense-in-depth limits include:
|
|
|
54
116
|
- command timeout: 1–600 seconds;
|
|
55
117
|
- process-session read wait: at most 30 seconds;
|
|
56
118
|
- direct directory result: 10,000 entries and 4 MiB of path metadata;
|
|
57
|
-
- recursive walk: 200,000 visited entries
|
|
119
|
+
- recursive walk: 200,000 visited entries;
|
|
120
|
+
- managed jobs: 50 retained, seven-day retention;
|
|
121
|
+
- managed-job steps: 16 main plus 16 finally;
|
|
122
|
+
- managed-job timeout: 1–3,600 seconds per step;
|
|
123
|
+
- managed-job output: 64 KiB per stream and 256 KiB total captured across one job;
|
|
124
|
+
- registered resources: 64, 1 MiB each, 8 MiB referenced per job;
|
|
125
|
+
- job-scoped temporary files: 16 files, 512 KiB total content.
|
|
58
126
|
|
|
59
127
|
## Upgrade behavior
|
|
60
128
|
|
|
61
|
-
|
|
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.
|
|
62
130
|
|
|
63
|
-
`full`
|
|
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.
|
|
64
132
|
|
|
65
133
|
Inspect effective policy with:
|
|
66
134
|
|
|
@@ -85,5 +153,6 @@ After suspected credential or client compromise:
|
|
|
85
153
|
1. stop foreground and autostart daemons;
|
|
86
154
|
2. run `machine-mcp rotate-secrets`;
|
|
87
155
|
3. restart without broad flags and redeploy;
|
|
88
|
-
4. inspect Cloudflare account access, Worker configuration, local state permissions, and service logs;
|
|
89
|
-
5.
|
|
156
|
+
4. inspect Cloudflare account access, Worker configuration, local state/resource permissions, managed-job results, and service logs;
|
|
157
|
+
5. cancel active managed jobs and remove compromised resource aliases;
|
|
158
|
+
6. remove the Worker and local state if continued remote access is unnecessary.
|
package/docs/TESTING.md
CHANGED
|
@@ -23,12 +23,19 @@ The suite includes:
|
|
|
23
23
|
- author-email privacy in `git_log`;
|
|
24
24
|
- isolated command HOME/temp/cache behavior;
|
|
25
25
|
- one-shot timeout, descendant termination, cancellation, and process-session interaction;
|
|
26
|
+
- layered fixed runtime diagnostics for filesystem, direct process, shell, managed-job storage, and resource availability;
|
|
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;
|
|
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;
|
|
26
33
|
- daemon/startup locking and state corruption recovery;
|
|
27
34
|
- guarded state-root removal, schema migration, policy-origin persistence, and legacy implicit-default migration;
|
|
28
35
|
- no filename-based sensitive-file denial under unrestricted policy;
|
|
29
|
-
- log redaction, control-character handling, message/field bounds,
|
|
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;
|
|
30
37
|
- CLI parsing, policy profiles, and client configuration boundaries;
|
|
31
|
-
- live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, and
|
|
38
|
+
- live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
|
32
39
|
- live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, dynamic tool advertisement, rich content, daemon replacement, and cancellation.
|
|
33
40
|
|
|
34
41
|
## Additional release checks
|
|
@@ -50,5 +57,7 @@ GitHub Actions executes the main suite on Linux, macOS, and Windows. Node 22 and
|
|
|
50
57
|
- Every bounded resource needs an over-limit test.
|
|
51
58
|
- Every multi-stage mutation needs a no-partial-commit test.
|
|
52
59
|
- Every remote call correlation change needs daemon replacement and cancellation coverage.
|
|
60
|
+
- Every durable workflow needs disconnect, cancellation, cleanup-failure, dead-runner, and plan-scrubbing coverage.
|
|
61
|
+
- Secret-bearing resource tests must assert absence of raw, path, base64, and hex forms from MCP-visible results.
|
|
53
62
|
- Logs and public metadata should be tested for absence of sensitive fields, arguments, outputs, and routine success noise—not only presence of expected fields.
|
|
54
63
|
- Cross-platform tests must avoid shell syntax, URL-path conversion, and executable-shim assumptions specific to one operating system.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Cross-client MCP bridge for
|
|
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,15 +39,19 @@
|
|
|
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 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",
|
|
43
|
-
"check": "npm run typecheck && npm run syntax && npm run catalog:test && npm run self-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",
|
|
47
47
|
"release:publish": "node scripts/github-release.mjs --publish",
|
|
48
48
|
"release:backfill": "node scripts/github-release.mjs --backfill",
|
|
49
49
|
"stdio:integration-test": "node tests/stdio-integration-test.mjs",
|
|
50
|
-
"catalog:test": "node tests/catalog-test.mjs"
|
|
50
|
+
"catalog:test": "node tests/catalog-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"
|
|
51
55
|
},
|
|
52
56
|
"dependencies": {
|
|
53
57
|
"wrangler": "4.110.0",
|
|
@@ -67,7 +71,10 @@
|
|
|
67
71
|
"stdio",
|
|
68
72
|
"coding-agent",
|
|
69
73
|
"oauth",
|
|
70
|
-
"cross-platform"
|
|
74
|
+
"cross-platform",
|
|
75
|
+
"ssh",
|
|
76
|
+
"automation",
|
|
77
|
+
"managed-jobs"
|
|
71
78
|
],
|
|
72
79
|
"repository": {
|
|
73
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
|
+
}
|