@zackbart/connecta 0.10.0 → 0.10.2

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 (123) hide show
  1. package/AGENTS.md +113 -0
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +62 -12
  4. package/bin/connecta.mjs +272 -0
  5. package/dist/access-tokens.d.ts +32 -0
  6. package/dist/access-tokens.d.ts.map +1 -0
  7. package/dist/access-tokens.js +225 -0
  8. package/dist/access-tokens.js.map +1 -0
  9. package/dist/catalog-service.d.ts +39 -1
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +133 -11
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/catalog.d.ts +17 -0
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +113 -13
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/execute.d.ts +45 -1
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +265 -68
  20. package/dist/execute.js.map +1 -1
  21. package/dist/index.d.ts +11 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +12 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/invocation.d.ts.map +1 -1
  26. package/dist/invocation.js +1 -5
  27. package/dist/invocation.js.map +1 -1
  28. package/dist/meta-tools.d.ts +1 -0
  29. package/dist/meta-tools.d.ts.map +1 -1
  30. package/dist/meta-tools.js +410 -12
  31. package/dist/meta-tools.js.map +1 -1
  32. package/dist/operator-ui/generated.d.ts +2 -2
  33. package/dist/operator-ui/generated.d.ts.map +1 -1
  34. package/dist/operator-ui/generated.js +2 -2
  35. package/dist/operator-ui/generated.js.map +1 -1
  36. package/dist/operator-ui/model.d.ts +2 -0
  37. package/dist/operator-ui/model.d.ts.map +1 -1
  38. package/dist/operator-ui/model.js.map +1 -1
  39. package/dist/routes/access-tokens.d.ts +7 -0
  40. package/dist/routes/access-tokens.d.ts.map +1 -0
  41. package/dist/routes/access-tokens.js +84 -0
  42. package/dist/routes/access-tokens.js.map +1 -0
  43. package/dist/routes/shared.d.ts +3 -0
  44. package/dist/routes/shared.d.ts.map +1 -1
  45. package/dist/routes/shared.js.map +1 -1
  46. package/dist/routes/ui.d.ts.map +1 -1
  47. package/dist/routes/ui.js +9 -1
  48. package/dist/routes/ui.js.map +1 -1
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +5 -0
  51. package/dist/server.js.map +1 -1
  52. package/dist/skills.d.ts +1 -1
  53. package/dist/skills.d.ts.map +1 -1
  54. package/dist/skills.js +1 -1
  55. package/dist/storage/file.d.ts.map +1 -1
  56. package/dist/storage/file.js +5 -0
  57. package/dist/storage/file.js.map +1 -1
  58. package/dist/storage/memory.d.ts.map +1 -1
  59. package/dist/storage/memory.js +8 -0
  60. package/dist/storage/memory.js.map +1 -1
  61. package/dist/tool-safety.d.ts +10 -0
  62. package/dist/tool-safety.d.ts.map +1 -0
  63. package/dist/tool-safety.js +12 -0
  64. package/dist/tool-safety.js.map +1 -0
  65. package/dist/types.d.ts +5 -0
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/ui.d.ts +4 -4
  68. package/dist/ui.d.ts.map +1 -1
  69. package/dist/ui.js +44 -1
  70. package/dist/ui.js.map +1 -1
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/documentation/architecture.md +7 -0
  74. package/documentation/auth.md +58 -0
  75. package/documentation/call-admission.md +7 -0
  76. package/documentation/code-first-exploration.md +292 -0
  77. package/documentation/code-mode.md +697 -0
  78. package/documentation/connector-guides.md +7 -0
  79. package/documentation/connectors.md +63 -0
  80. package/documentation/mcp-2026-07-28.md +46 -0
  81. package/documentation/meta-tools.md +167 -0
  82. package/documentation/operations.md +7 -0
  83. package/documentation/operator-ui.md +7 -0
  84. package/documentation/request-admission.md +7 -0
  85. package/documentation/storage-and-credentials.md +54 -0
  86. package/ethos.md +132 -0
  87. package/examples/node/README.md +53 -0
  88. package/examples/node/src/index.ts +73 -0
  89. package/examples/worker/README.md +160 -0
  90. package/examples/worker/src/cloudflare-kv.ts +43 -0
  91. package/examples/worker/src/d1-activity-row.ts +100 -0
  92. package/examples/worker/src/d1-activity.ts +144 -0
  93. package/examples/worker/src/index.ts +136 -0
  94. package/examples/worker/wrangler.jsonc +26 -0
  95. package/package.json +11 -1
  96. package/src/access-tokens.ts +289 -0
  97. package/src/catalog-service.ts +177 -15
  98. package/src/catalog.ts +143 -12
  99. package/src/execute.ts +372 -96
  100. package/src/index.ts +33 -1
  101. package/src/invocation.ts +1 -8
  102. package/src/meta-tools.ts +504 -11
  103. package/src/operator-ui/browser.css +63 -0
  104. package/src/operator-ui/browser.ts +288 -2
  105. package/src/operator-ui/generated.ts +2 -2
  106. package/src/operator-ui/model.ts +6 -0
  107. package/src/routes/access-tokens.ts +115 -0
  108. package/src/routes/shared.ts +3 -0
  109. package/src/routes/ui.ts +9 -0
  110. package/src/server.ts +5 -0
  111. package/src/skills.ts +1 -1
  112. package/src/storage/file.ts +5 -0
  113. package/src/storage/memory.ts +8 -0
  114. package/src/tool-safety.ts +15 -0
  115. package/src/types.ts +5 -0
  116. package/src/ui.ts +50 -1
  117. package/src/version.ts +1 -1
  118. package/templates/node/.env.example +5 -0
  119. package/templates/node/AGENTS.md +19 -0
  120. package/templates/node/README.md +33 -0
  121. package/templates/node/package.json +23 -0
  122. package/templates/node/src/index.ts +43 -0
  123. package/templates/node/tsconfig.json +12 -0
@@ -0,0 +1,697 @@
1
+ # Code mode — the guest API contract
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](./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
12
+ between them is a bug unless it appears in
13
+ [Executor exceptions](#executor-exceptions), which names the reason. Anyone can
14
+ implement a third executor from this document without reading either.
15
+
16
+ The [code-first exploration](./code-first-exploration.md) is the evidence behind
17
+ the direction; [`ethos.md`](../ethos.md) carries the verdicts. Where its prototype
18
+ and this document disagree, this document wins. Clause identifiers (`A1`, `E3`, …)
19
+ are stable and cited by the tests in [Verification](#verification).
20
+
21
+ ## Deploy-time capability
22
+
23
+ The `executor` passed to `createConnecta()` is the complete switch, and it selects
24
+ the whole surface rather than one tool
25
+ ([#224](https://github.com/zackbart/connecta/issues/224)): with a live `Executor`,
26
+ `tools/list` is exactly seven — `execute_code`, `search_tools`, `call_tool`,
27
+ `call_destructive_tool`, `authorize_connector`, `get_result`, `skills`; without one
28
+ it is the nine base meta-tools, whose `list_connectors`, `describe_tools`, and
29
+ `batch_call` are what seven folds into `connecta.search`, `connecta.describe`, and
30
+ `connecta.batch`.
31
+
32
+ No feature flag, and no code tool advertised before it can be honored.
33
+ `surface: "classic"` beside an executor is the one override (ten tools, the eval
34
+ gate's incremental arm); `surface: "code-first"` without one throws at
35
+ construction rather than advertise an absent program surface.
36
+
37
+ On Node, install the optional `quickjs-emscripten` peer and use the package's
38
+ QuickJS subpath:
39
+
40
+ ```ts
41
+ import { createConnecta } from "@zackbart/connecta";
42
+ import { quickJsExecutor } from "@zackbart/connecta/quickjs";
43
+
44
+ const connecta = createConnecta({
45
+ executor: quickJsExecutor(),
46
+ // connectors, auth, storage…
47
+ });
48
+ ```
49
+
50
+ `quickJsExecutor()` runs each program in a disposable child-process sandbox; its
51
+ CPU, wall-time, memory, stack, queue, result, log, and IPC bounds are configured
52
+ on the executor. Server bundlers must keep the `@zackbart/connecta/quickjs`
53
+ package files external so the child entry stays on disk. The
54
+ [Node example](../examples/node/README.md) is enabled; remove its `executor` field
55
+ for the nine-tool compatibility deployment.
56
+
57
+ On Cloudflare Workers, the Worker Loader binding is both the paid capability and
58
+ the configuration switch:
59
+
60
+ ```ts
61
+ createConnecta({
62
+ ...(env.LOADER
63
+ ? { executor: new DynamicWorkerExecutor({ loader: env.LOADER }) }
64
+ : {}),
65
+ // connectors, auth, storage…
66
+ });
67
+ ```
68
+
69
+ Leave the binding absent on the Workers Free plan. Its absence must also be
70
+ represented as optional in the deployment's `Env` type. The
71
+ [Worker example](../examples/worker/README.md#code-mode) carries the complete
72
+ binding and package setup.
73
+
74
+ ## What an executor must implement
75
+
76
+ The host side of the seam is two types in `src/types.ts` and nothing else.
77
+
78
+ ```ts
79
+ interface Executor {
80
+ execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
81
+ close?(): void | Promise<void>;
82
+ }
83
+
84
+ interface ExecutorProvider {
85
+ name: string; // a global's name
86
+ fns: Record<string, (...args: unknown[]) => Promise<unknown>>;
87
+ prelude?: string; // host-authored guest code, not model input
88
+ }
89
+
90
+ interface ExecuteResult {
91
+ result: unknown; // the program's resolved value
92
+ error?: string; // set instead of result when the run failed
93
+ logs?: string[]; // captured console output, in call order
94
+ }
95
+ ```
96
+
97
+ Connecta passes exactly one provider, named `connecta`. An executor must:
98
+
99
+ 1. **Expose each provider as a guest global** whose properties are its `fns`,
100
+ called with the program's arguments and awaited. Connecta's provider carries
101
+ `search`, `describe`, `call`, `batch`, and `__callNamespace` — see point 3.
102
+ 2. **Evaluate `prelude` after the provider globals exist and before the
103
+ program**, in a scope where those globals are reachable. It is host-authored
104
+ trusted code, never model input, and skipping it is not an option: connecta's
105
+ prelude is what installs the lazy connector shortcuts.
106
+ 3. **Let the prelude reach the provider.** That prelude
107
+ (`lazyNamespacePrelude` in `src/execute.ts`) assigns one
108
+ `globalThis[<connectorId>]` Proxy per connector, each forwarding to
109
+ `connecta.__callNamespace(connectorId, toolName, args)`. An executor exposing
110
+ only the four documented functions leaves every shortcut dead and breaks `A2`.
111
+ 4. **Marshal values as JSON** in both directions (`P3`), and reject a host call
112
+ whose function is not an own property of `fns` — the guest can ask for
113
+ anything, including inherited members.
114
+ 5. **Return, never throw, for a failed program**: set `error` to the guest's
115
+ message, leave `result` undefined. `createExecuteTool` reads `error` first and
116
+ matches it back to the failures recorded during the run, which is how an
117
+ uncaught tool failure keeps its type (`E1`).
118
+ 6. **Capture `console.log`, `console.warn`, and `console.error`** into `logs` in
119
+ call order (`R5`), bounding what it retains.
120
+ 7. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`), with no
121
+ network, filesystem, environment, or import capability (`P2`).
122
+ 8. **Grant no ambient authority of its own.** Never back this with `eval` or
123
+ `node:vm`: the sandbox is a containment layer on top of connecta's boundary,
124
+ not a replacement for it, and every capability arrives through `fns`.
125
+
126
+ Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
127
+ `execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
128
+ connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise.
129
+
130
+ ## The program
131
+
132
+ **P1.** A program is one JavaScript `async` arrow-function expression. It is
133
+ evaluated once and its resolved value is the program's result. Both executors
134
+ also accept markdown-fenced code and a bare statement body, and each normalizes
135
+ those differently; that leniency is a courtesy to model output, not contract. A
136
+ program that is not an async arrow expression may be accepted, rejected, or
137
+ reinterpreted, so do not rely on it.
138
+
139
+ **P2.** The only capabilities in the contract are:
140
+
141
+ - one lazy global per connector (see [Addressing](#addressing));
142
+ - `connecta.search`, `connecta.describe`, `connecta.call`, `connecta.batch`;
143
+ - `console.log`, `console.warn`, `console.error`, captured and returned.
144
+
145
+ `connecta` also carries the `__`-prefixed dispatcher the shortcut prelude uses.
146
+ It is host plumbing, callable but not contract: it takes a connector id and an
147
+ unsanitized-or-sanitized tool name and can change shape without notice.
148
+
149
+ Anything else a runtime happens to expose is outside the contract and must not
150
+ be used, even where it exists. Neither executor grants network egress,
151
+ filesystem access, credentials, or deployment configuration; what they leave
152
+ lying around otherwise differs (`X5`).
153
+
154
+ **P3.** Values cross the host bridge as JSON. Arguments must be
155
+ JSON-serializable and results arrive as plain JSON values. A value outside JSON —
156
+ a cycle, a `BigInt`, a function, a class instance — never round-trips: it either
157
+ ends the run with an error or is converted lossily, executor's choice (`X9`).
158
+ Return JSON-shaped data and the question does not arise.
159
+
160
+ **P4.** Nothing survives an execution. There is no module scope, cache, or
161
+ scratch storage carried to the next program, and no request-bound object outlives
162
+ the request that created it. Within one execution, host calls share one
163
+ downstream request scope.
164
+
165
+ **P5.** Plain JavaScript only. TypeScript syntax is a syntax error, and there is
166
+ no `import` or `require` to reach for.
167
+
168
+ ## Addressing
169
+
170
+ **A1.** The canonical address `<connectorId>.<toolName>` — byte-for-byte what
171
+ `search_tools` and `connecta.search` print — is always callable through
172
+ `connecta.call` and `connecta.batch`. This is never optional and never
173
+ sanitized. It is what prevents sanitized-name collisions and what gives a
174
+ generated program a stable escape hatch when a shortcut is ambiguous, absent, or
175
+ wrong. A program that can only reach a tool through a convenience name is one
176
+ rename away from broken.
177
+
178
+ **A2.** Shortcut namespaces are sugar over `A1`: every connector gets one lazy
179
+ global whose properties are its tools, so `<connectorId>.<toolName>(args)` works
180
+ with both parts sanitized into JavaScript identifiers — characters outside
181
+ `[A-Za-z0-9_$]` become `_`, a leading digit gets `_` prefixed, and a reserved
182
+ word gets `_` appended (`my-service.get.thing` → `my_service.get_thing`). The
183
+ globals are lazy: no catalog is fetched until a program touches one.
184
+
185
+ **A3.** A shortcut that resolves to more than one tool fails closed with
186
+ `ambiguous_tool_alias`, naming the colliding tool names and pointing at
187
+ `connecta.call`. It never picks one. The canonical addresses of both tools
188
+ remain callable.
189
+
190
+ **A4.** A deployment whose connector ids collide with each other after
191
+ sanitization, or that sanitize onto a name the sandbox reserves, fails *every*
192
+ `execute_code` request with an error naming the offending ids. Failing loudly on
193
+ the deployment's mistake beats silently answering from whichever connector
194
+ sorted first.
195
+
196
+ **A5 (verdict: shortcut namespaces are kept, and frozen).** They cost nothing to
197
+ keep, a working ergonomic surface should not be removed mid-arc, and the
198
+ exploration's cold-start sample used them naturally. Frozen means no typed method
199
+ lists, no per-tool closures, no generated `.d.ts`, no second sanitization rule —
200
+ every expansion invents a collision class the addressing in `A1` already solves.
201
+ The default has since flipped without revisiting them
202
+ ([#224](https://github.com/zackbart/connecta/issues/224)), so evidence rather
203
+ than a gate would take them away: if programs reach for `connecta.call` anyway,
204
+ or shortcut ambiguity shows up in failures, they lose.
205
+
206
+ ## The surface
207
+
208
+ Four functions, all `async`, plus the host-internal `__`-prefixed dispatcher
209
+ (`P2`) that is callable but not contract. Nothing else works: reading any other
210
+ property yields a function — the guest namespace is a Proxy, so `typeof
211
+ connecta.toString` is `"function"` — but *calling* it fails, because the host
212
+ resolves only own members of the provider's `fns`. A program must treat the four
213
+ documented functions as the whole surface.
214
+
215
+ ### connecta.search
216
+
217
+ ```js
218
+ const page = await connecta.search({
219
+ query: "pipeline run job logs", // 2–4 distinctive action/object terms
220
+ connector: "ci", // optional single-connector filter
221
+ safety: "readOnly", // or "approvalRequired" / "all"
222
+ limit: 8, // 1–100, default 8
223
+ offset: 0,
224
+ fullDescriptions: false,
225
+ includeSchemas: "compact", // or "json"
226
+ includeSchemaKeys: true, // default true in code mode
227
+ });
228
+ ```
229
+
230
+ **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes; capped shapes remain structurally valid with `unknown` types plus `/* truncated */`, and carry `inputSchemaTruncated` or `outputSchemaTruncated`. Use `connecta.describe` (or JSON search) for omitted exact constraints.
231
+
232
+ **S1a.** `safety: "readOnly"` returns exactly the tools available through `connecta.call`, connector shortcuts, and `connecta.batch`; `"approvalRequired"` returns the complementary fail-closed class, including false, missing, and contradictory annotations. Omitted or `"all"` preserves the complete catalog. This filters rows only: it grants no authority and changes no admission decision.
233
+
234
+ **S2.** With schemas requested, a match whose input (or output) schema resolves
235
+ to an object shape also carries `inputKeys`, `requiredInputKeys`, and
236
+ `outputKeys`: the same names the rendered schema shows, ready to check before
237
+ building arguments. A schema that is not an object shape — a union, an array, an
238
+ unresolvable `$ref` — carries no lists rather than empty ones, because absent
239
+ means "read the schema" where `[]` would claim the tool takes no fields. The
240
+ lists come from the same walk that renders the compact schema, so a top-level
241
+ `$ref` resolves and an `allOf` composes rather than reporting an empty list
242
+ beside a schema that plainly shows fields; an object with no properties is the
243
+ one case where `[]` is the truth. This metadata is code-mode-only:
244
+ `search_tools` never carries it, and `includeSchemaKeys: false` buys the bytes
245
+ back.
246
+
247
+ **S3.** Discovery is bounded and the bounds throw rather than silently shrink: a
248
+ `limit` outside 1–100 is `invalid_args`, and a page whose serialized form
249
+ exceeds 256,000 bytes is `result_too_large`, each with a hint naming the ways to
250
+ ask for less. As with every failure, the *thrown* error carries only the message
251
+ (`E1`); the code appears when the failure escapes the program uncaught.
252
+
253
+ ### connecta.describe
254
+
255
+ ```js
256
+ const { tools } = await connecta.describe({
257
+ addresses: ["ci.get_run", "ci.get_job_logs"], // ≤ 100
258
+ format: "compact", // or "json"
259
+ fullDescriptions: false,
260
+ });
261
+ ```
262
+
263
+ **S4.** Returns `{ tools }` in the order asked, one entry per address. An
264
+ address that is unknown, or whose connector's catalog could not be loaded,
265
+ returns an entry carrying `error` — one bad address never fails the whole call.
266
+ More than 100 addresses is `invalid_args`; the same 256,000-byte ceiling applies.
267
+
268
+ ### connecta.call
269
+
270
+ ```js
271
+ const run = await connecta.call("ci.get_run", { runId: 42 });
272
+ ```
273
+
274
+ **S5.** Takes a canonical address and one arguments object; returns the tool's
275
+ value already unwrapped. For an MCP connector that means `structuredContent`
276
+ when present, otherwise text content JSON-parsed when it parses and the raw text
277
+ when it does not; a downstream result flagged `isError` throws. Omitted `args`
278
+ is treated as `{}`.
279
+
280
+ **S6.** Every call — canonical or shortcut — goes through the same catalog,
281
+ fail-closed read-only predicate, admission, credential containment, timeout
282
+ classification, health accounting, and activity recording as an ordinary
283
+ meta-tool call. The sandbox is an additional containment layer, not a second
284
+ implementation of the boundary, and nothing a program does widens what it can
285
+ reach.
286
+
287
+ ### connecta.batch
288
+
289
+ ```js
290
+ const outcomes = await connecta.batch([
291
+ { address: "ci.get_run", args: { runId: 42 } },
292
+ { address: "ci.list_jobs", args: { runId: 42 } },
293
+ ]);
294
+ ```
295
+
296
+ **S7.** Runs 1–10 independent calls in parallel and returns their outcomes in
297
+ order. A success is `{ address, ok: true, data }`. A failure is
298
+ `{ address, ok: false, error, errorDetails }`, where `error` is the message and
299
+ `errorDetails` is the typed object described in [Errors](#errors) — the same two
300
+ field names `batch_call` uses. One failing call never rejects the batch, and more
301
+ than ten calls throws.
302
+
303
+ **S8.** `connecta.batch` is the classification channel: because a thrown host
304
+ error crosses the bridge as a bare message (`E1`), a batch of one is the supported
305
+ way for a program to *decide* something about a failure rather than report it.
306
+
307
+ ## Errors
308
+
309
+ **E1.** There are four error channels, and only two of them are typed.
310
+
311
+ | Channel | Shape | Typed? |
312
+ | --- | --- | --- |
313
+ | A throw inside the program | `Error` with `message` only | no |
314
+ | `connecta.batch` outcome | `{ ok: false, error, errorDetails }` | yes |
315
+ | An uncaught **tool or discovery** failure, as the model sees it | `{ error: { code, message, retryable, … } }` with `isError` | yes |
316
+ | Anything else that ends the run (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
317
+
318
+ The message-only throw is a hard limit of the guest bridge: both executors reduce a rejected
319
+ host call to `new Error(message)`, dropping every own property. A program must
320
+ therefore never branch on an error's fields and never parse its message. To
321
+ classify, use `errorDetails`; to hand a failure to the model with its type
322
+ intact, let it escape uncaught — connecta re-attaches the typed details on the
323
+ way out. The model-facing version of this lives in `execute_code`'s description,
324
+ not in the always-loaded usage skill, which `test/meta-tools.test.ts` caps at
325
+ 1,800 bytes with three bytes spare.
326
+
327
+ **E2.** The taxonomy. `retryable` is what connecta reports; `Y3` says what a
328
+ program may do about it.
329
+
330
+ | Code | Raised when | `retryable` |
331
+ | --- | --- | --- |
332
+ | `unknown_address` | no connector owns the address | false |
333
+ | `unknown_tool` | the connector has no such tool | false |
334
+ | `ambiguous_tool_alias` | a shortcut matches two tools (`A3`) | false |
335
+ | `destructive_tool_requires_approval` | the tool is not explicitly read-only | false |
336
+ | `auth_required` | the credential is missing, expired, or rejected | false |
337
+ | `invalid_args` | arguments or discovery bounds were rejected | false |
338
+ | `input_required_unsupported` | a downstream asked for mid-call input | false |
339
+ | `rate_limited` | the downstream reported a rate limit | true |
340
+ | `unavailable` | the downstream is down or unreachable | true |
341
+ | `timeout` | the per-call 15-second deadline expired | true |
342
+ | `cancelled` | the run ended while this call was in flight (`E5`) | false |
343
+ | `connector_call_failed` | anything else the connector threw, and the host-call budget (`L4`) | per message |
344
+ | `batch_call_failed` | a `connecta.batch` entry connecta could not even attempt | per message |
345
+ | `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
346
+ | `result_processing_failed` | the result could not be prepared | per message |
347
+
348
+ **E3.** `auth_required` carries the same recovery envelope as `call_tool`:
349
+ `connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
350
+ `unavailable`), `nextAction` naming `authorize_connector`, and a `retry`
351
+ sentence. A program cannot recover credentials — only an operator can — so the
352
+ right move is to stop and let the failure reach the model.
353
+
354
+ **E4.** A read-only refusal is not a downstream failure. An unannotated,
355
+ write-capable, or destructive tool is refused in the sandbox with
356
+ `destructive_tool_requires_approval` and stays refused; the program returns and
357
+ the model crosses `call_destructive_tool`, where the host can ask a human.
358
+ Generated code cannot mint that capability.
359
+
360
+ **E5.** Failures of the *execution*, not of a call, never appear inside the
361
+ guest: admission rejection (`executor_overloaded`, retryable, with
362
+ `retryAfterMs`), cancellation (`executor_cancelled`), shutdown
363
+ (`executor_closed`), deadline expiry, and sandbox crashes end the run and are
364
+ reported to the model as an error result. One seam: a host call still in flight
365
+ when the run is cancelled fails with `cancelled`, catchable on the way out but
366
+ never worth acting on (`Y3`).
367
+
368
+ **E6.** An error the program raises itself — a `TypeError`, a call to a
369
+ `connecta` member that is not a provider function (including an inherited one
370
+ like `toString`), a `throw` of its own — ends the run with an error result
371
+ carrying that message. It is not typed, because it is not a connector failure.
372
+ One precedence rule: connecta recognizes an escaped tool failure by its message —
373
+ exactly first, by containment second — so a program that *wraps* a failure's
374
+ message in its own text still reports the underlying typed failure. Keeping the
375
+ type beats keeping the prose.
376
+
377
+ **E7.** `retryable` for the four codes connecta frames itself — `unknown_address`,
378
+ `unknown_tool`, `ambiguous_tool_alias`, `destructive_tool_requires_approval` — is
379
+ pinned false in code, never derived from the message. Those messages embed the
380
+ address asked for, and the heuristic that classifies *connector* errors matches
381
+ `503`, `429`, `temporar`; a connector named `svc-503` would otherwise turn a
382
+ policy refusal into `retryable: true`, the exact failure this contract prevents.
383
+
384
+ ## Results and projection
385
+
386
+ **R1 (verdict: projection stays explicit).** A program's return value reaches
387
+ the model unchanged except for the size guard in `R2`. Connecta does not
388
+ summarize, reshape, or field-select it, and there is no automatic projection
389
+ mode. The 93%-byte win the exploration measured came from *program-authored*
390
+ projection; a host heuristic would silently drop fields a program deliberately
391
+ returned and would be invisible in the transcript. Host-side projection helpers
392
+ earn their way in only if [#222](https://github.com/zackbart/connecta/issues/222)
393
+ shows programs failing to project on their own.
394
+
395
+ **R2.** The boundary is 24,000 serialized characters (~6k tokens). A value over
396
+ it is replaced by exactly one envelope:
397
+
398
+ ```json
399
+ {
400
+ "truncated": true,
401
+ "preview": "…",
402
+ "totalChars": 5242880,
403
+ "hint": "filter/map/slice data inside execute_code and return only what you need"
404
+ }
405
+ ```
406
+
407
+ The envelope is itself bounded as serialized, so `totalChars` is always the true
408
+ size of what the program returned and truncation happens exactly once no matter
409
+ how many hops the value takes.
410
+
411
+ **R3.** Truncation is a *successful* result, not an error: the program ran, and
412
+ what came back is the honest report that its answer was too large. The fix is a
413
+ program that returns less, which is why the envelope says so.
414
+
415
+ **R4 (verdict: no result paging for programs).** A truncated program result
416
+ carries no `get_result` handle, unlike `call_tool`. `get_result` exists so a model
417
+ can page a *downstream payload* it could not shrink; a program can shrink
418
+ anything, so paging its result would reward the one behavior code mode exists to
419
+ remove — and stashing every unprojected return value would spend the result store
420
+ on data nobody asked for.
421
+
422
+ **R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive failure — they ride along with the error result, which is what makes them worth writing. How a non-string argument renders is not contract (`X4`).
423
+
424
+ **R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; omitted and `false` are byte-for-byte the ordinary response path.
425
+
426
+ **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`, `batch`) gets one aggregate with count, failures, duration, returned serialized bytes, and catalog/connector time; batch adds only its total child count.
427
+
428
+ **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.
429
+
430
+ ## Retry semantics
431
+
432
+ **Y1.** Connecta retries nothing beneath a program. `call_tool` accepts an
433
+ annotation-gated `maxRetries`; code mode fixes it at zero, so one
434
+ `connecta.call` is exactly one downstream attempt. The program is the retry
435
+ loop, and its budget is visible to it (`L4`).
436
+
437
+ **Y2.** A program may retry a failure whose `errorDetails.retryable` is true,
438
+ learned through `connecta.batch` (`S8`). Every attempt spends host-call budget,
439
+ so a retry loop that ignores the budget converts a transient failure into a
440
+ budget failure.
441
+
442
+ **Y3.** What must never be retried automatically:
443
+
444
+ - anything with `retryable: false` — a policy refusal, a missing credential, a
445
+ bad address, or malformed arguments will fail identically forever;
446
+ - `rate_limited`, immediately. The sandbox has no timers, so a program cannot
447
+ wait out a window; retrying inside it is the harm the signal exists to
448
+ prevent. Return the failure and let the model, which can wait, re-issue with
449
+ `retryAfterMs` in hand.
450
+ - a cancelled or timed-out *execution*: it is already over (`L1`).
451
+
452
+ **Y4.** Connecta's own retry machinery beneath the meta-tools honours a
453
+ connector-reported `Retry-After` exactly or not at all, and declines windows
454
+ longer than 10 seconds rather than shortening them. A program sees the window
455
+ verbatim as `errorDetails.retryAfterMs`.
456
+
457
+ ## Cancellation and limits
458
+
459
+ **L1.** Cancellation is not observable inside a program. There is no signal to
460
+ poll, no cancellation exception to catch, and no guarantee that a `finally`
461
+ block runs — a cancelled QuickJS child is terminated outright. Write programs
462
+ that need no cleanup.
463
+
464
+ **L2.** What cancellation guarantees: in-flight host calls abort, no further host
465
+ call is admitted, the admission lease is released, and nothing request-bound
466
+ survives the request.
467
+
468
+ **L3.** Every execution runs under a wall-clock deadline that includes time spent
469
+ waiting on host calls. Expiry ends the run with an execution error and no
470
+ partial result; the deadline's length is executor configuration (`X1`).
471
+
472
+ **L4.** Per-execution bounds that are contract, identical in both executors
473
+ because connecta enforces them above the sandbox:
474
+
475
+ | Bound | Value |
476
+ | --- | --- |
477
+ | Host calls per execution | 20 |
478
+ | Calls per `connecta.batch` | 10 |
479
+ | Deadline per host call | 15 s |
480
+ | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
481
+ | `describe` addresses | ≤ 100 |
482
+ | Result | 24,000 serialized characters |
483
+ | Logs presented to the model | 4,000 characters |
484
+
485
+ Exhausting the host-call budget fails that call like any other, with code
486
+ `connector_call_failed` (`E2`) and a message naming the budget — no connector was
487
+ reached, so nothing more specific is true. Retrying it is pointless: the budget
488
+ does not refill inside one execution.
489
+
490
+ **L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
491
+ exhausts a bound ends the run with an error instead of degrading the host. The
492
+ mechanism is the executor's: QuickJS enforces an explicit heap (64 MiB default),
493
+ stack (1 MiB), and guest-CPU budget (250 ms, which host waits do not consume);
494
+ the Dynamic Worker inherits the platform isolate's limits (`X2`). A third
495
+ executor must bound all three somehow — this is the clause that makes untrusted
496
+ code safe to run at all.
497
+
498
+ **L6.** A host call's serialized arguments and its serialized result are each
499
+ bounded — QuickJS caps both at 256 KiB (`X10`) — and exceeding either fails that
500
+ call, not the execution, so a program can catch it and ask for less. The failure
501
+ is untyped text (`E1`). An over-bound *result* names the address the program
502
+ called, not the internal dispatcher behind the shortcut namespaces; an over-bound
503
+ *argument* payload is refused before it is parsed, so it names no address at
504
+ all — parsing it to write a better message would spend exactly the work the bound
505
+ exists to refuse.
506
+
507
+ **L7.** Executions are admitted, not queued indefinitely: bounded concurrency plus
508
+ a bounded queue with a wait timeout. Overload is a retryable `executor_overloaded`
509
+ carrying `retryAfterMs`; cancellation and shutdown are terminal. Admission happens
510
+ *before* any catalog or provider is built, so a queued request holds no state.
511
+
512
+ **L8.** Bounds are deployment configuration, not program inputs: a program cannot
513
+ raise one by asking. `execute_code`'s description states the host-call budget, the
514
+ batch maximum, and the per-call deadline — the ones a program must plan around
515
+ before it runs. The result and log caps live here and in the truncation notice
516
+ itself (`R2`, `R5`).
517
+
518
+ ## Activity
519
+
520
+ **V1.** One payload-free activity event per call that named a real connector,
521
+ with `source: "execute_code"` — every dispatched call, plus every refusal
522
+ connecta could attribute to a connector: a read-only refusal, an unknown tool on
523
+ a known connector, an ambiguous shortcut, a connector whose catalog could not be
524
+ loaded, a credential connecta could not supply, an exhausted host-call budget. A
525
+ program that calls ten tools is ten events — as legible as ten `call_tool` calls,
526
+ which is what makes moving work into the sandbox an optimization, not a
527
+ blindfold.
528
+
529
+ **V2.** Each event carries `connectorId`, `toolName`, `address`, `source`,
530
+ `outcome` (`success`, `error`, `timeout`, `cancelled`), `durationMs`,
531
+ `attempts`, and `errorCode` when there was one — plus the request's id, actor,
532
+ and server identity. It has nowhere to put arguments, results, program source,
533
+ or raw error text, by construction. A failure the program *caught* is still
534
+ recorded: the call happened. `address` is canonical (`A1`) for every call that
535
+ resolved to a tool; for the refusals that never resolved to one it is the name the
536
+ program used, which for a shortcut is the sanitized alias — the honest record of
537
+ what was attempted.
538
+
539
+ **V3.** A call whose connector does not exist — an unknown address — emits
540
+ nothing. There is no connector to attribute it to.
541
+
542
+ **V4.** The execution itself emits no event. It has no address, and its one
543
+ distinctive artifact is the program source, which is exactly what a payload-free
544
+ history must never keep.
545
+
546
+ ## Executor exceptions
547
+
548
+ Documented divergences, with reasons. Everything else must match.
549
+
550
+ **X1. Deadline default.** QuickJS defaults to 30 s wall clock and terminates the
551
+ child; the Dynamic Worker defaults to 60 s and races the program against an
552
+ in-isolate timer. Both satisfy `L3`; the numbers are each executor's
553
+ configuration and the error text differs.
554
+
555
+ **X2. Memory, stack, and CPU mechanism.** QuickJS exposes explicit heap, stack,
556
+ and guest-CPU limits (`L5`); the Dynamic Worker has no such knobs, so workerd's
557
+ isolate limits apply untuned. A specific heap ceiling is a Node-only option.
558
+
559
+ **X3. Mid-flight cancellation.** The QuickJS pool receives the request's
560
+ `AbortSignal` and kills the child. The Dynamic Worker executor's `execute()` takes
561
+ no signal, so a cancelled request's program runs on until its host calls fail or
562
+ the deadline expires. `L2` holds either way — the calls abort, the response does
563
+ not wait — but "the run ends" is best-effort on Workers.
564
+
565
+ **X4. Log rendering and capture.** QuickJS JSON-stringifies non-string
566
+ arguments and captures `log`, `info`, `warn`, `error`, and `debug`; the Dynamic
567
+ Worker renders arguments with `String()` (so an object logs as
568
+ `[object Object]`) and captures only `log`, `warn`, and `error`, prefixing the
569
+ latter two. Only the three captured everywhere are contract (`R5`); rendering is
570
+ not.
571
+
572
+ **X5. Leftover globals.** The QuickJS guest has no `fetch`, `process`, timers,
573
+ `crypto`, or `WebSocket` at all. The Dynamic Worker guest has all of them:
574
+ `fetch` exists but throws on use because outbound access is disabled,
575
+ `process.env` is empty, and timers work. `P2` is the contract — a program that
576
+ uses `setTimeout` is writing Workers-only code, and it will fail on Node.
577
+
578
+ **X6. Stall detection.** QuickJS notices a program awaiting something that can
579
+ never settle and fails fast; the Dynamic Worker waits for its deadline. The fast
580
+ failure is better, but requiring it would require a host-driven job loop — not a
581
+ reasonable demand on a platform sandbox.
582
+
583
+ **X7. Value codec.** QuickJS is JSON-only; `@cloudflare/codemode` tunnels binary
584
+ values through a tagged envelope, so a `Uint8Array` may survive there. `P3` is the
585
+ contract: JSON-serializable values, or the program is Workers-only.
586
+
587
+ **X8. Unknown-property message.** An unknown `connecta` property throws
588
+ `Unknown function connecta.x` on QuickJS and `Tool "x" not found` on the Dynamic
589
+ Worker. Both satisfy `E6`; the text is not contract.
590
+
591
+ **X9. Refusing a value outside JSON.** The Dynamic Worker ends the run with an
592
+ error when a program returns something its codec cannot carry. QuickJS converts
593
+ lossily instead — a cyclic object comes back as the string `"[object Object]"`,
594
+ because the guest-to-host dump happens before any serializer can object.
595
+ Normalizing this would mean walking every returned value in the child for
596
+ JSON-representability, spending real CPU on every program to improve the error
597
+ message of a program that is already wrong. `P3` is the contract: neither
598
+ behavior returns the value.
599
+
600
+ **X10. Per-host-call payload bound.** `L6`'s 256 KiB ceiling on a host call's
601
+ arguments and result is QuickJS's, enforced at its IPC boundary. The Dynamic
602
+ Worker has no documented equivalent; Workers RPC limits apply and connecta does
603
+ not add one, because the boundary there is an isolate-to-isolate call rather than
604
+ a `process.send` with a hard ceiling. A program that returns a quarter-megabyte
605
+ from one tool call therefore fails on Node and may succeed on Workers — reduce
606
+ inside the program either way (`R1`).
607
+
608
+ ## Changes from earlier code mode
609
+
610
+ Five behaviors changed with this contract, matching the changelog's Unreleased
611
+ entry. Programs that ran before still run.
612
+
613
+ - **`connecta.batch` failures gained `errorDetails`** (`S7`). They carried only a
614
+ message, which left a program unable to tell a policy refusal from a transient
615
+ failure. Additive, and it reuses `batch_call`'s field names so one shape covers
616
+ both surfaces.
617
+ - **A policy refusal can no longer look retryable** (`E7`). Pinned in code rather
618
+ than read out of message text, so a connector named `svc-503` stops flipping a
619
+ permanent refusal to `retryable: true`. This reaches the call tools too.
620
+ - **An uncaught discovery-bound failure is typed** (`S3`): `invalid_args` or
621
+ `result_too_large` rather than prose, the same envelope a failed call gets.
622
+ - **A bridge-bound failure names the address** (`L6`), not the internal
623
+ dispatcher every shortcut namespace shares.
624
+ - **An oversized result is truncated once** (`R2`). The envelope is sized so its
625
+ *serialized* form fits the cap; the QuickJS path previously truncated in the
626
+ child and again in the parent, reporting the inner envelope's length as
627
+ `totalChars`. Previews are shorter now; `totalChars` is the real size.
628
+
629
+ The middle three were places where the contract described behavior the code did
630
+ not quite have. The code moved, because the described behavior is the one worth
631
+ having.
632
+
633
+ ## Verification
634
+
635
+ Every clause has a test. `test/guest-contract-cases.ts` holds the case table,
636
+ written once and run twice: `test/guest-api-contract-quickjs.test.ts` runs it on
637
+ the Node QuickJS executor, and `test/guest-api-contract.test.ts` runs it on a real
638
+ `DynamicWorkerExecutor` in workerd — a Miniflare Worker Loader binding makes that
639
+ arm real rather than simulated — alongside the clauses connecta enforces above any
640
+ executor. Rows naming `test/guest-api-contract.test.ts` are covered by both arms,
641
+ and each case's title carries its clauses. Two arms passing one table is also the
642
+ check on the executor duties above, with `test/codemode-compat.test.ts` holding
643
+ the upstream `Executor` shape assignable.
644
+
645
+ | Clauses | Test |
646
+ | --- | --- |
647
+ | `P1`, `P5` | `test/guest-api-contract.test.ts` (TypeScript syntax), `test/quickjs-executor.test.ts` (`normalizeCode`) |
648
+ | `P2`, `X5` | `test/guest-api-contract.test.ts` (no usable network, no config) |
649
+ | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
650
+ | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
651
+ | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (sanitizing) |
652
+ | `A3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (colliding alias) |
653
+ | `A4` | `test/execute.test.ts` (namespace collisions, reserved namespace) |
654
+ | `A5` | verdict; `A1`–`A3` are its enforcement |
655
+ | `S1`, `S2` | `test/guest-api-contract.test.ts` (flat page, schema keys, and the unfiltered browse that replaces `list_connectors`), `test/execute.test.ts` (`$ref`/`allOf`) |
656
+ | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
657
+ | `S4` | `test/guest-api-contract.test.ts` (unknown address in `describe`) |
658
+ | `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
659
+ | `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
660
+ | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
661
+ | `S8`, `E1` | `test/guest-api-contract.test.ts` (typed batch outcomes) |
662
+ | `E2` | `test/guest-api-contract.test.ts`, `test/errors.test.ts` (code → `retryable`) |
663
+ | `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
664
+ | `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
665
+ | `E5` | `test/guest-api-contract.test.ts` (execution-failure channel, in-flight `cancelled`), `test/execute.test.ts` (admission), `test/executor-admission.test.ts` |
666
+ | `E6`, `X8` | `test/guest-api-contract.test.ts` (unknown and inherited members, wrapped-message precedence), `test/quickjs-executor.test.ts` |
667
+ | `E7` | `test/guest-api-contract.test.ts` (refusals about a `503`-named connector), `test/errors.test.ts` |
668
+ | `R1`, `R3` | `test/guest-api-contract.test.ts` (pass-through, truncation is success) |
669
+ | `R2` | `test/guest-api-contract.test.ts` (envelope fits the cap, idempotent) |
670
+ | `R4` | verdict; `R2` is its enforcement |
671
+ | `R5` | `test/guest-api-contract.test.ts`, `test/quickjs-log-limits.test.ts` |
672
+ | `R6`–`R8` | `test/guest-api-contract.test.ts` (normal result keys), `test/execute.test.ts` (opt-in operation aggregates, failure paths, payload exclusion) |
673
+ | `Y1` | `test/guest-api-contract.test.ts` (one attempt per call) |
674
+ | `Y2`, `Y3` | `test/guest-api-contract.test.ts` (retryable flags by code) |
675
+ | `Y4` | `test/meta-tools.test.ts` (`retryBackoffMs`, `MAX_RETRY_BACKOFF_MS`) |
676
+ | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls) |
677
+ | `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors) |
678
+ | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (budgets) |
679
+ | `L5`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap) |
680
+ | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message) |
681
+ | `L7` | `test/execute.test.ts`, `test/executor-admission.test.ts` |
682
+ | `V1`, `V2` | `test/guest-api-contract.test.ts` (dispatched calls, and the four refusal classes that name a connector), `test/activity.test.ts` |
683
+ | `V3`, `V4` | `test/guest-api-contract.test.ts` (no event without a connector) |
684
+ | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
685
+ | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
686
+ | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
687
+ | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
688
+
689
+ The surface itself is checked by `test/server.test.ts` (the exact seven, nine, and
690
+ ten tool lists) and `test/code-first-surface.test.ts` (the fold's construction
691
+ rules, refusals, copy, and measured size). The release audit compares the same
692
+ two shapes:
693
+
694
+ ```sh
695
+ npm --prefix eval/current-version run audit
696
+ npm --prefix eval/current-version run audit -- --executor disabled
697
+ ```