@zackbart/connecta 0.30.0 → 0.32.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 (133) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +21 -16
  3. package/bin/connecta.mjs +1 -0
  4. package/bin/version.mjs +1 -1
  5. package/dist/auth-elicitation.js +1 -1
  6. package/dist/call-recovery.js +25 -0
  7. package/dist/catalog-service.js +7 -2
  8. package/dist/config-schema.d.ts +14 -7
  9. package/dist/config-schema.js +29 -10
  10. package/dist/config-value-sources.js +6 -3
  11. package/dist/config.d.ts +26 -8
  12. package/dist/config.js +38 -16
  13. package/dist/connector-access.d.ts +2 -1
  14. package/dist/connectors/guarded-fetch.d.ts +18 -1
  15. package/dist/connectors/guarded-fetch.js +21 -2
  16. package/dist/describe-config.d.ts +6 -3
  17. package/dist/describe-config.js +3 -2
  18. package/dist/errors.d.ts +3 -1
  19. package/dist/errors.js +1 -0
  20. package/dist/execute.js +16 -14
  21. package/dist/index.js +4 -3
  22. package/dist/invocation.js +39 -4
  23. package/dist/meta-tools.js +17 -19
  24. package/dist/operator-record.d.ts +2 -2
  25. package/dist/operator-record.js +2 -0
  26. package/dist/operator-ui/contract.d.ts +5 -4
  27. package/dist/operator-ui/generated.js +2 -2
  28. package/dist/operator-ui/model.d.ts +1 -1
  29. package/dist/operator-ui/view.d.ts +9 -0
  30. package/dist/operator-ui/view.js +12 -0
  31. package/dist/program-writes.js +4 -49
  32. package/dist/provider.d.ts +6 -5
  33. package/dist/provider.js +3 -4
  34. package/dist/providers/_shared/google/workspace.js +1 -1
  35. package/dist/providers/_shared/rest/dispatch.js +41 -0
  36. package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
  37. package/dist/providers/_shared/rest/operation-index.js +527 -0
  38. package/dist/providers/_shared/rest/tools.d.ts +141 -0
  39. package/dist/providers/_shared/rest/tools.js +569 -0
  40. package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
  41. package/dist/providers/_shared/rest/value-safety.js +580 -0
  42. package/dist/providers/breeze/skill.generated.js +2 -2
  43. package/dist/providers/cloudflare/graphql.js +385 -0
  44. package/dist/providers/cloudflare/index.d.ts +47 -51
  45. package/dist/providers/cloudflare/index.js +188 -1997
  46. package/dist/providers/cloudflare/openapi.generated.js +3347 -0
  47. package/dist/providers/cloudflare/rest.d.ts +37 -0
  48. package/dist/providers/cloudflare/rest.js +1056 -0
  49. package/dist/providers/cloudflare/skill.generated.js +7 -4
  50. package/dist/providers/cloudflare/value-safety.js +1247 -0
  51. package/dist/providers/docs/index.d.ts +1 -1
  52. package/dist/providers/docs/index.js +2 -2
  53. package/dist/providers/drive/index.d.ts +1 -1
  54. package/dist/providers/drive/index.js +1 -1
  55. package/dist/providers/forms/index.d.ts +1 -1
  56. package/dist/providers/forms/index.js +2 -2
  57. package/dist/providers/github/skill.generated.js +2 -2
  58. package/dist/providers/gmail/index.d.ts +1 -1
  59. package/dist/providers/gmail/index.js +274 -23
  60. package/dist/providers/gmail/skill.generated.js +2 -2
  61. package/dist/providers/linear/index.js +1 -1
  62. package/dist/providers/mixpanel/index.d.ts +27 -12
  63. package/dist/providers/mixpanel/index.js +58 -6
  64. package/dist/providers/mixpanel/operations.js +20 -0
  65. package/dist/providers/mixpanel/rest.js +603 -0
  66. package/dist/providers/mixpanel/skill.generated.js +3 -2
  67. package/dist/providers/mixpanel/value-safety.js +16 -0
  68. package/dist/providers/notion/index.d.ts +35 -31
  69. package/dist/providers/notion/index.js +76 -1499
  70. package/dist/providers/notion/openapi.generated.js +75 -0
  71. package/dist/providers/notion/rest.d.ts +24 -0
  72. package/dist/providers/notion/rest.js +1186 -0
  73. package/dist/providers/notion/skill.generated.js +5 -4
  74. package/dist/providers/notion/value-safety.js +31 -0
  75. package/dist/providers/planning-center/index.d.ts +27 -0
  76. package/dist/providers/planning-center/index.js +286 -24
  77. package/dist/providers/planning-center/skill.generated.js +24 -4
  78. package/dist/providers/sheets/index.d.ts +1 -1
  79. package/dist/providers/sheets/index.js +3 -3
  80. package/dist/providers/slides/index.d.ts +1 -1
  81. package/dist/providers/slides/index.js +3 -3
  82. package/dist/providers/stripe/index.d.ts +26 -20
  83. package/dist/providers/stripe/index.js +103 -130
  84. package/dist/providers/stripe/openapi.generated.js +649 -0
  85. package/dist/providers/stripe/rest.d.ts +14 -0
  86. package/dist/providers/stripe/rest.js +395 -0
  87. package/dist/providers/stripe/skill.generated.js +7 -5
  88. package/dist/providers/stripe/value-safety.js +705 -0
  89. package/dist/providers/vercel/index.d.ts +31 -22
  90. package/dist/providers/vercel/index.js +96 -853
  91. package/dist/providers/vercel/openapi.generated.js +456 -0
  92. package/dist/providers/vercel/rest.d.ts +20 -0
  93. package/dist/providers/vercel/rest.js +1002 -0
  94. package/dist/providers/vercel/skill.generated.js +4 -5
  95. package/dist/providers/vercel/value-safety.js +328 -0
  96. package/dist/providers/wispr-flow/index.d.ts +14 -0
  97. package/dist/providers/wispr-flow/index.js +117 -0
  98. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  99. package/dist/routes/mcp.js +30 -10
  100. package/dist/routes/operator.js +14 -7
  101. package/dist/routes/ui-config.js +2 -2
  102. package/dist/server.js +3 -0
  103. package/dist/skills.js +5 -4
  104. package/dist/tool-safety.js +13 -5
  105. package/dist/types.d.ts +1 -1
  106. package/dist/ui.d.ts +1 -1
  107. package/dist/ui.js +1 -1
  108. package/dist/usage-guide.js +3 -3
  109. package/dist/version.d.ts +1 -1
  110. package/dist/version.js +1 -1
  111. package/dist/write-outcome.js +47 -0
  112. package/documentation/README.md +1 -0
  113. package/documentation/architecture.md +155 -28
  114. package/documentation/auth.md +86 -27
  115. package/documentation/code-mode.md +81 -72
  116. package/documentation/deploying.md +10 -5
  117. package/documentation/integrating.md +72 -9
  118. package/documentation/meta-tools.md +52 -38
  119. package/documentation/operating.md +11 -8
  120. package/documentation/operator-tests.md +2 -3
  121. package/documentation/operator-ui.md +9 -3
  122. package/documentation/provider-auth-migration.md +308 -0
  123. package/documentation/provider-migration-0.29.md +18 -6
  124. package/examples/worker/AGENTS.md +5 -0
  125. package/examples/worker/README.md +2 -2
  126. package/examples/worker/src/connecta.config.ts +5 -0
  127. package/package.json +7 -2
  128. package/templates/node/AGENTS.md +6 -4
  129. package/templates/node/README.md +6 -4
  130. package/templates/node/package.json +1 -1
  131. package/templates/node/src/connecta.config.ts +5 -0
  132. package/dist/providers/vercel/mcp-ownership.js +0 -212
  133. package/dist/tool-safety.d.ts +0 -9
@@ -6,15 +6,15 @@
6
6
  and package maintenance. This guide owns identity, grants, and downstream
7
7
  credential contracts; use [deployment setup](./deploying.md) for runnable steps.
8
8
 
9
- | Task | Sections |
10
- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
- | Connect a machine or migrate an old client | [Managed tokens](#managed-client-tokens-and-upgrading-from-v023), [static bearer migration](#migrating-static-bearer-clients) |
12
- | Configure caller visibility, trust, or auth ownership | [Principals and operators](#principals-visibility-and-operators), [pools](#pools), [shared and personal auth](#shared-and-personal-auth) |
13
- | Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [management](#human-authentication-management) |
14
- | Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
15
- | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
16
- | Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens) |
17
- | Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
9
+ | Task | Sections |
10
+ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | Connect a machine or migrate an old client | [Managed tokens](#managed-client-tokens-and-upgrading-from-v023), [static bearer migration](#migrating-static-bearer-clients) |
12
+ | Configure caller visibility, read-only endpoints, or auth ownership | [Principals and operators](#principals-visibility-and-operators), [pools](#pools), [shared and personal auth](#shared-and-personal-auth) |
13
+ | Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [management](#human-authentication-management) |
14
+ | Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
15
+ | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
16
+ | Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens) |
17
+ | Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
18
18
 
19
19
  [Credential recognition and admission](#credential-recognition-and-admission)
20
20
  defines refusal precedence.
@@ -90,6 +90,7 @@ createConnecta({
90
90
  principal.id === "your-operator-id",
91
91
  },
92
92
  connectors, executor,
93
+ readOnly: false, // required: true refuses every write call
93
94
  });
94
95
  ```
95
96
 
@@ -101,7 +102,7 @@ the identity namespaces and connector/pool grant callbacks too; storage
101
102
  compatibility translates no configuration and invents no grants. `access_token`
102
103
  actor ids, the `connecta:access-tokens:v1` activity namespace, friendly names,
103
104
  and stored principals carry over; a token without a principal stays unbound.
104
- Every request evaluates current identity, tool, and pool grants, and a token
105
+ Every request evaluates current identity, tool, and endpoint grants, and a token
105
106
  never becomes an interactive operator. Names are labels, never permissions. A
106
107
  UI-issued token belongs to the issuing human's principal, and any explicitly
107
108
  permitted token manager can list, rename, or revoke deployment tokens. Machine tokens manage neither tokens nor connection credentials; operators
@@ -163,6 +164,10 @@ deployment's anonymous admission. Otherwise, or on `undefined`, the call fails
163
164
  `auth_required` before any request leaves. Arguments, headers, and programs
164
165
  never reach it; a deployment's own connectors still cannot read the caller.
165
166
 
167
+ The deployment's top-level `grant` decides who may use plain `/mcp`; named
168
+ pools use their own grants. These endpoint decisions receive the admitted
169
+ identity below and never widen its connector access.
170
+
166
171
  `identity.connectorAccess` returns `"all"` (the default) or grants, for
167
172
  discovery and use alike: a connector id opens every tool on it, a
168
173
  `connector.tool` address that tool alone, and
@@ -206,6 +211,7 @@ createConnecta({
206
211
  },
207
212
  },
208
213
  connectors: [issues, campaigns, billing], executor,
214
+ readOnly: false, // required: true refuses every write call
209
215
  });
210
216
  ```
211
217
 
@@ -215,12 +221,12 @@ grant both connectors in full. Unknown or unprincipalled identities get `[]`.
215
221
  Use a stable authenticated principal or subject, never request headers or tool
216
222
  arguments. A token's stored principal is vouched for by inbound auth. Pools narrow their endpoint, not plain `/mcp`.
217
223
 
218
- | Catalog change | Guarded exact grant | Unrestricted exact string grant |
219
- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
220
- | New name | Excluded until reviewed and added | Excluded until reviewed and added |
221
- | Removed or renamed name | Unreachable; warned when the scoped view reads the catalog | Same |
222
- | Stored write verdict after overrides and provider review | Removed from discovery and every invocation path, including `call_destructive_tool` and trusted programs | Still granted; `call_tool` refuses writes and write paths take over |
223
- | Schema change alone | Does not revoke, unless it breaks a reviewed digest: then it is a write, as above | Same as above |
224
+ | Catalog change | Guarded exact grant | Unrestricted exact string grant |
225
+ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
226
+ | New name | Excluded until reviewed and added | Excluded until reviewed and added |
227
+ | Removed or renamed name | Unreachable; warned when the scoped view reads the catalog | Same |
228
+ | Stored write verdict after overrides and provider review | Removed from discovery and every invocation path, including `call_destructive_tool` and programs on a writable endpoint | Still granted; `call_tool` refuses writes and write paths take over |
229
+ | Schema change alone | Does not revoke, unless it breaks a reviewed digest: then it is a write, as above | Same as above |
224
230
 
225
231
  The example assumes both exact tools were reviewed as read-only; names imply no safety.
226
232
  Review schemas, annotations, and downstream behavior
@@ -236,7 +242,7 @@ Failed remote loads are errors, not empty catalogs. A transient first-page tool
236
242
  listing failure may serve complete facts in the same auth partition for five
237
243
  minutes after expiry. Failed pagination, permission and auth failures do not
238
244
  use this fallback. The expired deadline cannot authorize auth recovery. Personal OAuth ownership and credential
239
- administration are separate from visibility; program writes follow the endpoint's trust tier and host approval. `test/identity-scope.test.ts` exercises these boundaries.
245
+ administration are separate from visibility; writes follow the endpoint's `readOnly` setting and client approval. `test/identity-scope.test.ts` exercises these boundaries.
240
246
 
241
247
  ## Migrating static bearer clients
242
248
 
@@ -284,6 +290,8 @@ createConnecta({
284
290
  storage,
285
291
  accessTokens: accessTokens(storage),
286
292
  auth: clerkAuth({ publishableKey, secretKey }),
293
+ readOnly: false, // required; pools without their own readOnly inherit it
294
+ grant: ({ actor }) => actor.kind !== "access_token", // machines use their pools
287
295
  pools: {
288
296
  support: {
289
297
  tools: ["linear", "notion.search_pages", "notion.fetch_page"],
@@ -293,6 +301,11 @@ createConnecta({
293
301
  tools: ["calendar.create_event"],
294
302
  grant: ({ actor }) => actor.kind === "access_token" && actor.id === calendarTokenId,
295
303
  },
304
+ reporting: {
305
+ tools: ["linear", "notion"],
306
+ readOnly: true,
307
+ grant: ({ principal }) => analysts.has(principal?.id ?? ""),
308
+ },
296
309
  },
297
310
  identity: { connectorAccess },
298
311
  connectors, executor,
@@ -302,14 +315,59 @@ createConnecta({
302
315
  Without configured `auth`, an open deployment has one anonymous, non-interactive
303
316
  identity; grants like these return false and every pool path 404s.
304
317
 
305
- | Rule | Contract |
306
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
307
- | Scope | `/mcp/<pool>` intersects pool tools with the identity's `connectorAccess`, never widening it. Plain `/mcp` is unchanged; the resolver remains the security boundary. |
308
- | Grant | Defaults to deny; only literal `true` admits. Other returns, throws, and undeclared names produce an identical 404 status, body, and headers, after auth. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
309
- | Timing | Declared names await grants, so timing is not hidden. Keep grants pure and fast and names unsecret; names grant no access. The operator log records the refusal reason. |
310
- | Construction | Malformed names, unknown options or connectors, empty pools, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
311
- | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
312
- | Drift | No wildcards; tool grants are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
318
+ The optional top-level `grant: (identity) => boolean | Promise<boolean>`
319
+ controls plain `/mcp`. Omitted, it admits everyone who passes auth, including
320
+ anonymous callers on an open deployment. Configured, only literal `true`
321
+ admits; other returns and throws deny. A refusal has the same 404 status,
322
+ body bytes, and headers as a refused or undeclared pool, after auth. The
323
+ operator log records `default_grant_denied` or `default_grant_threw`, never
324
+ the callback's error text. Invalid config values throw at construction.
325
+ Named pools use only their own grants, so a caller refused at `/mcp` may
326
+ still use a pool that grants it.
327
+
328
+ Protected-resource metadata stays public at both default aliases,
329
+ `/.well-known/oauth-protected-resource` and
330
+ `/.well-known/oauth-protected-resource/mcp`, just as at named-pool aliases.
331
+ Discovery does not authenticate callers or evaluate endpoint grants, so fresh
332
+ OAuth clients can discover how to authenticate. The grant applies after auth
333
+ to MCP requests and result/page handles. Handles remain bound
334
+ to their originating endpoint; paging rechecks the current endpoint grant,
335
+ identity, and connector access. A granting pool cannot read a `/mcp` handle,
336
+ and `/mcp` cannot read that pool's handle.
337
+
338
+ `describeConfig().hasGrant` and the operator UI report whether plain `/mcp`
339
+ has a configured grant without exposing callback source. `describeConfig()`
340
+ does not evaluate it. Operator catalog, config, activity, and connector auth
341
+ routes require the same default endpoint grant after their own authentication.
342
+ A refused caller gets a private JSON 404; the UI provides no per-pool view.
343
+ The public HTML shell carries no catalog data.
344
+
345
+ Each endpoint is writable or read-only. The deployment's `readOnly` sets the
346
+ plain `/mcp` endpoint and is required: a deployment that omits it fails at
347
+ construction and must choose `true` or `false`. A pool inherits it unless the
348
+ pool sets its own `readOnly`, in either direction. A writable endpoint lets
349
+ `call_destructive_tool` and programs write, and the MCP client decides what to
350
+ approve. On a read-only endpoint, write tools are absent from `search_tools`
351
+ results, `connecta.search`, and `connecta.describe`, and `call_tool`,
352
+ `call_destructive_tool`, and program writes are refused with
353
+ `read_only_endpoint` before validation or dispatch. Reads, including reads
354
+ through `call_destructive_tool`, still run.
355
+ Guides are documentation, not discovery: provider `SKILL.md` and usage guides,
356
+ surfaced through `queryAnalysis.guideSummary`, `skills`, `connecta.skill`, and
357
+ skill `resources/read`, are the same on every endpoint and may name tools a
358
+ read-only endpoint refuses.
359
+ Resource reads, grants, and `connectorAccess`, including guarded
360
+ `requireReadOnly` grants, are the same on both. The removed `trust` option fails
361
+ construction with a message naming `readOnly`.
362
+
363
+ | Rule | Contract |
364
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
365
+ | Scope | `/mcp/<pool>` intersects pool tools with the identity's `connectorAccess`, never widening it. Plain `/mcp` serves the identity's `connectorAccess` after its optional top-level `grant` admits it. |
366
+ | Grant | Pool grants default to deny; only literal `true` admits. Other returns, throws, and undeclared names produce an identical 404 status, body, and headers, after auth. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
367
+ | Timing | Declared names await grants, so timing is not hidden. Keep grants pure and fast and names unsecret; names grant no access. The operator log records the refusal reason. |
368
+ | Construction | Malformed names, unknown options or connectors, empty pools, a non-boolean `readOnly`, a retired `trust`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
369
+ | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
370
+ | Drift | No wildcards; tool grants are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
313
371
 
314
372
  ## Shared and personal auth
315
373
 
@@ -809,6 +867,7 @@ createConnecta({
809
867
  activityAccess: ({ id }) => id === "owner-id",
810
868
  },
811
869
  connectors, executor,
870
+ readOnly: false, // required: true refuses every write call
812
871
  });
813
872
  ```
814
873
 
@@ -821,7 +880,7 @@ Access trusts the identity validated at the Worker edge.
821
880
 
822
881
  ```ts
823
882
  import { cloudflareAccessAuth } from "@zackbart/connecta/auth/cloudflare-access";
824
- createConnecta({ auth: cloudflareAccessAuth(), connectors, executor });
883
+ createConnecta({ auth: cloudflareAccessAuth(), connectors, executor, readOnly: false });
825
884
  ```
826
885
 
827
886
  The adapter trusts only `ctx.access`, which Cloudflare attaches after validating
@@ -1011,7 +1070,7 @@ fallbacks, including a failed listing followed by cached annotations, cannot
1011
1070
  authorize a re-run. Normal calls retain their existing stale-fallback behavior.
1012
1071
  Before an accepted retry restarts any call, it rechecks every entered read.
1013
1072
  A write classification, changed classification digest, or stale catalog returns
1014
- `auth_replay_refused` with reconciliation guidance, including in trusted programs.
1073
+ `auth_replay_refused` with reconciliation guidance, including in programs on a writable endpoint.
1015
1074
  Pre-invocation credential-resolution eligibility is unchanged.
1016
1075
  Custom vaults may implement the host-only `requestStateKey` method; without it, the
1017
1076
  deployment retains the ordinary handoff. It must return a stable,
@@ -48,7 +48,7 @@ QuickJS subpath:
48
48
  import { createConnecta } from "@zackbart/connecta";
49
49
  import { quickJsExecutor } from "@zackbart/connecta/quickjs";
50
50
 
51
- createConnecta({ executor: quickJsExecutor() /* connectors, auth, storage… */ });
51
+ createConnecta({ executor: quickJsExecutor(), readOnly: false /* connectors, auth, storage… */ });
52
52
  ```
53
53
 
54
54
  `quickJsExecutor()` runs each program in a disposable child process, with CPU,
@@ -61,7 +61,7 @@ On Workers the Worker Loader is the sandbox; Dynamic Workers require the Paid pl
61
61
  ```ts
62
62
  import { workerExecutor } from "@zackbart/connecta/worker";
63
63
 
64
- createConnecta({ executor: workerExecutor({ loader: env.LOADER }) });
64
+ createConnecta({ executor: workerExecutor({ loader: env.LOADER }), readOnly: false /* … */ });
65
65
  ```
66
66
 
67
67
  The adapter uses only `loader` and an optional `timeout`. Its admission defaults
@@ -283,7 +283,10 @@ signature is byte-identical to the one `search_tools` returns.
283
283
  integration is ambiguous. `safety: "readOnly"` selects stored read verdicts;
284
284
  `"approvalRequired"` selects stored write verdicts; omitted or `"all"` selects
285
285
  both. Search and describe rows carry `classification`. These filters grant no
286
- authority and change no pool trust or call admission decision.
286
+ authority and change no call admission decision. On a read-only endpoint,
287
+ search and describe omit write tools (`W12`): a write is absent from every
288
+ filter, and describing one is `unknown_tool`. `connecta.skill` guides are not
289
+ filtered and may name such a write; calling it is still refused.
287
290
 
288
291
  **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`
289
292
  (declared properties only), and `outputKeys`: the names the rendered schema
@@ -394,10 +397,10 @@ and a 32 KiB bridge ceiling. `page` is zero-based and mutually exclusive with
394
397
  are at least four bytes wide and align both edges to UTF-8 boundaries.
395
398
  Follow `nextOffset`, reassemble inside the program, and return a reduced value.
396
399
  Before returning a page, the host rechecks current authentication, connector/tool
397
- grants, pool membership, and endpoint trust against both the original and current
398
- tool classification. A read-only endpoint cannot page a write result, so a
399
- direct write there returns an inline truncation notice without stashing the
400
- result or advertising a paging action. Binding
400
+ grants, pool membership, and whether the endpoint is read-only against both the
401
+ original and current tool classification. A read-only endpoint cannot page a
402
+ write result; it never runs a write, so any write result it is asked for came
403
+ from a writable endpoint. Binding
401
404
  mismatches, revoked access, and old entries without bindings are `not_found`, as
402
405
  are unknown or expired ids; a storage failure is `unavailable`. IDs are random UUIDs.
403
406
  The `get_result` meta-tool is removed.
@@ -477,7 +480,7 @@ Connecta passes an admitted decoded URI as an opaque MCP `resources/read`
477
480
  argument to the configured endpoint. It never fetches that
478
481
  URI or creates a connector from it.
479
482
 
480
- Resources are protocol reads in both `read-only` and `trusted` pools. A read
483
+ Resources are protocol reads on read-only and writable endpoints alike. A read
481
484
  requires a grant to the whole connector through the endpoint/pool and identity
482
485
  intersection. Exact-tool grants, including guarded grants, authorize no
483
486
  resources. An absent, hidden, tool-restricted or resource-incapable connector
@@ -553,27 +556,27 @@ URL-elicitation hosts retain their existing flow.
553
556
  may do. A provider maps each downstream failure to the code that tells the
554
557
  caller what to do next, and never invents a cause it was not told.
555
558
 
556
- | Code | Raised when | `retryable` |
557
- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
558
- | `unknown_address` | no visible connector owns the tool address or grants the resource reader | false |
559
- | `unknown_tool` | the connector has no such tool | false |
560
- | `destructive_tool_requires_approval` | a program in a read-only pool attempted a write (`E4`) | false |
561
- | `auth_required` | the credential is missing, expired, or rejected | false |
562
- | `invalid_args` | arguments or discovery bounds were rejected | false |
563
- | `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap | false |
564
- | `conflict` | the write named a base version someone else already moved past; nothing changed. `details.current` says where things stand (at most 20 whole-number entries) — re-read, reapply, retry with that base | false |
565
- | `input_required_unsupported` | a downstream asked for input; use `nextAction` for the equivalent direct call | false |
566
- | `rate_limited` | the downstream reported a rate limit | true |
567
- | `unavailable` | the downstream is down or unreachable; optional sanitized `details.host` and `details.code` describe the transport failure without paths, queries, credentials, or provider prose | true |
568
- | `timeout` | a call or sandbox deadline expired; details name operation, stage, elapsedMs, and deadlineMs | per operation |
569
- | `program_error` | guest JavaScript failed; `details.name`, `line`, and a fixed repair hint describe it | false |
570
- | `cancelled` | the run ended while this call was in flight (`E5`) | false |
571
- | `connector_call_failed` | anything else the connector threw | per message |
572
- | `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
573
- | `result_processing_failed` | the result could not be prepared | per message |
574
- | `result_too_large` | a discovery response exceeded its byte bound | false |
575
- | `budget_exceeded` | the run exhausted a host-call, write, or emitted-output budget | false |
576
- | `write_outcome_unknown` | a trusted-pool write was sent and no answer came back; it is never sent again (`W9`) | false |
559
+ | Code | Raised when | `retryable` |
560
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
561
+ | `unknown_address` | no visible connector owns the tool address or grants the resource reader | false |
562
+ | `unknown_tool` | the connector has no such tool | false |
563
+ | `read_only_endpoint` | a program on a read-only endpoint attempted a write (`E4`) | false |
564
+ | `auth_required` | the credential is missing, expired, or rejected | false |
565
+ | `invalid_args` | arguments or discovery bounds were rejected | false |
566
+ | `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap | false |
567
+ | `conflict` | the write named a base version someone else already moved past; nothing changed. `details.current` says where things stand (at most 20 whole-number entries) — re-read, reapply, retry with that base | false |
568
+ | `input_required_unsupported` | a downstream asked for input; use `nextAction` for the equivalent direct call | false |
569
+ | `rate_limited` | the downstream reported a rate limit | true |
570
+ | `unavailable` | the downstream is down or unreachable; optional sanitized `details.host` and `details.code` describe the transport failure without paths, queries, credentials, or provider prose | true |
571
+ | `timeout` | a call or sandbox deadline expired; details name operation, stage, elapsedMs, and deadlineMs | per operation |
572
+ | `program_error` | guest JavaScript failed; `details.name`, `line`, and a fixed repair hint describe it | false |
573
+ | `cancelled` | the run ended while this call was in flight (`E5`) | false |
574
+ | `connector_call_failed` | anything else the connector threw | per message |
575
+ | `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
576
+ | `result_processing_failed` | the result could not be prepared | per message |
577
+ | `result_too_large` | a discovery response exceeded its byte bound | false |
578
+ | `budget_exceeded` | the run exhausted a host-call, write, or emitted-output budget | false |
579
+ | `write_outcome_unknown` | a program write was sent and no answer came back; it is never sent again (`W9`) | false |
577
580
 
578
581
  **E3.** `auth_required` carries the same recovery envelope as `call_tool`:
579
582
  `connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
@@ -581,16 +584,13 @@ caller what to do next, and never invents a cause it was not told.
581
584
  sentence. A program cannot recover credentials — only an operator can — so stop
582
585
  and let the failure reach the model.
583
586
 
584
- **E4.** A tool classified as a write never runs in a
585
- program unless the pool is trusted (`W12`). It is refused with
586
- `destructive_tool_requires_approval` before validation and before anything is
587
- sent, `nextAction` carrying its canonical address to `call_destructive_tool`
588
- and a schema-filtered snapshot within the 512-byte echo budget. `argsRedacted`
589
- marks partial or withheld arguments; use the original arguments if this flag is
590
- set or `args` is absent. Snapshots are never clipped. The host's prompt on that
591
- call is the approval, and the only one: generated code can neither mint the
592
- capability nor approve its own write, and the model's short `reason` grants no
593
- authority and never goes downstream.
587
+ **E4.** A tool classified as a write never runs on a read-only endpoint
588
+ (`W12`). A program's call is refused with `read_only_endpoint` before
589
+ validation and before anything is sent. The refusal carries no `nextAction` and
590
+ echoes no arguments: no route on that endpoint writes, so the program reports
591
+ it rather than retrying. Generated code can neither mint the capability nor
592
+ change the endpoint; on a writable endpoint, approval of the program as a write
593
+ belongs to the client.
594
594
 
595
595
  **E5.** Failures of the _execution_, not of a call, never appear inside the
596
596
  guest: admission rejection (`executor_overloaded`, retryable, with
@@ -631,7 +631,7 @@ Program diagnostic fields are bounded before transport, including escaped text;
631
631
  a large custom error name cannot turn a failure into a truncated success.
632
632
 
633
633
  **E7.** `retryable` for `unknown_address`, `unknown_tool`, and
634
- `destructive_tool_requires_approval` is pinned false, never inferred from an
634
+ `read_only_endpoint` is pinned false, never inferred from an
635
635
  address containing `503`, `429`, or `temporar`. The first two carry
636
636
  `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }`
637
637
  — scoped discovery keyed to the surface the caller has, since a program cannot
@@ -686,7 +686,7 @@ Caught call failures retain that connector visibility when catalog loading or
686
686
  tool resolution fails. Recovery messages listing configured connectors or
687
687
  local guides retain visibility of those contributors too.
688
688
  Each page rechecks current auth,
689
- grants, pool membership, and trust. Reading a stash inside a program carries
689
+ grants, pool membership, and the endpoint's `readOnly`. Reading a stash inside a program carries
690
690
  its dependencies forward to any new return stash. The page cap is the smallest
691
691
  of the deployment cap and contributing connectors' effective `maxResultBytes`.
692
692
  Read-only endpoints cannot page write results. A UUID is a handle, never a grant.
@@ -882,17 +882,17 @@ deadline past the ceiling means raising the ceiling too.
882
882
  **L4.** Per-execution bounds that are contract, identical in both executors
883
883
  because connecta enforces them above the sandbox:
884
884
 
885
- | Bound | Value |
886
- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
887
- | Host calls per execution, shared by `search`, `describe`, `call`, and `read` | 20 by default, `execute.maxHostCalls` |
888
- | Trusted-pool writes per run, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
889
- | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
890
- | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
891
- | `describe` addresses | ≤ 100 |
892
- | `describe` nearby suggestions | ≤ 3 canonical addresses per failed entry |
893
- | Caller text echoed by `describe` recovery | ≤ 512 UTF-8 bytes per field, plus `…` |
894
- | Result | 24,000 serialized characters |
895
- | Logs presented to the model | 4,000 characters |
885
+ | Bound | Value |
886
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
887
+ | Host calls per execution, shared by `search`, `describe`, `call`, and `read` | 20 by default, `execute.maxHostCalls` |
888
+ | Writes per run on a writable endpoint, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
889
+ | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
890
+ | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
891
+ | `describe` addresses | ≤ 100 |
892
+ | `describe` nearby suggestions | ≤ 3 canonical addresses per failed entry |
893
+ | Caller text echoed by `describe` recovery | ≤ 512 UTF-8 bytes per field, plus `…` |
894
+ | Result | 24,000 serialized characters |
895
+ | Logs presented to the model | 4,000 characters |
896
896
 
897
897
  Every `call`, `read`, `search`, and `describe` spends one host call on entry, before
898
898
  resolution, validation, or dispatch. Catching a local refusal refunds nothing.
@@ -901,7 +901,7 @@ The first call beyond the budget ends the host run with one non-retryable
901
901
  `try/catch` cannot swallow it. Later host access, including `emit`, stops; pending
902
902
  replies are withheld. The refusal reaches no connector, returns no partial result,
903
903
  and discards accepted emits (`M4`). Calls still resolving or awaiting admission
904
- are cancelled, including trusted-pool writes past their gate. Dispatched trusted-pool writes
904
+ are cancelled, including writes past their gate. Dispatched writes
905
905
  drain under `W9`; unknown outcomes retain precedence. The lease then releases:
906
906
  QuickJS terminates its child, and the Worker adapter disposes RPC and loader
907
907
  handles independently of the guest deadline. Workers guest computation may
@@ -1003,22 +1003,22 @@ payload-free _by construction_ means the event has nowhere to put a payload.
1003
1003
  distinctive output is the program source — exactly what a payload-free history
1004
1004
  must never keep.
1005
1005
 
1006
- **V5.** A trusted-pool write a program issues after it has already returned is not
1006
+ **V5.** A write a program issues after it has already returned is not
1007
1007
  sent and records nothing: it was never an attempt (`W9`).
1008
1008
 
1009
1009
  ## Writes
1010
1010
 
1011
- Pool trust belongs to deployment code. The default `/mcp` endpoint uses
1012
- `trust: "read-only"`; each named pool has its own `trust`, also defaulting to
1013
- `"read-only"`. A trusted default endpoint does not make named pools trusted.
1014
- The default preserves refusal of ordinary program writes. A deployment must opt into program writes explicitly.
1011
+ Whether an endpoint writes belongs to deployment code. Every deployment sets
1012
+ `readOnly` for `/mcp` explicitly; construction fails without it. Each named pool inherits
1013
+ the deployment's `readOnly` unless it sets its own, in either direction. The
1014
+ client owns approval; Connecta answers one question per endpoint: can it write.
1015
1015
 
1016
1016
  ```ts
1017
1017
  createConnecta({
1018
1018
  connectors, executor,
1019
- trust: "read-only",
1019
+ readOnly: false, // required: false writes, true refuses every write call
1020
1020
  pools: {
1021
- automation: { tools: ["posthog"], trust: "trusted", grant },
1021
+ reporting: { tools: ["posthog"], readOnly: true, grant },
1022
1022
  },
1023
1023
  classification: {
1024
1024
  posthog: { exec: "read" }, // only for a deployment with a read-only exec contract
@@ -1027,6 +1027,9 @@ createConnecta({
1027
1027
  });
1028
1028
  ```
1029
1029
 
1030
+ The removed `trust` option, on the deployment or a pool, fails construction
1031
+ with a message naming `readOnly` (`INV-11`).
1032
+
1030
1033
  The registry computes one `classifyTool` verdict on each request-scoped
1031
1034
  published catalog entry. It caches and persists only raw downstream facts.
1032
1035
  Every caller consumes that stored verdict. Config cannot classify an individual
@@ -1044,7 +1047,7 @@ fail construction; unknown remote tool keys fail publication of the whole
1044
1047
  catalog. Overrides are exact tool names, including names containing dots.
1045
1048
  They are never inferred from descriptions or arguments.
1046
1049
 
1047
- **W9.** Programs in trusted pools may write. A dispatched write is awaited even
1050
+ **W9.** Programs on a writable endpoint may write. A dispatched write is awaited even
1048
1051
  if the program does not await it, bounded by its host-call deadline. A write
1049
1052
  that reaches accounting after the program settles is not sent. An unanswered
1050
1053
  write makes the run fail `write_outcome_unknown` with
@@ -1054,7 +1057,7 @@ write is automatically retried, including an ambiguous timeout. A downstream
1054
1057
  refusal or a tool's `isError` can establish failure; a timeout or an unanswered
1055
1058
  transport failure leaves the outcome unknown.
1056
1059
 
1057
- **W10.** `execute.maxWrites`, default 10, bounds writes in trusted programs in
1060
+ **W10.** `execute.maxWrites`, default 10, bounds a program's writes in
1058
1061
  addition to the host-call budget. It is checked after argument validation and
1059
1062
  before admission, so an over-budget write spends no permit.
1060
1063
 
@@ -1067,14 +1070,20 @@ reconciliation. Any new call must use the original arguments when its echo is
1067
1070
  partial or absent. A returned `writes` count cannot make a run safe to repeat.
1068
1071
  See [argument echo rules](./meta-tools.md#echo-budgets) for supported schema shapes.
1069
1072
 
1070
- **W12.** `trusted` endpoints annotate `execute_code` as a write and permit
1071
- program writes. `read-only` endpoints annotate it as read-only and refuse
1072
- program writes before validation, with `call_destructive_tool` as the recovery.
1073
- `call_tool` accepts only reads in either tier; `call_destructive_tool` may send
1074
- writes in either tier. Approval belongs to the host. Trust grants no tool or
1075
- connector access and cannot be set by guest code, request arguments, or a
1076
- downstream catalog. `execute.approval`, `Connector.approval`, and per-tool
1077
- approval exemptions are removed. `W1`–`W8` and `W11` remain retired.
1073
+ **W12.** Writable endpoints annotate `execute_code` as a write
1074
+ (`readOnlyHint: false, destructiveHint: true`) and permit program writes and
1075
+ `call_destructive_tool`; `call_tool` still accepts only reads and refuses a write
1076
+ with `destructive_tool_requires_approval`. Read-only endpoints annotate
1077
+ `execute_code` as read-only (`readOnlyHint: true, destructiveHint: false`),
1078
+ omit write tools from `search_tools`, `connecta.search`, and
1079
+ `connecta.describe`, and refuse every write call through `call_tool`,
1080
+ `call_destructive_tool`, and programs with `read_only_endpoint` before
1081
+ validation or dispatch. Guides and skills are documentation and may still name
1082
+ those tools. `tools/list` is the same six tools on both. Approval belongs to the client.
1083
+ `readOnly` grants no tool or connector access and cannot be set by guest code,
1084
+ request arguments, or a downstream catalog. `execute.approval`,
1085
+ `Connector.approval`, per-tool approval exemptions, and pool `trust` are
1086
+ removed. `W1`–`W8` and `W11` remain retired.
1078
1087
 
1079
1088
  ## Executor exceptions
1080
1089
 
@@ -1225,8 +1234,8 @@ rejection, and branded adapter acceptance across module copies.
1225
1234
  | `L5`, `L7`, `X2` | `test/quickjs-executor.node.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue) |
1226
1235
  | `L6`, `X10` | `test/guest-api-contract-quickjs.node.test.ts` and `test/guest-api-contract.test.ts` (whole oversized reads and one-write pageable handles), `test/sqlite-storage.node.test.ts` and `test/d1-storage.node.test.ts` (shared stash transfers), `test/quickjs-executor.node.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.node.test.ts` (outer reply serialization failure settles the call) |
1227
1236
  | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, the identity clamp, the one-attempt floor), `test/operator-view.test.ts`, `test/sql-storage-contract.ts`, run by `test/d1-storage.node.test.ts` and `test/sqlite-storage.node.test.ts` (historical pause and approval rows still render and round-trip) |
1228
- | `V5`, `W9` | `test/program-writes.test.ts` (an unawaited trusted-pool write finished and recorded, its unknown outcome reported, counts on a failed program, the classification table), `test/invocation-pipeline.test.ts` (the gate after validation, an unrecorded refusal) |
1229
- | `W10`, `W12` | `test/program-writes.test.ts` (a trusted-pool write runs and every other write keeps `E4`, the write budget, pool trust and override precedence, `call_tool` still refusing, search and describe verdicts, construction refusals), `test/operator-ui-model.test.ts`, `test/browser/operator-ui.spec.ts` (the badge) |
1237
+ | `V5`, `W9` | `test/program-writes.test.ts` (an unawaited program write finished and recorded, its unknown outcome reported, counts on a failed program, the classification table), `test/invocation-pipeline.test.ts` (the gate after validation, an unrecorded refusal) |
1238
+ | `W10`, `W12` | `test/program-writes.test.ts` (a writable endpoint's program write runs, a read-only endpoint omits writes from search and describe and refuses every write call on every path, including a write its guide names, a required deployment `readOnly`, the write budget, `readOnly` inheritance and overrides, `call_tool` still refusing, search and describe verdicts, construction refusals including retired `trust`), `test/operator-ui-model.test.ts`, `test/browser/operator-ui.spec.ts` (the badge) |
1230
1239
  | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (rejected shapes, bounded native snapshots, paging before metadata traversal, and cyclic presentation refusal) |
1231
1240
  | `M2`, `M3` | `test/guest-api-contract.test.ts` (delivery order, truncated return plus delivered blocks), `test/execute-emit.test.ts` (envelope, `structuredContent`, byte-for-byte no-emit path) |
1232
1241
  | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths), `test/quickjs-executor.node.test.ts` (mid-run shutdown) |
@@ -64,7 +64,7 @@ through to `fetch`. The example owns those details and
64
64
 
65
65
  ## Compose capabilities and access
66
66
 
67
- Both configurations use `defineConfig((env) => …)` and a required executor.
67
+ Both configurations use `defineConfig((env) => …)`, a required executor, and a required `readOnly` choice.
68
68
  [Integrating services](./integrating.md) explains the `connectors` array.
69
69
  Configuration selects optional factories through explicit slots:
70
70
 
@@ -96,10 +96,15 @@ personal connection, token management, and activity access explicit.
96
96
  [Clerk setup](./auth.md#clerk-oauth-tokens-and-operator-sessions) owns resource-bound
97
97
  tokens and `aud_claim_enabled`. A person's operator session is not an MCP token.
98
98
 
99
- Root and named pools default to `read-only`. A `trusted` pool allows writes
100
- inside programs and marks `execute_code` as a write. Host approval remains the
101
- host's decision. Pool membership intersects the caller's grants; it never
102
- widens them. See [pools](./auth.md#pools) before changing trust.
99
+ Every deployment must set `readOnly` in its configuration; startup fails without
100
+ it, and the template and Worker example set `readOnly: false`. Each named pool
101
+ inherits that value unless it sets its own. A writable endpoint (`false`) allows
102
+ writes through `call_destructive_tool` and programs and marks `execute_code` as
103
+ a write; the client decides what to approve. A read-only endpoint (`true`) omits
104
+ write tools from search and describe and refuses every write call; provider
105
+ guides may still name those tools. Pool membership intersects
106
+ the caller's grants; it never widens them. See [pools](./auth.md#pools) before
107
+ changing `readOnly`.
103
108
 
104
109
  ## Verify and diagnose
105
110