@zackbart/connecta 0.24.2 → 0.24.4

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 (97) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +5 -6
  4. package/dist/auth/bearer.js +2 -0
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/branding.d.ts +31 -2
  8. package/dist/branding.js +116 -8
  9. package/dist/call-admission.d.ts +4 -0
  10. package/dist/call-admission.js +26 -0
  11. package/dist/catalog-drift.js +9 -4
  12. package/dist/catalog-service.d.ts +2 -0
  13. package/dist/catalog-service.js +25 -8
  14. package/dist/catalog.d.ts +2 -0
  15. package/dist/catalog.js +246 -121
  16. package/dist/connectors/api.d.ts +1 -1
  17. package/dist/connectors/api.js +21 -3
  18. package/dist/connectors/guarded-fetch.d.ts +6 -2
  19. package/dist/connectors/guarded-fetch.js +61 -24
  20. package/dist/connectors/remote-mcp.js +92 -57
  21. package/dist/errors.d.ts +28 -3
  22. package/dist/errors.js +60 -1
  23. package/dist/execute.js +85 -23
  24. package/dist/executor-result.js +3 -1
  25. package/dist/executors/quickjs-child.js +5 -1
  26. package/dist/executors/quickjs-protocol.d.ts +4 -0
  27. package/dist/executors/quickjs-runtime.d.ts +1 -1
  28. package/dist/executors/quickjs-runtime.js +38 -21
  29. package/dist/executors/quickjs.js +68 -27
  30. package/dist/index.d.ts +15 -1
  31. package/dist/index.js +36 -4
  32. package/dist/invocation.js +134 -93
  33. package/dist/mcp-result.js +3 -2
  34. package/dist/meta-tools.js +191 -36
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +38 -1
  37. package/dist/operator-ui/view.js +71 -0
  38. package/dist/providers/cloudflare.d.ts +14 -2
  39. package/dist/providers/cloudflare.js +107 -16
  40. package/dist/providers/linear.d.ts +26 -4
  41. package/dist/providers/linear.js +19 -4
  42. package/dist/providers/mixpanel.d.ts +16 -3
  43. package/dist/providers/mixpanel.js +13 -2
  44. package/dist/providers/notion.d.ts +8 -1
  45. package/dist/providers/notion.js +83 -10
  46. package/dist/providers/revenuecat.d.ts +30 -4
  47. package/dist/providers/revenuecat.js +42 -4
  48. package/dist/providers/stripe.d.ts +7 -1
  49. package/dist/providers/stripe.js +30 -4
  50. package/dist/providers/vercel.js +11 -1
  51. package/dist/registry.d.ts +22 -2
  52. package/dist/registry.js +101 -13
  53. package/dist/routes/mcp.d.ts +4 -1
  54. package/dist/routes/mcp.js +84 -13
  55. package/dist/routes/oauth.js +4 -0
  56. package/dist/routes/shared.d.ts +1 -0
  57. package/dist/routes/shared.js +4 -4
  58. package/dist/server.js +15 -3
  59. package/dist/skills.js +6 -5
  60. package/dist/storage/file.d.ts +6 -2
  61. package/dist/storage/file.js +312 -34
  62. package/dist/storage/memory.js +12 -1
  63. package/dist/types.d.ts +37 -0
  64. package/dist/ui.js +18 -10
  65. package/dist/validate.js +3 -3
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/documentation/architecture.md +193 -165
  69. package/documentation/auth.md +199 -145
  70. package/documentation/code-mode.md +433 -316
  71. package/documentation/meta-tools.md +363 -355
  72. package/examples/worker/AGENTS.md +2 -1
  73. package/examples/worker/README.md +12 -10
  74. package/examples/worker/src/index.ts +12 -15
  75. package/package.json +1 -2
  76. package/templates/node/.env.example +3 -3
  77. package/templates/node/AGENTS.md +5 -4
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +23 -22
  81. package/documentation/call-admission.md +0 -142
  82. package/documentation/cloudflare.md +0 -471
  83. package/documentation/connector-guides.md +0 -176
  84. package/documentation/connectors.md +0 -389
  85. package/documentation/linear.md +0 -193
  86. package/documentation/mixpanel.md +0 -160
  87. package/documentation/notion.md +0 -308
  88. package/documentation/operations.md +0 -359
  89. package/documentation/operator-ui.md +0 -135
  90. package/documentation/optional-modules-upgrade.md +0 -243
  91. package/documentation/provider-conventions.md +0 -722
  92. package/documentation/request-admission.md +0 -170
  93. package/documentation/revenuecat.md +0 -305
  94. package/documentation/storage-and-credentials.md +0 -201
  95. package/documentation/stripe.md +0 -262
  96. package/documentation/upgrading.md +0 -754
  97. package/documentation/vercel.md +0 -241
@@ -1,21 +1,17 @@
1
1
  # Code mode — the guest API contract
2
2
 
3
- This is the normative description of what a program written for `execute_code`
4
- is promised: what it can reach, what it gets back, how failures look, what it
5
- may retry, what bounds it runs under, and what its execution leaves behind in
6
- the activity surface. It is the interface a model actually programs against, so
7
- it is specified in prose first and implemented second — the same discipline the
8
- [MCP spec bump](https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md) followed.
9
-
10
- Two executors implement this document: QuickJS in a child process on Node, and
11
- `DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence between
12
- them is a bug unless it appears in [Executor exceptions](#executor-exceptions),
13
- which names the reason. Anyone can implement a third from this document alone.
14
-
15
- The [code-first exploration](https://github.com/zackbart/connecta/blob/main/records/code-first-exploration.md) is the evidence behind
16
- the direction; [`ethos.md`](../ethos.md) carries the verdicts. Where its prototype
17
- and this document disagree, this document wins. Clause identifiers (`A1`, `E3`, …)
18
- are stable and cited by the tests in [Verification](#verification).
3
+ The normative description of what a program written for `execute_code` is
4
+ promised: what it can reach, what it gets back, how failures look, what it may
5
+ retry, what bounds it runs under, and what it leaves behind in activity. It is
6
+ specified in prose first and implemented second.
7
+
8
+ Two executors implement it: QuickJS in a child process on Node, and
9
+ `DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence is a
10
+ bug unless it appears in [Executor exceptions](#executor-exceptions), which
11
+ names the reason; a third executor is implementable from this document alone.
12
+ [`ethos.md`](../ethos.md) carries the verdicts behind the shape,
13
+ [`meta-tools.md`](./meta-tools.md) owns the top-level tool contract, and clause
14
+ identifiers (`A1`, `E3`, …) are stable and cited by [Verification](#verification).
19
15
 
20
16
  ## Deploy-time capability
21
17
 
@@ -32,10 +28,7 @@ QuickJS subpath:
32
28
  import { createConnecta } from "@zackbart/connecta";
33
29
  import { quickJsExecutor } from "@zackbart/connecta/quickjs";
34
30
 
35
- const connecta = createConnecta({
36
- executor: quickJsExecutor(),
37
- // connectors, auth, storage…
38
- });
31
+ createConnecta({ executor: quickJsExecutor() /* connectors, auth, storage… */ });
39
32
  ```
40
33
 
41
34
  `quickJsExecutor()` runs each program in a disposable child-process sandbox; its
@@ -44,17 +37,16 @@ on the executor. Server bundlers must keep the `@zackbart/connecta/quickjs`
44
37
  package files external so the child entry stays on disk. The
45
38
  [Node template](../templates/node/README.md) carries the complete setup.
46
39
 
47
- On Cloudflare Workers, the Worker Loader binding provides the required sandbox:
40
+ On Cloudflare Workers the Worker Loader binding is the sandbox, and Dynamic
41
+ Workers require the Workers Paid plan:
48
42
 
49
43
  ```ts
50
- createConnecta({
51
- executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
52
- // connectors, auth, storage…
53
- });
44
+ createConnecta({ executor: new DynamicWorkerExecutor({ loader: env.LOADER }) });
54
45
  ```
55
46
 
56
- Dynamic Workers require the Workers Paid plan. The supported constructor passes only `loader`; `bindings`, `modules`, or
57
- `globalOutbound` grant ambient guest authority and violate `P2`. The [Worker example](../examples/worker/README.md#code-mode) carries the full setup.
47
+ The supported constructor passes only `loader`; `bindings`, `modules`, or
48
+ `globalOutbound` grant ambient guest authority and violate `P2`. The
49
+ [Worker example](../examples/worker/README.md#code-mode) has the full setup.
58
50
 
59
51
  ## What an executor must implement
60
52
 
@@ -86,19 +78,19 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
86
78
  called with the program's arguments and awaited. Connecta supplies `search`,
87
79
  `describe`, `call`, and `emit`.
88
80
  2. **Evaluate `prelude` after the provider globals exist and before the
89
- program.** This is trusted host code. Connecta uses it to restore typed host
90
- errors in the guest without exposing the private error frame.
91
- 3. **Marshal values as JSON** in both directions (`P3`), and reject a host call
81
+ program.** It is trusted host code; connecta uses it to restore typed host
82
+ errors without exposing the private error frame.
83
+ 3. **Marshal values as JSON** both directions (`P3`), and reject a host call
92
84
  whose function is not an own property of `fns` — the guest can ask for
93
- anything, including inherited members.
85
+ inherited members too.
94
86
  4. **Return, never throw, for a failed program**: set `error` to the guest's
95
87
  message, leave `result` undefined. `createExecuteTool` reads `error` first and
96
88
  matches it back to the failures recorded during the run, which is how an
97
89
  uncaught tool failure keeps its type (`E1`).
98
90
  5. **Capture `console.log`, `console.warn`, and `console.error`** into `logs` in
99
91
  call order (`R5`), bounding what it retains.
100
- 6. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`). Keep
101
- ambient capabilities within the documented and tested `P2`/`X5` boundary.
92
+ 6. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`), staying
93
+ inside the tested `P2`/`X5` boundary.
102
94
  7. **Grant no ambient authority of its own.** Never back this with `eval` or
103
95
  `node:vm`: the sandbox is a containment layer on top of connecta's boundary,
104
96
  not a replacement for it, and every capability arrives through `fns`.
@@ -107,63 +99,60 @@ Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
107
99
  `execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
108
100
  connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise. The
109
101
  optional `name` — else a class's constructor name, which a minifier may rewrite
110
- — is what [`/health` and `doctor`](./operations.md#the-cli) report.
111
-
112
- Note what is *not* on this list: [emitted output](#emitted-output) asks nothing
113
- of an executor — `connecta.emit` is just another provider function (`M8`).
102
+ — is the executor name `/health` and `connecta doctor` report.
103
+ [Emitted output](#emitted-output) asks nothing of an executor: `connecta.emit`
104
+ is just another provider function (`M8`).
114
105
 
115
106
  ## The program
116
107
 
117
- **P1.** A program is one JavaScript `async` arrow-function expression. It is
118
- evaluated once and its resolved value is the program's result. Both executors
119
- also accept markdown-fenced code and a bare statement body, and each normalizes
120
- those differently; that leniency is a courtesy to model output, not contract. A
121
- program that is not an async arrow expression may be accepted, rejected, or
122
- reinterpreted, so do not rely on it.
123
-
124
- **P2.** The only capabilities in the contract are:
125
-
126
- - `connecta.search`, `connecta.describe`, `connecta.call`, `connecta.emit`;
127
- - `console.log`, `console.warn`, `console.error`, captured and returned.
128
-
129
- Anything else a runtime happens to expose is outside the portable contract and
130
- must not be used. QuickJS grants none of it. A loader-only Dynamic Worker denies
131
- external egress and filesystem access and keeps its environment maps empty, but
132
- it exposes the globals and runtime builtins described in `X5`.
133
-
134
- **P3.** Values cross the host bridge as JSON. Arguments must be
135
- JSON-serializable and results arrive as plain JSON values. A value outside JSON —
136
- a cycle, a `BigInt`, a function, a class instance — never round-trips: it either
108
+ **P1.** A program is one JavaScript `async` arrow-function expression, evaluated
109
+ once; its resolved value is the result. Both executors also accept
110
+ markdown-fenced code and a bare statement body, normalizing those differently —
111
+ a courtesy to model output, not contract, so anything else may be accepted,
112
+ rejected, or reinterpreted.
113
+
114
+ **P2.** The only capabilities in the contract are `connecta.search`,
115
+ `connecta.describe`, `connecta.call`, `connecta.emit`, and `console.log` /
116
+ `console.warn` / `console.error`, captured and returned. Anything else a runtime
117
+ exposes is outside the portable contract and must not be used. QuickJS grants
118
+ none of it. A loader-only Dynamic Worker denies external egress and filesystem
119
+ access and keeps its environment maps empty, but exposes the globals and runtime
120
+ builtins in `X5`.
121
+
122
+ **P3.** Values cross the host bridge as JSON: arguments must be
123
+ JSON-serializable and results arrive as plain JSON. A value outside JSON — a
124
+ cycle, a `BigInt`, a function, a class instance — never round-trips; it either
137
125
  ends the run with an error or is converted lossily, executor's choice (`X9`).
138
- Return JSON-shaped data and the question does not arise.
139
126
 
140
- **P4.** Nothing survives an execution. There is no module scope, cache, or scratch storage carried to the next program, and no request-bound object outlives the request that created it. Within one execution, host calls share one downstream request scope. `S9`'s host-owned output observation is catalog metadata, not guest memory: a later program receives no prior value or object, only a labeled field/type schema through discovery.
127
+ **P4.** Nothing survives an execution: no module scope, cache, or scratch
128
+ storage carried to the next program, and no request-bound object outliving its
129
+ request. Within one execution, host calls share one downstream request scope.
130
+ `S9`'s output observation is host-owned catalog metadata, not guest memory — a
131
+ later program receives a labeled field/type schema through discovery, never a
132
+ prior value or object.
141
133
 
142
- **P5.** Plain JavaScript only. TypeScript syntax is a syntax error. Portable code
143
- does not import: QuickJS blocks imports, while Dynamic Workers expose the `X5`
144
- runtime modules. Neither executor exposes `require`.
134
+ **P5.** Plain JavaScript only; TypeScript syntax is a syntax error. Portable
135
+ code does not import: QuickJS blocks imports, Dynamic Workers expose the `X5`
136
+ runtime modules, and neither exposes `require`.
145
137
 
146
138
  ## Addressing
147
139
 
148
- **A1.** A tool has one canonical address, `<connectorId>.<toolName>`, exactly
149
- as discovery returns it. Call it with `connecta.call(address, args)`. Punctuation
140
+ **A1.** A tool has one canonical address, `<connectorId>.<toolName>`, exactly as
141
+ discovery returns it. Call it with `connecta.call(address, args)`. Punctuation
150
142
  is preserved; no JavaScript identifier conversion takes place.
151
143
 
152
- **A2.** Connectors create no guest globals. Connector ids that resemble a
153
- JavaScript builtin, or would collide after sanitization, remain usable through
154
- their canonical addresses. The bounded connector inventory in the tool
155
- description shows canonical ids with bounded configured titles when present.
156
-
157
- Clauses A3–A5 belonged to shortcut dispatch and are retired. Clients and stored
158
- programs should follow the [migration guide](./upgrading.md#0230-program-api-pruning).
144
+ **A2.** Connectors create no guest globals, so an id resembling a JavaScript
145
+ builtin, or one that would collide after sanitization, stays usable through its
146
+ canonical address. The bounded connector inventory in the tool description shows
147
+ canonical ids with bounded configured titles when present. Clauses A3–A5
148
+ belonged to shortcut dispatch and stay retired; nothing reuses those ids.
159
149
 
160
150
  ## The surface
161
151
 
162
- Four functions, all `async`: `search`, `describe`, `call`, and `emit`. Nothing else works: reading any other
163
- property yields a function — the guest namespace is a Proxy, so `typeof
164
- connecta.toString` is `"function"` — but *calling* it fails, because the host
165
- resolves only own members of the provider's `fns`. A program must treat the four
166
- documented functions as the whole surface.
152
+ Four functions, all `async`: `search`, `describe`, `call`, and `emit`. Nothing
153
+ else works. Reading any other property yields a function — the guest namespace
154
+ is a Proxy, so `typeof connecta.toString` is `"function"` — but *calling* it
155
+ fails, because the host resolves only own members of the provider's `fns`.
167
156
 
168
157
  ### connecta.search
169
158
 
@@ -180,49 +169,104 @@ const page = await connecta.search({
180
169
  });
181
170
  ```
182
171
 
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.
184
-
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
-
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
188
- rank; search distinct operations separately and use `outputKeys`, not guessed roots. A non-object schema — a union, an array, an
189
- unresolvable `$ref` carries no lists rather than empty ones, because absent
190
- means "read the schema" where `[]` would claim the tool takes no fields. The
191
- lists come from the same walk that renders the compact schema, so a top-level
192
- `$ref` resolves and an `allOf` composes rather than reporting an empty list
193
- beside a schema that plainly shows fields. A zero-input object keeps `inputKeys:
194
- []` and `requiredInputKeys: []`; an output object with no declared properties
195
- omits `outputKeys` because it declares no useful inventory. A
196
- truncated schema omits the corresponding key list rather than repeating a
197
- large partial inventory. `search_tools`
198
- carries the same metadata whenever schemas are requested. Code-mode callers
199
- can set `includeSchemaKeys: false` to buy the bytes back.
200
-
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
205
- `details` fields (`E1`).
172
+ **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus
173
+ `nextOffset` when more remains and `matchMode: "partial"` when no tool matched
174
+ every term. Top-level `search_tools` is different it returns
175
+ `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Ranking is
176
+ [lexical discovery](./meta-tools.md#lexical-discovery)'s, and rows expose
177
+ neither lexical scores nor per-result coverage. An empty or whitespace-only
178
+ query browses; non-empty input with no ASCII lexical terms returns no tools plus
179
+ bounded no-match analysis in `queryAnalysis`; mixed input searches with its
180
+ ASCII terms.
181
+
182
+ Each entry carries `address`, `name`, the configured `connectorTitle` when
183
+ present (normalized whitespace, at most 120 UTF-8 bytes), and when requested —
184
+ `description`, `inputSchema`, `outputSchema`, and `annotations`. An entry whose
185
+ connector carries a usage guide also carries `guide` (the skill name) and a
186
+ bounded `guideSummary`, requested or not. An output shape learned under `S9`
187
+ also carries `outputSchemaSource: "observed"`; provider declarations carry no
188
+ source marker.
189
+
190
+ Compact shapes omit property prose and put required fields first, under these
191
+ bounds:
192
+
193
+ | Bound | Value |
194
+ | --- | --- |
195
+ | Rendered shape | 1,024 UTF-8 bytes each |
196
+ | Work per shape | 2,000 visits across schema nodes, property and required names, literal values, any constraint-free retry, and its key-only fallback |
197
+ | Each enum node | 256 of those bytes |
198
+ | Resolved `$ref` text | reused within one walk |
199
+
200
+ About three near-cap enum nodes therefore coexist while the final quarter stays
201
+ for surrounding syntax, and above 1,024 bytes the global fallback applies.
202
+ Exhausted work yields `unknown /* truncated */`; a capped enum preserves whole
203
+ values before `unknown` plus an exact omitted-value count; an empty enum renders
204
+ as `never` and small enums stay complete. Either cap sets `inputSchemaTruncated`
205
+ or `outputSchemaTruncated`, and a shape-wide cap stays structurally valid with
206
+ `unknown` types plus `/* truncated */`. Compound forms share those bounds:
207
+ `prefixItems` renders as a tuple with the declared `items` rest, an `unknown[]`
208
+ rest when open, or no rest when `items` is false; `dependentSchemas` and
209
+ `if`/`then`/`else` keep the base shape plus `/* conditional */` and set the
210
+ truncation flag; `$dynamicRef` resolves a same-named definition like `$ref`, and
211
+ an unresolved one becomes `unknown` with the flag. For omitted exact constraints
212
+ use `format: "json"` or JSON search.
213
+
214
+ **S1a.** `connector` loads only the named catalog; omit it only when the
215
+ integration is ambiguous, because an unscoped search fans out across every
216
+ configured connector. `safety: "readOnly"` returns exactly the tools available
217
+ through `connecta.call`; `"approvalRequired"` returns the complementary
218
+ fail-closed class, including false, missing, and contradictory annotations;
219
+ omitted or `"all"` preserves the complete catalog. These filters grant no
220
+ authority and change no admission decision.
221
+
222
+ **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`
223
+ (declared properties only), and `outputKeys`: the names the rendered schema
224
+ shows, ready to check before building arguments. Select on inputs, truncation,
225
+ safety, and outputs rather than lexical rank, and use `outputKeys` instead of
226
+ guessed roots. A non-object schema — a union, an array, an unresolvable `$ref` —
227
+ carries no lists rather than empty ones, because absent means "read the schema"
228
+ where `[]` would claim the tool takes no fields. The lists come from the walk
229
+ that renders the compact schema, so a top-level `$ref` resolves and an `allOf`
230
+ composes. A zero-input object keeps `inputKeys: []` and `requiredInputKeys: []`;
231
+ an output object with no declared properties omits `outputKeys`; a truncated
232
+ schema omits the corresponding list. `search_tools` carries the same metadata
233
+ whenever schemas are requested, and `includeSchemaKeys: false` buys the bytes
234
+ back.
235
+
236
+ **S3.** Discovery bounds throw rather than silently shrink. `invalid_args`
237
+ covers a `limit` outside 1–100, an `offset` that is not a non-negative integer,
238
+ a supplied `query` that is not a string, and a `connector` over 512 UTF-8 bytes
239
+ — refused whole, since a clipped scope names a different connector. Omitted
240
+ `offset` starts at 0; omitted `query` browses. A page whose serialized form
241
+ exceeds 256,000 bytes is `result_too_large`; a program's page is measured as its
242
+ serialized value, while top-level `search_tools` measures the complete tool
243
+ result including both copies and JSON escaping. Each error carries a correction
244
+ hint and the stable `code`, `retryable`, and `details` fields (`E1`).
206
245
 
207
246
  ### connecta.describe
208
247
 
209
248
  ```js
210
249
  const one = await connecta.describe({ address: "ci.get_run" });
211
-
212
- const { tools } = await connecta.describe({
250
+ const many = await connecta.describe({
213
251
  addresses: ["ci.get_run", "ci.get_job_logs"], // ≤ 100
214
252
  format: "compact", // or "json"
215
253
  fullDescriptions: false,
216
254
  });
217
255
  ```
218
256
 
219
- **S4.** Returns `{ tools }` in order, one entry per address. An unknown address
220
- or failed catalog returns `error` plus typed `errorDetails`: `code`, `message`, and `retryable`. Misses
221
- carry a route-aware `nextAction`; a close miss may add three canonical `suggestions`.
222
- Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
223
- caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
224
- 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.
257
+ **S4.** Returns `{ tools }` in order, one entry per address; one bad address
258
+ never fails the whole call. An unknown address or failed catalog returns `error`
259
+ plus typed `errorDetails` (`code`, `message`, `retryable`). Misses carry a
260
+ route-aware `nextAction`, a close miss may add three canonical `suggestions`,
261
+ and catalog failures add only `retryAfterMs` when known. Each failed entry
262
+ clamps its caller-authored `address` to 512 UTF-8 bytes with an `…` marker, and
263
+ entry order correlates a clipped address with its request; successes keep
264
+ canonical addresses. More than 100 addresses is `invalid_args`, and the same
265
+ 256,000-byte ceiling applies. Compact describe keeps property prose within a
266
+ separate 8,192-byte UTF-8 shape cap and shares search's 2,000-visit budget; a
267
+ capped shape sets `inputSchemaTruncated` or `outputSchemaTruncated`, and
268
+ `format: "json"` gives the exact schema. A success whose output shape came from
269
+ `S9` carries `outputSchemaSource: "observed"`.
226
270
 
227
271
  ### connecta.call
228
272
 
@@ -236,12 +280,11 @@ when present, otherwise text content JSON-parsed when it parses and the raw text
236
280
  when it does not; a downstream result flagged `isError` throws. Omitted `args`
237
281
  is treated as `{}`.
238
282
 
239
- **S6.** Every call goes through the same catalog,
240
- fail-closed read-only predicate, admission, credential containment, timeout
241
- classification, health accounting, and activity recording as an ordinary
242
- meta-tool call. The sandbox is an additional containment layer, not a second
243
- implementation of the boundary, and nothing a program does widens what it can
244
- reach.
283
+ **S6.** Every call goes through the same catalog, fail-closed read-only
284
+ predicate, admission, credential containment, timeout classification, health
285
+ accounting, and activity recording as an ordinary meta-tool call. The sandbox is
286
+ an additional containment layer, not a second implementation of the boundary,
287
+ and nothing a program does widens what it can reach.
245
288
 
246
289
  ### Parallel calls
247
290
 
@@ -250,21 +293,28 @@ program, or `Promise.allSettled` to retain every outcome in input order. Both
250
293
  use the same per-call admission, host-call budget, deadlines, and activity path
251
294
  as sequential calls. There is no separate batch size or result contract.
252
295
 
253
- ```js
254
- const outcomes = await Promise.allSettled([
255
- connecta.call("ci.get_run", { runId: 42 }),
256
- connecta.call("ci.list_jobs", { runId: 42 }),
257
- ]);
258
- return outcomes.map((outcome) => outcome.status === "fulfilled"
259
- ? { ok: true, data: outcome.value }
260
- : { ok: false, code: outcome.reason.code, message: outcome.reason.message });
261
- ```
262
-
263
296
  **S8.** A rejected promise retains the caught error's `code`, `retryable`, and
264
- `details`. Project those fields before returning; an Error object itself is
265
- not a JSON result contract.
266
-
267
- **S9.** A successful explicitly read-only call whose provider declared no `outputSchema` passively learns one from the unwrapped result. The observation retains field names and broad JSON types only: no arguments, scalar values, raw results, code, credentials, or errors. Property names may be user-authored. Objects stay open, every field stays optional, and search or describe labels the shape `outputSchemaSource: "observed"` so a model cannot mistake runtime evidence for a provider contract. Later observations merge fields and types in a process-local 256-entry LRU; a provider declaration always wins. Inference stops at depth 6, 128 schema nodes, 48 properties per object, 32 inspected array items, and 128 UTF-8 bytes per property name; `__proto__`, `constructor`, and `prototype` names are discarded. A tool definition over 64 KiB or an observed schema over 16 KiB is ignored. An entry expires after 24 hours and carries the exact serialized tool definition, so a changed catalog entry, process restart, or Worker isolate eviction starts cold. A failed call or failed result-processing step learns nothing, and any observation failure is discarded without changing a successful call. No discovery read, timer, refresh, background job, or storage adapter executes or persists work for this cache: the result-sampling refusal in [#282](https://github.com/zackbart/connecta/issues/282) stands.
297
+ `details`. Project those fields before returning an `Error` object is not a
298
+ JSON result contract.
299
+
300
+ **S9.** A successful explicitly read-only call whose provider declared no
301
+ `outputSchema` passively learns one from the unwrapped result. The observation
302
+ keeps field names and broad JSON types only — no arguments, scalar values, raw
303
+ results, code, credentials, or errors — and property names may be user-authored.
304
+ Objects stay open, every field stays optional, and discovery labels the shape
305
+ `outputSchemaSource: "observed"` so a model cannot mistake runtime evidence for
306
+ a provider contract. Later observations merge fields and types in a
307
+ process-local 256-entry LRU; a provider declaration always wins. Inference stops
308
+ at depth 6, 128 schema nodes, 48 properties per object, 32 inspected array
309
+ items, and 128 UTF-8 bytes per property name; `__proto__`, `constructor`, and
310
+ `prototype` names are discarded. A tool definition over 64 KiB or an observed
311
+ schema over 16 KiB is ignored. An entry expires after 24 hours and carries the
312
+ exact serialized tool definition, so a changed catalog entry, process restart,
313
+ or isolate eviction starts cold. A failed call or failed result-processing step
314
+ learns nothing, and any observation failure is discarded without changing a
315
+ successful call. No discovery read, timer, refresh, background job, or storage
316
+ adapter executes or persists work for this cache: the result-sampling refusal in
317
+ [#282](https://github.com/zackbart/connecta/issues/282) stands.
268
318
 
269
319
  ### connecta.emit
270
320
 
@@ -277,7 +327,8 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
277
327
 
278
328
  ## Errors
279
329
 
280
- **E1.** There are three error channels. Connecta failures are typed whether caught or uncaught.
330
+ **E1.** There are three error channels. Connecta failures are typed whether
331
+ caught or uncaught.
281
332
 
282
333
  | Channel | Shape | Typed? |
283
334
  | --- | --- | --- |
@@ -285,11 +336,24 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
285
336
  | An uncaught **tool or discovery** failure, as the model sees it | `{ error: { code, message, retryable, … } }` with `isError` | yes |
286
337
  | Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
287
338
 
288
- 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.
290
- Program-authored errors stay untyped, and code must never parse error prose.
291
-
292
- **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program may do.
339
+ Both executor bridges reduce a rejected host call to `new Error(message)`, and
340
+ connecta restores the typed failure in a trusted prelude with a per-execution
341
+ authenticated frame (`X11`). `message` stays human text, capped at 2,000
342
+ JSON-serialized characters including quotes and an `…` marker when clipped.
343
+ `code` and `retryable` are the stable branch fields; `details` carries the host
344
+ classification and fits 3,700 serialized characters. Optional recovery metadata
345
+ that would exceed that bound is omitted whole, preserving `code`, `message`,
346
+ `retryable`, and `retryAfterMs`, because a clipped recovery address or argument
347
+ describes a different call. This covers `call`, `search`, `describe`, `emit`, and
348
+ the host-call budget. Program-authored errors stay untyped, and code must never
349
+ parse error prose. An `unavailable` classification may add `details.host`, an
350
+ HTTP(S) origin of at most 253 UTF-8 bytes, and `details.code`, a validated
351
+ network errno, undici transport code, or `timeout` of at most 32 bytes; neither
352
+ enters activity.
353
+
354
+ **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program
355
+ may do. A provider maps each downstream failure to the code that tells the
356
+ caller what to do next, and never invents a cause it was not told.
293
357
 
294
358
  | Code | Raised when | `retryable` |
295
359
  | --- | --- | --- |
@@ -298,10 +362,10 @@ Program-authored errors stay untyped, and code must never parse error prose.
298
362
  | `destructive_tool_requires_approval` | the tool is not explicitly read-only | false |
299
363
  | `auth_required` | the credential is missing, expired, or rejected | false |
300
364
  | `invalid_args` | arguments or discovery bounds were rejected | false |
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 |
365
+ | `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 | false |
302
366
  | `input_required_unsupported` | a downstream asked for mid-call input | false |
303
367
  | `rate_limited` | the downstream reported a rate limit | true |
304
- | `unavailable` | the downstream is down or unreachable | true |
368
+ | `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
369
  | `timeout` | the per-call deadline (`execute.hostCallTimeoutMs`, default 15 s) expired | true |
306
370
  | `cancelled` | the run ended while this call was in flight (`E5`) | false |
307
371
  | `connector_call_failed` | anything else the connector threw | per message |
@@ -313,8 +377,8 @@ Program-authored errors stay untyped, and code must never parse error prose.
313
377
  **E3.** `auth_required` carries the same recovery envelope as `call_tool`:
314
378
  `connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
315
379
  `unavailable`), `nextAction` naming `authorize_connector`, and a `retry`
316
- sentence. A program cannot recover credentials — only an operator can — so the
317
- right move is to stop and let the failure reach the model.
380
+ sentence. A program cannot recover credentials — only an operator can — so stop
381
+ and let the failure reach the model.
318
382
 
319
383
  **E4.** An unannotated, write-capable, or destructive tool stays refused with
320
384
  `destructive_tool_requires_approval`; `nextAction` carries its canonical address
@@ -326,34 +390,55 @@ never goes downstream, and generated code cannot mint the capability.
326
390
  **E5.** Failures of the *execution*, not of a call, never appear inside the
327
391
  guest: admission rejection (`executor_overloaded`, retryable, with
328
392
  `retryAfterMs`), cancellation (`executor_cancelled`), shutdown
329
- (`executor_closed`), deadline expiry, and sandbox crashes end the run and are
330
- reported to the model as an error result. One seam: a host call still in flight
331
- when the run is cancelled fails with `cancelled`, catchable on the way out but
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.
393
+ (`executor_closed`), deadline expiry, and sandbox crashes end the run and reach
394
+ the model as an error result. One seam: a host call still in flight when the run
395
+ is cancelled fails with `cancelled`, catchable on the way out but never worth
396
+ acting on (`Y3`). When shutdown tears down a program that had already started,
397
+ accepted blocks are reported as discarded under `M4`; a failure before execution
398
+ started carries no discard fields. A returned `error` field is a failure even
399
+ when empty — an empty string reports `executor_failed` with
400
+ `Error: Execution failed without an error message.`
334
401
 
335
402
  **E6.** An error the program raises itself — a `TypeError`, a call to a
336
- `connecta` member that is not a provider function (including an inherited one
337
- like `toString`), a `throw` of its own — ends the run with an error result
338
- carrying that message. It is not typed, because it is not a connector failure.
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
341
- message in its own text still reports the underlying typed failure. Keeping the
342
- type beats keeping the prose.
343
-
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
-
346
- **E8.** A remote MCP tool whose advertised schema rejects the call fails before provider dispatch with `invalid_args`, carrying bounded, value-free `{ path, code, expected }` findings and scoped search recovery keyed `function: "connecta.search"` like every other in-program miss. A declared property reports the schema keyword that failed, never the validator's duplicate `additionalProperties` branch; a truly undeclared property still reports `additionalProperties`. Unsupported schemas pass through; unrecognized provider prose remains `connector_call_failed`.
403
+ `connecta` member that is not a provider function (an inherited one like
404
+ `toString` included), a `throw` of its own — ends the run with an untyped error
405
+ result carrying that message. One precedence rule: connecta recognizes an
406
+ escaped tool failure by its message, exactly first and by containment second for
407
+ messages of at least eight characters, so a program that *wraps* a failure's
408
+ message still reports the underlying typed failure keeping the type beats
409
+ keeping the prose. Matching retains only the most recent 64 failures per
410
+ execution, caught refusals included; an older escaped message stays an untyped
411
+ execution failure. An empty terminal error uses the fixed message in `E5`.
412
+
413
+ **E7.** `retryable` for `unknown_address`, `unknown_tool`, and
414
+ `destructive_tool_requires_approval` is pinned false, never inferred from an
415
+ address containing `503`, `429`, or `temporar`. The first two carry
416
+ `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }`
417
+ — scoped discovery keyed to the surface the caller has, since a program cannot
418
+ call `search_tools`. The message, the derived `query`, and a failed describe
419
+ entry's `address` clamp caller-authored text to 512 UTF-8 bytes with an `…`
420
+ marker, because those values land in both the text content and
421
+ `structuredContent`, where an invented 50 KB address would produce a refusal far
422
+ past the deployment's result cap. A clipped address still identifies the mistake
423
+ by its position; a short one is exact and untagged.
424
+
425
+ **E8.** A remote MCP tool whose advertised schema rejects the call fails before
426
+ provider dispatch with `invalid_args`, carrying bounded, value-free
427
+ `{ path, code, expected }` findings and the same scoped
428
+ `function: "connecta.search"` recovery every other in-program miss gets. A
429
+ declared property reports the schema keyword that failed, never the validator's
430
+ duplicate `additionalProperties` branch; a truly undeclared property still
431
+ reports `additionalProperties`. Unsupported schemas pass through; unrecognized
432
+ provider prose remains `connector_call_failed`.
347
433
 
348
434
  ## Results and projection
349
435
 
350
436
  **R1 (verdict: projection stays explicit).** A program's return value reaches
351
437
  the model unchanged except for the size guard in `R2`. Connecta does not
352
438
  summarize, reshape, or field-select it, and there is no automatic projection
353
- mode. The 93%-byte win the exploration measured came from *program-authored*
354
- projection; a host heuristic would silently drop fields a program deliberately
355
- returned and would be invisible in the transcript. Host-side projection helpers
356
- earn their way in only if [#222](https://github.com/zackbart/connecta/issues/222)
439
+ mode: the measured byte win came from *program-authored* projection, while a
440
+ host heuristic would drop deliberately returned fields invisibly. Helpers earn
441
+ their way in only if [#222](https://github.com/zackbart/connecta/issues/222)
357
442
  shows programs failing to project on their own.
358
443
 
359
444
  **R2.** The boundary is 24,000 serialized characters (~6k tokens). A value over
@@ -373,30 +458,45 @@ size of what the program returned and truncation happens exactly once no matter
373
458
  how many hops the value takes.
374
459
 
375
460
  **R3.** Truncation is a *successful* result, not an error: the program ran, and
376
- what came back is the honest report that its answer was too large. The fix is a
377
- program that returns less, which is why the envelope says so.
461
+ the honest report is that its answer was too large. The fix is a program that
462
+ returns less, which is why the envelope says so.
378
463
 
379
464
  **R4 (verdict: no result paging for programs).** A truncated program result
380
- carries no `get_result` handle, unlike `call_tool`. `get_result` exists so a model
381
- can page a *downstream payload* it could not shrink; a program can shrink
382
- anything, so paging its result would reward the one behavior code mode exists to
383
- remove — and stashing every unprojected return value would spend the result store
384
- on data nobody asked for.
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`).
387
-
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
-
390
- **R7.** Timing separates admission, provider setup, total executor wall time, catalog work, and connector work. Catalog and connector values are cumulative, so parallel work can exceed executor wall time. Each used operation kind (`search`, `describe`, `call`) gets one aggregate with count, failures, duration, returned serialized bytes, and catalog/connector time.
391
-
392
- **R8.** Diagnostics contain measurements and fixed operation names only: no addresses, arguments, results, code, credentials, logs, or raw errors. Result sizes are numbers, never previews. The collector exists only for the opted-in request; it is not activity, a session, or a stream.
465
+ carries no `get_result` handle, unlike `call_tool`. `get_result` exists so a
466
+ model can page a *downstream payload* it could not shrink; a program can shrink
467
+ anything, so paging its result would reward the behavior code mode exists to
468
+ remove.
469
+
470
+ **R5.** `console.log`, `console.warn`, and `console.error` are captured in call
471
+ order and returned as one `logs` string, capped at 4,000 characters with a
472
+ truncation marker. Logs survive program failure through either a returned error
473
+ result or a thrown error carrying `logs: string[]`. QuickJS streams captured
474
+ entries to its parent and preserves the received prefix on cancellation,
475
+ shutdown, deadline termination, child crashes, and IPC failures (`X4`). How a
476
+ non-string argument renders is not contract (`X4`).
477
+
478
+ **R6.** Nothing else is added to a normal program result. `diagnostics: true`
479
+ adds one request-local, payload-free `diagnostics` block; a program that emitted
480
+ adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are
481
+ byte-for-byte the ordinary response path
482
+ ([#247](https://github.com/zackbart/connecta/issues/247)).
483
+
484
+ **R7.** Diagnostic timing separates admission, provider setup, total executor
485
+ wall time, catalog work, and connector work. Catalog and connector values are
486
+ cumulative, so parallel work can exceed executor wall time. Each used operation
487
+ kind (`search`, `describe`, `call`) gets one aggregate with count, failures,
488
+ duration, returned serialized bytes, and catalog/connector time.
489
+
490
+ **R8.** Diagnostics contain measurements and fixed operation names only: no
491
+ addresses, arguments, results, code, credentials, logs, or raw errors. Result
492
+ sizes are numbers, never previews. The collector exists only for the opted-in
493
+ request; it is not activity, a session, or a stream.
393
494
 
394
495
  ## Emitted output
395
496
 
396
497
  MCP-native output a return value cannot carry: base64 is not projectable, so a
397
- block that survives intake uncapped (`S5`) must not die at the `R2` exit
398
- guard. The argument and the refused alternatives live in the
399
- [design record](https://github.com/zackbart/connecta/blob/main/records/rich-output-design.md) and `ethos.md`
498
+ block that survives intake uncapped (`S5`) must not die at the `R2` exit guard.
499
+ The refused alternatives are in `ethos.md`
400
500
  ([#267](https://github.com/zackbart/connecta/issues/267),
401
501
  [#270](https://github.com/zackbart/connecta/issues/270)).
402
502
 
@@ -404,18 +504,18 @@ guard. The argument and the refused alternatives live in the
404
504
  text }` or `{ type: "image" | "audio", data /* base64 */, mimeType }`, every
405
505
  field a string, no extra fields, no `annotations`, no `_meta`, no sugar forms.
406
506
  An invalid block throws catchably and nothing is accepted — rejected, not
407
- stripped. The refused types are pointers: a guest-minted `resource_link` URI
408
- is a lure a client may dereference.
507
+ stripped. The refused types are pointers: a guest-minted `resource_link` URI is
508
+ a lure a client may dereference.
409
509
 
410
- **M2.** Blocks collect on the host in emission order and are delivered only
411
- with a successful result, appended to `content` after the JSON envelope, which
412
- gains `emitted: N`. A program that never emits produces the byte-for-byte
413
- ordinary response (`R6`). `structuredContent` stays the envelope alone —
414
- emission is presentation, not a second data channel.
510
+ **M2.** Blocks collect on the host in emission order and are delivered only with
511
+ a successful result, appended to `content` after the JSON envelope, which gains
512
+ `emitted: N`. A program that never emits produces the byte-for-byte ordinary
513
+ response (`R6`). `structuredContent` stays the envelope alone — emission is
514
+ presentation, not a second data channel.
415
515
 
416
516
  **M3.** Return value and emission are independent: `R2` never measures emitted
417
- bytes, a truncated return does not suppress delivered blocks, and blocks do
418
- not shrink the return budget.
517
+ bytes, a truncated return does not suppress delivered blocks, and blocks do not
518
+ shrink the return budget.
419
519
 
420
520
  **M4.** A failed program delivers no blocks. The error result reports
421
521
  `emittedDiscarded: N` when N > 0 — a field on the structured envelope, a
@@ -423,52 +523,55 @@ trailing line on the plain-text paths — never silently.
423
523
 
424
524
  **M5.** Two budgets (`ConnectaConfig.execute.maxEmittedBytes` /
425
525
  `.maxEmittedBlocks`, defaults 4,000,000 serialized bytes and 32 blocks) fail
426
- loudly at the `emit` call, naming the budget and the room remaining; nothing
427
- is partially accepted and prior blocks stand. No `get_result` stash: the
428
- program learns while it can still choose differently. The byte default is a
429
- transport bound, not a context bound — emitted media reaches the model as
430
- media, not base64 text.
526
+ loudly at the `emit` call, naming the budget and the room remaining; nothing is
527
+ partially accepted and prior blocks stand. No `get_result` stash: the program
528
+ learns while it can still choose differently. The byte default is a transport
529
+ bound, not a context bound — emitted media reaches the model as media, not
530
+ base64 text.
431
531
 
432
- **M6.** No provenance is claimed: every emitted block is program output,
433
- trusted exactly as much as the return value. Preservation is re-emission of
434
- the raw downstream block, so `S5`'s uncapped fallthrough is contract.
532
+ **M6.** No provenance is claimed: every emitted block is program output, trusted
533
+ exactly as much as the return value. Preservation is re-emission of the raw
534
+ downstream block, so `S5`'s uncapped fallthrough is contract.
435
535
 
436
- **M7.** `emit` spends no host-call budget (`L4`); `M5`'s bounds are its only
437
- bounds.
536
+ **M7.** `emit` alone spends no host-call budget (`L4`); `search`, `describe`,
537
+ and `call` share it. `M5`'s bounds are emission's only bounds.
438
538
 
439
539
  **M8.** Emission asks nothing of an executor: `emit` is a provider function,
440
540
  blocks cross the guest boundary once as an argument, and `ExecuteResult` is
441
541
  unchanged — `Executor` stays assignable from `@cloudflare/codemode`'s
442
- `DynamicWorkerExecutor`, and any executor that bridges provider calls gets
542
+ `DynamicWorkerExecutor`, so any executor that bridges provider calls gets
443
543
  emission for free.
444
544
 
445
545
  **M9.** Request-local and unstreamed: blocks exist only in the finished
446
546
  response, and `emit` resolving means "accepted," never "delivered."
447
547
 
448
548
  **M10.** Activity stays payload-free. `diagnostics: true` adds one `emitted`
449
- aggregate — count and serialized bytes, numbers only (`R8`), present only
450
- when something was emitted.
549
+ aggregate — count and serialized bytes, numbers only (`R8`), present only when
550
+ something was emitted.
451
551
 
452
552
  ## Retry semantics
453
553
 
454
554
  **Y1.** Connecta makes one downstream attempt per admitted call, both inside a
455
555
  program and through either direct-call tool. It never waits and retries on the
456
- caller's behalf. An admission refusal may prevent even that attempt.
556
+ caller's behalf, and an admission refusal may prevent even that attempt.
457
557
 
458
- **Y2.** A program may retry a caught failure whose `retryable` is true, or a rejected promise whose `reason.retryable` is true (`S8`). Every attempt spends host-call budget, so an unchecked loop converts a transient failure into `budget_exceeded`.
558
+ **Y2.** A program may retry a caught failure whose `retryable` is true, or a
559
+ rejected promise whose `reason.retryable` is true (`S8`). Every attempt spends
560
+ host-call budget, so an unchecked loop converts a transient failure into
561
+ `budget_exceeded`.
459
562
 
460
563
  **Y3.** What must never be retried automatically:
461
564
 
462
565
  - anything with `retryable: false` — a policy refusal, a missing credential, a
463
566
  bad address, or malformed arguments will fail identically forever;
464
567
  - `rate_limited`, immediately. A portable program has no timer, and a
465
- Dynamic-Worker-only wait would spend the run's wall-clock budget on code that
466
- fails on QuickJS. Return the failure and let the model, which can wait,
467
- re-issue with `retryAfterMs` in hand.
568
+ Dynamic-Worker-only wait would spend the run's wall clock on code that fails
569
+ on QuickJS. Return the failure and let the model, which can wait, re-issue
570
+ with `retryAfterMs` in hand.
468
571
  - a cancelled or timed-out *execution*: it is already over (`L1`).
469
572
 
470
- **Y4.** A provider's `retryAfterMs` is returned unchanged. The caller decides
471
- whether and when to reissue. A later call receives its own deadline and
573
+ **Y4.** A provider's `retryAfterMs` is returned unchanged; the caller decides
574
+ whether and when to reissue, and a later call gets its own deadline and
472
575
  admission decision.
473
576
 
474
577
  ## Cancellation and limits
@@ -478,12 +581,12 @@ poll, no cancellation exception to catch, and no guarantee that a `finally`
478
581
  block runs — a cancelled QuickJS child is terminated outright. Write programs
479
582
  that need no cleanup.
480
583
 
481
- **L2.** What cancellation guarantees: in-flight host calls abort, no further host
482
- call is admitted, the admission lease is released, and nothing request-bound
483
- survives the request.
584
+ **L2.** What cancellation guarantees: in-flight host calls abort, no further
585
+ host call is admitted, the admission lease is released, and nothing
586
+ request-bound survives the request.
484
587
 
485
- **L3.** Every execution runs under a wall-clock deadline that includes time spent
486
- waiting on host calls. Expiry ends the run with an execution error and no
588
+ **L3.** Every execution runs under a wall-clock deadline that includes time
589
+ spent waiting on host calls. Expiry ends the run with an execution error and no
487
590
  partial result; the deadline's length is executor configuration (`X1`).
488
591
 
489
592
  **L4.** Per-execution bounds that are contract, identical in both executors
@@ -491,8 +594,8 @@ because connecta enforces them above the sandbox:
491
594
 
492
595
  | Bound | Value |
493
596
  | --- | --- |
494
- | Host calls per execution | 20 |
495
- | Deadline per host call | 15 s, `execute.hostCallTimeoutMs` |
597
+ | Host calls per execution, shared by `search`, `describe`, and `call` | 20 by default, `execute.maxHostCalls` |
598
+ | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
496
599
  | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
497
600
  | `describe` addresses | ≤ 100 |
498
601
  | `describe` nearby suggestions | ≤ 3 canonical addresses per failed entry |
@@ -500,7 +603,13 @@ because connecta enforces them above the sandbox:
500
603
  | Result | 24,000 serialized characters |
501
604
  | Logs presented to the model | 4,000 characters |
502
605
 
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.
606
+ Every `call` attempt spends one host call on entry, before address resolution,
607
+ catalog lookup, safety checks, validation, or dispatch, so unknown addresses,
608
+ unknown tools, catalog failures, and other pre-dispatch refusals cost what a
609
+ successful call costs; catching a refusal does not refund it. `search` and
610
+ `describe` likewise spend on entry. Exhausting the budget fails that call with
611
+ non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No
612
+ connector is reached, and the budget does not refill inside one execution.
504
613
 
505
614
  **L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
506
615
  exhausts a bound ends the run with an error instead of degrading the host. The
@@ -513,30 +622,33 @@ code safe to run at all.
513
622
  **L6.** A host call's serialized arguments and its serialized result are each
514
623
  bounded — QuickJS caps both at 256 KiB (`X10`) — and exceeding either fails that
515
624
  call, not the execution, so a program can catch it and ask for less. The failure
516
- is executor-owned untyped text, not a Connecta host failure (`E1`). An over-bound *result* names the address the program
517
- called, rather than only the generic bridge function; an over-bound
518
- *argument* payload is refused before it is parsed, so it names no address at
519
- all parsing it to write a better message would spend exactly the work the bound
520
- exists to refuse.
521
-
522
- **L7.** Executions are admitted, not queued indefinitely: bounded concurrency plus
523
- a bounded queue with a wait timeout. Overload is a retryable `executor_overloaded`
524
- carrying `retryAfterMs`; cancellation and shutdown are terminal. Admission happens
525
- *before* any catalog or provider is built, so a queued request holds no state.
526
-
527
- **L8.** Bounds are deployment configuration, not program inputs: a program cannot
528
- raise one by asking. `execute_code`'s description states the host-call budget and the per-call deadline — the ones a program must plan around
529
- before it runs. The result and log caps live here and in the truncation notice
530
- itself (`R2`, `R5`).
625
+ is executor-owned untyped text, not a Connecta host failure (`E1`). An
626
+ over-bound *result* names the address the program called rather than only the
627
+ generic bridge function; an over-bound *argument* payload is refused before it
628
+ is parsed, so it names no address at all.
629
+
630
+ **L7.** Executions are admitted, not queued indefinitely: bounded concurrency
631
+ plus a bounded queue with a wait timeout. Overload is a retryable
632
+ `executor_overloaded` carrying `retryAfterMs`; cancellation and shutdown are
633
+ terminal. Admission happens *before* any catalog or provider is built, so a
634
+ queued request holds no state.
635
+
636
+ **L8.** Bounds are deployment configuration, not program inputs: a program
637
+ cannot raise one by asking. `execute_code`'s description states the host-call
638
+ budget and the per-call deadline the ones a program must plan around before it
639
+ runs. The result and log caps live here and in the truncation notice itself
640
+ (`R2`, `R5`).
531
641
 
532
642
  ## Activity
533
643
 
534
644
  **V1.** One payload-free activity event per attempted call, with
535
645
  `source: "execute_code"` — every dispatched call plus every local refusal: a
536
- read-only refusal, an unknown tool, an unloadable
537
- catalog, a missing credential, an exhausted host-call budget, an address no
538
- connector owns. Ten tools called is ten events, as legible as ten `call_tool`
539
- calls which makes moving work into the sandbox an optimization, not a blindfold.
646
+ read-only refusal, an unknown tool, an unloadable catalog, a missing credential,
647
+ an address no connector owns. Ten tools called is ten events, as legible as ten
648
+ `call_tool` calls, which makes moving work into the sandbox an optimization
649
+ rather than a blindfold. An exhausted host-call budget is the exception: `L4`
650
+ refuses that attempt above the invocation path, so it is charged but records no
651
+ event.
540
652
 
541
653
  **V2.** Each event carries `connectorId`, `toolName`, `address`, `source`,
542
654
  `outcome` (`success`, `error`, `timeout`, `cancelled`), `durationMs`,
@@ -544,25 +656,23 @@ calls — which makes moving work into the sandbox an optimization, not a blindf
544
656
  server identity. Typed codes derive an optional `friction`: `tool_not_found`,
545
657
  `schema_retry`, `destructive_reroute`, or `auth_required`. The fifth class,
546
658
  `result_too_large`, cannot reach an `execute_code` event: it belongs to a
547
- `call_tool` result too large to return inline, and a program's own
548
- return is refused paging by design rather than truncated into friction. There is
549
- nowhere to put arguments, results, program source, or
550
- raw error text; a caught failure is still recorded. `address` is
551
- canonical (`A1`) where a tool resolved, otherwise the name the program used
552
- the honest record of what was attempted.
659
+ `call_tool` result too large to return inline, and a program's own return is
660
+ refused paging by design rather than truncated into friction. There is nowhere
661
+ to put arguments, results, program source, or raw error text; a caught failure
662
+ is still recorded. `address` is canonical (`A1`) where a tool resolved,
663
+ otherwise the name the program used the honest record of what was attempted.
553
664
 
554
665
  **V3.** A call whose connector does not exist is recorded at the address as
555
666
  written, *provided* it split into the two fields activity keeps — one with no
556
667
  interior dot records nothing. An invented id is the address mistake an operator
557
- most needs to see. But recording it as written puts caller-authored text in
668
+ most needs to see, but recording it as written puts caller-authored text in
558
669
  fields that are otherwise operator- and connector-authored, so `connectorId`
559
670
  and `toolName` clamp at 128 UTF-8 bytes (`address` at 257) with a `…` marker:
560
- payload-free *by construction* means the event has nowhere to put a payload,
561
- not merely that connecta declines to.
671
+ payload-free *by construction* means the event has nowhere to put a payload.
562
672
 
563
673
  **V4.** The execution itself emits no event. It has no address, and its one
564
- distinctive artifact is the program source, which is exactly what a payload-free
565
- history must never keep.
674
+ distinctive artifact is the program source exactly what a payload-free history
675
+ must never keep.
566
676
 
567
677
  ## Executor exceptions
568
678
 
@@ -578,30 +688,50 @@ and guest-CPU limits (`L5`); the Dynamic Worker has no such knobs, so workerd's
578
688
  isolate limits apply untuned. A specific heap ceiling is a Node-only option.
579
689
 
580
690
  **X3. Mid-flight cancellation.** The QuickJS pool receives the request's
581
- `AbortSignal` and kills the child. The Dynamic Worker executor's `execute()` takes
582
- no signal, so a cancelled request's program runs on until its host calls fail or
583
- the deadline expires. `L2` holds either way — the calls abort, the response does
584
- not wait — but "the run ends" is best-effort on Workers.
585
-
586
- **X4. Log rendering and capture.** QuickJS JSON-stringifies non-string
587
- arguments and captures `log`, `info`, `warn`, `error`, and `debug`; the Dynamic
588
- Worker renders arguments with `String()` (so an object logs as
589
- `[object Object]`) and captures only `log`, `warn`, and `error`, prefixing the
590
- latter two. Only the three captured everywhere are contract (`R5`); rendering is
591
- not.
592
-
593
- **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
- 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.
595
- `P2` is the portable contract. Programs use none of this runtime-only authority, including timers and `crypto`, because the same code fails on QuickJS. The `execute_code` description and served `usage` skill say so before an agent writes code.
691
+ `AbortSignal` and kills the child. The Dynamic Worker executor's `execute()`
692
+ takes no signal, so a cancelled request's program runs on until its host calls
693
+ fail or the deadline expires. `L2` holds either way — the calls abort, the
694
+ response does not wait — but "the run ends" is best-effort on Workers.
695
+
696
+ **X4. Log rendering and capture.** QuickJS JSON-stringifies non-string arguments
697
+ and captures `log`, `info`, `warn`, `error`, and `debug`; the Dynamic Worker
698
+ renders arguments with `String()` (so an object logs as `[object Object]`) and
699
+ captures only `log`, `warn`, and `error`, prefixing the latter two. Only the
700
+ three captured everywhere are contract (`R5`); rendering is not. QuickJS streams
701
+ each accepted entry within the existing IPC envelope bound while retaining its
702
+ per-entry and cumulative child caps, and the parent keeps at most 4,001 joined
703
+ characters for failure recovery one beyond the presentation cap, so truncation
704
+ stays visible. On a normal reply the child's complete log array wins and the two
705
+ copies are never joined; on termination or IPC failure the parent attaches its
706
+ retained prefix to the thrown error. Admission rejection before the program
707
+ starts has no guest logs to recover.
708
+
709
+ **X5. Leftover authority.** QuickJS blocks imports and has no `fetch`,
710
+ `process`, timers, `crypto`, or `WebSocket`, and its Node child starts with an
711
+ explicitly empty process environment rather than inheriting deployment variables
712
+ or `NODE_OPTIONS`. A Dynamic Worker has those globals plus a non-contract set of
713
+ runtime builtins through `import()` and `process.getBuiltinModule()`, including
714
+ `node:path`, `node:crypto`, `node:net`, `node:tls`, `node:dns`, `node:module`,
715
+ and `cloudflare:workers`; the upstream set drifts, so that is not an allowlist.
716
+ The supported construction is exactly `new DynamicWorkerExecutor({ loader })` —
717
+ `bindings`, `modules`, and `globalOutbound` each grant ambient configuration,
718
+ code, or egress. Under it, `process.env`, lexical `this.env`, and
719
+ `cloudflare:workers.env` are empty; `node:fs`, `node:http`, and `node:https` are
720
+ unavailable through either access route; external `fetch`, `WebSocket`,
721
+ `node:net`, and `node:tls` fail with workerd's outbound-denial error; DNS lookup
722
+ ends unresolved; and `fetch("data:...")` resolves locally. `P2` is the portable
723
+ contract: programs use none of this runtime-only authority, timers and `crypto`
724
+ included, because the same code fails on QuickJS, and the `execute_code`
725
+ description and served `usage` skill say so before an agent writes code.
596
726
 
597
727
  **X6. Stall detection.** QuickJS notices a program awaiting something that can
598
- never settle and fails fast; the Dynamic Worker waits for its deadline. The fast
599
- failure is better, but requiring it would require a host-driven job loop — not a
728
+ never settle and fails fast; the Dynamic Worker waits for its deadline.
729
+ Requiring the fast failure would require a host-driven job loop — not a
600
730
  reasonable demand on a platform sandbox.
601
731
 
602
732
  **X7. Value codec.** QuickJS is JSON-only; `@cloudflare/codemode` tunnels binary
603
- values through a tagged envelope, so a `Uint8Array` may survive there. `P3` is the
604
- contract: JSON-serializable values, or the program is Workers-only.
733
+ values through a tagged envelope, so a `Uint8Array` may survive there. `P3` is
734
+ the contract: JSON-serializable values, or the program is Workers-only.
605
735
 
606
736
  **X8. Unknown-property message.** An unknown `connecta` property throws
607
737
  `Unknown function connecta.x` on QuickJS and `Tool "x" not found` on the Dynamic
@@ -611,40 +741,38 @@ Worker. Both satisfy `E6`; the text is not contract.
611
741
  error when a program returns something its codec cannot carry. QuickJS converts
612
742
  lossily instead — a cyclic object comes back as the string `"[object Object]"`,
613
743
  because the guest-to-host dump happens before any serializer can object.
614
- Normalizing this would mean walking every returned value in the child for
615
- JSON-representability, spending real CPU on every program to improve the error
744
+ Normalizing that would spend real CPU on every program to improve the error
616
745
  message of a program that is already wrong. `P3` is the contract: neither
617
746
  behavior returns the value.
618
747
 
619
748
  **X10. Per-host-call payload bound.** `L6`'s 256 KiB ceiling on a host call's
620
749
  arguments and result is QuickJS's, enforced at its IPC boundary. The Dynamic
621
- Worker has no documented equivalent; Workers RPC limits apply and connecta does
622
- not add one, because the boundary there is an isolate-to-isolate call rather than
623
- a `process.send` with a hard ceiling. A program that returns a quarter-megabyte
624
- from one tool call therefore fails on Node and may succeed on Workers — reduce
625
- inside the program either way (`R1`).
626
-
627
- **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.
629
-
630
- ## Changes from earlier code mode
631
-
632
- MCP Apps rendering, connector shortcut globals, and `connecta.batch` are
633
- removed. Direct calls also lose automatic retries. The seven top-level tools,
634
- read-only boundary, JSON projection, and emitted media remain. See the
635
- [migration guide](./upgrading.md#0230-program-api-pruning).
750
+ Worker has no documented equivalent; Workers RPC limits apply and connecta adds
751
+ none, because that boundary is an isolate-to-isolate call rather than a
752
+ `process.send` with a hard ceiling. A program returning a quarter-megabyte from
753
+ one tool call therefore fails on Node and may succeed on Workers — reduce inside
754
+ the program either way (`R1`).
755
+
756
+ **X11. Typed host rejection.** Both executors rebuild Connecta's authenticated
757
+ host-failure frame as a thrown guest `Error` (`E1`). The per-run secret stays in
758
+ the trusted prelude closure and the prelude locks `globalThis.Error`, so guest
759
+ code and connector prose cannot forge the frame. The host bounds details before
760
+ framing (`E1`), JSON escapes included; QuickJS refuses an oversized frame whole
761
+ rather than slicing through its JSON, hides one whose JSON is malformed, and
762
+ keeps the raw bridge and its JSON decoder in a private closure so guest code
763
+ cannot intercept a frame first. A mismatched frame is ordinary untyped prose.
636
764
 
637
765
  ## Verification
638
766
 
639
767
  Every clause has a test. `test/guest-contract-cases.ts` holds the case table,
640
768
  written once and run twice: `test/guest-api-contract-quickjs.test.ts` runs it on
641
- the Node QuickJS executor, and `test/guest-api-contract.test.ts` runs it on a real
642
- `DynamicWorkerExecutor` in workerd — a Miniflare Worker Loader binding makes that
643
- arm real rather than simulated — alongside the clauses connecta enforces above any
644
- executor. Rows naming `test/guest-api-contract.test.ts` are covered by both arms,
645
- and each case's title carries its clauses. Two arms passing one table is also the
646
- check on the executor duties above, with `test/codemode-compat.test.ts` holding
647
- the upstream `Executor` shape assignable.
769
+ the Node QuickJS executor, and `test/guest-api-contract.test.ts` runs it on a
770
+ real `DynamicWorkerExecutor` in workerd — a Miniflare Worker Loader binding
771
+ makes that arm real rather than simulated — alongside the clauses connecta
772
+ enforces above any executor. Rows naming `test/guest-api-contract.test.ts` are
773
+ covered by both arms, and each case's title carries its clauses. Two arms
774
+ passing one table is also the check on the executor duties above, with
775
+ `test/codemode-compat.test.ts` holding the upstream `Executor` shape assignable.
648
776
 
649
777
  | Clauses | Test |
650
778
  | --- | --- |
@@ -653,52 +781,41 @@ the upstream `Executor` shape assignable.
653
781
  | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
654
782
  | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
655
783
  | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (canonical addressing), `test/server.test.ts` (bounded live connector inventory) |
656
- | `S1`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, and the unfiltered browse that replaces `list_connectors`), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools.test.ts` (mixed complete/partial ranking and stable pagination) |
784
+ | `S1`, `S1a`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, unfiltered browse), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools-search.test.ts` (mixed complete/partial ranking, stable pagination, the two safety classes) |
657
785
  | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
658
- | `S4` | both guest-contract executors (ordered mixed describe results with unknown-address, unknown-tool suggestion, and catalog-failure details), `test/meta-tools.test.ts` (top-level routing, no-suggestion, catalog-failure, and hostile-input bounds) |
659
- | `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
660
- | `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
786
+ | `S4` | both guest-contract executors (ordered mixed describe results with unknown-address, unknown-tool suggestion, and catalog-failure details), `test/meta-tools-search.test.ts` (top-level routing, no-suggestion, catalog-failure, and hostile-input bounds) |
787
+ | `S5`, `S6` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`, fail-closed annotations, activity parity) |
661
788
  | `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) |
663
- | `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
- | `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
- | `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
666
- | `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
789
+ | `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, forged frames) |
790
+ | `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, failure isolation) |
791
+ | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery), `test/meta-tools-call.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
792
+ | `E3`, `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`, destructive reroute) |
667
793
  | `E5` | `test/guest-api-contract.test.ts` (execution-failure channel, in-flight `cancelled`), `test/execute.test.ts` (admission), `test/executor-admission.test.ts`, `test/quickjs-executor.test.ts` (mid-run shutdown) |
668
794
  | `E6`, `X8` | `test/guest-api-contract.test.ts` (unknown and inherited members, wrapped-message precedence), `test/quickjs-executor.test.ts` |
669
795
  | `E7` | `test/guest-api-contract.test.ts` (refusals about a `503`-named connector), `test/errors.test.ts` |
670
- | `R1`, `R3` | `test/guest-api-contract.test.ts` (pass-through, truncation is success) |
671
- | `R2` | `test/guest-api-contract.test.ts` (envelope fits the cap, idempotent) |
672
- | `R4` | verdict; `R2` is its enforcement |
796
+ | `R1`, `R2`, `R3` | `test/guest-api-contract.test.ts` (pass-through, truncation is success, envelope fits the cap and is idempotent) |
797
+ | `R4`, `M6`, `M9` | verdicts; `R2`'s guard, `M1`'s strict typing, and `M2`'s collect-then-deliver are their enforcement |
673
798
  | `R5` | `test/guest-api-contract.test.ts`, `test/quickjs-log-limits.test.ts` |
674
799
  | `R6`–`R8` | `test/guest-api-contract.test.ts` (normal result keys), `test/execute.test.ts` (opt-in operation aggregates, failure paths, payload exclusion) |
675
- | `Y1` | `test/guest-api-contract.test.ts` (one attempt per call) |
676
- | `Y2`, `Y3` | `test/guest-api-contract.test.ts` (retryable flags by code) |
800
+ | `Y1`, `Y2`, `Y3` | `test/guest-api-contract.test.ts` (one attempt per call, retryable flags by code) |
677
801
  | `Y4` | `test/meta-tools-call.test.ts`, `test/call-admission.test.ts` (one attempt, retry hints, caller reissue) |
678
802
  | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls) |
679
803
  | `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) |
681
- | `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) |
683
- | `L7` | `test/execute.test.ts`, `test/executor-admission.test.ts` |
684
- | `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) |
804
+ | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets) |
805
+ | `L5`, `L7`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue) |
806
+ | `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) |
807
+ | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, the identity clamp) |
685
808
  | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
686
809
  | `M2`, `M3` | `test/guest-api-contract.test.ts` (delivery order, truncated return plus delivered blocks), `test/execute-emit.test.ts` (envelope, `structuredContent`, byte-for-byte no-emit path) |
687
810
  | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths), `test/quickjs-executor.test.ts` (mid-run shutdown) |
688
811
  | `M5`, `M7` | `test/execute-emit.test.ts` (both budgets fail the crossing block; host-call budget untouched) |
689
- | `M6`, `M9` | verdicts; `M1`'s strict typing and `M2`'s collect-then-deliver are their enforcement |
690
812
  | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
691
813
  | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
692
- | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
693
- | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
694
- | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
814
+ | `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) |
815
+ | `X3`, `X6` | `test/quickjs-executor.test.ts` (cancels a running child, never-settling await) |
695
816
  | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
696
817
 
697
- The surface itself is checked by `test/server.test.ts` (the exact seven-tool list)
698
- and `test/code-first-surface.test.ts` (the fold's construction rules, the
818
+ The surface itself is checked by `test/server.test.ts` (the exact seven-tool
819
+ list) and `test/code-first-surface.test.ts` (the fold's construction rules, the
699
820
  required executor, the refusals a removed top-level tool now gets, copy, and
700
- measured size). The small whole-agent benchmark checks both read routes, provider semantics, and private pagination:
701
-
702
- ```sh
703
- npm --prefix eval/current-version run benchmark
704
- ```
821
+ measured size).