machine-bridge-mcp 1.0.6 → 1.0.8
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 +15 -0
- package/README.md +4 -2
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/CLIENTS.md +2 -2
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/MULTI_ACCOUNT.md +2 -0
- package/docs/OPERATIONS.md +2 -1
- package/docs/TESTING.md +2 -1
- package/docs/TOOL_REFERENCE.md +2 -2
- package/package.json +4 -3
- package/src/shared/server-metadata.json +2 -2
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/authority.ts +146 -0
- package/src/worker/http.ts +12 -2
- package/src/worker/index.ts +25 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.8 - 2026-07-14
|
|
4
|
+
|
|
5
|
+
### Effective account authority reporting
|
|
6
|
+
|
|
7
|
+
- Separate the authenticated account authority from the local daemon capability ceiling in remote diagnostics. `server_info.authorization.effective_policy` and `effective_tools` are now the authoritative account fields, while `daemon.policy` and `daemon.tools` are explicitly scoped as pre-role ceilings. The response also includes a deterministic authority summary and scope labels so clients cannot reasonably equate an `editor` account with a `full` daemon.
|
|
8
|
+
- Make remote `project_overview` return the role-intersected policy and tools at its existing top-level fields, while preserving the daemon values separately as `daemonPolicy` and `daemonTools`. Add a Worker integration regression that authorizes an `editor` account against a canonical `full` daemon and proves shell/browser tools remain excluded at both reporting surfaces and at relay enforcement.
|
|
9
|
+
- Update MCP initialization guidance, tool descriptions, operations diagnostics, architecture, client, account, testing, and generated tool-reference documentation to use the effective-account fields when diagnosing permissions.
|
|
10
|
+
|
|
11
|
+
## 1.0.7 - 2026-07-14
|
|
12
|
+
|
|
13
|
+
### Browser-complete OAuth callback navigation
|
|
14
|
+
|
|
15
|
+
- Fix the authorization page CSP so a successful same-origin form POST may follow its `303 See Other` to the exact, already registered and validated OAuth redirect origin. The previous static `form-action 'self'` policy let the Worker issue a correct redirect but caused Chromium to block the callback navigation and leave the user on the consent page.
|
|
16
|
+
- Keep the policy narrow: each consent page receives only `'self'` plus that request's exact redirect origin, never a wildcard or scheme-wide allowance. Add a real headless-Chrome negative/positive regression proving the old policy blocks the callback and the new policy preserves `code` and `state`; retain Worker-level CSP, PKCE, redirect/resource, password, and token tests. Invalid credentials now preserve only the non-secret account name and display an accessible error while never reflecting the password.
|
|
17
|
+
|
|
3
18
|
## 1.0.6 - 2026-07-14
|
|
4
19
|
|
|
5
20
|
### OAuth navigation origin handling
|
package/README.md
CHANGED
|
@@ -138,12 +138,14 @@ MCP Server URL: https://<worker>.<account>.workers.dev/mcp
|
|
|
138
138
|
|
|
139
139
|
During OAuth authorization, enter the name and password of a Machine Bridge account. The first start creates `owner` and prints its generated password once. Additional accounts are managed with `machine-mcp account`. The authorization flow uses PKCE S256, exact redirect/resource binding, expiring hashed access tokens, per-account version checks, and a deployment-wide token version for emergency bulk revocation.
|
|
140
140
|
|
|
141
|
-
The Worker grants browser CORS response access to the exact first-party origins used by ChatGPT (`https://chatgpt.com` and the legacy `https://chat.openai.com`) and Grok (`https://grok.com` and its X-hosted surface at `https://x.com`). Users do not need to run Wrangler or edit Cloudflare variables. `MBM_ALLOWED_ORIGINS` remains an operator-only, comma-separated extension point for other exact origins; it is additive and does not replace the built-in set. Wildcards and `null` do not receive CORS permission. Actual OAuth navigations and form submissions are not rejected solely because a browser container supplies an opaque or client-specific `Origin`; authorization still depends on exact redirect/resource binding, PKCE, account authentication, and bearer-token validation.
|
|
141
|
+
The Worker grants browser CORS response access to the exact first-party origins used by ChatGPT (`https://chatgpt.com` and the legacy `https://chat.openai.com`) and Grok (`https://grok.com` and its X-hosted surface at `https://x.com`). Users do not need to run Wrangler or edit Cloudflare variables. `MBM_ALLOWED_ORIGINS` remains an operator-only, comma-separated extension point for other exact origins; it is additive and does not replace the built-in set. Wildcards and `null` do not receive CORS permission. Actual OAuth navigations and form submissions are not rejected solely because a browser container supplies an opaque or client-specific `Origin`; authorization still depends on exact redirect/resource binding, PKCE, account authentication, and bearer-token validation. The consent page CSP permits form navigation only to the Worker itself and the exact validated redirect origin for that request, so Chromium can follow the successful `303` callback without granting arbitrary form destinations.
|
|
142
142
|
|
|
143
143
|
### Multiple clients and accounts
|
|
144
144
|
|
|
145
145
|
One Worker supports several named accounts and OAuth clients. Accounts have independent passwords, roles, active state, versions, codes, and tokens. The roles are `reviewer`, `editor`, `operator`, and `owner`; their effective authority is intersected with the connected daemon policy and enforced in both the Worker and local runtime. Role changes, suspension, password rotation, and removal revoke only the affected account.
|
|
146
146
|
|
|
147
|
+
For remote connections, `server_info.authorization.effective_policy` and `server_info.authorization.effective_tools` are the authoritative account-permission fields. `server_info.daemon.policy` and `server_info.daemon.tools` describe only the local daemon capability ceiling before account-role filtering. Remote `project_overview` likewise places the effective account policy/tools in `policy` and `tools`, while preserving the daemon ceiling separately as `daemonPolicy` and `daemonTools`.
|
|
148
|
+
|
|
147
149
|
This is application-level authorization, not kernel or browser-profile isolation. All accounts reach one daemon running as one OS user. Use separate Workers and preferably separate low-privilege OS accounts, containers, or VMs for mutually untrusted users or hard tenant isolation. See [Multi-account authorization and tenancy](docs/MULTI_ACCOUNT.md).
|
|
148
150
|
|
|
149
151
|
## Optional local stdio MCP
|
|
@@ -497,7 +499,7 @@ npm audit --omit=dev --audit-level=high
|
|
|
497
499
|
npm pack --dry-run
|
|
498
500
|
```
|
|
499
501
|
|
|
500
|
-
`npm run check` covers privacy and release-impact gates, architecture/link invariants, generated Worker types, TypeScript, recursively discovered JavaScript syntax, semantic undefined-identifier checks, catalog-to-runtime handler parity, deterministic relay lifecycle and secure-file tests, concurrent lock/atomic-replacement/PID-reuse fixtures, fail-closed service lifecycle tests, descendant process-tree termination, local path/write/state/log/service invariants, Ed25519/RSA generation and key-pair validation, real-machine `full` sandbox acceptance, a clean npm package-manifest/mode/sensitive-artifact check, an isolated installation of the actual packed tarball whose zero-argument CLI startup must reach a controlled Worker-deployment boundary, managed-job integrity/redaction/finally/cancellation/recovery, a live stdio MCP flow,
|
|
502
|
+
`npm run check` covers privacy and release-impact gates, architecture/link invariants, generated Worker types, TypeScript, recursively discovered JavaScript syntax, semantic undefined-identifier checks, catalog-to-runtime handler parity, deterministic relay lifecycle and secure-file tests, concurrent lock/atomic-replacement/PID-reuse fixtures, fail-closed service lifecycle tests, descendant process-tree termination, local path/write/state/log/service invariants, Ed25519/RSA generation and key-pair validation, real-machine `full` sandbox acceptance, a clean npm package-manifest/mode/sensitive-artifact check, an isolated installation of the actual packed tarball whose zero-argument CLI startup must reach a controlled Worker-deployment boundary, managed-job integrity/redaction/finally/cancellation/recovery, a live stdio MCP flow, a live local OAuth/Worker/WebSocket/MCP flow, and a real Chromium negative/positive test for CSP-governed OAuth callback navigation. GitHub Actions runs the complete suite on Linux, macOS, and Windows with the pinned Node 26/npm 12 baseline.
|
|
501
503
|
|
|
502
504
|
The cross-cutting 0.12.0 review, corrected failure modes, and residual OS-level limits are recorded in [docs/AUDIT.md](docs/AUDIT.md). See also [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md), [docs/MULTI_ACCOUNT.md](docs/MULTI_ACCOUNT.md), [docs/AGENT_CONTEXT.md](docs/AGENT_CONTEXT.md), [docs/LOCAL_AUTOMATION.md](docs/LOCAL_AUTOMATION.md), [docs/MANAGED_JOBS.md](docs/MANAGED_JOBS.md), [docs/TESTING.md](docs/TESTING.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/ENGINEERING.md](docs/ENGINEERING.md), [docs/PROJECT_STANDARDS.md](docs/PROJECT_STANDARDS.md), the generated [MCP tool reference](docs/TOOL_REFERENCE.md), and [SECURITY.md](SECURITY.md).
|
|
503
505
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Machine Bridge Browser",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"action": {
|
|
31
31
|
"default_title": "Machine Bridge Browser"
|
|
32
32
|
},
|
|
33
|
-
"version_name": "1.0.
|
|
33
|
+
"version_name": "1.0.8"
|
|
34
34
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -235,11 +235,11 @@ Removal first acquires a state-root maintenance lock that blocks new profile/sta
|
|
|
235
235
|
|
|
236
236
|
OAuth metadata is pruned on access. Expired codes/tokens, old throttling records, and inactive clients without active credentials are removed. Source identities are deployment-keyed HMAC values, not stored source addresses or reversible unsalted hashes.
|
|
237
237
|
|
|
238
|
-
Browser-origin handling separates CORS response sharing from protocol authentication. Preflight succeeds only for the Worker's own origin, a fixed first-party set for ChatGPT (`https://chatgpt.com`, `https://chat.openai.com`) and Grok (`https://grok.com`, `https://x.com`), or optional exact comma-separated additions from `MBM_ALLOWED_ORIGINS`; wildcard and `null` origins are not granted CORS access. Actual requests are routed without using `Origin` as an authentication boundary, and `Access-Control-Allow-Origin` is added only when that exact predicate passes. This permits OAuth top-level navigation and form submission from opaque or client-specific containers while exact redirect/resource binding, PKCE, account credentials, bearer tokens, and private admin/daemon secrets enforce authority.
|
|
238
|
+
Browser-origin handling separates CORS response sharing from protocol authentication. Preflight succeeds only for the Worker's own origin, a fixed first-party set for ChatGPT (`https://chatgpt.com`, `https://chat.openai.com`) and Grok (`https://grok.com`, `https://x.com`), or optional exact comma-separated additions from `MBM_ALLOWED_ORIGINS`; wildcard and `null` origins are not granted CORS access. Actual requests are routed without using `Origin` as an authentication boundary, and `Access-Control-Allow-Origin` is added only when that exact predicate passes. This permits OAuth top-level navigation and form submission from opaque or client-specific containers while exact redirect/resource binding, PKCE, account credentials, bearer tokens, and private admin/daemon secrets enforce authority. The authorization document's `form-action` policy is generated only after request validation and contains `'self'` plus the exact origin of that validated redirect URI. This allows Chromium to follow the successful form submission's `303` callback without opening form navigation to unrelated origins.
|
|
239
239
|
|
|
240
240
|
## Observability
|
|
241
241
|
|
|
242
|
-
Public health exposes only server identity and version. Authenticated `server_info` exposes bounded runtime status,
|
|
242
|
+
Public health exposes only server identity and version. Authenticated `server_info` exposes bounded runtime status, managed-job counts, resource alias names without paths or values, relay route state without endpoint details, and privacy-preserving capability-routing evidence. It separates the daemon capability ceiling from the authenticated account authority: `daemon.policy`/`daemon.tools` retain the pre-role ceiling, while `authorization.effective_policy`/`authorization.effective_tools` and the top-level `tools` report the role-intersected authority before any host-side filtering. It explicitly reports that the host-exposed subset is unknown to the server. `diagnose_runtime` runs fixed local probes and explicitly reports that its own request reached the daemon.
|
|
243
243
|
|
|
244
244
|
Foreground logging defaults to `info`; autostart uses `warn`. Authenticated readiness, persistent degradation, and recovery are user-visible state transitions. Brief relay interruptions, raw transport close details, retry timing, and all per-tool starts/successes/failures/cancellations/durations are debug-only. Unexpected local and Worker infrastructure errors are reduced to classes. Messages, strings, arrays, object depth/key counts, and serialized fields are bounded.
|
|
245
245
|
|
package/docs/CLIENTS.md
CHANGED
|
@@ -122,7 +122,7 @@ Run:
|
|
|
122
122
|
machine-mcp --workspace /path/to/project
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
Enter the printed `/mcp` URL in the remote MCP connector. During OAuth authorization, verify the displayed client name and redirect URI before entering a Machine Bridge account name and password. The Worker has exact built-in CORS response support for `https://chatgpt.com`, legacy `https://chat.openai.com`, `https://grok.com`, and the X-hosted Grok surface at `https://x.com`; no direct Wrangler command or Cloudflare variable edit is required. OAuth navigations and form submissions from opaque or client-specific browser containers are routed normally but receive no cross-origin response-sharing permission unless their exact origin is allowed.
|
|
125
|
+
Enter the printed `/mcp` URL in the remote MCP connector. During OAuth authorization, verify the displayed client name and redirect URI before entering a Machine Bridge account name and password. The Worker has exact built-in CORS response support for `https://chatgpt.com`, legacy `https://chat.openai.com`, `https://grok.com`, and the X-hosted Grok surface at `https://x.com`; no direct Wrangler command or Cloudflare variable edit is required. OAuth navigations and form submissions from opaque or client-specific browser containers are routed normally but receive no cross-origin response-sharing permission unless their exact origin is allowed. The authorization page also permits form navigation only to itself and the exact validated redirect origin, which is required for Chromium to follow the successful `303` back to the client.
|
|
126
126
|
|
|
127
127
|
Several OAuth clients and named accounts can coexist. Accounts have independent passwords, roles, active state, versions, and targeted revocation. Their effective tool sets are intersected with the connected daemon policy. All accounts still share one daemon and OS user, so hard tenant isolation requires separate deployments; see [MULTI_ACCOUNT.md](MULTI_ACCOUNT.md).
|
|
128
128
|
|
|
@@ -149,7 +149,7 @@ For SSH automation, prefer `generate_ssh_key_resource` under canonical full, or
|
|
|
149
149
|
|
|
150
150
|
The local `full` profile controls Machine Bridge's own tool catalog, path resolver, path display, process environment, and shell availability. It does not control the MCP host's model policy, approval UI, connector gateway, or platform execution filters.
|
|
151
151
|
|
|
152
|
-
Machine Bridge itself does not block files because their names look sensitive.
|
|
152
|
+
Machine Bridge itself does not block files because their names look sensitive. In remote mode, first inspect `server_info.authorization.effective_policy` and `effective_tools`; `daemon.policy` is only the local ceiling. If the effective profile is `full` and the effective tool is present but a direct call is still rejected before a structured result, the host/connector may have blocked delivery. If `diagnose_runtime` responds but its fixed process or shell probe fails, the likely source is local OS policy, endpoint-security software, permissions, or shell configuration. Changing `--profile`, `--unrestricted-paths`, or `--absolute-paths` cannot override either layer.
|
|
153
153
|
|
|
154
154
|
Do not attempt to evade a host refusal by renaming, encoding, or switching to another arbitrary execution tool. Instead:
|
|
155
155
|
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -204,7 +204,7 @@ Current ChatGPT developer-mode flow, as documented in OpenAI's [Connect from Cha
|
|
|
204
204
|
7. Enter the Machine Bridge account name and password only after those values are recognized.
|
|
205
205
|
8. Create a new chat and enable the app for that conversation.
|
|
206
206
|
|
|
207
|
-
ChatGPT navigation labels can change. The invariant is that the client must connect to the public `/mcp` endpoint and complete the OAuth authorization page served by the Worker. The same public `/mcp` URL can be entered in Grok where remote MCP configuration is available. Machine Bridge grants CORS response access to the exact `https://grok.com` and `https://x.com` browser origins as well as ChatGPT's current and legacy origins without requiring a Wrangler command. OAuth navigation and form POST requests are not failed solely because an embedded authorization container reports an opaque or different origin.
|
|
207
|
+
ChatGPT navigation labels can change. The invariant is that the client must connect to the public `/mcp` endpoint and complete the OAuth authorization page served by the Worker. The same public `/mcp` URL can be entered in Grok where remote MCP configuration is available. Machine Bridge grants CORS response access to the exact `https://grok.com` and `https://x.com` browser origins as well as ChatGPT's current and legacy origins without requiring a Wrangler command. OAuth navigation and form POST requests are not failed solely because an embedded authorization container reports an opaque or different origin. After validation, the consent page CSP includes only the exact registered callback origin so the browser can complete the `303` return to ChatGPT or Grok.
|
|
208
208
|
|
|
209
209
|
## 8. Verify the first connection
|
|
210
210
|
|
package/docs/MULTI_ACCOUNT.md
CHANGED
|
@@ -25,6 +25,8 @@ The effective tool set is the intersection of:
|
|
|
25
25
|
|
|
26
26
|
The Worker filters `tools/list` and rejects unauthorized calls before relay. Every accepted relay call also carries `account_id`, `account_version`, and `role`; the local runtime validates the role again before dispatch. The shared policy contract remains the single source of capability semantics.
|
|
27
27
|
|
|
28
|
+
Authenticated `server_info` deliberately reports both layers. `authorization.effective_policy` and `authorization.effective_tools` are authoritative for the current account. The nested `daemon.policy` and `daemon.tools` fields are only the local capability ceiling before account-role filtering; a `full` daemon does not make an `editor` account full. Remote `project_overview` uses the effective values at top-level `policy` and `tools` and preserves the daemon values as `daemonPolicy` and `daemonTools`.
|
|
29
|
+
|
|
28
30
|
## Account lifecycle
|
|
29
31
|
|
|
30
32
|
List accounts:
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -14,7 +14,8 @@ machine-mcp service status
|
|
|
14
14
|
|
|
15
15
|
| Result | Interpretation |
|
|
16
16
|
|---|---|
|
|
17
|
-
| `
|
|
17
|
+
| `authorization.effective_policy.profile` is `full` and the tool is in `authorization.effective_tools`, but the current session UI exposes fewer tools | Host/connector post-relay filtering; Machine Bridge cannot enumerate or override that subset |
|
|
18
|
+
| `daemon.policy.profile` is `full` but `authorization.effective_policy.profile` is `review`, `edit`, or `agent` | Expected account-role narrowing; the daemon field is only a capability ceiling and must not be reported as the account permission |
|
|
18
19
|
| `capability_routing.bootstrap_observed` is false | The current local runtime has not received `session_bootstrap`; reconnect or inspect host initialization handling |
|
|
19
20
|
| `task_resolution_observed` is false after a substantive task | The host/model did not call `resolve_task_capabilities`; server-side discovery cannot force that host decision |
|
|
20
21
|
| Task resolution ran but all match counts are zero | The resolver ran successfully but found no sufficiently relevant local skill, command, or application |
|
package/docs/TESTING.md
CHANGED
|
@@ -53,8 +53,9 @@ The suite includes:
|
|
|
53
53
|
- deterministic property tests over hostile browser-protocol byte strings, canonical/custom policy combinations, argv bounds/NULs, and a real direct process proving shell metacharacters remain literal argv;
|
|
54
54
|
- CLI parsing, policy profiles, and client configuration boundaries;
|
|
55
55
|
- live stdio MCP initialization with session instructions, capability resolution, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
|
56
|
-
- live local Worker OAuth registration, consent, URL-constructed `303` callbacks including the ChatGPT redirect URI and encoded state, PKCE, token replay rejection, throttling, exact built-in ChatGPT/Grok browser origins, additive custom origins, unrelated-origin preflight rejection, no CORS response sharing for unrelated or opaque origins, opaque-origin authorization-form routing, protocol negotiation, HMAC-bound MCP session issuance, two-session same-id concurrency, sessionless same-id independence, session-scoped cancellation isolation, same-session duplicate rejection, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure.
|
|
56
|
+
- live local Worker OAuth registration, consent, URL-constructed `303` callbacks including the ChatGPT redirect URI and encoded state, PKCE, token replay rejection, throttling, exact built-in ChatGPT/Grok browser origins, additive custom origins, unrelated-origin preflight rejection, no CORS response sharing for unrelated or opaque origins, opaque-origin authorization-form routing, exact per-request redirect-origin CSP, accessible credential-error rendering, protocol negotiation, HMAC-bound MCP session issuance, two-session same-id concurrency, sessionless same-id independence, session-scoped cancellation isolation, same-session duplicate rejection, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure. The same integration runs an `editor` account against a canonical `full` daemon and proves that `server_info` and remote `project_overview` report effective `edit` authority while retaining the full daemon ceiling only in explicitly scoped fields.
|
|
57
57
|
- local runtime proof that one blocked tool handler does not serialize an independent handler, plus relay fault injection proving an undeliverable terminal result interrupts the ambiguous socket and enters reconnect backoff.
|
|
58
|
+
- a real headless-Chrome OAuth navigation regression with a negative control showing that `form-action 'self'` blocks a cross-origin `303`, followed by the production policy shape proving the exact callback origin receives `code` and `state`. Linux CI fails if Chrome is unavailable; other environments skip only this browser executable check while retaining the Worker CSP assertions.
|
|
58
59
|
|
|
59
60
|
## Opt-in live desktop and browser validation
|
|
60
61
|
|
package/docs/TOOL_REFERENCE.md
CHANGED
|
@@ -66,7 +66,7 @@ Tool count: **51**.
|
|
|
66
66
|
|
|
67
67
|
**Server information**
|
|
68
68
|
|
|
69
|
-
Return
|
|
69
|
+
Return authenticated account authority, effective policy/tools, daemon capability ceiling, runtime metadata, and protocol status. Treat authorization.effective_policy and authorization.effective_tools as authoritative; daemon.policy is only a ceiling.
|
|
70
70
|
|
|
71
71
|
| Contract field | Value |
|
|
72
72
|
|---|---|
|
|
@@ -89,7 +89,7 @@ Return bridge/runtime metadata, active policy, protocol versions, and available
|
|
|
89
89
|
|
|
90
90
|
**Project overview**
|
|
91
91
|
|
|
92
|
-
Summarize the connected workspace
|
|
92
|
+
Summarize the connected workspace and repository. Remote responses report the authenticated account effective policy/tools at policy and tools, with the daemon capability ceiling preserved separately as daemonPolicy and daemonTools.
|
|
93
93
|
|
|
94
94
|
| Contract field | Value |
|
|
95
95
|
|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"worker:types": "node scripts/generate-worker-types.mjs",
|
|
44
44
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
45
45
|
"syntax": "node scripts/syntax-check.mjs",
|
|
46
|
-
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run worker-secret-file:test && npm run sarif-security:test && npm run security-properties:test && npm run shell:test && npm run architecture:test && npm run markdown:test && npm run project-metadata:test && npm run numbers:test && npm run records:test && npm run state-inventory:test && npm run commit-message:test && npm run policy:test && npm run account:test && npm run policy-docs:check && npm run tool-docs:check && npm run runtime-infrastructure:test && npm run runtime-handlers:test && npm run cli-entrypoint:test && npm run lifecycle:test && npm run logging-structure:test && npm run coverage:test && npm run worker-runtime-infrastructure:test && npm run lint:test && npm run lint && npm run typecheck && npm run syntax && npm run deadline:test && npm run process-lock:test && npm run service-lifecycle:test && npm run service-platform:test && npm run catalog:test && npm run agent-context:test && npm run capability-ranking:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-service-worker:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run install: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
|
+
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run worker-secret-file:test && npm run sarif-security:test && npm run security-properties:test && npm run shell:test && npm run architecture:test && npm run markdown:test && npm run project-metadata:test && npm run numbers:test && npm run records:test && npm run state-inventory:test && npm run commit-message:test && npm run policy:test && npm run account:test && npm run policy-docs:check && npm run tool-docs:check && npm run runtime-infrastructure:test && npm run runtime-handlers:test && npm run cli-entrypoint:test && npm run lifecycle:test && npm run logging-structure:test && npm run coverage:test && npm run worker-runtime-infrastructure:test && npm run lint:test && npm run lint && npm run typecheck && npm run syntax && npm run deadline:test && npm run process-lock:test && npm run service-lifecycle:test && npm run service-platform:test && npm run catalog:test && npm run agent-context:test && npm run capability-ranking:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-service-worker:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run install: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 && npm run oauth-browser:test",
|
|
47
47
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
48
48
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
49
49
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -106,7 +106,8 @@
|
|
|
106
106
|
"lint": "eslint eslint.config.mjs bin src/local scripts tests browser-extension",
|
|
107
107
|
"lint:test": "node tests/lint-gate-test.mjs",
|
|
108
108
|
"shell:test": "node tests/shell-test.mjs",
|
|
109
|
-
"deadline:test": "node tests/monotonic-deadline-test.mjs"
|
|
109
|
+
"deadline:test": "node tests/monotonic-deadline-test.mjs",
|
|
110
|
+
"oauth-browser:test": "node tests/oauth-browser-navigation-test.mjs"
|
|
110
111
|
},
|
|
111
112
|
"dependencies": {
|
|
112
113
|
"https-proxy-agent": "9.1.0",
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"For existing-browser work, use the paired Machine Bridge browser extension. It operates the user current Chromium profile and tabs, can inspect DOM/source, fill complex forms, and inject sensitive text from registered local resources without returning those values.",
|
|
13
13
|
"For local application work, use structured application discovery and Accessibility actions. Arbitrary caller-supplied AppleScript or JavaScript is intentionally not accepted.",
|
|
14
14
|
"Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
|
|
15
|
-
"
|
|
15
|
+
"In remote multi-account mode, account authority is the intersection of the authenticated account role and the daemon capability ceiling. Only authorization.effective_policy and authorization.effective_tools describe that account authority; daemon.policy is not the account permission.",
|
|
16
16
|
"Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",
|
|
17
17
|
"A named full profile is canonical: it always enables writes, shell execution, unrestricted paths, the full parent environment, absolute paths, and the complete tool catalog. Any explicit narrowing is stored as custom.",
|
|
18
|
-
"The full catalog
|
|
18
|
+
"The local daemon may advertise the full catalog as its capability ceiling. In remote mode, the Worker relays only the intersection allowed by the authenticated account role; a connector host may then expose an even smaller subset that Machine Bridge cannot observe or override.",
|
|
19
19
|
"Use diagnose_runtime to distinguish a request that reached the daemon from local filesystem, process-spawn, shell, managed-job-storage, and resource failures. A tool call blocked before any response cannot be diagnosed by the server.",
|
|
20
20
|
"Never request or return secret-file contents when a local resource alias can be used. Resources are registered through the local machine-mcp CLI; under canonical full policy, generate_ssh_key_resource can generate and register an Ed25519 key without returning private content and may be injected by path, stdin, or environment without entering MCP arguments.",
|
|
21
21
|
"If execution-class tools are unavailable but write access remains, use stage_job to persist a reviewed plan without running it; the operator can launch it locally with machine-mcp job approve JOB_ID.",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{
|
|
3
3
|
"name": "server_info",
|
|
4
4
|
"title": "Server information",
|
|
5
|
-
"description": "Return
|
|
5
|
+
"description": "Return authenticated account authority, effective policy/tools, daemon capability ceiling, runtime metadata, and protocol status. Treat authorization.effective_policy and authorization.effective_tools as authoritative; daemon.policy is only a ceiling.",
|
|
6
6
|
"availability": "always",
|
|
7
7
|
"annotations": {
|
|
8
8
|
"readOnlyHint": true,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
{
|
|
19
19
|
"name": "project_overview",
|
|
20
20
|
"title": "Project overview",
|
|
21
|
-
"description": "Summarize the connected workspace
|
|
21
|
+
"description": "Summarize the connected workspace and repository. Remote responses report the authenticated account effective policy/tools at policy and tools, with the daemon capability ceiling preserved separately as daemonPolicy and daemonTools.",
|
|
22
22
|
"availability": "always",
|
|
23
23
|
"annotations": {
|
|
24
24
|
"readOnlyHint": true,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import policyContract from "../shared/policy-contract.json";
|
|
2
|
+
import { accountRolePolicy, accountRoleToolNames, type AccountRole } from "./access";
|
|
3
|
+
import type { DaemonPolicy } from "./policy";
|
|
4
|
+
|
|
5
|
+
type EffectivePolicy = {
|
|
6
|
+
scope: "authenticated_account_effective_authority";
|
|
7
|
+
derived_from: ["account_role_policy", "daemon_capability_ceiling"];
|
|
8
|
+
profile: string;
|
|
9
|
+
revision: number;
|
|
10
|
+
allowWrite: boolean;
|
|
11
|
+
allowExec: boolean;
|
|
12
|
+
execMode: DaemonPolicy["execMode"];
|
|
13
|
+
unrestrictedPaths: boolean;
|
|
14
|
+
minimalEnv: boolean;
|
|
15
|
+
exposeAbsolutePaths: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type AccountIdentity = { accountId: string; accountVersion: number; role: AccountRole };
|
|
19
|
+
|
|
20
|
+
type AccountAuthority = {
|
|
21
|
+
account: { account_id: string; role: AccountRole; version: number };
|
|
22
|
+
account_policy: DaemonPolicy & { scope: "account_role_capability_ceiling" };
|
|
23
|
+
daemon_policy_ceiling: DaemonPolicy | null;
|
|
24
|
+
effective_policy: EffectivePolicy | null;
|
|
25
|
+
effective_tools: string[];
|
|
26
|
+
effective_tool_count: number;
|
|
27
|
+
account_role_is_owner: boolean;
|
|
28
|
+
effective_profile_is_full: boolean;
|
|
29
|
+
interpretation: {
|
|
30
|
+
authoritative_permission_fields: ["effective_policy", "effective_tools"];
|
|
31
|
+
daemon_policy: string;
|
|
32
|
+
host_filtering: string;
|
|
33
|
+
};
|
|
34
|
+
summary: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type DaemonCeilingStatus = Record<string, unknown> & {
|
|
38
|
+
policy: DaemonPolicy | null;
|
|
39
|
+
tools: string[];
|
|
40
|
+
policy_scope: "daemon_capability_ceiling_not_account_authority";
|
|
41
|
+
tools_scope: "daemon_advertised_before_account_role_filtering";
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const profileFields = ["allowWrite", "execMode", "unrestrictedPaths", "minimalEnv", "exposeAbsolutePaths"] as const;
|
|
45
|
+
const profiles = policyContract.profiles as Record<string, Pick<DaemonPolicy, typeof profileFields[number]> & { profile: string }>;
|
|
46
|
+
const execRank: Record<DaemonPolicy["execMode"], number> = { off: 0, direct: 1, shell: 2 };
|
|
47
|
+
|
|
48
|
+
export function describeDaemonCeiling(value: Record<string, unknown>): DaemonCeilingStatus {
|
|
49
|
+
return {
|
|
50
|
+
...value,
|
|
51
|
+
policy: isDaemonPolicy(value.policy) ? value.policy : null,
|
|
52
|
+
tools: Array.isArray(value.tools) ? value.tools.filter((item): item is string => typeof item === "string") : [],
|
|
53
|
+
policy_scope: "daemon_capability_ceiling_not_account_authority",
|
|
54
|
+
tools_scope: "daemon_advertised_before_account_role_filtering",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function accountAuthoritySnapshot(input: AccountIdentity & {
|
|
59
|
+
daemonPolicy: DaemonPolicy | null;
|
|
60
|
+
effectiveTools: string[];
|
|
61
|
+
}): AccountAuthority {
|
|
62
|
+
const accountPolicy = accountRolePolicy(input.role);
|
|
63
|
+
const effectivePolicy = input.daemonPolicy ? intersectPolicies(accountPolicy, input.daemonPolicy) : null;
|
|
64
|
+
const effectiveTools = [...new Set(input.effectiveTools)];
|
|
65
|
+
const daemonProfile = input.daemonPolicy?.profile ?? "disconnected";
|
|
66
|
+
const effectiveProfile = effectivePolicy?.profile ?? "unavailable";
|
|
67
|
+
return {
|
|
68
|
+
account: { account_id: input.accountId, role: input.role, version: input.accountVersion },
|
|
69
|
+
account_policy: { ...accountPolicy, scope: "account_role_capability_ceiling" },
|
|
70
|
+
daemon_policy_ceiling: input.daemonPolicy,
|
|
71
|
+
effective_policy: effectivePolicy,
|
|
72
|
+
effective_tools: effectiveTools,
|
|
73
|
+
effective_tool_count: effectiveTools.length,
|
|
74
|
+
account_role_is_owner: input.role === "owner",
|
|
75
|
+
effective_profile_is_full: effectivePolicy?.profile === "full",
|
|
76
|
+
interpretation: {
|
|
77
|
+
authoritative_permission_fields: ["effective_policy", "effective_tools"],
|
|
78
|
+
daemon_policy: "daemon.policy is only the local daemon capability ceiling; it is not the authenticated account permission",
|
|
79
|
+
host_filtering: "the MCP host may expose a smaller subset than effective_tools, and Machine Bridge cannot observe that post-relay subset",
|
|
80
|
+
},
|
|
81
|
+
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.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function decorateProjectOverview(value: unknown, account: AccountIdentity): unknown {
|
|
88
|
+
if (!isRecord(value)) return value;
|
|
89
|
+
const daemonPolicy = isDaemonPolicy(value.policy) ? value.policy : null;
|
|
90
|
+
const daemonTools = Array.isArray(value.tools) ? value.tools.filter((item): item is string => typeof item === "string") : [];
|
|
91
|
+
const effectiveTools = ["server_info", ...accountRoleToolNames(account.role, daemonTools)];
|
|
92
|
+
const authority = accountAuthoritySnapshot({ ...account, daemonPolicy, effectiveTools });
|
|
93
|
+
return {
|
|
94
|
+
...value,
|
|
95
|
+
daemonPolicy,
|
|
96
|
+
daemonTools,
|
|
97
|
+
policy: authority.effective_policy,
|
|
98
|
+
tools: authority.effective_tools,
|
|
99
|
+
policyScope: "authenticated_account_effective_authority",
|
|
100
|
+
toolsScope: "authenticated_account_effective_tools_before_host_filtering",
|
|
101
|
+
authorization: authority,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function intersectPolicies(accountPolicy: DaemonPolicy, daemonPolicy: DaemonPolicy): EffectivePolicy {
|
|
106
|
+
const execMode = execRank[accountPolicy.execMode] <= execRank[daemonPolicy.execMode] ? accountPolicy.execMode : daemonPolicy.execMode;
|
|
107
|
+
const capabilities = {
|
|
108
|
+
allowWrite: accountPolicy.allowWrite && daemonPolicy.allowWrite,
|
|
109
|
+
execMode,
|
|
110
|
+
unrestrictedPaths: accountPolicy.unrestrictedPaths && daemonPolicy.unrestrictedPaths,
|
|
111
|
+
minimalEnv: accountPolicy.minimalEnv || daemonPolicy.minimalEnv,
|
|
112
|
+
exposeAbsolutePaths: accountPolicy.exposeAbsolutePaths && daemonPolicy.exposeAbsolutePaths,
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
scope: "authenticated_account_effective_authority",
|
|
116
|
+
derived_from: ["account_role_policy", "daemon_capability_ceiling"],
|
|
117
|
+
profile: matchingProfile(capabilities),
|
|
118
|
+
revision: Math.max(accountPolicy.revision, daemonPolicy.revision),
|
|
119
|
+
...capabilities,
|
|
120
|
+
allowExec: execMode !== "off",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function matchingProfile(capabilities: Pick<EffectivePolicy, typeof profileFields[number]>): string {
|
|
125
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
126
|
+
if (profileFields.every((field) => profile[field] === capabilities[field])) return name;
|
|
127
|
+
}
|
|
128
|
+
return "custom";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isDaemonPolicy(value: unknown): value is DaemonPolicy {
|
|
132
|
+
if (!isRecord(value)) return false;
|
|
133
|
+
return typeof value.profile === "string"
|
|
134
|
+
&& typeof value.origin === "string"
|
|
135
|
+
&& Number.isInteger(value.revision)
|
|
136
|
+
&& typeof value.allowWrite === "boolean"
|
|
137
|
+
&& typeof value.allowExec === "boolean"
|
|
138
|
+
&& (value.execMode === "off" || value.execMode === "direct" || value.execMode === "shell")
|
|
139
|
+
&& typeof value.unrestrictedPaths === "boolean"
|
|
140
|
+
&& typeof value.minimalEnv === "boolean"
|
|
141
|
+
&& typeof value.exposeAbsolutePaths === "boolean";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
145
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
146
|
+
}
|
package/src/worker/http.ts
CHANGED
|
@@ -62,13 +62,23 @@ export function json(value: unknown, status = 200, extraHeaders: HeadersInit = {
|
|
|
62
62
|
return new Response(JSON.stringify(value), { status, headers });
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export function html(value: string, status = 200): Response {
|
|
65
|
+
export function html(value: string, status = 200, formActionOrigin = ""): Response {
|
|
66
|
+
const allowedFormActions = new Set(["'self'"]);
|
|
67
|
+
if (formActionOrigin) {
|
|
68
|
+
try {
|
|
69
|
+
const origin = new URL(formActionOrigin).origin;
|
|
70
|
+
if (origin !== "null") allowedFormActions.add(origin);
|
|
71
|
+
} catch {
|
|
72
|
+
// Invalid values are ignored; callers pass a validated OAuth redirect origin.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const formAction = [...allowedFormActions].join(" ");
|
|
66
76
|
return new Response(value, {
|
|
67
77
|
status,
|
|
68
78
|
headers: {
|
|
69
79
|
"content-type": "text/html; charset=utf-8",
|
|
70
80
|
"cache-control": "no-store",
|
|
71
|
-
"content-security-policy":
|
|
81
|
+
"content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; form-action ${formAction}; base-uri 'none'; frame-ancestors 'none'`,
|
|
72
82
|
"referrer-policy": "no-referrer",
|
|
73
83
|
"x-content-type-options": "nosniff",
|
|
74
84
|
"x-frame-options": "DENY",
|
package/src/worker/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { WorkerObservability } from "./observability";
|
|
|
7
7
|
import { daemonToolError, publicWorkerToolError, WorkerToolError } from "./errors";
|
|
8
8
|
import { sanitizeDaemonPolicy, sanitizeDaemonTools, type DaemonPolicy } from "./policy";
|
|
9
9
|
import { accountRoleAllowsTool, accountRoleToolNames, type AccountRole } from "./access";
|
|
10
|
+
import { accountAuthoritySnapshot, decorateProjectOverview, describeDaemonCeiling } from "./authority";
|
|
10
11
|
import { accountAdminAuthorized, handleAccountAdminOperation } from "./account-admin";
|
|
11
12
|
import { serverInfoTool, workspaceTools } from "./tool-catalog";
|
|
12
13
|
import {
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
} from "./http";
|
|
23
24
|
|
|
24
25
|
const SERVER_NAME = String(serverMetadata.name);
|
|
25
|
-
const SERVER_VERSION = "1.0.
|
|
26
|
+
const SERVER_VERSION = "1.0.8";
|
|
26
27
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
27
28
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
28
29
|
const JSONRPC_VERSION = "2.0";
|
|
@@ -342,17 +343,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
342
343
|
}
|
|
343
344
|
return rpcError(request.id, -32601, `Method not found: ${request.method}`);
|
|
344
345
|
}
|
|
345
|
-
|
|
346
346
|
private async callTool(name: string, args: Record<string, unknown>, base: string, authorized: AuthorizedToken, requestKey?: string): Promise<unknown> {
|
|
347
347
|
if (name === "server_info") {
|
|
348
|
-
const daemon = this.
|
|
349
|
-
const tools = this.allTools(authorized.role).map((tool) => tool.name);
|
|
348
|
+
const { daemon, tools, authorization } = this.authorityContext(authorized);
|
|
350
349
|
return {
|
|
351
350
|
name: SERVER_NAME,
|
|
352
351
|
version: SERVER_VERSION,
|
|
353
352
|
mcp_url: `${base}/mcp`,
|
|
354
353
|
oauth: this.authorizationServerMetadata(base),
|
|
355
|
-
account:
|
|
354
|
+
account: authorization.account,
|
|
355
|
+
authorization,
|
|
356
|
+
authority_summary: authorization.summary,
|
|
356
357
|
daemon,
|
|
357
358
|
worker: {
|
|
358
359
|
pending_calls: this.pending.snapshot(),
|
|
@@ -360,10 +361,13 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
360
361
|
observability: this.observability.snapshot(),
|
|
361
362
|
},
|
|
362
363
|
tools,
|
|
364
|
+
tools_scope: "authenticated_account_effective_tools_before_host_filtering",
|
|
363
365
|
tool_delivery: {
|
|
364
|
-
full_profile_scope: "
|
|
366
|
+
full_profile_scope: "daemon-capability-ceiling-before-account-filtering",
|
|
365
367
|
daemon_advertised_tool_count: daemon.tool_count,
|
|
366
368
|
relay_advertised_tool_count: tools.length,
|
|
369
|
+
effective_account_tool_count: tools.length,
|
|
370
|
+
relay_advertised_scope: "authenticated_account_effective_tools_before_host_filtering",
|
|
367
371
|
host_exposed_tools_known_to_server: false,
|
|
368
372
|
host_may_expose_subset: true,
|
|
369
373
|
},
|
|
@@ -372,11 +376,12 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
372
376
|
if (workspaceTools.some((tool) => tool.name === name)) {
|
|
373
377
|
if (!this.daemonToolEnabled(name)) throw new Error(`tool disabled by local daemon policy: ${name}`);
|
|
374
378
|
if (!accountRoleAllowsTool(authorized.role, name)) throw new WorkerToolError("authorization_denied", "tool is not allowed for this account role");
|
|
375
|
-
|
|
379
|
+
const result = await this.callDaemonTool(name, args, authorized, requestKey);
|
|
380
|
+
return name === "project_overview" ? decorateProjectOverview(result, { accountId: authorized.accountId,
|
|
381
|
+
accountVersion: authorized.accountVersion, role: authorized.role }) : result;
|
|
376
382
|
}
|
|
377
383
|
throw new Error(`unknown tool: ${name}`);
|
|
378
384
|
}
|
|
379
|
-
|
|
380
385
|
private async callDaemonTool(name: string, args: Record<string, unknown>, authorized: AuthorizedToken, requestKey?: string): Promise<unknown> {
|
|
381
386
|
const socket = this.daemonSockets()[0];
|
|
382
387
|
if (!socket) throw new WorkerToolError("unavailable", "local daemon is not connected; keep the CLI start command running", true);
|
|
@@ -419,7 +424,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
419
424
|
throw error;
|
|
420
425
|
}
|
|
421
426
|
}
|
|
422
|
-
|
|
423
427
|
private cancelClientRequest(requestKey?: string): void {
|
|
424
428
|
if (!requestKey) return;
|
|
425
429
|
this.pending.cancelRequest(requestKey, (record) => {
|
|
@@ -456,11 +460,16 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
456
460
|
const localTools = workspaceTools.filter((tool) => advertised.has(tool.name));
|
|
457
461
|
return [serverInfoTool, ...localTools].map((tool) => structuredClone(tool));
|
|
458
462
|
}
|
|
459
|
-
|
|
463
|
+
private authorityContext(authorized: AuthorizedToken) {
|
|
464
|
+
const daemon = describeDaemonCeiling(this.daemonStatus(true));
|
|
465
|
+
const tools = this.allTools(authorized.role).map((tool) => String(tool.name));
|
|
466
|
+
const authorization = accountAuthoritySnapshot({ accountId: authorized.accountId, accountVersion: authorized.accountVersion,
|
|
467
|
+
role: authorized.role, daemonPolicy: daemon.policy, effectiveTools: tools });
|
|
468
|
+
return { daemon, tools, authorization };
|
|
469
|
+
}
|
|
460
470
|
private daemonToolEnabled(name: string): boolean {
|
|
461
471
|
return this.daemonAdvertisedTools().has(name);
|
|
462
472
|
}
|
|
463
|
-
|
|
464
473
|
private daemonAdvertisedTools(): Set<string> {
|
|
465
474
|
const socket = this.daemonSockets()[0];
|
|
466
475
|
if (!socket) return new Set();
|
|
@@ -746,15 +755,17 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
746
755
|
? `<p><strong>Client:</strong> ${escapeHtml(authorization.client.client_name)}</p>
|
|
747
756
|
<p><strong>Redirect URI:</strong> <code>${escapeHtml(authorization.redirectUri)}</code></p>`
|
|
748
757
|
: "";
|
|
749
|
-
const errorBlock = error ? `<p role="alert" style="color:#b91c1c">${escapeHtml(error)}</p>` : "";
|
|
758
|
+
const errorBlock = error ? `<p role="alert" aria-live="assertive" style="color:#b91c1c; font-weight:600">${escapeHtml(error)}</p>` : "";
|
|
759
|
+
const accountName = normalizeDisplayText(String(submitted?.account_name ?? ""), 64, "");
|
|
750
760
|
const form = allowSubmit
|
|
751
761
|
? `<form method="post" action="/oauth/authorize">
|
|
752
762
|
${hidden}
|
|
753
|
-
<label>Account name<br><input name="account_name" autocomplete="username" autofocus required style="width: 100%; box-sizing: border-box; padding: 8px;"></label>
|
|
763
|
+
<label>Account name<br><input name="account_name" value="${escapeHtml(accountName)}" autocomplete="username" autofocus required style="width: 100%; box-sizing: border-box; padding: 8px;"></label>
|
|
754
764
|
<p><label>Account password<br><input name="account_password" type="password" autocomplete="current-password" required style="width: 100%; box-sizing: border-box; padding: 8px;"></label></p>
|
|
755
765
|
<p><button type="submit">Authorize</button></p>
|
|
756
766
|
</form>`
|
|
757
767
|
: "<p>Authorization cannot continue. Return to the MCP client and start the connection again.</p>";
|
|
768
|
+
const redirectOrigin = authorization ? new URL(authorization.redirectUri).origin : "";
|
|
758
769
|
return html(`<!doctype html>
|
|
759
770
|
<html>
|
|
760
771
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Authorize ${SERVER_NAME}</title></head>
|
|
@@ -766,7 +777,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
766
777
|
${errorBlock}
|
|
767
778
|
${form}
|
|
768
779
|
</body>
|
|
769
|
-
</html>`, status);
|
|
780
|
+
</html>`, status, redirectOrigin);
|
|
770
781
|
}
|
|
771
782
|
|
|
772
783
|
private async authorizeSubmit(request: Request, base: string): Promise<Response> {
|