@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.
- package/AGENTS.md +113 -0
- package/CHANGELOG.md +83 -0
- package/README.md +62 -12
- package/bin/connecta.mjs +272 -0
- package/dist/access-tokens.d.ts +32 -0
- package/dist/access-tokens.d.ts.map +1 -0
- package/dist/access-tokens.js +225 -0
- package/dist/access-tokens.js.map +1 -0
- package/dist/catalog-service.d.ts +39 -1
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +133 -11
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +113 -13
- package/dist/catalog.js.map +1 -1
- package/dist/execute.d.ts +45 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +265 -68
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +1 -5
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +1 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +410 -12
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +2 -0
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/routes/access-tokens.d.ts +7 -0
- package/dist/routes/access-tokens.d.ts.map +1 -0
- package/dist/routes/access-tokens.js +84 -0
- package/dist/routes/access-tokens.js.map +1 -0
- package/dist/routes/shared.d.ts +3 -0
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +9 -1
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +5 -0
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +8 -0
- package/dist/storage/memory.js.map +1 -1
- package/dist/tool-safety.d.ts +10 -0
- package/dist/tool-safety.d.ts.map +1 -0
- package/dist/tool-safety.js +12 -0
- package/dist/tool-safety.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +44 -1
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -0
- package/documentation/auth.md +58 -0
- package/documentation/call-admission.md +7 -0
- package/documentation/code-first-exploration.md +292 -0
- package/documentation/code-mode.md +697 -0
- package/documentation/connector-guides.md +7 -0
- package/documentation/connectors.md +63 -0
- package/documentation/mcp-2026-07-28.md +46 -0
- package/documentation/meta-tools.md +167 -0
- package/documentation/operations.md +7 -0
- package/documentation/operator-ui.md +7 -0
- package/documentation/request-admission.md +7 -0
- package/documentation/storage-and-credentials.md +54 -0
- package/ethos.md +132 -0
- package/examples/node/README.md +53 -0
- package/examples/node/src/index.ts +73 -0
- package/examples/worker/README.md +160 -0
- package/examples/worker/src/cloudflare-kv.ts +43 -0
- package/examples/worker/src/d1-activity-row.ts +100 -0
- package/examples/worker/src/d1-activity.ts +144 -0
- package/examples/worker/src/index.ts +136 -0
- package/examples/worker/wrangler.jsonc +26 -0
- package/package.json +11 -1
- package/src/access-tokens.ts +289 -0
- package/src/catalog-service.ts +177 -15
- package/src/catalog.ts +143 -12
- package/src/execute.ts +372 -96
- package/src/index.ts +33 -1
- package/src/invocation.ts +1 -8
- package/src/meta-tools.ts +504 -11
- package/src/operator-ui/browser.css +63 -0
- package/src/operator-ui/browser.ts +288 -2
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +6 -0
- package/src/routes/access-tokens.ts +115 -0
- package/src/routes/shared.ts +3 -0
- package/src/routes/ui.ts +9 -0
- package/src/server.ts +5 -0
- package/src/skills.ts +1 -1
- package/src/storage/file.ts +5 -0
- package/src/storage/memory.ts +8 -0
- package/src/tool-safety.ts +15 -0
- package/src/types.ts +5 -0
- package/src/ui.ts +50 -1
- package/src/version.ts +1 -1
- package/templates/node/.env.example +5 -0
- package/templates/node/AGENTS.md +19 -0
- package/templates/node/README.md +33 -0
- package/templates/node/package.json +23 -0
- package/templates/node/src/index.ts +43 -0
- 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
|
+
```
|