@zackbart/connecta 0.31.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 (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -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/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. package/templates/node/src/provision-token.ts +0 -17
@@ -0,0 +1,344 @@
1
+ # Migration to 0.33
2
+
3
+ Use this upgrade guide for an existing deployment moving to 0.33.
4
+ [Deployment upgrades](./deploying.md#upgrade-an-existing-deployment) cover the
5
+ general procedure; [the agent index](./README.md) routes other work.
6
+
7
+ 0.33 makes pools the access model.
8
+ Every deployment with inbound auth changes its configuration. Each removed
9
+ option fails construction with a message naming its replacement, so a
10
+ deployment that starts has no leftover option silently ignored. Work through
11
+ the sections in order, then [verify](#verify).
12
+
13
+ ## Old option to new option
14
+
15
+ | 0.32 | 0.33 |
16
+ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `identity.connectorAccess` | A pool per audience, with `tools` and `members`. A guarded `{ tool, requireReadOnly: true }` grant becomes a pool with `readOnly: true` |
18
+ | Top-level `grant` | `maintainers` |
19
+ | Pool `grant` | Pool `members` |
20
+ | `describeConfig().hasGrant` (root and pools) | `describeConfig().maintainers` and each pool's `members` |
21
+ | `identity.credentialAdministration` | `maintainers` (interactive) |
22
+ | `identity.personalConnection` | Pool `members` for personal connectors in their pools; maintainers for their own |
23
+ | `identity.accessTokenManagement` | `maintainers` (interactive) |
24
+ | `identity.activityAccess` | `maintainers`, still narrowed by `activity.readGate` |
25
+ | The `identity` section, even empty | Removed; delete it |
26
+ | `ConnectaIdentityConfig`, `ConnectorPermission` types | Removed |
27
+ | Tokens created or named in the UI, human-bound tokens | `agents: ["<name>"]`, one token slot each |
28
+ | `accessTokens(storage, { maxActive })` | `accessTokens(storage)` |
29
+ | `describeConfig().modules.accessTokens.maxActive` | `describeConfig().agents` |
30
+ | Node template `provision-token` | `npm run --silent rotate-token -- <agent>`; existing tokens: `npm run adopt-token` |
31
+ | A machine at `/mcp` or `/mcp/<pool>` with `cta_` | `/agent/<pool>`, with `"agent:<name>"` in the pool's `members` |
32
+ | Access service token plus `cta_` behind Cloudflare Access | `cta_` alone, through an `/agent/*` Access Bypass application |
33
+ | Grant callbacks matching token record ids | Membership by agent name; `identity.agent` in custom code |
34
+ | A Workspace `subject` mapping that answered for machine tokens | The pool's `owner` |
35
+ | One Workspace connector per person (`gmail_alice`) | One connector with a mapping `subject`, plus pool owners |
36
+ | `POST`/`DELETE /ui/access-tokens/<agent>` without a body | `{ "expect": "<revision>" }` from `GET /ui/access-tokens` |
37
+ | Operator contract `schemaVersion: 1`, `you.grants`, `you.permissions`, and `hasGrant` | `schemaVersion: 2` with `role`, `root`, `pools`, `agents`, `matrix`, and `connectors`; `OperatorUiContract` names v2 |
38
+ | `OperatorViewer`, `OperatorPool` types | Removed; `OperatorPoolV2`, `OperatorRootEndpoint`, `OperatorMemberConnector` and the other v2 types |
39
+ | `/ui/data`, `/ui/connectors/<id>` | Maintainers only; members read their view from `/ui/api/config` |
40
+ | A connector that connects with `startAuth` alone, or disconnects only alongside it | Connect needs `startAuth`, `finishAuth`, and `verifyState`; disconnect needs only `disconnectAuth` |
41
+ | `DELETE /ui/credentials/<id>` on an empty slot answering 204 | 409 `{ "problem": "credential_required" }` |
42
+ | The `agent_not_maintainer` denial reason | Removed; agents are refused at human endpoints before admission |
43
+
44
+ ## Maintainers and pool members
45
+
46
+ 0.32 combined an identity resolver, a top-level `grant`, and pool `grant`
47
+ callbacks. 0.33 reads access from two static lists in code:
48
+
49
+ - `maintainers` names the people who reach plain `/mcp`, which serves the
50
+ whole catalog, and every operator route. It is required whenever an inbound
51
+ provider that admits people is configured.
52
+ - Each pool's `members` names who reaches it: people at `/mcp/<pool>`, agents
53
+ as `"agent:<name>"` at `/agent/<pool>`. A pool serves exactly its `tools`,
54
+ intersected with nothing. A maintainer opens no pool implicitly.
55
+
56
+ ```ts
57
+ // 0.32
58
+ createConnecta({
59
+ auth: clerkAuth({ publishableKey, secretKey }),
60
+ grant: ({ principal }) => principal?.id === "user_alice",
61
+ identity: { connectorAccess, credentialAdministration, activityAccess },
62
+ pools: {
63
+ support: { tools: ["tickets", "crm.search"], grant: ({ principal }) => support.has(principal?.id ?? "") },
64
+ },
65
+ // …
66
+ });
67
+
68
+ // 0.33
69
+ createConnecta({
70
+ auth: clerkAuth({ publishableKey, secretKey }),
71
+ maintainers: ["user_alice"],
72
+ pools: {
73
+ support: { tools: ["tickets", "crm.search"], members: [...support] },
74
+ },
75
+ // …
76
+ });
77
+ ```
78
+
79
+ A reference is a bare principal id when the deployment has exactly one
80
+ interactive provider, `{ namespace, id }` otherwise, or `"agent:<name>"`.
81
+ Rewrite a predicate as the list it computed; ordinary code at construction
82
+ (`team.map(...)`) can build it. [Principals and operators](./auth.md#principals-visibility-and-operators)
83
+ owns the rules. A non-maintainer at `/mcp`, a non-member, and an undeclared
84
+ pool get the same 404.
85
+
86
+ ### Preserve effective access
87
+
88
+ Before deleting `identity.connectorAccess`, compute what each audience could
89
+ actually use: its endpoint's tools intersected with that identity's old
90
+ connector permissions, including read-only guards. Write that effective set
91
+ into the new pool. Copying an old pool's broader `tools` verbatim would widen
92
+ access because 0.33 has no second intersection. A person previously admitted
93
+ to plain `/mcp` with restricted connector permissions needs a pool and a new
94
+ `/mcp/<pool>` client URL, not a maintainer entry. Maintainers have the whole
95
+ root catalog and administrative authority.
96
+
97
+ For example, suppose Alice administered the deployment, while Bob's old team
98
+ pool listed `crm` and `tickets` but his `connectorAccess` allowed only
99
+ `{ tool: "tickets.search", requireReadOnly: true }`. The new configuration is:
100
+
101
+ ```ts
102
+ maintainers: ["user_alice"],
103
+ pools: {
104
+ team: { tools: ["tickets.search"], members: ["user_bob"], readOnly: true },
105
+ },
106
+ // Delete identity and all grant callbacks; Bob connects at /mcp/team.
107
+ ```
108
+
109
+ A read-only guard applies to the whole new endpoint. If an audience previously
110
+ had both writable tools and guarded reads, separate those into explicit pools
111
+ with the appropriate `readOnly` choice rather than dropping the guard. Then
112
+ update clients to the intended endpoints.
113
+
114
+ ## Management
115
+
116
+ The four management resolvers are gone, and the `identity` section with them.
117
+ [Management permissions](./auth.md#management-permissions) has the table:
118
+ interactive maintainers administer shared credentials and OAuth grants and
119
+ issue, rotate, and revoke agent tokens; maintainers read activity, within an
120
+ optional `readGate`; a pool member connects and disconnects their own account
121
+ on a personal connector in one of their pools. Agents manage nothing.
122
+
123
+ ## Agents and their tokens
124
+
125
+ Machine clients are agents declared by name. Each has one token slot that a
126
+ maintainer issues, rotates, and revokes; nothing outside code adds, renames,
127
+ or rebinds one, and removing a name stops its token on the next request.
128
+
129
+ 1. Add `agents: ["<name>", …]` and keep `accessTokens(storage)` on the same
130
+ storage and key namespace. Drop `maxActive`.
131
+ 2. Keep each machine's existing secret by adopting its token once:
132
+ `npm run adopt-token` lists the tokens that can be adopted, and
133
+ `npm run adopt-token -- <token-id> <agent>` binds one (Node template), or
134
+ call `AccessTokenManager.adopt(tokenId, agent)` from trusted provisioning.
135
+ Until then the token refuses.
136
+ 3. A human-bound token, issued from the UI before 0.33, cannot be adopted and
137
+ stays refused. Issue that agent a new token with
138
+ `npm run --silent rotate-token -- <agent>` or from the Tokens page.
139
+ 4. Clients that call the token routes send the slot's `revision`:
140
+ `POST /ui/access-tokens/<agent>` (issue, rotate) and `DELETE` (revoke) take
141
+ `{ "expect": "<revision>" }` from `GET /ui/access-tokens`, and a slot that
142
+ changed answers 409 `slot_changed`. A missing `expect` is 400.
143
+ `AccessTokenManager` takes the same optional `expect`.
144
+
145
+ ### Programmatic token management
146
+
147
+ Trusted provisioning code that constructs its own `AccessTokenManager` must
148
+ pass the declared agents. The deployment's top-level `agents` does not bind a
149
+ separately constructed manager, so `new AccessTokenManager(storage)` knows no
150
+ agent and every `rotate` and `adopt` fails with `unknown_agent`.
151
+
152
+ | 0.32 | 0.33 |
153
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
154
+ | `new AccessTokenManager(storage, { maxActive })` | `new AccessTokenManager(storage, { agents })`, with the same names as the deployment's `agents` |
155
+ | `create(name, createdBy)` | `rotate(agent, issuedBy?, options?)`, returning `{ token, agent }`; the first rotation issues the slot's token |
156
+ | `rename(id, name)` | Removed; agents are named only in code |
157
+ | `revoke(tokenId, revokedBy)` | `revoke(agent, options?)`, returning the slot |
158
+ | `list()` of token records | `list()` of one slot per declared agent: `{ name, state, issuedAt?, revokedAt?, revision }` |
159
+ | (none) | `adopt(tokenId, agent, options?)` and `unboundTokens()` for tokens issued before agents |
160
+
161
+ `options` is `{ expect?, signal?, deadline? }`. `expect` is the slot
162
+ `revision` read from `list()`; when given, a slot that changed since refuses
163
+ with an `AccessTokenError` whose `code` is `slot_changed`, and nothing is
164
+ minted or written. Without it the calls behave as before.
165
+
166
+ ```ts
167
+ import { AccessTokenManager } from "@zackbart/connecta/auth/access-tokens";
168
+
169
+ const manager = new AccessTokenManager(storage, { agents: ["assistant"] });
170
+ const slot = (await manager.list()).find((agent) => agent.name === "assistant")!;
171
+ const { token } = await manager.rotate("assistant", "provisioning", { expect: slot.revision });
172
+ ```
173
+
174
+ Rotation is an immediate cutover: the old secret refuses on its next request.
175
+ An agent has no principal, so personal connectors are absent for it.
176
+ [Agent tokens](./auth.md#agent-tokens-and-upgrading-from-earlier-tokens) owns
177
+ the slot, revision, and adoption contracts.
178
+
179
+ ## Agent endpoints
180
+
181
+ Agents move from `/mcp/<pool>` to `/agent/<pool>`, and every human route
182
+ (`/mcp`, `/mcp/<pool>`, the operator pages and APIs, `/connect`, OAuth starts
183
+ and callbacks) refuses a `cta_` token before reading token storage. Agents are
184
+ never maintainers, so a machine that used plain `/mcp` moves to a pool that
185
+ holds its tools.
186
+
187
+ 1. List `"agent:<name>"` in the `members` of each pool the agent uses.
188
+ 2. Point its client at `https://<host>/agent/<pool>` with
189
+ `Authorization: Bearer cta_…` as its only credential.
190
+ 3. Behind Cloudflare Access, add one self-hosted Access application per agent
191
+ hostname for `<host>/agent/*` with a single Bypass policy, beside the
192
+ Worker-level application, then drop the agent's `CF-Access-Client-Id` and
193
+ `CF-Access-Client-Secret` headers. The
194
+ [deploying guide](./deploying.md#agent-endpoints-behind-cloudflare-access)
195
+ has the steps and the checks to repeat until the change settles. Never
196
+ bypass any other path.
197
+ 4. Run `connecta doctor --pool <pool>` with the agent's token; doctor checks
198
+ `/agent/<pool>` for a `cta_` token and requires `--pool`.
199
+
200
+ [Agent endpoints](./auth.md#agent-endpoints) owns the path, auth, and
201
+ membership contract.
202
+
203
+ ## Google Workspace pool owners
204
+
205
+ In 0.32 a `cta_` token reached a Workspace connector's `subject` mapping,
206
+ which usually answered nothing for a caller without a principal. In 0.33 an
207
+ agent never reaches the mapping. It acts as the `owner` of the pool it came
208
+ through, and fails `auth_required` in a pool without one. People always act
209
+ as themselves through the mapping, in any pool. A fixed `subject` address
210
+ still applies to every caller.
211
+
212
+ If your mapping returned an address for a machine token, set that address as
213
+ the `owner` of the agent's pool. Replace per-person connectors such as
214
+ `gmail_alice` and `gmail_bob` with one `gmail()` whose `subject` is a mapping
215
+ function, and give each person's pool its `owner`.
216
+ [Pool owners](./auth.md#pool-owners-for-google-workspace) owns the rules.
217
+
218
+ ## Operator contract v2
219
+
220
+ `GET /ui/api/config` serves `OperatorUiContractV2` (`schemaVersion: 2`) and
221
+ is the operator page's only gate; the v1 contract is removed:
222
+
223
+ - A signed-out caller gets 401 with the auth challenge.
224
+ - A `cta_` agent token gets the machine-only 401 Bearer challenge, as on every
225
+ human route, before token storage is read.
226
+ - A signed-in person who is neither a maintainer nor in any pool's `members`
227
+ gets the private JSON 404.
228
+ - A maintainer, or a person in a pool's `members`, gets 200 with their `role`.
229
+
230
+ The maintainer view carries `config`, `configSources`, `root`, every pool
231
+ with its owner and labelled members, every declared agent with its token slot
232
+ and `lastSeenAt`, and the connector × endpoint `matrix`. The member view
233
+ carries only that member's pools, without owner or members, and the
234
+ connectors in them, with their own connection and a `manage` naming the
235
+ actions they may take. `/ui/data` and `/ui/connectors/<id>` are
236
+ maintainer-only and give anyone else the same 404. Code that read v1's
237
+ `hasGrant`, `you.grants`, or `you.permissions` reads v2's `role`, `root`,
238
+ `pools`, `agents`, `matrix`, and `connectors` instead;
239
+ [the operator contract](./operator-ui.md#operator-data-contract) documents
240
+ every field. Read the published declarations in
241
+ `dist/operator-ui/contract.d.ts` inside the installed package for the exact
242
+ typed shape; `you.interactive` remains in v2. `OperatorUiContract` now names v2, and `OperatorViewer` and
243
+ `OperatorPool` are removed. `connecta doctor --config` reads v2 and prints a
244
+ maintainer's `config`; a member's view has none.
245
+
246
+ The connector auth routes accept what `manage` lists, with one recovery
247
+ exception: a stored credential that cannot be read can still be cleared,
248
+ although `manage` lists only `save` in that state.
249
+ `/ui/oauth/<id>` gates each verb on its own hooks: connecting needs
250
+ `startAuth`, `finishAuth`, and `verifyState`, and disconnecting needs only
251
+ `disconnectAuth`. A custom connector that offered connect with `startAuth`
252
+ alone adds the other two, without which its callback never completed. A
253
+ connector provisioned out of band, with `disconnectAuth` and no `startAuth`,
254
+ can now be disconnected. `DELETE /ui/credentials/<id>` refuses to clear a slot
255
+ that holds nothing, the caller's own partition for a personal connector, with
256
+ 409 `{ "error": "no credential is stored to clear", "problem": "credential_required" }`.
257
+ The bundled operator UI needs no change.
258
+
259
+ ## Security fix: pre-auth listen buffering
260
+
261
+ Versions 0.29.0 through 0.32.0 buffered the whole body of an unauthenticated
262
+ `POST` carrying `Mcp-Method: subscriptions/listen` to `/mcp` or `/mcp/<pool>`,
263
+ with no byte cap, before authentication and request admission. An anonymous
264
+ client could hold unbounded memory per request without taking an admission
265
+ permit. No credential or data was exposed; the risk is memory exhaustion.
266
+
267
+ 0.33 authenticates first, then checks endpoint admission, before reading any
268
+ body on `/mcp`, `/mcp/<pool>`, and `/agent/<pool>`. A pre-dispatch read takes
269
+ at most 64 KiB; past that, connecta answers 413 with JSON-RPC code `-33006`
270
+ and stops reading. The new `/agent/*` Access bypass therefore does not reopen
271
+ the issue.
272
+
273
+ What operators should know:
274
+
275
+ - The fix ships in 0.33.0 only; there is no patch release for 0.29–0.32.
276
+ Upgrade.
277
+ - No configuration change is needed for the fix itself.
278
+ - A Worker behind a Worker-level Access application received only requests
279
+ Access admitted. A deployment reachable without edge authentication, such
280
+ as Node behind a public URL, was exposed. If you cannot upgrade at once,
281
+ cap request body size at your proxy or refuse unauthenticated requests that
282
+ carry `Mcp-Method: subscriptions/listen` there.
283
+
284
+ ## Moving to the agent pool layout
285
+
286
+ A 0.32 agent usually reached `/mcp/<pool>` through a pool `grant` matching
287
+ its token, often with one connector per person and a second connection or
288
+ pool for each person's accounts. Move it to
289
+ [connecta's agent pool layout](./auth.md#organizing-an-agents-pools): one
290
+ connection; a base pool named after the agent with shared connectors and no
291
+ owner; one `<agent>-<user>` pool per person with their own connectors and
292
+ their Workspace address as `owner`.
293
+
294
+ ```ts
295
+ // 0.32
296
+ pools: {
297
+ assistant: {
298
+ tools: ["crm", "tickets", "notes_alice", "gmail_alice", "notes_bob", "gmail_bob"],
299
+ grant: ({ actor }) => actor.kind === "access_token" && actor.id === assistantTokenId,
300
+ },
301
+ },
302
+
303
+ // 0.33
304
+ agents: ["assistant"],
305
+ pools: {
306
+ assistant,
307
+ "assistant-alice": { ...assistant, tools: [...assistant.tools, "notes_alice", "gmail"], owner: "alice@example.com" },
308
+ "assistant-bob": { ...assistant, tools: [...assistant.tools, "notes_bob", "gmail"], owner: "bob@example.com" },
309
+ },
310
+ // with const assistant = { tools: ["crm", "tickets"], members: ["agent:assistant"] };
311
+ ```
312
+
313
+ 1. Declare the agent in `agents` and adopt its token (see
314
+ [Agents and their tokens](#agents-and-their-tokens)).
315
+ 2. Define the base pool `<agent>` once with the shared connectors and
316
+ `members: ["agent:<agent>"]`, and spread it into one `<agent>-<user>` pool
317
+ per person, adding that person's connectors and `owner`.
318
+ 3. Collapse per-person Workspace connectors into one connector with a mapping
319
+ `subject`. Keep other per-person accounts as their own shared-auth
320
+ connector ids (`notes_alice`) and list each only in its person's pool.
321
+ 4. Change the agent's client to one connection whose URL it sets per turn:
322
+ `/agent/<agent>-<user>` in a one-on-one with that user, `/agent/<agent>`
323
+ otherwise. Remove any second Connecta connection.
324
+ 5. Add this line to your deployment's own AGENTS.md (or its CLAUDE.md
325
+ equivalent), so agents find all operator procedures after the upgrade:
326
+
327
+ ```text
328
+ For every Connecta operator task, start at node_modules/@zackbart/connecta/documentation/README.md#operator-tasks
329
+ ```
330
+
331
+ ## Verify
332
+
333
+ 1. Start the deployment; construction errors name each removed option's
334
+ replacement.
335
+ 2. Sign in as a maintainer and confirm plain `/mcp` and the operator UI work.
336
+ 3. Run `connecta doctor --pool <pool>` with each agent's token, and confirm
337
+ the same token at `/mcp` is refused.
338
+ 4. Confirm a pool member sees exactly that pool's tools at `/mcp/<pool>`.
339
+ 5. For Workspace, call a read in an agent's personal pool and confirm it acts
340
+ as the pool's owner. In the base pool, confirm Workspace connectors are
341
+ absent: search finds none of their tools and a call fails
342
+ `unknown_address`. `auth_required` is what an agent gets only in a pool
343
+ without an owner that does list a Workspace connector.
344
+ 6. Confirm each `<agent>-<user>` pool lists no other person in `members`.
@@ -15,14 +15,17 @@ instructions and `skills` usage guide for deployment-specific context.
15
15
  | ------------------------------------------------------------------ | --------------------------------------------------------- |
16
16
  | One known read, with usable arguments and a small result | `call_tool` |
17
17
  | Discover a tool, chain calls, combine services, or reduce a result | `execute_code` |
18
- | One write on a read-only endpoint | `call_destructive_tool` |
18
+ | One known write on a writable endpoint | `call_destructive_tool` |
19
19
  | Obtain connector instructions | `skills` with the returned guide name |
20
20
  | Start a required authentication handoff | `authorize_connector`, using the returned recovery action |
21
21
 
22
- The endpoint's trust is part of its tool descriptions. On a `trusted` endpoint,
23
- programs may write within the caller's grants, and `execute_code` is annotated
24
- as a write. On `read-only`, every write is a separate top-level call. The host
25
- controls approval. Never change pool, identity, or trust to work around a refusal.
22
+ Whether the endpoint is read-only or writable is part of its tool
23
+ descriptions. On a writable endpoint, programs and `call_destructive_tool` may
24
+ write within the caller's grants, and `execute_code` is annotated as a write;
25
+ the client controls approval. On a read-only endpoint, search and describe omit
26
+ write tools and every write call is refused with `read_only_endpoint`, even one
27
+ a connector guide names; tell the user rather than looking for another route. Never change pool, identity, or `readOnly` to work
28
+ around a refusal.
26
29
  [Routing between the call surfaces](./meta-tools.md#routing-between-the-call-surfaces)
27
30
  owns the full decision rules.
28
31
 
@@ -35,9 +38,9 @@ failed catalog or a service outside the caller's grants.
35
38
 
36
39
  For normal reads, keep discovery, calls, and reduction in one program when the
37
40
  schemas suffice; return the answer rather than catalog matches. Use top-level
38
- `search_tools` for catalog inspection or to discover a write on a read-only
39
- endpoint, then dispatch each write with `call_destructive_tool`. An unfamiliar
40
- read result may return a bounded sample for inspection before the next program.
41
+ `search_tools` for catalog inspection or to find one known write on a writable
42
+ endpoint, then dispatch it with `call_destructive_tool`. An unfamiliar read
43
+ result may return a bounded sample for inspection before the next program.
41
44
 
42
45
  Use the exact returned `address`, including punctuation. A TypeScript signature
43
46
  is guidance for writing JavaScript; do not paste types into executable code.
@@ -123,7 +126,9 @@ mean the provider applied it; inspect state or ask for the missing decision
123
126
  before issuing it again. [Retry semantics](./code-mode.md#retry-semantics) and
124
127
  [writes](./code-mode.md#writes) describe program outcomes and write accounting.
125
128
 
126
- ## Implementation and evidence
129
+ ## Contributor reference
130
+
131
+ ### Implementation and evidence
127
132
 
128
133
  The endpoint's built-in [usage skill](https://github.com/zackbart/connecta/blob/main/src/skills.ts)
129
134
  is maintained alongside [meta-tools](https://github.com/zackbart/connecta/blob/main/src/meta-tools.ts)
@@ -1,16 +1,33 @@
1
1
  # Operator UI tests
2
2
 
3
+ ## Contributor reference
4
+
5
+ This guide describes Connecta implementation and repository verification.
6
+ It is optional for deployment operators. Start with the
7
+ [operator task index](./README.md#operator-tasks) for package-local procedures.
8
+
3
9
  Use this repository-maintenance guide when changing operator pages, data routes,
4
10
  or browser evidence. Read the [operator UI contract](./operator-ui.md) first.
5
11
  [The agent index](./README.md) routes deployment and endpoint tasks.
6
12
 
7
- `test/fixtures/operator-visual.ts` owns the visual data. It builds the config,
8
- live overlay, viewer, and legacy auth details through the current server routes,
9
- using the types in `src/operator-ui/contract.ts` and `model.ts`. Collections use
10
- their corresponding browser types. `test/operator-visual-fixtures.test.ts`
11
- validates contract versions, connector IDs, granted tools, classification,
12
- permissions, provenance, and secret exclusion on Node and Workers. A fixture's
13
- config cannot silently become a stale copy of the server serializer.
13
+ `test/fixtures/operator-visual.ts` owns the visual data. It builds the
14
+ maintainer's contract v2, live overlay, and legacy auth details through the
15
+ current server routes, using the types in `src/operator-ui/contract.ts` and
16
+ `model.ts`; its people carry labels from a fake directory. Its Tokens agents
17
+ (`claude-desktop` issued, `triage` none, `nightly` revoked) are real slots
18
+ committed through the deployment's own store, so the contract's `agents` and
19
+ `GET /ui/access-tokens` agree, revisions included; only their timestamps and
20
+ last seen are fixed. The restricted state is the member fixture from
21
+ `test/fixtures/operator-contract-v2.ts`, and the endpoint and matrix variants
22
+ use its maintainer fixture, which covers every runs-as kind and agent pools,
23
+ with `test/fixtures/operator-console.ts` crowding the team pool so its chips
24
+ collapse to "+3 more".
25
+ Collections use their corresponding browser types.
26
+ `test/operator-visual-fixtures.test.ts` validates contract versions, connector
27
+ IDs, granted tools, matrix cells, classification, provenance, and secret
28
+ exclusion on Node and Workers, and runs `expectMemberViewWithholds` on the
29
+ member body. A fixture's config cannot silently become a stale copy of the
30
+ server serializer.
14
31
 
15
32
  `test/browser/operator-visual.spec.ts` intercepts API reads and serves the real
16
33
  hashed assets and HTML with the production CSP. Loading responses wait on an
@@ -22,30 +39,78 @@ its state and fonts before capturing.
22
39
  ## Page and state matrix
23
40
 
24
41
  Each row runs empty, loading, error, populated, and restricted-viewer fixtures
25
- in both light and dark themes, for 120 screenshots.
26
-
27
- | Page or tab | Empty state | Loading/error request | Restricted viewer |
28
- | --------------------- | ------------------------------------ | --------------------- | --------------------------------------------------- |
29
- | Overview | No connectors or attention items | Config | Scoped connectors, no privileged navigation |
30
- | Connectors | No connectors | Config | Read grant and credential slot, no other connectors |
31
- | Connector Config | Zero-tool connector | Config | Scoped connector snapshot |
32
- | Connector Tools | Empty catalog | Config | Read tool only |
33
- | Connector Auth | Deployment-managed auth | Config | Visible slot, deployment-managed auth |
34
- | Connector Activity | No calls | Activity | Unavailable |
35
- | Connector Diagnostics | No observed drift/call | Config | Scoped diagnostics |
36
- | Tools | Empty catalog | Config | Read tool only |
37
- | Access | No pools or client tokens | Config | No token management |
38
- | Access tokens | No client tokens | Token list | No token management |
39
- | Activity | No calls | Activity | Unavailable |
40
- | Config | Snapshot without connectors or pools | Config | Scoped snapshot |
41
-
42
- Another 36 screenshots cover configured, mismatched, unreadable and multifield
42
+ in both light and dark themes, for 80 screenshots. The restricted viewer is a
43
+ pool member, who gets the member page, with no tabs, on every path.
44
+
45
+ | Page or tab | Empty state | Loading/error request | Restricted viewer (member) |
46
+ | -------------------- | ------------------------------------ | --------------------- | ----------------------------------- |
47
+ | Endpoints | Root endpoint without tools | Config | Member page: endpoints, accounts |
48
+ | Connectors | No connectors | Config | Member page, no connector request |
49
+ | Connector Tools | Empty catalog | Config | Member page, no connector request |
50
+ | Connector Connection | Deployment-managed auth | Config | Member page, no connector request |
51
+ | Connector Activity | No calls | Activity | Member page, no activity request |
52
+ | Tokens | No declared agents | Token list | Member page, no token request |
53
+ | Activity | No calls | Activity | Member page, no activity request |
54
+ | Settings | Snapshot without connectors or pools | Config | Member page, no configuration facts |
55
+
56
+ Populated Endpoints shows who can use each endpoint; populated Tokens has one
57
+ agent per slot state, with pools and last seen.
58
+
59
+ Another 54 screenshots cover configured, mismatched, unreadable and multifield
43
60
  credentials; all three OAuth client mechanisms; clean and unobserved drift;
44
61
  historical Activity; client setup; schema dialog; read/write filters; expanded
45
- config provenance; narrow sign-in and Auth; and deployment branding. The base
62
+ config provenance; narrow sign-in and Connection; deployment branding; `/mcp`
63
+ and an agent pool expanded with every runs-as kind; command search; narrow
64
+ endpoints; the connector × endpoint matrix, one cell's open popover, and the
65
+ narrow matrix; a new token's one-time secret dialog; and the member page, wide
66
+ and narrow. `endpoints-expanded` and `narrow-endpoints` include the team row
67
+ collapsed to "+3 more". The base
46
68
  populated Activity fixture includes grouped calls, an ungrouped legacy row,
47
69
  classification/result size/client facts, and a catalog-change event.
48
70
 
71
+ `test/browser/operator-matrix.spec.ts` serves the frozen v2 fixtures to the
72
+ real shell. It covers the matrix's table semantics, cell names and runs-as
73
+ labels, popover keyboard behavior, placement at a short viewport, the in-flow
74
+ fallback without CSS anchors, and the absence of `style` attributes, agent URLs on Endpoints, the member page (its
75
+ received body checked with `expectMemberViewWithholds`, `/ui/data` answering 404
76
+ and never requested, no maintainer fact rendered, no maintainer request sent),
77
+ the 401 sign-in and 404 no-access gates on `/ui/api/config` with no maintainer
78
+ navigation, the shared recovery banner on Tokens and Activity through a failed
79
+ refresh that stalls past its deadline (one focusable Retry, no maintainer
80
+ request until Retry succeeds), an open client setup drawer closed by a failed
81
+ refresh with focus on Retry, Tokens under recovery (an open rotate question
82
+ closes with focus on Retry and nothing sent; an unseen secret dialog stays open
83
+ over the banner until dismissed), payload sentinels, CSP, and the refusal of a v1 body. `test/operator-endpoints.test.ts` covers the view model.
84
+ `test/browser/operator-console.spec.ts` covers UI-3: who-can-use chips by
85
+ label, raw id and agent mark and "+N more" from the keyboard; Tokens rows,
86
+ issue with the once-only secret dialog, in-row rotate and revoke questions,
87
+ 503, 499 and 409 failures that send exactly one request, slots missing from
88
+ the list or left unresolved shown with disabled controls until Refresh; and the
89
+ member page (its received body checked with `expectMemberViewWithholds`, no
90
+ tabs, Search or maintainer fact, OAuth Connect, Reconnect and Disconnect sent as
91
+ `?mode=continue`, `?mode=restart` and `DELETE`, the credential `actions`
92
+ shapes, a refresh that drops an action closing its confirmation, a failed or
93
+ stalled refresh keeping member chrome with Retry, a consent that outlasts the
94
+ OAuth poll window), plus keyboard, sentinels, CSP and no `style` attributes.
95
+ `test/operator-console.test.ts` covers the chip, slot-row and member-page view
96
+ models. `test/browser/operator-scheme.spec.ts` holds the main bundle and checks the
97
+ first painted background: saved dark on a light system paints dark, a pinned
98
+ light deployment paints light over saved dark, and throwing storage keeps the
99
+ system scheme while the released bundle settles on the normal bearer gate, and a
100
+ sign-in there raises no error. The real-route token test in `test/browser/operator-ui.spec.ts` serves
101
+ the contract, `/ui/data` and the slot routes from one real v2 deployment and
102
+ checks that each mutation sent a distinct 22-character `expect`.
103
+ `test/operator-store.test.ts` serves `/ui/api/config` from its own mock, so
104
+ each test's data and detail answers stay separate, and covers role changes: a
105
+ maintainer demoted after a failed refresh keeps no maintainer state and sends
106
+ no maintainer request, and a promoted member loads maintainer data fresh. It
107
+ also drives UI-3's slot fence with a session-token read held open: reloads and
108
+ contract refreshes that move or drop a slot before dispatch, late results that
109
+ cannot overwrite newer evidence, unknown and unreadable outcomes that only a
110
+ later list read settles, revisions sent as `expect` and compared with the
111
+ contract, an identity change that leaves the next identity's fence alone, and
112
+ member actions whose `manage` a refresh changes.
113
+
49
114
  Screenshots cover presentation. Keep unit and server tests for INV-6 sentinels,
50
115
  secret disclosure, auth and grant boundaries, CSRF, identity fences, cancellation,
51
116
  request lifetime, and error sanitization. Keep browser tests for keyboard/focus,
@@ -63,8 +128,8 @@ non-Linux hosts. Do not commit Darwin baselines.
63
128
  When Docker is unavailable, use the **Operator snapshots** workflow:
64
129
 
65
130
  ```sh
66
- git push --force-with-lease origin feat/p4-ui-tests
67
- gh workflow run operator-snapshots.yml --ref feat/p4-ui-tests
131
+ git push --force-with-lease origin ui/834-shell
132
+ gh workflow run operator-snapshots.yml --ref ui/834-shell
68
133
  # Read the completed run ID from the Actions page or gh run list.
69
134
  gh run download RUN_ID --name operator-linux-snapshots \
70
135
  --dir test/browser/operator-visual.spec.ts-snapshots
@@ -72,9 +137,8 @@ gh run download RUN_ID --name operator-linux-snapshots \
72
137
 
73
138
  The workflow uploads all generated baselines and never commits them. Review the
74
139
  images and changed files, commit the intentional changes, and push again. The
75
- normal CI browser job must compare and pass against that commit. Opening the
76
- initial PR also runs the update workflow, so the first baselines can be generated
77
- before the workflow reaches main. Later updates use `workflow_dispatch`.
140
+ normal CI browser job must compare and pass against that commit. Snapshot
141
+ updates run only through `workflow_dispatch`.
78
142
 
79
143
  On a matching Linux runner, `npm ci`, install Chromium with the command above,
80
144
  then run `npm run test:browser:snapshots`. The normal command is
@@ -95,3 +159,25 @@ The flows cover Clerk sign-in/reload/sign-out; OAuth connect/consent/callback/
95
159
  connected reread/reload; and credential save/test/replace/remove. They collect
96
160
  CSP violations across every document and popup, and fail on unexpected network
97
161
  traffic. No credentials enter visual fixtures or screenshot baselines.
162
+
163
+ `test/browser/operator-auth-recovery.spec.ts` serves the production UI and real
164
+ operator routes with a trusted Worker Access context, serving the server's own
165
+ contract v2. Each admission reads the contract first,
166
+ so its read counts include one contract read per admission. It covers transient and
167
+ rejected identity reads, missing shell context, delayed OAuth grant commits,
168
+ catalog/auth reconciliation, and unreadable credential metadata, all without
169
+ reloading the document. The repeated-refusal browser case drives ten focus and visibility returns
170
+ 160 ms apart with a controlled clock, checks the shared backoff and exhausted
171
+ retry budget, then recovers without reloading. Store tests cover the same
172
+ state transitions for 401, 403, and 503 refusals and debounced explicit Retry.
173
+ Delayed config/detail cases in both runtimes and Chromium keep returning focus
174
+ every 2.1 seconds, require eventual catalog/auth publication, and assert one
175
+ recovery cycle. Store cases also preserve explicit Retry and sign-out while
176
+ that cycle is pending. The Retry focus browser case uses a controlled clock to
177
+ check click coalescing inside 150 ms and heading focus after a later Retry.
178
+ Store tests defend bounded recovery and OAuth polling,
179
+ atomic refresh publication, identity cancellation, and late-response fencing.
180
+ Multi-connector browser cases assert scoped polling and cancellation after
181
+ navigation, successful authorization, and failed status. The Clerk browser
182
+ fixture holds sign-out pending to check that queued focus and later passive
183
+ returns cannot restore operator data.