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/CHANGELOG.md +71 -0
- package/CONTRIBUTING.md +31 -0
- package/README.md +20 -17
- package/SECURITY.md +11 -7
- package/docs/ARCHITECTURE.md +14 -12
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +159 -0
- package/docs/LOGGING.md +65 -26
- package/docs/MANAGED_JOBS.md +14 -13
- package/docs/OPERATIONS.md +15 -6
- package/docs/PRIVACY.md +43 -0
- package/docs/RELEASING.md +9 -4
- package/docs/TESTING.md +23 -1
- package/package.json +35 -8
- package/scripts/network-retry.mjs +47 -0
- package/scripts/privacy-check.mjs +177 -0
- package/scripts/release-impact-check.mjs +78 -0
- package/src/local/cli.mjs +369 -272
- package/src/local/full-access-test.mjs +2 -2
- package/src/local/job-runner.mjs +73 -31
- package/src/local/log.mjs +28 -2
- package/src/local/managed-jobs.mjs +194 -125
- package/src/local/process-sessions.mjs +5 -5
- package/src/local/relay-connection.mjs +495 -0
- package/src/local/{daemon.mjs → runtime.mjs} +188 -198
- package/src/local/secure-file.mjs +27 -0
- package/src/local/service.mjs +128 -30
- package/src/local/shell.mjs +1 -1
- package/src/local/ssh-key.mjs +41 -21
- package/src/local/state.mjs +7 -5
- package/src/local/stdio.mjs +157 -81
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +5 -1
- package/src/worker/index.ts +45 -21
- package/wrangler.jsonc +1 -1
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
|
-
|
|
7
|
+
Logs should answer:
|
|
6
8
|
|
|
7
|
-
1. Did deployment and relay
|
|
8
|
-
2. Is
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
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
|
-
|
|
27
|
+
Foreground mode defaults to `info`. Platform autostart services use `warn`.
|
|
25
28
|
|
|
26
29
|
| Level | Intended events |
|
|
27
30
|
|---|---|
|
|
28
|
-
| `error` |
|
|
29
|
-
| `warn` |
|
|
30
|
-
| `info` | Deployment/startup transitions and
|
|
31
|
-
| `debug` |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
With `--verbose`, the same incident may additionally include bounded transport diagnostics.
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
## Tool events
|
|
40
65
|
|
|
41
|
-
- tool
|
|
42
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
112
|
+
See [ENGINEERING.md](ENGINEERING.md) for the project-wide review rules.
|
package/docs/MANAGED_JOBS.md
CHANGED
|
@@ -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/
|
|
50
|
-
machine-mcp resource add
|
|
51
|
-
machine-mcp resource list
|
|
52
|
-
machine-mcp resource
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
156
|
-
"
|
|
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", "
|
|
178
|
+
"argv": ["ssh", "admin@server.example", "perform-maintenance"]
|
|
178
179
|
}
|
|
179
180
|
],
|
|
180
181
|
"finally_steps": [
|
|
181
182
|
{
|
|
182
|
-
"argv": ["ssh", "
|
|
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`
|
|
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
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -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
|
|
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
|
|
package/docs/PRIVACY.md
ADDED
|
@@ -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`,
|
|
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
|
|
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.
|
|
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": ">=
|
|
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/
|
|
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": "
|
|
62
|
-
"typescript": "
|
|
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
|
+
}
|