@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,72 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.32.0 — 2026-10-14
6
+
7
+ Connecta 0.32.0 makes each endpoint simply writable or read-only, and lets a deployment keep chosen callers, such as an agent's token, off plain `/mcp` (#820, #821). A deployment can then give an agent one token and per-person pools, and the agent sees only the pool its client picks for each turn.
8
+
9
+ Every deployment must now choose `readOnly`, and this breaks existing configurations (#822). `readOnly` replaces the pool `trust` setting. The deployment's value applies to `/mcp`, and each pool inherits it unless it sets its own. A deployment that sets neither `readOnly` nor `trust` fails at startup and asks the operator to choose. One that still sets `trust` anywhere fails with a message naming `readOnly`. Replace `trust: "trusted"` with `readOnly: false`, and `trust: "read-only"` with `readOnly: true`. On a read-only endpoint, write tools are left out of search and describe, and every write is refused with `read_only_endpoint` before validation or dispatch, whether it comes from `call_tool`, `call_destructive_tool` or a program. That includes `call_destructive_tool`, which a read-only pool used to allow with host approval. On a writable endpoint, `call_destructive_tool` and programs write, and the MCP client owns approval. Guides are documentation and are not filtered, so a guide may name a tool the endpoint refuses. A deployment that set no `trust` used to let programs read only and allow host-approved direct writes. Choose `readOnly: true` to stop all writes, or `readOnly: false` to let programs write too. A pool that relied on the old read-only default now inherits the deployment's value, so set `readOnly: true` on every pool that must stay read-only. The Node template and Worker example set `readOnly: false`.
10
+
11
+ A new top-level `grant(identity)` admits callers to plain `/mcp` the way pool grants admit callers to pools (#824). Without it, everyone admitted today is still admitted. Only a literal `true` admits. A refused caller gets the same 404 as a refused or undeclared pool, and can still open any pool whose own grant admits it. Result paging and the operator UI's catalog and config reads apply the same grant, so a refused token cannot read the full catalog through `/ui`. OAuth protected-resource metadata stays public, so MCP clients can still discover where to sign in. Deployments that don't set `grant` need no changes.
12
+
13
+ `mixpanel()` gains a REST implementation, `via: "api"`, for what the hosted MCP server cannot answer (#825). It covers raw event export with a size bound, one user's activity stream, profile queries, saved Insights and Funnels reports, retention queries and event import. It authenticates with an operator-managed service account, takes `project_id` per call (with optional `defaultProjectId`) and lists the projects the account can reach. Profile updates are left out because Mixpanel's profile endpoint requires the project token, which service-account credentials alone don't provide. The hosted MCP implementation stays the default, so existing Mixpanel configurations are unchanged. A deployment that wants both declares two connections.
14
+
15
+ The release also adds `excludeApps` to `planningCenter()`, Gmail `get_attachment` and a maintained Wispr Flow provider, and updates the Notion connector to Notion's current limits and errors. CI now runs its checks in parallel, and the redaction tests no longer depend on wall-clock time.
16
+
17
+ ### Added
18
+
19
+ - Let deployments configure a grant for plain `/mcp`, independently of named pools. Refusals use the same 404 as refused pools, result paging and operator catalog reads respect the grant while OAuth metadata discovery stays public, and the operator UI and configuration snapshot report whether it is configured.
20
+ - Read Gmail attachment text and small binary files with `get_attachment`. Attachment listings now expose immutable `partId` identifiers for reads across rotated download ids and for inline attachments. Binary content uses standard base64 for Drive uploads; downloads and whole results have explicit size limits.
21
+
22
+ Parsed forwarded attachments include their headers and readable body after checking descendant sizes. MIME charset parameters accept whitespace and quoted values, unsupported charsets are reported, and inconsistent attachment byte counts are refused.
23
+ - Let `mixpanel()` use service-account REST APIs with `via: "api"` for reachable projects, bounded raw event export, profile activity and queries, Insights and Funnels reports, retention queries and event import. Existing hosted MCP configurations remain the default. Retention queries cap intervals at 90 only for day units, as Mixpanel documents.
24
+ - `planningCenter()` accepts `excludeApps`, a list of Planning Center products the connection never reaches, such as `["giving"]` for a deployment that keeps agents away from donor and financial data. An excluded product's named tools leave the catalog; the description, usage guide, and version pins stop naming it; and `pco_api_get` and `pco_api_mutate` refuse its paths with `provider_permission_denied` before sending, checked against the exact URL the transport would fetch. While any product is excluded, a hatch path must be literal (no dot segments, empty segments, backslashes, whitespace, or percent-encoded delimiters), and the connection also removes the People list tools and refuses People lists, list results, rules, and conditions by path or `include`, since a list's conditions can draw on any product; refuses webhook event and delivery payloads and any change to an existing webhook subscription; and accepts a new subscription only to a reachable product's event. Any product except People can be excluded; an unknown name or People throws at construction. The default excludes nothing and is unchanged. Exclusion does not hide records copied into a reachable product by the church's automations or staff, such as People workflow cards a Giving automation creates for first-time donors, notes, or custom fields; for a hard boundary, use a personal access token from a user with No Access in the excluded product, since Planning Center sets permissions per product for each user, and treat `excludeApps` as defense in depth.
25
+ - Add a maintained Wispr Flow hosted-MCP provider for OAuth access to Notetaker meetings, Scratchpad notes, calendar events and tasks, with reviewed tool classification and account-specific usage guidance.
26
+
27
+ ### Changed
28
+
29
+ - Run CI checks in parallel, cancel superseded PR runs, and request snapshot updates explicitly while preserving the full check gate.
30
+ - **Breaking:** Each endpoint is now writable or read-only, set by a plain `readOnly` boolean that replaces pool `trust`. The deployment's `readOnly` sets `/mcp` and is required for this release: a deployment that sets neither `readOnly` nor the removed `trust` fails at startup with a configuration error asking the operator to choose `readOnly: true` or `readOnly: false`. Each pool inherits the deployment value unless it sets its own, in either direction. The client owns approval. A writable endpoint (`false`) behaves as a `trusted` pool did: `call_destructive_tool` and `execute_code` programs write (program writes stay capped by `execute.maxWrites`), `call_tool` still refuses writes, and `execute_code` is annotated `readOnlyHint: false, destructiveHint: true`. On a read-only endpoint (`true`), write tools are absent from `search_tools` results, `connecta.search`, and `connecta.describe`, and every write call through `call_tool`, `call_destructive_tool`, or a program is refused before validation or dispatch with the new code `read_only_endpoint` (`retryable: false`, no `nextAction`); reads still run, and `execute_code` is annotated `readOnlyHint: true, destructiveHint: false`. Guides are documentation and are not filtered: provider `SKILL.md` and usage guides, surfaced through `queryAnalysis.guideSummary`, `skills`, `connecta.skill`, and skill `resources/read`, may still name tools a read-only endpoint refuses. `tools/list` stays six tools on both. Resource reads, grants, `connectorAccess` (including guarded `requireReadOnly` grants), classification, and `execute.maxWrites` are unchanged. `describeConfig()` and the operator UI report `readOnly` instead of `trust`, and programs no longer receive `destructive_tool_requires_approval`, which remains `call_tool`'s refusal of a write on a writable endpoint.
31
+
32
+ To migrate, set `readOnly` explicitly in deployment code and remove every `trust`; a config that still sets `trust`, on the deployment or a pool, fails at construction with a message naming `readOnly`, and one that sets no `readOnly` fails too. Replace `trust: "trusted"` with `readOnly: false`. Replace `trust: "read-only"` with `readOnly: true`, and note that a read-only endpoint now also refuses writes through `call_destructive_tool`; a pool that should still allow host-approved direct writes but no program writes has no equivalent and must choose. A deployment that set no `trust` was read-only for programs and allowed host-approved direct writes; choose `readOnly: true` to keep it from writing at all, or `readOnly: false` to let programs write too. Pools that relied on the old read-only default inherit the deployment value, so set `readOnly: true` on each pool that must stay read-only under a writable deployment. The Node template and Worker example now set `readOnly: false`, and `connecta init` asks operators to choose.
33
+
34
+ ### Fixed
35
+
36
+ - Make redaction regression tests robust under machine load with CPU scaling, exact output and low-heap checks, without adding production instrumentation.
37
+ - `notion()` over an integration token now splits text longer than Notion's 2,000-character rich-text run limit into consecutive runs in `integration_create_page` and `integration_append_blocks`, instead of sending a request Notion rejects, and refuses text no single value can carry. Failures follow Notion's current contract: a 429 whose reason is `public_api_request_blocked` is no longer retryable, the retry window falls back to the body's `additional_data.retry_after`, a 403 names the workspace block limit, a 5xx on a write that Notion reports as committed names the created object or appended blocks so the caller reads them instead of repeating the write, and a 5xx on a write that carries Notion's `retry_guidance` relays it, unless the operation withholds Notion's error text, and is not retryable. A data source query stopped at Notion's 10,000-result cap reports `incomplete: true` from `integration_query_data_source` and `page.hasMore` without `page.next` from the generic read. Projections name native Notion icons and custom emoji and read an `unsupported` formula or rollup value as `null`. The OAuth connector classifies `notion-ai-search`, `notion-get-self`, `notion-get-tool-access`, and `notion-download-skill` as reads. The guide routes whole-page reads and Markdown writes through the page Markdown endpoints.
38
+
39
+ ## 0.31.0 — 2026-10-09
40
+
41
+ Connecta 0.31.0 lets the auth mode choose how Stripe, Notion, Cloudflare and Vercel connect (#801). `auth: { type: "oauth" }` connects the vendor's hosted MCP server, OAuth only. A key connects Connecta's own REST connector with an operator-managed credential: Stripe `apiKey`, Notion `token`, Cloudflare `apiToken` or `globalApiKey`, and Vercel `token`. Each implementation is now complete on its own. This replaces the 0.29 split, in which the hosted server owned writes and the REST surface refused them, so a Cloudflare API token could not change a DNS record. Decision record 0005 explains the choice. Deployments that use none of these four providers need no changes.
42
+
43
+ The timing comes from Stripe. Its hosted MCP server stops accepting `sk_` secret keys and untagged `rk_` restricted keys on 2026-10-31, so a deployment that sends one of those keys to `mcp.stripe.com` loses Stripe on that date. Upgrade before then. Stripe still documents agent-tagged restricted keys for its MCP server, but Connecta's hosted Stripe connection is now OAuth only, and every Stripe key, tagged or not, uses the REST connector. For these four providers, Connecta no longer sends a bearer key, literal header or operator credential to the hosted MCP server. Other hosted providers, such as RevenueCat, Mixpanel and GitHub, are unchanged.
44
+
45
+ Every key connector serves the same four tools. `<vendor>_api_search` and `<vendor>_api_details` read an operation index compiled from the vendor's pinned OpenAPI document. `<vendor>_api_read` and `<vendor>_api_write` check each call's path and parameters against that index before sending it, and return `{ status, data, page? }`. The read tool admits GET plus reviewed read-only POSTs such as Stripe's invoice preview, Cloudflare analytics and Vercel observability queries. Each vendor keeps a few named tools. Stripe POST writes send the caller's `idempotencyKey`, or a generated one, as `Idempotency-Key` and return it; DELETE writes send none. When a deadline interrupts one of those POST writes, or its outcome is otherwise unknown, `uncertainCall.recovery` carries the generated key, or a caller-supplied key of at most 128 characters. A longer caller key is still sent to Stripe but not echoed there, since the caller already has it. Every Stripe request, v1 and v2, sends `Stripe-Version: 2026-09-30.endive`, the pinned index's version, whatever the account's default API version. Response shapes and accepted parameters follow that version, so check the fields programs rely on with `stripe_api_details`. Stripe's `connectedAccount` now works with an operator-managed key. `npm run providers:check` reports when a pinned index falls behind the vendor's live document.
46
+
47
+ Most existing configurations of these providers need edits. `auth` is now required, with no default, and omitting it throws an error that lists the valid types. `surface` is removed from Notion, Vercel and Cloudflare, along with Cloudflare `authentication` and its `credential` override. Stripe loses literal `headers` auth and hosted `credential` auth. Cloudflare bearer-token auth to `mcp.cloudflare.com` is removed, so those deployments move to `apiToken` and its REST tools. A credential stored under the same connector id carries over; run its Test action after upgrading.
48
+
49
+ Tool names change on the key paths. Cloudflare's `cloudflare_api_get` and `cloudflare_api_mutate` become `cloudflare_api_read` and `cloudflare_api_write`. `verify_api_token` and `verify_global_api_key` become `verify_credential`. Cloudflare's 23 hand-written reads give way to `cloudflare_api_read`, which returns unprojected results. Vercel's `vercel_api_get` and `vercel_api_mutate` become `vercel_api_read` and `vercel_api_write`, and `verify_project_domain`, `remove_project_domain` and `delete_deployment` are removed. Notion's token connector drops eight `integration_*` tools in favor of the generic ones. Stripe keeps its tool names with Connecta's contracts, but natural-language Sigma, metrics, docs search, the planner, reports and feedback exist only on the OAuth connection.
50
+
51
+ Several exports are removed. Cloudflare loses `CloudflareApiOptions`, `CloudflareMcpOptions`, `CloudflareConnectionOptions` and `CloudflareAuthentication`, plus `CLOUDFLARE_DNS_RECORD_TYPES` and `CLOUDFLARE_CONTENT_DNS_RECORD_TYPES`. Notion and Vercel lose their `ApiOptions`, `McpOptions` and `ConnectionOptions` types, and Stripe loses `StripeHeaderOptions`. Each `<Vendor>Options` is now a union of per-auth types, such as `StripeApiKeyOptions`, `NotionTokenOptions`, `VercelTokenOptions` and `CloudflareGlobalApiKeyOptions`.
52
+
53
+ A Cloudflare Global API Key reaches every account and zone its user can reach. `auth: { type: "globalApiKey" }` therefore requires `pin: { accountIds, zoneIds }` unless the configuration states `unpinned: true`. It never writes `/user` or `/memberships`. Pins deny by default and also apply to GraphQL tags and list results. An operation that names no account or zone is refused unless it was reviewed as safe. Pins are optional for scoped API tokens. A Global API Key cannot reach R2.
54
+
55
+ Every key connector applies one reviewed value-safety table. No REST tool response, success or error, returns a credential that grants access to the vendor or a third party, or a stored secret value. Operations that mint or hand out credentials are refused before they are sent. Stored secrets read as `"[redacted]"`. URLs that act as capabilities keep only their scheme and host, or lose their signing parameters. Reviewed operations withhold the vendor's error text unless their review keeps it. Results therefore lose fields they used to return. Stripe withholds client secrets, webhook signing secrets, Issuing card numbers and CVCs, and Checkout Session URLs other than those of guest payment sessions. Notion strips the signatures from pre-signed file URLs, so a returned file URL no longer downloads. Vercel returns `"[redacted]"` for environment values and header rule values. Cloudflare redacts Hyperdrive passwords, dispatch binding values and transform-rule header values. The guarantee assumes an honest vendor server and covers only the REST key connectors. Secrets that people type into free text, names or identifiers are out of scope. Hosted OAuth connections fall outside it: they return what the vendor's server returns, under Connecta's existing credential and `writeOnly` redaction.
56
+
57
+ The four provider entries now carry both transports and a pinned index. Measured gzip, Stripe grows from 171 KB to 243 KB, Notion from 191 KB to 216 KB, Vercel from 190 KB to 231 KB and Cloudflare from 193 KB to 338 KB, all within their stated caps. Vercel admits only reviewed and approved MCP clients to its hosted server. If it refuses authorization for that reason, the deployment needs Vercel's approval or a token connector. Vercel's hosted connection now lists its four environment variable tools. The two that can return decrypted values are classified as writes. Cloudflare's hosted `execute` is always a write, so a read-only pool on a Cloudflare OAuth connector reaches only `search`. Use an API token for read-only API access. Agent evals against fake vendors for all four providers are planned. `documentation/provider-auth-migration.md` gives the step-by-step migration for each provider.
58
+
59
+ ### Changed
60
+
61
+ - **Breaking:** `cloudflare()` now requires `auth`, which selects the implementation. `auth: { type: "oauth" }` connects Cloudflare's hosted MCP server (`search` and `execute`), OAuth only; bearer-token and credential auth to `mcp.cloudflare.com` are removed. `auth: { type: "apiToken" }` and `auth: { type: "globalApiKey" }` connect Connecta's own REST connector to the v4 API with an operator-managed credential: `cloudflare_api_search` and `cloudflare_api_details` read a pinned Cloudflare API index (3,336 operations, deprecated ones dropped), `cloudflare_api_read` (GET plus 47 reviewed read-only POSTs such as analytics, Analytics Engine and Log Explorer SQL, and KV bulk get) and `cloudflare_api_write` check every call against that index before sending it and return `{ status, data, page? }` with the envelope unwrapped, and the named tools are `verify_credential`, `list_accounts`, `list_zones`, `graphql_query` (read-only, refuses mutations), and `cloudflare_api_upload`. `accountId` and `zoneId` fill path placeholders; `pin: { accountIds, zoneIds }` restricts every call by default deny (an operation naming no account or zone is refused unless reviewed safe), including GraphQL tags and list results, and is required for a Global API Key unless `unpinned: true`. A reviewed value-safety table derived from the pinned spec gives every credential-bearing operation a verdict: credential producers (API tokens, Access service tokens, R2 temporary credentials, signing keys, direct-upload URLs, and more) and payment secrets are refused, stored and embedded secrets and environment and plain-text binding values are redacted on every method and tool, and operations that take or return credentials withhold vendor error text; URL Scanner captures are refused; a pinned connector's GraphQL documents hold one query and only fragments it uses, and a key-name, typed-secret, and URL-credential heuristic backs it up; a Global API Key never writes `/user` or `/memberships`. `surface`, `authentication`, `credential` overrides, the API-token mutation refusals, `verify_api_token`, `verify_global_api_key` (now `verify_credential`), `cloudflare_api_get`, `cloudflare_api_mutate`, the 23 hand-written reads from `get_zone` and `list_dns_records` to `list_pages_domains` (use `cloudflare_api_read`), `CLOUDFLARE_DNS_RECORD_TYPES`, `CLOUDFLARE_CONTENT_DNS_RECORD_TYPES`, and the `CloudflareApiOptions`, `CloudflareMcpOptions`, `CloudflareConnectionOptions`, and `CloudflareAuthentication` types are removed. See the provider auth migration guide.
62
+ - Record that the auth mode selects the provider implementation: OAuth uses the vendor's hosted MCP and keys use a Connecta REST connector, superseding the hosted MCP ownership rule for Stripe, Vercel, Cloudflare and Notion (decision record 0005). Each provider's change is its own entry.
63
+ - **Breaking:** `notion()` now requires `auth`, which selects the implementation, and `surface` is removed. `auth: { type: "oauth" }` connects Notion's hosted MCP server as the signed-in user, OAuth only, with the same reviewed catalog. `auth: { type: "token" }` connects Connecta's REST connector to `api.notion.com` as an internal integration with an operator-managed token: `notion_api_search`, `notion_api_details`, `notion_api_read`, and `notion_api_write` reach every operation in a pinned Notion API index (with the sent `Notion-Version` pinned to it), check each call against it before sending, admit Notion's read-only POST queries on the read tool, and return `{ status, data, page? }`, where `page.in: "body"` marks a body-borne cursor, `page.path` names a view query's continuation, and `hasMore` without `next` marks a cursorless cut-off such as meeting notes' limit. The token connector keeps `integration_search`, `integration_get_page`, `integration_get_page_content`, `integration_get_data_source_schema`, `integration_query_data_source`, `integration_create_page`, and `integration_append_blocks` (which now also takes `checklist`); `integration_get_page_property`, `integration_get_database`, `integration_list_users`, `integration_get_self`, `integration_list_comments`, `integration_update_page_properties`, `integration_trash_page`, and `integration_add_comment` are removed in favor of the generic tools. Notion's OAuth token endpoints and multipart file sends are refused. Notion's reconciliation table is retired. `npm run providers:spec` accepts `options.versionHeader` for documents that carry their API version in a header parameter. See the provider auth migration guide.
64
+ - **Breaking:** `stripe()` now requires `auth`, which selects the implementation. `auth: { type: "oauth" }` connects Stripe's hosted MCP server exactly as before, OAuth only. `auth: { type: "apiKey" }` with `mode` (and optional `connectedAccount`) connects Connecta's own REST connector to `api.stripe.com` with an operator-managed secret or restricted key: `stripe_api_search` and `stripe_api_details` read a pinned Stripe API index, `stripe_api_read` and `stripe_api_write` check every call against that index before sending it, form-encode v1 bodies, send v2 bodies as JSON, send `Stripe-Version: 2026-09-30.endive` (the pinned index's version) on every v1 and v2 request whatever the account's default API version, so response shapes and accepted parameters follow it (check them with `stripe_api_details`), return `{ status, data, page? }` with optional `select`, and on every POST write send the caller's `idempotencyKey` or a generated one as `Idempotency-Key` and return it (DELETE sends none); `get_stripe_account_info` and `get_balance_summary` are named reads. A key whose prefix contradicts `mode` is refused before any request, and `connectedAccount` now works with an operator-managed key. Literal `headers` and hosted `credential` auth are removed; omitting `auth` is refused with the valid types. Providers gain the `"dual"` kind, `variants()` accepts a nested discriminant such as `["auth", "type"]`, the operator UI shows whether a provider connector uses hosted MCP or REST, and `npm run providers:spec` compiles pinned OpenAPI documents into operation indexes that `check:providers-generated` checks offline and `providers:check` compares with the live document. A POST write's key also reaches the caller when the response body is lost or the call deadline interrupts it: `write_outcome_unknown` now carries `uncertainCall.recovery` with the generated key, or a caller-supplied key of at most 128 characters (a longer caller key is still sent to Stripe but not echoed, since the caller already has it). See the provider auth migration guide.
65
+ - **Breaking:** `vercel()` now requires `auth`, which selects the implementation, and `surface` is removed. `auth: { type: "oauth" }` connects Vercel's hosted MCP server as before, OAuth only; it now also lists `filter_project_envs`, `get_project_env`, `create_project_env`, and `edit_project_env`, with the two value-returning reads reviewed as writes. `auth: { type: "token" }` with optional `teamId` and `baseUrl` connects Connecta's own REST connector to `api.vercel.com` with an operator-managed access token, complete on its own: `vercel_api_search` and `vercel_api_details` read Vercel's OpenAPI document pinned by content hash, `vercel_api_read` (GET plus reviewed read-only POSTs such as `POST /v2/observability/query`) and `vercel_api_write` check every call against that index before sending it, apply the default team to operations that accept one (`teamId: null` for the personal account), and return `{ status, data, page? }` with `until` or `cursor` pagination. Named tools are `list_teams`, the value-safe environment variable tools, `get_deployment_build_logs`, `get_runtime_logs` (bounded by `waitMs` and `maxRows`), and `vercel_api_upload` for octet-stream operations. Value safety follows a reviewed per-operation table derived from the pinned spec (`scripts/value-safety.mjs` flags every operation whose response may carry a credential or secret value; a test fails on any without a verdict): credential minting, Connect authorization, project transfer requests, decrypted environment values, Global Config items and tokens, a domain move-out, KMS signing, and domain transfer codes are refused; environment values, protection-bypass secrets, deploy hook URLs, drain and webhook headers, signing secrets, and destination URLs beyond their origin, external route and redirect destinations beyond their origin, the args and values of every header, cookie, and query rule (route transforms and conditions, firewall conditions), team invite codes, synced Global Config items, and URL credentials are redacted on every path; a key-name heuristic covers the rest; and secret-family failures carry fixed messages. The 0.29 ownership refusals, reconciliation tables, `vercel_api_get`, `vercel_api_mutate`, `verify_project_domain`, `remove_project_domain`, `delete_deployment`, and the no-op `defaultPageSize` option are removed. See the provider auth migration guide.
66
+
67
+ ### Security
68
+
69
+ - **Breaking:** Every key connector (Stripe, Notion, Cloudflare, Vercel) now applies one reviewed value-safety mechanism: one detector over each pinned spec (the union of the former Cloudflare and Vercel detectors), one per-vendor table of `refuse`, `redact`, and `safe` verdicts, one redaction engine on every result, and one error policy. Stripe refuses operations that hand out a credential (ephemeral keys, Terminal connection tokens and onboarding links, v1 and v2 account links, login links, account, customer, and billing portal sessions, file link creation, Financial Connections session creation, and the meter event session), refuses Issuing `number` and `cvc` expansions on every Issuing path, and redacts client secrets, webhook and event destination signing secrets, Apps secret payloads, app-install authorization codes, Terminal Wi-Fi passwords, forwarded header values, and Issuing card numbers and CVCs; webhook, file link, and pre-signed import URLs keep only their origin. Resource rules keyed on Stripe's `object` apply these wherever an object appears (expansions, lists, events and their `previous_attributes`). Identity verification URLs are unchanged; a Checkout Session's URL is returned whole only for a provably guest payment session (no `customer`, `customer_account`, customer creation, or saved payment method options) and otherwise keeps only its origin. Notion withholds the signature parameters of pre-signed file URLs and refuses its OAuth endpoints through the table. Cloudflare's review grows by 259 operations, newly redacting Hyperdrive passwords, RealtimeKit storage credentials, transform-rule header values, dispatch binding values, and webhook destinations beyond their origin. Vercel replaces environment values with `"[redacted]"` instead of dropping them, and redacts audit-event invite codes, integration drain headers, and route header values. Where the former engines differed the stricter rule holds: a credential-named field loses its whole value, and every reviewed operation (or one whose request takes a credential-named field) withholds the vendor's error text unless its review keeps it. `providers:spec` (now run under tsx) checks every reviewed path against the pinned response schema. `RestVendor.valueSafety` is required and the `redact` hook is removed; Cloudflare's generated index no longer carries `secrets`. See the provider auth migration guide.
70
+
5
71
  ## 0.30.0 — 2026-10-09
6
72
 
7
73
  Connecta 0.30.0 makes large results, sign-in and rich output dependable for agents, on both direct calls and programs. The six-tool surface from 0.29.0 stays. We measured the alternative, folding everything into `execute_code`, against it on Sonnet 5.5 and GPT-6-Luna (#765). The code-only surface scored lower and made a duplicate billable write, so it was not adopted.
package/README.md CHANGED
@@ -65,16 +65,20 @@ across them in one task. Changing your connected services does not require
65
65
  changing the agent's connection. Your deployment decides which services and
66
66
  operations each caller can reach.
67
67
 
68
- Connecta includes maintained connections for <!-- providers:start -->Basecamp, Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Infisical, Linear, Mixpanel, Notion, Overflow, Planning Center, RevenueCat, Stripe, Tithe.ly, and Vercel<!-- providers:end -->.
68
+ Connecta includes maintained connections for <!-- providers:start -->Basecamp, Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Infisical, Linear, Mixpanel, Notion, Overflow, Planning Center, RevenueCat, Stripe, Tithe.ly, Vercel, and Wispr Flow<!-- providers:end -->.
69
69
  Each connection has its own setup guide.
70
- Notion, Vercel, and Cloudflare offer hosted MCP connections and selected HTTP
71
- API capabilities. You can also connect other remote MCP servers or define the
72
- operations you need from an HTTP API.
70
+ Stripe, Notion, Cloudflare, and Vercel use their hosted MCP servers when you
71
+ sign in with OAuth, and Connecta's own API connections when you use an API key,
72
+ a Cloudflare API token or Global API Key, a Notion integration token, or a
73
+ Vercel access token. You can also
74
+ connect other remote MCP servers or define the operations you need from an
75
+ HTTP API.
73
76
 
74
77
  Google Workspace connections use the account your deployment assigns to each
75
78
  signed-in person. A Workspace administrator authorizes them for your domain.
76
- Gmail reads mail and writes drafts, but does not send mail. Google Drive can
77
- read, write, and share files, but does not permanently delete them. Available
79
+ Gmail reads mail and attachments and writes drafts, but does not send mail.
80
+ Google Drive can read, write, and share files, but does not permanently delete
81
+ them. Available
78
82
  operations and account access depend on the integration and your deployment.
79
83
 
80
84
  Large results need not fill the conversation. The agent can filter and
@@ -82,19 +86,20 @@ summarize them in its program, or read a large result in smaller pieces.
82
86
 
83
87
  ## Writes, credentials, and access
84
88
 
85
- Connecta distinguishes reads from writes. By default, programs can read, and
86
- each change uses a separate visible tool call. Your agent's host controls
87
- whether it asks for approval. You can explicitly allow programs to make
88
- changes too; the host then sees the program tool as a write. This choice does
89
- not give a caller access to additional services.
89
+ Connecta distinguishes reads from writes, and each endpoint is either writable
90
+ or read-only. By default an endpoint is writable: your agent can make a change
91
+ with one visible tool call or inside a program, and your agent's host decides
92
+ whether to ask for approval; it sees the program tool as a write. You can mark
93
+ the whole deployment or a single endpoint read-only, and then nothing there can
94
+ change anything: search leaves write tools out and every write is refused. You
95
+ make this choice explicitly for each deployment. Neither choice gives a caller
96
+ access to additional services.
90
97
 
91
98
  ```mermaid
92
99
  flowchart TB
93
- Change["Your agent wants to change something"] --> Choice{"Are programs allowed<br/>to make changes?"}
94
- Choice -->|"Default: no"| Separate["A separate visible call for each change"]
95
- Choice -->|"Explicit choice: yes"| Program["A program that can make changes"]
96
- Separate --> Host["Your agent's host handles approval"]
97
- Program --> Host
100
+ Change["Your agent wants to change something"] --> Choice{"Is this endpoint<br/>read-only?"}
101
+ Choice -->|"Explicit choice: yes"| Refused["Refused; nothing is sent"]
102
+ Choice -->|"Default: no"| Host["Your agent's host handles approval"]
98
103
  Host --> Access["Connecta checks the caller's access"]
99
104
  Access --> Services["The connected service receives the change"]
100
105
 
package/bin/connecta.mjs CHANGED
@@ -77,6 +77,7 @@ async function init() {
77
77
  console.log("Next:");
78
78
  console.log(` ${shellCd(target)}`);
79
79
  console.log(" npm install");
80
+ console.log(" # Choose readOnly in src/connecta.config.ts: false writes with client approval, true refuses writes.");
80
81
  console.log(' npm run --silent provision-token -- "local-machine"');
81
82
  console.log(" # Save the returned cta_ token privately for your MCP client and doctor.");
82
83
  console.log(" npm start");
package/bin/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.30.0";
3
+ export const CONNECTA_VERSION = "0.32.0";
@@ -148,7 +148,7 @@ export class AuthElicitation {
148
148
  requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
149
149
  });
150
150
  // Recheck every entered read before restarting any part of a program.
151
- // A trusted pool cannot authorize replay of a newly classified write.
151
+ // A writable endpoint cannot authorize replay of a newly classified write.
152
152
  for (const read of previous.reads) {
153
153
  const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
154
154
  if (!current.ok ||
@@ -0,0 +1,25 @@
1
+ const MAX_FACTS = 4;
2
+ const MAX_TEXT = 128;
3
+ const NAME = /^[A-Za-z][A-Za-z0-9]*$/;
4
+ const facts = new WeakMap();
5
+ /**
6
+ * Record facts that let the caller reconcile or safely retry this call's
7
+ * write. Bounded: at most four string facts with short camelCase names; a fact
8
+ * outside the bound is dropped rather than truncated.
9
+ */
10
+ export function recordRecovery(ctx, recovery) {
11
+ const kept = { ...facts.get(ctx) };
12
+ for (const [name, value] of Object.entries(recovery)) {
13
+ if (Object.keys(kept).length >= MAX_FACTS && !Object.hasOwn(kept, name))
14
+ break;
15
+ if (!NAME.test(name) || name.length > 32 || typeof value !== "string" || value.length > MAX_TEXT)
16
+ continue;
17
+ kept[name] = value;
18
+ }
19
+ facts.set(ctx, Object.freeze(kept));
20
+ }
21
+ /** The facts a connector recorded on this call's context, if any. */
22
+ export function recoveryFor(ctx) {
23
+ const recorded = ctx ? facts.get(ctx) : undefined;
24
+ return recorded && Object.keys(recorded).length > 0 ? recorded : undefined;
25
+ }
@@ -377,6 +377,7 @@ export class CatalogService {
377
377
  concurrency;
378
378
  searchRoute;
379
379
  requestSignal;
380
+ readOnly;
380
381
  // The request-scoped catalog cache: one shared read per connector asked
381
382
  // about, started by the first asker and joined by every later one. A
382
383
  // success stays for the rest of the request, so a search and the call after
@@ -401,6 +402,7 @@ export class CatalogService {
401
402
  this.searchRoute = options.searchRoute ?? "search_tools";
402
403
  this.requestSignal = options.requestSignal;
403
404
  this.defer = options.defer;
405
+ this.readOnly = options.readOnly === true;
404
406
  }
405
407
  /**
406
408
  * Send a caller back to discovery through the surface it can actually reach.
@@ -460,9 +462,12 @@ export class CatalogService {
460
462
  // each settling in its input slot: an unavailable catalog is a Failure
461
463
  // there, not a failed discovery. `route` names the surface in a timeout.
462
464
  // A probe also ends with its request, so no listing outlives the caller
463
- // that asked for it.
465
+ // that asked for it. On a read-only endpoint a write is absent from search
466
+ // and describe, as if the connector never had it. Guides are not filtered.
464
467
  discoveryCatalogs(ids, route) {
465
- return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal }), {
468
+ return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.readOnly
469
+ ? Effect.map(this.catalog(id, { signal }), (tools) => tools.filter((tool) => tool.classification === "read"))
470
+ : this.catalog(id, { signal }), {
466
471
  ...(this.requestSignal ? { signal: this.requestSignal } : {}),
467
472
  timeoutMs: this.probeTimeoutMs,
468
473
  timeoutError: new ConnectorCallError("timeout", `${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
@@ -25,9 +25,13 @@ type AnyField = Field<any, any, boolean>;
25
25
  export type Shape = {
26
26
  readonly [key: string]: AnyField;
27
27
  };
28
- /** A discriminated union of closed objects: `cases[value[key]]`, else `cases[otherwise]`. */
28
+ /**
29
+ * A discriminated union of closed objects: `cases[value[key[0]][key[1]]…]`,
30
+ * else `cases[otherwise]`. Each case is the whole object's shape, including
31
+ * the nested objects on the key path.
32
+ */
29
33
  interface Variants {
30
- readonly key: string;
34
+ readonly key: readonly string[];
31
35
  readonly cases: Readonly<Record<string, Shape>>;
32
36
  readonly otherwise?: string;
33
37
  }
@@ -116,13 +120,16 @@ export declare function instance<F extends AnyField>(field: F): F & {
116
120
  };
117
121
  /**
118
122
  * A closed object whose allowed keys depend on a discriminant, such as
119
- * `remoteMcp({ auth: { type: "headers", headers } })`. An omitted
123
+ * `remoteMcp({ auth: { type: "headers", headers } })`. The discriminant may
124
+ * be nested: `variants(["auth", "type"], cases)` selects the whole options
125
+ * shape from `options.auth.type`, so `{ auth: { type: "apiKey" }, mode }` and
126
+ * `{ auth: { type: "oauth" } }` accept different top-level keys. An omitted
120
127
  * discriminant selects `otherwise` when given; an omitted one without it, an
121
- * unrecognized one, or a value that is not an object throws with its path and
122
- * the valid values, so no union falls through to "no case". Only the walk
123
- * reads variants; the factory validates the remaining values itself.
128
+ * unrecognized one, or a value on the path that is not an object throws with
129
+ * its path and the valid values, so no union falls through to "no case". Only
130
+ * the walk reads variants; the factory validates the remaining values itself.
124
131
  */
125
- export declare function variants(key: string, cases: Readonly<Record<string, Shape>>, otherwise?: string): Field<unknown, unknown, true>;
132
+ export declare function variants(key: string | readonly string[], cases: Readonly<Record<string, Shape>>, otherwise?: string): Field<unknown, unknown, true>;
126
133
  /**
127
134
  * A closed object for the unknown-key walk only: each named key may hold
128
135
  * anything, which its factory validates. For the options of a built-in
@@ -183,16 +183,22 @@ export function instance(field) {
183
183
  }
184
184
  /**
185
185
  * A closed object whose allowed keys depend on a discriminant, such as
186
- * `remoteMcp({ auth: { type: "headers", headers } })`. An omitted
186
+ * `remoteMcp({ auth: { type: "headers", headers } })`. The discriminant may
187
+ * be nested: `variants(["auth", "type"], cases)` selects the whole options
188
+ * shape from `options.auth.type`, so `{ auth: { type: "apiKey" }, mode }` and
189
+ * `{ auth: { type: "oauth" } }` accept different top-level keys. An omitted
187
190
  * discriminant selects `otherwise` when given; an omitted one without it, an
188
- * unrecognized one, or a value that is not an object throws with its path and
189
- * the valid values, so no union falls through to "no case". Only the walk
190
- * reads variants; the factory validates the remaining values itself.
191
+ * unrecognized one, or a value on the path that is not an object throws with
192
+ * its path and the valid values, so no union falls through to "no case". Only
193
+ * the walk reads variants; the factory validates the remaining values itself.
191
194
  */
192
195
  export function variants(key, cases, otherwise) {
196
+ const path = typeof key === "string" ? [key] : [...key];
197
+ if (path.length === 0)
198
+ throw new Error("variants() requires a discriminant key.");
193
199
  return {
194
200
  ...opaque(),
195
- variants: { key, cases, ...(otherwise !== undefined ? { otherwise } : {}) },
201
+ variants: { key: path, cases, ...(otherwise !== undefined ? { otherwise } : {}) },
196
202
  };
197
203
  }
198
204
  /**
@@ -309,14 +315,27 @@ function inspectRecord(value, path, field) {
309
315
  * is never echoed.
310
316
  */
311
317
  function selectShape(inspected, path, variants) {
312
- const at = `${path}.${variants.key}`;
318
+ const at = `${path}.${variants.key.join(".")}`;
313
319
  const valid = Object.keys(variants.cases)
314
320
  .map((name) => JSON.stringify(name))
315
321
  .join(", ");
316
- const descriptor = inspected.own.find(([key]) => key === variants.key)?.[1];
317
- if (descriptor && isAccessor(descriptor))
318
- fail(`${at} must be a plain value, not a getter or setter.`);
319
- const discriminant = descriptor?.value;
322
+ // Walk the key path one plain object at a time, reading descriptors only.
323
+ let holder = inspected;
324
+ let discriminant;
325
+ for (const [depth, key] of variants.key.entries()) {
326
+ const step = `${path}.${variants.key.slice(0, depth + 1).join(".")}`;
327
+ const descriptor = holder.own.find(([own]) => own === key)?.[1];
328
+ if (descriptor && isAccessor(descriptor))
329
+ fail(`${step} must be a plain value, not a getter or setter.`);
330
+ discriminant = descriptor?.value;
331
+ if (depth === variants.key.length - 1 || discriminant === undefined)
332
+ break;
333
+ if (typeof discriminant !== "object" || discriminant === null || isArrayAt(discriminant, step)) {
334
+ fail(`${step} must be an object.`);
335
+ }
336
+ holder = inspectRecord(discriminant, step, OPAQUE);
337
+ discriminant = undefined;
338
+ }
320
339
  if (discriminant === undefined) {
321
340
  if (variants.otherwise !== undefined)
322
341
  return variants.cases[variants.otherwise];
@@ -56,8 +56,10 @@ export function describeConfigSources(snapshot, raw) {
56
56
  source = provided(raw.serverInfo?.[key]);
57
57
  else if (group === "urls")
58
58
  source = key === "mcpPath" ? "default" : provided(raw[key]);
59
- else if (group === "trust")
60
- source = provided(raw.trust);
59
+ else if (group === "hasGrant")
60
+ source = provided(raw.grant);
61
+ else if (group === "readOnly")
62
+ source = provided(raw.readOnly);
61
63
  else if (group === "identity")
62
64
  source = provided(raw.identity?.[key]);
63
65
  else if (group === "storage")
@@ -67,7 +69,8 @@ export function describeConfigSources(snapshot, raw) {
67
69
  else if (group === "classification")
68
70
  source = provided(raw.classification);
69
71
  else if (group === "pools")
70
- source = field === "trust" ? provided(raw.pools?.[key]?.trust) : provided(raw.pools);
72
+ // An inherited value comes from wherever the deployment's came from.
73
+ source = field === "readOnly" ? provided(raw.pools?.[key]?.readOnly ?? raw.readOnly) : provided(raw.pools);
71
74
  else if (group === "modules") {
72
75
  const module = key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens;
73
76
  const factory = moduleSources[key];
package/dist/config.d.ts CHANGED
@@ -76,7 +76,7 @@ declare const execute: {
76
76
  */
77
77
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
78
78
  /**
79
- * Trusted-pool writes one program may send, on top of the host-call
79
+ * Writes one program may send on a writable endpoint, on top of the host-call
80
80
  * budget every call already spends. Default 10.
81
81
  */
82
82
  maxWrites: import("./config-schema.js").Field<number, number, true>;
@@ -183,8 +183,13 @@ declare const identity: {
183
183
  personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
184
184
  };
185
185
  declare const pool: {
186
- /** Programs may write only in trusted pools. Default read-only. */
187
- trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
186
+ /**
187
+ * Whether this pool's endpoint refuses every write: write tools are absent
188
+ * from search_tools, tool listings, and describe, and every write call is
189
+ * refused. Inherits the deployment's `readOnly` unless set here, in either
190
+ * direction.
191
+ */
192
+ readOnly: import("./config-schema.js").Field<boolean, boolean | undefined, true>;
188
193
  /** Connector ids and exact `connector.tool` addresses in this pool. */
189
194
  tools: import("./config-schema.js").Field<readonly string[], readonly string[], false>;
190
195
  /**
@@ -233,18 +238,22 @@ declare const connectaConfig: {
233
238
  personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
234
239
  };
235
240
  };
241
+ /**
242
+ * Whether this admitted identity may open plain `/mcp`. Omitted admits
243
+ * everyone; only literal true admits when configured. Other returns and
244
+ * throws receive the same 404 as a refused or undeclared pool.
245
+ */
246
+ grant: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
236
247
  /** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
237
248
  pools: import("./config-schema.js").Field<Record<string, {
238
- trust?: "trusted" | "read-only" | undefined;
249
+ readOnly?: boolean | undefined;
239
250
  grant?: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
240
251
  tools: readonly string[];
241
252
  }>, Readonly<Record<string, {
242
- readonly trust: "trusted" | "read-only";
253
+ readonly readOnly: boolean | undefined;
243
254
  readonly tools: readonly string[];
244
255
  readonly grant: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
245
256
  }>> | undefined, true>;
246
- /** Trust of the default /mcp endpoint. Default read-only. */
247
- trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
248
257
  /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
249
258
  classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;
250
259
  /**
@@ -394,7 +403,7 @@ declare const connectaConfig: {
394
403
  */
395
404
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
396
405
  /**
397
- * Trusted-pool writes one program may send, on top of the host-call
406
+ * Writes one program may send on a writable endpoint, on top of the host-call
398
407
  * budget every call already spends. Default 10.
399
408
  */
400
409
  maxWrites: import("./config-schema.js").Field<number, number, true>;
@@ -548,6 +557,15 @@ declare const connectaConfig: {
548
557
  * `customExecutor(myExecutor, { lifecycle: "self-managed" })`.
549
558
  */
550
559
  executor: import("./config-schema.js").Field<Executor, Executor, false>;
560
+ /**
561
+ * Required. Whether the default /mcp endpoint refuses every write. `true`:
562
+ * write tools are absent from search_tools, tool listings, and describe,
563
+ * and call_tool, call_destructive_tool, and program writes are refused.
564
+ * `false`: call_destructive_tool and programs may write, and the MCP
565
+ * client owns approval. Pools inherit it unless they set their own. It has
566
+ * no default while deployments migrate from `trust` (#820).
567
+ */
568
+ readOnly: import("./config-schema.js").Field<boolean, boolean, false>;
551
569
  };
552
570
  /** A deployment's whole configuration, passed to `createConnecta`. */
553
571
  export type ConnectaConfig = ConfigInput<typeof connectaConfig>;
package/dist/config.js CHANGED
@@ -134,7 +134,7 @@ const execute = {
134
134
  */
135
135
  watchdogMs: whole({ min: 1, default: D.execute.watchdogMs }),
136
136
  /**
137
- * Trusted-pool writes one program may send, on top of the host-call
137
+ * Writes one program may send on a writable endpoint, on top of the host-call
138
138
  * budget every call already spends. Default 10.
139
139
  */
140
140
  maxWrites: whole({ min: 1, default: D.execute.maxWrites }),
@@ -195,17 +195,21 @@ const identity = {
195
195
  /** Connecting or changing the caller's personal account. Defaults to none. */
196
196
  personalConnection: fn(),
197
197
  };
198
- const trust = () => opaque({
199
- check: (value, path) => {
200
- if (value !== "trusted" && value !== "read-only") {
201
- throw new ConfigError(`${path} must be "trusted" or "read-only".`);
202
- }
203
- },
204
- resolve: (value) => value ?? "read-only",
205
- });
198
+ /** The choice every deployment makes explicitly while `trust` migrates (#820). */
199
+ const READ_ONLY_REQUIRED = (path) => `${path} is required. Choose readOnly: true to refuse every write on /mcp, or readOnly: false to let ` +
200
+ "call_destructive_tool and programs write with MCP client approval. Pools inherit it unless they set their own.";
201
+ function assertFlag(value, path) {
202
+ if (typeof value !== "boolean")
203
+ throw new ConfigError(`${path} must be true or false.`);
204
+ }
206
205
  const pool = {
207
- /** Programs may write only in trusted pools. Default read-only. */
208
- trust: trust(),
206
+ /**
207
+ * Whether this pool's endpoint refuses every write: write tools are absent
208
+ * from search_tools, tool listings, and describe, and every write call is
209
+ * refused. Inherits the deployment's `readOnly` unless set here, in either
210
+ * direction.
211
+ */
212
+ readOnly: opaque({ check: assertFlag }),
209
213
  /** Connector ids and exact `connector.tool` addresses in this pool. */
210
214
  tools: required(opaque({
211
215
  check: (value, path) => {
@@ -240,7 +244,7 @@ const connectaConfig = {
240
244
  connectors: required(array(opaque({
241
245
  check: (value, path) => {
242
246
  if (isObject(value) && Object.hasOwn(value, "approval")) {
243
- throw new ConfigError(`${path}.approval was removed; configure pool trust instead.`);
247
+ throw new ConfigError(`${path}.approval was removed; the MCP client owns approval. Set readOnly: true to refuse writes.`);
244
248
  }
245
249
  if (!isObject(value) ||
246
250
  typeof value.id !== "string" ||
@@ -274,10 +278,14 @@ const connectaConfig = {
274
278
  }),
275
279
  /** Code-derived connection visibility and independent management permissions. */
276
280
  identity: object(identity),
281
+ /**
282
+ * Whether this admitted identity may open plain `/mcp`. Omitted admits
283
+ * everyone; only literal true admits when configured. Other returns and
284
+ * throws receive the same 404 as a refused or undeclared pool.
285
+ */
286
+ grant: fn(),
277
287
  /** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
278
288
  pools: record(object(pool)),
279
- /** Trust of the default /mcp endpoint. Default read-only. */
280
- trust: trust(),
281
289
  /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
282
290
  classification: record(record(opaque({
283
291
  check: (value, path) => {
@@ -415,10 +423,21 @@ const connectaConfig = {
415
423
  '"@zackbart/connecta/quickjs" on Node, or ' +
416
424
  "workerExecutor({ loader: env.LOADER }) from " +
417
425
  '"@zackbart/connecta/worker" around DynamicWorkerExecutor on Workers.'),
426
+ /**
427
+ * Required. Whether the default /mcp endpoint refuses every write. `true`:
428
+ * write tools are absent from search_tools, tool listings, and describe,
429
+ * and call_tool, call_destructive_tool, and program writes are refused.
430
+ * `false`: call_destructive_tool and programs may write, and the MCP
431
+ * client owns approval. Pools inherit it unless they set their own. It has
432
+ * no default while deployments migrate from `trust` (#820).
433
+ */
434
+ readOnly: required(opaque({ check: assertFlag }), (path) => READ_ONLY_REQUIRED(path)),
418
435
  };
419
436
  const CONFIG = object(connectaConfig);
420
437
  /** Pause-only options #672 removed with `resume_execution`. */
421
438
  const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
439
+ /** `trust`, on the deployment or a pool, was replaced by `readOnly` (#820). */
440
+ const isRetiredTrust = (path) => path === "ConnectaConfig.trust" || (path.startsWith("ConnectaConfig.pools.") && path.endsWith(".trust"));
422
441
  function rejectUnknownOptions(paths) {
423
442
  if (paths.length === 0)
424
443
  return;
@@ -431,10 +450,13 @@ function rejectUnknownOptions(paths) {
431
450
  ? "\nMove branding into ui: operatorUi({ branding }) from @zackbart/connecta/ui."
432
451
  : "") +
433
452
  (paths.includes("ConnectaConfig.execute.approval")
434
- ? '\nexecute.approval was removed. Set trust: "trusted" on a pool to allow program writes; the default is read-only.'
453
+ ? "\nexecute.approval was removed. The MCP client owns approval; set readOnly: true on the deployment or a pool to refuse writes."
435
454
  : "") +
436
455
  (paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
437
- ? "\nPrograms no longer pause at writes, so there is nothing to configure: read-only pools route writes through call_destructive_tool; trusted pools allow program writes (issue #672). Delete the option."
456
+ ? "\nPrograms no longer pause at writes, so there is nothing to configure: writable endpoints let programs and call_destructive_tool write; readOnly endpoints refuse every write (issue #672). Delete the option."
457
+ : "") +
458
+ (paths.some(isRetiredTrust)
459
+ ? '\ntrust was removed (#820); set readOnly, which every deployment now chooses explicitly. trust: "read-only" becomes readOnly: true, which now also refuses call_destructive_tool. trust: "trusted" becomes readOnly: false. A pool inherits the deployment readOnly unless it sets its own.'
438
460
  : ""));
439
461
  }
440
462
  /** Checks that span fields; each value is already individually valid. */
@@ -2,7 +2,8 @@ import type { ToolAccess } from "./registry.js";
2
2
  import type { AuthenticatedIdentity } from "./types.js";
3
3
  /** A declared pool after construction-time validation. */
4
4
  export interface ResolvedPool {
5
- trust: import("./tool-safety.js").PoolTrust;
5
+ /** Resolved: the pool's own `readOnly`, else the deployment's. */
6
+ readOnly: boolean;
6
7
  access: ConnectorAccess;
7
8
  grant(identity: Readonly<AuthenticatedIdentity>): boolean | Promise<boolean>;
8
9
  }