@zackbart/connecta 0.36.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/version.mjs +1 -1
  3. package/dist/auth/downstream-oauth.js +34 -12
  4. package/dist/auth/oauth-authorization-params.js +52 -0
  5. package/dist/auth/static-oauth.js +4 -6
  6. package/dist/auth/stored-observation.js +8 -0
  7. package/dist/call-admission.js +27 -14
  8. package/dist/connectors/api-connector.js +4 -0
  9. package/dist/connectors/option-shapes.js +6 -1
  10. package/dist/connectors/remote-mcp.d.ts +32 -3
  11. package/dist/connectors/remote-mcp.js +152 -21
  12. package/dist/execute.js +5 -5
  13. package/dist/executor-admission.js +27 -13
  14. package/dist/executors/quickjs.js +4 -1
  15. package/dist/google.d.ts +13 -0
  16. package/dist/google.js +42 -0
  17. package/dist/index.d.ts +2 -1
  18. package/dist/invocation.js +6 -7
  19. package/dist/operator-ui/generated.js +2 -2
  20. package/dist/operator-ui/model.d.ts +2 -0
  21. package/dist/providers/_shared/google/service-account.d.ts +1 -1
  22. package/dist/providers/_shared/google/service-account.js +20 -8
  23. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  24. package/dist/request-principal.js +34 -0
  25. package/dist/request-token.d.ts +53 -0
  26. package/dist/request-token.js +1 -0
  27. package/dist/routes/mcp.js +6 -6
  28. package/dist/routes/ui-config.js +38 -9
  29. package/dist/routes/ui-data.js +30 -5
  30. package/dist/routes/ui.js +10 -2
  31. package/dist/runtime/settlement.js +59 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/dist/worker.js +4 -1
  35. package/documentation/README.md +13 -12
  36. package/documentation/architecture.md +91 -26
  37. package/documentation/auth.md +317 -12
  38. package/documentation/code-mode.md +25 -5
  39. package/documentation/integrating.md +18 -4
  40. package/documentation/operator-tests.md +19 -0
  41. package/documentation/operator-ui.md +57 -46
  42. package/examples/worker/README.md +5 -1
  43. package/guides/connecting-agents.md +6 -0
  44. package/guides/safety-and-trust.md +1 -0
  45. package/package.json +5 -1
  46. package/templates/node/package.json +1 -1
@@ -1199,6 +1199,15 @@ meaning. These facts do not change agent error fields or write outcomes.
1199
1199
 
1200
1200
  ## Configuration
1201
1201
 
1202
+ Downstream OAuth browser extensions use `auth.authorizationParams` on
1203
+ `remoteMcp()` and `oauth.authorizationParams` on `api()`. Both snapshot and
1204
+ validate the string map at construction, reserve protocol/security parameters
1205
+ case-insensitively, and add entries only when publishing the consent URL.
1206
+ Remote MCP includes canonical sorted entries in its existing client configuration
1207
+ binding (omitted and empty maps remain equivalent); static API OAuth retains
1208
+ its existing token-endpoint binding. The [auth contract](./auth.md#browser-authorization-parameters)
1209
+ owns limits, reserved names, and reconnect requirements.
1210
+
1202
1211
  `ConnectaConfig` is one schema (`src/config.ts`, combinators in
1203
1212
  `src/config-schema.ts`). The same value produces the TypeScript type a
1204
1213
  deployment writes, the unknown-key walk createConnecta runs before reading any
@@ -1210,8 +1219,8 @@ is resolved in place of the caller's object, so no getter or Proxy trap runs;
1210
1219
  an accessor on a config object or array is refused by path, as is an object
1211
1220
  that cannot be inspected. A closed object or record slot takes a plain object
1212
1221
  only: an array or a class instance there is refused by path, unread. String
1213
- maps (static `headers`, `api()`'s `authorizationParams` and
1214
- `tokenRequestHeaders`) are copied the same way and each value must be a
1222
+ maps (static `headers`, remote MCP and `api()` OAuth `authorizationParams`,
1223
+ and `api()` OAuth `tokenRequestHeaders`) are copied the same way and each value must be a
1215
1224
  string. A discriminated union (`remoteMcp()`'s `auth.type`,
1216
1225
  a provider's `auth.type`) with a missing or unrecognized discriminant throws
1217
1226
  with the valid values. Only plain data is copied. Connectors, modules,
@@ -1744,6 +1753,20 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1744
1753
  Native transport parity, OAuth and write-provenance suites retain their
1745
1754
  workerd coverage. The shared throwaway RSA key in `test/fixtures/rsa.ts` is
1746
1755
  used by write-provenance and unknown-write-advice fixtures.
1756
+ - `test/executor-requests.test.ts` runs overlapping `execute_code` programs
1757
+ as separate workerd requests through the real Worker Loader executor, with
1758
+ real TCP host calls that read their own request's signal. Queued programs
1759
+ and the programs whose release admitted them return their own answers; queue
1760
+ refusals keep the retryable shape. Its fixture applies workerd's request
1761
+ binding of async-context frames, which the pinned runtime predates, through a
1762
+ signal each request owns, so it refuses another request's entry with or
1763
+ without an async-context token, as workerd does; pointing
1764
+ `MINIFLARE_WORKERD_PATH` at a newer workerd runs the native check.
1765
+ `test/executor-native.test.ts` shows that a slot granted just before
1766
+ `close()` never starts a program on the native Worker Loader.
1767
+ `test/request-context.node.test.ts` pins the same wake order and async context
1768
+ on Node for executor and call admission and QuickJS host calls, and gives back
1769
+ grants that shutdown overtook.
1747
1770
  - The request, catalog, auxiliary-call and renewal lifetime
1748
1771
  suites, plus `test/auth-admission-binding.test.ts`, exercise cancellation,
1749
1772
  held native work and garbage collection. These test actual request ownership;
@@ -1770,6 +1793,28 @@ fetch, streams, Web Crypto, lifetime and request-byte behavior, as listed in
1770
1793
 
1771
1794
  ## Optional deployment modules
1772
1795
 
1796
+ `/google` (`src/google.ts`) exports the Promise-based
1797
+ `googleServiceAccountToken` request-token resolver outside the root graph.
1798
+ It reuses `src/providers/_shared/google/service-account.ts` for Web Crypto,
1799
+ in-memory token caching and exact-token invalidation, with an absent subject
1800
+ for service accounts acting as themselves. Workspace providers keep supplying
1801
+ subjects and retain their delegated error guidance. `src/request-token.ts`
1802
+ owns the callable resolver contract and optional HTTP-401 invalidation hook;
1803
+ `remoteMcp()` invokes it without replaying the failed operation. With
1804
+ `auth.principal`, `src/request-principal.ts` reads the admitted caller only
1805
+ through `callerOf` (see `src/connector-caller.ts`), refuses callers the policy
1806
+ does not accept before resolving, and gives `remoteMcp()` a principal key that
1807
+ joins the credential digest, so no client crosses principals. A custom `fetch`
1808
+ replaces global fetch and the owned write sender in that connector's transport,
1809
+ behind the same redirect wrapper and a same-origin guard.
1810
+ `test/remote-mcp-principal-forwarding.test.ts` checks admission, forging,
1811
+ client replacement, transport confinement and secrecy on Node, and a real
1812
+ service binding on workerd.
1813
+ `test/google-service-account-token.test.ts` exercises signing/cache behavior on
1814
+ Node and workerd; `test/google-service-account-remote.node.test.ts` checks
1815
+ real-socket token/MCP requests, invalidation and one physical attempt per write.
1816
+ Operator setup lives in [Google hosted MCP](./auth.md#google-service-account-tokens-and-hosted-mcp).
1817
+
1773
1818
  `createConnecta` takes closed typed `ui`, `vault`, and `activity`
1774
1819
  slots, with factories at `/ui`, `/credentials`, and `/activity`
1775
1820
  and agent tokens at `/auth/access-tokens`. Root exports the contracts, never the implementations, and there
@@ -2004,11 +2049,13 @@ existing execute pipeline, using its request identity and call capabilities.
2004
2049
  The executor seam creates no second request or call owner.
2005
2050
 
2006
2051
  Both built-ins acquire their permits through native admission and return
2007
- execute-once leases with synchronous, idempotent release. A queued standalone
2008
- execution crosses `awaitExecutorLease`'s Promise handoff before host setup, so
2009
- workerd resumes in the waiting request. `execute_code` retains its public
2010
- `acquire()` handoff for that same reason. A cancelled queued handoff withdraws
2011
- its waiter, and a late custom acquisition releases its abandoned lease.
2052
+ execute-once leases with synchronous, idempotent release. Admission wakes a
2053
+ queued waiter through a Settlement, so it resumes in its own request
2054
+ ([across requests](#across-requests)). A queued standalone execution also
2055
+ crosses `awaitExecutorLease`'s Promise handoff before host setup, and
2056
+ `execute_code` retains its public `acquire()` handoff. A cancelled queued
2057
+ handoff withdraws its waiter, and a late custom acquisition releases its
2058
+ abandoned lease.
2012
2059
 
2013
2060
  The Worker SDK prepares source and dispatchers against an inert loader. The
2014
2061
  native host then acquires the actual Loader handle and RPC entrypoint with
@@ -2024,7 +2071,9 @@ Both built-ins normalize provider leaf returns with `Promise.resolve`, retaining
2024
2071
  direct JavaScript callers' synchronous value tolerance and original host
2025
2072
  rejections. The published provider-function signature remains Promise-shaped.
2026
2073
 
2027
- QuickJS owns host-call fibers in each active run's Scope. Completion,
2074
+ QuickJS owns host-call fibers in each active run's Scope, started in the
2075
+ async context the run captured: a warm child's IPC events arrive in the
2076
+ context of the request that spawned it. Completion,
2028
2077
  interruption, cancellation, recycling and shutdown revoke provider lookup and
2029
2078
  interrupt those fibers; late SDK results and stale IPC cannot reply to another
2030
2079
  run. Releasing a lease during readiness or crash backoff also cancels the old
@@ -2209,24 +2258,40 @@ request's abort handler also releases them idempotently if workerd discards the
2209
2258
  pending continuation on disconnect. Membership cache
2210
2259
  hits precede these bounds; see [organization admission](./auth.md#one-clerk-instance-one-organization-per-deployment).
2211
2260
 
2212
- Asynchronous work shared across requests meets only through a Deferred that the owning
2213
- request completes, never through a fiber that outlives its request. Two
2214
- workerd rules follow, and both have bitten.
2215
-
2216
- **Waiting on a shared Deferred is its own edge.** Deferred resumes a waiting
2217
- fiber synchronously, inside the call that completed it, which on Workers means
2218
- inside the completing request's I/O context. A waiter that goes on to touch its
2219
- own body, transport, or storage there fails with "Cannot perform I/O on behalf
2220
- of a different request", or hangs until its deadline. So the wait runs through
2221
- `runEdge`, and the waiter does its own I/O after awaiting that promise, which
2222
- workerd resumes in the waiter's request as it does any promise resolved from
2223
- another. The same holds one level out for every queue: a queued call, a queued
2224
- `execute_code`, and a queued `/mcp` request are each handed their permit from
2225
- inside the request that released one, so each awaits the controller's
2226
- Promise-shaped `acquire` rather than yielding its Effect program, and does its
2227
- own work only once that promise resolves. Effect's `Semaphore` is not used
2228
- across requests for the same reason, and because a same-tick newcomer can take
2229
- a released permit ahead of a waiter that queued earlier.
2261
+ Asynchronous work shared across requests meets only through a `Settlement`
2262
+ (`src/runtime/settlement.ts`) that one request settles, never through a fiber
2263
+ that outlives its request or a Deferred another request awaits. Two workerd
2264
+ rules follow, and both have bitten.
2265
+
2266
+ **No request resumes another request's fiber.** Deferred resumes a waiting
2267
+ fiber synchronously, inside the call that completed it, and Effect re-enters
2268
+ the async context the fiber suspended in through the `AsyncResource` it
2269
+ captured. On Workers that context belongs to the waiter's request. Since
2270
+ workerd's September 2026 releases, a Worker with compatibility date 2025-06-16
2271
+ or later (or `bind_asynclocalstorage_snapshot_to_request`) refuses that entry
2272
+ from any other request: "Cannot call this AsyncLocalStorage bound function
2273
+ outside of the request in which it was created." The throw escapes the
2274
+ completing call, so the request that released an admission slot failed with
2275
+ `executor_failed`, and the stranded waiter later timed out (#1106). Older
2276
+ runtimes ran the waiter in the completing request instead, where its own I/O
2277
+ failed with "Cannot perform I/O on behalf of a different request". Awaiting a
2278
+ Promise around the wait fixes neither: the fiber waiting inside that Promise is
2279
+ the one resumed. A Settlement records its outcome synchronously, for queue
2280
+ arbitration and late-grant cleanup, and wakes each waiter by resolving a
2281
+ Promise the waiter's own request created. workerd runs that Promise's reactions
2282
+ in the request that created it, and Node runs them a microtask later in the
2283
+ waiter's async context. That wake is a turn late, so whatever a waiter acts on
2284
+ it rechecks there: an admission queue gives back a grant its controller closed
2285
+ in between, while a grant that beat the waiter's own abort stays the waiter's to
2286
+ release, and a refresh waiter rechecks its own signal before using a flight's
2287
+ answer. Executor admission, `/mcp` request
2288
+ admission, connector call admission and OAuth refresh flights all queue through
2289
+ Settlements; Effect's `Semaphore` is not used across requests for the same
2290
+ reason, and because a same-tick newcomer can take a released permit ahead of a
2291
+ waiter that queued earlier. Events from shared native objects follow the same
2292
+ rule: a warm QuickJS child's IPC messages arrive in the async context of the
2293
+ request that spawned it, so host calls dispatch inside the running program's
2294
+ captured context.
2230
2295
 
2231
2296
  **Never read another request's signal.** On workerd, reading `aborted` or
2232
2297
  `reason`, or calling `abort()`, on a signal that belongs to another request
@@ -6,16 +6,16 @@
6
6
  and package maintenance. This guide owns identity, membership, and downstream
7
7
  credential contracts; use [deployment setup](./deploying.md) for runnable steps.
8
8
 
9
- | Task | Sections |
10
- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
11
- | Connect a machine or migrate an old client | [Agent tokens](#agent-tokens-and-upgrading-from-earlier-tokens), [agent endpoints](#agent-endpoints), [static bearer migration](#migrating-static-bearer-clients) |
12
- | Configure caller visibility, read-only endpoints, or auth ownership | [Principals and operators](#principals-visibility-and-operators), [pools](#pools), [shared and personal auth](#shared-and-personal-auth) |
13
- | Lay out an agent's pools | [Organizing an agent's pools](#organizing-an-agents-pools) |
14
- | Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [Clerk Organizations](#one-clerk-instance-one-organization-per-deployment), [management](#human-authentication-management) |
15
- | Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
16
- | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
17
- | Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens) |
18
- | Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
9
+ | Task | Sections |
10
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | Connect a machine or migrate an old client | [Agent tokens](#agent-tokens-and-upgrading-from-earlier-tokens), [agent endpoints](#agent-endpoints), [static bearer migration](#migrating-static-bearer-clients) |
12
+ | Configure caller visibility, read-only endpoints, or auth ownership | [Principals and operators](#principals-visibility-and-operators), [pools](#pools), [shared and personal auth](#shared-and-personal-auth) |
13
+ | Lay out an agent's pools | [Organizing an agent's pools](#organizing-an-agents-pools) |
14
+ | Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [Clerk Organizations](#one-clerk-instance-one-organization-per-deployment), [management](#human-authentication-management) |
15
+ | Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
16
+ | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
17
+ | Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens), [principal forwarding](#forwarding-the-admitted-principal), [Google hosted MCP](#google-service-account-tokens-and-hosted-mcp) |
18
+ | Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
19
19
 
20
20
  [Credential recognition and admission](#credential-recognition-and-admission)
21
21
  defines refusal precedence.
@@ -1336,6 +1336,29 @@ healthy. Restart invalidates it even if the connection ends healthy. Continue
1336
1336
  trusts the stored registration's `redirect_uris`; after changing `publicUrl`,
1337
1337
  Restart registers for the current callback URL.
1338
1338
 
1339
+ For `remoteMcp()` OAuth, the owner's stored grant decides whether a connection
1340
+ is connected, not whether the downstream answers. Some servers answer
1341
+ `initialize` and `tools/list` without credentials and challenge only
1342
+ `tools/call`. The operator status, the UI's connected state, and a start agree
1343
+ with the gate a call passes:
1344
+
1345
+ | Stored grant | Status | Start (Connect, Continue, Restart, `authorize_connector`) |
1346
+ | ------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1347
+ | Disconnected | `auth_required`; the downstream is not asked | Replaces the epoch, then proceeds as for no tokens |
1348
+ | No tokens | Connects; `auth_required` even when the downstream answers | Connects first, so a 401's `resource_metadata` starts consent as before. A downstream that answers without credentials still gets consent, discovered from RFC 9728 metadata at the configured URL's well-known locations. The start answers `auth_required` with the consent URL. |
1349
+ | Tokens (any expiry) | Connects; `ok`, or `auth_required` when a 401 needs consent | Connects; `ok` without starting a flow when healthy |
1350
+
1351
+ A start without a challenge runs the same SDK flow as a 401 without
1352
+ `resource_metadata`. Issuer and resource checks, consent state, PKCE, and
1353
+ the CIMD, registration, and static-client order are unchanged, and nothing is
1354
+ sent to the MCP endpoint. A call never starts consent. A call's 401 that names
1355
+ per-tool metadata, such as `/.well-known/oauth-protected-resource/<tool>`,
1356
+ renews through it when the grant holds no discovery. That metadata must name
1357
+ the configured resource, as any other must. When renewal is refused, the grant
1358
+ loses its tokens and the next start hands out consent. Restart (`force: true`)
1359
+ removes the tokens first, so it starts consent unless the downstream or
1360
+ discovery fails.
1361
+
1339
1362
  ### Consents and callbacks
1340
1363
 
1341
1364
  A start stores its flow record, then the grant's pointer to it. A reset that
@@ -2498,6 +2521,48 @@ stored static-client state and reselects the configured identity.
2498
2521
  Configured `scope` is a fallback; a downstream challenge or protected-resource
2499
2522
  declaration takes precedence, and the SDK adds `offline_access` when advertised.
2500
2523
 
2524
+ ### Browser authorization parameters
2525
+
2526
+ Set `auth.authorizationParams` on an OAuth `remoteMcp()` connector to add
2527
+ non-secret provider parameters to the browser consent URL. This works with
2528
+ pre-registered clients, CIMD, and DCR. For example:
2529
+
2530
+ ```ts
2531
+ remoteMcp("crm", {
2532
+ url: "https://mcp.example/mcp",
2533
+ auth: {
2534
+ type: "oauth",
2535
+ scope: "records.read",
2536
+ authorizationParams: { access_type: "offline", prompt: "consent" },
2537
+ },
2538
+ });
2539
+ ```
2540
+
2541
+ Each configured parameter occurs once, replacing any same-name value already
2542
+ on the authorization endpoint URL. These parameters are never added to token
2543
+ exchange, refresh, registration, revocation, metadata, or MCP requests.
2544
+ Descriptions expose parameter names only. Values are public configuration,
2545
+ visible in the consent URL and stored with it; never put secrets here.
2546
+
2547
+ The map must be a plain string record with at most 32 entries. Names contain
2548
+ 1–128 ASCII letters, digits, dots, underscores, or hyphens. Values must be
2549
+ nonempty (not whitespace-only), at most 2048 UTF-16 code units, and contain no
2550
+ ASCII control characters. Case-variant duplicate names are refused. The
2551
+ following names, including every case variant, fail construction with
2552
+ `ConfigError`: `response_type`, `response_mode`, `client_id`, `client_secret`,
2553
+ `redirect_uri`, `state`, `code_challenge`, `code_challenge_method`, `scope`,
2554
+ `resource`, `request`, and `request_uri`. Configure scopes through `auth.scope`.
2555
+ `response_mode` is reserved because Connecta accepts callbacks only as GET query
2556
+ parameters.
2557
+
2558
+ For remote MCP, the map joins the stored client configuration binding, like
2559
+ scope and client identity. Adding, removing, or changing a parameter requires
2560
+ Restart or Disconnect and reconnect to obtain a grant with the new consent
2561
+ settings. The map does not rewrite existing grants or pending consent URLs;
2562
+ as with scope, reconfiguring it alone does not obtain a new grant. A DCR
2563
+ registration created under different parameters cannot be carried through
2564
+ Restart. Key order is irrelevant; an empty map and omission are equivalent.
2565
+
2501
2566
  ## Downstream OAuth on `api()`
2502
2567
 
2503
2568
  `api()` takes a static authorization-code configuration for APIs without MCP
@@ -2532,7 +2597,7 @@ No discovery or registration occurs; all URLs are configuration, so the
2532
2597
  | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2533
2598
  | Endpoints | Checked at construction: HTTPS, HTTP only on loopback, no credentials or fragment. |
2534
2599
  | `pkce` | S256 by default; `false` omits challenge and verifier. |
2535
- | `authorizationParams` | Adds provider parameters; cannot restate the grant's own. |
2600
+ | `authorizationParams` | Browser-only string parameters. Exact names `response_type`, `client_id`, `redirect_uri`, `state`, `scope`, `code_challenge`, `code_challenge_method` and `resource` are refused; other names and string values are accepted as before. Remote MCP's [stricter rules](#browser-authorization-parameters) do not apply here. |
2536
2601
  | `expectedCallbackParameters` | Optional `Readonly<Record<string, string>>` of fixed provider response values. Each configured parameter must occur exactly once with its exact decoded value, after pending-state verification and before issuer validation, flow claims, code exchange, or grant persistence. Missing, duplicate, or mismatched values refuse without exchanging a code or returning parameter values. Omission preserves existing behavior. |
2537
2602
  | `revocationEndpoint` | Optional trusted HTTPS revocation URL (loopback HTTP allowed), without credentials, query or fragment. Disconnect removes the local grant before one bounded attempt using the configured client. Failure reports `oauth_revocation_failed`; a repeated Disconnect never retries. |
2538
2603
  | `revocationRequestFormat` | `"form"` by default; `"json"` sends JSON without `token_type_hint`. Requires `revocationEndpoint` when set. |
@@ -2585,6 +2650,15 @@ owner's access token as `Authorization: Bearer` with these rules:
2585
2650
  - The answer's `.text()` and `.json()`, clones' too, decode its bytes as UTF-8, so
2586
2651
  workerd never quotes a downstream's Content-Type in its own log.
2587
2652
 
2653
+ `api()` already supports `oauth.authorizationParams` through the same OAuth
2654
+ provider and browser-only injection. It keeps its existing, more permissive
2655
+ validation (see the `api()` option table), so existing configurations still
2656
+ construct. Use only GET query response modes with it. Its existing binding remains
2657
+ unchanged: the token endpoint identifies the grant; scope, client identity,
2658
+ and authorization parameters do not add a stored client configuration binding.
2659
+ Changing authorization parameters affects new consents; reconnect to obtain a
2660
+ grant with different consent settings.
2661
+
2588
2662
  The grant record and consents, epoch fencing, vault sealing, shared/personal ownership,
2589
2663
  callback state/principal checks, `authorize_connector`, Connect, Restart, and
2590
2664
  Disconnect match `remoteMcp()`. Status calls report a stored grant for the
@@ -2597,12 +2671,26 @@ For providers offering both, deployment config chooses; handlers check `ctx.oaut
2597
2671
 
2598
2672
  `remoteMcp("<id>", { auth: { type: "request", token: async (ctx) => … } })` resolves
2599
2673
  its token before connecting and before reusing a client. The callback receives
2600
- the operation's cancellation context, never an agent-selected credential owner.
2674
+ the operation's cancellation context only. It never receives an agent-selected
2675
+ credential owner, and without the opt-in below it never receives the caller.
2601
2676
  Only shared ownership is accepted. Tokens remain inside the request's hardened
2602
2677
  transport, are not stored or described, and a changed token replaces the client.
2603
2678
  This mode refuses cleartext non-loopback origins. Optional `auth.headers` carry
2604
2679
  static protocol/catalog controls and cannot set Authorization.
2605
2680
 
2681
+ Return the same token for the same caller while it remains valid, and cache a
2682
+ minted token until shortly before it expires. A value that changes on every
2683
+ resolution replaces the client each time; a change that lands while a call
2684
+ connects fails that call with a retryable error.
2685
+
2686
+ A resolver may expose `token.invalidate(rejectedToken, ctx): Promise<void>` to
2687
+ remove a cached token after a downstream HTTP 401. The hook receives the exact
2688
+ token sent by that transport, so it must leave a newer cached token intact.
2689
+ Hook failures are withheld and preserve the original 401. Request auth never
2690
+ automatically repeats the operation or renews the grant inside the failed
2691
+ scope: authorization failure stays latched for that scope. A new request resolves
2692
+ the callback again. In particular, no dispatched write is replayed.
2693
+
2606
2694
  The GitHub provider validates each tool's owner/repo and scope access before
2607
2695
  resolving an installation token. It creates one token-bound MCP client for that
2608
2696
  operation and closes it before returning. Completed installation/token caches
@@ -2613,6 +2701,223 @@ is retained across requests, and rejected writes are never replayed. An omitted
2613
2701
  `describe()` includes neither the key nor the minted tokens. The maintained
2614
2702
  provider skill explains App permissions and installation prerequisites.
2615
2703
 
2704
+ ### Forwarding the admitted principal
2705
+
2706
+ Set `auth.principal` instead of `auth.token` to let the resolver act for the
2707
+ caller inbound auth admitted. Use it only for a downstream the deployment
2708
+ controls and that already trusts this deployment, so the downstream needs no
2709
+ per-person OAuth:
2710
+
2711
+ ```ts
2712
+ auth: {
2713
+ type: "request",
2714
+ principal: {
2715
+ accept: "human",
2716
+ token: async (ctx, { principal }) => await notesAssertion(principal),
2717
+ },
2718
+ },
2719
+ ```
2720
+
2721
+ - `principal` is `{ kind: "human", namespace, id }`: the admitted person's
2722
+ stable reference, the same `{ namespace, id }` a pool member resolves to.
2723
+ Inbound auth admits no verified email or display name into it; map the id in
2724
+ deployment code or in the downstream when one is needed.
2725
+ - `accept: "human"` admits only people. Agents, anonymous callers,
2726
+ principal-less machine credentials and contexts no request admitted are
2727
+ refused with `auth_required` before the resolver runs. Agents still see the
2728
+ connector, and their calls fail. No identity is ever substituted.
2729
+ - `accept: "human-or-agent"` also admits declared agents as
2730
+ `{ kind: "agent", name, pool }`, the pool being the one the agent arrived on.
2731
+ An agent is never presented as a person, its pool's owner included.
2732
+ - The caller comes only from the authorization that admitted the request.
2733
+ Tool arguments, program code and request headers cannot choose or change it.
2734
+ - The principal joins the client's identity. A different principal never
2735
+ reuses a client, connection or connect in flight, even when the resolver
2736
+ returns the same token. `token.invalidate(rejected, ctx, { principal })`
2737
+ receives the principal the rejected token was minted for.
2738
+ - Catalog listing needs an admitted caller like any call, and its cache stays
2739
+ partitioned per caller. Operator status and catalog checks carry no caller,
2740
+ so they report `auth_required` without connecting.
2741
+ - The connector stays shared. Connecta keeps no per-principal credential state
2742
+ and never stores, logs or describes the principal or the token. `describe()`
2743
+ reports `mode: "request"` as for any request resolver.
2744
+
2745
+ #### Over a Workers service binding
2746
+
2747
+ Pair principal forwarding with the optional `fetch` to reach a sibling Worker
2748
+ through a service binding. Then neither Cloudflare Access, the public internet
2749
+ nor a service token lies between the two Workers:
2750
+
2751
+ ```ts
2752
+ const notes = remoteMcp("notes", {
2753
+ url: "https://notes.example.com/mcp",
2754
+ fetch: (input, init) => env.NOTES.fetch(input, init),
2755
+ auth: {
2756
+ type: "request",
2757
+ principal: {
2758
+ accept: "human",
2759
+ token: async (_ctx, { principal }) => await notesAssertion(principal, env.NOTES_ASSERTION_KEY),
2760
+ },
2761
+ },
2762
+ });
2763
+ ```
2764
+
2765
+ `notesAssertion` is deployment code. It signs a short-lived assertion naming
2766
+ `principal.namespace` and `principal.id` with a key both Workers hold, and
2767
+ caches it per principal until shortly before it expires. Over a binding, the
2768
+ token may instead be an unsigned principal reference, provided the downstream
2769
+ reads it only where the trust rule below holds.
2770
+
2771
+ The downstream must accept a forwarded identity only through its binding
2772
+ entry point or after verifying the signature. Bind to a named entrypoint, such
2773
+ as `{ "binding": "NOTES", "service": "notes", "entrypoint": "Connecta" }` in
2774
+ the deployment's Wrangler `services`, and read the identity only in that
2775
+ entrypoint. Never accept it on a public route: anyone who can reach that route
2776
+ could name any person. A signature verified with a deployment-held key is also
2777
+ safe on a public route.
2778
+
2779
+ `fetch` carries every MCP request, including handshakes, catalog reads, calls,
2780
+ writes and session close. Global fetch is never used for that connector. The
2781
+ configured `url` stays the logical origin: its HTTPS rule, cleartext refusal,
2782
+ redirect policy and same-origin redirect checks apply unchanged. A request for
2783
+ any other origin is refused before it reaches `fetch`. Each request, writes
2784
+ included, is handed to `fetch` once with `redirect: "manual"`, and a dispatched
2785
+ write is never sent again, so `fetch` must not retry or follow redirects. On
2786
+ Node, such writes do not need the owned transport. `fetch` cannot be combined
2787
+ with OAuth auth, because discovery, registration and token requests go to
2788
+ authorization servers the downstream names.
2789
+
2790
+ Contributor evidence: `test/remote-mcp-principal-forwarding.test.ts` runs on
2791
+ Node and, through a real service binding, on workerd.
2792
+
2793
+ ### Google service-account tokens and hosted MCP
2794
+
2795
+ Import `googleServiceAccountToken` from `@zackbart/connecta/google` to connect a
2796
+ Google hosted MCP server using a service-account JSON key on Node or Workers.
2797
+ This optional subpath is outside the root import graph. Supply deployment secrets
2798
+ through the environment or platform secret store:
2799
+
2800
+ ```ts
2801
+ import { remoteMcp } from "@zackbart/connecta";
2802
+ import { googleServiceAccountToken } from "@zackbart/connecta/google";
2803
+
2804
+ const warehouse = remoteMcp("warehouse", {
2805
+ url: "https://bigquery.googleapis.com/mcp",
2806
+ authScope: "shared",
2807
+ auth: {
2808
+ type: "request",
2809
+ token: googleServiceAccountToken({
2810
+ serviceAccount: env.GOOGLE_SERVICE_ACCOUNT_JSON,
2811
+ scopes: ["https://www.googleapis.com/auth/bigquery"],
2812
+ }),
2813
+ },
2814
+ });
2815
+
2816
+ const warehousePersonal = remoteMcp("warehouse_personal", {
2817
+ url: "https://bigquery.googleapis.com/mcp",
2818
+ authScope: "personal",
2819
+ auth: {
2820
+ type: "oauth",
2821
+ scope: "https://www.googleapis.com/auth/bigquery",
2822
+ authorizationParams: { access_type: "offline", prompt: "consent" },
2823
+ client: {
2824
+ issuer: "https://accounts.google.com",
2825
+ clientId: env.GOOGLE_CLIENT_ID,
2826
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
2827
+ tokenEndpointAuthMethod: "client_secret_post",
2828
+ },
2829
+ },
2830
+ });
2831
+ ```
2832
+
2833
+ `serviceAccount` accepts downloaded JSON text or
2834
+ `{ clientEmail, privateKey, clientId? }`; `privateKey` is a PKCS#8 RSA PEM of at
2835
+ least 2048 bits. `scopes` must be a non-empty array of HTTPS URLs without
2836
+ credentials, whitespace, or fragments. Unknown options fail construction with
2837
+ `ConfigError`. An optional fixed `subject: "alice@example.com"` requests
2838
+ Workspace domain-wide delegation and requires its admin grant. Omit `subject`
2839
+ for BigQuery: the signed assertion then has no `sub` and acts as the service
2840
+ account itself. See [Google's service-account grant documentation](https://developers.google.com/identity/protocols/oauth2/service-account).
2841
+
2842
+ The helper signs RS256 JWT-bearer assertions and exchanges them at Google's fixed
2843
+ token endpoint. It keeps only bounded in-memory token entries, partitioned by
2844
+ account, key fingerprint, absent-or-present subject and scope set. Concurrent
2845
+ mints share a flight; tokens refresh with a one-minute margin. Keys, assertions
2846
+ and tokens are never logged, persisted or described. Bearer tokens use request
2847
+ auth's existing secret redaction. A 401 evicts only the rejected token; the next
2848
+ request mints again. The failed operation is never automatically repeated.
2849
+
2850
+ #### Separate agent and people connections
2851
+
2852
+ Grant the shared connection to agent pools (including pools used by Eve agents)
2853
+ and the personal connection to pools used by people. Add these entries to the
2854
+ existing deployment configuration, keeping its inbound auth, executor, vault and
2855
+ storage configuration:
2856
+
2857
+ ```ts
2858
+ connectors: [warehouse, warehousePersonal],
2859
+ agents: ["assistant"],
2860
+ pools: {
2861
+ warehouse_agents: { tools: ["warehouse"], members: ["agent:assistant"] },
2862
+ warehouse_people: { tools: ["warehouse_personal"], members: ["user_alice"] },
2863
+ },
2864
+ ```
2865
+
2866
+ Replace `user_alice` with Alice's admitted principal ID. Agents have no principal;
2867
+ `authScope: "personal"` connectors never appear for them, even if included in a
2868
+ pool's tools. The shared connection uses its fixed service account for every
2869
+ member; the personal connection uses each person's own encrypted OAuth grant.
2870
+ People connect through `authorize_connector` or their member page. Follow
2871
+ [Add a person](#add-a-person), [agent tokens](#agent-tokens-and-upgrading-from-earlier-tokens)
2872
+ and [shared credential ownership](#connect-a-shared-credential) for admission and
2873
+ pool membership. The request-token connection gets its key from deployment
2874
+ configuration and does not need a credential-vault slot.
2875
+
2876
+ #### Google-side setup
2877
+
2878
+ 1. Enable the BigQuery API in the Google Cloud project and confirm that the
2879
+ BigQuery MCP service is available under the project's organization policies.
2880
+ Google's current setup uses `https://bigquery.googleapis.com/mcp` and API
2881
+ enablement; follow its current server setup if enablement requirements change.
2882
+ Grant the service account MCP Tool User (`roles/mcp.toolUser`) and BigQuery
2883
+ Job User (`roles/bigquery.jobUser`) on the project where queries run.
2884
+ [BigQuery MCP setup and required roles](https://docs.cloud.google.com/bigquery/docs/use-bigquery-mcp).
2885
+ 2. Grant BigQuery Data Viewer (`roles/bigquery.dataViewer`) on the datasets the
2886
+ service account may query. Dataset grants restrict data access separately
2887
+ from project-level job execution. Grant further permissions only for the
2888
+ operations you intend to expose; OAuth scopes do not replace IAM.
2889
+ [BigQuery IAM roles and grant levels](https://docs.cloud.google.com/bigquery/docs/access-control).
2890
+ Create a service-account JSON key and store it as
2891
+ `GOOGLE_SERVICE_ACCOUNT_JSON` in the deployment secret store; the helper
2892
+ does not use ADC, metadata-server credentials, or workload identity federation.
2893
+ [Service-account key creation](https://docs.cloud.google.com/iam/docs/keys-create-delete).
2894
+ 3. For the personal connection, configure Google Auth Platform branding,
2895
+ audience and data access. Choose **Internal** for an application restricted to
2896
+ people in your Workspace organization, using a project belonging to that
2897
+ organization. Internal consent does not grant dataset access; each person
2898
+ still needs the applicable IAM permissions.
2899
+ [Configure consent and audience](https://developers.google.com/workspace/guides/configure-oauth-consent).
2900
+ 4. Create a **Web application** OAuth client under Google Auth Platform → Clients.
2901
+ Register the exact Connecta callback, for example
2902
+ `https://connecta.example/oauth/callback/warehouse_personal`, using the
2903
+ deployment's configured public origin. Store its client ID and secret as
2904
+ deployment secrets. Google's hosted MCP servers do not support DCR or client
2905
+ metadata documents, so configure the static `auth.client` shown above.
2906
+ [Web OAuth client setup](https://docs.cloud.google.com/mcp/set-up-authentication-mcp-servers#web),
2907
+ [Google MCP registration limitations](https://docs.cloud.google.com/mcp/authenticate-mcp#limitations).
2908
+
2909
+ The pre-registered client path supports Google's exact issuer and
2910
+ `client_secret_post`; Google's [discovery document](https://accounts.google.com/.well-known/openid-configuration)
2911
+ advertises that method, PKCE S256, and `https://oauth2.googleapis.com/token`.
2912
+ The personal example requests `access_type=offline` to obtain a refresh token
2913
+ and `prompt=consent` to show consent again when reconnecting. See
2914
+ [Google's offline-access requirements](https://developers.google.com/identity/protocols/oauth2/web-server#offline).
2915
+ Connecta retains the stored refresh token when a successful refresh response
2916
+ omits `refresh_token`, allowing later refreshes with that same token. After
2917
+ adding these parameters to an existing connector, Disconnect and reconnect.
2918
+ Live Google consent and project-specific permissions still need verification
2919
+ against the operator's project.
2920
+
2616
2921
  ## Contributor reference
2617
2922
 
2618
2923
  ### INV-5 output redaction limits
@@ -65,9 +65,15 @@ createConnecta({ executor: workerExecutor({ loader: env.LOADER }), readOnly: fal
65
65
  ```
66
66
 
67
67
  The adapter uses only `loader` and an optional `timeout`. Its admission defaults
68
- are two active runs, eight queued runs, and a five-second queue deadline, settable
69
- through `admission`. Lease release disposes RPC and loader handles independently
70
- of guest settlement. `bindings`, `modules`, or `globalOutbound` violate `P2`.
68
+ are two active runs, eight queued runs, a 60-second queue deadline (the default
69
+ guest deadline) and a one-second `retryAfterMs` hint. `admission` replaces all
70
+ four: `{ concurrency, maxQueueSize, queueTimeoutMs, retryAfterMs? }`, where an
71
+ omitted `retryAfterMs` equals `queueTimeoutMs`. The pool belongs to the executor
72
+ instance, so a Worker that builds one per isolate bounds each isolate, not the
73
+ deployment or a caller: concurrent requests that land on one isolate share its
74
+ slots, and requests on other isolates do not count. Lease release disposes RPC
75
+ and loader handles independently of guest settlement. `bindings`, `modules`, or
76
+ `globalOutbound` violate `P2`.
71
77
  The [Worker example](../examples/worker/README.md#code-mode) has the full setup.
72
78
 
73
79
  Keep the Worker's optional peer. Unbranded executors throw, including bundled upstream copies.
@@ -981,7 +987,21 @@ payload is refused before parsing and names no address.
981
987
  plus a bounded queue with a wait timeout. Overload is a retryable
982
988
  `executor_overloaded` carrying `retryAfterMs`; cancellation and shutdown are
983
989
  terminal. Admission happens _before_ any catalog or provider is built, so a
984
- queued request holds no state.
990
+ queued request holds no state. A full queue refuses at once with
991
+ `Executor queue is full.`; a queued run that outlives `queueTimeoutMs` is
992
+ refused with `Executor admission timed out after <n>ms.`. Either arrives as an
993
+ error result whose `structuredContent.error` is
994
+ `{ code: "executor_overloaded", message, retryable: true, retryAfterMs }`, with
995
+ `hostCalls` all zero; the program never started, so retrying it after
996
+ `retryAfterMs` sends nothing twice. Whichever request releases a slot, the
997
+ queued run resumes, builds its providers, makes its host calls and holds its
998
+ sandbox handles in its own request; the releasing request's result is
999
+ unaffected. A slot handed to a queued run is heard a turn after it is granted.
1000
+ When the executor closes in that gap, the slot is given back and the run is
1001
+ refused with `executor_closed`, so nothing granted before `close()` starts after
1002
+ it. A grant that beat the caller's abort stays the caller's, which releases it
1003
+ on arrival. A queued run also holds its MCP request permit
1004
+ ([request admission](./architecture.md#admission-in-two-places)) while it waits.
985
1005
 
986
1006
  **L8.** Result, log, host-call, and emission budgets are deployment configuration.
987
1007
  A call may request `timeoutMs` just as a direct call does; the guest deadline is
@@ -1287,7 +1307,7 @@ rejection, and branded adapter acceptance across module copies.
1287
1307
  | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls, discovery included, and refuses discovery after the run; a cancelled wedged executor, or one whose `acquire()` ignores the signal, returns promptly and releases its lease) |
1288
1308
  | `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors), `test/execute.test.ts` (the watchdog ends a never-settling executor, frees the default pool, spares a slow run, and falls back from an unusable value) |
1289
1309
  | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets and terminal catch-and-continue loops and queued-write cancellation on both executors), `test/worker-budget-response.node.test.ts` (native handle disposal and admission recovery across completed HTTP responses) |
1290
- | `L5`, `L7`, `X2` | `test/quickjs-executor.node.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue) |
1310
+ | `L5`, `L7`, `X2` | `test/quickjs-executor.node.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue), `test/executor-requests.test.ts` (queued handoffs and refusal shape across separate workerd requests), `test/request-context.node.test.ts` (queued and QuickJS host-call request context) |
1291
1311
  | `L6`, `X10` | `test/guest-api-contract-quickjs.node.test.ts` and `test/guest-api-contract.test.ts` (whole oversized reads and one-write pageable handles), `test/sqlite-storage.node.test.ts` and `test/kv-to-d1.test.ts` (SQL stash and native D1 fault probes), `test/quickjs-executor.node.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.node.test.ts` (outer reply serialization failure settles the call) |
1292
1312
  | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, the identity clamp, the one-attempt floor), `test/browser/operator-ui.spec.ts` (identity changes), `test/sql-storage-contract.ts`, run by `test/sqlite-storage.node.test.ts` (historical activity rows still round-trip) |
1293
1313
  | `V5`, `W9` | `test/program-writes.test.ts` (unawaited program drain, failed-program accounting), `test/identity-write-journey.node.test.ts` (committed ambiguity, reconciliation and physical writes), `test/invocation-pipeline.test.ts` (the gate after validation, an unrecorded refusal) |