@zackbart/connecta 0.24.3 → 0.25.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 (145) hide show
  1. package/AGENTS.md +58 -30
  2. package/CHANGELOG.md +317 -1
  3. package/README.md +5 -6
  4. package/dist/advertised-schema.js +53 -0
  5. package/dist/auth/downstream-oauth.js +490 -106
  6. package/dist/branding.d.ts +31 -2
  7. package/dist/branding.js +116 -8
  8. package/dist/call-admission.js +139 -113
  9. package/dist/catalog-service.js +141 -105
  10. package/dist/catalog.js +258 -33
  11. package/dist/concurrency.js +24 -23
  12. package/dist/connector-scope.js +6 -37
  13. package/dist/connectors/api.d.ts +3 -3
  14. package/dist/connectors/api.js +10 -2
  15. package/dist/connectors/guarded-fetch.js +34 -4
  16. package/dist/connectors/remote-mcp.d.ts +6 -5
  17. package/dist/connectors/remote-mcp.js +325 -245
  18. package/dist/credential-contract.d.ts +9 -0
  19. package/dist/credential-rules.js +2 -2
  20. package/dist/credentials.d.ts +6 -1
  21. package/dist/credentials.js +45 -3
  22. package/dist/errors.d.ts +11 -3
  23. package/dist/errors.js +2 -1
  24. package/dist/execute.js +363 -257
  25. package/dist/executor-admission.js +67 -50
  26. package/dist/executors/quickjs-runtime.js +4 -2
  27. package/dist/executors/quickjs.js +251 -210
  28. package/dist/fix-prompt.js +37 -0
  29. package/dist/index.d.ts +11 -2
  30. package/dist/index.js +63 -21
  31. package/dist/invocation.js +324 -285
  32. package/dist/meta-tools.js +304 -114
  33. package/dist/oauth-callback-outcome.js +94 -0
  34. package/dist/oauth-sealing.js +29 -0
  35. package/dist/operator-ui/fix-prompts.js +87 -0
  36. package/dist/operator-ui/generated.js +2 -2
  37. package/dist/operator-ui/model.d.ts +46 -1
  38. package/dist/operator-ui/setup-commands.js +53 -0
  39. package/dist/operator-ui/view.js +109 -2
  40. package/dist/program-source.js +205 -0
  41. package/dist/providers/cloudflare.d.ts +14 -2
  42. package/dist/providers/cloudflare.js +107 -16
  43. package/dist/providers/linear.d.ts +26 -4
  44. package/dist/providers/linear.js +19 -4
  45. package/dist/providers/mixpanel.d.ts +16 -3
  46. package/dist/providers/mixpanel.js +13 -2
  47. package/dist/providers/notion.d.ts +8 -1
  48. package/dist/providers/notion.js +85 -12
  49. package/dist/providers/revenuecat.d.ts +30 -4
  50. package/dist/providers/revenuecat.js +70 -5
  51. package/dist/providers/stripe.d.ts +10 -3
  52. package/dist/providers/stripe.js +30 -4
  53. package/dist/providers/vercel.js +12 -2
  54. package/dist/registry.d.ts +13 -5
  55. package/dist/registry.js +317 -258
  56. package/dist/routes/activity.js +120 -137
  57. package/dist/routes/credentials.js +174 -163
  58. package/dist/routes/mcp.js +129 -70
  59. package/dist/routes/oauth-management.js +60 -71
  60. package/dist/routes/oauth.js +44 -16
  61. package/dist/routes/operator.js +82 -0
  62. package/dist/routes/shared.d.ts +2 -0
  63. package/dist/routes/ui-data.js +210 -0
  64. package/dist/routes/ui.js +64 -32
  65. package/dist/runtime/admission.js +43 -0
  66. package/dist/runtime/call-admission.js +67 -0
  67. package/dist/runtime/connector-scope.js +72 -0
  68. package/dist/runtime/run.js +174 -0
  69. package/dist/runtime/services.js +130 -0
  70. package/dist/runtime/storage.js +66 -0
  71. package/dist/server.js +111 -89
  72. package/dist/skills.js +1 -1
  73. package/dist/storage/file.js +39 -2
  74. package/dist/storage/memory.js +28 -16
  75. package/dist/timeout.js +17 -47
  76. package/dist/types.d.ts +64 -7
  77. package/dist/ui.d.ts +16 -2
  78. package/dist/ui.js +68 -176
  79. package/dist/url-safety.js +157 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/documentation/architecture.md +368 -182
  83. package/documentation/auth.md +277 -176
  84. package/documentation/code-mode.md +479 -329
  85. package/documentation/meta-tools.md +494 -417
  86. package/ethos.md +44 -47
  87. package/examples/worker/AGENTS.md +2 -1
  88. package/examples/worker/README.md +62 -14
  89. package/examples/worker/src/d1-storage.ts +116 -0
  90. package/examples/worker/src/index.ts +18 -21
  91. package/examples/worker/wrangler.jsonc +16 -4
  92. package/package.json +11 -4
  93. package/templates/node/.env.example +3 -3
  94. package/templates/node/AGENTS.md +5 -4
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +23 -22
  98. package/dist/auth/downstream-oauth.d.ts +0 -200
  99. package/dist/catalog-fingerprint.d.ts +0 -19
  100. package/dist/catalog-limits.d.ts +0 -9
  101. package/dist/catalog-service.d.ts +0 -263
  102. package/dist/catalog.d.ts +0 -87
  103. package/dist/concurrency.d.ts +0 -3
  104. package/dist/connectors/guarded-fetch.d.ts +0 -92
  105. package/dist/credential-rules.d.ts +0 -85
  106. package/dist/execute.d.ts +0 -154
  107. package/dist/executor-result.d.ts +0 -10
  108. package/dist/executors/quickjs-child.d.ts +0 -1
  109. package/dist/executors/quickjs-protocol.d.ts +0 -67
  110. package/dist/favicon.d.ts +0 -1
  111. package/dist/identity.d.ts +0 -4
  112. package/dist/invocation.d.ts +0 -74
  113. package/dist/mcp-result.d.ts +0 -8
  114. package/dist/meta-tools.d.ts +0 -129
  115. package/dist/operator-ui/generated.d.ts +0 -2
  116. package/dist/operator-ui/view.d.ts +0 -151
  117. package/dist/result-shapes.d.ts +0 -13
  118. package/dist/retired-toolkits.d.ts +0 -2
  119. package/dist/routes/activity.d.ts +0 -2
  120. package/dist/routes/credentials.d.ts +0 -2
  121. package/dist/routes/mcp.d.ts +0 -10
  122. package/dist/routes/oauth-management.d.ts +0 -2
  123. package/dist/routes/oauth.d.ts +0 -2
  124. package/dist/routes/ui.d.ts +0 -2
  125. package/dist/server.d.ts +0 -9
  126. package/dist/skills.d.ts +0 -44
  127. package/dist/timeout.d.ts +0 -21
  128. package/dist/tool-safety.d.ts +0 -9
  129. package/documentation/call-admission.md +0 -158
  130. package/documentation/cloudflare.md +0 -471
  131. package/documentation/connector-guides.md +0 -176
  132. package/documentation/connectors.md +0 -431
  133. package/documentation/linear.md +0 -193
  134. package/documentation/mixpanel.md +0 -160
  135. package/documentation/notion.md +0 -308
  136. package/documentation/operations.md +0 -359
  137. package/documentation/operator-ui.md +0 -135
  138. package/documentation/optional-modules-upgrade.md +0 -243
  139. package/documentation/provider-conventions.md +0 -729
  140. package/documentation/request-admission.md +0 -204
  141. package/documentation/revenuecat.md +0 -305
  142. package/documentation/storage-and-credentials.md +0 -254
  143. package/documentation/stripe.md +0 -262
  144. package/documentation/upgrading.md +0 -768
  145. package/documentation/vercel.md +0 -241
@@ -4,8 +4,10 @@ One Web-standard `fetch(request) => Promise<Response>` handler, a long-lived
4
4
  registry behind it, and a strict rule about what may be imported. Everything
5
5
  else in this repository is a detail of those three things.
6
6
 
7
- Read [`ethos.md`](../ethos.md) first. This guide says how the shape it
8
- describes is actually assembled, and where a change is likely to break it.
7
+ Read [`ethos.md`](../ethos.md) first; this guide says how the shape it describes
8
+ is assembled and where each subsystem lives. The surface itself belongs to
9
+ [meta-tools](./meta-tools.md), [code mode](./code-mode.md), and
10
+ [inbound auth](./auth.md).
9
11
 
10
12
  ## The two lifetimes
11
13
 
@@ -13,234 +15,418 @@ Almost every bug in this codebase is a lifetime mistake, so the split is worth
13
15
  stating before anything else.
14
16
 
15
17
  **Per isolate, built once.** `createConnecta(config)` returns
16
- `{ fetch, registry, close }`. The `Registry` owns the connector set, address
17
- resolution, catalog caches, observed output schemas, connector health, and the
18
- per-connector call limiters. It is constructed once and lives as long as the
19
- isolate or process —
20
- on Workers that means a lazy module-scope singleton, which is why both
21
- deployment shapes build it outside the request handler.
18
+ `{ fetch, registry, close }` (`src/index.ts`). The `Registry` owns the connector
19
+ set, address resolution, catalog caches, observed output schemas, connector
20
+ health, and the per-connector call limiters. It is built once and lives as long
21
+ as the isolate — on Workers a lazy module-scope singleton, which is why both
22
+ deployment shapes construct it outside the request handler.
22
23
 
23
24
  An OAuth `remoteMcp()` connector also owns a runtime-local refresh completion
24
- gate. It coordinates credential mutation across concurrent request scopes but
25
- never shares their clients, transports, or responses, and never lets a follower
26
- cancel the owner. Every participant still awaits the refresh inside its own
27
- request lifetime; a cancelled follower leaves the shared owner untouched and
28
- removes only its own wait. The owner's request signal belongs to its token
29
- fetch. Cancelling that owner fails current joiners too because promoting one
30
- could replay a refresh token the authorization server already consumed.
31
-
32
- A valid token response is a consumed refresh token whether or not the owner
33
- survives to save it. The coordinator therefore keeps the accepted tokens on the
34
- flight, and when the owner fails after that response — cancelled, redirected
35
- to authorization, or invalidated — it persists the rotation on the host's own
36
- write, holds contenders behind the pending-mutation marker until that write
37
- lands, and hands them the saved rotation. No contender ever redeems the retired
38
- token again, and the marker can no longer outlive the write that clears it
39
- ([#526](https://github.com/zackbart/connecta/issues/526)).
40
-
41
- The coordinator retains the owner's abort signal only through one temporary
42
- listener on the exact active refresh. Save, failure, cancellation, or
43
- generation retirement removes it along with the map entry. It never retains a
44
- token response, client, or transport. If cancellation lands after a valid
45
- response while its credential write is still running, a generation-keyed
46
- identity marker rejects new owners until that exact write finishes. The marker
47
- contains no promise and generation retirement removes it.
25
+ gate (`src/auth/downstream-oauth.ts`). It coordinates credential mutation across
26
+ concurrent request scopes while sharing no client, transport, or response, and
27
+ never lets a follower cancel the owner. The subtle part is that a valid token
28
+ response consumes the refresh token whether or not the owner survives to save it,
29
+ so the accepted tokens live on the flight: cancelling the owner *before* a valid
30
+ response fails the joiners, because promoting one could replay a token the
31
+ authorization server already consumed, while cancelling it *after* one does not —
32
+ the host persists the rotation on its own write, holds contenders behind a
33
+ generation-keyed pending-mutation marker until that write lands, and hands them
34
+ the saved rotation ([#526](https://github.com/zackbart/connecta/issues/526)).
48
35
 
49
36
  **Per request, and no longer.** The MCP server, its transport, downstream MCP
50
37
  clients, abort signals, and the connector scope a probe opens all belong to the
51
- request that created them. `Nothing request-bound survives a request` is an
52
- ethos invariant, not a style preference: a client retained across requests on
53
- Workers is a cross-request capability leak, and a promise awaited after the
54
- response is work the runtime may have already torn down. Deferred work has one
55
- sanctioned channel — `ctx.waitUntil`, threaded through `fetch(request, env,
56
- ctx)`. Best-effort activity writes use it. An agent read that already demanded
57
- an expired catalog refresh may also use it while serving a complete catalog
58
- inside its stale window. That refresh owns a new scope and deadline; it never
59
- carries the inbound scope or signal past the request.
60
-
61
- The registry is deliberately on the long side of that line and the MCP server
62
- deliberately on the short side. A fresh `McpServer` per request is what makes
63
- the deployment stateless: no sessions, no server push, no resumability, and
64
- scope resolved from the request rather than remembered.
38
+ request that created them. `Nothing request-bound survives a request` is an ethos
39
+ invariant, not a style preference: a client retained across requests on Workers
40
+ is a cross-request capability leak, and a promise awaited after the response is
41
+ work the runtime may already have torn down. Deferred work has one sanctioned
42
+ channel, `ctx.waitUntil`, threaded through `fetch(request, env, ctx)` — activity
43
+ writes use it, as does a stale-window catalog refresh, which owns a fresh scope
44
+ and deadline rather than carrying the inbound one past the request. And a fresh
45
+ `McpServer` per request is what makes the deployment stateless: no sessions, no
46
+ server push, no stream resumability, scope resolved rather than remembered.
65
47
 
66
48
  ## Request lifecycle
67
49
 
68
- `src/server.ts` is the composition root. It checks MCP origins, upgrades the
69
- scheme when it must, runs the route table, then wraps whatever came back in
70
- security headers. Route *order* is the contract — several routes would behave
71
- differently if they were reachable in another order — so the table below is
72
- read top to bottom.
50
+ `src/server.ts` is the composition root: MCP origin check, scheme upgrade, route
51
+ table, security headers. Route *order* is the contract — several routes would
52
+ behave differently if they were reachable in another order — so read the table
53
+ top to bottom.
73
54
 
74
55
  | Order | Route | Notes |
75
56
  | --- | --- | --- |
76
- | 0 | MCP Origin check | `/mcp` and every `/mcp/` suffix reject a disallowed `Origin` with a fixed 403 before redirects, admission, auth, or preflight. `allowedOrigins` defaults to the configured public origin plus HTTP(S) loopback origins at any port. Requests without Origin are admitted. |
77
- | 0 | HTTPS upgrade | 308 to `publicUrl` when it is HTTPS and the request arrived over HTTP. Path and query are *assigned* onto the configured URL, never resolved against it, so a `//host` pathname cannot replace the deployment origin. `/health` is exempt: a loopback container probe must not depend on public DNS and TLS. `/ui` is canonicalized to `/` while upgrading. |
78
- | 0 | Cloudflare Access (Worker deployment, when enabled) | Edge admission before this route table. Managed OAuth owns its challenge and discovery metadata; an admitted direct invocation carries trusted identity in `ctx.access`. |
79
- | 1 | Mounted UI routes | The optional UI handles its shells, assets, data, details, and auth mutations before wildcard OPTIONS. Mutation routes refuse preflight rather than inheriting MCP CORS. No UI module means none of these routes. |
80
- | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp` or any `/mcp/` suffix returns 204 without admission or auth. Reflect the allowed origin and requested valid `mcp-param-*` header names. |
81
- | 2 | Other `OPTIONS` | Auth metadata gets a chance, otherwise compatibility CORS preflight. |
57
+ | 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
58
+ | 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query *assigned* onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
59
+ | 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
60
+ | 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
61
+ | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
62
+ | 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
82
63
  | 3 | `/.well-known/*` | Auth metadata, or 404. |
83
- | 4 | `/health` | Open payload-free health, executor, admission, and deployment metadata; connector drift uses stable short hashes and downstream admission sums shared and personal controllers without ids. Reserved routes reflect installed modules. |
84
- | 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state verification and personal ownership checks; independent of UI. |
85
- | 6 | `/mcp`, `/mcp/<pool>` | Origin check before admission, admission before auth, then a request-local MCP server. A pool path serves the declared pool intersected with the identity's own view; any undeclared suffix, including malformed names, a refusing grant, and a throwing grant return the same 404 status, body, and headers after auth. Grant lookup latency is not hidden; see [pools](./auth.md#pools). |
64
+ | 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata, with drift as stable short hashes. |
65
+ | 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state and personal-ownership checked, independent of the UI. |
66
+ | 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
86
67
  | 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
87
68
 
69
+ Every response leaves through `withSecurityHeaders`, and the UI module adds a
70
+ nonce-based script CSP and framing denial to its shells.
71
+ `test/server-route-contracts.test.ts` pins the ordering and the exact refusal
72
+ bodies; it exists because the ordering is invisible in any one file and a
73
+ reordering reads like a harmless refactor.
88
74
 
89
- Every response leaves through `withSecurityHeaders`: `nosniff`, a no-referrer
90
- policy, HSTS on HTTPS, while the UI module adds a nonce-based script CSP and framing denial to its shells. `test/server-route-contracts.test.ts` pins this ordering
91
- and the exact refusal bodies; it exists because the ordering is invisible in
92
- any one file and a reordering reads like a harmless refactor.
75
+ An admitted non-preflight `/mcp` request then takes five steps in
76
+ `src/routes/mcp.ts`:
93
77
 
94
- `/mcp` first checks Origin, including on preflight. A disallowed browser origin
95
- costs no permit and no auth lookup. The explicit `allowedOrigins: "*"` escape
96
- hatch preserves open CORS; a list reflects only admitted origins and varies
97
- responses by Origin. An originless client needs no CORS allow-origin header.
98
-
99
- An admitted non-preflight request then takes six steps:
100
-
101
- 1. **Admit.** One permit from the deployment-wide FIFO pool, taken before auth
102
- so an unauthenticated flood costs a permit rather than a Clerk lookup
103
- ([request admission](./request-admission.md)). The permit is held until the
104
- response *body* completes, not until the handler returns.
78
+ 1. **Admit.** One permit from the deployment-wide pool, taken before auth so an
79
+ unauthenticated flood costs a permit rather than a Clerk lookup, and held
80
+ until the response *body* completes or the caller leaves, not until the
81
+ handler returns.
105
82
  2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
106
- before interactive providers. First `ok` admits; if all fail, the last provider's challenge
107
- response is returned. No providers configured means open — development
108
- only, and it warns at construction.
109
- 3. **Derive the registry view.** Auth supplies a namespaced subject and, for a
110
- human, a principal. `identity.connectorAccess` selects declared connector
111
- ids and, for a narrower slice, exact `connector.tool` addresses; the
112
- scoped view filters every catalog read through them. Personal connectors use the principal partition; result paging uses
113
- the subject partition. No caller parameter selects either.
114
- 4. **Narrow to the pool.** On `/mcp/<pool>`, look the name up in the
115
- declared pools and run its grant against the authenticated identity. The
116
- view becomes the pool intersected with the identity's `connectorAccess`;
117
- a pool can never widen it. Anything else is a 404 that names no pool.
118
- 5. **Refuse `?toolkit=`.** Caller-selected toolkits were removed ([#178](https://github.com/zackbart/connecta/issues/178))
119
- but the URLs naming them were handed out, so the parameter is a 404 rather
120
- than silently serving the full registry. Retiring a scoping boundary into
121
- fail-open is the one outcome worse than the 404.
122
- 6. **Serve.** A fresh `McpServer` per request, the seven meta-tools registered
123
- against the registry and the response
124
- handed back.
83
+ before interactive. First `ok` admits; if all fail, the last provider's
84
+ challenge is returned. No providers means open — development only, and it
85
+ warns at construction.
86
+ 3. **Narrow to the pool.** On `/mcp/<pool>`, look the name up, run its grant
87
+ against the identity, then `intersectAccess` the pool with the identity's own
88
+ access. A pool can never widen a view; anything else is a 404 naming no pool.
89
+ 4. **Derive the registry view.** One `registry.scoped(...)` call with those
90
+ connector ids, exact `connector.tool` addresses when the identity declares a
91
+ narrower slice, and the subject and principal keys. Personal connectors use
92
+ the principal partition, result paging the subject partition, and no caller
93
+ parameter selects either (`test/identity-scope.test.ts`).
94
+ 5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
95
+ ([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
96
+ handed out, and retiring a scoping boundary into fail-open is worse than any
97
+ 404 — then register the seven meta-tools on a fresh `McpServer`
98
+ (`test/server.test.ts`, `test/code-first-surface.test.ts`).
125
99
 
126
100
  ## Layers below the meta-tools
127
101
 
128
- The meta-tool handlers are thin. The work sits in four services the registry
129
- owns or hands out, and a change usually belongs in exactly one of them:
102
+ The meta-tool handlers are thin. The work sits in five modules the registry owns
103
+ or hands out, and a change usually belongs in exactly one of them:
130
104
 
131
105
  | Module | Owns |
132
106
  | --- | --- |
133
107
  | `src/registry.ts` | The connector set, identity-scoped views, personal storage partitions, address resolution, catalog TTL/persistence/completeness, refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
134
- | `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
108
+ | `src/catalog-service.ts` | Request-local listing, search, and describe. Caches catalogs inside one request, fans discovery probes out under deadlines, and opts agent reads into the runtime's deferred catalog channel when one exists. |
135
109
  | `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
136
- | `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
110
+ | `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
137
111
  | `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
138
112
 
139
- `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
140
- three services. That is the point: a program's `connecta.call` and a top-level
141
- `call_tool` reach `InvocationService.invoke` by different routes and get the
142
- same admission, the same credential resolution, and the same fail-closed
143
- read-only check. `test/execute.test.ts` asserts that parity directly, because
144
- the alternative — a sandbox path that quietly diverges — is how generated code
145
- would mint a capability.
113
+ `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
114
+ services, `CatalogService` and `InvocationService`. That is the point: a
115
+ program's `connecta.call` and a top-level `call_tool` reach
116
+ `InvocationService.invoke` by different routes and get the same admission, the
117
+ same credential resolution, and the same fail-closed read-only check.
118
+ `test/execute.test.ts` asserts the parity directly, because a sandbox path that
119
+ quietly diverges is how generated code would mint a capability.
120
+
121
+ ## Admission, in two places
122
+
123
+ Request admission (`src/executor-admission.ts`, applied in `src/routes/mcp.ts`)
124
+ bounds the MCP envelope: one deployment-wide FIFO pool, plus a deliberately
125
+ smaller code pool a program takes a *second* permit from, so one request cannot
126
+ trade ordinary capacity for unbounded sandboxes. `admission.code` is only a
127
+ fallback — an executor implementing `acquire()` owns a bounded pool already, its
128
+ settings win, and connecta warns the fallback was ignored. Invalid bounds throw
129
+ at construction, because a pool that quietly became unbounded is worse than a
130
+ deployment that refuses to boot. The queue is global FIFO across identities: a
131
+ capacity boundary, not tenant fairness, and one deployment serves one tenant.
132
+
133
+ Call admission (`src/call-admission.ts`) answers what the envelope cannot see —
134
+ a connector's optional policy over its own `Connector.callTool` attempts,
135
+ partitioned by an optional `partitionKey` and bounded by concurrency, a
136
+ rolling-window budget, or both. Exactly one rule is accepted, because several
137
+ cannot be faked as sequential leases: consuming a rolling token before a later
138
+ rule refuses would charge a call that never reached the provider, the exact
139
+ accounting error a budget exists to prevent. Both layers are pinned by
140
+ `test/request-admission.test.ts` and `test/call-admission.test.ts`.
141
+
142
+ ## Storage, credentials, and connectors
143
+
144
+ `KVStorage` is `get`/`set`/`delete` with optional `list(prefix)`; core uses it for
145
+ connector state, catalogs, and result paging — a 15-minute TTL with one
146
+ runtime-wide accounting of stash bytes and entries, where a full stash returns the
147
+ successful call's preview and a paging-unavailable notice rather than a result id.
148
+ A second optional method, `compareAndSet(key, expected, next)`, is an atomic
149
+ claim: `null` means absent (expired counts) on the way in and delete on the way
150
+ out. Nothing in core requires it yet. Downstream OAuth uses it where the store
151
+ has it, so resealing legacy plaintext and discarding a refused grant cannot
152
+ overwrite a consent that landed in between, and falls back to a read and a
153
+ write where it does not. A subsystem that needs an exactly-once claim will
154
+ require it explicitly rather than emulate it that way.
155
+ Adapters: `src/storage/memory.ts` and `src/storage/file.ts` (Node) both provide
156
+ it, and the namespaced views core hands connectors forward it only when the
157
+ underlying store has it. `examples/worker/` carries two more: Cloudflare KV,
158
+ eventually consistent and so declaring none, and a D1 store that provides it —
159
+ copyable reference source beside the D1 activity store, deliberately not an
160
+ importable subpath. The shared cases live in `test/storage-contract.ts`.
161
+
162
+ `src/credentials.ts` is the AES-GCM vault behind the root-exported
163
+ `CredentialVault` contract, selected through the `vault` slot. It binds connector
164
+ id and owner into the authenticated encryption context, because sharing a backend
165
+ is not permission to share a principal's credentials. Two rules carry the
166
+ subsystem: credentials never leave the host — read only through the owning
167
+ connector's `ctx.credential`, rendered by nothing, absent from activity and model
168
+ recovery — and they fail at use, proactive liveness probing having been removed by
169
+ decision. The vault is read per call, so a replacement needs no restart
170
+ (`test/credentials.test.ts`).
171
+
172
+ Connectors are the boundary between the fixed meta-tool surface and downstream
173
+ capability, and `api()`, `remoteMcp()`, and a hand-written `Connector`
174
+ (`src/connectors/`, plus the prebuilt connections under `src/providers/`) all
175
+ produce instances that take the same catalog, read-only, credential, storage,
176
+ invocation, result-size, and activity paths. Every one is deployment
177
+ configuration, never runtime registration. `authScope: "shared" | "personal"`
178
+ partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
179
+ shapes — by principal, and *only* connecta-owned context: a secret a custom
180
+ handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
181
+ literal-headers-plus-personal version of that mistake. Visibility
182
+ (`identity.connectorAccess`) is a separate rule; hiding a connector does not
183
+ change who owns its auth.
146
184
 
147
185
  ## Optional deployment modules
148
186
 
149
- `createConnecta` takes closed typed `ui`, `vault`, and `activity` slots. Factories
150
- live at `/ui`, `/credentials`, and `/activity`; bearer auth lives at
151
- `/auth/bearer`. Root exports the contracts, never these implementations. There
152
- is no module array, runtime registration, or plugin lifecycle.
153
-
154
- Core keeps connector discovery, the executor contract, invocation, permissions,
155
- and OAuth callback verification together. Optional modules contribute no
156
- runtime work when omitted. The UI supplies credential handoff URLs only while
157
- mounted. Status reads never initiate OAuth, and each lazy details request owns
158
- its downstream scope. See [operator UI](./operator-ui.md).
187
+ `createConnecta` takes closed typed `ui`, `vault`, and `activity` slots, with
188
+ factories at `/ui`, `/credentials`, and `/activity` and bearer auth at
189
+ `/auth/bearer`. Root exports the contracts, never the implementations, and there
190
+ is no module array, runtime registration, or plugin lifecycle. Core keeps
191
+ discovery, the executor contract, invocation, permissions, and OAuth callback
192
+ verification; an omitted module contributes no runtime work at all.
193
+
194
+ The operator UI — `src/ui.ts` (data-free shell and `/ui/data` payload),
195
+ `src/routes/ui.ts`, `src/operator-ui/` (the Preact app and its pure rules) —
196
+ shows a human what a deployment exposes and manages only the authentication
197
+ material code explicitly permitted; it never edits the connector set, catalog,
198
+ annotations, scopes, or permission rules. Two invariants shape it: a status read
199
+ never starts authorization, since OAuth begins with an explicit authorized POST,
200
+ and each lazy details request owns a bounded downstream scope, so one failing
201
+ provider leaves the other connections usable. A connector's status message
202
+ never reaches the page: it can quote a downstream error body, and that body can
203
+ quote the secret it rejected, so the details payload carries only a classified
204
+ `problem` — which picks fixed on-screen copy and a fixed fix prompt — and the raw
205
+ text goes to the server log. Credential handoff URLs exist only while the UI is
206
+ mounted; OAuth callbacks never need it.
207
+
208
+ Its appearance is one token layer. `src/operator-ui/browser.css` resolves every
209
+ color, radius, and font through a custom property and mixes the rest from those
210
+ with `color-mix`, so `branding.theme` only has to append a `:root` block after
211
+ that stylesheet. The five tokens it accepts are gated in `src/branding.ts`, each
212
+ by a narrow syntactic check: deployment config reaches a `<style>` element here,
213
+ and an unvalidated value would be CSS injection. The dark palette is the same
214
+ tokens under `prefers-color-scheme`; `colorScheme` pins one with a `data-scheme`
215
+ attribute on the page.
159
216
 
160
217
  ## Import-graph purity
161
218
 
162
- Nothing reachable from `src/index.ts` may import a `node:` builtin. The core is
163
- Web-API only so the same code runs unchanged in workerd and in Node.
164
-
165
- The Node-touching paths are `src/node.ts` (the `node:http` adapter),
166
- `src/storage/file.ts`, and the QuickJS process pool
167
- (`src/executors/quickjs.ts` and its child entry). Each lives behind an explicit
168
- subpath export — `@zackbart/connecta/node`, `@zackbart/connecta/quickjs` — and
169
- must stay unreachable from the root entry. The optional Clerk adapter is behind
170
- `./auth/clerk` for the adjacent reason: `@clerk/backend` is an optional peer,
171
- not a dependency. The zero-dependency Cloudflare Access adapter likewise stays
172
- behind `./auth/cloudflare-access`: it is Web-API-pure, but its trust contract is
173
- specific to a direct Worker invocation carrying `ctx.access`.
174
-
175
- `test/purity.test.ts` walks the relative-import graph from `src/index.ts` and
176
- fails on (a) any `node:` specifier in a reachable file and (b) the Node
177
- adapter, file storage, QuickJS parent or child, auth adapters, UI bundle,
178
- encrypted vault implementation, or activity implementation being reachable at all. `test/package-surface.test.ts` and
179
- `scripts/check-package.mjs` guard the other half — that the published tarball
180
- matches the same boundary.
181
-
182
- The failure mode this prevents is not theoretical: a single convenience import
183
- of `node:crypto` in a shared helper makes the whole Worker deployment shape
184
- stop building, and it will do so in someone else's repository rather than
185
- this one.
186
-
187
- ## Where things live
219
+ Nothing reachable from `src/index.ts` may import a `node:` builtin, so the same
220
+ core runs unchanged in workerd and in Node. The Node-touching paths — `src/node.ts`
221
+ (the `node:http` adapter), `src/storage/file.ts`, and the QuickJS process pool
222
+ (`src/executors/quickjs.ts` plus its child) — each sit behind an explicit subpath
223
+ and must stay unreachable from the root. `./auth/clerk` is separate because
224
+ `@clerk/backend` is an optional peer rather than a dependency, and
225
+ `./auth/cloudflare-access` for a third reason: it is Web-API-pure, but its trust
226
+ contract is specific to a direct Worker invocation carrying `ctx.access`.
227
+
228
+ `test/purity.test.ts` walks the relative-import graph and fails on any `node:`
229
+ specifier in a reachable file, or on any of those modules — plus the UI bundle,
230
+ encrypted vault, and activity implementation — being reachable at all;
231
+ `test/package-surface.test.ts` and `scripts/check-package.mjs` guard the same
232
+ boundary in the published tarball. The failure mode is not theoretical: one
233
+ convenience import of `node:crypto` in a shared helper stops the whole Worker
234
+ shape from building, in someone else's repository rather than this one.
235
+
236
+ ## Effect inside
237
+
238
+ The core runs on Effect v4; no API a deployment touches does. `createConnecta`,
239
+ `remoteMcp()`, `api()`, the `Connector` contract, and every shipped `.d.ts`
240
+ are Promise-shaped and name no Effect type, so a connector author never meets a
241
+ second async paradigm. The reason for the rewrite is the first section of this
242
+ guide: the bugs here are lifetime bugs, and Effect makes a lifetime a value —
243
+ a Scope that closes on every exit, an interrupt that reaches whatever a fiber
244
+ is waiting on, a Deferred that one party completes and any number join. The
245
+ conversion found and closed a dozen of them, each with a test that fails on the
246
+ code before it: permits and leases held after their caller left, waits with no
247
+ bound, and races between requests.
248
+
249
+ ### Shell and core
250
+
251
+ Each converted module is a shell and a core. The shell keeps its exported class
252
+ or function exactly — same members, same error classes, same `.d.ts`, private
253
+ member names included, because TypeScript ships those too. The core is an
254
+ Effect program behind it. In a published class it lives in a `static` block,
255
+ the one place that can read private state without adding a member to the
256
+ declarations; elsewhere it lives in an unexported function or in
257
+ `src/runtime/`, which no `exports` target reaches. Errors keep their classes
258
+ (`ConnectorCallError`, `ExecutorAdmissionError`, `CallAdmissionError`) because
259
+ callers and tests check `instanceof`, `name`, and `message`.
260
+
261
+ Effect callers skip the Promise and take the program: `admit` and
262
+ `acquireScoped` for request admission (`src/runtime/admission.ts`), `admitCall`
263
+ and `acquireCallScoped` for call admission, `closeScope` and `closeScopeOnExit`
264
+ for a connector scope, `withDeadlineEffect` where Promise code has
265
+ `withDeadline`. The build prunes every declaration no `exports` target reaches,
266
+ and `npm run check:declarations` fails if what remains names an Effect type.
267
+
268
+ ### One runner
269
+
270
+ `src/runtime/run.ts` is the only place a fiber starts, and
271
+ `test/purity.test.ts` fails if any other file calls `Effect.run*`, `runFork`,
272
+ `forkDaemon`, or `ManagedRuntime.make`. It exports two ways out:
273
+
274
+ - **`runEdge(effect, { signal, runtime? })`** runs an effect at a Promise
275
+ boundary. It rethrows the original failure or defect, never a wrapper. It
276
+ turns an interrupt into the caller's `signal.reason` rather than Effect's
277
+ "All fibers interrupted without error". And it runs every fiber on a
278
+ scheduler that yields through microtasks: Effect's default yields through
279
+ `setImmediate`, which vitest's fake timers freeze and Workers never had. The
280
+ price is that a fiber never yields to I/O, so CPU-heavy work stays out of
281
+ Effect loops.
282
+ - **`detach(effect, ctx)`** is the only fire-and-forget, and it hands the work
283
+ to `ctx.waitUntil` when there is one.
284
+
285
+ Beside them sit `withDeadlineEffect`, which aborts the operation's own signal
286
+ with the labelled timeout error *before* interrupting it, so work that honors
287
+ the signal sees the same reason the caller does, and `fromSignal`, which turns
288
+ an `AbortSignal` into a failure to race against. Nothing runs Effect at module
289
+ scope, where a Worker may not start work, and nothing logs through
290
+ `Effect.log`: logging goes through the configured `Logger`, which is what
291
+ honors `logger: "silent"`.
292
+
293
+ Each Connecta also gets a runtime of its own (`src/runtime/services.ts`):
294
+ `Storage`, `Vault`, `ActivityRecorder` (one that records nothing when the module
295
+ is omitted), `Logger`, and `ResolvedConfig`. Creating it builds nothing, since
296
+ a Worker constructs its Connecta at global scope; the first run that needs the
297
+ services builds them, and `close()` disposes the runtime last. Two things
298
+ deliberately do not run on it. The request pipeline runs on no runtime, so
299
+ `/health` and a closed deployment's 503 keep answering after `close()`. And a
300
+ registry provides its own storage and logger to its programs
301
+ (`runOnPartition` in `src/runtime/storage.ts`), because a personal registry's
302
+ storage is the root's namespaced to its principal and the runtime's `Storage`
303
+ would be another partition's.
304
+
305
+ ### What runs on Effect
306
+
307
+ | Area | Shape |
308
+ | --- | --- |
309
+ | Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, or at once if the handler fails first. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment. |
310
+ | Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
311
+ | One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
312
+ | `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
313
+ | Discovery (`catalog-service.ts`) | A request-scoped cache: one Deferred per connector, completed by the first asker's read from that read's own settlement, so a probe deadline ends one asker's wait and never the read the others share. Fan-out is `Effect.forEach` under the discovery concurrency. |
314
+ | Registry (`registry.ts`) | Catalog persistence and the result stash are programs over `Storage`. A refresh flight is a Deferred its publishing request completes; persisted-catalog writes take per-connector turns, each a Deferred its own request completes. |
315
+ | Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
316
+ | Downstream OAuth (`auth/downstream-oauth.ts`) | A refresh flight is a Deferred; the owner's redemption is a fiber its abort interrupts, and committing an answer that already exists is uninterruptible. |
317
+ | Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation. |
318
+ | QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`. |
319
+
320
+ ### What stays plain
321
+
322
+ - **Everything a deployment writes against.** The `Connector` contract,
323
+ `api()`, the providers, custom connectors, storage adapters, and the vault
324
+ are Promises. A connector is called with `Effect.tryPromise`, never asked to
325
+ return an Effect.
326
+ - **Inbound authorization.** `authorize` in `src/routes/shared.ts` ships in
327
+ the declarations and in the `./activity` bundle; converting it cost 32 KB
328
+ gzip there for nothing a caller could see.
329
+ - **Pure synchronous code.** Ranking, schema rendering, classification,
330
+ `validateToolInput`, the host-call budget, the emit collector, failure
331
+ framing, and result shaping. The microtask scheduler never yields to I/O, so
332
+ wrapping them would buy nothing and could starve a request.
333
+ - **The MCP edges.** `@modelcontextprotocol/server` upward and the SDK client
334
+ downstream; Effect's own `McpServer` is not used.
335
+ - **The Node and browser leaves.** `node.ts`, `fileStorage`, the QuickJS
336
+ child, runtime, and protocol, and the operator UI's browser app.
337
+
338
+ ### Across requests
339
+
340
+ Work shared across requests meets only through a Deferred that the owning
341
+ request completes, never through a fiber that outlives its request. Two
342
+ workerd rules follow, and both have bitten.
343
+
344
+ **Waiting on a shared Deferred is its own edge.** Deferred resumes a waiting
345
+ fiber synchronously, inside the call that completed it, which on Workers means
346
+ inside the completing request's I/O context. A waiter that goes on to touch its
347
+ own body, transport, or storage there fails with "Cannot perform I/O on behalf
348
+ of a different request", or hangs until its deadline. So the wait runs through
349
+ `runEdge`, and the waiter does its own I/O after awaiting that promise, which
350
+ workerd resumes in the waiter's request as it does any promise resolved from
351
+ another. The same holds one level out for every queue: a queued call, a queued
352
+ `execute_code`, and a queued `/mcp` request are each handed their permit from
353
+ inside the request that released one, so each awaits the controller's
354
+ Promise-shaped `acquire` rather than yielding its Effect program, and does its
355
+ own work only once that promise resolves. Effect's `Semaphore` is not used
356
+ across requests for the same reason, and because a same-tick newcomer can take
357
+ a released permit ahead of a waiter that queued earlier.
358
+
359
+ **Never read another request's signal.** On workerd, reading `aborted` or
360
+ `reason`, or calling `abort()`, on a signal that belongs to another request
361
+ throws; adding and removing listeners does not. Code that runs from another
362
+ request's completion — a queue handing out a permit, `close()` — must not look
363
+ at a waiter's signal. A waiter whose signal aborts leaves the queue from its
364
+ own listener, in its own request.
365
+
366
+ One corollary is easy to break by tidying. Workerd cancels a request as hung
367
+ when its only pending work is waiting on another request and it has no timer or
368
+ I/O of its own. Connecta's deadline timers are what keep a waiting request
369
+ alive and bounded, so no wait may lose its timer.
370
+
371
+ ### Why not Schema or HttpApi
372
+
373
+ Both were measured and both lost. `effect` is imported through its barrel,
374
+ which defeats tree-shaking: the first conversion step measured about +20 ms
375
+ of Worker cold start for it, and that cost was accepted over deep imports at
376
+ every call site. The catch is that a module the barrel reaches is paid for in
377
+ full, however little of it is used. Effect Schema for config validation and the
378
+ meta-tool inputs came to about +62 KB gzip and +13 ms of cold start in the
379
+ root, for a validator no clearer than the hand-written `CONFIG_SCHEMA`; the
380
+ meta-tool inputs stay zod, which the MCP SDK bundles regardless. `HttpApi` for
381
+ the operator and activity routes cost about +100 KB gzip on `./ui` and +130 KB
382
+ on `./activity`, and matching the wire format meant opting out of most of what
383
+ it does: unowned paths fall through rather than 404, a wrong method is a JSON
384
+ 405, and the content-type and size checks run before a body is read. The root
385
+ entry may import only `effect` itself (`test/purity.test.ts`); nothing in
386
+ `src/` uses `effect/unstable/*`, which would have to stay behind a subpath.
387
+
388
+ The core's cost is recorded rather than guessed. Against 0.24.4 the root entry
389
+ grew from 235,346 to 279,526 bytes gzip, the Worker example from 263,949 to
390
+ 314,336, and each provider by about 36 KB, since each reaches Effect through
391
+ `remoteMcp()`; `npm run check:bundle` caps every entry against
392
+ `scripts/bundle-budget.json`. An install carries about 53 MB of `effect`, with
393
+ no dependencies of its own.
394
+
395
+ ## Where else to look
396
+
397
+ Beyond the modules already named:
188
398
 
189
399
  ```
190
400
  src/
191
- index.ts createConnecta + the public re-exports (Workers-clean)
192
401
  server.ts route ordering, HTTPS upgrade, security wrapper
193
402
  routes/ one file per surface; shared.ts holds the auth gate
194
- meta-tools.ts the six non-execute meta-tools over the registry
195
- execute.ts execute_code, the sandbox host bridge, emitted media
196
403
  skills.ts MCP instructions, the usage skill, connector guides
197
- registry.ts connector set, addresses, health, call limiters
198
- catalog-service.ts request-local catalog access, search, and describe
199
- catalog.ts ranking, summaries, compact schema rendering
200
- result-shapes.ts passive runtime-only observed output schemas
201
- invocation.ts one tool call, end to end
202
404
  catalog-drift.ts vetted manifests and the counts a refresh produces
203
- credentials.ts the AES-GCM connector vault over KVStorage
204
405
  activity.ts optional history factory and best-effort recorder
205
- call-admission.ts connector-partitioned downstream permits and budgets
206
- executor-admission.ts the portable bounded queue both pools use
207
- ui.ts the served operator shell and /ui/data payload
208
- operator-ui/ the Preact app, its pure rules, and the built bundle
209
- connectors/ remote-mcp.ts, api.ts, guarded-fetch.ts
210
- providers/ the maintained prebuilt connections
211
406
  auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth
212
407
  executors/ the QuickJS pool and child (Node only)
213
- storage/ memory.ts, file.ts (Node only)
214
408
  node.ts listen() + fileStorage re-export (Node only)
215
409
  ```
216
410
 
411
+ There are exactly two deployment shapes — `templates/node/`, which
412
+ `connecta init` copies with its container files, and `examples/worker/` — and
413
+ `test/deployment-shapes.test.ts` with `npm run check:examples` keeps both
414
+ compiling and configuring the real thing.
415
+
217
416
  ## Sharp edges
218
417
 
219
418
  - **The root registry is shared; identity views are partitioned.** Shared
220
- connector caches are visible to later requests in the isolate. Personal
221
- connectors use a bounded principal registry, and transient results use the
222
- authenticated subject. Anything cached per request still dies with it.
223
- Putting a downstream client or credential on the wrong side of those lines
224
- is the highest-severity mistake available here.
225
- - **Route order is behavior.** Moving a mutation route below the
226
- wildcard `OPTIONS` opts it into CORS preflight.
227
- - **Admission runs before auth, on purpose.** Reordering them to "authenticate
228
- first" makes the cheapest possible attack the most expensive request.
229
- - **`close()` is idempotent and ordered.** It closes both admission pools and
230
- the connector limiters, then the executor. Node's `listen()` calls it on
419
+ connector caches are visible to later requests in the isolate; personal
420
+ connectors use a bounded principal registry and transient results the
421
+ authenticated subject. Putting a downstream client or credential on the wrong
422
+ side of those lines is the highest-severity mistake available here.
423
+ - **Route order is behavior.** Moving a mutation route below the wildcard
424
+ `OPTIONS` opts it into CORS preflight; reordering admission after auth makes
425
+ the cheapest possible attack the most expensive request.
426
+ - **`close()` is idempotent and ordered.** Both admission pools, then the
427
+ connector limiters, then the executor, then the Connecta's runtime; Node's `listen()` calls it on
231
428
  SIGTERM/SIGINT.
232
429
  - **Structural mistakes throw at construction.** A duplicate connector id, an
233
- invalid admission rule, removed `accessTokens` option, or missing executor: all refuse to boot. A deployment that starts in the wrong shape is
234
- worse than one that does not start.
235
-
236
- ## Tests that enforce this
237
-
238
- | Invariant | Suite |
239
- | --- | --- |
240
- | The core imports no `node:` builtin and reaches no Node-only module | `test/purity.test.ts` |
241
- | The published surface matches the same boundary | `test/package-surface.test.ts`, `scripts/check-package.mjs` |
242
- | Route order, per-route auth, and byte-exact refusals | `test/server-route-contracts.test.ts` |
243
- | `/mcp` end to end, the open routes, exactly seven tools, bounded connector orientation | `test/server.test.ts`, `test/code-first-surface.test.ts` |
244
- | Construction-time refusals and the grouped config boundary | `test/config.test.ts`, `test/registry.test.ts` |
245
- | Program and top-level calls take the same enforced path | `test/execute.test.ts` |
246
- | Both deployment shapes still compile and configure the real thing | `test/deployment-shapes.test.ts`, `npm run check:examples` |
430
+ invalid admission rule, the removed `accessTokens` option, a missing executor:
431
+ all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
432
+ in the wrong shape is worse than not starting.