@zackbart/connecta 0.32.0 → 0.33.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 (137) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +15 -7
  3. package/bin/connecta.mjs +24 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/call-admission-context.js +15 -0
  10. package/dist/call-admission.js +30 -43
  11. package/dist/call-recovery.js +6 -2
  12. package/dist/config-value-sources.js +11 -9
  13. package/dist/config.d.ts +37 -75
  14. package/dist/config.js +110 -36
  15. package/dist/connector-access.d.ts +4 -34
  16. package/dist/connector-access.js +15 -99
  17. package/dist/connector-caller.d.ts +7 -1
  18. package/dist/connector-caller.js +3 -2
  19. package/dist/connectors/api-connector.d.ts +27 -4
  20. package/dist/connectors/api-connector.js +2 -0
  21. package/dist/connectors/remote-mcp.js +2 -0
  22. package/dist/describe-config.d.ts +29 -7
  23. package/dist/describe-config.js +7 -17
  24. package/dist/index.d.ts +4 -3
  25. package/dist/index.js +29 -10
  26. package/dist/invocation.js +5 -3
  27. package/dist/membership.d.ts +51 -0
  28. package/dist/membership.js +121 -0
  29. package/dist/module-contracts.d.ts +23 -6
  30. package/dist/operator-record.d.ts +2 -2
  31. package/dist/operator-record.js +2 -4
  32. package/dist/operator-ui/agents.d.ts +74 -0
  33. package/dist/operator-ui/agents.js +104 -0
  34. package/dist/operator-ui/contract.d.ts +149 -28
  35. package/dist/operator-ui/endpoints.js +192 -0
  36. package/dist/operator-ui/fix-prompts.js +4 -4
  37. package/dist/operator-ui/generated.js +5 -4
  38. package/dist/operator-ui/member-page.js +112 -0
  39. package/dist/operator-ui/model.d.ts +14 -6
  40. package/dist/operator-ui/view.d.ts +84 -19
  41. package/dist/operator-ui/view.js +82 -21
  42. package/dist/page-styles.js +1 -1
  43. package/dist/provider.d.ts +61 -8
  44. package/dist/provider.js +46 -7
  45. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  46. package/dist/providers/_shared/google/workspace.js +30 -8
  47. package/dist/providers/_shared/rest/tools.js +2 -2
  48. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  49. package/dist/providers/_shared/rest/value-safety.js +699 -7
  50. package/dist/providers/ccb/index.js +1 -1
  51. package/dist/providers/cloudflare/index.js +3 -3
  52. package/dist/providers/cloudflare/rest.js +25 -3
  53. package/dist/providers/cloudflare/skill.generated.js +1 -1
  54. package/dist/providers/cloudflare/value-safety.js +32 -0
  55. package/dist/providers/docs/index.js +3 -3
  56. package/dist/providers/docs/skill.generated.js +2 -2
  57. package/dist/providers/drive/index.js +3 -3
  58. package/dist/providers/drive/skill.generated.js +2 -2
  59. package/dist/providers/forms/index.js +3 -3
  60. package/dist/providers/forms/skill.generated.js +2 -2
  61. package/dist/providers/gmail/index.js +10 -8
  62. package/dist/providers/gmail/skill.generated.js +2 -2
  63. package/dist/providers/mixpanel/index.js +1 -1
  64. package/dist/providers/notion/index.d.ts +7 -0
  65. package/dist/providers/notion/index.js +100 -11
  66. package/dist/providers/notion/rest.js +45 -5
  67. package/dist/providers/notion/skill.generated.js +2 -2
  68. package/dist/providers/notion/value-safety.js +30 -5
  69. package/dist/providers/overflow/index.js +1 -1
  70. package/dist/providers/planning-center/index.js +1 -1
  71. package/dist/providers/sheets/index.d.ts +3 -0
  72. package/dist/providers/sheets/index.js +6 -3
  73. package/dist/providers/sheets/skill.generated.js +2 -2
  74. package/dist/providers/slides/index.js +3 -3
  75. package/dist/providers/slides/skill.generated.js +2 -2
  76. package/dist/providers/stripe/rest.js +9 -2
  77. package/dist/providers/stripe/skill.generated.js +1 -1
  78. package/dist/providers/stripe/value-safety.js +22 -0
  79. package/dist/providers/vercel/rest.js +20 -2
  80. package/dist/providers/vercel/skill.generated.js +1 -1
  81. package/dist/providers/vercel/value-safety.js +22 -0
  82. package/dist/registry.d.ts +2 -3
  83. package/dist/registry.js +8 -9
  84. package/dist/routes/access-tokens.js +82 -75
  85. package/dist/routes/activity-disclosure.js +17 -18
  86. package/dist/routes/activity.js +34 -54
  87. package/dist/routes/actor-labels.js +33 -4
  88. package/dist/routes/agent-path.js +43 -0
  89. package/dist/routes/connect.js +16 -12
  90. package/dist/routes/credentials.js +16 -40
  91. package/dist/routes/mcp.js +145 -95
  92. package/dist/routes/oauth-management.js +11 -5
  93. package/dist/routes/oauth.js +3 -9
  94. package/dist/routes/operator.js +24 -23
  95. package/dist/routes/shared.d.ts +60 -20
  96. package/dist/routes/shared.js +187 -96
  97. package/dist/routes/ui-config.js +437 -97
  98. package/dist/routes/ui-data.js +2 -2
  99. package/dist/routes/ui.js +9 -16
  100. package/dist/runtime/call-admission.js +9 -0
  101. package/dist/server.js +5 -0
  102. package/dist/storage/keys.js +9 -1
  103. package/dist/types.d.ts +11 -0
  104. package/dist/ui.d.ts +1 -1
  105. package/dist/ui.js +16 -14
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/workspace-address.js +9 -0
  109. package/dist/workspace-subject.js +23 -0
  110. package/documentation/README.md +46 -22
  111. package/documentation/architecture.md +355 -50
  112. package/documentation/auth.md +775 -243
  113. package/documentation/code-mode.md +14 -11
  114. package/documentation/deploying.md +121 -24
  115. package/documentation/integrating.md +92 -21
  116. package/documentation/meta-tools.md +14 -9
  117. package/documentation/migration-0.33.md +344 -0
  118. package/documentation/operating.md +3 -1
  119. package/documentation/operator-tests.md +115 -28
  120. package/documentation/operator-ui.md +326 -61
  121. package/documentation/provider-auth-migration.md +37 -23
  122. package/documentation/provider-migration-0.29.md +2 -2
  123. package/examples/worker/AGENTS.md +30 -8
  124. package/examples/worker/README.md +92 -53
  125. package/examples/worker/src/connecta.config.ts +36 -20
  126. package/examples/worker/wrangler.jsonc +3 -0
  127. package/package.json +3 -2
  128. package/templates/node/.env.example +3 -0
  129. package/templates/node/AGENTS.md +27 -6
  130. package/templates/node/Dockerfile +2 -2
  131. package/templates/node/README.md +67 -42
  132. package/templates/node/docker-compose.yml +3 -2
  133. package/templates/node/package.json +4 -3
  134. package/templates/node/src/adopt-token.ts +23 -0
  135. package/templates/node/src/connecta.config.ts +28 -12
  136. package/templates/node/src/rotate-token.ts +17 -0
  137. package/templates/node/src/provision-token.ts +0 -17
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.33.0 — 2026-10-16
6
+
7
+ Connecta 0.33.0 makes pools the access model, and every deployment with inbound auth must change its configuration (#837). Work through `documentation/migration-0.33.md` before upgrading; it maps every removed option to its replacement. A new top-level `maintainers` list names the people who reach plain `/mcp` and administer the deployment. Each pool now has a static `members` list and serves exactly its `tools` to those members. Agents are declared by name in `agents`, each with one rotatable `cta_` token. Each removed option fails at startup with a message naming its replacement, so a deployment that starts has nothing silently ignored: `identity` (even `{}`) and its `connectorAccess`, `credentialAdministration`, `personalConnection`, `accessTokenManagement` and `activityAccess`, plus the top-level `grant` and pool `grant`.
8
+
9
+ Agents move from `/mcp/<pool>` to `/agent/<pool>`, which admits only a declared agent's `cta_` token for a pool it belongs to. `/mcp`, `/mcp/<pool>` and the operator routes refuse `cta_` tokens, so point every agent at `/agent/<pool>`. Deployments behind Cloudflare Access add one Access application for `<host>/agent/*` with a single Bypass policy, after which agents send only their token. Tokens issued by earlier releases stop admitting until trusted provisioning adopts each one for an agent (`AccessTokenManager.adopt`, or `npm run adopt-token` in the Node template). Creating, renaming and human-bound tokens are gone, along with `maxActive`. Programmatic callers construct `AccessTokenManager` with `{ agents }` and call `rotate` where they called `create`, and `revoke(agent, options)`. Activity names agents by name. Rotation is an immediate cutover. HTTP issue, rotate and revoke requests must send the observed slot revision as `expect`; a stale revision gets 409 `slot_changed`. `AccessTokenManager.rotate`, `revoke` and `adopt` accept optional `expect` and reject a stale revision with `AccessTokenError.code: "slot_changed"`. A pool can set `owner`, the Google Workspace address its agents act as, so one Workspace connector serves a pool per person. An agent never reaches a `subject` mapping function, so a 0.32 mapping that answered for machine tokens must move to `owner`.
10
+
11
+ This release also defines how to organize an agent's pools. Each agent has one Connecta connection whose client picks the pool per turn. The base pool `<agent>` holds shared connectors and is used in group chats, scheduled runs and anything else without exactly one identified person. An `<agent>-<user>` pool adds that person's own connector ids, and its `owner` is their Workspace address; it is used only in a one-on-one with that person. Never mount a second Connecta connection beside the first. The rules and an example are in the auth guide, "Organizing an agent's pools".
12
+
13
+ The shipped documentation is now written first for the agent running a deployment from the installed package (#858). `documentation/README.md` has an operator task index: add or remove a connector, organize an agent's pools, add a person, connect a shared credential, issue, rotate or revoke an agent token, upgrade, and diagnose with `connecta doctor`. Each row links to a self-contained section, and the Node template and Worker example route agents through it. Existing deployments should add the pointer line from the migration guide to their own `AGENTS.md`.
14
+
15
+ The operator UI is rebuilt as an admin console and a member page (#834). Maintainers land on Endpoints, which lists `/mcp` and every pool with who can use it, its tools grouped by connector and who each connector runs as. They also get Connectors with a connector × endpoint matrix, Activity, Tokens with one row per agent and in-row issue, rotate and revoke, and Settings. Old URLs redirect (`/access` to `/`, `/tools` to `/connectors`, `/config` to `/settings`). Pool members get one page with their endpoints and client setup, their own accounts to connect or disconnect, and shared connectors by name only; anyone else sees a No access page. A saved dark mode no longer flashes light on load, and private browsing no longer breaks token sign-in. The UI reads operator contract v2 from `GET /ui/api/config`, which is now the page's only gate: 401 when signed out, 404 for anyone who is neither a maintainer nor a member, and 200 with `role` otherwise. Contract v1 and its types are removed, and `/ui/data` and `/ui/connectors/<id>` are maintainer-only. The UI also recovers from transient Cloudflare Access and auth failures without a reload (#830).
16
+
17
+ Security: 0.29.0 through 0.32.0 buffered an unauthenticated POST carrying `Mcp-Method: subscriptions/listen` with no size limit before authentication, so an anonymous client could exhaust memory on deployments without an edge gate such as Cloudflare Access. Every MCP endpoint now authenticates before reading any body and caps the pre-dispatch read at 64 KiB. Stripe, Vercel, Cloudflare and Notion REST responses now withhold signed URL parameters, and writes that carry the redaction placeholder are refused before sending (#812, #818).
18
+
19
+ The release also fixes Notion property filtering, Gmail's charset notice under truncation, recovery of long caller idempotency keys, Cloudflare ownership lookups against call admission, and `callAdmission` values that silently disabled rate limits. Notion token connectors accept `callAdmission`, and the fake-vendor agent evaluations now run.
20
+
21
+ ### Added
22
+
23
+ - Add the operator UI contract v2 types (`OperatorUiContractV2` and its pool, member, agent, connector and matrix types) from the pools-are-the-access-model decision, with maintainer and member fixtures; `GET /ui/api/config` serves it (see Changed).
24
+ - **Breaking:** Pools take an optional `owner`, the Google Workspace address agents in that pool act as on a Workspace connector (`gmail()`, `drive()`, `sheets()`, `slides()`, `forms()`, `docs()`) whose `subject` is a mapping function, so one connector serves a pool per person instead of one connector per mailbox. People always act as themselves through the mapping, in any pool. An agent acts as the owner of the pool it came through, resolved before and instead of the mapping, which is never called for an agent. An agent in a pool without an owner fails `auth_required` before any request leaves, and the message names no address. A fixed `subject` address still applies to every caller, agents included, and ignores owners. Nothing in arguments, headers, or programs can choose the owner. `owner` must be a Workspace email address, checked at construction by the rule a fixed `subject` uses, and `describeConfig()` pools add `owner` (`null` when unset). `/ui/api/config` serves the address only to an authenticated maintainer; an open deployment's anonymous caller reads `null`. Breaking for agents only: people and fixed subjects behave as before. In 0.32 a `cta_` token reached the mapping function, which usually answered `undefined` for a caller with no principal (`auth_required`). Now an agent never reaches it, so a mapping that answered for machine tokens, by token subject or otherwise, no longer applies; set that address as the `owner` of the agent's pool.
25
+ - Operator contract v2 `runsAs` for shared connectors now says where the credential lives (`vault`, `missing`, `oauth`, `config` or `unauthenticated`), so a credential supplied in deployment config, or a connector that needs none, is not shown as unsaved.
26
+ - Activate ten deterministic agent evaluations for Stripe, Notion, Cloudflare, and Vercel REST connectors, with state, recovery, pin, paging, and value-safety graders and saved evidence for offline regrading.
27
+
28
+ Model Stripe idempotency by account and mode, reject refused named write attempts in read-only graders, bind analytics counts to their metrics, and align environment confirmation with its prompt. Replace fixture-invalid measurements with fresh live trials.
29
+ - Allow Notion token connectors to replace the default call-admission policy with `callAdmission`. The default remains 180 calls per minute and 3 concurrent. Malformed policies, including falsy values other than `undefined`, fail at construction for both token and OAuth connectors. Shared admission validation also rejects malformed nested budgets instead of silently disabling their limit.
30
+
31
+ ### Changed
32
+
33
+ - Endpoints shows who can use each endpoint: `/mcp` lists its maintainers and each pool its members, people by name (or raw id) and agents as `agent:<name>`, with long lists collapsed behind "+N more". Tokens lists one row per agent declared in code with its pools, token state, date and last seen; issue, rotate and revoke keep their confirmations, a new secret appears once in a dialog, an unknown or aborted outcome is a failure notice that is never retried, and an agent missing from the live slot list cannot be acted on until a refresh lists it. Pool members get their own page with their endpoints and client setup, connect, reconnect or disconnect for their personal OAuth accounts, a credential form for personal credential slots whose Add, Replace, Test and Clear follow `manage.actions`, and the names of connectors shared with them. Every token and member action is checked again immediately before it is sent, so a slot list or contract refreshed meanwhile stops it, and a token operation with an unknown outcome disables that agent's controls until a refresh. Every token mutation, Issue included, sends the slot revision the operator confirmed against as `expect` (P3b of #837); a slot without a revision is never acted on, a contract revision that differs from the list's unresolves the slot, and a 409 `slot_changed` reads "Changed since you looked, refresh".
34
+ - The operator UI reads operator contract v2 and refuses v1. Endpoints lists `/mcp` and every pool, with an agent URL for pools that have agent members, and each expanded row shows who every connector runs as. Connectors adds a Matrix view of connectors by endpoint, where a cell opens its exact tools. Pool members see only their endpoints and client setup, and someone who is neither a maintainer nor a member sees a No access page. A shared connector's runs-as names where its credential comes from.
35
+ - **Breaking:** Agents move from `/mcp/<pool>` to `/agent/<pool>`. An agent endpoint admits only the built-in `cta_` adapter and a member agent of that pool, serves exactly the pool's tools with its `readOnly`, binds result handles to `/agent/<pool>`, answers a missing or invalid credential with a machine-only `WWW-Authenticate: Bearer` challenge, and publishes no protected-resource metadata. It never runs Clerk or Access and ignores cookies, Clerk handshake parameters, `ctx.access`, and Access headers. Only a raw pathname byte-equal to `/agent/` plus a pool name is an agent endpoint; every other path in the agent namespace (any case, escapes, doubled slashes, trailing slash, extra segments) is routed first and answers the pool 404 before auth, so no path Cloudflare Access bypasses as `/agent/*` reaches a human route. Breaking: `/mcp`, `/mcp/<pool>`, and the operator, connect, and callback routes refuse `cta_` tokens without reading token storage (401 on MCP and operator data routes, 403 on interactive-only routes), so point every agent at `/agent/<pool>`; `connecta doctor` checks `/agent/<pool>` for a `cta_` token and requires `--pool`, and the Node template's doctor checks `/agent/local-machine`. Deployments behind Cloudflare Access add one self-hosted Access application per agent hostname for `<host>/agent/*` with a single Bypass policy beside the Worker-level application, after which agents send only their `cta_` token (deploying guide, "Agent endpoints behind Cloudflare Access"). The `agent_not_maintainer` denial reason is removed.
36
+ - **Breaking:** Machine clients are agents declared by name in a new top-level `agents` option, each with one `cta_` token slot that operators issue, rotate, and revoke from `/ui/access-tokens`, the operator UI, or `AccessTokenManager.rotate` and `revoke`. Rotation is an immediate cutover: the old secret refuses on its next request, and of concurrent rotations exactly one returns a secret. The agent's slot is the only authority admission consults, and each operation commits by one compare-and-set on it and only moves forward: a rotation cancelled before its commit publishes nothing, and one cancelled after it moves the slot to revoked within one deadline that bounds every storage call of the operation, the commit's acknowledgement included (otherwise it reports the outcome unknown, never success; rotate again for a replacement secret). The request path deletes nothing it did not just write: superseded records and lookups stay behind, inert, so storage grows by one small record per rotation or adoption. A token admits only while it is bound to a currently declared agent, so removing a name stops its token on the next request. Admitted agents carry `identity.agent` (set only by the built-in adapter; any other provider returning `agent` is refused), have no principal and so no personal connectors, and are attributed in activity by agent name rather than token record id. Breaking: creating, renaming, and human-bound tokens are removed from the routes, manager, and UI; tokens issued by earlier releases stop admitting until trusted provisioning adopts each machine token for an agent with `AccessTokenManager.adopt(tokenId, agent)` (Node template: `npm run adopt-token`), which keeps its secret without rewriting the legacy record and takes a never-deleted claim so a legacy token is adopted by at most one agent, once, and human-bound tokens cannot be adopted; `accessTokens(storage, { maxActive })` throws; `describeConfig()` adds `agents` and drops `modules.accessTokens.maxActive`; grant callbacks that matched token record ids should match `identity.agent`. The Node template replaces `provision-token` with `npm run rotate-token -- <agent>` and declares `local-machine`.
37
+ - Record the decision that pools are the access model: maintainers alone use plain `/mcp`, pool members get exactly their pool's tools, agents are declared in code and reach `/agent/<pool>` with a `cta_` token, and a pool owner sets the Google Workspace account agents act as.
38
+ - **Breaking:** Management follows `maintainers` and pool `members` (#837). Shared credentials and OAuth grants, and agent token list, issue, rotate, and revoke, take an interactive maintainer; activity reads take a maintainer principal, interactive or not, within the optional `readGate`. A personal connector's own account may be connected and disconnected, always in the caller's own partition, by an interactive maintainer or by an interactive member of a pool containing that connector, through `authorize_connector` at their pool, the `/connect` link and callback, and `/ui/oauth/<id>` and `/ui/credentials/<id>`, which now admit such a member for their pools' personal connectors. Agents and other machine credentials manage nothing. Pool members get the private 404 on activity and token routes. A `cta_` credential is refused before token storage is read: 401 on activity data routes and 403 on interactive token-management routes. Breaking: `identity.credentialAdministration`, `identity.personalConnection`, `identity.accessTokenManagement`, and `identity.activityAccess` are removed and each fails construction naming its replacement; `describeConfig()` drops `identity`; the `ConnectaIdentityConfig` and `ConnectorPermission` types are no longer exported.
39
+ - **Breaking:** Add the 0.33 migration guide (`documentation/migration-0.33.md`), mapping every removed access option to `maintainers`, pool `members`, and `agents`, with agent endpoints, pool owners, token revisions, operator contract v2, and the 0.29.0–0.32.0 pre-auth listen fix. Add connecta's agent pool layout as a documented standard in the auth guide ("Organizing an agent's pools") and decision 0007: one Connecta connection per agent, a base pool `<agent>` with shared connectors and no `owner`, and an `<agent>-<user>` pool per person with that person's own connector ids and their Workspace address as `owner`. The Node template and Worker example agent guidance follow it. Breaking: the empty `identity` option is removed: `identity`, even `{}`, now fails construction, still naming each retired key's replacement. The Access bypass probe requires `--profile`.
40
+ - **Breaking:** `GET /ui/api/config` serves operator contract v2 and is the operator page's only gate: 401 with the auth challenge when signed out (an agent token gets the machine-only 401 every human route gives it), the private 404 for anyone who is neither a maintainer nor a pool member, and 200 with `role` otherwise. A maintainer gets the whole view: `root` and every pool with their members and directory labels, pool owners, every declared agent with its token slot and last activity, and a connector × endpoint matrix naming who each connector runs as. A pool member gets only their own pools and connectors, with their own connection state and the actions they may take. Contract v1, `you.grants`, `you.permissions`, `hasGrant`, and the `OperatorViewer` and `OperatorPool` types are removed; `you.interactive` remains, and `OperatorUiContract` now names v2. `/ui/data` and `/ui/connectors/<id>` stay maintainer-only. `/ui/oauth/<id>` gates each verb on its own hooks: connecting needs `startAuth`, `finishAuth` and `verifyState`, and disconnecting needs only `disconnectAuth`, so a connector provisioned out of band can be disconnected. `DELETE /ui/credentials/<id>` refuses to clear an empty partition or slot with 409 `credential_required`. `connecta doctor --config` reads v2 and refuses a member's view.
41
+ - **Breaking:** Pools are the access model (decision 0006). A new top-level `maintainers` list names the people who reach plain `/mcp`, which serves the whole catalog with the deployment `readOnly`, and the operator routes that administer the deployment (catalog, activity, shared connector auth, access tokens); it is required whenever an inbound provider other than the built-in agent-token adapter is configured. Each pool now requires a non-empty static `members` list and serves exactly its `tools` to those members, with no intersection against anything else. A reference is a bare principal id resolved in the namespace of the deployment's one interactive provider, `{ namespace, id }`, or `"agent:<name>"` for a declared agent; ambiguous, unknown, repeated, or misplaced references (an agent in `maintainers`) fail construction. A non-maintainer at `/mcp`, a non-member, and an undeclared pool get the identical 404 after auth, logged as `not_maintainer`, `not_pool_member`, or `pool_not_declared`; result handles stay bound to their endpoint and paging re-admits the caller. A pool member may connect their own personal connector in that pool through `/connect/<id>`, which starts directly for non-maintainers. Shared connector auth (issuing a link, starting, completing the callback, disconnecting, and writing a credential) requires a maintainer. Breaking: `identity.connectorAccess` (with its guarded `{ tool, requireReadOnly: true }` grants; use a `readOnly: true` pool), the top-level `grant` (use `maintainers`), and pool `grant` (use `members`) are removed and fail construction naming their replacement; `describeConfig()` drops `hasGrant` at the root and on pools and `identity.connectorAccess`, and adds `maintainers` and pool `members`; anyone neither a maintainer nor a pool member gets the private 404 on operator routes, and the member access the management and operator contract entries describe is all a member gets there. Agent members reach their pools at `/agent/<pool>` (agent endpoints entry) and are refused at plain `/mcp`; `connecta doctor --pool <name>` checks a pool, and the Node template (`CONNECTA_MAINTAINERS`, a `local-machine` pool for the `local-machine` agent, `npm run doctor`) and Worker example (`CONNECTA_MAINTAINERS`, a `ci` pool for the `ci` agent) are updated accordingly.
42
+ - **Breaking:** Agent token slots carry an opaque `revision` that changes on every commit to the slot (issue, rotate, revoke, adopt). `GET /ui/access-tokens`, the slot each mutation returns, and operator contract v2's `OperatorAgent.token` list it. Breaking: `POST` (issue or rotate) and `DELETE` (revoke) `/ui/access-tokens/<agent>` now require `{ "expect": "<revision>" }` as a JSON body (`DELETE` also takes `?expect=<revision>`); a missing or malformed `expect` is 400, and a slot whose revision differs is 409 `{ error, code: "slot_changed" }` before anything is minted or written. The slot's compare-and-set commits only from the slot with that revision, so a change landing between the read and the commit refuses the same way instead of being overwritten or revoked. `AccessTokenManager.rotate`, `revoke`, and `adopt` take the same optional `expect`; without it they behave as before. Each revocation stores a random, non-secret `revocationId`, so no two revocations store the same slot and a call recognizes only its own: of two conditional revocations of one slot, exactly one succeeds and the other is 409 `slot_changed`.
43
+ - Make the shipped documentation an operator entry point with self-contained connector, people, credential, token, pool, upgrade, and diagnosis procedures, and separate contributor references.
44
+ - Record a production Cloudflare probe showing that a path-scoped Access Bypass application for `<host>/agent/*` overrides a Worker-level Access application on that path only, with the edge's path matching and the pathnames the Worker receives, so `/agent/<pool>` can carry a single `cta_` credential behind Access.
45
+ - Rework the operator dashboard around Endpoints, Connectors, Activity, Tokens and Settings with a top bar, light and dark Geist typography, expandable endpoint tool lists, three connector tabs and catalog search. Preserve legacy links and existing permission gates.
46
+
47
+ ### Fixed
48
+
49
+ - Operator pages apply a saved color scheme before first paint, through a small blocking hashed initializer, so a viewer who saved dark mode on a light system no longer sees a light frame. A deployment's pinned scheme still wins. Operator pages also load when browser storage throws, as in private browsing; a bearer token is then not remembered.
50
+ - Charge Cloudflare zone-ownership lookups for account pin enforcement to the
51
+ connector's shared call-admission budget. An exhausted budget refuses the lookup
52
+ and enclosing call before sending the lookup or target request. Keep the separate
53
+ limits of three ownership lookups per call and 60 per five minutes per connector.
54
+ - Keep Gmail's unsupported-charset UTF-8 fallback notice in a separate `bodyNote` field on message, thread, and draft reads so body truncation cannot remove it.
55
+ - Give load-sensitive test setup, static scans and child processes explicit deadline headroom while preserving correctness, CPU budget and heartbeat assertions.
56
+ - Send selected properties to Notion as repeated `filter_properties` URL parameters for named page reads and data source queries, reducing response size while preserving the projected page title. Page reads whose deduplicated selection plus title exceeds Notion's 100-filter limit fetch all properties and project locally instead.
57
+ - Review 22 additional Notion hosted MCP tools from a live catalog, preserving write enforcement for mutations and documenting signed download and upload credentials.
58
+ - Recover operator pages from transient Cloudflare Access and auth-state failures without reloading. Distinguish unavailable identity lookups from rejected identities, prevent caching refusals, reconcile catalog and auth refreshes, scope and cancel bounded OAuth status polling, prevent queued focus recovery after sign-out, preserve shared passive retry throttling and backoff across Access refusals, coalesce passive returns through slow catalog/auth recovery with a 15-second admission deadline that releases Retry without aborting healthy pending reads, and show unreadable credentials as unavailable.
59
+ - Preserve caller-supplied REST idempotency keys up to the schema's 255-character limit in `uncertainCall.recovery` after an ambiguous write. Versions 0.31.0 through 0.32.0 echoed caller-supplied keys only up to 128 characters. Other recovery values retain their 128-character cap, and keys longer than 255 characters are still rejected before dispatch.
60
+ - Refuse malformed `callAdmission` policies at construction across connector and maintained provider factories, including custom connectors registered by a deployment. Only omission or `undefined` selects the documented default; `false`, `null`, `0`, and `""` no longer silently disable admission or select a default.
61
+
62
+ ### Security
63
+
64
+ - Affected versions: 0.29.0–0.32.0. An unauthenticated POST with `Mcp-Method: subscriptions/listen` to `/mcp` or `/mcp/<pool>` was buffered with no byte cap before authentication and request admission, so an anonymous client could hold unbounded memory per request without taking an admission permit. Every MCP endpoint (`/mcp`, `/mcp/<pool>`, and `/agent/<pool>`) now authenticates first, and endpoint admission follows, before any body is read. Every pre-dispatch body read is bounded: classification reads at most 64 KiB, and past that connecta answers 413 with JSON-RPC code `-33006` and stops reading. A confirmed modern listen still takes no permit and is refused as unsupported; any other body carrying that header takes a permit before dispatch.
65
+ - Withhold the whole query and fragment of canonical Notion file links embedded in every REST response and error string, including Markdown and async results. Match only each URL's actual authority, preserving external proxies and look-alike hosts with ordinary parameters; AWS and Google signing parameter values are still withheld at any host. Refuse writes carrying the exact plain or encoded `[redacted]` placeholder in a Notion file link's own query or fragment before dispatch. Admission checks every adjacent Markdown destination, recognizes leading spaces, tabs, or one line ending, and respects balanced or escaped parentheses. Both scans stay linear. Split rich-text runs, encoded hosts or signing names, bare hosts without `//`, and userinfo before a file host are outside this scan's threat model.
66
+ - Withhold AWS SigV4/Google V4 signing-parameter values in every decoded Stripe, Vercel, and Cloudflare REST string, and generic credential-named URL queries/fragments only on reviewed diagnostic paths and vendor error text. Preserve ordinary code/key/token URLs in customer descriptions, metadata, billing text, DNS TXT, KV values and operator HTML. Refuse withheld URL destinations before every supported write transport, including decoded raw/base64 and multipart uploads, documented base64 JSON fields, and JSON object keys; scan every JSON key and value iteratively within the host's request-size limit without separate depth/breadth ceilings. Decode documented base64 fields once within their encoded length, retaining Cloudflare KV's 25 MiB value limit instead of applying the response cap. Cover indexed Vercel deployment lists and Cloudflare telemetry source payloads and invocation maps, with a pinned-operation guard for embedded-URL rules. Scan original escaped text and relative Markdown destinations before projections. Binary/base64 response bodies remain unchanged; Cloudflare omits envelope cursors that sanitizing would change. Notion retains its reviewed file-host and write-admission policy.
67
+
5
68
  ## 0.32.0 — 2026-10-14
6
69
 
7
70
  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.
package/README.md CHANGED
@@ -31,8 +31,10 @@ flowchart LR
31
31
  style Connecta fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b,stroke-width:2px
32
32
  ```
33
33
 
34
- You choose which services to connect and who can use them. Your agent keeps
35
- the same connection as those services change.
34
+ You choose which services to connect and who can use them: the maintainers
35
+ you name reach everything, and everyone else, people and agents alike, reaches
36
+ only the pools of tools you list them in. Your agent keeps the same connection
37
+ as those services change.
36
38
 
37
39
  ## From a question to an answer
38
40
 
@@ -114,16 +116,22 @@ deployment owner. Permission to use a connection is separate from permission
114
116
  to manage its credentials.
115
117
 
116
118
  People sign in through Clerk on Node or Cloudflare Access on Workers. Machine
117
- clients use tokens issued by your Connecta deployment. A deployment can serve
118
- several people within one tenant, each with the access you assign.
119
+ clients are agents you name in deployment code, each with one token issued by
120
+ your Connecta deployment. Agents connect through their own address for each
121
+ pool, separate from the one people use, so an agent sends only its token, even
122
+ behind Cloudflare Access. A deployment can serve
123
+ several people within one tenant, each with the access you assign. An agent
124
+ that serves several people keeps one connection and switches pools per turn:
125
+ a shared pool for group chats and scheduled work, and a pool per person, with
126
+ that person's own accounts, used only when that person is the one talking.
119
127
 
120
128
  ## A place to see your connections
121
129
 
122
130
  The optional operator UI shows connected services, available tools, access,
123
- and configuration. Authorized people can connect accounts, manage credentials,
124
- and issue client tokens. Optional activity history shows who called what,
131
+ and configuration. Maintainers manage shared credentials, issue or rotate agent
132
+ tokens, and read the optional activity history, which shows who called what,
125
133
  when, and whether it worked, without recording arguments, results, or program
126
- code. The UI's controls follow the signed-in person's permissions.
134
+ code. Pool members connect their own accounts on personal connections.
127
135
 
128
136
  Your agent or developer changes the integrations and access rules in deployment
129
137
  code. The UI helps you inspect them and manage authentication.
package/bin/connecta.mjs CHANGED
@@ -20,7 +20,7 @@ function usage() {
20
20
  console.log(`Usage:
21
21
  connecta init [directory]
22
22
  connecta migrate-state <state.json> <connecta.sqlite>
23
- CONNECTA_TOKEN=<bearer> connecta doctor [--config] [--url http://localhost:8787]
23
+ CONNECTA_TOKEN=<bearer> connecta doctor [--config] [--url http://localhost:8787] [--pool <name>]
24
24
  CF_ACCESS_CLIENT_ID=<id> CF_ACCESS_CLIENT_SECRET=<secret> connecta doctor --url https://worker.example`);
25
25
  }
26
26
 
@@ -78,7 +78,8 @@ async function init() {
78
78
  console.log(` ${shellCd(target)}`);
79
79
  console.log(" npm install");
80
80
  console.log(" # Choose readOnly in src/connecta.config.ts: false writes with client approval, true refuses writes.");
81
- console.log(' npm run --silent provision-token -- "local-machine"');
81
+ console.log(" # Declare each machine client in agents in src/connecta.config.ts; local-machine is the first.");
82
+ console.log(" npm run --silent rotate-token -- local-machine");
82
83
  console.log(" # Save the returned cta_ token privately for your MCP client and doctor.");
83
84
  console.log(" npm start");
84
85
  console.log(" # README.md covers database paths and Docker provisioning.");
@@ -137,13 +138,19 @@ async function doctorFetch(url, init = {}) {
137
138
  async function doctor() {
138
139
  for (let index = 0; index < args.length; index++) {
139
140
  if (args[index] === "--config") continue;
140
- if (args[index] !== "--url" || !args[++index] || args[index].startsWith("--")) {
141
+ if (!["--url", "--pool"].includes(args[index]) || !args[++index] || args[index].startsWith("--")) {
141
142
  usage();
142
143
  process.exitCode = 1;
143
144
  return;
144
145
  }
145
146
  }
146
147
  const requestedUrl = option("--url", "http://localhost:8787");
148
+ // An agent token reaches only /agent/<pool> for the pools that list the
149
+ // agent; /mcp and /mcp/<pool> admit people alone.
150
+ const pool = option("--pool", undefined);
151
+ if (pool !== undefined && !/^[a-z0-9_-]+$/.test(pool)) {
152
+ throw new Error("Doctor --pool must be a pool name: lowercase letters, digits, _ and -.");
153
+ }
147
154
  const parsedUrl = new URL(requestedUrl);
148
155
  if (parsedUrl.username || parsedUrl.password) {
149
156
  throw new Error("Doctor URL must not contain credentials.");
@@ -167,6 +174,10 @@ async function doctor() {
167
174
  "Set CONNECTA_TOKEN or a CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET pair so doctor can inspect the MCP surface.",
168
175
  );
169
176
  }
177
+ const agent = Boolean(token?.startsWith("cta_"));
178
+ if (agent && pool === undefined && !args.includes("--config")) {
179
+ throw new Error("An agent token reaches only /agent/<pool>: pass --pool with a pool that lists the agent.");
180
+ }
170
181
  const authHeaders = {
171
182
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
172
183
  ...(accessClientId && accessClientSecret
@@ -189,7 +200,12 @@ async function doctor() {
189
200
  } catch {
190
201
  throw new Error("Deployment returned an invalid config contract.");
191
202
  }
192
- if (contract?.schemaVersion !== 1 || contract.config?.schemaVersion !== 1) {
203
+ // Operator contract v2 (#837) serves a pool member their own view, which
204
+ // carries no config; only a maintainer's carries the snapshot.
205
+ if (contract?.schemaVersion === 2 && contract.role === "member") {
206
+ throw new Error("The config snapshot is for maintainers only.");
207
+ }
208
+ if (contract?.schemaVersion !== 2 || contract.role !== "maintainer" || contract.config?.schemaVersion !== 1) {
193
209
  throw new Error("Deployment returned an unsupported config contract.");
194
210
  }
195
211
  // The server builds this with describeConfig's allowlist; do not print
@@ -221,8 +237,10 @@ async function doctor() {
221
237
  { name: "connecta-doctor", version: CONNECTA_VERSION },
222
238
  { versionNegotiation: { mode: "auto", probe: { timeoutMs: DOCTOR_TIMEOUT_MS } } },
223
239
  );
224
- const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`), {
225
- requestInit: { headers: authHeaders },
240
+ // Agent endpoints bypass Access at the edge and read only the cta_ token.
241
+ const endpoint = agent ? `/agent/${pool}` : `/mcp${pool ? `/${pool}` : ""}`;
242
+ const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}${endpoint}`), {
243
+ requestInit: { headers: agent ? { Authorization: `Bearer ${token}` } : authHeaders },
226
244
  fetch: doctorFetch,
227
245
  });
228
246
  let negotiatedVersion;
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.32.0";
3
+ export const CONNECTA_VERSION = "0.33.0";
@@ -1,38 +1,182 @@
1
- import type { IdentityReference, InboundAuth, KVStorage } from "./types.js";
1
+ import type { InboundAuth, KVStorage } from "./types.js";
2
2
  import type { AccessTokensModule } from "./module-contracts.js";
3
- export interface AccessTokenMetadata {
4
- id: string;
3
+ /**
4
+ * One declared agent's token slot as management sees it. Never secret
5
+ * material. `revision` is opaque: it changes on every commit to the slot and
6
+ * is what `expect` names to make an operation conditional on this view.
7
+ */
8
+ export type AgentTokenSlot = {
5
9
  name: string;
6
- tokenPrefix: string;
7
- createdAt: string;
8
- revokedAt?: string;
9
- }
10
- export interface CreatedAccessToken {
10
+ state: "none";
11
+ revision: string;
12
+ } | {
13
+ name: string;
14
+ state: "issued";
15
+ issuedAt: string;
16
+ revision: string;
17
+ } | {
18
+ name: string;
19
+ state: "revoked";
20
+ revokedAt: string;
21
+ revision: string;
22
+ };
23
+ /** A newly issued secret, returned once, and the slot that now admits it. */
24
+ export interface IssuedAgentToken {
11
25
  token: string;
12
- accessToken: AccessTokenMetadata;
26
+ agent: AgentTokenSlot;
13
27
  }
28
+ /** Why a token operation was refused, for routes and provisioning scripts. */
29
+ export declare class AccessTokenError extends Error {
30
+ readonly code: "unknown_agent" | "conflict" | "slot_changed" | "not_adoptable" | "aborted" | "outcome_unknown";
31
+ readonly name = "AccessTokenError";
32
+ constructor(code: "unknown_agent" | "conflict" | "slot_changed" | "not_adoptable" | "aborted" | "outcome_unknown", message: string);
33
+ }
34
+ /** Options every token mutation takes. */
35
+ export interface AccessTokenOperationOptions {
36
+ /**
37
+ * The request this operation serves. Checked before the commit point, the
38
+ * slot's compare-and-set: an abort before it publishes nothing. An abort
39
+ * after a rotation commits revokes the slot, since the secret cannot be
40
+ * delivered.
41
+ */
42
+ signal?: AbortSignal;
43
+ /**
44
+ * The request's own deadline, in epoch milliseconds, when it has one. The
45
+ * operation ends at the earlier of this and `OPERATION_WINDOW_MS` after it
46
+ * starts.
47
+ */
48
+ deadline?: number;
49
+ /**
50
+ * The slot `revision` the caller decided from. When set, the operation
51
+ * refuses with `slot_changed` before minting or writing anything if the
52
+ * slot's current revision differs, and its commit compare-and-sets from
53
+ * exactly the slot with that revision, so a change that lands between the
54
+ * read and the commit refuses the same way instead of being overwritten.
55
+ */
56
+ expect?: string;
57
+ }
58
+ /** Whether `value` has the shape of a slot revision. */
59
+ export declare function isSlotRevision(value: unknown): value is string;
14
60
  /**
15
- * Deployment-scoped personal access tokens. Secret material is never
61
+ * Agent tokens: one slot per agent declared in code. Secret material is never
16
62
  * recoverable: authentication indexes a SHA-256 digest of a random 256-bit
17
- * token, while separately enumerable metadata powers operator management.
63
+ * token, and only that digest persists. A token admits only while its record
64
+ * is bound to a currently declared agent and that agent's slot names it.
18
65
  */
19
66
  export declare class AccessTokenManager {
20
67
  private readonly storage;
21
68
  readonly auth: InboundAuth;
22
- private readonly maxActive;
69
+ private readonly agents;
70
+ private readonly agentOrder;
23
71
  constructor(storage: KVStorage, options?: {
24
- maxActive?: number;
72
+ agents?: readonly string[];
25
73
  });
74
+ private declared;
26
75
  private read;
27
- list(): Promise<AccessTokenMetadata[]>;
28
- create(name: unknown, createdBy: string | IdentityReference): Promise<CreatedAccessToken>;
29
- rename(id: string, name: unknown): Promise<AccessTokenMetadata | null>;
30
- revoke(id: string, revokedBy: string): Promise<AccessTokenMetadata | null>;
31
- private updateRecord;
32
- private updateActive;
76
+ private readSlot;
77
+ /** One slot per declared agent, in declaration order. */
78
+ list(): Promise<AgentTokenSlot[]>;
79
+ /**
80
+ * Issue the agent's first token, or replace its current one.
81
+ *
82
+ * 1. The slot is read. With `expect`, a slot whose revision differs refuses
83
+ * with `slot_changed` before any secret is minted.
84
+ * 2. The new record and its lookup are written. They are inert: admission
85
+ * requires the slot to name the record.
86
+ * 3. `signal` is checked.
87
+ * 4. One compare-and-set of the slot, from exactly what was read to
88
+ * `issued` with the new record, is the only commit point. From it the
89
+ * new secret admits and the previous one refuses. Of concurrent
90
+ * rotations exactly one commits; the others publish nothing and refuse
91
+ * with `slot_changed` when they named `expect`, a conflict otherwise.
92
+ * 5. If the request has ended once the commit is decided, the secret cannot
93
+ * be delivered, so the slot moves forward to `revoked`. Everything after
94
+ * the commit runs inside one `OperationWindow`; if it cannot settle in
95
+ * time this rejects with `outcome_unknown` ("rotate again"), never success.
96
+ *
97
+ * Nothing is ever deleted except this call's own record and lookup, while
98
+ * they are known to be uncommitted. The previous record and lookup stay
99
+ * behind, inert, because the slot no longer names them.
100
+ */
101
+ rotate(name: string, issuedBy?: string, options?: AccessTokenOperationOptions): Promise<IssuedAgentToken>;
102
+ /** Whether the agent's slot names `tokenId`, read back inside the window. */
103
+ private slotNames;
104
+ /**
105
+ * Move the slot forward to `revoked` if it still names `tokenId`, inside the
106
+ * window. An unsettled outcome is reported, never swallowed.
107
+ */
108
+ private revokeIfNamed;
109
+ /**
110
+ * Revoke the agent's token. One compare-and-set of the slot to `revoked` is
111
+ * the commit; `signal` is checked after the read and before it. The record
112
+ * and lookup stay behind, inert. With `expect`, a slot whose revision
113
+ * differs refuses with `slot_changed` before anything is written, and so
114
+ * does a slot that changes between the read and the commit; without it,
115
+ * whatever the slot names by then is revoked instead.
116
+ */
117
+ revoke(name: string, options?: AccessTokenOperationOptions): Promise<AgentTokenSlot>;
118
+ /**
119
+ * Settle a conditional revocation whose commit went unanswered, inside the
120
+ * window. Only a slot carrying this call's own `revocationId` is its
121
+ * success. A slot still as read is revoked again from exactly that slot,
122
+ * with the same id; anything else is `slot_changed`.
123
+ */
124
+ private settleRevocation;
125
+ /**
126
+ * Trusted provisioning: let a declared agent keep using a machine token
127
+ * issued before agents. The legacy record is never changed.
128
+ *
129
+ * 1. A write-once claim, `access-token:v1:adopted:<legacy id>`, is set from
130
+ * absent to the agent's name. It is never deleted: once any adoption has
131
+ * started, the legacy token belongs to that agent forever, even after it
132
+ * is revoked or rotated away, so a retired secret can never be adopted
133
+ * again. It is inert to admission; an abort after it leaves the token
134
+ * adoptable only by the same agent.
135
+ * 2. A new record carrying `agent` and the legacy digest is written; it is
136
+ * inert until the slot names it.
137
+ * 3. `signal` is checked.
138
+ * 4. One compare-and-set of the slot from none to `issued` with the new
139
+ * record is the commit. Adopt delivers no secret, so once committed it is
140
+ * completed even if the request ended.
141
+ * 5. The digest's lookup is moved from the legacy record to the new one by
142
+ * compare-and-set; from then the existing secret admits as the agent.
143
+ *
144
+ * The commit and everything after it run inside one `OperationWindow`.
145
+ * Retrying an adoption whose slot already names a record with this digest
146
+ * completes step 5 and succeeds. With `expect`, a slot whose revision
147
+ * differs refuses with `slot_changed` before the claim, and so does a
148
+ * commit that loses its compare-and-set. Human-bound, revoked, and claimed tokens are
149
+ * refused, as is an agent whose slot names anything else.
150
+ */
151
+ adopt(tokenId: string, name: string, options?: AccessTokenOperationOptions): Promise<AgentTokenSlot>;
152
+ /**
153
+ * Step 4 of adoption, idempotent and inside the window: point the digest's
154
+ * lookup at the record the slot names. If another agent's adoption took the
155
+ * lookup first, this slot can never admit, so it moves forward to `revoked`
156
+ * and this refuses.
157
+ */
158
+ private completeAdoption;
159
+ /** The declared agent whose issued slot names a record with this digest, if any, read inside the window. */
160
+ private agentCarrying;
161
+ /**
162
+ * Trusted provisioning: the legacy records `adopt` can take, oldest first:
163
+ * unbound, unrevoked, never claimed by an adoption, still the owner of their
164
+ * digest's lookup, and with a digest no declared agent's slot carries. Ids, names and creation times
165
+ * only; never secret material.
166
+ */
167
+ unboundTokens(): Promise<Array<{
168
+ id: string;
169
+ name: string;
170
+ createdAt: string;
171
+ }>>;
33
172
  private authorize;
34
173
  }
35
- /** Opt in using the same storage namespace that held the v0.23 records. */
36
- export declare function accessTokens(storage: KVStorage, options?: {
37
- maxActive?: number;
38
- }): AccessTokensModule;
174
+ /** accessTokens() takes no options: the agents declared in code bound its slots. */
175
+ type AccessTokensOptions = Record<never, never>;
176
+ /**
177
+ * Agent token authentication and management over the deployment's storage.
178
+ * Reuse the storage that held earlier tokens so their records stay readable;
179
+ * `createConnecta` binds the module to `agents`.
180
+ */
181
+ export declare function accessTokens(storage: KVStorage, options?: AccessTokensOptions): AccessTokensModule;
182
+ export {};