@zackbart/connecta 0.22.3 → 0.23.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.
@@ -83,30 +83,23 @@ interface ExecuteResult {
83
83
  Connecta passes exactly one provider, named `connecta`. An executor must:
84
84
 
85
85
  1. **Expose each provider as a guest global** whose properties are its `fns`,
86
- called with the program's arguments and awaited. Connecta's provider carries
87
- `search`, `describe`, `call`, `batch`, `emit`, and `__callNamespace` — see
88
- point 3.
86
+ called with the program's arguments and awaited. Connecta supplies `search`,
87
+ `describe`, `call`, and `emit`.
89
88
  2. **Evaluate `prelude` after the provider globals exist and before the
90
- program**, in a scope where those globals are reachable. It is host-authored
91
- trusted code, never model input, and skipping it is not an option: connecta's
92
- prelude is what installs the lazy connector shortcuts.
93
- 3. **Let the prelude reach the provider.** That prelude
94
- (`lazyNamespacePrelude` in `src/execute.ts`) assigns one
95
- `globalThis[<connectorId>]` Proxy per connector, each forwarding to
96
- `connecta.__callNamespace(connectorId, toolName, args)`. An executor exposing
97
- only the four documented functions leaves every shortcut dead and breaks `A2`.
98
- 4. **Marshal values as JSON** in both directions (`P3`), and reject a host call
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
99
92
  whose function is not an own property of `fns` — the guest can ask for
100
93
  anything, including inherited members.
101
- 5. **Return, never throw, for a failed program**: set `error` to the guest's
94
+ 4. **Return, never throw, for a failed program**: set `error` to the guest's
102
95
  message, leave `result` undefined. `createExecuteTool` reads `error` first and
103
96
  matches it back to the failures recorded during the run, which is how an
104
97
  uncaught tool failure keeps its type (`E1`).
105
- 6. **Capture `console.log`, `console.warn`, and `console.error`** into `logs` in
98
+ 5. **Capture `console.log`, `console.warn`, and `console.error`** into `logs` in
106
99
  call order (`R5`), bounding what it retains.
107
- 7. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`). Keep
100
+ 6. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`). Keep
108
101
  ambient capabilities within the documented and tested `P2`/`X5` boundary.
109
- 8. **Grant no ambient authority of its own.** Never back this with `eval` or
102
+ 7. **Grant no ambient authority of its own.** Never back this with `eval` or
110
103
  `node:vm`: the sandbox is a containment layer on top of connecta's boundary,
111
104
  not a replacement for it, and every capability arrives through `fns`.
112
105
 
@@ -130,14 +123,9 @@ reinterpreted, so do not rely on it.
130
123
 
131
124
  **P2.** The only capabilities in the contract are:
132
125
 
133
- - one lazy global per connector (see [Addressing](#addressing));
134
- - `connecta.search`, `connecta.describe`, `connecta.call`, `connecta.batch`;
126
+ - `connecta.search`, `connecta.describe`, `connecta.call`, `connecta.emit`;
135
127
  - `console.log`, `console.warn`, `console.error`, captured and returned.
136
128
 
137
- `connecta` also carries the `__`-prefixed dispatcher the shortcut prelude uses.
138
- It is host plumbing, callable but not contract: it takes a connector id and an
139
- unsanitized-or-sanitized tool name and can change shape without notice.
140
-
141
129
  Anything else a runtime happens to expose is outside the portable contract and
142
130
  must not be used. QuickJS grants none of it. A loader-only Dynamic Worker denies
143
131
  external egress and filesystem access and keeps its environment maps empty, but
@@ -157,49 +145,21 @@ runtime modules. Neither executor exposes `require`.
157
145
 
158
146
  ## Addressing
159
147
 
160
- **A1.** The canonical address `<connectorId>.<toolName>` — byte-for-byte what
161
- `search_tools` and `connecta.search` print is always callable through
162
- `connecta.call` and `connecta.batch`. This is never optional and never
163
- sanitized. It is what prevents sanitized-name collisions and what gives a
164
- generated program a stable escape hatch when a shortcut is ambiguous, absent, or
165
- wrong. A program that can only reach a tool through a convenience name is one
166
- rename away from broken.
167
-
168
- **A2.** Shortcut namespaces are sugar over `A1`: every connector gets one lazy
169
- global whose properties are its tools, so `<connectorId>.<toolName>(args)` works
170
- with both parts sanitized into JavaScript identifiers — characters outside
171
- `[A-Za-z0-9_$]` become `_`, a leading digit gets `_` prefixed, and a reserved
172
- word gets `_` appended (`my-service.get.thing` → `my_service.get_thing`). The
173
- globals are lazy: no catalog is fetched until a program touches one. The
174
- bounded deployment inventory in the `execute_code` description shows each
175
- canonical connector id and labels the shortcut only when it differs; the
176
- [discovery guide](./meta-tools.md#discovery-context) defines that bound. The sugar is frozen: every expansion invents a collision class `A1` already solves ([#223](https://github.com/zackbart/connecta/issues/223)).
177
-
178
- **A3.** A shortcut that resolves to more than one tool fails closed with
179
- `ambiguous_tool_alias`, naming the colliding tool names and pointing at
180
- `connecta.call`. It never picks one. The canonical addresses of both tools
181
- remain callable.
182
-
183
- **A4.** A deployment whose connector ids collide with each other after
184
- sanitization, or that sanitize onto a name the sandbox reserves, fails *every*
185
- `execute_code` request with an error naming the offending ids. Failing loudly on
186
- the deployment's mistake beats silently answering from whichever connector
187
- sorted first.
188
-
189
- **A5 (verdict: shortcut namespaces are kept, and frozen).** They cost nothing to
190
- keep, a working ergonomic surface should not be removed mid-arc, and the
191
- exploration's cold-start sample used them naturally. Frozen means no typed method
192
- lists, no per-tool closures, no generated `.d.ts`, no second sanitization rule —
193
- every expansion invents a collision class the addressing in `A1` already solves.
194
- The default has since flipped without revisiting them
195
- ([#224](https://github.com/zackbart/connecta/issues/224)), so evidence rather
196
- than a gate would take them away: if programs reach for `connecta.call` anyway,
197
- or shortcut ambiguity shows up in failures, they lose.
148
+ **A1.** A tool has one canonical address, `<connectorId>.<toolName>`, exactly
149
+ as discovery returns it. Call it with `connecta.call(address, args)`. Punctuation
150
+ is preserved; no JavaScript identifier conversion takes place.
151
+
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).
198
159
 
199
160
  ## The surface
200
161
 
201
- Four functions, all `async`, plus the host-internal `__`-prefixed dispatcher
202
- (`P2`) that is callable but not contract. Nothing else works: reading any other
162
+ Four functions, all `async`: `search`, `describe`, `call`, and `emit`. Nothing else works: reading any other
203
163
  property yields a function — the guest namespace is a Proxy, so `typeof
204
164
  connecta.toString` is `"function"` — but *calling* it fails, because the host
205
165
  resolves only own members of the provider's `fns`. A program must treat the four
@@ -220,9 +180,9 @@ const page = await connecta.search({
220
180
  });
221
181
  ```
222
182
 
223
- **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`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
183
+ **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, the configured `connectorTitle` when present (normalized whitespace, at most 120 UTF-8 bytes), and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
224
184
 
225
- **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`, 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. These filters grant no authority and change no admission decision.
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.
226
186
 
227
187
  **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`, and `outputKeys`: the same names the rendered schema shows, ready to check before building arguments. Match inputs, truncation, safety, and outputs, not lexical
228
188
  rank; search distinct operations separately and use `outputKeys`, not guessed roots. A non-object schema — a union, an array, an
@@ -276,30 +236,33 @@ when present, otherwise text content JSON-parsed when it parses and the raw text
276
236
  when it does not; a downstream result flagged `isError` throws. Omitted `args`
277
237
  is treated as `{}`.
278
238
 
279
- **S6.** Every call — canonical or shortcut — goes through the same catalog,
239
+ **S6.** Every call goes through the same catalog,
280
240
  fail-closed read-only predicate, admission, credential containment, timeout
281
241
  classification, health accounting, and activity recording as an ordinary
282
242
  meta-tool call. The sandbox is an additional containment layer, not a second
283
243
  implementation of the boundary, and nothing a program does widens what it can
284
244
  reach.
285
245
 
286
- ### connecta.batch
246
+ ### Parallel calls
247
+
248
+ **S7.** Use `Promise.all` for independent calls when any failure should fail the
249
+ program, or `Promise.allSettled` to retain every outcome in input order. Both
250
+ use the same per-call admission, host-call budget, deadlines, and activity path
251
+ as sequential calls. There is no separate batch size or result contract.
287
252
 
288
253
  ```js
289
- const outcomes = await connecta.batch([
290
- { address: "ci.get_run", args: { runId: 42 } },
291
- { address: "ci.list_jobs", args: { runId: 42 } },
254
+ const outcomes = await Promise.allSettled([
255
+ connecta.call("ci.get_run", { runId: 42 }),
256
+ connecta.call("ci.list_jobs", { runId: 42 }),
292
257
  ]);
258
+ return outcomes.map((outcome) => outcome.status === "fulfilled"
259
+ ? { ok: true, data: outcome.value }
260
+ : { ok: false, code: outcome.reason.code, message: outcome.reason.message });
293
261
  ```
294
262
 
295
- **S7.** Runs 1–10 independent calls in parallel and returns their outcomes in
296
- order. A success is `{ address, ok: true, data }`. A failure is
297
- `{ address, ok: false, error, errorDetails }`, where `error` is the message and
298
- `errorDetails` is the typed object described in [Errors](#errors) — the same two
299
- field names the host's internal batch path uses. One failing call never rejects
300
- the batch, and more than ten calls throws.
301
-
302
- **S8.** Batch and thrown failures share one vocabulary (`E1`): an entry's `errorDetails.code` and `retryable` equal the fields on the error the same call would throw. Use batch for independent concurrency, not to recover lost type.
263
+ **S8.** A rejected promise retains the caught error's `code`, `retryable`, and
264
+ `details`. Project those fields before returning; an Error object itself is
265
+ not a JSON result contract.
303
266
 
304
267
  **S9.** A successful explicitly read-only call whose provider declared no `outputSchema` passively learns one from the unwrapped result. The observation retains field names and broad JSON types only: no arguments, scalar values, raw results, code, credentials, or errors. Property names may be user-authored. Objects stay open, every field stays optional, and search or describe labels the shape `outputSchemaSource: "observed"` so a model cannot mistake runtime evidence for a provider contract. Later observations merge fields and types in a process-local 256-entry LRU; a provider declaration always wins. Inference stops at depth 6, 128 schema nodes, 48 properties per object, 32 inspected array items, and 128 UTF-8 bytes per property name; `__proto__`, `constructor`, and `prototype` names are discarded. A tool definition over 64 KiB or an observed schema over 16 KiB is ignored. An entry expires after 24 hours and carries the exact serialized tool definition, so a changed catalog entry, process restart, or Worker isolate eviction starts cold. A failed call or failed result-processing step learns nothing, and any observation failure is discarded without changing a successful call. No discovery read, timer, refresh, background job, or storage adapter executes or persists work for this cache: the result-sampling refusal in [#282](https://github.com/zackbart/connecta/issues/282) stands.
305
268
 
@@ -314,17 +277,16 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
314
277
 
315
278
  ## Errors
316
279
 
317
- **E1.** There are four error channels. Connecta failures are typed whether caught or uncaught.
280
+ **E1.** There are three error channels. Connecta failures are typed whether caught or uncaught.
318
281
 
319
282
  | Channel | Shape | Typed? |
320
283
  | --- | --- | --- |
321
284
  | A caught Connecta host failure | `Error` with `message`, `code`, `retryable`, and `details` | yes |
322
- | `connecta.batch` outcome | `{ ok: false, error, errorDetails }` | yes |
323
285
  | An uncaught **tool or discovery** failure, as the model sees it | `{ error: { code, message, retryable, … } }` with `isError` | yes |
324
286
  | Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
325
287
 
326
288
  Both executor bridges reduce a rejected host call to `new Error(message)`. Connecta restores the typed failure in a trusted prelude with a per-execution authenticated frame (`X11`), without turning the rejection into a returned value.
327
- `message` remains the human text. `code` and `retryable` are the stable branch fields; `details` is the complete host classification. This covers `call`, connector shortcuts, `search`, `describe`, `emit`, `ui`, rejected batch input, and the host-call budget.
289
+ `message` remains the human text. `code` and `retryable` are the stable branch fields; `details` is the complete host classification. This covers `call`, `search`, `describe`, `emit`, and the host-call budget.
328
290
  Program-authored errors stay untyped, and code must never parse error prose.
329
291
 
330
292
  **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program may do.
@@ -333,7 +295,6 @@ Program-authored errors stay untyped, and code must never parse error prose.
333
295
  | --- | --- | --- |
334
296
  | `unknown_address` | no connector owns the address | false |
335
297
  | `unknown_tool` | the connector has no such tool | false |
336
- | `ambiguous_tool_alias` | a shortcut matches two tools (`A3`) | false |
337
298
  | `destructive_tool_requires_approval` | the tool is not explicitly read-only | false |
338
299
  | `auth_required` | the credential is missing, expired, or rejected | false |
339
300
  | `invalid_args` | arguments or discovery bounds were rejected | false |
@@ -344,7 +305,6 @@ Program-authored errors stay untyped, and code must never parse error prose.
344
305
  | `timeout` | the per-call 15-second deadline expired | true |
345
306
  | `cancelled` | the run ended while this call was in flight (`E5`) | false |
346
307
  | `connector_call_failed` | anything else the connector threw | per message |
347
- | `batch_call_failed` | a `connecta.batch` entry connecta could not even attempt | per message |
348
308
  | `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
349
309
  | `result_processing_failed` | the result could not be prepared | per message |
350
310
  | `result_too_large` | a discovery response exceeded its byte bound | false |
@@ -370,8 +330,7 @@ guest: admission rejection (`executor_overloaded`, retryable, with
370
330
  reported to the model as an error result. One seam: a host call still in flight
371
331
  when the run is cancelled fails with `cancelled`, catchable on the way out but
372
332
  never worth acting on (`Y3`). When shutdown tears down a program that had
373
- already started, accepted blocks and UI are reported as discarded under `M4`
374
- and `U3`; a failure before execution started carries no discard fields.
333
+ already started, accepted blocks are reported as discarded under `M4`; a failure before execution started carries no discard fields.
375
334
 
376
335
  **E6.** An error the program raises itself — a `TypeError`, a call to a
377
336
  `connecta` member that is not a provider function (including an inherited one
@@ -382,7 +341,7 @@ exactly first, by containment second — so a program that *wraps* a failure's
382
341
  message in its own text still reports the underlying typed failure. Keeping the
383
342
  type beats keeping the prose.
384
343
 
385
- **E7.** `retryable` for `unknown_address`, `unknown_tool`, `ambiguous_tool_alias`, 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.
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.
386
345
 
387
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`.
388
347
 
@@ -428,7 +387,7 @@ on data nobody asked for.
428
387
 
429
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)).
430
389
 
431
- **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.
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.
432
391
 
433
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.
434
393
 
@@ -490,125 +449,13 @@ response, and `emit` resolving means "accepted," never "delivered."
490
449
  aggregate — count and serialized bytes, numbers only (`R8`), present only
491
450
  when something was emitted.
492
451
 
493
- ## Rendered output
494
-
495
- `connecta.emit` gave programs pixels; it did not give them a *view*. This is the
496
- one the human looks at directly while the model keeps its cheap textual summary:
497
- one MCP Apps view per successful run, assembled where composition already
498
- happens. Programs supply HTML content and nothing else — the only `ui://` URI in
499
- the system is connecta's build-time shell, so nothing a client could dereference
500
- is derived from anything a program said. The argument, the refused shapes, and
501
- the security posture live in the [design record](https://github.com/zackbart/connecta/blob/main/records/mcp-ui-design.md)
502
- ([#266](https://github.com/zackbart/connecta/issues/266),
503
- [#277](https://github.com/zackbart/connecta/issues/277)); this section is the
504
- contract, and it wins where the two disagree.
505
-
506
- **U1.** `connecta.ui(html)` accepts exactly one non-empty HTML string. There is
507
- no options parameter, read manifest, or sugar form. Every other shape throws
508
- catchably and accepts nothing.
509
-
510
- **U2.** At most one payload per run. A second call throws catchably, naming the
511
- constraint; the first accepted payload stands. One tool result renders one view,
512
- and last-wins would silently discard a payload the program deliberately
513
- supplied.
514
-
515
- **U3.** Delivered on success only, and out of model context: the tool result
516
- gains `_meta["connecta/ui"] = { html }` and the JSON envelope gains `ui: true`,
517
- so the model learns a view rendered without seeing its bytes. `structuredContent`
518
- stays the envelope alone. The single-label `connecta/ui` prefix is deliberate —
519
- connecta has no domain to reverse, and fabricating one to satisfy MCP's
520
- reverse-DNS SHOULD would be a worse answer than the shape the key format's MUST
521
- already permits. A program that never calls `connecta.ui` produces the
522
- byte-for-byte ordinary response (`R6`). A failed program delivers nothing and
523
- reports `uiDiscarded: true` *only* when a payload had been accepted — a field on
524
- the structured envelope, a trailing line on the plain-text paths — coexisting
525
- with `emittedDiscarded: N` when one failure discards both.
526
-
527
- **U4.** The payload spends the aggregate emit byte budget
528
- (`ConnectaConfig.execute.maxEmittedBytes`), measured at the call as the
529
- serialized bytes of `{ html }` — `M5`'s measurement. Over budget throws
530
- catchably, naming the budget and the room remaining, with nothing partially
531
- accepted. It spends no block count (`maxEmittedBlocks`: it is not a block) and no
532
- host-call budget (`L4`). One transport bound covers everything rich a program
533
- delivers.
534
-
535
- **U5.** One static shell: a connecta-authored HTML5 document at
536
- `ui://connecta/program-ui/v3`, mimeType `text/html;profile=mcp-app`, declared on
537
- `execute_code` via `_meta.ui.resourceUri` together with an explicit
538
- `_meta.ui.visibility: ["model"]`. The other six tools declare the same
539
- model-only visibility without a resource URI. Omission defaults to model and
540
- app visibility, which would let a display-only view call them. A
541
- `resources/read` handler answers exactly that URI and fails on any other;
542
- `resources/list` is served and returns an empty list.
543
- The version segment bumps whenever the shell's bytes change, because hosts cache
544
- templates by URI.
545
-
546
- **U6.** The shell renders the payload in a nested iframe
547
- (`srcdoc`, `sandbox="allow-scripts"`, no `allow-same-origin`) and declares no CSP
548
- domains, so the host applies its restrictive default and the `about:srcdoc` frame
549
- inherits `default-src 'none'; connect-src 'none'`. The shell offers no direct
550
- network, tool calls, discovery, conversation messages, writes, or links. It
551
- participates in the Apps lifecycle — initialize, tool-result, size-changed,
552
- resource-teardown — and forwards no channel whatsoever from the inner frame to
553
- the host. That isolation makes
554
- program views fixed-height by construction: with no bridge there is no
555
- content-height signal, the shell reports only its own box, and content taller
556
- than that scrolls inside the inner frame rather than growing the view.
557
-
558
- **U7.** Structural executor parity, per `M8`: `connecta.ui` is a provider
559
- function, `ExecuteResult` and the `Executor` interface are unchanged, and both
560
- executors get it through the bridge they already have.
561
-
562
- **U8.** Request-local and unstreamed, per `M9`. The payload exists only in the
563
- finished response, and `connecta.ui` resolving means "accepted," never
564
- "rendered."
565
-
566
- **U9.** `diagnostics: true` adds a distinct `ui` aggregate — the payload's byte
567
- size, a number and nothing else (`R8`), present only when a payload was accepted.
568
- UI bytes are not folded into `emitted`: that aggregate pairs a block count with
569
- the bytes those blocks cost, and bytes without a block would desync the pair.
570
-
571
- **U10.** `_meta.ui.resourceUri` is declared unconditionally. A host without the
572
- extension ignores unknown `_meta` and sees the ordinary envelope, which *is* the
573
- text fallback the Apps spec mandates; `connecta.ui` never fails because a client
574
- cannot render. A stateless aggregator cannot reliably know, and connecta is not a
575
- nanny.
576
-
577
- **U11.** connecta declares `io.modelcontextprotocol/ui` in its server capability
578
- declaration, and that is the one extension it advertises. The Apps extension must
579
- be explicitly negotiated and a conforming client acts on one only when both sides
580
- declare it, so without this declaration no host reads `_meta.ui.resourceUri`, no
581
- host fetches the shell, and the whole design is inert. Reading the *client's*
582
- declaration in order to register tool metadata conditionally stays refused
583
- (`U10`), knowingly against a spec SHOULD.
584
-
585
- **U12.** The return value, not the view, is what the model reads. `U3` puts the
586
- view out of model context, so a program that renders one also returns the summary
587
- the model should reason over, built from the same variables the initial view renders — a
588
- view the return value does not mirror is a view nobody in the loop can check.
589
- This binds program authors and nothing else: connecta never reads the HTML, diffs
590
- it against the return, or enforces the correspondence. A heuristic there would be
591
- the same mistake as automatic host-side projection, refused in `ethos.md`
592
- ([#282](https://github.com/zackbart/connecta/issues/282)).
593
-
594
- **U13.** The always-loaded MCP instructions locate `connecta.ui(html)` before an
595
- agent chooses a route: it exists only inside `execute_code`, never in connector
596
- search, and carries `U12`'s mirrored-return duty. The detailed call, budget,
597
- and repair rules live in the on-demand `usage` skill. The location
598
- distinction rides `initialize`, under a 1,000-character ceiling for the complete
599
- instructions string. This promotes existing contract, not capability: the
600
- seven-tool surface, guest API, catalog, Apps delivery, and runtime do not change
601
- ([#286](https://github.com/zackbart/connecta/issues/286),
602
- [#418](https://github.com/zackbart/connecta/issues/418)).
603
-
604
452
  ## Retry semantics
605
453
 
606
- **Y1.** Connecta retries nothing beneath a program. `call_tool` accepts an
607
- annotation-gated `maxRetries`; code mode fixes it at zero, so one
608
- `connecta.call` is exactly one downstream attempt. The program is the retry
609
- loop, and its budget is visible to it (`L4`).
454
+ **Y1.** Connecta makes one downstream attempt per admitted call, both inside a
455
+ 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.
610
457
 
611
- **Y2.** A program may retry a caught failure whose `retryable` is true, or a batch failure whose `errorDetails.retryable` is true (`S8`). Every attempt spends host-call budget, so an unchecked loop converts a transient failure into `budget_exceeded`.
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`.
612
459
 
613
460
  **Y3.** What must never be retried automatically:
614
461
 
@@ -620,10 +467,9 @@ loop, and its budget is visible to it (`L4`).
620
467
  re-issue with `retryAfterMs` in hand.
621
468
  - a cancelled or timed-out *execution*: it is already over (`L1`).
622
469
 
623
- **Y4.** Connecta's own retry machinery beneath the meta-tools honours a
624
- connector-reported `Retry-After` exactly or not at all, and declines windows
625
- longer than 10 seconds rather than shortening them. A program sees the window
626
- verbatim as `err.details.retryAfterMs` or `errorDetails.retryAfterMs`.
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
472
+ admission decision.
627
473
 
628
474
  ## Cancellation and limits
629
475
 
@@ -646,7 +492,6 @@ because connecta enforces them above the sandbox:
646
492
  | Bound | Value |
647
493
  | --- | --- |
648
494
  | Host calls per execution | 20 |
649
- | Calls per `connecta.batch` | 10 |
650
495
  | Deadline per host call | 15 s |
651
496
  | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
652
497
  | `describe` addresses | ≤ 100 |
@@ -669,7 +514,7 @@ code safe to run at all.
669
514
  bounded — QuickJS caps both at 256 KiB (`X10`) — and exceeding either fails that
670
515
  call, not the execution, so a program can catch it and ask for less. The failure
671
516
  is executor-owned untyped text, not a Connecta host failure (`E1`). An over-bound *result* names the address the program
672
- called, not the internal dispatcher behind the shortcut namespaces; an over-bound
517
+ called, rather than only the generic bridge function; an over-bound
673
518
  *argument* payload is refused before it is parsed, so it names no address at
674
519
  all — parsing it to write a better message would spend exactly the work the bound
675
520
  exists to refuse.
@@ -680,8 +525,7 @@ carrying `retryAfterMs`; cancellation and shutdown are terminal. Admission happe
680
525
  *before* any catalog or provider is built, so a queued request holds no state.
681
526
 
682
527
  **L8.** Bounds are deployment configuration, not program inputs: a program cannot
683
- raise one by asking. `execute_code`'s description states the host-call budget, the
684
- batch maximum, and the per-call deadline — the ones a program must plan around
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
685
529
  before it runs. The result and log caps live here and in the truncation notice
686
530
  itself (`R2`, `R5`).
687
531
 
@@ -689,7 +533,7 @@ itself (`R2`, `R5`).
689
533
 
690
534
  **V1.** One payload-free activity event per attempted call, with
691
535
  `source: "execute_code"` — every dispatched call plus every local refusal: a
692
- read-only refusal, an unknown tool, an ambiguous shortcut, an unloadable
536
+ read-only refusal, an unknown tool, an unloadable
693
537
  catalog, a missing credential, an exhausted host-call budget, an address no
694
538
  connector owns. Ten tools called is ten events, as legible as ten `call_tool`
695
539
  calls — which makes moving work into the sandbox an optimization, not a blindfold.
@@ -705,7 +549,7 @@ return is refused paging by design rather than truncated into friction. There is
705
549
  nowhere to put arguments, results, program source, or
706
550
  raw error text; a caught failure is still recorded. `address` is
707
551
  canonical (`A1`) where a tool resolved, otherwise the name the program used —
708
- for a shortcut its sanitized alias, the honest record of what was attempted.
552
+ the honest record of what was attempted.
709
553
 
710
554
  **V3.** A call whose connector does not exist is recorded at the address as
711
555
  written, *provided* it split into the two fields activity keeps — one with no
@@ -785,37 +629,10 @@ The human message is unchanged; a mismatched frame is ordinary untyped prose.
785
629
 
786
630
  ## Changes from earlier code mode
787
631
 
788
- Six behaviors changed with this contract, matching the 0.10.0 release notes.
789
- Programs that ran before still run.
790
-
791
- - **Caught Connecta failures expose their classification** (`E1`, `X11`). Their human message and thrown semantics stay unchanged; `code`, `retryable`, and `details` are additive.
792
-
793
- - **`connecta.batch` failures gained `errorDetails`** (`S7`). They carried only a
794
- message, which left a program unable to tell a policy refusal from a transient
795
- failure. Additive, and it reuses the host's internal batch field names, so a
796
- program and the host describe a failed call the same way.
797
- - **A policy refusal can no longer look retryable** (`E7`). Pinned in code rather
798
- than read out of message text, so a connector named `svc-503` stops flipping a
799
- permanent refusal to `retryable: true`. This reaches the call tools too.
800
- - **An uncaught discovery-bound failure is typed** (`S3`): `invalid_args` or
801
- `result_too_large` rather than prose, the same envelope a failed call gets.
802
- - **A bridge-bound failure names the address** (`L6`), not the internal
803
- dispatcher every shortcut namespace shares.
804
- - **An oversized result is truncated once** (`R2`). The envelope is sized so its
805
- *serialized* form fits the cap; the QuickJS path previously truncated in the
806
- child and again in the parent, reporting the inner envelope's length as
807
- `totalChars`. Previews are shorter now; `totalChars` is the real size.
808
-
809
- The middle three were places where the contract described behavior the code did
810
- not quite have. The code moved, because the described behavior is the one worth
811
- having.
812
-
813
- Two surfaces were added since, both additive by construction and each with its
814
- byte-for-byte no-call promise pinned by test:
815
- [emitted output](#emitted-output) (`M1`–`M10`,
816
- [#270](https://github.com/zackbart/connecta/issues/270)) and
817
- [rendered output](#rendered-output) (`U1`–`U12`,
818
- [#277](https://github.com/zackbart/connecta/issues/277)).
632
+ MCP Apps rendering, connector shortcut globals, and `connecta.batch` are
633
+ removed. Direct calls also lose automatic retries. The seven top-level tools,
634
+ read-only boundary, JSON projection, and emitted media remain. See the
635
+ [migration guide](./upgrading.md#0230-program-api-pruning).
819
636
 
820
637
  ## Verification
821
638
 
@@ -835,19 +652,16 @@ the upstream `Executor` shape assignable.
835
652
  | `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) |
836
653
  | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
837
654
  | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
838
- | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (sanitizing), `test/server.test.ts` (bounded live connector inventory) |
839
- | `A3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (colliding alias) |
840
- | `A4` | `test/execute.test.ts` (namespace collisions, reserved namespace) |
841
- | `A5` | verdict; `A1`–`A3` are its enforcement |
655
+ | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (canonical addressing), `test/server.test.ts` (bounded live connector inventory) |
842
656
  | `S1`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, and the unfiltered browse that replaces `list_connectors`), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools.test.ts` (mixed complete/partial ranking and stable pagination) |
843
657
  | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
844
658
  | `S4` | both guest-contract executors (ordered mixed describe results with unknown-address, unknown-tool suggestion, and catalog-failure details), `test/meta-tools.test.ts` (top-level routing, no-suggestion, catalog-failure, and hostile-input bounds) |
845
659
  | `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
846
660
  | `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
847
- | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
848
- | `S8`, `E1`, `X11` | both guest-contract executors (caught call, namespace, discovery, utility, batch-validation, budget, and forgery cases; typed batch equivalence) |
661
+ | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (parallel calls and shared admission) |
662
+ | `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections) |
849
663
  | `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, and failure isolation) |
850
- | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, batch, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, batch, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
664
+ | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
851
665
  | `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
852
666
  | `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
853
667
  | `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) |
@@ -860,7 +674,7 @@ the upstream `Executor` shape assignable.
860
674
  | `R6`–`R8` | `test/guest-api-contract.test.ts` (normal result keys), `test/execute.test.ts` (opt-in operation aggregates, failure paths, payload exclusion) |
861
675
  | `Y1` | `test/guest-api-contract.test.ts` (one attempt per call) |
862
676
  | `Y2`, `Y3` | `test/guest-api-contract.test.ts` (retryable flags by code) |
863
- | `Y4` | `test/meta-tools.test.ts` (`retryBackoffMs`, `MAX_RETRY_BACKOFF_MS`) |
677
+ | `Y4` | `test/meta-tools-call.test.ts`, `test/call-admission.test.ts` (one attempt, retry hints, caller reissue) |
864
678
  | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls) |
865
679
  | `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors) |
866
680
  | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (budgets) |
@@ -875,15 +689,6 @@ the upstream `Executor` shape assignable.
875
689
  | `M6`, `M9` | verdicts; `M1`'s strict typing and `M2`'s collect-then-deliver are their enforcement |
876
690
  | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
877
691
  | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
878
- | `U1`, `U2` | `test/guest-api-contract.test.ts` (invalid and repeated calls throw catchably, first payload stands), `test/execute-ui.test.ts` (every rejected shape) |
879
- | `U3` | `test/guest-api-contract.test.ts` (`_meta` payload and `ui: true`, identical on both executors), `test/execute-ui.test.ts` (`structuredContent`, byte-for-byte no-call path, discard structured and plain, coexistence with `emittedDiscarded`), `test/quickjs-executor.test.ts` (mid-run shutdown) |
880
- | `U4` | `test/execute-ui.test.ts` (one shared byte aggregate crossed in either order; block count and host-call budget untouched) |
881
- | `U5`, `U10`, `U11` | `test/server.test.ts` (the shell URI, mimeType, and body; every other URI fails; empty listing; exact model-only `_meta.ui` on all seven tools; exactly one declared extension) |
882
- | `U6` | `test/execute-ui.test.ts` (valid HTML5, `srcdoc` and sandbox attributes, no `allow-same-origin`, no path from the inner frame to the host) |
883
- | `U7`, `U8` | two arms passing one case table, `test/codemode-compat.test.ts` |
884
- | `U9` | `test/execute-ui.test.ts` (a `ui` byte aggregate distinct from `emitted`, absent when nothing was accepted) |
885
- | `U12` | `test/server.test.ts` (the `connecta.ui` bullet carries the return-value clause); a duty on program authors, so the description is the only place it can be enforced |
886
- | `U13` | `test/code-first-surface.test.ts`, `test/server.test.ts` (served `initialize.instructions` locate UI inside `execute_code`, exclude it from connector search, state the mirrored-return duty, and stay within the complete 1,000-character budget; the usage skill carries detailed call rules) |
887
692
  | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
888
693
  | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
889
694
  | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
@@ -21,6 +21,10 @@ produce ordinary `Connector` instances and pass through the same catalog,
21
21
  read-only admission, credentials, storage, invocation, result-size, and
22
22
  activity paths.
23
23
 
24
+ Custom HTTP routes belong to the deployment fetch handler. Connectors expose
25
+ tools and the documented OAuth hooks; a removed `handleRequest` declaration
26
+ refuses construction.
27
+
24
28
  Connector instances are deployment configuration. They are not registered or
25
29
  reconfigured at runtime. Request-local clients, transports, abort signals, and
26
30
  catalogs must be released with the request that created them.
@@ -292,9 +296,8 @@ Connecta deliberately sits between protocol generations
292
296
  five-minute fingerprinted catalog cache; that remains gated in
293
297
  [#206](https://github.com/zackbart/connecta/issues/206).
294
298
  - **Multi-round-trip results:** a downstream `input_required` result becomes a
295
- non-retryable `input_required_unsupported` failure. `call_tool`, the
296
- `execute_code` host bridge and internal batch path both preserve the
297
- structured code. Relaying the
299
+ non-retryable `input_required_unsupported` failure. `call_tool` and the
300
+ `execute_code` host bridge preserve the structured code. Relaying the
298
301
  opaque `requestState` is architecturally possible but gated until real hosts
299
302
  and downstreams adopt it.
300
303
 
@@ -320,8 +323,7 @@ freshness. The operator page reports whether the last agent read in this runtime
320
323
  was fresh or stale; this payload-free timestamp is not persisted. No timer or
321
324
  idle warmup originates downstream traffic.
322
325
 
323
- Tool calls must use the shared invocation path. That keeps direct calls, batch
324
- children, and code-mode host calls aligned on safety, retries, admission,
326
+ Tool calls must use the shared invocation path. That keeps direct calls and code-mode host calls aligned on safety, retry hints, admission,
325
327
  timeouts, validation, result guards, and typed failures.
326
328
 
327
329
  That path also learns an observed output schema after a successful explicitly