@zackbart/connecta 0.33.0 → 0.34.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 (86) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +99 -128
  3. package/SECURITY.md +1 -1
  4. package/bin/version.mjs +1 -1
  5. package/dist/activity.d.ts +3 -0
  6. package/dist/branding.d.ts +7 -0
  7. package/dist/branding.js +76 -9
  8. package/dist/catalog-service.js +15 -1
  9. package/dist/config-value-sources.js +6 -1
  10. package/dist/config.d.ts +13 -2
  11. package/dist/config.js +36 -2
  12. package/dist/connector-caller.js +2 -1
  13. package/dist/describe-config.d.ts +3 -1
  14. package/dist/describe-config.js +1 -0
  15. package/dist/execute.js +1 -11
  16. package/dist/fix-prompt.js +2 -2
  17. package/dist/index.js +1 -0
  18. package/dist/membership.d.ts +2 -0
  19. package/dist/oauth-callback-outcome.js +2 -2
  20. package/dist/operator-ui/agents.d.ts +1 -1
  21. package/dist/operator-ui/agents.js +16 -16
  22. package/dist/operator-ui/config-summary.js +562 -0
  23. package/dist/operator-ui/connection-map.js +375 -0
  24. package/dist/operator-ui/endpoints.js +44 -15
  25. package/dist/operator-ui/fix-prompts.js +1 -1
  26. package/dist/operator-ui/generated.js +4 -4
  27. package/dist/operator-ui/member-page.js +13 -28
  28. package/dist/operator-ui/overview.js +134 -0
  29. package/dist/operator-ui/view.d.ts +27 -22
  30. package/dist/operator-ui/view.js +104 -88
  31. package/dist/page-styles.js +5 -3
  32. package/dist/providers/_shared/google/workspace.d.ts +6 -4
  33. package/dist/providers/_shared/google/workspace.js +6 -2
  34. package/dist/providers/ccb/index.js +1 -1
  35. package/dist/providers/docs/index.js +1 -1
  36. package/dist/providers/docs/skill.generated.js +2 -2
  37. package/dist/providers/drive/index.js +1 -1
  38. package/dist/providers/drive/skill.generated.js +2 -2
  39. package/dist/providers/forms/index.js +1 -1
  40. package/dist/providers/forms/skill.generated.js +2 -2
  41. package/dist/providers/gmail/index.js +1 -1
  42. package/dist/providers/gmail/skill.generated.js +2 -2
  43. package/dist/providers/mixpanel/skill.generated.js +3 -3
  44. package/dist/providers/notion/skill.generated.js +1 -1
  45. package/dist/providers/overflow/index.js +1 -1
  46. package/dist/providers/overflow/skill.generated.js +2 -2
  47. package/dist/providers/planning-center/index.js +1 -1
  48. package/dist/providers/planning-center/skill.generated.js +4 -4
  49. package/dist/providers/revenuecat/skill.generated.js +2 -2
  50. package/dist/providers/sheets/index.js +1 -1
  51. package/dist/providers/sheets/skill.generated.js +2 -2
  52. package/dist/providers/slides/index.js +1 -1
  53. package/dist/providers/slides/skill.generated.js +2 -2
  54. package/dist/providers/tithely/index.js +1 -1
  55. package/dist/providers/vercel/index.js +1 -1
  56. package/dist/providers/vercel/skill.generated.js +1 -1
  57. package/dist/registry.js +15 -0
  58. package/dist/routes/mcp.js +20 -13
  59. package/dist/routes/oauth.js +2 -2
  60. package/dist/skills.js +10 -0
  61. package/dist/storage/sql.js +5 -1
  62. package/dist/types.d.ts +18 -7
  63. package/dist/ui.js +9 -4
  64. package/dist/usage-guide.js +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/documentation/README.md +2 -2
  68. package/documentation/architecture.md +18 -13
  69. package/documentation/auth.md +183 -67
  70. package/documentation/code-mode.md +23 -5
  71. package/documentation/deploying.md +5 -1
  72. package/documentation/integrating.md +9 -6
  73. package/documentation/meta-tools.md +36 -22
  74. package/documentation/migration-0.33.md +21 -13
  75. package/documentation/migration-0.34.md +67 -0
  76. package/documentation/operator-tests.md +46 -20
  77. package/documentation/operator-ui.md +294 -104
  78. package/examples/worker/AGENTS.md +15 -9
  79. package/examples/worker/README.md +41 -14
  80. package/examples/worker/src/connecta.config.ts +5 -4
  81. package/examples/worker/wrangler.jsonc +1 -1
  82. package/package.json +19 -19
  83. package/templates/node/AGENTS.md +13 -7
  84. package/templates/node/README.md +14 -11
  85. package/templates/node/package.json +1 -1
  86. package/templates/node/src/connecta.config.ts +2 -2
@@ -66,7 +66,8 @@ comma-separated list of these ids; update that environment variable when
66
66
  using their roster wiring. Pool membership remains code. To remove a person,
67
67
  remove their memberships and maintainer entry, restart/redeploy, and remove
68
68
  their sign-in admission as appropriate. Never add another person to an
69
- agent's `<agent>-<user>` pool; follow [the pool layout](#organizing-an-agents-pools).
69
+ agent's `<agent>-<user>` pool, and add a person to an agent's tier only if
70
+ they are cleared for it; follow [the pool layout](#organizing-an-agents-pools).
70
71
 
71
72
  ## Connect a shared credential
72
73
 
@@ -108,8 +109,8 @@ remoteMcp("crm", {
108
109
 
109
110
  To rotate a saved token, save its replacement in the same slot; subsequent
110
111
  calls use it. Revoke the retired token at the downstream service. For OAuth,
111
- use Restart to authorize afresh, Continue to resume a pending consent, and
112
- Disconnect to invalidate the local grant and catalog. To remove a credential,
112
+ use Reconnect (or Connect account) to authorize afresh, Continue requested
113
+ authorization to resume a pending consent link, and Disconnect to invalidate the local grant and catalog. To remove a credential,
113
114
  use the connector's credential delete control. Clearing a slot with no saved
114
115
  value returns 409 with `problem: "credential_required"`; it does not revoke
115
116
  anything downstream. An unreadable stored value can still be cleared for
@@ -132,7 +133,7 @@ pools: { assistant: { tools: ["crm", "tickets"], members: ["agent:assistant"] }
132
133
  Typecheck and restart/redeploy, then use either procedure:
133
134
 
134
135
  - **Signed-in maintainer:** with human auth and `operatorUi()` enabled, open
135
- the Access tokens page, select `assistant`, and issue, rotate, or revoke
136
+ the Tokens page, select `assistant`, and issue, rotate, or revoke
136
137
  its token. Save an issued secret privately immediately; it appears once.
137
138
  The UI reads the slot revision and sends it with the operation. If the slot
138
139
  changed, reload and decide again.
@@ -233,7 +234,7 @@ lists them: each name matches `^[a-z0-9_-]+$`, at most 64 characters, and is
233
234
  unique; anything else, or agents without `accessTokens`, throws at
234
235
  construction. `describeConfig()` lists the names and never token state.
235
236
  `accessTokens` from `@zackbart/connecta/auth/access-tokens` installs the `cta_`
236
- inbound adapter and, beside `ui`, the Access tokens page and `/ui/access-tokens`
237
+ inbound adapter and, beside `ui`, the Tokens page and `/ui/access-tokens`
237
238
  routes. Omitted, none of it loads or is served. It takes no options;
238
239
  `accessTokens(storage, { maxActive })` throws, since each declared agent has
239
240
  exactly one token slot.
@@ -281,7 +282,8 @@ The token routes require the revision the caller decided from:
281
282
  | `POST /ui/access-tokens/<agent>` (issue, rotate) | JSON body `{ "expect": "<revision>" }`, `Content-Type: application/json` |
282
283
  | `DELETE /ui/access-tokens/<agent>` (revoke) | the same body, or `?expect=<revision>` for a client that cannot send a body with DELETE; not both |
283
284
 
284
- A missing or malformed `expect` is 400; a body that is not JSON is 415 and one
285
+ A missing or malformed `expect` is 400, as is a JSON body that does not parse
286
+ to an object; a body without `Content-Type: application/json` is 415, and one
285
287
  over 1,024 characters is 413. They are checked after authorization and before
286
288
  the operation. If the slot's current revision differs, the route answers 409
287
289
  `{ "error": "…", "code": "slot_changed" }` before minting a secret or writing
@@ -387,8 +389,9 @@ outcome depends on cleanup or on undoing a write.
387
389
  undelivered rotation secret may keep admitting, but it never left the server:
388
390
  a liveness problem reported as `outcome_unknown`, never a credential exposure.
389
391
 
390
- The routes pass the request's `AbortSignal` to `rotate`, `revoke`, and `adopt`
391
- (`{ signal }`); a signal already aborted performs no storage call at all. Authorization caches no token, and D1 and SQLite are strongly
392
+ The routes pass the request's `AbortSignal` to `rotate` and `revoke`
393
+ (`{ signal, expect }`); `adopt` has no route and runs only from trusted
394
+ provisioning, which may pass its own `{ signal }`; a signal already aborted performs no storage call at all. Authorization caches no token, and D1 and SQLite are strongly
392
395
  consistent; admitted requests are not recalled. Management writes require an
393
396
  exact same-origin Origin; responses are private and non-cacheable.
394
397
 
@@ -463,8 +466,11 @@ one fixed address, is applied once per call and only to a request an inbound
463
466
  provider authenticated: any `ok`, a subject-less custom credential included, never an open
464
467
  deployment's anonymous admission. An agent never reaches the function; it acts
465
468
  as its pool's `owner` ([Pool owners](#pool-owners-for-google-workspace)).
466
- Otherwise, or on `undefined`, the call fails
467
- `auth_required` before any request leaves. Arguments, headers, and programs
469
+ Without an owner, a mapping-subject connector is absent for agents, and a call
470
+ by address fails `unknown_address`. The provider-level fallback is
471
+ `auth_required` when no inbound provider authenticated the caller or the
472
+ mapping returns `undefined`; it also rejects an ownerless agent context that
473
+ bypasses the scoped registry. These refusals happen before any request leaves. Arguments, headers, and programs
468
474
  never reach it; a deployment's own connectors still cannot read the caller.
469
475
 
470
476
  Access is read from one place, deployment code. Top-level
@@ -496,10 +502,11 @@ provider other than the built-in agent-token adapter), and a non-empty
496
502
  can be a maintainer, and plain `/mcp` and every operator API admit nobody; the
497
503
  agents reach only `/agent/<pool>` for the pools that list them. With no inbound auth at all, the
498
504
  deployment is open (see [pools](#pools)). `describeConfig()` lists
499
- `maintainers` as `{ namespace, id }` and each pool's resolved `members`.
505
+ `maintainers` as `{ namespace, id }` and each pool's resolved `members` and
506
+ trimmed `description` (`null` when unset).
500
507
 
501
508
  Pool tools are enforced in the scoped registry view, below the catalog service,
502
- so `search_tools`, `describe_tools`, both call tools, a program's
509
+ so `search_tools`, both call tools, a program's
503
510
  `connecta.search`, `connecta.describe`, and `connecta.call` read one filtered
504
511
  list. Connector-level discovery, guides, and `authorize_connector` keep a
505
512
  connector when any of its tools is in the pool — `docs.read` permits the
@@ -540,6 +547,10 @@ Marketing's every `campaigns` tool and only `issues.search`. Neither pool
540
547
  serves `billing`; only the owner's plain `/mcp` does. A person in both pools
541
548
  uses two endpoints, never a union.
542
549
 
550
+ A team pool can also be one of an agent's tiers: the same pool lists the
551
+ cleared people and the agent, and each reaches it at their own address
552
+ ([people in a tier](#organizing-an-agents-pools)).
553
+
543
554
  | Catalog change | Exact `connector.tool` in a pool |
544
555
  | ----------------------- | ----------------------------------------------------------------------- |
545
556
  | New name | Excluded until reviewed and added |
@@ -572,7 +583,7 @@ secrets. Declare each machine as an agent instead:
572
583
 
573
584
  1. Configure the existing D1 or SQLite store as `storage`, and install
574
585
  `accessTokens(storage)`. Keep its existing keys and namespaces.
575
- 2. Add the machine's name to `agents`. Issue its token from the Access tokens
586
+ 2. Add the machine's name to `agents`. Issue its token from the Tokens
576
587
  page as a signed-in maintainer, or from trusted
577
588
  deployment code with
578
589
  `new AccessTokenManager(storage, { agents }).rotate("machine-name", "deployment-provisioning")`.
@@ -609,7 +620,11 @@ createConnecta({
609
620
  maintainers: ["user_owner"],
610
621
  agents: ["calendar-bot"],
611
622
  pools: {
612
- support: { tools: ["linear", "notion.search_pages", "notion.fetch_page"], members: supportTeam },
623
+ support: {
624
+ tools: ["linear", "notion.search_pages", "notion.fetch_page"],
625
+ members: supportTeam,
626
+ description: "Support tools: Linear and three Notion reads. Ask a maintainer for anything else.",
627
+ },
613
628
  calendar_bot: { tools: ["calendar.create_event"], members: ["agent:calendar-bot"] },
614
629
  reporting: { tools: ["linear", "notion"], readOnly: true, members: analysts },
615
630
  },
@@ -623,14 +638,20 @@ warning says, every pool path answers the 404 because nobody can be a member,
623
638
  and every agent endpoint answers the machine-only 401 because no agent can be
624
639
  admitted.
625
640
 
626
- | Rule | Contract |
627
- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
628
- | Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers. Neither intersects with or widens anything. |
629
- | Refusal | A non-maintainer at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer`, `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
630
- | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
631
- | Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
632
- | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
633
- | Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
641
+ | Rule | Contract |
642
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
643
+ | Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers. Neither intersects with or widens anything. |
644
+ | Refusal | A non-maintainer at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer`, `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
645
+ | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
646
+ | Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` append it to the MCP `initialize` instructions after `This endpoint: `; plain `/mcp` and a pool without one serve the shared instructions unchanged. It grants, hides, and refuses nothing, and changes no tool or tool description. |
647
+ | Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 characters of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
648
+ | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
649
+ | Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
650
+
651
+ Pool descriptions are trimmed before validation. The remaining text must be
652
+ 1 to 500 characters and contain no C0 or C1 control characters (including tabs,
653
+ newlines, and NEL U+0085), LINE SEPARATOR (U+2028), or PARAGRAPH SEPARATOR
654
+ (U+2029).
634
655
 
635
656
  ### Agent endpoints
636
657
 
@@ -715,7 +736,16 @@ mapping function, the caller decides whose account a call opens:
715
736
  | ----------------------------- | ------------------------------------------------------------------------------- |
716
737
  | A person, in any pool | Their own account, through the `subject` mapping. A pool `owner` never applies. |
717
738
  | An agent in a pool | The pool's `owner`, resolved before the mapping, which never sees the agent. |
718
- | An agent, pool has no `owner` | Nothing: the call fails `auth_required` before any request leaves. |
739
+ | An agent, pool has no `owner` | Nothing. The connector is absent for the agent, as a personal connector is. |
740
+
741
+ In a pool without an `owner`, a mapping-subject Workspace connector is absent
742
+ for agents, just as an `authScope: "personal"` connector is: `search_tools`,
743
+ `connecta.search`, `connecta.describe`, `skills`, and connector discovery omit
744
+ it, `authorize_connector` answers "Unknown connector", and a call by address
745
+ fails `unknown_address`, the same as for a connector that does not exist.
746
+ People in the same pool still see it and act as themselves. This is what lets
747
+ one pool be a team pool for people, each acting as themselves on Workspace, and
748
+ also list an agent that reaches only the pool's shared connectors.
719
749
 
720
750
  `owner` is a Workspace email address, checked at construction and listed in
721
751
  `describeConfig()` (`null` when unset). Over HTTP, `/ui/api/config` serves it
@@ -728,19 +758,19 @@ change it, and the `auth_required` message never names an address. One
728
758
 
729
759
  ```ts
730
760
  const team = [
731
- { id: "alice", email: "alice@example.com" },
732
- { id: "bob", email: "bob@example.com" },
761
+ { handle: "alice", id: "user_alice", email: "alice@example.com" },
762
+ { handle: "bob", id: "user_bob", email: "bob@example.com" },
733
763
  ];
734
764
  createConnecta({
735
765
  auth: clerkAuth({ publishableKey, secretKey }),
736
766
  storage,
737
767
  accessTokens: accessTokens(storage),
738
768
  readOnly: false,
739
- maintainers: ["alice"],
769
+ maintainers: ["user_alice"],
740
770
  agents: ["assistant"],
741
771
  pools: Object.fromEntries(
742
772
  team.map((person) => [
743
- `assistant-${person.id}`,
773
+ `assistant-${person.handle}`,
744
774
  { tools: ["gmail", "drive"], owner: person.email, members: [person.id, "agent:assistant"] },
745
775
  ]),
746
776
  ),
@@ -767,9 +797,12 @@ members must not share that account.
767
797
 
768
798
  ### Organizing an agent's pools
769
799
 
770
- Use this layout for every agent's pools. It gives each person's own accounts
771
- to the agent only when that person is the one talking, without a second
772
- connection. This section holds the complete rules.
800
+ Use this layout for every agent's pools. It gives a chat no more than
801
+ everyone in it is cleared for, and gives each person's own accounts to the
802
+ agent only when that person is the one talking, all over one connection. This
803
+ section holds the complete rules.
804
+
805
+ **The pools**
773
806
 
774
807
  1. **One connection per agent.** Each agent has exactly one Connecta
775
808
  connection, and its client (the harness, never the model) chooses the
@@ -777,24 +810,32 @@ connection. This section holds the complete rules.
777
810
  it: the model would get two sets of meta-tools, and `execute_code` cannot
778
811
  join results across connections. The agent's one `cta_` token works on
779
812
  every pool that lists it.
780
- 2. **Base pool `<agent>`.** `/agent/<agent>` serves pool `<agent>`: shared and
781
- organization connectors only, with no `owner`. Use it in group chats,
782
- scheduled runs, and any turn without exactly one identified person.
813
+ 2. **Tiers.** An agent's tiers are named pools ordered from least to most
814
+ capable. Recommended names are `<agent>` for the lowest tier and
815
+ `<agent>-<tier>` for higher tiers (`assistant-team`, `assistant-leads`).
816
+ A tier may be any named pool, including an existing team pool such as
817
+ `team` or `leadership`, as long as its human members are all cleared for
818
+ it (rule 10). An agent with one tier uses it as the lowest. Tiers never
819
+ hold per-user connectors and never set an `owner`.
783
820
  3. **Personal pool `<agent>-<user>`.** `/agent/<agent>-<user>` serves pool
784
- `<agent>-<user>`: the base plus that user's own connectors. Use it only in
785
- a one-on-one with that user. `<user>` is a short, stable handle you choose
786
- (`alice`), never a provider id, principal id, or address; pool names match
787
- `[a-z0-9_-]+`.
788
- 4. **Define the base once.** Every personal pool spreads one base constant, so
789
- a change to the base reaches every pool.
821
+ `<agent>-<user>`: the highest tier that user is cleared for, plus that
822
+ user's own connectors. Use it only in a one-on-one with that user.
823
+ `<user>` is a short, stable handle you choose (`alice`), never a provider
824
+ id, principal id, or address. When using the recommended `<agent>-<tier>`
825
+ names, no user handle may equal a tier suffix: their pool names would
826
+ collide. With any naming scheme, keep personal pool names distinct from
827
+ tier pool names. Pool names match `[a-z0-9_-]+`.
828
+ 4. **Nest by spreading tool lists.** Define each tier's `tools` once, as a
829
+ constant that spreads the tier below it, and spread a user's highest tier
830
+ into their personal pool's `tools`. A change to a tier then reaches every
831
+ pool above it. Spread tool lists, not whole pools: `members`, `readOnly`,
832
+ and `owner` belong to each pool, and a spread pool would copy them.
790
833
  5. **`owner` is the user's Workspace address.** Set `owner` on
791
834
  `<agent>-<user>` to that user's Google Workspace email address; it is
792
835
  checked as an address at construction, and it makes Workspace connectors
793
836
  act as that user for the agent ([pool owners](#pool-owners-for-google-workspace)).
794
- The base pool has no owner, so keep Workspace connectors out of it: there
795
- they are absent, as in any pool that does not list them. (A Workspace
796
- connector listed in a pool without an owner fails `auth_required` for an
797
- agent.)
837
+ Tiers have no owner, so a Workspace connector with a mapping `subject`
838
+ is absent for the agent there.
798
839
  6. **A user's own account is its own connector id.** Agents have no
799
840
  principal, so `authScope: "personal"` connectors never appear for them.
800
841
  Give each user's account its own connector id with shared auth, which a
@@ -802,20 +843,68 @@ connection. This section holds the complete rules.
802
843
  that user's pool. Google Workspace is the exception: one connector whose
803
844
  `subject` is a mapping function, with each personal pool's `owner`
804
845
  choosing the account.
805
- 7. **Changes stay in config.** Adding an account for a user adds its
806
- connector and lists it in that user's pool `tools`; the client does not
807
- change. Adding a user adds one `<agent>-<user>` pool.
808
- 8. **No other person in a personal pool.** The only human member an
846
+ 7. **Where each connector goes.** A shared connector (the deployment's
847
+ account, including a fixed-subject Workspace connector) goes in the lowest
848
+ tier whose entire audience is authorized to use the connector, and so
849
+ reaches every tier above. For example, billing restricted to leads goes
850
+ in the leads tier. Per-user connectors (`notes_alice`) go only in personal
851
+ pools, and so do the Workspace connectors with a mapping `subject` that
852
+ the agent uses; rule 10 covers listing them in a tier for its people.
853
+ 8. **Changes stay in config.** Adding an account for a user adds its
854
+ connector and lists it in that user's pool `tools`. Adding a user adds one
855
+ `<agent>-<user>` pool. Adding a tier adds one pool and one constant, and
856
+ the tier's place in the harness's tier list.
857
+ 9. **No other person in a personal pool.** The only human member an
809
858
  `<agent>-<user>` pool may have is that same user, and usually it has none.
810
859
  Never add another person: its per-user connectors (`notes_alice`) use that
811
860
  user's account for everyone the pool admits, so another member would read
812
861
  and write the user's account as their own. A person's own use normally
813
- goes through their own pools or plain `/mcp`, not the agent's.
862
+ goes through their own pools or plain `/mcp`.
863
+ 10. **People in a tier are cleared for it.** A tier can also be a human team
864
+ pool: list the people cleared for that tier in its `members` beside the
865
+ agent. They use it at `/mcp/<pool>`, the agent at `/agent/<pool>`.
866
+ List no one else: a member reaches
867
+ every shared connector the tier holds, on the deployment's account. Such a
868
+ pool may also list Workspace mapping connectors or `authScope: "personal"`
869
+ connectors for its people, who act as themselves on them; those connectors
870
+ are absent for the agent, since a tier has no owner and an agent has no
871
+ principal.
872
+
873
+ **Choosing the pool each turn**
874
+
875
+ The harness decides which pool a turn uses, from its own verified view of
876
+ the chat: the platform's sender and participant ids, never names or claims in
877
+ message text, and never the model. Connecta checks only that the agent is a
878
+ member of the pool it opens. The agent's one token opens every pool that
879
+ lists it, so clearance is enforced nowhere but the harness.
880
+
881
+ - In a one-on-one with a user who has a personal pool, use that pool.
882
+ - Otherwise, use the most capable tier that everyone present is cleared for.
883
+ Everyone present means every participant who can read the reply, people and
884
+ other bots, not only the sender.
885
+ - If anyone present is unknown to the harness, or it cannot verify who is
886
+ present, use the lowest tier.
887
+ - A scheduled run uses the tier of the audience that will read its output, by
888
+ the same rule; with no audience, the lowest tier.
889
+ - Choose again on every turn. Someone joining a chat lowers its tier from the
890
+ next turn.
891
+
892
+ **The lowest tier.** The first pool in the harness's tier list is the
893
+ fallback (recommended name: `<agent>`), so make it safe in front of anyone
894
+ who can reach the agent. Where unknown people can, make it a guest tier:
895
+ `readOnly: true`, with exact `connector.tool` addresses for the reads guests
896
+ may use. A new downstream tool then never appears there, and no write runs.
897
+ The tier above spreads the guest list and adds whole connectors.
814
898
 
815
899
  ```ts
816
900
  import type { GoogleWorkspaceOptions } from "@zackbart/connecta/providers/gmail";
817
901
 
818
- const assistant = { tools: ["crm", "tickets"], members: ["agent:assistant"] };
902
+ const agent = "agent:assistant";
903
+ // Tiers, lowest first. Each tier's tools spread the tier below.
904
+ const guestTools = ["crm.search_contacts", "tickets.list_tickets"];
905
+ const teamTools = [...guestTools, "crm", "tickets"];
906
+ const leadsTools = [...teamTools, "billing"];
907
+
819
908
  const emailOf = (id?: string) => (id === "user_alice" ? "alice@example.com" : undefined);
820
909
  const workspace: GoogleWorkspaceOptions = {
821
910
  purpose: "Mail and files of the person in a one-on-one",
@@ -831,21 +920,27 @@ createConnecta({
831
920
  maintainers: ["user_alice"],
832
921
  agents: ["assistant"],
833
922
  pools: {
834
- assistant,
923
+ // Lowest tier and fallback: reads guests may use.
924
+ assistant: { tools: guestTools, readOnly: true, members: [agent] },
925
+ // Also the team's own pool at /mcp/assistant-team.
926
+ "assistant-team": { tools: teamTools, members: [agent, "user_alice", "user_bob"] },
927
+ "assistant-leads": { tools: leadsTools, members: [agent] },
928
+ // Each personal pool spreads its user's highest tier.
835
929
  "assistant-alice": {
836
- ...assistant,
837
- tools: [...assistant.tools, "notes_alice", "gmail", "drive"],
930
+ tools: [...leadsTools, "notes_alice", "gmail", "drive"],
838
931
  owner: "alice@example.com",
932
+ members: [agent],
839
933
  },
840
934
  "assistant-bob": {
841
- ...assistant,
842
- tools: [...assistant.tools, "notes_bob", "gmail"],
935
+ tools: [...teamTools, "notes_bob", "gmail"],
843
936
  owner: "bob@example.com",
937
+ members: [agent],
844
938
  },
845
939
  },
846
940
  connectors: [
847
941
  crm,
848
942
  tickets,
943
+ billing,
849
944
  // Shared auth: a maintainer connects each user's own account once.
850
945
  remoteMcp("notes_alice", { url: "https://notes.example.com/mcp", auth: { type: "oauth" } }),
851
946
  remoteMcp("notes_bob", { url: "https://notes.example.com/mcp", auth: { type: "oauth" } }),
@@ -856,17 +951,38 @@ createConnecta({
856
951
  });
857
952
  ```
858
953
 
859
- The agent's client picks the pool from who it is talking to, for example:
954
+ Alice is cleared for leads and Bob for team. The agent's client picks the
955
+ pool from who is present, for example:
860
956
 
861
957
  ```ts
862
- const handles: Record<string, string | undefined> = { "<alice's sender id>": "alice", "<bob's sender id>": "bob" };
863
- const user = turn.isDirect ? handles[turn.verifiedSender] : undefined;
864
- const url = `https://connecta.example.com/agent/${user ? `assistant-${user}` : "assistant"}`;
958
+ // Lowest first. A person's tier is an index into this list.
959
+ const tiers = ["assistant", "assistant-team", "assistant-leads"];
960
+ const people: Record<string, { tier: number; handle?: string }> = {
961
+ "<alice's sender id>": { tier: 2, handle: "alice" },
962
+ "<bob's sender id>": { tier: 1, handle: "bob" },
963
+ };
964
+
965
+ // `verifiedParticipants`: everyone who can read the reply, other than the
966
+ // agent, from the platform's ids; undefined when the harness cannot verify it.
967
+ function poolFor(turn: { isDirect: boolean; verifiedParticipants?: string[] }) {
968
+ const present = turn.verifiedParticipants;
969
+ if (!present?.length) return tiers[0];
970
+ const known = present.map((id) => people[id]);
971
+ if (turn.isDirect && present.length === 1 && known[0]?.handle) {
972
+ return `assistant-${known[0].handle}`;
973
+ }
974
+ return tiers[Math.min(...known.map((person) => person?.tier ?? 0))];
975
+ }
976
+ const url = `https://connecta.example.com/agent/${poolFor(turn)}`;
865
977
  ```
866
978
 
867
- Rule 8 is why the example lists only the agent in each pool. People act as
868
- themselves only through a Workspace mapping; a shared-auth connector such as
979
+ A chat with Alice and Bob gets `assistant-team`; add an unknown guest and it
980
+ gets `assistant`, even if the platform marks the chat as direct. Rule 9 is
981
+ why each personal pool lists only the agent: people act as themselves only
982
+ through a Workspace mapping, and a shared-auth connector such as
869
983
  `notes_alice` acts as Alice for whoever the pool admits.
984
+ Rule 10 is why `assistant-team` may list Alice and Bob: it holds only
985
+ connectors that every member may use on the deployment's account.
870
986
 
871
987
  ## Shared and personal auth
872
988
 
@@ -941,12 +1057,12 @@ namespace is left to clean up. They then delete each consent whose epoch is no
941
1057
  longer live; one left behind cannot complete and expires with its link.
942
1058
  `POST /ui/oauth/<id>` takes a `mode`:
943
1059
 
944
- | Request | What it does |
945
- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
946
- | `POST /ui/oauth/<id>` or `?mode=restart` | Issues a signed `/connect/<id>` link requesting a fresh epoch. No downstream authorization starts until the verified browser visits it. |
947
- | `?mode=continue` | Issues a signed `/connect/<id>` link requesting continuation. At the browser visit, a recent pending consent can be reused; otherwise authorization begins in the current epoch. |
948
- | `GET /connect/<id>?h=...` | Verifies the signed handoff and, when signed in, browser identity, connector visibility and management permission. With the UI mounted, hands off to `/connectors/<id>?h=...#auth` without consuming the link or starting OAuth; Clerk sign-in uses that shell. The Auth tab continues with `start=1`, which repeats verification, consumes the link and calls `startAuth`. Without the UI, the original browser visit starts consent. Explicit `POST /ui/oauth/<id>` actions return a signed link with `start=1` when the UI is mounted. |
949
- | `DELETE /ui/oauth/<id>` | Disconnects and invalidates the cached catalog, even if the browser leaves. |
1060
+ | Request | What it does |
1061
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1062
+ | `POST /ui/oauth/<id>` or `?mode=restart` | Issues a signed `/connect/<id>` link requesting a fresh epoch. No downstream authorization starts until the verified browser visits it. |
1063
+ | `?mode=continue` | Issues a signed `/connect/<id>` link requesting continuation. At the browser visit, a recent pending consent can be reused; otherwise authorization begins in the current epoch. |
1064
+ | `GET /connect/<id>?h=...` | Verifies the signed handoff and, when signed in, browser identity, connector visibility and management permission. With the UI mounted and a maintainer signed in, hands off to `/connectors/<id>?h=...#auth` without consuming the link or starting OAuth; Clerk sign-in uses that shell. The Auth tab continues with `start=1`, which repeats verification, consumes the link and calls `startAuth`. Without the UI, or for a pool member, the original browser visit starts consent. Explicit `POST /ui/oauth/<id>` actions return a signed link with `start=1` when the UI is mounted. |
1065
+ | `DELETE /ui/oauth/<id>` | Disconnects and invalidates the cached catalog, even if the browser leaves. |
950
1066
 
951
1067
  `authorize_connector` issues the same browser link, with `force` carried in the
952
1068
  signed handoff. On hosts without URL elicitation, direct and program auth
@@ -1827,7 +1943,7 @@ For providers offering both, deployment config chooses; handlers check `ctx.oaut
1827
1943
 
1828
1944
  ## Request-local downstream Bearer tokens
1829
1945
 
1830
- `remoteMcp({ auth: { type: "request", token: async (ctx) => … } })` resolves
1946
+ `remoteMcp("<id>", { auth: { type: "request", token: async (ctx) => … } })` resolves
1831
1947
  its token before connecting and before reusing a client. The callback receives
1832
1948
  the operation's cancellation context, never an agent-selected credential owner.
1833
1949
  Only shared ownership is accepted. Tokens remain inside the request's hardened
@@ -555,6 +555,15 @@ URL-elicitation hosts retain their existing flow.
555
555
  **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program
556
556
  may do. A provider maps each downstream failure to the code that tells the
557
557
  caller what to do next, and never invents a cause it was not told.
558
+ The table lists the codes a program commonly sees. Codes owned by one
559
+ mechanism are specified with it: `resource_template_ambiguous` and
560
+ `resource_match_budget_exceeded` with resource reads above; the
561
+ `executor_*` admission codes in `E5`; `destructive_tool_requires_approval` with
562
+ direct calls; and `input_required_invalid`, `input_required_limit`,
563
+ `downstream_oauth_required`, `oauth_revocation_failed`,
564
+ `provider_permission_denied`, and `auth_replay_refused` in
565
+ [meta-tools](./meta-tools.md). Each of those is non-retryable except
566
+ `executor_overloaded`.
558
567
 
559
568
  | Code | Raised when | `retryable` |
560
569
  | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
@@ -577,6 +586,7 @@ caller what to do next, and never invents a cause it was not told.
577
586
  | `result_too_large` | a discovery response exceeded its byte bound | false |
578
587
  | `budget_exceeded` | the run exhausted a host-call, write, or emitted-output budget | false |
579
588
  | `write_outcome_unknown` | a program write was sent and no answer came back; it is never sent again (`W9`) | false |
589
+ | `executor_failed` | the executor returned a malformed result or failed outside the guest program | false |
580
590
 
581
591
  **E3.** `auth_required` carries the same recovery envelope as `call_tool`:
582
592
  `connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
@@ -685,6 +695,12 @@ when the metadata describes a write on a read-only endpoint.
685
695
  Caught call failures retain that connector visibility when catalog loading or
686
696
  tool resolution fails. Recovery messages listing configured connectors or
687
697
  local guides retain visibility of those contributors too.
698
+ Search guidance retains its referenced connectors even when no tool matches,
699
+ including connectors named by id or title and identities summarized as an
700
+ additional count. Scoped guides and summaries, absence rosters, and catalog
701
+ errors retain their contributing connectors as well.
702
+ Search counts, ranking, and term analysis retain every searched connector,
703
+ including matches outside the returned page or beyond the requested offset.
688
704
  Each page rechecks current auth,
689
705
  grants, pool membership, and the endpoint's `readOnly`. Reading a stash inside a program carries
690
706
  its dependencies forward to any new return stash. The page cap is the smallest
@@ -711,7 +727,7 @@ has no host log stream and cannot supply logs from its suspended guest (`L4`).
711
727
 
712
728
  **R6.** Every run returns `hostCalls: { attempted, admitted, succeeded, failed }`,
713
729
  including admission failure, cancellation, budget refusal, and code-size refusal.
714
- Calls include search, describe, call, result, and skill. `emit` has separate budgets.
730
+ Calls include search, describe, call, read, result, and skill. `emit` has separate budgets.
715
731
  An abandoned host call counts as failed when the run ends; guest catches do not
716
732
  change the counts. These numbers are outside the program's returned `result`.
717
733
  `diagnostics: true` adds the existing payload-free timing block. Emitting adds
@@ -770,7 +786,8 @@ a successful result, appended to `content` after the JSON envelope, which gains
770
786
  response (`R6`). `structuredContent` stays the envelope alone — emission is
771
787
  presentation, not a second data channel.
772
788
 
773
- **M3.** Text emission shares `R2`'s 24,000 serialized-character cap. Its
789
+ **M3.** Text emission shares `R2`'s 24,000 serialized-character cap, less 512
790
+ characters reserved for the envelope (23,488 for emitted text). Its
774
791
  serialized block size reduces the return budget, and oversized text fails with
775
792
  `result_too_large`. Image/audio blocks use the separate transport budget and
776
793
  reach the model as real MCP media content, including when `emit` is not awaited.
@@ -881,7 +898,7 @@ because connecta enforces them above the sandbox:
881
898
 
882
899
  | Bound | Value |
883
900
  | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
884
- | Host calls per execution, shared by `search`, `describe`, `call`, and `read` | 20 by default, `execute.maxHostCalls` |
901
+ | Host calls per execution, shared by every guest call except `emit` | 20 by default, `execute.maxHostCalls` |
885
902
  | Writes per run on a writable endpoint, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
886
903
  | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
887
904
  | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
@@ -891,8 +908,9 @@ because connecta enforces them above the sandbox:
891
908
  | Result | 24,000 serialized characters |
892
909
  | Logs presented to the model | 4,000 characters |
893
910
 
894
- Every `call`, `read`, `search`, and `describe` spends one host call on entry, before
895
- resolution, validation, or dispatch. Catching a local refusal refunds nothing.
911
+ Every `call`, `read`, `search`, `describe`, `result`, and `skill` spends one host
912
+ call on entry, before resolution, validation, or dispatch; automatic transfer
913
+ pages of a paged result do not. Catching a local refusal refunds nothing.
896
914
  The first call beyond the budget ends the host run with one non-retryable
897
915
  `budget_exceeded` (`E2`). Its bridge stays pending until lease disposal so guest
898
916
  `try/catch` cannot swallow it. Later host access, including `emit`, stops; pending
@@ -197,7 +197,10 @@ CF_ACCESS_CLIENT_ID='<access-service-id>' CF_ACCESS_CLIENT_SECRET='<access-servi
197
197
 
198
198
  The service headers admit the health probe at the edge; only the `cta_` token
199
199
  authenticates the agent endpoint inside Connecta. If the operator UI is enabled, maintainer-authenticated
200
- `connecta doctor --config --url <public-url>` prints the caller-scoped
200
+ `connecta doctor --config --url <public-url>` (on Node, `CONNECTA_TOKEN` set to a
201
+ maintainer's current Clerk session token issued for this deployment's origin;
202
+ Clerk OAuth access tokens are refused here, and Access on Workers has no
203
+ non-browser maintainer credential, so otherwise use the UI's Settings page) prints the caller-scoped
201
204
  configuration snapshot as JSON. It runs no diagnostic program and prints no
202
205
  raw HTTP failure body. [Operator UI](./operator-ui.md) describes the returned
203
206
  configuration, live facts, and permission rules. The config route is a human
@@ -227,6 +230,7 @@ Bump the exact package pin, read the [changelog](../CHANGELOG.md) for every
227
230
  intervening release, and apply the relevant migration before startup. `init`
228
231
  does not merge changes into an existing deployment.
229
232
 
233
+ - [Migration to 0.34](./migration-0.34.md) covers the Overview landing page, Endpoints at `/endpoints`, default browser titles, and Workspace discovery in owner-less agent pools. No storage migration or token rotation is needed from 0.33.
230
234
  - [Migration to 0.33](./migration-0.33.md) maps every removed access option to `maintainers`, pool `members`, and `agents`, moves agents to `/agent/<pool>`, and covers pool owners, token revisions, operator contract v2 with its operator route changes, and the pre-auth listen fix.
231
235
  - [Tokens issued before agents](./auth.md#agent-tokens-and-upgrading-from-earlier-tokens) stop admitting until each machine's token is bound once to a declared agent with `npm run adopt-token -- <token-id> <agent>` (Node) or `AccessTokenManager.adopt`, which keeps its secret. Human-bound tokens stay refused; issue those agents new tokens. Pool members name `"agent:<name>"` instead of matching token record ids.
232
236
  - [Node storage from 0.28](../templates/node/README.md#storage) uses `connecta migrate-state` to copy the JSON state into SQLite. Activity JSONL is not imported.
@@ -19,8 +19,10 @@ To add one:
19
19
  3. Add the id (all its tools) or exact `id.tool` addresses to the intended
20
20
  pools' `tools`. Maintainers at plain `/mcp` already see the whole catalog.
21
21
  Follow [the agent pool layout](./auth.md#organizing-an-agents-pools) for an
22
- agent: shared connectors go in its base constant, per-user connectors only
23
- in that user's pool.
22
+ agent: a shared connector goes in the tool list of the lowest tier whose
23
+ entire audience is authorized to use it (billing restricted to leads goes
24
+ in the leads tier); per-user connectors and Workspace connectors with a
25
+ mapping `subject` go only in personal pools.
24
26
  4. Run the deployment's `npm run typecheck` (Worker copies: `tsc --noEmit`),
25
27
  restart Node or redeploy the Worker, and
26
28
  [connect its credential](./auth.md#connect-a-shared-credential) if needed.
@@ -38,14 +40,15 @@ const crm = remoteMcp("crm", {
38
40
  authScope: "shared",
39
41
  auth: { type: "credential", credential: { label: "CRM API token" } },
40
42
  });
41
- const assistant = { tools: ["crm", "tickets"], members: ["agent:assistant"] };
42
- // connectors: [crm, tickets], agents: ["assistant"], pools: { assistant }
43
+ const base = ["crm", "tickets"]; // the agent's lowest tier
44
+ // connectors: [crm, tickets], agents: ["assistant"],
45
+ // pools: { assistant: { tools: base, members: ["agent:assistant"] } }
43
46
  ```
44
47
 
45
48
  To remove `crm`, first disconnect its OAuth grant or delete its saved
46
49
  credential while the connector still exists, using a signed-in maintainer
47
50
  for shared auth. Each person disconnects their own personal account. Remove
48
- `crm` and all `crm.*` addresses from every pool (including base constants),
51
+ `crm` and all `crm.*` addresses from every pool (including tier tool lists),
49
52
  then remove its entry from `connectors`, unused imports, and unused secret
50
53
  configuration. Remove any pools left with no tools (empty `tools` fails
51
54
  construction), and update or retire clients pointing at those pools. Typecheck, restart/redeploy, run doctor, and confirm discovery
@@ -92,7 +95,7 @@ Linear defaults to OAuth; its read-only endpoint requests only the downstream
92
95
  read scope. That is separate from a Connecta `readOnly` endpoint. Read the installed `dist/providers/linear/index.d.ts` for API-key framing,
93
96
  workspace instructions, and admission choices.
94
97
 
95
- The [human README](../README.md#what-you-can-do-with-it) lists available
98
+ The [human README](../README.md#what-you-can-connect) lists available
96
99
  integrations. In the installed package, `dist/providers/<name>/index.d.ts`
97
100
  and its imported declarations describe each factory's options. After adding
98
101
  a connector, use `search_tools` to discover its current tools and `connecta.skill("connector:<id>")` in `execute_code`