@zackbart/connecta 0.24.2 → 0.24.3

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 (57) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/dist/auth/bearer.js +2 -0
  3. package/dist/auth/downstream-oauth.d.ts +12 -1
  4. package/dist/auth/downstream-oauth.js +147 -35
  5. package/dist/call-admission.d.ts +4 -0
  6. package/dist/call-admission.js +26 -0
  7. package/dist/catalog-drift.js +9 -4
  8. package/dist/catalog-service.d.ts +2 -0
  9. package/dist/catalog-service.js +25 -8
  10. package/dist/catalog.d.ts +2 -0
  11. package/dist/catalog.js +246 -121
  12. package/dist/connectors/api.js +11 -1
  13. package/dist/connectors/guarded-fetch.d.ts +1 -1
  14. package/dist/connectors/guarded-fetch.js +27 -20
  15. package/dist/connectors/remote-mcp.js +84 -53
  16. package/dist/errors.d.ts +17 -0
  17. package/dist/errors.js +58 -0
  18. package/dist/execute.js +85 -23
  19. package/dist/executor-result.js +3 -1
  20. package/dist/executors/quickjs-child.js +5 -1
  21. package/dist/executors/quickjs-protocol.d.ts +4 -0
  22. package/dist/executors/quickjs-runtime.d.ts +1 -1
  23. package/dist/executors/quickjs-runtime.js +38 -21
  24. package/dist/executors/quickjs.js +68 -27
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.js +24 -3
  27. package/dist/invocation.js +134 -93
  28. package/dist/mcp-result.js +3 -2
  29. package/dist/meta-tools.js +118 -39
  30. package/dist/registry.d.ts +14 -2
  31. package/dist/registry.js +87 -13
  32. package/dist/routes/mcp.d.ts +4 -1
  33. package/dist/routes/mcp.js +84 -13
  34. package/dist/routes/oauth.js +4 -0
  35. package/dist/routes/shared.d.ts +1 -0
  36. package/dist/routes/shared.js +4 -4
  37. package/dist/server.js +15 -3
  38. package/dist/skills.js +6 -5
  39. package/dist/storage/file.d.ts +6 -2
  40. package/dist/storage/file.js +312 -34
  41. package/dist/storage/memory.js +12 -1
  42. package/dist/validate.js +3 -3
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/documentation/architecture.md +22 -6
  46. package/documentation/auth.md +42 -9
  47. package/documentation/call-admission.md +24 -8
  48. package/documentation/code-mode.md +34 -22
  49. package/documentation/connectors.md +47 -5
  50. package/documentation/meta-tools.md +74 -6
  51. package/documentation/operations.md +19 -19
  52. package/documentation/provider-conventions.md +7 -0
  53. package/documentation/request-admission.md +38 -4
  54. package/documentation/storage-and-credentials.md +54 -1
  55. package/documentation/upgrading.md +18 -4
  56. package/package.json +1 -1
  57. package/templates/node/package.json +1 -1
@@ -103,15 +103,28 @@ checks, and authorization operations stay outside the budget: they are not the
103
103
  calls a provider is rate-limiting, and charging discovery for them would make
104
104
  a program's first search cost it capacity to act.
105
105
 
106
- Queue admission and connector execution have separate clocks. `queueTimeoutMs`
107
- bounds only the wait for a permit; a per-attempt `timeoutMs` starts after
108
- admission. A saturated call can therefore take up to their sum. With
106
+ `queueTimeoutMs` bounds only the wait for a permit. The per-call `timeoutMs`
107
+ is one deadline over catalog resolution, the permit wait, and the connector
108
+ call together, so a saturated call can never take longer than `timeoutMs`;
109
+ the queue timeout may only end it sooner. With
109
110
  `diagnostics: true`, `admissionMs` reports the permit wait and `connectorMs`
110
111
  the admitted attempt — which is the only way to tell "the provider is slow"
111
112
  from "we are throttling ourselves".
112
113
 
113
114
  ## Enforcement scope
114
115
 
116
+ Shared connectors use the root registry's controller. Personal connectors use
117
+ the owning principal's registry, so two accounts each receive their own budget;
118
+ all requests and call paths for one principal still share it. `close()` rejects
119
+ queued and future admissions on both kinds of controller, including a personal
120
+ registry constructed after shutdown.
121
+
122
+ The principal cache is bounded at 1,024 registries. Eviction closes an idle
123
+ registry so an older request view cannot use an orphaned controller. A registry
124
+ with active calls, queued calls, or unexpired rolling entries is never evicted.
125
+ If all 1,024 are occupied, a new principal view fails closed with HTTP 403 until
126
+ one drains. Cache churn cannot reset a live account's quota.
127
+
115
128
  This is deliberately **per-runtime**. It completely contains fan-out inside one
116
129
  request, including parallel `connecta.call` calls in one Worker isolate. A rolling
117
130
  budget is exact inside one Node process or Worker isolate, and best-effort
@@ -124,13 +137,16 @@ the bound actually is rather than implying a global one.
124
137
 
125
138
  ## Observations
126
139
 
127
- `/health` exposes payload-free aggregates at
128
- `admission.downstreamCalls.connectors.<id>`: retained partition count, current
140
+ `/health` exposes one payload-free aggregate at
141
+ `admission.downstreamCalls.aggregate`, summed across root and retained personal
142
+ controllers: rule and retained partition counts, current
129
143
  active and queued gauges, cumulative admitted/queued/rejected/rate-limited/
130
144
  cancelled counts, and queue-wait count, total, and maximum. The open endpoint
131
- never exposes partition keys, tool arguments, or results — a partition key can
145
+ never exposes connector ids, principal ids, partition keys, tool arguments, or results — a partition key can
132
146
  be a customer identifier, which is precisely why it stays out of an unauthenticated
133
- payload. Ordinary payload-free activity records the final call outcome and its
147
+ payload. Queue-wait maxima take the maximum, other counters sum, and `closed`
148
+ means every included controller is closed. Evicted idle controllers no longer
149
+ contribute to these runtime snapshots. Ordinary payload-free activity records the final call outcome and its
134
150
  typed error code.
135
151
 
136
152
  ## Tests that enforce this
@@ -138,5 +154,5 @@ typed error code.
138
154
  | Invariant | Suite |
139
155
  | --- | --- |
140
156
  | Independent partitions, exact rolling-window reset and retry, queued cancellation charging no budget, synchronous cancel during partition derivation, validated values snapshotted rather than read from mutable config, bounded partition state and contained `partitionKey` failures, empty and multi-rule policies refused | `test/call-admission.test.ts` (controller) |
141
- | One base-registry limiter shared by direct and program calls, promise concurrency with input order preserved, cancellation threading, no dispatch or retry or health poisoning after cancellation, retry hints returned without waiting or poisoning health, payload-free `/health` aggregates | `test/call-admission.test.ts` (integration, Node + Workers) |
157
+ | Shared and personal limiters shared by direct and program calls, independent principal budgets, shutdown covering both, eviction preserving live budgets, promise concurrency with input order preserved, cancellation threading, no dispatch or retry or health poisoning after cancellation, retry hints returned without waiting or poisoning health, payload-free `/health` aggregates | `test/call-admission.test.ts` (integration, Node + Workers) |
142
158
  | Where provider budgets are allowed to come from at all | [provider conventions P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number), [provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md) |
@@ -180,11 +180,11 @@ const page = await connecta.search({
180
180
  });
181
181
  ```
182
182
 
183
- **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, the configured `connectorTitle` when present (normalized whitespace, at most 120 UTF-8 bytes), and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
183
+ **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, the configured `connectorTitle` when present (normalized whitespace, at most 120 UTF-8 bytes), and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each render spends at most 2,000 visits across schema nodes, property and required names, literal values, any constraint-free retry, and its key-only fallback. Resolved `$ref` text is reused within that walk; exhausted work yields `unknown /* truncated */` and the corresponding truncation flag. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. `prefixItems` renders as a tuple with the declared `items` rest, an `unknown[]` rest when open, or no rest when `items` is false. `dependentSchemas` and `if`/`then`/`else` keep the base shape plus `/* conditional */` and set the truncation flag. `$dynamicRef` resolves a same-named definition like `$ref`; unresolved dynamic references become `unknown` with the truncation flag. These forms share the byte and work bounds above. Use `connecta.describe({ address, format: "json" })` or JSON search for omitted exact constraints.
184
184
 
185
185
  **S1a.** `connector` loads only the named catalog; omit it only when the integration is ambiguous, because an unscoped search fans out across every configured connector. `safety: "readOnly"` returns exactly the tools available through `connecta.call`; `"approvalRequired"` returns the complementary fail-closed class, including false, missing, and contradictory annotations. Omitted or `"all"` preserves the complete catalog. These filters grant no authority and change no admission decision.
186
186
 
187
- **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`, and `outputKeys`: the same names the rendered schema shows, ready to check before building arguments. Match inputs, truncation, safety, and outputs, not lexical
187
+ **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`, and `outputKeys`: the same names the rendered schema shows, ready to check before building arguments. `requiredInputKeys` includes only declared properties. Match inputs, truncation, safety, and outputs, not lexical
188
188
  rank; search distinct operations separately and use `outputKeys`, not guessed roots. A non-object schema — a union, an array, an
189
189
  unresolvable `$ref` — carries no lists rather than empty ones, because absent
190
190
  means "read the schema" where `[]` would claim the tool takes no fields. The
@@ -199,9 +199,8 @@ carries the same metadata whenever schemas are requested. Code-mode callers
199
199
  can set `includeSchemaKeys: false` to buy the bytes back.
200
200
 
201
201
  **S3.** Discovery is bounded and the bounds throw rather than silently shrink: a
202
- `limit` outside 1–100 is `invalid_args`, and a page whose serialized form
203
- exceeds 256,000 bytes is `result_too_large`, each with a hint naming the ways to
204
- ask for less. The thrown error carries the stable `code`, `retryable`, and
202
+ `limit` outside 1–100, an `offset` that is not a non-negative integer, or a supplied `query` that is not a string is `invalid_args`. Omitted `offset` starts at 0; omitted `query` browses. A page whose serialized form
203
+ exceeds 256,000 bytes is `result_too_large`; a program's page is measured as its serialized value, while top-level `search_tools` measures the complete tool result including both copies and JSON escaping. Each error carries a hint for correcting the request. The thrown error carries the stable `code`, `retryable`, and
205
204
  `details` fields (`E1`).
206
205
 
207
206
  ### connecta.describe
@@ -222,7 +221,7 @@ carry a route-aware `nextAction`; a close miss may add three canonical `suggesti
222
221
  Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
223
222
  caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
224
223
  correlates a clipped address with its request; successes keep canonical addresses. More than 100
225
- addresses is `invalid_args`; the same 256,000-byte ceiling applies. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
224
+ addresses is `invalid_args`; the same 256,000-byte ceiling applies. Compact describe keeps property prose within a separate 8,192-byte UTF-8 shape cap and shares search's 2,000-visit work budget. A capped shape sets `inputSchemaTruncated` or `outputSchemaTruncated`; use `format: "json"` for the exact schema. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
226
225
 
227
226
  ### connecta.call
228
227
 
@@ -286,8 +285,9 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
286
285
  | Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
287
286
 
288
287
  Both executor bridges reduce a rejected host call to `new Error(message)`. Connecta restores the typed failure in a trusted prelude with a per-execution authenticated frame (`X11`), without turning the rejection into a returned value.
289
- `message` remains the human text. `code` and `retryable` are the stable branch fields; `details` is the complete host classification. This covers `call`, `search`, `describe`, `emit`, and the host-call budget.
288
+ `message` remains human text, capped at 2,000 JSON-serialized characters including quotes and an `…` marker when clipped. `code` and `retryable` are the stable branch fields; `details` carries the host classification. The complete details object fits 3,700 serialized characters. If optional recovery metadata would exceed that bound, it is omitted whole, preserving `code`, `message`, `retryable`, and `retryAfterMs`; a clipped recovery address or argument would describe a different call. This covers `call`, `search`, `describe`, `emit`, and the host-call budget.
290
289
  Program-authored errors stay untyped, and code must never parse error prose.
290
+ An `unavailable` classification may include optional `details.host` as an HTTP(S) origin of at most 253 UTF-8 bytes and `details.code` as a validated network errno, undici transport code, or `timeout` of at most 32 bytes; these diagnostics never enter activity records.
291
291
 
292
292
  **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program may do.
293
293
 
@@ -301,7 +301,7 @@ Program-authored errors stay untyped, and code must never parse error prose.
301
301
  | `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap ([H11](./provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next)) | false |
302
302
  | `input_required_unsupported` | a downstream asked for mid-call input | false |
303
303
  | `rate_limited` | the downstream reported a rate limit | true |
304
- | `unavailable` | the downstream is down or unreachable | true |
304
+ | `unavailable` | the downstream is down or unreachable; optional sanitized `details.host` and `details.code` describe the transport failure without paths, queries, credentials, or provider prose | true |
305
305
  | `timeout` | the per-call deadline (`execute.hostCallTimeoutMs`, default 15 s) expired | true |
306
306
  | `cancelled` | the run ended while this call was in flight (`E5`) | false |
307
307
  | `connector_call_failed` | anything else the connector threw | per message |
@@ -330,16 +330,16 @@ guest: admission rejection (`executor_overloaded`, retryable, with
330
330
  reported to the model as an error result. One seam: a host call still in flight
331
331
  when the run is cancelled fails with `cancelled`, catchable on the way out but
332
332
  never worth acting on (`Y3`). When shutdown tears down a program that had
333
- already started, accepted blocks are reported as discarded under `M4`; a failure before execution started carries no discard fields.
333
+ already started, accepted blocks are reported as discarded under `M4`; a failure before execution started carries no discard fields. A returned `error` field is a failure even when empty; an empty string reports `executor_failed` with `Error: Execution failed without an error message.`
334
334
 
335
335
  **E6.** An error the program raises itself — a `TypeError`, a call to a
336
336
  `connecta` member that is not a provider function (including an inherited one
337
337
  like `toString`), a `throw` of its own — ends the run with an error result
338
338
  carrying that message. It is not typed, because it is not a connector failure.
339
339
  One precedence rule: connecta recognizes an escaped tool failure by its message —
340
- exactly first, by containment second — so a program that *wraps* a failure's
340
+ exactly first, by containment second for messages of at least eight characters — so a program that *wraps* a failure's
341
341
  message in its own text still reports the underlying typed failure. Keeping the
342
- type beats keeping the prose.
342
+ type beats keeping the prose. Matching retains only the most recent 64 failures per execution, including caught refusals; an older escaped message remains an untyped execution failure. An empty terminal error uses the fixed message in `E5`.
343
343
 
344
344
  **E7.** `retryable` for `unknown_address`, `unknown_tool`, and `destructive_tool_requires_approval` is pinned false, never inferred from an address containing `503`, `429`, or `temporar`. The first two carry `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }` — the same scoped discovery the top-level record names, keyed to the surface the caller actually has. A program cannot call `search_tools`, so it is never told to. The message, the derived `query`, and a failed describe entry's `address` clamp caller-authored text to 512 UTF-8 bytes with an `…` marker. Those values land in the text content and `structuredContent`, so an invented 50 KB address would otherwise produce a refusal orders of magnitude past the deployment's result cap. A clipped address still identifies the mistake by its position; a short one — the common case — is exact and untagged.
345
345
 
@@ -383,7 +383,7 @@ anything, so paging its result would reward the one behavior code mode exists to
383
383
  remove — and stashing every unprojected return value would spend the result store
384
384
  on data nobody asked for.
385
385
 
386
- **R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive failure they ride along with the error result, which is what makes them worth writing. How a non-string argument renders is not contract (`X4`).
386
+ **R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive program failure through either a returned error result or a thrown error carrying `logs: string[]`. QuickJS streams captured entries to its parent and preserves the received prefix on cancellation, shutdown, deadline termination, child crashes, and IPC failures (`X4`). How a non-string argument renders is not contract (`X4`).
387
387
 
388
388
  **R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; a program that emitted adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are byte-for-byte the ordinary response path. Diagnostics exist so catalog, connector, and executor costs are distinguishable without persisting payloads or charging normal responses ([#247](https://github.com/zackbart/connecta/issues/247)).
389
389
 
@@ -433,8 +433,8 @@ media, not base64 text.
433
433
  trusted exactly as much as the return value. Preservation is re-emission of
434
434
  the raw downstream block, so `S5`'s uncapped fallthrough is contract.
435
435
 
436
- **M7.** `emit` spends no host-call budget (`L4`); `M5`'s bounds are its only
437
- bounds.
436
+ **M7.** `emit` alone spends no host-call budget (`L4`); `search`, `describe`,
437
+ and `call` share it. `M5`'s bounds are emission's only bounds.
438
438
 
439
439
  **M8.** Emission asks nothing of an executor: `emit` is a provider function,
440
440
  blocks cross the guest boundary once as an argument, and `ExecuteResult` is
@@ -491,7 +491,7 @@ because connecta enforces them above the sandbox:
491
491
 
492
492
  | Bound | Value |
493
493
  | --- | --- |
494
- | Host calls per execution | 20 |
494
+ | Host calls per execution, shared by `search`, `describe`, and `call` | 20 by default, `execute.maxHostCalls` |
495
495
  | Deadline per host call | 15 s, `execute.hostCallTimeoutMs` |
496
496
  | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
497
497
  | `describe` addresses | ≤ 100 |
@@ -500,7 +500,7 @@ because connecta enforces them above the sandbox:
500
500
  | Result | 24,000 serialized characters |
501
501
  | Logs presented to the model | 4,000 characters |
502
502
 
503
- Exhausting the host-call budget fails that call with non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No connector is reached, and the budget does not refill inside one execution.
503
+ Every `call` attempt spends one host call on entry, before address resolution, catalog lookup, safety checks, validation, or dispatch. Unknown addresses, unknown tools, catalog failures, and other pre-dispatch refusals spend the same budget as successful calls; catching a refusal does not refund it. `search` and `describe` likewise spend on entry. Exhausting the host-call budget fails that call with non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No connector is reached, and the budget does not refill inside one execution.
504
504
 
505
505
  **L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
506
506
  exhausts a bound ends the run with an error instead of degrading the host. The
@@ -588,7 +588,14 @@ arguments and captures `log`, `info`, `warn`, `error`, and `debug`; the Dynamic
588
588
  Worker renders arguments with `String()` (so an object logs as
589
589
  `[object Object]`) and captures only `log`, `warn`, and `error`, prefixing the
590
590
  latter two. Only the three captured everywhere are contract (`R5`); rendering is
591
- not.
591
+ not. QuickJS streams each accepted entry within the existing IPC envelope
592
+ bound while retaining its per-entry and cumulative child caps. The parent
593
+ keeps at most 4,001 joined characters for failure recovery, one beyond the
594
+ presentation cap so truncation stays visible. On a normal reply, its complete
595
+ log array takes precedence over that prefix; the two copies are never joined.
596
+ On termination or IPC failure, the parent attaches its retained prefix to the
597
+ thrown error. Admission rejection before the program starts has no guest logs
598
+ to recover.
592
599
 
593
600
  **X5. Leftover authority.** QuickJS blocks imports and has no `fetch`, `process`, timers, `crypto`, or `WebSocket`. Its Node child starts with an explicitly empty process environment rather than inheriting deployment variables or `NODE_OPTIONS`. A Dynamic Worker has those globals plus a non-contract set of runtime builtins through `import()` and `process.getBuiltinModule()`, including `node:path`, `node:crypto`, `node:net`, `node:tls`, `node:dns`, `node:module`, and `cloudflare:workers`. The upstream set can drift; this list is not an allowlist.
594
601
  The supported Worker construction is exactly `new DynamicWorkerExecutor({ loader })`. Do not pass `bindings`, `modules`, or `globalOutbound`: each can grant ambient configuration, code, or egress. Under it, `process.env`, lexical `this.env`, and `cloudflare:workers.env` are empty; `node:fs`, `node:http`, and `node:https` are unavailable through either access route; external `fetch`, `WebSocket`, `node:net`, and `node:tls` fail with workerd's outbound-denial error; DNS lookup ends unresolved; and `fetch("data:...")` resolves locally.
@@ -625,7 +632,12 @@ from one tool call therefore fails on Node and may succeed on Workers — reduce
625
632
  inside the program either way (`R1`).
626
633
 
627
634
  **X11. Typed host rejection.** Both executors rebuild Connecta's authenticated host-failure frame as a thrown guest `Error` (`E1`). The per-run secret stays in the trusted prelude closure, and the prelude locks `globalThis.Error`, so guest code and connector prose cannot forge the host transport frame.
628
- The human message is unchanged; a mismatched frame is ordinary untyped prose.
635
+ The host bounds details before framing (`E1`), including JSON escapes. QuickJS
636
+ refuses an oversized authenticated frame whole rather than slicing through its
637
+ JSON, and the prelude hides an authenticated frame whose JSON is malformed.
638
+ QuickJS keeps the raw bridge and its JSON decoder in a private closure so guest
639
+ code cannot intercept the frame before the prelude handles it. A mismatched
640
+ frame is ordinary untyped prose.
629
641
 
630
642
  ## Changes from earlier code mode
631
643
 
@@ -659,7 +671,7 @@ the upstream `Executor` shape assignable.
659
671
  | `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
660
672
  | `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
661
673
  | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (parallel calls and shared admission) |
662
- | `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections) |
674
+ | `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections), `test/quickjs-executor.test.ts` (oversized messages, private transport, and forged frames) |
663
675
  | `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, and failure isolation) |
664
676
  | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
665
677
  | `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
@@ -677,9 +689,9 @@ the upstream `Executor` shape assignable.
677
689
  | `Y4` | `test/meta-tools-call.test.ts`, `test/call-admission.test.ts` (one attempt, retry hints, caller reissue) |
678
690
  | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls) |
679
691
  | `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors) |
680
- | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (budgets) |
692
+ | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets) |
681
693
  | `L5`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap) |
682
- | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message) |
694
+ | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.test.ts` (outer reply serialization failure settles the call) |
683
695
  | `L7` | `test/execute.test.ts`, `test/executor-admission.test.ts` |
684
696
  | `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, and the identity clamp) |
685
697
  | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
@@ -690,7 +702,7 @@ the upstream `Executor` shape assignable.
690
702
  | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
691
703
  | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
692
704
  | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
693
- | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
705
+ | `X4` | `test/guest-api-contract.test.ts` (string logs only), `test/quickjs-executor.test.ts` (logs before cancellation), `test/quickjs-child-stderr.test.ts` (crash, shutdown, deadline, IPC failure, bounded parent retention), `test/quickjs-log-limits.test.ts` (unchanged successful logs) |
694
706
  | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
695
707
  | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
696
708
 
@@ -188,6 +188,21 @@ downstream wrote them, and an unannotated or contradictory one stays
188
188
  fail-closed onto `call_destructive_tool`. The contract binds the surfaces we
189
189
  write, not the catalogs we relay.
190
190
 
191
+ An `unavailable` failure can carry `details.host` and `details.code` in its
192
+ classified error. `host` is an HTTP(S) origin, including a non-default port,
193
+ with no userinfo, path, query, or fragment and at most 253 UTF-8 bytes. `code`
194
+ is an allowlisted network errno, an undici transport code, or `timeout`, at
195
+ most 32 bytes. Invalid or oversized fields are omitted, never clipped.
196
+ `remoteMcp()` and the guarded transport know the failed fetch destination;
197
+ `api()` can classify a handler's structured runtime code but cannot recover
198
+ its destination from `ctx.baseUrl`, which names Connecta itself. A handler can
199
+ supply a known URL in a typed `ConnectorCallError` and the constructor reduces
200
+ it to an origin. AbortError and TimeoutError become diagnostic `timeout`;
201
+ workerd outbound denial supplies no errno, so a fetch boundary reports the
202
+ known origin alone. No provider prose supplies a code or retryability.
203
+ These diagnostics belong in tool failures and the bounded warning log, never
204
+ in payload-free activity records.
205
+
191
206
  ## The guarded fetch transport
192
207
 
193
208
  Every hand-written HTTP surface re-derives the same safety machinery, and two
@@ -237,9 +252,13 @@ What it owns is mechanical and provider-independent:
237
252
  as an absurd response is a fact about the API, not about HTTP. A declared
238
253
  `Content-Length` past the ceiling fails before a byte is read, and a
239
254
  streaming body is abandoned at the ceiling rather than buffered past it.
255
+ Without a body stream, text is measured in UTF-8 bytes before it is accepted;
256
+ JSON is parsed from that same bounded text. Such runtimes still buffer their
257
+ read internally, but cannot return an oversized body as a successful result.
240
258
  - **Normalization.** An unreachable provider becomes a retryable `unavailable`
241
259
  instead of whatever `TypeError` the runtime threw, and `ctx.signal` rides
242
- every request.
260
+ every request. `Retry-After` hints accept delta-seconds and HTTP dates;
261
+ a date in the past means no further wait.
243
262
 
244
263
  What it deliberately does not own is meaning. It never reads a status code and
245
264
  never invents an authentication scheme: the provider's `authenticate` callback
@@ -288,9 +307,13 @@ Connecta deliberately sits between protocol generations
288
307
  compatibility concession, so modern protocol support is still discovered.
289
308
  - **Legacy sessions:** Connecta's own endpoint creates no protocol session, but
290
309
  a stateful legacy downstream can still issue `Mcp-Session-Id`. Closing a
291
- request scope explicitly sends the legacy DELETE before closing its transport.
292
- SDK v2 `Client.close()` does not do that on Connecta's behalf, so
293
- `terminateSession` remains required and tested.
310
+ request scope, rotating credentials, retiring an OAuth generation, and
311
+ abandoning a connect all send the legacy DELETE before closing the transport.
312
+ The DELETE gets at most one second and failures are logged. Rotation and
313
+ abandoned connects start cleanup without waiting; the connector context has
314
+ no deferred-work hook. Scope teardown gives its bounded tail back to core,
315
+ which can attach the runtime's deferred channel. SDK v2 `Client.close()` does
316
+ not send DELETE on Connecta's behalf.
294
317
  - **Modern cache hints:** `tools/list` is deployment-fixed and returns a
295
318
  one-hour private cache hint. Downstream hints do not alter Connecta's existing
296
319
  five-minute fingerprinted catalog cache; that remains gated in
@@ -352,7 +375,11 @@ For remote MCP tools, that path checks the catalog's advertised `inputSchema`
352
375
  before provider dispatch. Supported mismatches become bounded, payload-free
353
376
  `invalid_args` findings; a schema the local validator cannot evaluate passes
354
377
  through unchanged. Connecta does not parse provider error prose to invent a
355
- validation classification.
378
+ validation classification. A downstream JSON-RPC `-32602` is an explicit
379
+ `invalid_args` refusal, with a bounded server message and no retry. HTTP 4xx
380
+ refusals preserve a bounded JSON `message`, including `error.message`, and are
381
+ non-retryable; 429 becomes `rate_limited` and 408 becomes `timeout`. The SDK
382
+ still owns OAuth challenges and step-up authorization.
356
383
 
357
384
  ## Authentication
358
385
 
@@ -363,6 +390,21 @@ server issuer discovered and validated by the SDK; see
363
390
  The callback route validates `state` before passing the complete callback query
364
391
  to the SDK so RFC 9207 `iss` validation is not lost.
365
392
 
393
+ Within one runtime, overlapping refreshes share one completion gate per
394
+ credential partition and generation. A valid token response is a consumed
395
+ refresh token, so the coordinator keeps that response's tokens on the flight.
396
+ If the owner then fails before the SDK saves them — cancelled, redirected to
397
+ authorization, or invalidated — the host persists the rotation itself, holds
398
+ contenders behind the mutation marker until that write lands, and hands them
399
+ the saved rotation; a retired token is never redeemed twice. A write already
400
+ running in `saveTokens` clears the marker on its own success or failure, and a
401
+ late duplicate save from a detached owner is harmless. Nothing can leave a
402
+ permanent 503 gate ([#526](https://github.com/zackbart/connecta/issues/526)).
403
+ Refresh token response validation
404
+ reads at most 65,536 bytes, and credential reads stop after 64 revision races
405
+ with `temporarily_unavailable` so churn cannot spin indefinitely. Request
406
+ cancellation is checked before each attempt.
407
+
366
408
  A remote MCP connector that authenticates with a static key has two ways to
367
409
  receive one. `{ type: "headers", headers }` bakes the literal value into the
368
410
  deployment file, which suits a secret the runtime already holds.
@@ -121,15 +121,25 @@ bytes. Within that unchanged total, each enum node and each constraint
121
121
  annotation may spend at most 256 UTF-8 bytes. Numeric bounds, string length
122
122
  bounds, patterns, and formats render beside their type. A constraint that does
123
123
  not fit is dropped whole. If constraints push the full shape over 1,024 bytes,
124
- search retries the shape without them. Compact describe keeps all declared
125
- constraints. A large enum keeps the longest whole-value prefix that fits, then
124
+ search retries the shape without them. Compact describe keeps declared
125
+ constraints and property prose within its own 8,192-byte shape cap, sharing
126
+ search's 2,000-visit rendering budget; a capped shape sets
127
+ `inputSchemaTruncated` or `outputSchemaTruncated`, and `format: "json"` or
128
+ JSON search returns the exact schema. A large enum keeps the longest whole-value prefix that fits, then
126
129
  adds `unknown` and a comment with the exact omitted-value count. An empty enum
127
130
  renders as the valid `never` type. A capped object becomes a valid
128
131
  required-first shape with `unknown` types; other shapes become
129
132
  `unknown /* truncated */`. Any cap marks the match with
130
133
  `inputSchemaTruncated` or `outputSchemaTruncated`; repeat the search with
131
134
  `includeSchemas: "json"` or use the existing describe path when exact
132
- constraints matter. Small enums and both exact paths remain complete.
135
+ constraints matter. `prefixItems` renders as a tuple, with the `items` type
136
+ as its rest, an `unknown[]` rest when open, and no rest for `items: false`.
137
+ `dependentSchemas` and `if`/`then`/`else` preserve the base shape and append
138
+ `/* conditional */`, setting the truncation flag so the caller reads the exact
139
+ JSON schema. `$dynamicRef` resolves a same-named definition like `$ref`; an
140
+ unresolved dynamic reference renders as `unknown` with the truncation flag.
141
+ These shapes share the same byte and work budgets. Small enums and both exact
142
+ paths remain complete.
133
143
 
134
144
  ## Connector guide selection
135
145
 
@@ -206,12 +216,42 @@ removing or summarizing the text copy is deferred until host-forwarding
206
216
  measurements demonstrate that supported clients do not need it.
207
217
 
208
218
  Plain-text guidance and errors remain text-only. A downstream MCP tool's native
209
- content blocks also pass through unchanged when `call_tool` uses MCP result
210
- mode; they are not a duplicated Connecta object result. Newly stashed JSON and
219
+ content blocks pass through when `call_tool` uses MCP result mode. When no
220
+ text block exists and `structuredContent` is present, Connecta appends a text
221
+ block containing its compact JSON, then applies the same content size guard.
222
+ This preserves structured-only results, including `null`, arrays, and scalars.
223
+ An existing text mirror stays unchanged; Connecta does not add another copy. Newly stashed JSON and
211
224
  downstream content envelopes use compact serialization, so `get_result` byte
212
225
  offsets and totals refer to that exact compact text.
213
226
 
214
- A `call_tool` truncation notice carries both the historical `resultId` and an
227
+ The direct-call stash keeps results for 15 minutes. `results.maxStashBytes`
228
+ defaults to 8 MiB and `results.maxStashEntries` to 64 per `createConnecta`
229
+ runtime, shared across all subjects and pools. Both accept non-negative safe
230
+ integers; zero disables stashing. The byte budget counts the stored ASCII
231
+ paging envelope, including base64 overhead, rather than only the result text.
232
+ Capacity is reserved before each storage write, so concurrent requests cannot
233
+ oversubscribe it. A full stash refuses new entries. A later stash attempt
234
+ deletes expired entries before reusing their capacity; a failed deletion keeps
235
+ the charge. These bounds cover writes by this runtime, not other processes,
236
+ Worker isolates, or entries left by a previous runtime.
237
+
238
+ Results belong to the authenticated subject whenever auth supplies a subject
239
+ or user id, independently of activity configuration. The provider's namespace
240
+ is used when present; otherwise the namespace is `connecta:auth:<provider kind>`.
241
+ Keep subject ids distinct within that namespace. An explicit principal is the
242
+ fallback subject when neither id is supplied. Open deployments and auth
243
+ providers that supply no identity share one partition.
244
+
245
+ New entries store UTF-8 bytes in a base64 envelope. After the KV read,
246
+ `get_result` decodes only the requested byte range and a few boundary bytes;
247
+ it does not encode the full text on every page. Storage still reads one full
248
+ value. Pre-upgrade raw-text entries remain readable during their TTL using
249
+ one full encoding per page. Offsets and `totalBytes` always describe the
250
+ original UTF-8 text, not the envelope. A supplied offset inside a character
251
+ moves back to its start; page ends also align to character boundaries, and a
252
+ page smaller than one character widens just enough to make progress.
253
+
254
+ A successfully stashed `call_tool` truncation notice carries both the historical `resultId` and an
215
255
  exact `nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`. The
216
256
  handle is therefore
217
257
  directly actionable without copying an identifier out of prose. Program results
@@ -219,6 +259,28 @@ and oversized discovery responses carry no such route — paging a program's
219
259
  return value is a refused shape, because a program can shrink anything before
220
260
  it returns.
221
261
 
262
+ A refused stash or failed stash write cannot undo a downstream success. Both direct call tools
263
+ return a truncated preview where usable, with a paging-unavailable notice and
264
+ no `resultId` or paging action. Activity records success; the operator logger
265
+ receives a fixed warning with the connector and tool, without storage error
266
+ prose. For read-only work, reduce the result inside `execute_code`; repeating
267
+ an approved write is not a way to recover its output. Other result-processing
268
+ failures use a fixed `result_processing_failed` message and are never retryable.
269
+
270
+ Downstream MCP `isError` text is bounded at its source to 512 UTF-8 bytes plus
271
+ an `…` marker. Error framing may shorten it further to fit the call's result
272
+ cap, counting JSON escaping and both copies in value mode.
273
+
274
+ The top-level discovery ceiling is 256,000 UTF-8 bytes for the serialized tool
275
+ result, including text, `structuredContent`, and JSON escaping. Measuring only
276
+ one copy would advertise half the bytes the adapter actually returns.
277
+
278
+ A per-call `timeoutMs` covers catalog resolution, admission, and connector
279
+ execution with one deadline. The admission queue's own timeout may expire
280
+ sooner, but it cannot extend the call deadline. Result processing happens after
281
+ that deadline ends, because a completed downstream call must not turn into a
282
+ retryable timeout while Connecta prepares its response.
283
+
222
284
  ## Lexical discovery
223
285
 
224
286
  `search_tools` tokenizes tool names and descriptions at punctuation and
@@ -373,6 +435,12 @@ recovery query, each of which lands in both the text content and
373
435
  the address is the thing being corrected, a clipped one still identifies the
374
436
  mistake, and a short one — every real one — comes back exact and untagged.
375
437
 
438
+ Unknown `get_result.id`, `authorize_connector.connector`, and `skills.name`
439
+ echoes use the same 512-byte clamp. `search_tools.connector` instead rejects
440
+ values over 512 UTF-8 bytes with `invalid_args` before catalog lookup, so a
441
+ clipped scope can never select a different connector. A failed result-storage
442
+ read returns typed `unavailable` without exposing backend error text.
443
+
376
444
  `call_destructive_tool` accepts an optional
377
445
  `reason` of at most 500 characters for the host's human approval view. It is
378
446
  outer-call context only: Connecta neither treats it as authority nor passes it
@@ -231,30 +231,30 @@ in.
231
231
  | Suite | Covers |
232
232
  | --- | --- |
233
233
  | `activity.test.ts` | payload-free delivery: a rejected async write attaches to `waitUntil` instead of throwing, approved destructive calls record under their real entry point, result-size friction records without retaining the result, and a hallucinated connector id or invented identity is clamped so the event still cannot carry a payload |
234
- | `api-connector.test.ts` | `api()` — kind, description, tool defs, dispatch, default args, unknown tools, handler throws, argument validation, and the construction contract |
234
+ | `api-connector.test.ts` | `api()` — kind, description, tool defs, dispatch, default args, unknown tools, handler throws, argument validation, structured transport diagnostics, and the construction contract |
235
235
  | `bearer.test.ts` | constant-time bearer compare, case-insensitive scheme, 401 challenges, and the retired audience options refusing rather than silently unbinding |
236
236
  | `branding.test.ts` | branding fallbacks and overrides across the operator shells, OAuth result pages, `/favicon.*`, page titles, and escaping — branding is not an injection vector |
237
- | `call-admission.test.ts` | connector-scoped per-runtime downstream admission ([call admission](./call-admission.md)): independent partitions, exact rolling-window reset, cancellation that charges no budget, bounded partition state, local-refusal health isolation, one shared limiter across direct and program calls, and payload-free `/health` aggregates |
238
- | `catalog-drift.test.ts` | `vettedCatalog()`, `detectCatalogDrift()`, and `withVettedCatalog()`; drift on the registry surface and on `/health`; the connector seam projected rather than echoed; and the drift types being public |
239
- | `catalog.test.ts` | lexical ranking and the compact schema renderer `const`, `allOf` beside siblings, `$ref`, the depth limit, per-schema caching, and 2020-12 keyword compatibility |
237
+ | `call-admission.test.ts` | connector-scoped per-runtime downstream admission ([call admission](./call-admission.md)): independent partitions, exact rolling-window reset, cancellation that charges no budget, bounded partition state, local-refusal health isolation, direct and program calls sharing the appropriate controller, independent personal budgets, shared budgets, shutdown of both controller kinds, eviction preserving live budgets, and id-free `/health` aggregates |
238
+ | `catalog-drift.test.ts` | `vettedCatalog()`, `detectCatalogDrift()`, and `withVettedCatalog()`; bounded schema-digest depth; drift on the registry surface and on `/health`; the connector seam projected rather than echoed; and the drift types being public |
239
+ | `catalog.test.ts` | lexical ranking and the compact schema renderer: `const`, `allOf` beside siblings, memoized `$ref`, node/depth/byte bounds, truncation flags, declared required-key metadata, typed search argument validation, per-schema caching, and 2020-12 tuples, conditional markers, and dynamic references in search and describe |
240
240
  | `clerk.test.ts` | protected-resource metadata, the browser sign-in config, OAuth and session tokens, cached best-effort activity labels with their caps, the hand-applied `azp` rejection, and the `allowedDomains` allowlist including every lookalike that must not be repaired into a match |
241
241
  | `cloudflare-access-auth.test.ts` | trusted `ctx.access` human and service identities, absent/error fail-closed behavior, service-token MCP admission without operator mutation, human same-origin mutation, and the Clerk-to-ambient shell switch |
242
242
  | `cloudflare-provider.test.ts` | `cloudflare()` API and MCP construction, the code-mode safety manifest, API tool surface, current R2 and KV jurisdictions, useful output declarations, request building, projections including additive provider fields, typed failures, and credential test |
243
243
  | `code-first-surface.test.ts` | the seven-tool surface itself — an executor required, every removed option and top-level tool refused, compact always-loaded routing pinned below 1,000 characters, complete on-demand usage served, and no rendering instructions |
244
244
  | `codemode-compat.test.ts` | the `Executor` seam staying structurally compatible with `@cloudflare/codemode`'s `DynamicWorkerExecutor`, enforced by `tsc` |
245
- | `config.test.ts` | the grouped `ConnectaConfig` boundary — each group forwarding to its internals, malformed admission bounds failing construction, and unknown own-properties rejected by their complete path before construction does work |
245
+ | `config.test.ts` | the grouped `ConnectaConfig` boundary — each group forwarding to its internals, malformed admission bounds, result stash limits, and origin lists failing construction, open-connector warnings including static API auth, and unknown own-properties rejected by their complete path before construction does work |
246
246
  | `credentials.test.ts` | the pure stored-shape classifier (containment, not equality) and the AES-GCM vault: round-trip, ciphertext bound to its connector id, named field sets, masked metadata, wrong-key rejection, deletion, coexistence with OAuth keys |
247
247
  | `d1-activity-example.test.ts` | the Worker example's deployment-owned D1 activity store: actor namespace round-trip, payload-free friction reconstructed from the persisted code, and agreement with the package's friction table |
248
- | `downstream-oauth.test.ts` | `KvOAuthProvider` round-trips and generation races, runtime-local rotating-token refresh coordination across request scopes, refresh failure/retry, `auth_required` versus `error`, `startAuth`/`finishAuth`, callback refusal equality, bounded diagnostics, and HTML escaping |
249
- | `errors.test.ts` | `ConnectorCallError` codes, retryable defaults and overrides, `retryAfterMs` round-trip, typed-over-heuristic classification, `AbortError` as a retryable timeout, and framing errors |
250
- | `execute.test.ts` | the code-mode host bridge: identifier sanitization, account titles in program discovery, MCP-result unwrapping, sandbox provider construction, authenticated thrown-failure framing, fail-closed filtering of destructive and unannotated tools, MCP/code-mode invocation parity, and payload-free describe diagnostics |
248
+ | `downstream-oauth.test.ts` | `KvOAuthProvider` round-trips and generation races, two eight-scope waves of runtime-local rotating-token refresh, recovery after abort/redirect/invalidation, bounded token reads and revision churn, refresh failure/retry, `auth_required` versus `error`, `startAuth`/`finishAuth`, callback refusal equality and identity-free 401 versus explicit 403, bounded diagnostics, and HTML escaping |
249
+ | `errors.test.ts` | `ConnectorCallError` codes, retryable defaults and overrides, `retryAfterMs` round-trip, typed-over-heuristic classification, `AbortError` as a retryable timeout, sanitized bounded unavailable diagnostics, and framing errors |
250
+ | `execute.test.ts` | the code-mode host bridge: identifier sanitization, account titles in program discovery, MCP-result unwrapping, sandbox provider construction, authenticated thrown-failure framing, shared discovery/call budgets including pre-dispatch refusals, bounded recent-failure matching, empty terminal errors, logs on thrown executor failures, short-message failure matching, fail-closed filtering of destructive and unannotated tools, MCP/code-mode invocation parity, and payload-free describe diagnostics |
251
251
  | `execute-emit.test.ts` | `connecta.emit` (M1–M10) — block validation, budgets, the provider, delivery after the result envelope on success only, and the defaults |
252
252
  | `executor-admission.test.ts` | the portable bounded FIFO both pools use: active and queue ceilings, stable retryable overload, queue timeout, cancellation removal, idempotent release, shutdown |
253
- | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, and response handling |
253
+ | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, response handling, bounded no-stream text/JSON reads, sanitized transport diagnostics, and delta-seconds/HTTP-date Retry-After hints |
254
254
  | `guest-api-contract.test.ts` | the shared guest contract on the Dynamic Worker, including caught call, typed inline describe recovery, discovery, utility, parallel-call, and budget failure codes; plus the real authority boundary — local `data:` fetch, denied egress, unresolved DNS, empty environment paths, unavailable filesystem/HTTP builtins, and present runtime globals |
255
- | `identity-scope.test.ts` | identity-derived connector visibility, named pools at `/mcp/<pool>` (grant-gated, intersected with the identity ceiling, identical 404 for undeclared, refused, and throwing grants, construction-time refusals), exact `connector.tool` grants enforced identically across discovery, direct calls, the program host bridge, and the connection UI, fail-closed grant parsing, the once-per-isolate absent-grant warning, personal credential isolation, separate shared-auth and personal-auth management permissions, and personal OAuth callback ownership |
255
+ | `identity-scope.test.ts` | identity-derived connector visibility, named pools at `/mcp/<pool>` (grant-gated, intersected with the identity ceiling, identical 404 for undeclared, refused, and throwing grants, construction-time refusals), exact `connector.tool` grants enforced identically across discovery, direct calls, the program host bridge, and the connection UI, fail-closed grant parsing, the once-per-isolate absent-grant warning, personal credential isolation, separate shared-auth and personal-auth management permissions, bearer-subject result isolation without activity namespaces, connector authorization visibility for tool grants, and personal OAuth callback ownership |
256
256
  | `linear-provider.test.ts` | the Linear proxy's construction, guide, plan-aware catalog superset, and current workspace, template, and issue-sharing classifications |
257
- | `meta-tools-call.test.ts` | registry-backed calls: structured errors, truncation and `get_result`, per-connector result bounds, JSON representation failures, MCP content bounds, and offset alignment |
257
+ | `meta-tools-call.test.ts` | registry-backed calls: structured errors, truncation and `get_result`, per-connector result bounds, JSON representation failures, MCP content bounds, offset alignment, runtime-wide stash quotas including pending writes and expiry reclamation, and page-proportional decoding |
258
258
  | `meta-tools-search.test.ts` | registry-backed discovery: bounded search with page and address maxima, compact and JSON schemas with constraints, typed describe recovery and suggestions, and structured-result compatibility |
259
259
  | `meta-tools.test.ts` | the remaining registry-backed meta-tools: the complete on-demand usage skill, connector-guide selection and summary bounds, stored-credential drift, catalog health, authorization, probe timeouts, and unavailable or unknown browse recovery |
260
260
  | `mixpanel-provider.test.ts` | the Mixpanel proxy, its conditional-input guide, destructive metadata fill, and complete 64-tool schema-digest manifest |
@@ -264,21 +264,21 @@ in.
264
264
  | `optional-modules.test.ts` | absent modules, UI-free OAuth, fast lists and independent detail deadlines, explicit auth-management grants, invalid-resolver refusal, and passive OAuth consent-state protection |
265
265
  | `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), their compact discovery schemas staying complete (H7), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
266
266
  | `provider-registry.test.ts` | all seven maintained providers inside real deployments: boot, description, address, catalog, storage, credential, admission, and activity isolation; plus provider-specific discovery and guide contracts |
267
- | `registry.test.ts` | construction and id validation, startup warnings, address resolution, version 2 catalog TTL/persistence/completeness, agent-only stale-while-revalidate with cross-request single-flight shared with blocking reads in both start orders, owned teardown, invalidation/fingerprint guards, blocking diagnostics, and broken-connector isolation |
268
- | `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
267
+ | `registry.test.ts` | construction and id validation, startup warnings, bounded absent-grant warning state, address resolution, version 2 catalog TTL/persistence/completeness, agent-only stale-while-revalidate with cross-request single-flight shared with blocking reads in both start orders, owned teardown, invalidation/fingerprint guards, blocking diagnostics, broken-connector isolation, and bounded opportunistic memory-storage expiry |
268
+ | `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, session termination on scope close/rotation/abandonment, bounded JSON-RPC/HTTP error classification, and sanitized transport diagnostics |
269
269
  | `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from the connection UI: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, the payload-free activity event, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
270
270
  | `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
271
271
  | `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
272
272
  | `result-shapes.test.ts` | passive output-shape learning: value-free bounded inference, merging, 256-entry LRU eviction, 24-hour expiry, runtime isolation, read-only admission, declared-schema precedence, definition-change invalidation, discovery provenance, and failure isolation |
273
273
  | `revenuecat-provider.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the refund-preference read and argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot` |
274
- | `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory, account titles, on-demand investigation guidance, and no Apps metadata or resource capability → complete usage skill → `call_tool`), conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, removed connector HTTP hooks rejected at construction, and deferred catalog reads through both discovery surfaces |
275
- | `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: built-in routes and unknown-path 404s inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
274
+ | `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory, account titles, on-demand investigation guidance, and no Apps metadata or resource capability → complete usage skill → `call_tool`), fixed seven-tool registration order across requests, conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, removed connector HTTP hooks rejected at construction, and deferred catalog reads through both discovery surfaces |
275
+ | `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: Origin refusal before admission/auth/redirects, exact-origin and wildcard CORS, SEP-2243 preflight headers, all pool suffixes behind auth, application overload/shutdown codes, id-free health preserving doctor drift counts, built-in routes and unknown-path 404s inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
276
276
  | `startup-warnings.test.ts` | every construction-time `logger.warn` and, as importantly, the conditions that must *not* trigger one: open mode with a credential or OAuth connector, `publicUrl` unset beside OAuth, dropped branding and `uiAuth` URLs, a missing `verifyState`, a credential test-hook mismatch, and an unusable `calls.maxResultBytes` |
277
277
  | `stripe-provider.test.ts` | the Stripe proxy's mixed-mode OAuth and fixed-mode header contracts, current eleven-tool classifications, admission, exact account selectors, and no-guess rule |
278
278
  | `operator-view.test.ts` | the app's pure state rules from `view.ts`: filtering, page routing, capability states, activity summaries, drift display, and identity reset |
279
279
  | `ui-credentials.test.ts` | credential-management routes: save, test, delete, validation, authentication, same-origin checks, and multi-field credential shapes |
280
280
  | `ui.test.ts` | the server shell and remaining `/ui/*` routes: gated `/ui/data` with broken-connector isolation and registry-owned catalog-observation containment, plus the URL safety gates |
281
- | `validate.test.ts` | `validateToolInput()` — a returned (not thrown) `invalid_args` naming the path, `additionalProperties: false` enforcement, per-schema validator caching, and an unusable schema passed through with one warning |
281
+ | `validate.test.ts` | `validateToolInput()` — a returned (not thrown) `invalid_args` naming the path, `additionalProperties: false` enforcement, bounded UTF-8 validation detail, per-schema validator caching, and an unusable schema passed through with one warning |
282
282
  | `vercel-provider.test.ts` | `vercel()` API and MCP construction, MCP inventory classification, team scoping, project and deployment projections, finite build and runtime logs, value-safe environment variables, domains, lifecycle writes, REST hatches, typed failures, and credential test |
283
283
 
284
284
  ### Node-bound (`NODE_ONLY_SUITES`)
@@ -292,15 +292,15 @@ justification for *not* re-running it in workerd, so "it was easier" is not one.
292
292
  | `doc-links.test.ts` | the documentation checker itself — local file and fragment resolution, repository URLs resolved back to the checkout, duplicate heading slugs, fenced-code exclusion, and useful failures | spawns the Node checker against filesystem fixtures |
293
293
  | `doctor-cli.test.ts` | `connecta doctor`'s executor line and credentials end to end — the sandbox the deployment reports is the one named, an unidentifiable executor gets an executor-neutral line, a hostile name is bounded, and a complete Cloudflare Access service-token pair is accepted while a partial pair is refused | spawns the CLI against a Node HTTP deployment over real sockets |
294
294
  | `drift-check.test.ts` | the credential-free maintainer drift checker: recorded touched endpoints, heading, table, and inline MCP inventories, setup-only providers, live-schema ownership, a quiet revision bump, clear failures for unavailable inputs, `$ref` traversal, and one well-formed row per endpoint | spawns the checker against filesystem fixtures |
295
- | `file-storage.test.ts` | `fileStorage()` across instances, logical TTL plus physical pruning without clobbering a newer value, and corrupt-file quarantine | exercises the Node filesystem storage adapter |
295
+ | `file-storage.test.ts` | `fileStorage()` round trips, exclusive writer locks, namespace-aware heartbeat expiry and stale-guard recovery, close/exit cleanup, unique temp files, logical TTL plus physical pruning, and corrupt-file quarantine | exercises the Node filesystem storage adapter |
296
296
  | `guest-api-contract-quickjs.test.ts` | the shared guest-contract cases on the real QuickJS executor, including identical caught failure codes and inline describe recovery, its exact absent globals, and blocked runtime imports | runs the contract cases on the Node QuickJS executor |
297
297
  | `node.test.ts` | the `listen()` adapter propagating an HTTP client disconnect through the Web `Request` and the MCP handler into a program's connector call, releasing both admission permits | exercises the Node HTTP adapter over real TCP sockets |
298
298
  | `packed-links.test.ts` | the packed-link gate itself — shipped targets and repository URLs accepted, relative links into unshipped paths and directories rejected with the citation to write instead, reference definitions seen, fenced examples ignored, the changelog exempt | spawns the Node packed-link gate against filesystem fixtures |
299
299
  | `package-surface.test.ts` | the published boundary — built output shipped, the `exports` map carrying exactly the documented subpaths plus `./package.json`, only generic factories, platform storage kept in examples, Clerk and QuickJS behind optional subpaths, dependency-free Cloudflare Access behind its Worker subpath, every provider independently importable, and the Cloudflare API provider free of bare specifiers | walks the package tree with Node filesystem APIs |
300
300
  | `purity.test.ts` | the import-graph guardrail ([architecture](./architecture.md#import-graph-purity)) — the core stays Workers-clean | walks the source import graph with Node filesystem APIs |
301
301
  | `quickjs-child-entry.test.ts` | a missing QuickJS child entry failing before `fork()`, with the expected path and the bundler-externalization constraint | mocks Node child-process and filesystem APIs |
302
- | `quickjs-child-stderr.test.ts` | the QuickJS child-process boundary: an explicitly empty environment despite parent secrets and `NODE_OPTIONS`, plus abnormal exits retaining only an 8 KiB stderr tail in the parent-side diagnostic | mocks Node child-process streams |
303
- | `quickjs-executor.test.ts` | executable usage example with dependent, missing, and approval-required evidence; the child-process sandbox — code normalization, provider bridges and canonical connector calls, bounded IPC, separate guest-CPU and wall budgets, saturation, cancellation and shutdown, crash and OOM recovery, host-call hangs, stalled-promise detection | runs the Node QuickJS child-process executor |
302
+ | `quickjs-child-stderr.test.ts` | the QuickJS child-process boundary: an explicitly empty environment despite parent secrets and `NODE_OPTIONS`, abnormal exits retaining only an 8 KiB stderr tail in the parent-side diagnostic, host-result serialization failure returning a bounded error reply, streamed logs on real child crashes, bounded parent retention, and log recovery on shutdown, deadline termination, and IPC failures | mocks Node child-process streams and runs the Node QuickJS child-process executor |
303
+ | `quickjs-executor.test.ts` | executable usage example with dependent, missing, and approval-required evidence; the child-process sandbox — code normalization, provider bridges and canonical connector calls, private authenticated-failure transport and bounded error details, bounded IPC, separate guest-CPU and wall budgets, saturation, cancellation and shutdown, crash and OOM recovery, host-call hangs, stalled-promise detection, logs preserved on cancellation after a program starts | runs the Node QuickJS child-process executor |
304
304
  | `quickjs-log-limits.test.ts` | bounded `console.*` capture — per-entry cut, cumulative character and transport budgets, escape-heavy floods preserving the guest result | runs the Node QuickJS child-process executor |
305
305
  | `suite-partition.test.ts` | this partition, including itself: every `*.test.ts` in exactly one list, stale entries and empty reasons refused | walks the test directory to guard the partition |
306
306
  | `template-file-activity.test.ts` | the Node template's own activity store — persistence across restart, torn-line repair, newest-first paging, and compaction past the slack window | runs it against real files |
@@ -231,6 +231,13 @@ convenient reading. A call that can only fail is refused locally as
231
231
  `invalid_args` before the round trip. Provider error prose is never parsed to
232
232
  invent a classification.
233
233
 
234
+ An unreachable transport may report `unavailable` with optional sanitized
235
+ `details.host` and `details.code`. The host is an HTTP(S) origin only, at most
236
+ 253 UTF-8 bytes; the code is an allowlisted runtime network code or `timeout`,
237
+ at most 32 bytes. Omit what the runtime cannot establish. The typed error
238
+ constructor strips URL credentials, paths, queries, and fragments, and drops
239
+ invalid or oversized fields. Transport diagnostics never enter activity records.
240
+
234
241
  **A downstream 404 is `not_found` — when the provider means it.** The code
235
242
  exists because the next move is none of the others': you do not wait, you do
236
243
  not send the agent to `authorize_connector`, you do not repair the argument