@zackbart/connecta 0.31.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 (95) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +15 -13
  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/catalog-service.js +7 -2
  7. package/dist/config-value-sources.js +6 -3
  8. package/dist/config.d.ts +26 -8
  9. package/dist/config.js +38 -16
  10. package/dist/connector-access.d.ts +2 -1
  11. package/dist/connectors/guarded-fetch.d.ts +9 -0
  12. package/dist/connectors/guarded-fetch.js +7 -0
  13. package/dist/describe-config.d.ts +6 -3
  14. package/dist/describe-config.js +3 -2
  15. package/dist/errors.d.ts +1 -1
  16. package/dist/errors.js +1 -0
  17. package/dist/execute.js +16 -14
  18. package/dist/index.js +4 -3
  19. package/dist/invocation.js +7 -4
  20. package/dist/meta-tools.js +17 -19
  21. package/dist/operator-record.d.ts +2 -2
  22. package/dist/operator-record.js +2 -0
  23. package/dist/operator-ui/contract.d.ts +5 -4
  24. package/dist/operator-ui/generated.js +2 -2
  25. package/dist/operator-ui/model.d.ts +1 -1
  26. package/dist/operator-ui/view.js +1 -0
  27. package/dist/program-writes.js +2 -2
  28. package/dist/provider.d.ts +2 -3
  29. package/dist/providers/_shared/google/workspace.js +1 -1
  30. package/dist/providers/breeze/skill.generated.js +2 -2
  31. package/dist/providers/cloudflare/skill.generated.js +1 -1
  32. package/dist/providers/docs/index.d.ts +1 -1
  33. package/dist/providers/docs/index.js +2 -2
  34. package/dist/providers/drive/index.d.ts +1 -1
  35. package/dist/providers/drive/index.js +1 -1
  36. package/dist/providers/forms/index.d.ts +1 -1
  37. package/dist/providers/forms/index.js +2 -2
  38. package/dist/providers/github/skill.generated.js +2 -2
  39. package/dist/providers/gmail/index.d.ts +1 -1
  40. package/dist/providers/gmail/index.js +274 -23
  41. package/dist/providers/gmail/skill.generated.js +2 -2
  42. package/dist/providers/linear/index.js +1 -1
  43. package/dist/providers/mixpanel/index.d.ts +27 -12
  44. package/dist/providers/mixpanel/index.js +58 -6
  45. package/dist/providers/mixpanel/operations.js +20 -0
  46. package/dist/providers/mixpanel/rest.js +603 -0
  47. package/dist/providers/mixpanel/skill.generated.js +3 -2
  48. package/dist/providers/mixpanel/value-safety.js +16 -0
  49. package/dist/providers/notion/index.js +16 -0
  50. package/dist/providers/notion/rest.js +138 -19
  51. package/dist/providers/notion/skill.generated.js +4 -4
  52. package/dist/providers/planning-center/index.d.ts +27 -0
  53. package/dist/providers/planning-center/index.js +286 -24
  54. package/dist/providers/planning-center/skill.generated.js +24 -4
  55. package/dist/providers/sheets/index.d.ts +1 -1
  56. package/dist/providers/sheets/index.js +3 -3
  57. package/dist/providers/slides/index.d.ts +1 -1
  58. package/dist/providers/slides/index.js +3 -3
  59. package/dist/providers/stripe/skill.generated.js +2 -2
  60. package/dist/providers/vercel/index.js +2 -2
  61. package/dist/providers/vercel/skill.generated.js +2 -2
  62. package/dist/providers/wispr-flow/index.d.ts +14 -0
  63. package/dist/providers/wispr-flow/index.js +117 -0
  64. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  65. package/dist/routes/mcp.js +30 -10
  66. package/dist/routes/operator.js +14 -7
  67. package/dist/routes/ui-config.js +2 -2
  68. package/dist/server.js +3 -0
  69. package/dist/skills.js +5 -4
  70. package/dist/tool-safety.js +13 -5
  71. package/dist/types.d.ts +1 -1
  72. package/dist/ui.d.ts +1 -1
  73. package/dist/ui.js +1 -1
  74. package/dist/usage-guide.js +3 -3
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/documentation/architecture.md +38 -23
  78. package/documentation/auth.md +86 -27
  79. package/documentation/code-mode.md +81 -72
  80. package/documentation/deploying.md +10 -5
  81. package/documentation/integrating.md +56 -4
  82. package/documentation/meta-tools.md +44 -37
  83. package/documentation/operating.md +11 -8
  84. package/documentation/operator-tests.md +2 -3
  85. package/documentation/operator-ui.md +9 -3
  86. package/documentation/provider-auth-migration.md +5 -4
  87. package/documentation/provider-migration-0.29.md +1 -1
  88. package/examples/worker/AGENTS.md +5 -0
  89. package/examples/worker/src/connecta.config.ts +5 -0
  90. package/package.json +5 -1
  91. package/templates/node/AGENTS.md +6 -4
  92. package/templates/node/README.md +4 -2
  93. package/templates/node/package.json +1 -1
  94. package/templates/node/src/connecta.config.ts +5 -0
  95. package/dist/tool-safety.d.ts +0 -9
@@ -85,20 +85,20 @@ table, security headers. Route _order_ is the contract — several routes would
85
85
  behave differently if they were reachable in another order — so read the table
86
86
  top to bottom.
87
87
 
88
- | Order | Route | Notes |
89
- | ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
90
- | 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
91
- | 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query _assigned_ onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
92
- | 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
93
- | 1 | `/oauth/client-metadata/<connectorId>` | Public GET-only CIMD for configured self-hosted OAuth connectors; fixed `publicUrl`, no auth or storage. |
94
- | 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
95
- | 1 | `/connect/<connectorId>`, `/oauth/callback/<connectorId>` | GET-only browser OAuth routes, before wildcard OPTIONS. Both verify the initiating user and management permission, independent of the UI. |
96
- | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
97
- | 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
98
- | 3 | `/.well-known/*` | Auth metadata, or 404. |
99
- | 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata. |
100
- | 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. Body-confirmed modern listens skip admission and are refused after auth and SDK validation. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
101
- | 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
88
+ | Order | Route | Notes |
89
+ | ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
90
+ | 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
91
+ | 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query _assigned_ onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
92
+ | 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
93
+ | 1 | `/oauth/client-metadata/<connectorId>` | Public GET-only CIMD for configured self-hosted OAuth connectors; fixed `publicUrl`, no auth or storage. |
94
+ | 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
95
+ | 1 | `/connect/<connectorId>`, `/oauth/callback/<connectorId>` | GET-only browser OAuth routes, before wildcard OPTIONS. Both verify the initiating user and management permission, independent of the UI. |
96
+ | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
97
+ | 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
98
+ | 3 | `/.well-known/*` | Public auth metadata, or 404. Protected-resource discovery does not authenticate callers or evaluate endpoint grants. |
99
+ | 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata. |
100
+ | 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. Body-confirmed modern listens skip admission and are refused after auth and SDK validation. An undeclared pool, a refusing endpoint grant, and a throwing endpoint grant are one identical 404; see [pools](./auth.md#pools). |
101
+ | 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
102
102
 
103
103
  Every response leaves through `withSecurityHeaders`. HTML uses one common
104
104
  security-header helper, including status and error pages. Operator shells
@@ -131,12 +131,17 @@ An admitted non-preflight `/mcp` request then takes five steps in
131
131
  3. **Narrow to the pool.** On `/mcp/<pool>`, look the name up, run its grant
132
132
  against the identity, then `intersectAccess` the pool with the identity's own
133
133
  access. A pool can never widen a view; anything else is a 404 naming no pool.
134
+ The endpoint's `readOnly` is the pool's own value, else the deployment's;
135
+ plain `/mcp` uses the deployment's. It travels to the meta-tools and the
136
+ program as one boolean that drops writes from `CatalogService` search and
137
+ describe and makes `writeRefusal` (`src/tool-safety.ts`) refuse every write
138
+ call. Guides and skills are not filtered.
134
139
  4. **Derive the registry view.** One `registry.scoped(...)` call with those
135
140
  connector ids, exact `connector.tool` addresses when the identity declares a
136
141
  narrower slice, and the subject and principal keys. Personal connectors use
137
142
  the principal partition and result paging the subject partition with principal,
138
143
  endpoint/pool, origin, connector, and tool bindings. Paging rechecks current
139
- grants, pool membership, and trust; no caller parameter selects a partition (`test/identity-scope.test.ts`).
144
+ grants, pool membership, and the endpoint's `readOnly`; no caller parameter selects a partition (`test/identity-scope.test.ts`).
140
145
  5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
141
146
  ([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
142
147
  handed out, and retiring a scoping boundary into fail-open is worse than any
@@ -217,13 +222,15 @@ or hands out, and a change usually belongs in exactly one of them:
217
222
  | `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
218
223
  | `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
219
224
  | `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
220
- | `src/program-writes.ts` | Trusted-pool write accounting: write budgets, dispatch draining, and known or unknown outcomes. |
225
+ | `src/program-writes.ts` | Program write accounting on writable endpoints: write budgets, dispatch draining, and known or unknown outcomes. |
221
226
 
222
227
  `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
223
228
  services, `CatalogService` and `InvocationService`. That is the point: a
224
229
  program's `connecta.call` and a top-level `call_tool` reach
225
230
  `InvocationService.invoke` by different routes and get the same admission, the
226
- same credential resolution, and the same fail-closed read-only check.
231
+ same credential resolution, and the same fail-closed read-only check: one
232
+ `writeRefusal` decision per call, from the stored classification, the calling
233
+ surface, and the endpoint's `readOnly`.
227
234
  `test/execute.test.ts` asserts the parity directly, because a sandbox path that
228
235
  quietly diverges is how generated code would mint a capability.
229
236
 
@@ -723,18 +730,26 @@ connection context `create` supplies, the maintained text, then deployment
723
730
  instructions, which append and never replace. `src/provider.ts` imports neither
724
731
  transport, so an `api()` provider gains no MCP client or Effect graph from it
725
732
  (`test/purity.node.test.ts`). The factory carries its `definition`, which build and
726
- check tools read instead of keeping provider lists. All twenty providers
727
- live in their own folders. The eight hosted implementations, including the
733
+ check tools read instead of keeping provider lists. Providers
734
+ live in their own folders. Hosted implementations, including the
728
735
  MCP branches of Notion, Vercel, and Cloudflare, use reviewed presets over
729
- `remoteMcp({ classify })`. The eleven API-only factories retain an internal
736
+ `remoteMcp({ classify })`. The migrated API-only factories retain an internal
730
737
  adapter to the same definition and description-stamping path. GitHub composes hosted tools with a scope-enforced REST complement.
731
- A `"dual"` provider lets `auth` select the implementation
738
+ A `"dual"` provider selects one implementation at construction, usually through `auth`
732
739
  ([decision 0005](https://github.com/zackbart/connecta/blob/main/decisions/0005-auth-selects-implementation.md)):
733
740
  `variants(["auth", "type"], …)` closes each case's options, and
734
741
  `byAuth()` in `src/providers/_shared/rest/dispatch.ts` sends OAuth to
735
742
  `hostedOAuth()` (`remoteMcp()`, OAuth only, the reviewed classification) and a
736
- key to Connecta's REST connector. Stripe, Notion, Cloudflare, and Vercel are dual; `auth` is required.
737
- The REST connector (`src/providers/_shared/rest/`) serves
743
+ key to Connecta's REST connector. Stripe, Notion, Cloudflare, and Vercel require `auth`. Mixpanel is also dual,
744
+ with explicit `via: "mcp" | "api"` and MCP as the unchanged default: service accounts
745
+ reach both implementations, so auth cannot distinguish them. API mode requires
746
+ operator-managed service-account credentials and never falls back to MCP.
747
+ Mixpanel uses the same REST transport, operation matching and value-safety engine
748
+ for a narrow reviewed subset. Its official contracts are YAML and app identity
749
+ has no published response schema, so its guide and drift record carry explicit
750
+ manual review evidence instead of claiming a machine-checkable JSON pin.
751
+
752
+ The generic REST connector (`src/providers/_shared/rest/`) serves
738
753
  `<vendor>_api_search`, `_api_details`, `_api_read`, and `_api_write` over an
739
754
  `OperationIndex` that `npm run providers:spec` compiles offline from the
740
755
  provider's pinned `openapi.source.json` into `openapi.generated.ts`.
@@ -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,