@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.
- package/CHANGELOG.md +63 -0
- package/README.md +15 -7
- package/bin/connecta.mjs +24 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/config-value-sources.js +11 -9
- package/dist/config.d.ts +37 -75
- package/dist/config.js +110 -36
- package/dist/connector-access.d.ts +4 -34
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +29 -7
- package/dist/describe-config.js +7 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +29 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -4
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +149 -28
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +14 -6
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +82 -21
- package/dist/page-styles.js +1 -1
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +30 -8
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +3 -3
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +3 -3
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +3 -3
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +10 -8
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +100 -11
- package/dist/providers/notion/rest.js +45 -5
- package/dist/providers/notion/skill.generated.js +2 -2
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +6 -3
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +145 -95
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +24 -23
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +5 -0
- package/dist/storage/keys.js +9 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +16 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +355 -50
- package/documentation/auth.md +775 -243
- package/documentation/code-mode.md +14 -11
- package/documentation/deploying.md +121 -24
- package/documentation/integrating.md +92 -21
- package/documentation/meta-tools.md +14 -9
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +115 -28
- package/documentation/operator-ui.md +326 -61
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +30 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +36 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +3 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +27 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +67 -42
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +28 -12
- package/templates/node/src/rotate-token.ts +17 -0
- 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`.
|
|
@@ -126,7 +126,9 @@ mean the provider applied it; inspect state or ask for the missing decision
|
|
|
126
126
|
before issuing it again. [Retry semantics](./code-mode.md#retry-semantics) and
|
|
127
127
|
[writes](./code-mode.md#writes) describe program outcomes and write accounting.
|
|
128
128
|
|
|
129
|
-
##
|
|
129
|
+
## Contributor reference
|
|
130
|
+
|
|
131
|
+
### Implementation and evidence
|
|
130
132
|
|
|
131
133
|
The endpoint's built-in [usage skill](https://github.com/zackbart/connecta/blob/main/src/skills.ts)
|
|
132
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
|
|
8
|
-
live overlay,
|
|
9
|
-
using the types in `src/operator-ui/contract.ts` and
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
| Connector Tools
|
|
33
|
-
| Connector
|
|
34
|
-
| Connector Activity
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Another
|
|
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
|
|
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
|
|
67
|
-
gh workflow run operator-snapshots.yml --ref
|
|
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
|
|
@@ -94,3 +159,25 @@ The flows cover Clerk sign-in/reload/sign-out; OAuth connect/consent/callback/
|
|
|
94
159
|
connected reread/reload; and credential save/test/replace/remove. They collect
|
|
95
160
|
CSP violations across every document and popup, and fail on unexpected network
|
|
96
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.
|