@zackbart/connecta 0.24.3 → 0.25.0

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