@zackbart/connecta 0.24.4 → 0.26.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 (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -2,34 +2,43 @@
2
2
 
3
3
  Connecta keeps one small tool surface in model context and resolves downstream
4
4
  tools behind it. `search_tools` finds addresses, the call tools enforce safety
5
- annotations, `execute_code` runs read-only work as a program, and `get_result`
6
- pages bounded results.
5
+ annotations, `execute_code` runs work as a program that pauses at each write
6
+ until `resume_execution` approves it, and `get_result` pages bounded results.
7
7
 
8
8
  This guide is the contract an MCP client sees. The in-program `connecta.*` API
9
9
  those tools imply belongs to [code mode](./code-mode.md); inbound identity and
10
10
  credential administration belong to [auth](./auth.md).
11
11
 
12
- ## The seven tools
12
+ ## The eight tools
13
13
 
14
- Every deployment requires an executor, so `tools/list` is exactly seven. No
15
- configuration adds an eighth or removes one.
14
+ Every deployment requires an executor, so `tools/list` is exactly eight. No
15
+ configuration adds a ninth or removes one — not even turning resumable writes
16
+ off, which changes what `resume_execution` answers and nothing about whether it
17
+ is listed, so a client's cached tool list never depends on the storage behind
18
+ the deployment. `execute_code` keeps its read-only hint although its programs
19
+ may now reach writes, because no write runs there: a program stops before
20
+ sending one, and the write runs only inside the destructive-annotated
21
+ `resume_execution` that repeats it ([code mode](./code-mode.md#pausing-and-resuming)).
16
22
 
17
23
  | Tool | Arguments | Returns |
18
24
  | --- | --- | --- |
19
- | `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked |
20
- | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
25
+ | `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked; or, when it reached a write, `{ paused: { address, args, token, expiresAt, nextAction, hint } }` |
26
+ | `resume_execution` | `token`, `address`, `args`, `approval?: "call" \| "tool"`, `reason?` | what `execute_code` would have returned had the write been approved: the result, the next pause, or a typed failure |
27
+ | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
21
28
  | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
22
29
  | `call_destructive_tool` | the same, plus `reason?` | the same |
23
30
  | `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
24
- | `get_result` | `id`, `offset?`, `maxBytes?` | `{ offset, nextOffset?, totalBytes, text }` |
31
+ | `get_result` | `id`, `offset?`, `maxBytes?` | a one-line JSON header `{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }`, a newline, then the page as raw text ([paging](#paging-with-get_result)) |
25
32
  | `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
26
33
 
27
34
  `limit` defaults to 8 and is capped at 100, as is one `connecta.describe` batch.
28
35
  `get_result.offset` is a whole number of bytes ≥ 0 defaulting to 0 and
29
- `maxBytes` a whole number ≥ 1 defaulting to the deployment result cap — itself
30
- 50,000 bytes unless `calls.maxResultBytes` or a per-connector override says
31
- otherwise. Both are validated rather than clamped: a bad value is an input
32
- error. `reason` is at most 500 characters of context for the host's human
36
+ `maxBytes` a whole number ≥ 1 that defaults to, and is clamped to, the inline
37
+ cap of the call that stashed the result — 24,000 bytes unless
38
+ `calls.maxResultBytes` or a per-connector override says otherwise
39
+ ([why 24,000](#truncated-direct-call-results)). A value outside those domains is
40
+ an input error; a valid `maxBytes` above the cap is an upper bound, not an
41
+ error ([why](#paging-with-get_result)). `reason` is at most 500 characters of context for the host's human
33
42
  approval view; Connecta neither treats it as authority nor sends it downstream,
34
43
  and an empty or whitespace-only one reads as no reason rather than as grounds to
35
44
  refuse a consequential call.
@@ -43,9 +52,13 @@ credentials, logs, or raw error text.
43
52
 
44
53
  Connecta's own tools carry the annotations it demands of downstream tools:
45
54
  read-only hints on all but `authorize_connector`, which mutates stored auth
46
- state, and `call_destructive_tool`. Otherwise a host that gates on annotations
47
- would prompt for every search, and a connecta aggregated behind another connecta
48
- would be refused by its own policy.
55
+ state, and the two that send writes, `call_destructive_tool` and
56
+ `resume_execution`, both destructive. Otherwise a host that gates on
57
+ annotations would prompt for every search, and a connecta aggregated behind
58
+ another connecta would be refused by its own policy. `resume_execution` is
59
+ where the prompt belongs: its arguments are the exact write, so the human
60
+ approves what will be sent rather than a program that might send anything.
61
+ Its `reason`, like `call_destructive_tool`'s, is dropped before anything runs.
49
62
 
50
63
  ## Routing between the call surfaces
51
64
 
@@ -61,12 +74,21 @@ as a small sample for inspection before continuing in another call, which avoids
61
74
  repeated guesses at text formats and collection roots without restoring a
62
75
  mandatory discovery-only round trip.
63
76
 
64
- That covers read-only work, because that is the only work a program can do.
65
- Anything unannotated, write-capable, or destructive is inadmissible inside the
66
- sandbox, so multi-step destructive work discovers at the top level and runs each
67
- step through `call_destructive_tool`, where the host can put the question to a
68
- human. Telling an agent never to search at the top level for multiple calls
69
- would close the only route that work has.
77
+ Writes take the same route. A program that reaches a tool not explicitly
78
+ annotated read-only stops before sending it — unless the deployment's config
79
+ exempts that tool, and then it runs unasked — and returns the exact call with a
80
+ token; `resume_execution` repeating it is the approval, and the program
81
+ replays from a journal to send it and carry on — to its answer, or to its next
82
+ write. So "close the stale issues and post a summary" is one program and a few
83
+ approvals, not one program and thirty-one top-level calls, and the program's
84
+ reasoning survives between them. `approval: "tool"` covers the rest of the
85
+ run's calls to that tool, so thirty closes cost one prompt. One known write
86
+ still goes straight to `call_destructive_tool`, and top-level discovery stays
87
+ for catalog inspection. On a deployment without resumable writes (storage
88
+ without `compareAndSet`), a program cannot write at all: the call fails
89
+ `destructive_tool_requires_approval`, multi-step writes discover at the top
90
+ level and run each step through `call_destructive_tool`, and the MCP
91
+ instructions say so.
70
92
 
71
93
  The three discovery routes use deliberately different envelopes. These are their
72
94
  smallest successful one-tool shapes:
@@ -104,9 +126,9 @@ discovery. Both take the same arguments.
104
126
  | --- | --- |
105
127
  | `query` | two to four action/object terms; empty or whitespace-only browses |
106
128
  | `connector` | scopes to one id, loading that catalog alone instead of fanning out across every configured connector. Set it when the integration is obvious, omit it when the right one is genuinely ambiguous |
107
- | `safety` | `"readOnly"` for what generated code may call, `"approvalRequired"` for the complementary set that must cross `call_destructive_tool`, omitted or `"all"` for the complete configured catalog |
129
+ | `safety` | `"readOnly"` for what runs unasked, `"approvalRequired"` for the complementary set that pauses a program or crosses `call_destructive_tool`, omitted or `"all"` for the complete configured catalog. A config-exempt write stays `"approvalRequired"` and its row says `approval: "exempt"`: a program calls it without pausing, and `call_tool` still refuses it ([code mode](./code-mode.md#pausing-and-resuming) `W12`) |
108
130
  | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
109
- | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema |
131
+ | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
110
132
  | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
111
133
 
112
134
  Neither `connector` nor `safety` grants authority or changes invocation
@@ -141,6 +163,44 @@ act on is the flag: any cap sets `inputSchemaTruncated` or
141
163
  `outputSchemaTruncated`, and that means repeat with `includeSchemas: "json"`, or
142
164
  describe, when the exact constraints matter.
143
165
 
166
+ ### TypeScript signatures
167
+
168
+ `includeSchemas: "typescript"` replaces both schema fields with one
169
+ `signature`: the function `connecta.call(address, args)` resolves to, written
170
+ as a TypeScript type because agents write code against types more reliably
171
+ than against JSON Schema ([executor](https://github.com/UsefulSoftwareCo/executor)'s
172
+ discovery made the same bet).
173
+
174
+ ```ts
175
+ (args: { team: string; limit?: number /* <= 250 */ }) => Promise<unknown>
176
+ (args?: {}) => Promise<{ accounts: { stripe_context: string; livemode: boolean; name?: string }[] }>
177
+ (args: { team: string }) => Promise</* observed, not declared */ { cursor?: string; issues?: { id?: string }[] }>
178
+ ```
179
+
180
+ It is something to read, not something that runs: programs stay JavaScript,
181
+ and the refusal of erasable TypeScript in `execute_code` stands
182
+ ([ethos](../ethos.md#decisions)). The output half is the provider's declared
183
+ schema, else an `S9` observation that opens with the
184
+ `/* observed, not declared */` marker and keeps the `outputSchemaSource`
185
+ field, else `unknown` — runtime evidence never reads as a contract, even when
186
+ the signature is lifted out of its entry. A tool without parameters takes
187
+ `args?: {}`.
188
+
189
+ The walk, byte budgets, and truncation flags are compact's, each half
190
+ separately, and search, describe, and every discovery route share them, so the
191
+ same tool renders the same signature from `search_tools` and `connecta.search`.
192
+ What changes is the dialect: `number` for `integer`, `;` between members,
193
+ quoted non-identifier keys, `| null` for OpenAPI `nullable` and type lists,
194
+ index signatures for schema-valued `additionalProperties`,
195
+ `Record<string, unknown>` for an open object with no declared properties, and
196
+ JSDoc for property prose where describe keeps it. Both dialects parenthesize a
197
+ union or intersection before an array suffix, `(A | B)[]`.
198
+ Where compact would print a bare definition name or raw JSON, the signature
199
+ prints `unknown` with a marker and sets the flag: `/* recursive */` for a
200
+ `$ref` cycle, `/* unresolved */` for a missing target, `/* truncated */` past
201
+ depth four or an exhausted budget. `inputKeys` and friends come from the
202
+ declared schema exactly as they do for compact.
203
+
144
204
  ## Connector guides and skills
145
205
 
146
206
  A connector may attach a deployment-owned guide as markdown, keeping the
@@ -202,7 +262,8 @@ text block carrying its compact JSON and then applies the same content size
202
262
  guard, which preserves structured-only results including `null`, arrays, and
203
263
  scalars. An existing text mirror stays unchanged; Connecta adds no second copy.
204
264
  Newly stashed JSON and downstream content envelopes use compact serialization,
205
- so `get_result` offsets and totals describe that exact compact text.
265
+ and a lone text block stashes as its own text, so `get_result` offsets and
266
+ totals describe exactly that text.
206
267
 
207
268
  | Bound | Value |
208
269
  | --- | --- |
@@ -244,30 +305,124 @@ original UTF-8 text, not the envelope. A supplied offset inside a character
244
305
  moves back to its start; page ends also align to character boundaries, and a
245
306
  page smaller than one character widens just enough to make progress.
246
307
 
247
- A successfully stashed `call_tool` truncation notice carries both the historical
248
- `resultId` and an exact
249
- `nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`, so the handle
250
- is actionable without copying an identifier out of prose. Program results and
251
- oversized discovery responses carry no such route: paging a program's return
252
- value is a refused shape, because a program can shrink anything before it
253
- returns.
254
-
255
- A refused or failed stash write cannot undo a downstream success. Both call tools
256
- then return a truncated preview where one is usable, with a paging-unavailable
257
- notice and no `resultId` or paging action; an envelope carrying non-text blocks
258
- gets the notice alone, because the head of a half-written base64 image helps
259
- nobody. Activity records success, and the operator logger receives a fixed
260
- warning naming the connector and tool without storage error prose. For read-only
261
- work, reduce the result inside `execute_code` — repeating an approved write is
262
- not a way to recover its output. Other result-processing failures use a fixed
263
- `result_processing_failed` message and are never retryable.
264
-
265
308
  A per-call `timeoutMs` covers catalog resolution, admission, and connector
266
309
  execution under one deadline. The admission queue's own timeout may expire
267
310
  sooner but cannot extend the call deadline. Result processing happens after that
268
311
  deadline ends, because a completed downstream call must not turn into a
269
312
  retryable timeout while Connecta prepares its response.
270
313
 
314
+ ### Truncated direct-call results
315
+
316
+ A `call_tool` or `call_destructive_tool` result over its cap — the connector's
317
+ `maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — comes back
318
+ as one text block. Its first line is the truncation notice, one line of compact
319
+ JSON; everything after the first newline is the preview:
320
+
321
+ ```text
322
+ {"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with get_result using nextAction.","nextAction":{"tool":"get_result","arguments":{"id":"…","offset":24000}}}
323
+ {"ts":"2026-09-16T21:31:11.759Z","actor":"sam.ortiz@example.com",…
324
+ ```
325
+
326
+ The notice leads because clients cut oversized results from the end, and a
327
+ notice at the tail is the part they drop. Claude Code (measured on 2.1.280, from
328
+ its bundled source and with `claude -p` against a probe server) passes an MCP
329
+ result through untouched while its text blocks total at most 50,000 characters.
330
+ One character more and it replaces the whole result with a pointer to a spill
331
+ file plus the first 2,000 characters of `JSON.stringify(content, null, 2)`,
332
+ whatever `MAX_MCP_OUTPUT_TOKENS` says. Once the characters exceed twice
333
+ `MAX_MCP_OUTPUT_TOKENS` (25,000 by default, so again 50,000) it also counts real
334
+ tokens against that limit, and a result over it arrives as an error carrying no
335
+ content at all — no preview, no notice. The old layout — a
336
+ 50,000-byte preview, then the notice — crossed the first line every time, so an
337
+ agent without file tools never saw the handle; in the eval's write task two of
338
+ three models re-ran a billable export three times trying to read its result.
339
+
340
+ The default cap sits under both lines with room to spare. Preview plus notice
341
+ stays under 25,000 bytes, which is under 50,000 characters for any text, because
342
+ a character is at least one UTF-8 byte, and under 25,000 tokens for any text,
343
+ because a token covers at least one byte. A `get_result` page obeys the same
344
+ cap, and 24,000 matches the program result boundary in
345
+ [code mode](./code-mode.md). A deployment whose
346
+ clients take more can raise the cap, and one whose clients take less can lower
347
+ it, per connector if need be; a result between 24,000 and 50,000 bytes that
348
+ used to arrive whole now pages. A client that spills rather than rejects keeps
349
+ the notice whatever its threshold, since the notice is the first few hundred
350
+ characters. One that rejects outright — Claude Code with
351
+ `MAX_MCP_OUTPUT_TOKENS` set below 12,500 — needs a cap under twice its limit.
352
+
353
+ The preview is the head of what `get_result` pages, cut on a character
354
+ boundary, so `nextAction.arguments.offset` continues exactly where it stops. A
355
+ lone text block — including the one synthesized from `structuredContent` — is
356
+ measured, stashed, previewed, and paged as its text: wrapping it in a content
357
+ envelope added nothing but a second layer of JSON escaping, so a JSON payload
358
+ used to preview as `[{"type":"text","text":"{\"ts\":…` and every page carried
359
+ the escaping twice. Several text blocks keep the serialized envelope as the one
360
+ string measured, stashed, and paged, because their boundaries live there; they
361
+ preview as their text joined by newlines, and their next action starts at
362
+ offset 0. A content array carrying non-text blocks returns the notice alone,
363
+ because the head of a half-written base64 image helps nobody. In value mode the
364
+ notice is `data` itself, with no preview and a next action at offset 0.
365
+
366
+ The hint says what to do next. For a call not explicitly annotated read-only it
367
+ opens with “This write already ran: do not call it again to see its result.”
368
+ The approved write happened; repeating it to look again is how one export
369
+ becomes three. For an explicitly read-only call, repeating is the better route,
370
+ so the hint offers it first: to find something specific, repeat the read inside
371
+ `execute_code` with `connecta.call` and filter or search the result there; to
372
+ read it in full, page with `get_result`. Inside a program the inline cap does
373
+ not apply to a host call's result, only to what the program returns (`L6` in
374
+ [code mode](./code-mode.md) bounds a host call far higher), so the reduction
375
+ sees the whole result at once. When the hint named paging alone, the eval's
376
+ weakest model read a 185 KB CI log in 24,000-byte pages, skipped the range
377
+ holding the real failure, and named the flaky test near the top; before the
378
+ notice was visible, it had reduced the log in a program and found the failure.
379
+ `nextAction` stays the page handle for either kind of call, because paging is
380
+ the one next step connecta can spell out exactly, while a reduction is a
381
+ program the agent has to write. `resultId` stays beside the exact `nextAction`,
382
+ so the handle is actionable without copying an identifier out of prose.
383
+ Program results and oversized discovery responses carry no such route: paging a
384
+ program's return value is a refused shape, because a program can shrink
385
+ anything before it returns.
386
+
387
+ ### Paging with get_result
388
+
389
+ A page is the truncated result's shape again: one text block whose first line
390
+ is a JSON header and whose remainder is the page, raw.
391
+
392
+ ```text
393
+ {"resultId":"…","offset":24000,"bytes":24000,"totalBytes":161420,"hasMore":true,"nextAction":{"tool":"get_result","arguments":{"id":"…","offset":48000}}}
394
+ {"ts":"2026-09-17T23:42:55.357Z","actor":"noor.haddad@example.com",…
395
+ ```
396
+
397
+ `bytes` is what this page returned; `hasMore` is false on the last page, which
398
+ carries no `nextAction`. The page is the stashed text as-is — a lone text
399
+ block's own text, a value's compact JSON, several blocks' serialized content
400
+ array — and never a JSON string holding it. Pages used to arrive as
401
+ `{ offset, nextOffset?, totalBytes, text }`, which escaped every quote and
402
+ newline in the page: a second layer over JSON payloads that made them hard to
403
+ read and swelled a 20,000-byte page of JSON lines to 25,000 characters or more.
404
+
405
+ `maxBytes` is an upper bound clamped to the inline cap of the call that stashed
406
+ the result — the connector's override when it had one, recorded in the stash
407
+ entry — and a clamped request is answered, not refused, with `bytes` saying how
408
+ much came back. Clients cut an oversized page exactly as they cut the original
409
+ result. In the eval, agents that could finally see the handle asked for pages of
410
+ 50,000 bytes and more, and Claude Code rejected every one of those answers
411
+ outright, leaving an agent that had just been told not to repeat a write with
412
+ nothing to read. So no response to a truncated call or to a page request
413
+ exceeds the cap plus a header of a few hundred bytes. Value mode's unpageable
414
+ preview is cut short enough that its JSON escaping still fits. Entries stashed
415
+ before the cap was recorded page at the deployment cap.
416
+
417
+ A refused or failed stash write cannot undo a downstream success. Both call tools
418
+ then return the same layout with a paging-unavailable notice and no `resultId`
419
+ or paging action — the notice first, then the preview where one is usable, and
420
+ for a write the same warning not to repeat it. Activity records success, and
421
+ the operator logger receives a fixed warning naming the connector and tool
422
+ without storage error prose. For read-only work, reduce the result inside
423
+ `execute_code` — repeating an approved write is not a way to recover its output. Other result-processing failures use a fixed
424
+ `result_processing_failed` message and are never retryable.
425
+
271
426
  ## Lexical discovery
272
427
 
273
428
  `search_tools` tokenizes tool names and descriptions at punctuation and
@@ -378,9 +533,10 @@ attempted tool name; an unknown tool scopes the same query to the connector that
378
533
  answered. The suggested route follows the route the caller took:
379
534
  `tool: "search_tools"` for a top-level call, `function: "connecta.search"` with
380
535
  the same arguments when the miss happened inside `execute_code`, which has no
381
- way to call a tool. A read path that reaches an unannotated, write-capable, or
536
+ way to call a tool. `call_tool` reaching an unannotated, write-capable, or
382
537
  destructive tool returns `nextAction` for `call_destructive_tool` with the
383
- canonical address. Nothing is executed by these records.
538
+ canonical address; inside a program the same call pauses, and the pause result
539
+ names `resume_execution` instead. Nothing is executed by these records.
384
540
 
385
541
  `connecta.describe` keeps its failures inline instead, so one miss cannot
386
542
  discard the other schemas; each failed entry carries a human `error`, typed
package/ethos.md CHANGED
@@ -4,92 +4,91 @@ Connecta’s scope, refusals, and invariants. Contradictions require a design de
4
4
 
5
5
  ## What this is
6
6
 
7
- - **One MCP endpoint.** Agents reach configured connectors through JavaScript
8
- and seven meta-tools. Every deployment requires an executor.
9
- - **Config-as-code.** One tenant and connector set; identities receive
10
- config-derived views. Maintained providers, `remoteMcp()`, and `api()` obey
11
- the same connector contract.
12
- - **Safe by default.** Only explicitly read-only tools run inside a program.
13
- Others cross `call_destructive_tool`; the host owns human approval.
14
- - **One fetch-native core.** Web APIs support Node and Workers. Platform code
15
- and optional features use explicit subpath imports.
7
+ - **One MCP endpoint.** Agents reach configured connectors through JavaScript,
8
+ a required executor, and eight meta-tools.
9
+ - **Config-as-code.** One tenant and connector set; identities get
10
+ config-derived views. Maintained providers, `remoteMcp()`, and `api()` share
11
+ one connector contract.
12
+ - **Safe by default.** Programs run only read-only or config-exempt tools
13
+ unasked; others pause for `resume_execution` or cross
14
+ `call_destructive_tool`. The host owns approval.
15
+ - **One fetch-native core.** Web APIs on Node and Workers; platform code and
16
+ optional features behind explicit subpaths; Effect inside, Promises outside.
16
17
  - **Human auth management.** Optional pages show status and payload-free history.
17
18
  Explicit permissions allow credential changes and OAuth. Capabilities stay in code.
18
19
 
19
20
  ## What this isn't
20
21
 
21
- No runtime registration, admin-editable capabilities, policy engine, approvals,
22
- or pauses. No schema ingestion, accounts, groups, protocol sessions, or server
23
- push. Inbound providers own identity; personal state remains within one tenant.
24
- Credentials fail at use, without background probes. Breaking changes remain
25
- acceptable; version numbers signal change, not stability.
22
+ No runtime registration, admin-editable capabilities, or policy engine; config
23
+ may only exempt a tool from asking. No schema ingestion, accounts, or groups;
24
+ personal state remains within one tenant. Breaking changes remain acceptable;
25
+ version numbers signal change, not stability.
26
26
 
27
27
  ## Decisions
28
28
 
29
29
  Revisiting a verdict requires a new argument. Accepted designs live in
30
- subsystem guides and the CHANGELOG.
30
+ subsystem guides and the CHANGELOG; `planned` ones bind review before either
31
+ carries them.
31
32
 
32
33
  | Decision | Verdict | Why |
33
34
  | --- | --- | --- |
34
- | OpenAPI / GraphQL ingestion | refused | the disease is a tool nobody chose — a document authored it; hand-written literals, even through a shared factory, are still authorship |
35
+ | OpenAPI / GraphQL ingestion | refused | the disease is a document-authored tool nobody chose; hand-written literals, even through a shared factory, are still authorship |
35
36
  | Multi-tenancy / account model | refused | one deployment per tenant; inbound auth owns identity |
36
- | Policy engine, approvals, pauses | refused | the host asks the human; connecta only annotates |
37
+ | Approvals and pauses | accepted | a program pauses host-side at its first unapproved write until the destructive-annotated `resume_execution` repeats it; an expiring journal replays it, never re-sending unknown outcomes ([#565](https://github.com/zackbart/connecta/issues/565)) |
38
+ | Approval exemptions | accepted | config-only, program-only, never read-only elsewhere ([#566](https://github.com/zackbart/connecta/issues/566)) |
37
39
  | Runtime connector registration | refused | config-as-code is the security model |
38
- | Optional deployment modules | accepted | explicit imports and typed config slots select UI, activity history, credential vault, and inbound auth; discovery, execution, invocation, and enforcement stay in core |
39
- | Generic plugin lifecycle or marketplace | refused | modules organize deployment code; runtime installation and discovery add no required behavior |
40
- | Connecta-issued access tokens | removed | inbound identity providers own client authentication; configured bearer auth remains an optional adapter |
41
- | Provider registry / marketplace | refused | prebuilt connections are imports; discovery happens in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
40
+ | Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement |
41
+ | Artifacts module | accepted | a built-in connector for team-only sandboxed pages over stored JSON; immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) |
42
+ | Plugin lifecycle, provider registry, or marketplace | refused | modules are deployment code, not runtime installs; prebuilt connections are imports, discovered in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
43
+ | Connecta-issued access tokens | removed | inbound providers authenticate clients; bearer auth stays an optional adapter |
42
44
  | Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) |
43
- | Protocol sessions & server push | refused | stateless per request |
44
- | Resources & prompts aggregation | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) |
45
- | Elicitation passthrough | refused | no route through a stateless aggregator |
45
+ | Resources, prompts, and downstream MCP Apps templates | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) |
46
+ | Protocol sessions, server push, elicitation passthrough | refused | stateless per request; elicitation has no route |
46
47
  | Repository formatter | refused | style is authored, not enforced |
47
- | Host-side projection of program results | refused | a program projects; a heuristic drops fields invisibly ([#223](https://github.com/zackbart/connecta/issues/223)) |
48
- | `get_result` paging for program results | refused | paging rewards the unprojected return code mode exists to remove ([#223](https://github.com/zackbart/connecta/issues/223)) |
48
+ | Host-side projection or paging of program results | refused | a program projects: heuristics drop fields invisibly, paging rewards unprojected returns ([#223](https://github.com/zackbart/connecta/issues/223)) |
49
49
  | Native Tasks for oversized results | refused | tasks solve duration, `get_result` solves size ([#176](https://github.com/zackbart/connecta/issues/176)) |
50
50
  | Widening the `Executor` result contract | refused | `{ result, error?, logs? }` is the `@cloudflare/codemode` parity guarantee ([#267](https://github.com/zackbart/connecta/issues/267)) |
51
- | Erasable TypeScript in `execute_code` | refused | 24 MB dependency in the core, no measured agent benefit ([#419](https://github.com/zackbart/connecta/issues/419)) |
52
- | Guest-minted `resource` / `resource_link` blocks | refused | a program can never mint a URI a client may dereference ([#266](https://github.com/zackbart/connecta/issues/266)) |
51
+ | Erasable TypeScript in `execute_code` | refused | a 24 MB core dependency, no measured agent benefit ([#419](https://github.com/zackbart/connecta/issues/419)) |
52
+ | Guest-minted `resource` / `resource_link` blocks | refused | no program mints a URI a client may dereference ([#266](https://github.com/zackbart/connecta/issues/266)) |
53
53
  | Provenance tracking for emitted content | refused | everything a program emits is program output ([#267](https://github.com/zackbart/connecta/issues/267)) |
54
- | Result sampling on the catalog surface | refused | sampling is execution and cannot ride a catalog read ([#282](https://github.com/zackbart/connecta/issues/282)) |
54
+ | Result sampling on the catalog surface | refused | sampling is execution, not a catalog read ([#282](https://github.com/zackbart/connecta/issues/282)) |
55
55
  | Legacy embedded `UIResource` delivery | refused | superseded upstream, rendered by no client we face ([#266](https://github.com/zackbart/connecta/issues/266)) |
56
- | Effect as the core effect system | refused | −4% of the core for +75 KB gzip and a second async paradigm; re-measure at v4 stable ([#470](https://github.com/zackbart/connecta/issues/470)) |
57
- | Shared bounded queue under both admission controllers | refused | built and measured −17 lines for a hook-parameterised abstraction ([#453](https://github.com/zackbart/connecta/issues/453)) |
56
+ | Effect as the core effect system | accepted | lifetimes held the bugs: the rewrite closed leaked permits, hung waits, and cross-request races. Overturns [#470](https://github.com/zackbart/connecta/issues/470) |
57
+ | Shared bounded queue under both admission controllers | refused | built: −17 lines bought a hook-parameterised abstraction ([#453](https://github.com/zackbart/connecta/issues/453)) |
58
58
  | MCP Apps rendering and `connecta.ui` | removed | clients render returned data |
59
59
  | Connector shortcut globals | removed | canonical addresses need no sanitization |
60
60
  | `connecta.batch` | removed | JavaScript promises suffice |
61
61
  | Automatic direct-call retries | removed | callers own retry timing |
62
- | Connector HTTP routes | removed | deployments own custom routes |
63
- | Caller-selected toolkits | removed | config derives every view; config-declared, grant-gated pools at `/mcp/<pool>` are not caller-selected ([#178](https://github.com/zackbart/connecta/issues/178), [#531](https://github.com/zackbart/connecta/issues/531)) |
62
+ | Connector HTTP routes | removed | deployments own custom routes; the operator UI's artifact viewer is the exception |
63
+ | Caller-selected toolkits | removed | config derives every view, grant-gated `/mcp/<pool>` pools included ([#178](https://github.com/zackbart/connecta/issues/178), [#531](https://github.com/zackbart/connecta/issues/531)) |
64
64
  | Proactive credential liveness | removed | fail-at-use is enough ([#179](https://github.com/zackbart/connecta/issues/179)) |
65
65
  | Classic (executor-free) surface | removed | an executor is mandatory ([#273](https://github.com/zackbart/connecta/issues/273)) |
66
- | Per-result lexical query coverage | removed | did not earn its response bytes in a precommitted gate ([#323](https://github.com/zackbart/connecta/issues/323)) |
67
- | Stabilized workflows | gated | needs programs that actually recur ([#225](https://github.com/zackbart/connecta/issues/225)) |
66
+ | Per-result lexical query coverage | removed | failed a precommitted response-bytes gate ([#323](https://github.com/zackbart/connecta/issues/323)) |
67
+ | Stabilized workflows | accepted | only as artifact refresh: deployment-triggered read-only programs, shared credentials ([#225](https://github.com/zackbart/connecta/issues/225), [#564](https://github.com/zackbart/connecta/issues/564)) |
68
68
  | Semantic tool search | gated | keyword search has not been shown to fail ([#27](https://github.com/zackbart/connecta/issues/27)) |
69
69
  | MRTR / `input_required` passthrough | gated | relayable statelessly; no host or downstream emits it yet ([#176](https://github.com/zackbart/connecta/issues/176)) |
70
70
  | Downstream `ttlMs` cache hints | gated | needs refresh-churn evidence ([#206](https://github.com/zackbart/connecta/issues/206)) |
71
- | Downstream MCP Apps template passthrough | refused | clients own presentation; Connecta serves tools and data |
72
71
  | Worker Access inbound auth | provisional | Managed OAuth and Clerk migration need production evidence ([#506](https://github.com/zackbart/connecta/issues/506)) |
73
72
  | Program UI tool calls | removed | duplicated calls without improving retrieval ([#287](https://github.com/zackbart/connecta/issues/287), [#484](https://github.com/zackbart/connecta/issues/484)) |
74
73
 
75
74
  ## Invariants
76
75
 
77
- Tests beside subsystem documentation enforce these invariants. Breaking one
78
- requires a design decision.
76
+ Tests beside subsystem documentation enforce these; breaking one requires a
77
+ design decision.
79
78
 
80
79
  - **Fail-closed read-only.** A missing, false, or contradictory annotation never gets the benefit of the doubt.
81
80
  - **Generated code cannot mint capabilities.** Admission, credentials, and classification are enforced below the sandbox.
82
- - **Only explicitly read-only work runs inside the sandbox.** Everything else crosses `call_destructive_tool`.
83
- - **Nothing request-bound survives a request.** No transport, stream, signal, or awaited promise outlives it.
84
- - **A downstream catalog is complete or it is a failure.** A partial catalog is never cached, persisted, or served.
81
+ - **Only explicitly read-only or config-exempt work runs unasked in the sandbox.** Others pause or cross `call_destructive_tool`.
82
+ - **Nothing request-bound survives a request.** No transport, stream, signal, or awaited promise outlives it; a paused journal is data, not a request.
83
+ - **A downstream catalog is complete or it is a failure.** A partial one is never cached, persisted, or served.
85
84
  - **Activity is payload-free by construction.** The event type has nowhere to put arguments, results, code, or raw errors.
86
85
  - **An observed shape is never a declaration.** Names and broad types only, labeled, and gone behind any declared schema.
87
- - **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector and, for personal auth, its owning principal; rendered by nothing.
88
- - **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin.
89
- - **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath.
90
- - **Human routes manage auth, never capability.** Visibility grants use, not credential administration. Shared and personal auth mutations require separate config-derived permissions, both denied by default. Activity has its own read permission.
91
- - **Omitted modules do no work.** Core imports no UI bundle, encrypted vault implementation, activity implementation, or bearer adapter. OAuth callbacks remain available without UI.
92
- - **Status reads do not start authorization.** OAuth starts through an explicit authorized action; loading the UI never creates a consent flow.
86
+ - **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector (and principal, for personal auth), rendered by nothing.
87
+ - **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin or `effect/testing`.
88
+ - **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath; Effect is the one hard dependency, named by no published type.
89
+ - **Human routes manage auth, never capability.** Visibility grants use, not administration. Auth mutations need separate config-derived permissions, denied by default; activity reads its own. Artifact viewing respects connector grants and cannot invoke tools.
90
+ - **Omitted modules do no work.** Core imports no UI bundle, vault, activity, artifacts, or bearer implementation. OAuth callbacks work without UI.
91
+ - **Status reads do not start authorization.** Only an explicit authorized action starts OAuth, never a page load.
93
92
  - **Structural mistakes throw at construction.** Booting into the wrong shape is worse than not booting.
94
93
 
95
94
  Connecta simplifies [executor](https://github.com/UsefulSoftwareCo/executor).
@@ -37,6 +37,12 @@ This repository is deployment configuration, not a copy of Connecta itself.
37
37
  A configured Connecta bearer cannot cross the Access edge alone. Connecta-issued
38
38
  tokens are removed. UI, vault, and activity use explicit optional imports;
39
39
  auth changes require code-derived shared or personal management permissions.
40
+ - Brand the operator UI in `operatorUi({ branding })`: product and owner names,
41
+ description, favicon, and `theme` (`accent`, `radius`, `fontFamily`,
42
+ `monoFamily`, `colorScheme`). Ask the deployment's owner for their brand
43
+ rather than leaving the default. The commented block above
44
+ `ui: operatorUi()` in `src/index.ts` shows the shape; README "UI and
45
+ encrypted credentials" covers the rest.
40
46
  - After configuration changes, typecheck: inside the connecta repository that
41
47
  is `npm run check:examples`; a copied deployment runs its own `tsc --noEmit`.
42
48
  After deployment, connect both Claude and ChatGPT to `<PUBLIC_URL>/mcp` and