@zackbart/connecta 0.10.5 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts +11 -1
  6. package/dist/activity.d.ts.map +1 -1
  7. package/dist/activity.js +44 -3
  8. package/dist/activity.js.map +1 -1
  9. package/dist/catalog-service.d.ts +24 -0
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +68 -9
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/connectors/api.d.ts +2 -2
  14. package/dist/connectors/remote-mcp.d.ts +1 -1
  15. package/dist/errors.d.ts +49 -4
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +68 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/execute.d.ts +73 -3
  20. package/dist/execute.d.ts.map +1 -1
  21. package/dist/execute.js +161 -29
  22. package/dist/execute.js.map +1 -1
  23. package/dist/index.d.ts +28 -30
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +29 -37
  26. package/dist/index.js.map +1 -1
  27. package/dist/invocation.d.ts +9 -2
  28. package/dist/invocation.d.ts.map +1 -1
  29. package/dist/invocation.js +61 -31
  30. package/dist/invocation.js.map +1 -1
  31. package/dist/meta-tools.d.ts +24 -59
  32. package/dist/meta-tools.d.ts.map +1 -1
  33. package/dist/meta-tools.js +107 -359
  34. package/dist/meta-tools.js.map +1 -1
  35. package/dist/operator-ui/generated.d.ts +1 -1
  36. package/dist/operator-ui/generated.d.ts.map +1 -1
  37. package/dist/operator-ui/generated.js +1 -1
  38. package/dist/operator-ui/generated.js.map +1 -1
  39. package/dist/registry.d.ts +12 -10
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +8 -17
  42. package/dist/registry.js.map +1 -1
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +19 -21
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/shared.d.ts +9 -11
  47. package/dist/routes/shared.d.ts.map +1 -1
  48. package/dist/routes/shared.js.map +1 -1
  49. package/dist/server.js +5 -4
  50. package/dist/server.js.map +1 -1
  51. package/dist/skills.d.ts +8 -18
  52. package/dist/skills.d.ts.map +1 -1
  53. package/dist/skills.js +13 -60
  54. package/dist/skills.js.map +1 -1
  55. package/dist/types.d.ts +6 -20
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/documentation/code-first-exploration.md +16 -16
  60. package/documentation/code-mode.md +137 -63
  61. package/documentation/connectors.md +1 -1
  62. package/documentation/meta-tools.md +96 -33
  63. package/documentation/rich-output-design.md +212 -0
  64. package/ethos.md +17 -19
  65. package/examples/node/README.md +1 -2
  66. package/examples/node/src/index.ts +1 -3
  67. package/examples/worker/README.md +19 -16
  68. package/examples/worker/src/d1-activity-row.ts +40 -0
  69. package/examples/worker/src/d1-activity.ts +3 -2
  70. package/examples/worker/src/index.ts +6 -14
  71. package/examples/worker/wrangler.jsonc +3 -6
  72. package/package.json +1 -1
  73. package/src/activity.ts +69 -3
  74. package/src/catalog-service.ts +113 -20
  75. package/src/connectors/api.ts +2 -2
  76. package/src/connectors/remote-mcp.ts +1 -1
  77. package/src/errors.ts +104 -3
  78. package/src/execute.ts +237 -37
  79. package/src/index.ts +60 -67
  80. package/src/invocation.ts +61 -19
  81. package/src/meta-tools.ts +136 -482
  82. package/src/operator-ui/browser.ts +10 -2
  83. package/src/operator-ui/generated.ts +1 -1
  84. package/src/registry.ts +7 -35
  85. package/src/routes/mcp.ts +19 -21
  86. package/src/routes/shared.ts +8 -11
  87. package/src/server.ts +7 -7
  88. package/src/skills.ts +11 -74
  89. package/src/types.ts +6 -21
  90. package/src/version.ts +1 -1
  91. package/templates/node/README.md +2 -1
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +1 -1
@@ -0,0 +1,212 @@
1
+ # Rich MCP output from programs — design record
2
+
3
+ The decision record for [#267](https://github.com/zackbart/connecta/issues/267):
4
+ whether and how an `execute_code` program may deliver MCP-native output — text,
5
+ images, audio — instead of only a single JSON return value. The verdicts live in
6
+ [`ethos.md`](../ethos.md); this document carries the argument, the contract
7
+ precisely enough to implement, and the shapes that were considered and refused.
8
+ The implementation landed via
9
+ [#270](https://github.com/zackbart/connecta/issues/270): the contract clauses
10
+ below (`M1`–`M10`) are now normative in [`code-mode.md`](./code-mode.md)'s
11
+ "Emitted output" section, which wins where the two disagree. This document
12
+ remains the argument and the record of rejected shapes.
13
+
14
+ ## The problem
15
+
16
+ A program returns one JSON value, bounded at 24,000 serialized characters
17
+ (`R2`). That is the right contract for data: compose, project, return only what
18
+ you need. It is no contract at all for an image. Base64 is not projectable — a
19
+ program cannot "return less" of a screenshot — so any rich block a downstream
20
+ tool produces dies at the exit guard, converted to a truncation envelope whose
21
+ preview is the head of a base64 string, which is of use to no one.
22
+
23
+ The asymmetry is already visible in the direct-call surface. `call_tool` forwards
24
+ non-text downstream blocks to the client untouched when they fit the result cap
25
+ (the fix for issue #43 made the guard measure every block, then pass them
26
+ through in original order). And on the intake side, a host call **inside** a
27
+ program is not size-capped at all: `unwrapMcpResult` hands mixed content
28
+ through as a raw object, base64 and all. So today a downstream image survives
29
+ the trip *into* the sandbox and survives the explicit tool boundary — the one
30
+ place it cannot survive is the exit of the surface connecta calls primary.
31
+
32
+ ## The shape: `connecta.emit(block)`
33
+
34
+ One new provider function. A program emits zero or more validated MCP content
35
+ blocks during its run; the host collects them request-locally and appends them
36
+ to the final `execute_code` result, after the JSON envelope, in emission order.
37
+
38
+ The single most load-bearing fact about this shape: **it does not touch the
39
+ executor contract.** `Executor.execute()` still returns
40
+ `{ result, error?, logs? }`, which is what keeps the interface structurally
41
+ compatible with `DynamicWorkerExecutor` from `@cloudflare/codemode` — a class
42
+ connecta does not control and must not fork. `emit` is a provider function like
43
+ `connecta.call`: both executors already bridge provider calls (QuickJS over
44
+ child-process IPC, the Dynamic Worker over isolate RPC), so emitted bytes cross
45
+ the guest boundary exactly once, as an argument, and are never seen again by
46
+ the sandbox, the `ExecuteResult`, or the `R2` guard. Executor parity is not a
47
+ test obligation bolted on afterward; it is structural. A third-party executor
48
+ that correctly bridges provider functions gets emission without writing a line.
49
+
50
+ ### Contract (drafts for code-mode.md)
51
+
52
+ **M1.** `connecta.emit(block)` accepts exactly one MCP content block of type
53
+ `text`, `image`, or `audio`:
54
+
55
+ ```
56
+ { type: "text", text: string }
57
+ { type: "image", data: string /* base64 */, mimeType: string }
58
+ { type: "audio", data: string /* base64 */, mimeType: string }
59
+ ```
60
+
61
+ The shape is validated strictly at the call — required fields present, no
62
+ extra fields, no `annotations`, no `_meta`. An invalid block throws a
63
+ catchable error and nothing is accepted. Rejected, not stripped: silently
64
+ deleting fields would deliver something the program did not ask to emit.
65
+ There is no sugar form (`emit("text")` is invalid); sugar is how a
66
+ one-shape contract grows hair.
67
+
68
+ **M2.** Emitted blocks are collected on the host in emission order and
69
+ delivered only with a successful result: the response's `content` array is the
70
+ JSON envelope text block first, then the emitted blocks. When at least one
71
+ block was emitted the envelope gains `emitted: N`; when none were, the
72
+ response is byte-for-byte the ordinary path (the `R6` discipline).
73
+ `structuredContent` remains the JSON envelope alone — emission is a
74
+ presentation channel, not a second data channel.
75
+
76
+ **M3.** The return value and emission are independent. `R2` applies to the
77
+ return value exactly as before and never measures emitted bytes; a truncated
78
+ return value does not suppress emitted blocks, and emitted blocks do not
79
+ shrink the return budget.
80
+
81
+ **M4.** A program that ends in an error delivers no emitted blocks. The error
82
+ envelope reports `emittedDiscarded: N` when N > 0, so the discard is visible
83
+ rather than silent. Partial rich output from a failed program is ambiguity,
84
+ not a deliverable.
85
+
86
+ **M5.** Two budgets, both deployment-configurable, both failing loudly at the
87
+ `emit` call: an aggregate serialized-byte budget (default 4,000,000 bytes) and
88
+ a block count (default 32). An emit that would exceed either throws a
89
+ catchable error naming the budget and the room remaining; the block is not
90
+ partially accepted, and prior accepted blocks are unaffected. There is no
91
+ `get_result` stash and no paging: the program learns it is over budget while
92
+ it can still choose differently, which is `R4`'s spirit applied to bytes that
93
+ genuinely cannot be shrunk — refuse at the door instead of paging after the
94
+ fact.
95
+
96
+ **M6.** Connecta claims no provenance. Every emitted block is program output,
97
+ trusted exactly as much as the program's return value — no more because its
98
+ bytes happen to have come from a downstream call, no less because they were
99
+ assembled in the sandbox. Preserving a downstream image means re-emitting it:
100
+ the program selects the block from the raw downstream result (which reaches it
101
+ uncapped) and emits it. The host attaches no attribution.
102
+
103
+ **M7.** `emit` does not spend the host-call budget (`L4`). Its bounds are
104
+ `M5`'s and only `M5`'s.
105
+
106
+ **M8.** `ExecuteResult` is unchanged. Neither QuickJS, Dynamic Worker, nor a
107
+ third-party executor needs modification, and the parity suite runs the same
108
+ emitting program through both vitest projects and asserts identical delivered
109
+ content.
110
+
111
+ **M9.** Emission is request-local and unstreamed. Blocks exist only in the
112
+ finished response; nothing is pushed early, nothing survives the request, and
113
+ `emit` resolving means "accepted into the collection," never "delivered."
114
+
115
+ **M10.** Activity remains payload-free by construction. With
116
+ `diagnostics: true` the diagnostics block gains one aggregate — emitted block
117
+ count and serialized bytes — numbers only, per `R8`.
118
+
119
+ ### Intake, specified
120
+
121
+ `unwrapMcpResult`'s current behavior becomes contract rather than accident: an
122
+ all-text downstream result is JSON-parsed when possible (as today), and a
123
+ result carrying non-text blocks passes through as the raw object, `content`
124
+ array intact, uncapped. That raw fallthrough is the preservation path `M6`
125
+ depends on. Bounding it would kill re-emission at intake; projecting it would
126
+ repeat the mistake `R1` refuses.
127
+
128
+ ## Sizing rationale
129
+
130
+ The 24,000-character return boundary is a *context* budget — the return value
131
+ lands in the model's window as text. Emitted image and audio blocks do not:
132
+ MCP-aware hosts deliver them as media, which models ingest at media prices,
133
+ not base64-text prices. So the emission budget is a *transport* bound, not a
134
+ context bound, and 4,000,000 serialized bytes (roughly a 3 MB binary after
135
+ base64's 4/3 inflation — two or three real screenshots) is deliberately far
136
+ above `R2` without being a file-hosting ambition. Deployments that know their
137
+ client's limits tune it; the default just has to make `emit` useful for the
138
+ motivating case without inviting anyone to ship video through an MCP response.
139
+
140
+ ## Security posture
141
+
142
+ - **Nothing is minted.** `emit` grants no authority: it cannot cause a fetch,
143
+ reference a credential, or make the host serve anything. It moves bytes the
144
+ program already had into the response.
145
+ - **Strict typing is the lure defense.** The block validator accepting only
146
+ `text`, `image`, and `audio` is what keeps a program from emitting a
147
+ `resource_link` whose URI a helpful client might dereference. The refused
148
+ types are refused precisely because they are pointers, and pointers get
149
+ followed.
150
+ - **Image-borne injection is the existing class.** A downstream screenshot
151
+ containing hostile text is the same hazard through `emit` as through
152
+ `call_tool`'s block passthrough today; connecta's posture is unchanged — all
153
+ tool output is untrusted input to the client, and connecta adds no claim
154
+ otherwise.
155
+
156
+ ## Considered and refused
157
+
158
+ **A sentinel return shape** (`return { $mcpContent: [...] }`). Overloads the
159
+ one data channel with a magic key that collides with honest data, subjects
160
+ rich blocks to the `R2` guard they cannot survive, and turns "what did this
161
+ program return" into a parse question. The return value stays a value.
162
+
163
+ **Widening `ExecuteResult` or the `Executor` interface.** The obvious place
164
+ and the wrong one: the interface's value is that `@cloudflare/codemode`'s
165
+ executor already satisfies it. A `content` field connecta added would either
166
+ fork the Workers executor or wait on a vendor; and every third-party executor
167
+ would need matching surgery. Refused as a class, not just deferred.
168
+
169
+ **Provenance-preserving handles** (downstream blocks stay host-side; the guest
170
+ gets `{ $ref }` tokens to pass to `emit`). Honest attribution, but it creates
171
+ host-side object identity that generated code holds references to —
172
+ capability-shaped machinery in the one place the ethos says generated code
173
+ mints nothing. And the label buys nothing: no MCP client trusts a tool result
174
+ more because an aggregator vouches for its lineage. Re-emission delivers the
175
+ same bytes with a simpler story: everything a program emits is program output.
176
+
177
+ **`resource` and `resource_link` emission.** Connecta already refuses to
178
+ aggregate resources; letting programs emit them would introduce through the
179
+ back door a surface the front door refused, and a guest-minted URI is a lure
180
+ (see the posture above). A future argument would have to be a new one.
181
+
182
+ **A `get_result` stash for over-budget emissions.** Paging exists for
183
+ downstream payloads a model could not shrink; an over-budget emission is a
184
+ program decision that has not happened yet. Failing the `emit` call while the
185
+ program can still adapt beats stashing megabytes nobody may page.
186
+
187
+ **Streaming or partial delivery.** Standing invariant — no server push,
188
+ nothing request-bound survives the request. Emission is collect-then-deliver
189
+ by construction (`M9`).
190
+
191
+ **Delivering emissions from failed programs.** The blocks may describe a world
192
+ the error contradicts. Failure delivers the error, the logs, and an honest
193
+ count of what was discarded (`M4`).
194
+
195
+ ## Verification sketch
196
+
197
+ - Parity: one emitting program, both vitest projects (`WORKERS_SUITES`),
198
+ identical delivered content arrays.
199
+ - Validation: each rejected shape (missing field, extra field, `annotations`,
200
+ unknown type, bare string) throws catchably and accepts nothing.
201
+ - Budgets: byte and count budgets fail at the crossing call, prior blocks
202
+ intact; error names the budget.
203
+ - Independence: over-`R2` return value + emitted blocks → truncation envelope
204
+ *and* delivered blocks in one response.
205
+ - Discard: throwing program with prior emits → error envelope with
206
+ `emittedDiscarded`, no blocks in `content`.
207
+ - Byte-for-byte: a program that never emits produces today's exact response.
208
+
209
+ The implementation issue carries these as acceptance criteria; the clauses
210
+ fold into `code-mode.md` under a new "Emitted output" section when it closes,
211
+ and the test-map row in [`operations.md`](./operations.md) lands with the
212
+ suite.
package/ethos.md CHANGED
@@ -18,13 +18,15 @@ order, and amending it is a design decision, not a drive-by edit.
18
18
  - **Two equal ways in.** `remoteMcp()` proxies a downstream MCP server;
19
19
  `api()` hand-writes a deliberate tool surface over a plain HTTP API. Both
20
20
  come out identical: same addresses, same catalog, same safety rules.
21
- - **Every meta-tool earns its keep**, and the bar has gone up. The default
22
- answer to "agents need X" is the program surface: a capability has to be
23
- shown inexpressible through it before it earns a top-level tool, and one
24
- that isn't worth its context cost still goes. Seven tools counting
25
- `execute_code` where a deployment has an executor, which is what a model
26
- sees by default; nine where it doesn't
27
- ([#224](https://github.com/zackbart/connecta/issues/224)).
21
+ - **Seven tools, an executor required.** The primary surface is a program, so
22
+ every deployment runs an executor a Dynamic Worker on Cloudflare, QuickJS
23
+ behind its optional-peer subpath on Node and one without refuses to boot
24
+ ([#273](https://github.com/zackbart/connecta/issues/273)). Every meta-tool
25
+ earns its keep: the default answer to "agents need X" is the program
26
+ surface, and a capability has to be shown inexpressible through it before
27
+ it earns a top-level tool
28
+ ([#224](https://github.com/zackbart/connecta/issues/224)). Packaging is
29
+ unchanged: required describes the deployment, never the dependencies.
28
30
  - **Safe by default.** Only tools explicitly annotated read-only are callable
29
31
  without crossing the destructive boundary — directly or from generated code;
30
32
  everything else goes through `call_destructive_tool`, where the MCP host can
@@ -33,12 +35,6 @@ order, and amending it is a design decision, not a drive-by edit.
33
35
  - **One fetch-native core, two runtimes.** The same code runs unchanged on
34
36
  Cloudflare Workers and in Node — a Worker or a Docker stack, your pick. Web
35
37
  APIs only in the core; Node touches live behind explicit subpaths.
36
- - **An executor is the assumed posture.** The primary surface is a program, so
37
- the deployment connecta is written toward has one — a Dynamic Worker on
38
- Cloudflare, QuickJS behind its optional-peer subpath on Node. Configuring one
39
- is what selects the code-first surface; executor-free stays supported as
40
- compatibility, not as an equal citizen. Packaging is unchanged: assumed is
41
- about defaults, never about dependencies.
42
38
  - **Observable, never administrable.** Operator pages show connector status,
43
39
  masked credentials, and payload-free activity. They can rotate a secret;
44
40
  they cannot add a connector, change policy, or alter what an agent can call.
@@ -79,12 +75,10 @@ proposing one without a new argument is not.
79
75
  | Agent credential recovery | accepted | one `auth_required` route through `authorize_connector`; only an operator handles secrets ([#192](https://github.com/zackbart/connecta/issues/192)) |
80
76
  | Operator-issued MCP access tokens | accepted | named, revocable authentication gives header-capable clients a small alternative to OAuth; tokens identify callers but never scope tools or become operator credentials |
81
77
  | Structured result surface | accepted | canonical `structuredContent` plus complete compact `content`; summary-only text is gated on host-forwarding evidence ([#191](https://github.com/zackbart/connecta/issues/191)) |
82
- | Code mode (`execute_code`) | accepted | the primary read, discovery, and composition surface: smaller serialized definitions (the exploration estimated ~32%, the shipped fold measures 19.6% — see the consolidation row), far smaller results once composition and projection happen before the model sees them, and a cold-start model that read the interface without help ([exploration](./documentation/code-first-exploration.md)) |
83
- | Code-first as the user-facing default | accepted | owner decision, 2026-07-30: "we don't need a deploy time flip, I'm the only one who uses it… We already decided it's good let's just do it." One operator, one deployment, and the exploration plus the smoke runs were enough judgment for him; the gate below was written for a user base connecta does not have ([#224](https://github.com/zackbart/connecta/issues/224)) |
84
- | The repeated per-model eval as the flip's gate | removed | it decided a question the owner has now decided himself; [`eval/code-first-gate`](./eval/code-first-gate/README.md) remains as measurement the arms are real deployment shapes and it still reports which surface performs but nothing waits on its verdict ([#222](https://github.com/zackbart/connecta/issues/222)) |
85
- | Surface consolidation to seven tools | accepted | folding `list_connectors`, `describe_tools`, and `batch_call` into the program surface deletes the overlapping routing choice between direct calls, batches, discovery, and execution; `call_tool` stays because a simple call is not cheaper through code. Measured at 19.6% fewer serialized definition bytes than the ten-tool shape the same deployment used to serve (10,675B → 8,587B), correcting the exploration's ~32% ([#224](https://github.com/zackbart/connecta/issues/224)) |
86
- | Executor-assumed posture | accepted | the primary surface is a program, so a deployment without an executor can only ever be the compatibility shape; packaging invariants are untouched ([#224](https://github.com/zackbart/connecta/issues/224)) |
87
- | Classic surface retention | accepted | what an executor-free deployment necessarily serves, the rollback path, and the eval's control arm; `surface: "classic"` alongside an executor is the only knob, and whether classic is ever removed is a separate future decision ([#224](https://github.com/zackbart/connecta/issues/224)) |
78
+ | Code mode (`execute_code`) | accepted | the primary read, discovery, and composition surface: smaller serialized definitions, far smaller results once composition and projection happen before the model sees them, and a cold-start model that read the interface without help ([exploration](./documentation/code-first-exploration.md), [#224](https://github.com/zackbart/connecta/issues/224)) |
79
+ | Code-first as the default; the eval gate retired | accepted | owner decision, 2026-07-30: one operator, no deploy-time flip; [`eval/code-first-gate`](./eval/code-first-gate/README.md) survives as measurement, but nothing waits on its verdict ([#222](https://github.com/zackbart/connecta/issues/222), [#224](https://github.com/zackbart/connecta/issues/224)) |
80
+ | Surface consolidation to seven tools | accepted | `list_connectors`, `describe_tools`, and `batch_call` fold into the program surface, deleting the routing choice between direct calls, batches, discovery, and execution; `call_tool` stays because a simple call is not cheaper through code ([#224](https://github.com/zackbart/connecta/issues/224)) |
81
+ | Classic (executor-free) surface | removed | supersedes its provisional retention under [#224](https://github.com/zackbart/connecta/issues/224) an executor is mandatory, and a deployment without one refuses to boot rather than serving a fallback shape ([#273](https://github.com/zackbart/connecta/issues/273)) |
88
82
  | Connector shortcut namespaces in programs | accepted | sugar over canonical addressing, kept but frozen — every expansion invents a collision class `<connectorId>.<toolName>` already solved ([#223](https://github.com/zackbart/connecta/issues/223)) |
89
83
  | Automatic host-side projection of program results | refused | the measured win was program-authored projection; a host heuristic drops fields a program chose to return and is invisible in the transcript ([#223](https://github.com/zackbart/connecta/issues/223)) |
90
84
  | Caller-visible execution diagnostics | accepted | optional request-local timing and size aggregates make catalog, connector, and executor costs distinguishable without persisting payloads, adding a tool, or charging normal responses context ([#247](https://github.com/zackbart/connecta/issues/247)) |
@@ -94,6 +88,10 @@ proposing one without a new argument is not.
94
88
  | MRTR / `input_required` passthrough | gated | statelessly relayable via `requestState`, but no host or downstream emits it yet; fails loudly until adoption evidence ([#176](https://github.com/zackbart/connecta/issues/176)) |
95
89
  | Native Tasks for oversized results | refused | tasks solve duration, `get_result` solves size; paging on a polling extension adds round trips for nothing ([#176](https://github.com/zackbart/connecta/issues/176)) |
96
90
  | Downstream `ttlMs` cache hints | gated | fixed TTL + fingerprint is battle-tested and catalog reads are ~3 ms; earns its way in with refresh-churn evidence ([#176](https://github.com/zackbart/connecta/issues/176)) |
91
+ | Rich program output (`connecta.emit`) | accepted | one host-collected emission channel: programs emit strictly validated text/image/audio blocks, delivered after the result envelope on success only; budgets fail loudly at the emit call ([design record](./documentation/rich-output-design.md), [#267](https://github.com/zackbart/connecta/issues/267)) |
92
+ | Result-channel widening of the `Executor` contract | refused | `ExecuteResult` stays `{ result, error?, logs? }` — structural compatibility with `@cloudflare/codemode` is the parity guarantee; emission rides the provider bridge instead ([#267](https://github.com/zackbart/connecta/issues/267)) |
93
+ | Guest-emitted `resource` / `resource_link` blocks | gated | a program can never mint a URI a client may dereference; MCP UI ([#266](https://github.com/zackbart/connecta/issues/266)) may design the one carve-out — programs supply only content, connecta mints the `ui://` address outside the sandbox ([#267](https://github.com/zackbart/connecta/issues/267)) |
94
+ | Provenance tracking for emitted content | refused | everything a program emits is program output; handles or attribution labels are capability-shaped machinery that changes no client's trust posture ([#267](https://github.com/zackbart/connecta/issues/267)) |
97
95
 
98
96
  ## Invariants
99
97
 
@@ -32,8 +32,7 @@ value outside this local example.
32
32
  Keep this deployment small:
33
33
 
34
34
  - Edit `src/index.ts` to change connectors, auth, storage, and the public URL.
35
- - Keep `executor: quickJsExecutor()` unless you deliberately want the classic
36
- compatibility surface.
35
+ - Keep the required `executor: quickJsExecutor()` configuration.
37
36
  - Keep secrets in environment variables or an external secret store. Never put
38
37
  tokens in `src/index.ts`, connector guides, or committed JSON.
39
38
  - Add application code only when implementing a deliberate `api()` connector.
@@ -30,9 +30,7 @@ const connecta = createConnecta({
30
30
  auth: bearerToken(token, { subjectId: "operator" }),
31
31
  // Downstream OAuth callbacks use this deployment origin.
32
32
  publicUrl: `http://localhost:${port}`,
33
- // Code mode: QuickJS runs model-written JS in a bounded disposable child.
34
- // This line is also what selects the seven-tool code-first surface; remove it
35
- // to serve the nine classic meta-tools instead.
33
+ // Required: QuickJS runs model-written JS in a bounded disposable child.
36
34
  executor: quickJsExecutor(),
37
35
  connectors: [
38
36
  api("time", {
@@ -2,9 +2,8 @@
2
2
 
3
3
  A deployable Worker that aggregates a downstream remote MCP and an in-code HTTP
4
4
  API connector, guarded by Clerk OAuth *and* a static bearer token, with state in
5
- a KV namespace. One Wrangler binding turns on paid code mode, which is also what
6
- selects the seven-tool code-first surface; the checked-in configuration deploys
7
- without it, serving the nine classic meta-tools on the Workers Free plan.
5
+ a KV namespace. Its required Worker Loader binding backs the seven-tool surface
6
+ and requires the Workers Paid plan.
8
7
 
9
8
  This is also the **starting template for a deployment**: a real deployment
10
9
  should be its own repository that pins an exact `@zackbart/connecta` version and
@@ -56,21 +55,17 @@ Connections. Credentials is at `/credentials`, named MCP access tokens are at
56
55
 
57
56
  ## Code mode
58
57
 
59
- Code mode is a deploy-time opt-in because its Dynamic Worker sandbox requires
60
- the [Workers Paid plan](https://developers.cloudflare.com/dynamic-workers/pricing/).
61
- The Worker Loader binding is the switch; no TypeScript change or separate
62
- environment variable is needed. Add this block to `wrangler.jsonc` (and a comma
63
- after the preceding property):
58
+ The Dynamic Worker sandbox requires the
59
+ [Workers Paid plan](https://developers.cloudflare.com/dynamic-workers/pricing/).
60
+ The required Worker Loader binding is checked into `wrangler.jsonc`:
64
61
 
65
62
  ```jsonc
66
63
  "worker_loaders": [{ "binding": "LOADER" }]
67
64
  ```
68
65
 
69
- `src/index.ts` detects `env.LOADER`, constructs `DynamicWorkerExecutor`, and
70
- serves the seven-tool code-first surface. Leave the binding absent as it is in
71
- the checked-in config — to deploy the same source on the Workers Free plan with
72
- the nine classic meta-tools. A deployment copied into its own repository must
73
- also install the executor package before enabling the binding:
66
+ `src/index.ts` constructs `DynamicWorkerExecutor` from `env.LOADER` and serves
67
+ the seven-tool surface. A deployment copied into its own repository must also
68
+ install the executor package:
74
69
 
75
70
  ```sh
76
71
  npm install @cloudflare/codemode
@@ -111,6 +106,7 @@ into `src/index.ts`**, so the example deploys without a database. To enable it:
111
106
  duration_ms INTEGER NOT NULL,
112
107
  attempts INTEGER NOT NULL,
113
108
  error_code TEXT,
109
+ friction TEXT,
114
110
  server_name TEXT NOT NULL,
115
111
  server_version TEXT NOT NULL,
116
112
  deployment_id TEXT
@@ -120,14 +116,21 @@ into `src/index.ts`**, so the example deploys without a database. To enable it:
120
116
  ON tool_call_activity (occurred_at_ms DESC, id DESC);
121
117
  ```
122
118
 
123
- **Already have this table?** `actor_namespace` was added after the original
124
- example, and `CREATE TABLE IF NOT EXISTS` will not add it to a table that
125
- already exists. Add it as a migration:
119
+ **Already have this table?** `actor_namespace` and `friction` were added
120
+ after the original example, and `CREATE TABLE IF NOT EXISTS` will not add
121
+ them to a table that already exists. Add them as migrations:
126
122
 
127
123
  ```sql
128
124
  ALTER TABLE tool_call_activity ADD COLUMN actor_namespace TEXT;
125
+ ALTER TABLE tool_call_activity ADD COLUMN friction TEXT;
129
126
  ```
130
127
 
128
+ `friction` is stored rather than derived because one of its classes belongs
129
+ to a call that *succeeded*: a result too large to return inline is friction
130
+ for the agent and carries no error code. Rows written before the column keep
131
+ working — the mapping module derives their friction from `error_code` — and
132
+ `error_code IS NOT NULL` remains an honest count of failures.
133
+
131
134
  Do this **before** deploying the updated `d1-activity.ts`: its `INSERT`
132
135
  names the column, so against an un-migrated table every write fails with
133
136
  `no such column`. Activity writes are best-effort by design — connecta logs
@@ -23,11 +23,44 @@ export interface ActivityEvent {
23
23
  durationMs: number;
24
24
  attempts: number;
25
25
  errorCode?: string;
26
+ friction?:
27
+ | "tool_not_found"
28
+ | "schema_retry"
29
+ | "destructive_reroute"
30
+ | "auth_required"
31
+ | "result_too_large";
26
32
  serverName: string;
27
33
  serverVersion: string;
28
34
  deploymentId?: string;
29
35
  }
30
36
 
37
+ /**
38
+ * Backfill for rows written before `friction` had a column of its own. Every
39
+ * arm must agree with the package's own `agentFrictionForCode`; the repository
40
+ * test suite pins that, because a dashboard that groups by friction should not
41
+ * change its answer depending on which side of a migration a row landed on.
42
+ * Friction that never had an error code — an oversized but successful result —
43
+ * is only recoverable from the column, which is why the column exists.
44
+ */
45
+ function frictionForCode(code: string | null): ActivityEvent["friction"] {
46
+ switch (code) {
47
+ case "unknown_address":
48
+ case "unknown_tool":
49
+ case "ambiguous_tool_alias":
50
+ return "tool_not_found";
51
+ case "invalid_args":
52
+ return "schema_retry";
53
+ case "destructive_tool_requires_approval":
54
+ return "destructive_reroute";
55
+ case "auth_required":
56
+ return "auth_required";
57
+ case "result_too_large":
58
+ return "result_too_large";
59
+ default:
60
+ return undefined;
61
+ }
62
+ }
63
+
31
64
  export interface ActivityRow {
32
65
  id: string;
33
66
  occurred_at_ms: number;
@@ -42,6 +75,7 @@ export interface ActivityRow {
42
75
  duration_ms: number;
43
76
  attempts: number;
44
77
  error_code: string | null;
78
+ friction: ActivityEvent["friction"] | null;
45
79
  server_name: string;
46
80
  server_version: string;
47
81
  deployment_id: string | null;
@@ -64,6 +98,10 @@ export function activityEventToRow(
64
98
  duration_ms: event.durationMs,
65
99
  attempts: event.attempts,
66
100
  error_code: event.errorCode ?? null,
101
+ // Stored beside the code rather than derived from it: a truncated result is
102
+ // friction on a call that succeeded, so it has no error code to derive from
103
+ // — and `error_code IS NOT NULL` stays an honest count of failures.
104
+ friction: event.friction ?? null,
67
105
  server_name: event.serverName,
68
106
  server_version: event.serverVersion,
69
107
  deployment_id: event.deploymentId ?? null,
@@ -73,6 +111,7 @@ export function activityEventToRow(
73
111
  export function activityRowToEvent(
74
112
  row: ActivityRow,
75
113
  ): ActivityEvent {
114
+ const friction = row.friction ?? frictionForCode(row.error_code);
76
115
  return {
77
116
  schemaVersion: 1,
78
117
  id: row.id,
@@ -91,6 +130,7 @@ export function activityRowToEvent(
91
130
  durationMs: row.duration_ms,
92
131
  attempts: row.attempts,
93
132
  ...(row.error_code ? { errorCode: row.error_code } : {}),
133
+ ...(friction ? { friction } : {}),
94
134
  serverName: row.server_name,
95
135
  serverVersion: row.server_version,
96
136
  ...(row.deployment_id
@@ -50,8 +50,8 @@ export function d1ActivityStore(db: D1Database): ActivityStore {
50
50
  id, occurred_at_ms, request_id, actor_kind, actor_id,
51
51
  actor_namespace,
52
52
  connector_id, tool_name, source, outcome, duration_ms, attempts,
53
- error_code, server_name, server_version, deployment_id
54
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
53
+ error_code, friction, server_name, server_version, deployment_id
54
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
55
55
  )
56
56
  .bind(
57
57
  row.id,
@@ -67,6 +67,7 @@ export function d1ActivityStore(db: D1Database): ActivityStore {
67
67
  row.duration_ms,
68
68
  row.attempts,
69
69
  row.error_code,
70
+ row.friction ?? null,
70
71
  row.server_name,
71
72
  row.server_version,
72
73
  row.deployment_id,
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * One MCP endpoint aggregating a downstream remote MCP and an HTTP API, guarded
5
5
  * by Clerk OAuth *and* a static bearer token, with OAuth/cache state in a KV
6
- * namespace. Add the optional Worker Loader binding in wrangler.jsonc for the
7
- * seven-tool code-first surface; without it this serves the nine classic tools.
6
+ * namespace. The required Worker Loader binding in wrangler.jsonc backs the
7
+ * seven-tool surface.
8
8
  *
9
9
  * Setup (this example has no package.json of its own — it self-references the
10
10
  * installed `@zackbart/connecta` package):
@@ -19,8 +19,7 @@
19
19
  * and CLERK_PUBLISHABLE_KEY + PUBLIC_URL as plain vars in wrangler.jsonc.
20
20
  * 4. Enable Dynamic Client Registration in the Clerk dashboard
21
21
  * (OAuth Applications -> DCR toggle) so Claude/Cursor can self-register.
22
- * 5. Optional paid code mode: add the documented `worker_loaders` binding to
23
- * wrangler.jsonc. Binding presence enables execute_code automatically.
22
+ * 5. Use the Workers Paid plan required by the `worker_loaders` binding.
24
23
  * 6. `wrangler deploy` from this folder (examples/worker), where wrangler.jsonc
25
24
  * lives. Point your MCP client at `<PUBLIC_URL>/mcp`.
26
25
  */
@@ -46,23 +45,16 @@ interface Env {
46
45
  PUBLIC_URL: string;
47
46
  /**
48
47
  * Worker Loader binding (wrangler.jsonc `worker_loaders`) powering
49
- * execute_code and, with it, the code-first surface. Dynamic Workers require
50
- * the Workers Paid plan; leave the binding absent for the nine classic
51
- * meta-tools on either plan.
48
+ * execute_code. Dynamic Workers require the Workers Paid plan.
52
49
  */
53
- LOADER?: WorkerLoader;
50
+ LOADER: WorkerLoader;
54
51
  }
55
52
 
56
53
  function build(env: Env) {
57
54
  return createConnecta({
58
55
  publicUrl: env.PUBLIC_URL,
59
56
  storage: cloudflareKvStorage(env.CONNECTA_KV),
60
- // Binding-as-switch: adding worker_loaders in wrangler.jsonc enables code
61
- // mode and the seven-tool code-first surface with it; leaving it absent
62
- // keeps this deployment free-tier compatible on the classic surface.
63
- ...(env.LOADER
64
- ? { executor: new DynamicWorkerExecutor({ loader: env.LOADER }) }
65
- : {}),
57
+ executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
66
58
  auth: [
67
59
  // Multiple credentials may identify callers in one deployment. Every
68
60
  // admitted caller reaches this deployment's deliberate connector set.
@@ -16,11 +16,8 @@
16
16
  // Create with `wrangler kv namespace create CONNECTA_KV` and paste the id.
17
17
  "kv_namespaces": [
18
18
  { "binding": "CONNECTA_KV", "id": "replace-with-kv-namespace-id" }
19
- ]
19
+ ],
20
20
 
21
- // Optional paid code mode: add a comma above, then uncomment this binding.
22
- // Its presence is the entire switch — src/index.ts detects env.LOADER and
23
- // serves the seven-tool code-first surface. Leave it absent for a free-tier
24
- // deployment with the nine classic meta-tools.
25
- // "worker_loaders": [{ "binding": "LOADER" }]
21
+ // Required by execute_code; Dynamic Workers use the Workers Paid plan.
22
+ "worker_loaders": [{ "binding": "LOADER" }]
26
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.10.5",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",