machine-bridge-mcp 3.0.0-beta.10 → 3.0.0-beta.11

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0-beta.11 - 2026-07-23
4
+
5
+ ### External-review verification and observability hardening
6
+
7
+ - Share one portable content-redaction implementation between local and Worker logs. Worker string fields now redact embedded bearer/API tokens, credential URLs, email addresses, private-key headers, and user-home paths even when the field name itself is not sensitive.
8
+ - Prevent caller-supplied local or Worker fields from replacing authoritative `timestamp`, `level`, `component`, `message`, or `event` metadata, and add regression coverage for both value leakage and metadata forgery.
9
+ - Make the automatic execution model explicit in authenticated `server_info` authority snapshots and `machine-mcp doctor`: operations inside effective authority do not use per-operation prompts, and remote owner shell/browser/application actions have the daemon OS user's ambient authority.
10
+ - Extract local-resource reads and SSH-resource registration into `runtime-resource-service.mjs`, reducing `LocalRuntime` from 697 to 653 lines while retaining the existing zero-extra-step browser/application resource path and public result contract.
11
+ - Re-verify review claims against the repository invariants. The default `full` profile, single-maintainer release controls, Node 26/npm 12 baseline, packaged deployment/release helpers, systemd-user support, and cross-platform behavior suites remain intentional; none is weakened or removed merely to reduce surface area or ceremony.
12
+ - Refresh the exact Wrangler runtime from 4.112.0 to 4.113.0 and advance the reviewed npm install-script allowlist to its exact `workerd 1.20260721.1`; a clean install must not depend on an unreviewed or locally cached postinstall.
13
+
3
14
  ## 3.0.0-beta.10 - 2026-07-22
4
15
 
5
16
  ### Published prerelease activation repair
@@ -30,6 +30,6 @@
30
30
  "action": {
31
31
  "default_title": "Machine Bridge Browser"
32
32
  },
33
- "version_name": "3.0.0-beta.10",
33
+ "version_name": "3.0.0-beta.11",
34
34
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxryYkpZhq8+VAQLHcGS9BAHQcyKX8RHGIpIwvtIVRU/rcOcE0bNdnM0aZJ/h6xWQsGDHlhvjT2+1aJaAn/9k8473BRWajzVXld961CdHYVFVHoce2hHiSJ0xydWrHMMZhAm0mN0UzjEpgZ0tMw209efcZHIvSwuxhteZMRy4kyiVjwFlOf5oXFCxRuCJnPj3AK9CmCf4XgEBuPIJ0TZmjGHOOdBvJmbCNnAWXYEo5/mf7MfCGhV4IJ1hNuhpoNQfOFKMUcw9/v/IpT62XpfXdGYTfGYCmCjC+gntK1spbkr2P4/2+sYMQtLpse71mpSNGXfcf3abU55Vpn+gncSxRQIDAQAB"
35
35
  }
@@ -37,6 +37,7 @@ A canonical workspace receives an independent profile, Worker name, secret set,
37
37
  - `runtime-tool-handlers.mjs` owns catalog-to-handler registration;
38
38
  - `runtime-relay.mjs` owns relay construction and inbound envelope normalization, while `relay-call-recovery.mjs` owns the bounded disconnect grace, result queue, authoritative resumed-call reconciliation, replay, and expiry cleanup;
39
39
  - `runtime-paths.mjs` owns runtime-directory creation, containment checks, and error-path redaction;
40
+ - `runtime-resource-service.mjs` owns registered-resource lookup, bounded binary/UTF-8 reads for browser/application injection, and SSH-resource registration/result projection;
40
41
  - `managed-job-lock.mjs`, `managed-job-runner.mjs`, `managed-job-storage.mjs`, and `managed-job-projection.mjs` separate transition ownership, detached runner identity, private persistence/diagnostics, and public result shaping from the managed-job lifecycle;
41
42
  - `browser-request-registry.mjs`, `browser-broker-routes.mjs`, `browser-broker-server.mjs`, and `browser-bridge-http.mjs` separate direct request ownership, runtime-client proxy routing, authenticated loopback WebSocket upgrades/listening, and loopback HTTP handling from broker startup and extension handover;
42
43
  - managed jobs, local resources, application automation, and browser automation remain separate managers.
package/docs/AUDIT.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Security and privacy audit notes
2
2
 
3
+ ## 2026-07-23 version 3.0.0-beta.11 external-review verification
4
+
5
+ The supplied read-only review was rechecked against source, package gates, platform plans, and the repository's explicit product/maintenance invariants rather than accepted as a backlog wholesale. Its central observability concern was correct: Worker field-name redaction was materially weaker than the local logger, so a future event could place a token, email address, credential URL, private-key header, or user-home path in an innocently named string field. The review did not notice a second defect in both structured loggers: sanitized caller fields were spread after core fields and could replace local `timestamp`, `level`, `component`, `message`, or `event` metadata and the corresponding Worker metadata, weakening incident chronology and event identity without executing code.
6
+
7
+ Beta.11 moves the portable free-form patterns into one shared module used by both runtimes. Local logging retains environment-derived home aliases and path-key suppression; Worker events gain content-aware value redaction without importing Node-only state. Local and Worker core metadata is assigned after caller fields and therefore remains authoritative. Tests cover sensitive-key redaction, sensitive content under a non-sensitive key, user-home aliases, email/token removal, control fields that attempt to forge event metadata, and unchanged safe route fields.
8
+
9
+ The line-cap warning was also directionally correct. `LocalRuntime` was 697 lines against a 700-line guardrail, but an arbitrary split would only hide coupling. Resource lookup, bounded binary/UTF-8 reads, and SSH resource registration form an independent lifecycle used by browser/application injection and the public generation tool. They now live in `runtime-resource-service.mjs`; `LocalRuntime` remains the composition root and falls to 653 lines. A direct boundary test and an independent architecture/package cap protect the extraction.
10
+
11
+ Several recommendations were rejected because their premises did not match the current tree or because they would reduce usability without a demonstrated failure mechanism. Linux already has a systemd-user service adapter and detached restart path. macOS and Windows run the complete platform plan, including service, process-lock, sandbox, application, managed-job, filesystem, and full-access behavior tests; Ubuntu alone carries the additional Worker/browser/package integrations. `full-access-test.mjs` is also the implementation behind the supported `machine-mcp full-test` command, not an unused test-only harness. Node 26/npm 12 is an explicitly documented single-baseline policy. Wrangler and the packaged release/deployment helpers support the zero-extra-install first-run and self-contained release state machine; splitting them would add setup and version-skew failure modes for the sole maintainer. Source visibility is not treated as a security boundary. The default canonical `full` profile and absence of per-operation prompts are deliberate usability decisions, so beta.11 makes them harder to misinterpret in diagnostics rather than inserting prompts or narrowing authority.
12
+
13
+ The review's suggestion to delete or rotate unreadable managed-job state was not adopted. An unreadable record deliberately blocks destructive state removal because automatically moving or deleting a directory that cannot be safely read can discard active recovery material or cross a replacement race. The bounded 50-job admission limit already prevents unbounded job-count growth; terminal readable records retain seven days, staged secrets retain twenty-four hours, and unreadable records require explicit operator inspection. A future quarantine feature would need token/snapshot-safe ownership, byte accounting, and removal semantics before it could improve rather than weaken this contract.
14
+
15
+ The dependency pass found only the next exact Wrangler patch. Updating to 4.113.0 advanced Miniflare/workerd to the 2026-07-21 build; npm initially blocked the new workerd postinstall because the repository intentionally permits scripts by exact package version. Beta.11 updates that allowlist to `workerd 1.20260721.1`, and clean-install/package/Worker dry-run gates must prove the new pin rather than relying on an existing binary. Registry audit, production-only audit, signatures, and attestations remain separate release evidence.
16
+
17
+ The second pass found no reason to reduce default authority, require a second reviewer, remove systemd support, broaden compatibility branches, or split the package into separately versioned runtime/deployment products. Remaining residuals are explicit: an authenticated remote owner is intentionally high authority; Cloudflare sampled logs are operational telemetry rather than an audit trail; unknown or transformed secrets can evade regex redaction, so arguments/results remain omitted; browser automation acts inside the real user profile; and OS CPU, memory, syscall, and egress isolation remain external deployment responsibilities. No Worker deployment, daemon/service replacement, global installation, credential rotation, Git push, tag, npm publication, or GitHub Release is implied by this source audit.
18
+
3
19
  ## 2026-07-22 version 3.0.0-beta.7 nested lifecycle PATH audit
4
20
 
5
21
  Beta.6 passed its automated gates and exact live activation, but the launchd PATH still contained repository `node_modules/.bin` entries and npm's private `node-gyp-bin`. Source, tarball, and isolated runtime inspection proved that the intended sanitizer was packaged. A direct invocation over the installed plist PATH also produced the expected clean value. The remaining variable was the activation process environment.
package/docs/LOGGING.md CHANGED
@@ -112,6 +112,8 @@ Application discovery and Accessibility operations follow the same rule: permiss
112
112
 
113
113
  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 and path-like keys are recursively redacted. Free-form sanitization covers generic private-key headers, AWS/GitHub/GitLab/npm/Slack/Google/live-payment/API token forms, JWT-shaped values, URLs with embedded credentials, email addresses, and user-home paths.
114
114
 
115
+ Local and Worker free-form strings use the same portable value sanitizer. Worker fields are therefore inspected by content even when their key is not secret-shaped. Both structured loggers assign their authoritative metadata after sanitizing caller fields. Local `timestamp`, `level`, `component`, `message`, and `event`, plus Worker `timestamp`, `level`, `component`, and `event`, therefore cannot be forged or replaced by an event payload. Local-only recursive path-key redaction and environment-derived home aliases remain additional protections around the portable rules.
116
+
115
117
  The logger exposes two intentional plain-output boundaries. `safePlain` sanitizes operational guidance and diagnostic text. Raw `plain` output is reserved for explicitly requested credentials or local paths whose display is the command's purpose; callers must not pass external exception text or tool content to it.
116
118
 
117
119
  This is defense in depth, not content classification. Unknown, split, transformed, encrypted, or application-specific secret formats can evade pattern matching, which is why tool arguments and outputs are omitted rather than merely filtered.
@@ -8,7 +8,7 @@ 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. 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.
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, the configured policy, the automatic-without-per-operation-prompts authorization model, and the same fixed local filesystem/process/shell/job-storage/resource probes exposed by `diagnose_runtime`. Authenticated `server_info.authorization.execution_model` reports the same contract and identifies whether the account has daemon-OS-user ambient authority. Public `/healthz` output contains only server identity and version; daemon details require an authenticated `server_info` call.
12
12
 
13
13
  ### Worker deployment and health convergence
14
14
 
package/docs/TESTING.md CHANGED
@@ -55,7 +55,7 @@ The suite includes:
55
55
  - isolated command HOME/temp/cache behavior;
56
56
  - one-shot timeout, descendant process-group/tree termination including descendants that ignore graceful shutdown after the direct child has already exited, cancellation, and process-session interaction;
57
57
  - layered fixed runtime diagnostics for filesystem, direct process, shell, managed-job storage, and resource availability; machine-readable execution guardrails that must keep CPU, memory, and network isolation marked unenforced unless an actual OS boundary is added;
58
- - local resource CLI registration, permission checks, dynamic reload, state-path redaction, and content non-disclosure;
58
+ - local resource CLI registration, permission checks, dynamic reload, state-path redaction, content non-disclosure, and the extracted runtime-resource boundary for bounded binary/UTF-8 reads plus generation authorization;
59
59
  - real Ed25519 and RSA generation, idempotent reuse, public/private correspondence, mode enforcement, incomplete/mismatched/symlink rejection, and private-content non-disclosure;
60
60
  - 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;
61
61
  - deterministic injected atomic-replace failures, sustained transient Windows sharing contention beyond the old retry budget, bounded exponential delay selection, and repeated Windows full-sandbox runs;
@@ -64,7 +64,7 @@ The suite includes:
64
64
  - daemon/startup locking, successfully-read corrupt-JSON recovery, and explicit propagation/preservation of oversized, symbolic-link, permission, and I/O failures;
65
65
  - guarded state-root removal, unsafe state-root/workspace overlap rejection before creation, all-profile lock/daemon scanning, strict current-schema validation, corrupt-JSON isolation, and policy-origin persistence;
66
66
  - no filename-based sensitive-file denial under unrestricted policy;
67
- - log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, service warning-level configuration, JSON-mode parity across event and direct logger methods with timestamp/stream/redaction assertions, current-schema reset, and bounded tail trimming;
67
+ - shared local/Worker free-form log redaction, sensitive content under non-sensitive Worker keys, immutable local/Worker structured metadata, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, service warning-level configuration, JSON-mode parity across event and direct logger methods with timestamp/stream/redaction assertions, current-schema reset, and bounded tail trimming;
68
68
  - deterministic relay connection lifecycle coverage for transport construction/error/deadline, pre-handshake `welcome` validation, separate `hello_ack` authentication and `ready_ack` end-to-end readiness, session-bound probe return, pre-ready tool rejection, premature-ready rejection, identity/version mismatch, retryable Worker hello/readiness errors, fatal protocol errors, autonomous outage-reminder backoff, handshake/readiness/heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
69
69
  - shared no-follow bounded-file reads for normal files, over-limit data, directories, and symbolic links;
70
70
  - owner-only directory enforcement rejecting final symlinks, failing closed on POSIX chmod errors, verifying `0700`, and retaining Windows portability; Worker temporary-secret lifecycle coverage for process-start-bound names, valid stale-owner reclamation, ambiguous-owner retention, `0600` mode, deletion failures, and simultaneous deployment/cleanup failures;
@@ -146,7 +146,7 @@ Run `npm run privacy:check` before committing and before packaging. Run and revi
146
146
 
147
147
  ## Package manifest
148
148
 
149
- `npm run package:test` executes a real silent `npm pack --dry-run --json`, requires clean parseable JSON, rejects sensitive local artifacts, credential-like file classes, and generated Worker type declarations, validates every packaged mode as `0644` or `0755`, and verifies that required runtime, script, browser-extension, privacy, governance, and release files are present. `npm run install:test` requires npm 12, installs the real tarball from a package-free directory into an isolated global prefix with the documented options, verifies the packaged npm engine requirement, rejects blocked-script warnings, confirms optional `fsevents` is absent, verifies `--version`, then runs the installed CLI with zero arguments from an isolated workspace/state root. A fake Wrangler JavaScript entrypoint terminates at a controlled deployment boundary, so the probe exercises startup without mutating a live account. Package testing is full-plan-only; installation smoke runs in the Ubuntu full plan, the macOS/Windows platform plan, and package audit.
149
+ `npm run package:test` executes a real silent `npm pack --dry-run --json`, requires clean parseable JSON, rejects sensitive local artifacts, credential-like file classes, and generated Worker type declarations, validates every packaged mode as `0644` or `0755`, and verifies that required runtime, script, browser-extension, privacy, governance, and release files are present. Exact runtime install-script approvals are version-bound, so a Wrangler/workerd refresh must also update the reviewed allowlist and pass a clean npm install without blocked-script warnings. `npm run install:test` requires npm 12, installs the real tarball from a package-free directory into an isolated global prefix with the documented options, verifies the packaged npm engine requirement, rejects blocked-script warnings, confirms optional `fsevents` is absent, verifies `--version`, then runs the installed CLI with zero arguments from an isolated workspace/state root. A fake Wrangler JavaScript entrypoint terminates at a controlled deployment boundary, so the probe exercises startup without mutating a live account. Package testing is full-plan-only; installation smoke runs in the Ubuntu full plan, the macOS/Windows platform plan, and package audit.
150
150
 
151
151
  `npm run lint` uses ESLint as a semantic JavaScript correctness gate rather than a style formatter. It covers the Node CLI/runtime, repository scripts, tests, and packaged browser extension and rejects undefined identifiers in function bodies that `node --check` cannot detect. A dedicated lint-gate self-test proves that both Node and browser configurations reject a synthetic undefined binding while accepting the service-worker `importScripts` global. A focused `shell:test` requires Wrangler to run through the current Node executable and its package JavaScript entrypoint rather than a `.cmd` or shell shim. Architecture tests require `shell:test`, `lint:test`, `lint`, and `install:test` to remain in the complete check pipeline and reject non-exact direct dependency ranges.
152
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "3.0.0-beta.10",
3
+ "version": "3.0.0-beta.11",
4
4
  "description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -168,7 +168,7 @@
168
168
  "dependencies": {
169
169
  "https-proxy-agent": "9.1.0",
170
170
  "proxy-from-env": "2.1.0",
171
- "wrangler": "4.112.0",
171
+ "wrangler": "4.113.0",
172
172
  "ws": "8.21.1"
173
173
  },
174
174
  "devDependencies": {
@@ -207,8 +207,8 @@
207
207
  "allowScripts": {
208
208
  "esbuild@0.28.1": true,
209
209
  "fsevents": false,
210
- "workerd@1.20260714.1": true,
211
- "sharp@0.35.3": true
210
+ "sharp@0.35.3": true,
211
+ "workerd@1.20260721.1": true
212
212
  },
213
213
  "packageManager": "npm@12.0.1",
214
214
  "devEngines": {
package/src/local/cli.mjs CHANGED
@@ -519,6 +519,13 @@ async function doctorCommand(args) {
519
519
  const state = loadState(workspace, { stateDir: args.stateDir });
520
520
  state.policy = resolvePolicy({}, state.policy);
521
521
  checks.push({ name: "policy", ok: true, detail: formatPolicySummary(state.policy) });
522
+ checks.push({
523
+ name: "authorization-model",
524
+ ok: true,
525
+ detail: state.policy.profile === "full"
526
+ ? "local owner operations execute automatically without per-operation prompts; shell, browser, and application automation use the daemon OS user's ambient authority"
527
+ : "operations allowed by the selected local policy execute automatically without per-operation prompts",
528
+ });
522
529
  if (state.policy.profile === "full") {
523
530
  try {
524
531
  assertCanonicalFullPolicy(state.policy);
package/src/local/log.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import process from "node:process";
2
2
  import os from "node:os";
3
3
  import { errorCode } from "./errors.mjs";
4
+ import { sanitizePortableLogText } from "../shared/log-redaction.mjs";
4
5
 
5
6
  const COLORS = {
6
7
  reset: "\x1b[0m",
@@ -18,20 +19,6 @@ const MAX_LOG_OBJECT_KEYS = 48;
18
19
  const LEVEL_RANK = Object.freeze({ debug: 10, info: 20, success: 20, warn: 30, error: 40 });
19
20
  const SENSITIVE_KEY = /(authorization|cookie|password|passwd|secret|token|api[_-]?key|private[_-]?key|credential)/i;
20
21
  const LOCAL_PATH_KEY = /(path|paths|cwd|workspace|directory|(?:^|[_-])dir(?:$|[_-])|root|home)/i;
21
- const SECRET_VALUE = /\b(?:account_admin|account_password|daemon_secret|token_version|mcp_at|mcp_code)_[A-Za-z0-9_-]+\b/g;
22
- const BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*\b/gi;
23
- const EMAIL_VALUE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
24
- const AWS_ACCESS_KEY = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g;
25
- const GITHUB_TOKEN = /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g;
26
- const GITLAB_TOKEN = /\bglpat-[A-Za-z0-9_-]{20,}\b/g;
27
- const NPM_TOKEN = /\bnpm_[A-Za-z0-9]{30,}\b/g;
28
- const SLACK_TOKEN = /\bxox[aboprs]-[A-Za-z0-9-]{10,}\b/g;
29
- const GOOGLE_API_KEY = /\bAIza[A-Za-z0-9_-]{30,}\b/g;
30
- const PAYMENT_API_KEY = /\b(?:sk|rk|pk)_live_[A-Za-z0-9]{16,}\b/g;
31
- const JWT_VALUE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
32
- const URL_CREDENTIALS = /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@[^\s/"'<>]+/gi;
33
- const API_SECRET = /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g;
34
- const PRIVATE_KEY_HEADER = /-----BEGIN\s+(?:(?:OPENSSH|RSA|EC|DSA)\s+|ENCRYPTED\s+)?PRIVATE\s+KEY-----/g;
35
22
  const HOME_PATHS = [...new Set([process.env.HOME, process.env.USERPROFILE, safeHomeDirectory()].filter(value => typeof value === "string" && value.length > 1))]
36
23
  .sort((left, right) => right.length - left.length);
37
24
 
@@ -49,11 +36,11 @@ export function createLogger(options = {}) {
49
36
  const sanitizedMessage = sanitizeLogText(message, MAX_LOG_MESSAGE_CHARS);
50
37
  if (options.format === "json") {
51
38
  const entry = sanitizeLogValue({
39
+ ...(fields && typeof fields === "object" ? fields : {}),
52
40
  timestamp: new Date().toISOString(),
53
41
  level: level === "success" ? "info" : level,
54
42
  component,
55
43
  message: sanitizedMessage,
56
- ...(fields && typeof fields === "object" ? fields : {}),
57
44
  });
58
45
  stream.write(`${JSON.stringify(entry)}\n`);
59
46
  return;
@@ -66,16 +53,16 @@ export function createLogger(options = {}) {
66
53
  const event = (level, name, fields = {}, message = "") => {
67
54
  const normalizedLevel = normalizeEventLevel(level);
68
55
  const eventName = sanitizeEventName(name);
69
- const payload = { event: eventName, ...fields };
56
+ const payload = { ...fields, event: eventName };
70
57
  const humanMessage = message || humanizeEventName(eventName);
71
58
  if (options.format === "json") {
72
59
  if (LEVEL_RANK[normalizedLevel] < LEVEL_RANK[minimumLevel]) return;
73
60
  const entry = sanitizeLogValue({
61
+ ...payload,
74
62
  timestamp: new Date().toISOString(),
75
63
  level: normalizedLevel === "success" ? "info" : normalizedLevel,
76
64
  component,
77
65
  message: sanitizeLogText(humanMessage, MAX_LOG_MESSAGE_CHARS),
78
- ...payload,
79
66
  });
80
67
  const target = normalizedLevel === "info" || normalizedLevel === "success" ? stdout : stderr;
81
68
  target.write(`${JSON.stringify(entry)}\n`);
@@ -158,33 +145,7 @@ function sanitizeLogValue(value, key = "", seen = new WeakSet(), depth = 0) {
158
145
  }
159
146
 
160
147
  export function sanitizeLogText(value, maxChars = MAX_LOG_MESSAGE_CHARS) {
161
- let raw;
162
- try { raw = String(value ?? ""); } catch { raw = "<unprintable>"; }
163
- let sanitized = raw
164
- .replace(SECRET_VALUE, "<redacted-secret>")
165
- .replace(BEARER_VALUE, "Bearer <redacted>")
166
- .replace(AWS_ACCESS_KEY, "<redacted-cloud-key>")
167
- .replace(GITHUB_TOKEN, "<redacted-access-token>")
168
- .replace(GITLAB_TOKEN, "<redacted-access-token>")
169
- .replace(NPM_TOKEN, "<redacted-access-token>")
170
- .replace(SLACK_TOKEN, "<redacted-access-token>")
171
- .replace(GOOGLE_API_KEY, "<redacted-cloud-key>")
172
- .replace(PAYMENT_API_KEY, "<redacted-api-secret>")
173
- .replace(JWT_VALUE, "<redacted-bearer-token>")
174
- .replace(URL_CREDENTIALS, "<redacted-credential-url>")
175
- .replace(API_SECRET, "<redacted-api-secret>")
176
- .replace(PRIVATE_KEY_HEADER, "<redacted-private-key-header>")
177
- .replace(EMAIL_VALUE, "<redacted-email>");
178
- for (const home of HOME_PATHS) sanitized = sanitized.split(home).join("<home>");
179
- sanitized = sanitized
180
- .replace(/\/(?:Users|home)\/[^/\s"'<>]+(?=\/|$)/g, "<home>")
181
- .replace(/\b[A-Za-z]:\\Users\\[^\\\s"'<>]+(?=\\|$)/g, "<home>")
182
- .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "")
183
- .replace(/[\r\n\t]/g, match => match === "\t" ? "\\t" : "\\n")
184
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "?");
185
- if (!Number.isFinite(Number(maxChars)) || Number(maxChars) <= 0) return "";
186
- const limit = Math.max(16, Number(maxChars));
187
- return sanitized.length > limit ? `${sanitized.slice(0, limit - 1)}…` : sanitized;
148
+ return sanitizePortableLogText(value, { maxChars, homePaths: HOME_PATHS });
188
149
  }
189
150
 
190
151
  function safeHomeDirectory() {
@@ -0,0 +1,77 @@
1
+ import { resolve } from "node:path";
2
+ import { inspectResourceFile } from "./managed-job-plan.mjs";
3
+ import { generateRegisteredSshKey } from "./resource-operations.mjs";
4
+ import { expandHome } from "./state.mjs";
5
+ import { stateRootFromProfileStatePath } from "./runtime-paths.mjs";
6
+ import { readBoundedRegularFileSync } from "./secure-file.mjs";
7
+
8
+ const MAX_BROWSER_RESOURCE_BYTES = 1024 * 1024;
9
+
10
+ export class RuntimeResourceService {
11
+ constructor({ workspace, resourceStatePath = "", currentResources, authorizeTool }) {
12
+ this.workspace = resolve(workspace);
13
+ this.resourceStatePath = resourceStatePath ? resolve(resourceStatePath) : "";
14
+ this.currentResources = currentResources;
15
+ this.authorizeTool = authorizeTool;
16
+ }
17
+
18
+ readBinary(name) {
19
+ const registry = this.currentResources();
20
+ const resource = Object.hasOwn(registry, name) ? registry[name] : null;
21
+ if (!resource) throw new Error(`unknown local resource: ${name}`);
22
+ const inspected = inspectResourceFile(resource.path, {
23
+ allowInsecurePermissions: resource.allowInsecurePermissions === true,
24
+ });
25
+ if (inspected.size > MAX_BROWSER_RESOURCE_BYTES) {
26
+ throw new Error("local resource exceeds 1 MiB browser injection limit");
27
+ }
28
+ return {
29
+ buffer: readBoundedRegularFileSync(resource.path, MAX_BROWSER_RESOURCE_BYTES),
30
+ path: resource.path,
31
+ size: inspected.size,
32
+ };
33
+ }
34
+
35
+ readText(name) {
36
+ const { buffer } = this.readBinary(name);
37
+ try {
38
+ return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
39
+ } catch {
40
+ throw new Error(`local resource is not valid UTF-8 text: ${name}`);
41
+ }
42
+ }
43
+
44
+ async generateSshKey(args = {}) {
45
+ this.authorizeTool("generate_ssh_key_resource");
46
+ if (!this.resourceStatePath) throw new Error("local resource state is unavailable in this runtime");
47
+ const home = process.env.HOME || process.env.USERPROFILE;
48
+ if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
49
+ const target = args.path
50
+ ? resolve(expandHome(String(args.path)))
51
+ : resolve(home, ".ssh", `machine-mcp-${args.name}-ed25519`);
52
+ const key = await generateRegisteredSshKey({
53
+ workspace: this.workspace,
54
+ stateDir: stateRootFromProfileStatePath(this.resourceStatePath),
55
+ name: args.name,
56
+ targetPath: target,
57
+ comment: args.comment || `machine-mcp:${args.name}`,
58
+ });
59
+ const exposePaths = args.expose_paths === true;
60
+ return {
61
+ name: key.name,
62
+ created: key.created,
63
+ registered: key.registered,
64
+ fingerprint: key.fingerprint,
65
+ key_type: key.keyType,
66
+ private_mode: key.privateMode,
67
+ public_mode: key.publicMode,
68
+ private_key_content_exposed: key.privateKeyContentExposed,
69
+ available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
70
+ paths_exposed: exposePaths,
71
+ ...(exposePaths ? {
72
+ private_key_path: resolve(key.privateKeyPath),
73
+ public_key_path: resolve(key.publicKeyPath),
74
+ } : {}),
75
+ };
76
+ }
77
+ }
@@ -19,14 +19,11 @@ import { LifecycleController } from "./lifecycle.mjs";
19
19
  import { MAX_WRITE_BYTES, sha256, WorkspaceFileService } from "./workspace-file-service.mjs";
20
20
  export { MAX_WRITE_BYTES, sha256 } from "./workspace-file-service.mjs";
21
21
  import { classifyOperationalError } from "./log.mjs";
22
- import { inspectResourceFile, ManagedJobManager } from "./managed-jobs.mjs";
23
- import { generateRegisteredSshKey } from "./resource-operations.mjs";
24
- import { expandHome } from "./state.mjs";
22
+ import { ManagedJobManager } from "./managed-jobs.mjs";
25
23
  import { AgentContextManager } from "./agent-context.mjs";
26
24
  import { AppAutomationManager } from "./app-automation.mjs";
27
25
  import { BrowserBridgeManager } from "./browser-bridge.mjs";
28
26
  import { CapabilityObserver } from "./capability-observer.mjs";
29
- import { readBoundedRegularFileSync } from "./secure-file.mjs";
30
27
  import { isPlainRecord } from "./records.mjs";
31
28
  import { AccountAccessGate } from "./account-access.mjs";
32
29
  import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
@@ -38,7 +35,8 @@ import { delegatedProcessIsolationStatus } from "./delegated-process-sandbox.mjs
38
35
  import { policyForContext } from "./authority-context.mjs";
39
36
  import { createRuntimeRelayConnection, normalizeRelayResumeCalls, normalizeRelayToolCall } from "./runtime-relay.mjs";
40
37
  import { RelayCallRecovery } from "./relay-call-recovery.mjs";
41
- import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
38
+ import { RuntimeResourceService } from "./runtime-resource-service.mjs";
39
+ import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage } from "./runtime-paths.mjs";
42
40
  import {
43
41
  resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
44
42
  sessionBootstrap as buildRuntimeSessionBootstrap,
@@ -155,9 +153,15 @@ export class LocalRuntime {
155
153
  gitExecutable: () => this.resolveGitExecutable(),
156
154
  maximumBytes: MAX_WRITE_BYTES,
157
155
  });
156
+ this.runtimeResourceService = new RuntimeResourceService({
157
+ workspace: this.workspace,
158
+ resourceStatePath: this.resourceStatePath,
159
+ currentResources: () => this.managedJobManager.currentResources(),
160
+ authorizeTool: (tool) => this.policyGate.assert(tool),
161
+ });
158
162
  const runProcess = (cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin) => this.runProcess(cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
159
- const readResourceText = (name) => this.readLocalResourceText(name);
160
- const readResourceBinary = (name) => this.readLocalResourceBinary(name);
163
+ const readResourceText = (name) => this.runtimeResourceService.readText(name);
164
+ const readResourceBinary = (name) => this.runtimeResourceService.readBinary(name);
161
165
  this.appAutomationManager = new AppAutomationManager({
162
166
  ...applicationAutomation,
163
167
  policy: this.policy,
@@ -518,37 +522,7 @@ export class LocalRuntime {
518
522
 
519
523
  async generateSshKeyResource(args = {}, context = {}) {
520
524
  this.throwIfCancelled(context);
521
- this.policyGate.assert("generate_ssh_key_resource");
522
- if (!this.resourceStatePath) throw new Error("local resource state is unavailable in this runtime");
523
- const home = process.env.HOME || process.env.USERPROFILE;
524
- if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
525
- const target = args.path
526
- ? resolve(expandHome(String(args.path)))
527
- : resolve(home, ".ssh", `machine-mcp-${args.name}-ed25519`);
528
- const key = await generateRegisteredSshKey({
529
- workspace: this.workspace,
530
- stateDir: stateRootFromProfileStatePath(this.resourceStatePath),
531
- name: args.name,
532
- targetPath: target,
533
- comment: args.comment || `machine-mcp:${args.name}`,
534
- });
535
- const exposePaths = args.expose_paths === true;
536
- return {
537
- name: key.name,
538
- created: key.created,
539
- registered: key.registered,
540
- fingerprint: key.fingerprint,
541
- key_type: key.keyType,
542
- private_mode: key.privateMode,
543
- public_mode: key.publicMode,
544
- private_key_content_exposed: key.privateKeyContentExposed,
545
- available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
546
- paths_exposed: exposePaths,
547
- ...(exposePaths ? {
548
- private_key_path: resolve(key.privateKeyPath),
549
- public_key_path: resolve(key.publicKeyPath),
550
- } : {}),
551
- };
525
+ return this.runtimeResourceService.generateSshKey(args);
552
526
  }
553
527
 
554
528
  async sessionBootstrap(args = {}, context = {}) {
@@ -569,24 +543,6 @@ export class LocalRuntime {
569
543
  }, args, context);
570
544
  }
571
545
 
572
- readLocalResourceBinary(name) {
573
- const registry = this.managedJobManager.currentResources();
574
- const resource = Object.hasOwn(registry, name) ? registry[name] : null;
575
- if (!resource) throw new Error(`unknown local resource: ${name}`);
576
- const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
577
- if (inspected.size > 1024 * 1024) throw new Error("local resource exceeds 1 MiB browser injection limit");
578
- return { buffer: readBoundedRegularFileSync(resource.path, 1024 * 1024), path: resource.path, size: inspected.size };
579
- }
580
-
581
- readLocalResourceText(name) {
582
- const { buffer } = this.readLocalResourceBinary(name);
583
- try {
584
- return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
585
- } catch {
586
- throw new Error(`local resource is not valid UTF-8 text: ${name}`);
587
- }
588
- }
589
-
590
546
  runDirectProcess(args, context = {}) {
591
547
  return this.processExecutionService.runDirect(args, context);
592
548
  }
@@ -0,0 +1,9 @@
1
+ export type PortableLogSanitizationOptions = {
2
+ maxChars: number;
3
+ homePaths?: string[];
4
+ };
5
+
6
+ export function sanitizePortableLogText(
7
+ value: unknown,
8
+ options: PortableLogSanitizationOptions,
9
+ ): string;
@@ -0,0 +1,51 @@
1
+ const SECRET_VALUE = /\b(?:account_admin|account_password|daemon_secret|token_version|mcp_at|mcp_code)_[A-Za-z0-9_-]+\b/g;
2
+ const BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+\/-]+=*\b/gi;
3
+ const EMAIL_VALUE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
4
+ const AWS_ACCESS_KEY = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g;
5
+ const GITHUB_TOKEN = /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g;
6
+ const GITLAB_TOKEN = /\bglpat-[A-Za-z0-9_-]{20,}\b/g;
7
+ const NPM_TOKEN = /\bnpm_[A-Za-z0-9]{30,}\b/g;
8
+ const SLACK_TOKEN = /\bxox[aboprs]-[A-Za-z0-9-]{10,}\b/g;
9
+ const GOOGLE_API_KEY = /\bAIza[A-Za-z0-9_-]{30,}\b/g;
10
+ const PAYMENT_API_KEY = /\b(?:sk|rk|pk)_live_[A-Za-z0-9]{16,}\b/g;
11
+ const JWT_VALUE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
12
+ const URL_CREDENTIALS = /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@[^\s/"'<>]+/gi;
13
+ const API_SECRET = /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g;
14
+ const PRIVATE_KEY_HEADER = /-----BEGIN\s+(?:(?:OPENSSH|RSA|EC|DSA)\s+|ENCRYPTED\s+)?PRIVATE\s+KEY-----/g;
15
+
16
+ export function sanitizePortableLogText(value, options = {}) {
17
+ let raw;
18
+ try { raw = String(value ?? ""); } catch { raw = "<unprintable>"; }
19
+ let sanitized = raw
20
+ .replace(SECRET_VALUE, "<redacted-secret>")
21
+ .replace(BEARER_VALUE, "Bearer <redacted>")
22
+ .replace(AWS_ACCESS_KEY, "<redacted-cloud-key>")
23
+ .replace(GITHUB_TOKEN, "<redacted-access-token>")
24
+ .replace(GITLAB_TOKEN, "<redacted-access-token>")
25
+ .replace(NPM_TOKEN, "<redacted-access-token>")
26
+ .replace(SLACK_TOKEN, "<redacted-access-token>")
27
+ .replace(GOOGLE_API_KEY, "<redacted-cloud-key>")
28
+ .replace(PAYMENT_API_KEY, "<redacted-api-secret>")
29
+ .replace(JWT_VALUE, "<redacted-bearer-token>")
30
+ .replace(URL_CREDENTIALS, "<redacted-credential-url>")
31
+ .replace(API_SECRET, "<redacted-api-secret>")
32
+ .replace(PRIVATE_KEY_HEADER, "<redacted-private-key-header>")
33
+ .replace(EMAIL_VALUE, "<redacted-email>");
34
+
35
+ const homePaths = Array.isArray(options.homePaths) ? options.homePaths : [];
36
+ for (const home of [...new Set(homePaths.filter(item => typeof item === "string" && item.length > 1))]
37
+ .sort((left, right) => right.length - left.length)) {
38
+ sanitized = sanitized.split(home).join("<home>");
39
+ }
40
+ sanitized = sanitized
41
+ .replace(/\/(?:Users|home)\/[^/\s"'<>]+(?=\/|$)/g, "<home>")
42
+ .replace(/\b[A-Za-z]:\\Users\\[^\\\s"'<>]+(?=\\|$)/g, "<home>")
43
+ .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "")
44
+ .replace(/[\r\n\t]/g, match => match === "\t" ? "\\t" : "\\n")
45
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "?");
46
+
47
+ const numericLimit = Number(options.maxChars);
48
+ if (!Number.isFinite(numericLimit) || numericLimit <= 0) return "";
49
+ const limit = Math.max(16, numericLimit);
50
+ return sanitized.length > limit ? `${sanitized.slice(0, limit - 1)}…` : sanitized;
51
+ }
@@ -31,6 +31,12 @@ type AccountAuthority = {
31
31
  daemon_policy: string;
32
32
  host_filtering: string;
33
33
  };
34
+ execution_model: {
35
+ within_effective_authority: "automatic_without_per_operation_prompt";
36
+ owner_ambient_authority: "daemon_os_user" | "not_owner";
37
+ generic_control_plane_paths: "denied_even_for_owner";
38
+ mutually_untrusted_execution_requires: "external_os_isolation";
39
+ };
34
40
  summary: string;
35
41
  };
36
42
 
@@ -78,9 +84,15 @@ export function accountAuthoritySnapshot(input: AccountIdentity & {
78
84
  daemon_policy: "daemon.policy is only the local daemon capability ceiling; it is not the authenticated account permission",
79
85
  host_filtering: "the MCP host may expose a smaller subset than effective_tools, and Machine Bridge cannot observe that post-relay subset",
80
86
  },
87
+ execution_model: {
88
+ within_effective_authority: "automatic_without_per_operation_prompt",
89
+ owner_ambient_authority: input.role === "owner" ? "daemon_os_user" : "not_owner",
90
+ generic_control_plane_paths: "denied_even_for_owner",
91
+ mutually_untrusted_execution_requires: "external_os_isolation",
92
+ },
81
93
  summary: input.daemonPolicy
82
- ? `Authenticated account role ${input.role} has effective profile ${effectiveProfile}. daemon.policy.profile=${daemonProfile} is only the daemon capability ceiling, not this account's permission.`
83
- : `Authenticated account role ${input.role} is configured for profile ${accountPolicy.profile}, but no daemon is connected; only Worker-local tools are currently available.`,
94
+ ? `Authenticated account role ${input.role} has effective profile ${effectiveProfile}. daemon.policy.profile=${daemonProfile} is only the daemon capability ceiling, not this account's permission. Operations within effective authority execute automatically without a per-operation prompt${input.role === "owner" ? "; owner shell, browser, and application automation can act with the daemon OS user's ambient authority" : ""}.`
95
+ : `Authenticated account role ${input.role} is configured for profile ${accountPolicy.profile}, but no daemon is connected; only Worker-local tools are currently available. Operations within effective authority execute automatically without a per-operation prompt.`,
84
96
  };
85
97
  }
86
98
 
@@ -36,7 +36,7 @@ import {
36
36
  } from "./websocket-protocol.ts";
37
37
 
38
38
  const SERVER_NAME = String(serverMetadata.name);
39
- const SERVER_VERSION = "3.0.0-beta.10";
39
+ const SERVER_VERSION = "3.0.0-beta.11";
40
40
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
41
41
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
42
42
  const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
@@ -1,3 +1,5 @@
1
+ import { sanitizePortableLogText } from "../shared/log-redaction.mjs";
2
+
1
3
  const MAX_ERROR_CODES = 64;
2
4
  const MAX_TOOLS = 128;
3
5
  const SENSITIVE_FIELD = /(?:authorization|cookie|credential|password|secret|token|verifier|private[_-]?key)/i;
@@ -63,11 +65,11 @@ export class WorkerObservability {
63
65
 
64
66
  event(level: "info" | "warn" | "error", event: string, fields: Record<string, unknown> = {}): void {
65
67
  const entry = {
68
+ ...sanitizeFields(fields),
66
69
  timestamp: new Date().toISOString(),
67
70
  level,
68
71
  component: "worker",
69
72
  event: sanitizeName(event),
70
- ...sanitizeFields(fields),
71
73
  };
72
74
  const text = JSON.stringify(entry);
73
75
  if (level === "error") console.error(text);
@@ -108,7 +110,7 @@ function sanitizeFields(fields: Record<string, unknown>): Record<string, unknown
108
110
  continue;
109
111
  }
110
112
  if (typeof value === "boolean" || typeof value === "number" || value === null) out[safeKey] = value;
111
- else if (typeof value === "string") out[safeKey] = value.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 256);
113
+ else if (typeof value === "string") out[safeKey] = sanitizePortableLogText(value, { maxChars: 256 });
112
114
  }
113
115
  return out;
114
116
  }