@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.34.0 — 2026-10-16
6
+
7
+ Connecta 0.34.0 rebuilds the operator UI around an Overview with connector health, endpoints, people and agents, a Needs attention list with fix prompts, and a connection map (#867). Maintainers now land on Overview at `/`; Endpoints moves to `/endpoints`, and `/access` redirects there. Update bookmarks, links and browser automation that expected Endpoints at `/`. Members still get their member page on every operator page path. The upgrade steps are in `documentation/migration-0.34.md`.
8
+
9
+ The redesign carries through Connectors, Endpoints, the access matrix, Tokens, Settings, Activity and the member page, with warm neutral surfaces, one accent, self-hosted Hanken Grotesk and JetBrains Mono, and light and dark themes. Configuration uses tiles, connector tools open in a schema drawer, Activity groups calls by request, and Account holds Sign out and Change token. Default browser titles now join the product and owner with `·`; set `branding.pageTitle` explicitly if an integration needs the old title. Custom titles stay unchanged. Activity outcome links can use `failed` or `succeeded`; older exact outcome values still filter by that value. Custom activity stores may optionally report `describe().optionSources.retentionDays` as `"config"` or `"default"` so Settings can distinguish an explicit retention setting; existing stores need no change.
10
+
11
+ Agents in a pool without an `owner` no longer discover Google Workspace connectors whose `subject` is a mapping function (#873). Those connectors previously appeared but failed calls with `auth_required`; now they are absent from discovery, search, describe, skills and code mode, and calls by address fail `unknown_address`. Saved program returns are rechecked too. To give an agent Workspace access, use a pool with the intended person's Workspace address as its `owner`, such as `alice@example.com`; do not add an owner to a shared pool just to restore discovery. People, agents in owned pools and fixed-subject connectors keep their behavior.
12
+
13
+ Pools can now take a one-line `description` of 1 to 500 characters, appended to that pool's MCP instructions after `This endpoint: ` (#872). Pools without a description and plain `/mcp` keep their instructions. The auth guide now documents group tiers: pick the most capable pool everyone present is cleared for, use the base pool when anyone is unknown, and build personal pools on that person's highest tier (#875). Shipped docs also correct operator, provider and deployment guidance (#866). Repository development and CI now use pnpm (#877); installed deployments, the Node template and npm publishing keep using npm. Upgrading from 0.33.0 needs no storage migration or token rotation. Deployments that do not use the operator UI or mapping-subject Workspace connectors in owner-less agent pools can ignore the UI and discovery migration steps; all new configuration fields are optional.
14
+
15
+ ### Added
16
+
17
+ - Add an Overview page as the maintainer's landing page at `/`: a status pill and stat cards for connector health, endpoints, and people and agents, a "Needs attention" list that links each problem to its existing fix with a copyable fix prompt, and a connection map of callers, endpoints and connectors that traces what a node reaches on hover or focus and opens its page on click. Endpoints moves to `/endpoints`, and `/access` now redirects there. Members still get their member page on every path.
18
+ - Pools take an optional `description`: one line of plain text, 1 to 500 characters, that `/mcp/<pool>` and `/agent/<pool>` append to their MCP instructions after `This endpoint: ` so the model learns that endpoint's scope. Plain `/mcp` and pools without one keep the shared instructions, and `describeConfig()` lists each pool's `description` (`null` when unset).
19
+
20
+ ### Changed
21
+
22
+ - The operator UI's Activity page and the member page ("Your connections") follow the calm redesign (#867). Activity shows counts and a calls-over-time strip, filters with a search box, an All / Failed / Succeeded control and connector and source chips instead of select menus, and rows grouped by request with one status dot, the caller as a person or agent chip, a connector monogram, the tool, and duration and size; a row opens for the rest of what was recorded. A connector's Activity tab uses the same rows. The member page shows the signed-in email, endpoints as cards, accounts with distinct monograms and aligned status badges, and a compact shared list. The Activity outcome filter now takes `failed` or `succeeded`; an older exact outcome value in a link still filters by that value.
23
+ - An agent in a pool without an `owner` no longer sees Google Workspace connectors whose `subject` is a mapping function: they are absent from discovery, search, describe, skills and code mode, and a call by address fails `unknown_address`, as for a personal connector. Before, the agent saw them and every call failed `auth_required`. People in the same pool, agents in owned pools, and fixed-subject connectors are unchanged (#870).
24
+
25
+ Paging a saved program return rechecks every connector contributing to search counts, ranking, term analysis, or guidance, including matches outside the returned page and connector references when no tools matched. Removing a pool owner therefore also hides those saved contributions from agents.
26
+ - Document group tiers in the agent pool layout: a chat gets the most capable tier everyone present is cleared for, tiers nest by spreading tool lists, the base pool is the lowest tier and the fallback for anyone unknown, a tier can double as a team pool for the people cleared for it, and personal pools build on their person's highest tier. Shorten the README to the visual overview.
27
+ - The operator UI and the pages connecta renders for people (sign-in, no access, OAuth results, 404s) take a new look: warm neutral surfaces, one indigo accent, soft cards, status pills, and Hanken Grotesk with JetBrains Mono for values, in light and dark. The fonts stay self-hosted. The top bar puts the sections, Search, Appearance and a new Account button on one row; Sign out and Change token now live under Account. The drawn connecta mark shows only on an unbranded deployment; a configured `favicon.svg` shows instead. A custom accent derives readable button text and dark-mode link color from itself.
28
+ - The operator UI's Connectors page shows a card per connector with its status, Shared or Per person badge, a read/write tool bar and its last call, filtered by All, Needs attention or Healthy. A connector's page opens with its status and badges; Tools filters by Read or Write and opens a schema in a side drawer, and Connection shows authentication facts, a credential card with its masked last four, and diagnostics as tiles (#867).
29
+ - The operator UI's Endpoints, access matrix and Tokens pages adopt the calm design. Endpoint rows show who can use them as an avatar stack, their connectors as monograms and their tools over a read/write bar, with a read-only pill only where it applies; client setup switches between Claude Code, Codex and JSON over one snippet. Matrix cells show the share of a connector they grant as a pie, read-only columns are hatched, and the Status and Runs as columns give way to a status dot and each cell's popover, which marks every tool read or write. Tokens rows show the agent mark, pool chips, a state pill and dates, with one primary action at most, and stack into cards on a narrow screen. Matrix shares count only live granted tools, show unknown coverage when the catalog is unavailable, and name both granted and total live counts for assistive technology.
30
+ - Develop the repository with pnpm instead of npm: `pnpm-lock.yaml` replaces `package-lock.json`, `pnpm-workspace.yaml` allows only the esbuild and workerd build scripts, CI installs with `pnpm install --frozen-lockfile`, and `check:security` runs `pnpm audit`. The published package, its install instructions, and the Node template still use npm, and releases still publish through `npm publish` with trusted publishing.
31
+ - Settings and the connector Configuration card show configuration as tiles instead of a key/value tree. Deployment facts are chips (name with server version, Writes allowed or Read-only, and a warning chip for unconfigured memory storage); sign-in shows provider tiles, an agent count and one Maintainers tile; features show on or off; headline limits draw large with an accent dot when set in code, and the remaining defaults fold into one "17 defaults" disclosure of grouped cards. Sizes read in decimal units with the exact byte count as a tooltip, and a null tool timeout shows the host-call timeout it falls back to. A connector's card adds an auth row: mode, header names, OAuth scope, API origins and OAuth endpoints as compact badges. The D1 and SQLite activity stores report whether code set `retentionDays`, and Settings marks retention set only then. Snapshot keys the UI does not map still appear as labelled tiles.
32
+
33
+ Open deployments retain the Maintainers tile beside Open, with "None" when no maintainers are configured.
34
+ - The operator UI uses shorter copy throughout (#867): labels, empty and error states, confirmations, notices, gates and the OAuth callback and 404 pages, with no em dashes. Page titles separate with a middle dot. On the Overview, narrow stat tiles sit two up, the map draws a muted dash instead of "0 tools" for a connector whose catalog is unknown, and caller chips in the narrow list link like map nodes.
35
+
36
+ ### Fixed
37
+
38
+ - Correct inaccurate guidance across the shipped docs, provider skills, and the usage skill: token-route statuses, OAuth control labels, meta-tool paging and timing, host-call budgets, error codes, doctor output and credentials, local `.env` use, Worker secrets and copied-repo setup, the 0.33 Node template script upgrade, and provider argument names and write classifications. Rewrite the README to explain people, agents, pools, and account ownership with diagrams, and drop the hero image.
39
+
5
40
  ## 0.33.0 — 2026-10-16
6
41
 
7
42
  Connecta 0.33.0 makes pools the access model, and every deployment with inbound auth must change its configuration (#837). Work through `documentation/migration-0.33.md` before upgrading; it maps every removed option to its replacement. A new top-level `maintainers` list names the people who reach plain `/mcp` and administer the deployment. Each pool now has a static `members` list and serves exactly its `tools` to those members. Agents are declared by name in `agents`, each with one rotatable `cta_` token. Each removed option fails at startup with a message naming its replacement, so a deployment that starts has nothing silently ignored: `identity` (even `{}`) and its `connectorAccess`, `credentialAdministration`, `personalConnection`, `accessTokenManagement` and `activityAccess`, plus the top-level `grant` and pool `grant`.
package/README.md CHANGED
@@ -1,157 +1,128 @@
1
1
  # connecta
2
2
 
3
- ![A monochrome clay Connecta hub joining many tools](https://raw.githubusercontent.com/zackbart/connecta/main/assets/connecta-clay-hero.png)
3
+ **One connection between your agent and the services you choose.**
4
4
 
5
- One connection between your agent and the services you choose.
5
+ Connecta puts your MCP servers and HTTP APIs behind one endpoint that you run
6
+ yourself. Connect your agent once; decide in code who can reach what.
6
7
 
7
- Connecta brings your MCP servers and HTTP APIs together behind one endpoint.
8
- Connect your agent once, then choose which integrations it can use. You run
9
- Connecta yourself, on your own machine or infrastructure.
10
-
11
- ## The mental model
12
-
13
- Ask your agent to find open issues, compare them with customer feedback, or
14
- prepare a report. Connecta gives it a small set of tools for finding and using
15
- the services you connected, without loading every service's tool definitions
16
- into the conversation.
17
-
18
- The agent writes a short program that runs in a sandbox beside those
19
- integrations. It can gather results, combine them, and return the parts that
20
- answer your question. Fifty issues can become a short list grouped by owner
21
- before the result reaches the conversation. The agent writes the code; you
22
- ask for the work.
8
+ ## How it works
23
9
 
24
10
  ```mermaid
25
11
  flowchart LR
26
- Agent["Your agent"] <-->|"One connection"| Connecta["Your Connecta deployment"]
27
- Connecta <--> Work["Work tools<br/>Linear, GitHub, Notion"]
28
- Connecta <--> Business["Business services<br/>Stripe, RevenueCat, Mixpanel"]
29
- Connecta <--> Yours["Your other services<br/>Remote MCP servers and HTTP APIs"]
12
+ Agent["Your agent"] <-->|"One connection<br/>six small tools"| Connecta
13
+
14
+ subgraph Connecta["Your Connecta deployment"]
15
+ Sandbox["Sandbox<br/>runs the agent's short programs"]
16
+ Keys["Credentials<br/>stay on the server"]
17
+ end
18
+
19
+ Connecta <--> Work["Linear, GitHub, Notion"]
20
+ Connecta <--> Business["Stripe, RevenueCat, Mixpanel"]
21
+ Connecta <--> Yours["Any remote MCP server<br/>or HTTP API"]
30
22
 
31
23
  style Connecta fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b,stroke-width:2px
32
24
  ```
33
25
 
34
- You choose which services to connect and who can use them: the maintainers
35
- you name reach everything, and everyone else, people and agents alike, reaches
36
- only the pools of tools you list them in. Your agent keeps the same connection
37
- as those services change.
26
+ The agent doesn't load every service's tools into the conversation. It writes
27
+ a short program that gathers, joins, and trims results inside Connecta, so
28
+ only the answer comes back.
29
+
30
+ ## What you can connect
38
31
 
39
- ## From a question to an answer
32
+ Maintained connections: <!-- providers:start -->Basecamp, Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Infisical, Linear, Mixpanel, Notion, Overflow, Planning Center, RevenueCat, Stripe, Tithe.ly, Vercel, and Wispr Flow<!-- providers:end -->.
33
+ Plus any remote MCP server or HTTP API you describe.
40
34
 
41
- Say you ask, "Which open issues affect our highest-value customers?" Your agent
42
- can gather information from your issue tracker and customer records, combine
43
- it inside Connecta, and bring back a focused answer.
35
+ ## Who can use it
36
+
37
+ | | People | Agents |
38
+ | ----------------- | ---------------------------------------------------- | ---------------------------------------- |
39
+ | What they are | You and your team | Bots, assistants, CI, scheduled jobs |
40
+ | How they prove it | Sign in: Clerk (Node) or Cloudflare Access (Workers) | One token Connecta issues to that agent |
41
+ | Their own logins | Yes, on personal connections | No. An agent uses accounts you assign it |
42
+
43
+ **Maintainers** are the people who run the deployment: they reach every tool
44
+ and manage credentials and agent tokens. Everyone else reaches only the pools
45
+ they are listed in.
46
+
47
+ ### Pools
48
+
49
+ A **pool** is a named set of tools plus a list of who may use it. Each pool
50
+ has its own address, and every caller sees exactly that pool's tools.
44
51
 
45
52
  ```mermaid
46
- flowchart TB
47
- Question["You ask a question"] --> Plan["Your agent works out what to look up"]
48
- Plan --> Gather["A program in Connecta gathers the relevant records"]
49
- Gather --> Tracker["Open issues from your tracker"]
50
- Gather --> Customers["Customer records from a connected service"]
51
- Tracker --> Combine["The program matches, filters, and groups the results"]
52
- Customers --> Combine
53
- Combine --> Answer["Your agent explains the answer"]
54
-
55
- style Combine fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b,stroke-width:2px
56
- style Answer fill:#dcfce7,stroke:#15803d,color:#14532d
53
+ flowchart LR
54
+ Maint["Maintainer<br/>(signs in)"] --> Main["/mcp<br/>every tool"]
55
+ Bob["Bob<br/>(signs in)"] --> Team["/mcp/team<br/>CRM + tickets"]
56
+ Bot["assistant agent<br/>(token)"] --> AgentPool["/agent/assistant<br/>CRM + tickets"]
57
+
58
+ style Main fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b
59
+ style Team fill:#dcfce7,stroke:#15803d,color:#14532d
60
+ style AgentPool fill:#fef3c7,stroke:#b45309,color:#78350f
57
61
  ```
58
62
 
59
- The records are an example; the available information depends on the services
60
- and access you have configured. Processing results inside Connecta keeps the
61
- conversation focused on the answer.
62
-
63
- ## What you can do with it
64
-
65
- Connect hosted MCP services, add an internal HTTP API, and let the agent work
66
- across them in one task. Changing your connected services does not require
67
- changing the agent's connection. Your deployment decides which services and
68
- operations each caller can reach.
69
-
70
- Connecta includes maintained connections for <!-- providers:start -->Basecamp, Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Infisical, Linear, Mixpanel, Notion, Overflow, Planning Center, RevenueCat, Stripe, Tithe.ly, Vercel, and Wispr Flow<!-- providers:end -->.
71
- Each connection has its own setup guide.
72
- Stripe, Notion, Cloudflare, and Vercel use their hosted MCP servers when you
73
- sign in with OAuth, and Connecta's own API connections when you use an API key,
74
- a Cloudflare API token or Global API Key, a Notion integration token, or a
75
- Vercel access token. You can also
76
- connect other remote MCP servers or define the operations you need from an
77
- HTTP API.
78
-
79
- Google Workspace connections use the account your deployment assigns to each
80
- signed-in person. A Workspace administrator authorizes them for your domain.
81
- Gmail reads mail and attachments and writes drafts, but does not send mail.
82
- Google Drive can read, write, and share files, but does not permanently delete
83
- them. Available
84
- operations and account access depend on the integration and your deployment.
85
-
86
- Large results need not fill the conversation. The agent can filter and
87
- summarize them in its program, or read a large result in smaller pieces.
88
-
89
- ## Writes, credentials, and access
90
-
91
- Connecta distinguishes reads from writes, and each endpoint is either writable
92
- or read-only. By default an endpoint is writable: your agent can make a change
93
- with one visible tool call or inside a program, and your agent's host decides
94
- whether to ask for approval; it sees the program tool as a write. You can mark
95
- the whole deployment or a single endpoint read-only, and then nothing there can
96
- change anything: search leaves write tools out and every write is refused. You
97
- make this choice explicitly for each deployment. Neither choice gives a caller
98
- access to additional services.
63
+ People use a pool at `/mcp/<pool>` and agents at `/agent/<pool>`. A caller
64
+ outside a pool gets the same "not found" as a pool that doesn't exist, and
65
+ someone in two pools uses two addresses: pools never merge.
66
+
67
+ ### One agent, many people
68
+
69
+ An assistant that talks to several people keeps **one token and one
70
+ connection**. Its harness, not the model, picks the pool each turn: a chat
71
+ gets the most capable tier that everyone in it is cleared for, and a
72
+ one-on-one gets that person's own pool.
99
73
 
100
74
  ```mermaid
101
- flowchart TB
102
- Change["Your agent wants to change something"] --> Choice{"Is this endpoint<br/>read-only?"}
103
- Choice -->|"Explicit choice: yes"| Refused["Refused; nothing is sent"]
104
- Choice -->|"Default: no"| Host["Your agent's host handles approval"]
105
- Host --> Access["Connecta checks the caller's access"]
106
- Access --> Services["The connected service receives the change"]
107
-
108
- style Host fill:#fef3c7,stroke:#b45309,color:#78350f
109
- style Access fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b,stroke-width:2px
75
+ flowchart LR
76
+ Guests["Chat with anyone unknown"] --> Base["/agent/assistant<br/>read-only basics"]
77
+ Group["Chat of team members"] --> TeamTier["/agent/assistant-team<br/>shared tools"]
78
+ Leads["Chat of leads only"] --> LeadsTier["/agent/assistant-leads<br/>shared tools + billing"]
79
+ Alice["One-on-one<br/>with Alice"] --> PA["/agent/assistant-alice<br/>her tier + her accounts"]
80
+
81
+ Base -.-> TeamTier -.-> LeadsTier
82
+
83
+ style Base fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b
84
+ style TeamTier fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b
85
+ style LeadsTier fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b
86
+ style PA fill:#dcfce7,stroke:#15803d,color:#14532d
110
87
  ```
111
88
 
112
- Connecta handles integration credentials on the server. Its sandbox does not
113
- receive them. Optional encrypted storage protects saved credentials. Connections can use
114
- shared credentials or a person's own account, with access rules set by the
115
- deployment owner. Permission to use a connection is separate from permission
116
- to manage its credentials.
117
-
118
- People sign in through Clerk on Node or Cloudflare Access on Workers. Machine
119
- clients are agents you name in deployment code, each with one token issued by
120
- your Connecta deployment. Agents connect through their own address for each
121
- pool, separate from the one people use, so an agent sends only its token, even
122
- behind Cloudflare Access. A deployment can serve
123
- several people within one tenant, each with the access you assign. An agent
124
- that serves several people keeps one connection and switches pools per turn:
125
- a shared pool for group chats and scheduled work, and a pool per person, with
126
- that person's own accounts, used only when that person is the one talking.
127
-
128
- ## A place to see your connections
129
-
130
- The optional operator UI shows connected services, available tools, access,
131
- and configuration. Maintainers manage shared credentials, issue or rotate agent
132
- tokens, and read the optional activity history, which shows who called what,
133
- when, and whether it worked, without recording arguments, results, or program
134
- code. Pool members connect their own accounts on personal connections.
135
-
136
- Your agent or developer changes the integrations and access rules in deployment
137
- code. The UI helps you inspect them and manage authentication.
89
+ Each tier builds on the one below it. A person's own pool builds on the
90
+ highest tier they are cleared for and holds only the agent, since anyone else
91
+ in it would act as that person. A tier can double as a team's own pool for
92
+ the people cleared for it. [Organizing an agent's pools](./documentation/auth.md#organizing-an-agents-pools)
93
+ has the full layout.
94
+
95
+ ### Whose account a connection uses
96
+
97
+ - **Shared** (the default): the deployment's account, connected once by a
98
+ maintainer.
99
+ - **Personal**: each person's own account. Agents never see these.
100
+ - **Google Workspace**: the signed-in person's account; for an agent, the
101
+ owner of a person's pool, or one fixed shared account. Without an owner,
102
+ mapping connections are absent for agents.
103
+
104
+ ## Reads and writes
105
+
106
+ Every endpoint is writable or read-only, and you choose explicitly. A
107
+ read-only endpoint hides write tools and refuses every write. On a writable
108
+ one, your agent's host decides what to approve.
138
109
 
139
110
  ## Where it runs
140
111
 
141
- Run Connecta locally or on a server with Node, including in Docker, or host it
142
- on Cloudflare Workers. Both offer the same agent tools. Running programs on
143
- Workers requires Cloudflare's Workers Paid plan.
112
+ Node or Docker (Clerk sign-in, SQLite) or Cloudflare Workers (Cloudflare
113
+ Access, D1). Both serve the same agent tools. An optional operator UI shows
114
+ a connection map, endpoints, connectors, and activity; integrations and access
115
+ still change only in deployment code.
144
116
 
145
117
  ## Getting started
146
118
 
147
- Give your agent the [agent documentation](./documentation/README.md) and ask it
148
- to set up Connecta with the integrations you want. Tell it where you want to
149
- run it and who should have access. The setup guides cover Node, Docker, and
150
- Cloudflare Workers, including connecting your agent client.
119
+ Give your agent the [agent documentation](./documentation/README.md) and ask
120
+ it to set up Connecta with the integrations you want, where to run it, and who
121
+ should have access.
151
122
 
152
123
  <a id="existing-client-tokens"></a>
153
124
 
154
- For an existing deployment, ask your agent to follow the
125
+ Upgrading? Point your agent at the
155
126
  [upgrade guidance](./documentation/deploying.md#upgrade-an-existing-deployment).
156
127
 
157
128
  ## Status
@@ -160,7 +131,7 @@ Built for its author's deployments first and published openly. Breaking
160
131
  changes are expected before 1.0. See the [changelog](./CHANGELOG.md) and
161
132
  [security policy](./SECURITY.md).
162
133
 
163
- The [principles](https://github.com/zackbart/connecta/blob/main/PRINCIPLES.md)
164
- state the goals and tested rules. [Decision records](https://github.com/zackbart/connecta/tree/main/decisions)
165
- explain past choices. [GitHub issues](https://github.com/zackbart/connecta/issues)
166
- track planned work; a plan is not a shipped feature.
134
+ [Principles](https://github.com/zackbart/connecta/blob/main/PRINCIPLES.md) ·
135
+ [Decision records](https://github.com/zackbart/connecta/tree/main/decisions) ·
136
+ [Planned work](https://github.com/zackbart/connecta/issues) (a plan is not a
137
+ shipped feature)
package/SECURITY.md CHANGED
@@ -20,5 +20,5 @@ The MCP TypeScript SDK 2.x packages and Effect are exact-pinned. Effect is the
20
20
  runtime core, and its unstable modules can break in a minor release, so its pin
21
21
  moves in a dedicated upgrade. The split client and server packages no longer
22
22
  install Hono or `@hono/node-server`, so the temporary root-project override
23
- used with SDK 1.x is gone. A dependency bump must pass `npm run release:check`
23
+ used with SDK 1.x is gone. A dependency bump must pass `pnpm run release:check`
24
24
  and the Node/Workers runtime suites before a pin moves.
package/bin/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.33.0";
3
+ export const CONNECTA_VERSION = "0.34.0";
@@ -126,10 +126,13 @@ export interface ActivityStore extends ActivitySink {
126
126
  /**
127
127
  * The adapter and its retention, for `Connecta.describeConfig()`. Kinds
128
128
  * other than the shipped `"d1"` and `"sqlite"` are described as `"custom"`.
129
+ * `optionSources` says whether code set `retentionDays` (`"config"`) or the
130
+ * store chose it (`"default"`); without it the retention reads as a default.
129
131
  */
130
132
  describe?(): {
131
133
  kind: string;
132
134
  retentionDays?: number;
135
+ optionSources?: Readonly<Record<string, "default" | "config">>;
133
136
  };
134
137
  }
135
138
  /** Reader implementations throw this for an opaque cursor they cannot decode. */
@@ -99,6 +99,13 @@ export interface PageLayout {
99
99
  * `mastheadEnd`, and `tail` are the caller's markup and are not.
100
100
  */
101
101
  export declare function renderPage(branding: ConnectaBranding | undefined, layout: PageLayout): string;
102
+ /**
103
+ * The mark beside the brand name. Connecta's drawn "C" belongs to an
104
+ * unbranded deployment only; a deployment that serves its own SVG favicon
105
+ * shows that; any other branding shows its name alone.
106
+ */
107
+ export type BrandMark = "connecta" | "favicon" | "none";
108
+ export declare function brandMark(branding: ConnectaBranding | undefined, uiMounted: boolean): BrandMark;
102
109
  /**
103
110
  * Status marks for one-message pages. Decorative: the label and heading beside
104
111
  * each carry the meaning, so the shape repeats it and color is never alone.
package/dist/branding.js CHANGED
@@ -1,4 +1,4 @@
1
- import { PAGE_CSS, TOKENS_CSS } from "./page-styles.js";
1
+ import { PAGE_CSS, PAGE_FONTS, TOKENS_CSS } from "./page-styles.js";
2
2
  import { htmlSecurityHeaders } from "./html-security.js";
3
3
  /** Connecta's default monochrome "C" mark. */
4
4
  export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
@@ -9,6 +9,7 @@ export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" vie
9
9
  <path class="fg" d="M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z"/>
10
10
  </svg>`;
11
11
  const DEFAULT_FAVICON_HREF = "/favicon.svg";
12
+ const DEFAULT_PRODUCT_NAME = "Connecta";
12
13
  /**
13
14
  * Branding arrives from operator config, which is untyped at a JS call site, so
14
15
  * every field is treated as `unknown`: a non-string is read as unset rather than
@@ -19,7 +20,7 @@ function trimmedString(value) {
19
20
  return typeof value === "string" ? value.trim() || undefined : undefined;
20
21
  }
21
22
  export function resolveBranding(branding) {
22
- const productName = trimmedString(branding?.productName) ?? "Connecta";
23
+ const productName = trimmedString(branding?.productName) ?? DEFAULT_PRODUCT_NAME;
23
24
  const ownerName = trimmedString(branding?.ownerName);
24
25
  // Operator branding URLs become masthead/callback hrefs, so a non-http(s)
25
26
  // scheme (javascript:, data:) is dropped the same as an unset URL — the
@@ -34,7 +35,7 @@ export function resolveBranding(branding) {
34
35
  ...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
35
36
  description: trimmedString(branding?.description) ??
36
37
  `Manage the services this ${productName} instance makes available to agents.`,
37
- pageTitle: trimmedString(branding?.pageTitle) ?? (ownerName ? `${productName} — ${ownerName}` : productName),
38
+ pageTitle: trimmedString(branding?.pageTitle) ?? (ownerName ? `${productName} · ${ownerName}` : productName),
38
39
  faviconHref: faviconHref && isSafeIconHref(faviconHref) ? faviconHref : DEFAULT_FAVICON_HREF,
39
40
  themeColor: configuredThemeColor(branding) ?? "#ffffff",
40
41
  theme: resolveTheme(branding?.theme),
@@ -128,6 +129,46 @@ export function droppedThemeTokens(theme) {
128
129
  }
129
130
  return dropped.map((token) => `theme.${token}`);
130
131
  }
132
+ function rgbOf(hex) {
133
+ const digits = hex.slice(1);
134
+ // `#rgb` and `#rgba` double each digit; alpha never affects contrast here.
135
+ const short = digits.length <= 4;
136
+ const pairs = [0, 1, 2].map((i) => (short ? digits[i].repeat(2) : digits.slice(i * 2, i * 2 + 2)));
137
+ return pairs.map((pair) => parseInt(pair, 16));
138
+ }
139
+ function luminance([r, g, b]) {
140
+ const channel = (value) => {
141
+ const c = value / 255;
142
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
143
+ };
144
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
145
+ }
146
+ function contrast(a, b) {
147
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
148
+ return (hi + 0.05) / (lo + 0.05);
149
+ }
150
+ const hexOf = (rgb) => `#${rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("")}`;
151
+ /** The dark palette's lightest surface that carries accent text (`--hover`). */
152
+ const DARK_TEXT_GROUND = [0x24, 0x22, 0x1e];
153
+ /**
154
+ * A configured accent's two derived colors. Text on its fill is white or
155
+ * near-black, whichever reads better; accent text on dark surfaces mixes the
156
+ * accent with white until it reaches 4.5:1, starting from the built-in
157
+ * accent's 80% so a light accent keeps its color.
158
+ */
159
+ function accentTokens(accent) {
160
+ const rgb = rgbOf(accent);
161
+ const ink = [0x0f, 0x0e, 0x1f];
162
+ const onAccent = contrast(rgb, [255, 255, 255]) >= contrast(rgb, ink) ? "#ffffff" : hexOf(ink);
163
+ let darkText = hexOf(rgb.map((c) => c * 0.8 + 255 * 0.2));
164
+ for (let share = 0.8; share >= 0; share -= 0.05) {
165
+ const mixed = rgb.map((c) => c * share + 255 * (1 - share));
166
+ darkText = hexOf(mixed);
167
+ if (contrast(mixed, DARK_TEXT_GROUND) >= 4.5)
168
+ break;
169
+ }
170
+ return { onAccent, darkText };
171
+ }
131
172
  /**
132
173
  * The resolved theme as a `:root` block, or "" when a deployment configured
133
174
  * nothing. It is emitted after the stylesheet so it overrides the defaults.
@@ -135,8 +176,10 @@ export function droppedThemeTokens(theme) {
135
176
  * anything that would need escaping is dropped rather than rewritten.
136
177
  */
137
178
  export function themeCss(theme) {
179
+ const derived = theme.accent ? accentTokens(theme.accent) : undefined;
138
180
  const declarations = [
139
181
  theme.accent ? `--accent:${theme.accent}` : "",
182
+ derived ? `--custom-on-accent:${derived.onAccent};--custom-accent-text:${derived.darkText}` : "",
140
183
  theme.radius !== undefined ? `--radius:${theme.radius}` : "",
141
184
  theme.fontFamily ? `--sans:${theme.fontFamily}` : "",
142
185
  theme.monoFamily ? `--mono:${theme.monoFamily}` : "",
@@ -156,11 +199,11 @@ export function escapeHtml(value) {
156
199
  return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
157
200
  }
158
201
  /**
159
- * The masthead's `--surface` in each palette, for the browser chrome a
202
+ * The top bar's `--bg` in each palette, for the browser chrome a
160
203
  * `theme-color` paints. A configured `themeColor` wins; without one a dark
161
204
  * page should not sit under a white status bar.
162
205
  */
163
- const SURFACE = { light: "#ffffff", dark: "#151a21" };
206
+ const SURFACE = { light: "#f6f4ef", dark: "#12110f" };
164
207
  /** The operator's own `themeColor`, read the one way both callers read it. */
165
208
  function configuredThemeColor(branding) {
166
209
  return trimmedString(branding?.themeColor);
@@ -202,6 +245,7 @@ export function renderPage(branding, layout) {
202
245
  : label("brand", brand.productName, brand.productUrl);
203
246
  const product = brand.ownerName ? label("product", brand.productName, brand.productUrl) : "";
204
247
  const customIcon = brand.faviconHref !== DEFAULT_FAVICON_HREF;
248
+ const mark = brandMarkHtml(brandMark(branding, layout.uiMounted));
205
249
  // `isSafeIconHref` already limited a relative href to one leading slash, so
206
250
  // resolving it can only land on `iconOrigin` itself.
207
251
  const iconHref = (href) => layout.iconOrigin && href.startsWith("/") ? new URL(href, layout.iconOrigin).toString() : href;
@@ -214,7 +258,13 @@ export function renderPage(branding, layout) {
214
258
  const skip = layout.skipTo
215
259
  ? `<a class="skip-link" href="#${escapeHtml(layout.skipTo.id)}">${escapeHtml(layout.skipTo.label)}</a>\n`
216
260
  : "";
217
- const styles = (layout.styles ?? TOKENS_CSS + PAGE_CSS) + (layout.extraStyles ?? "");
261
+ // The operator bundle declares its own fonts. Other pages link the same
262
+ // files only where the UI module serves them, root-relative so the page's
263
+ // own origin serves them; elsewhere the stacks fall back to system fonts.
264
+ const fonts = layout.uiMounted
265
+ ? PAGE_FONTS.map((font) => `@font-face{font-family:"${font.family}";font-style:normal;font-weight:${font.weight};font-display:swap;src:url("${font.path}") format("woff2")}`).join("")
266
+ : "";
267
+ const styles = (layout.styles ?? fonts + TOKENS_CSS + PAGE_CSS) + (layout.extraStyles ?? "");
218
268
  return `<!doctype html>
219
269
  <html lang="en"${schemeAttribute(brand.theme)}>
220
270
  <head>
@@ -231,7 +281,7 @@ ${layout.head ?? ""}
231
281
  ${skip}<header class="masthead shell">
232
282
  <div class="masthead-inner">
233
283
  <div class="mast-nav">
234
- ${owner}${product ? `\n ${product}` : ""}
284
+ ${mark}${owner}${product ? `\n ${product}` : ""}
235
285
  </div>${layout.mastheadEnd ? `\n ${layout.mastheadEnd}` : ""}
236
286
  </div>
237
287
  </header>
@@ -239,6 +289,23 @@ ${layout.body}${layout.tail ? `\n${layout.tail}` : ""}
239
289
  </body>
240
290
  </html>`;
241
291
  }
292
+ export function brandMark(branding, uiMounted) {
293
+ if (uiMounted && trimmedString(branding?.favicon?.svg))
294
+ return "favicon";
295
+ const brand = resolveBranding(branding);
296
+ const unbranded = brand.productName === DEFAULT_PRODUCT_NAME &&
297
+ !brand.ownerName &&
298
+ brand.faviconHref === DEFAULT_FAVICON_HREF &&
299
+ !branding?.favicon?.svg;
300
+ return unbranded ? "connecta" : "none";
301
+ }
302
+ function brandMarkHtml(mark) {
303
+ if (mark === "connecta")
304
+ return '<span class="brand-mark" aria-hidden="true"></span>';
305
+ if (mark === "favicon")
306
+ return `<img class="brand-mark" src="${DEFAULT_FAVICON_HREF}" alt="">`;
307
+ return "";
308
+ }
242
309
  /**
243
310
  * Status marks for one-message pages. Decorative: the label and heading beside
244
311
  * each carry the meaning, so the shape repeats it and color is never alone.
@@ -281,12 +348,12 @@ export function notFoundResponse(request, opts) {
281
348
  <p class="status-label">404</p>
282
349
  </div>
283
350
  <h1>Page not found</h1>
284
- <p class="status-copy">Nothing is served at this address. Check the link, or ask whoever runs ${escapeHtml(brand.productName)} for the right one.</p>
351
+ <p class="status-copy">Nothing is here. Check the link, or ask whoever runs ${escapeHtml(brand.productName)}.</p>
285
352
  ${home}
286
353
  </section>
287
354
  </main>`;
288
355
  return new Response(renderPage(opts.ui?.branding, {
289
- title: `Page not found — ${brand.pageTitle}`,
356
+ title: `Page not found · ${brand.pageTitle}`,
290
357
  uiMounted,
291
358
  // Same bytes on either host: the icons, like the home link, come from
292
359
  // the public origin.
@@ -582,7 +582,7 @@ export class CatalogService {
582
582
  }
583
583
  return withAuthorizationHandoff(this.requestScope, error);
584
584
  }
585
- async search(args) {
585
+ async search(args, onCatalogReference) {
586
586
  if (args.query !== undefined && typeof args.query !== "string") {
587
587
  throw new DiscoveryPolicyError("invalid_args", "query must be a string. Omit it or use an empty string to browse the catalog.");
588
588
  }
@@ -915,6 +915,20 @@ export class CatalogService {
915
915
  : "No single tool matched every term in the catalogs that answered. Split distinct intents into separate searches.";
916
916
  };
917
917
  const guidance = searchGuidance();
918
+ // Counts, ranking, and term analysis use the whole searched corpus, not
919
+ // just this page's tools. Bind every searched connector, including failed
920
+ // catalogs and off-page matches, so paging cannot reveal a hidden
921
+ // connector's contribution. Keep provenance host-only.
922
+ for (const connector of connectors)
923
+ onCatalogReference?.(connector.id);
924
+ for (const entry of entries)
925
+ onCatalogReference?.(entry.connector.id, entry.tool.name);
926
+ for (const connector of absence?.configuredConnectors ?? [])
927
+ onCatalogReference?.(connector);
928
+ // An unknown scope's guidance lists the visible roster without searching it.
929
+ if (reportsQueryAnalysis && args.connector && !scopedConnector)
930
+ for (const connector of visibleConnectors)
931
+ onCatalogReference?.(connector.id);
918
932
  return {
919
933
  catalogErrors,
920
934
  ...(absence ? { absence } : {}),
@@ -74,7 +74,12 @@ export function describeConfigSources(snapshot, raw) {
74
74
  ? provided(raw.pools?.[key]?.readOnly ?? raw.readOnly)
75
75
  : field === "owner"
76
76
  ? provided(raw.pools?.[key]?.owner)
77
- : provided(raw.pools);
77
+ : field === "description"
78
+ ? provided(raw.pools?.[key]?.description)
79
+ : provided(raw.pools);
80
+ else if (group === "modules" && key === "activity" && field === "store" && parts[3] !== "kind")
81
+ // A store option such as retentionDays is set only when the store says so.
82
+ source = factorySource(factorySources(raw.activity?.store), parts.slice(3)) ?? "default";
78
83
  else if (group === "modules") {
79
84
  source = provided(key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens);
80
85
  }
package/dist/config.d.ts CHANGED
@@ -181,10 +181,19 @@ declare const pool: {
181
181
  * The Google Workspace address agents in this pool act as, on a Workspace
182
182
  * connector whose `subject` is a mapping function. Humans always act as
183
183
  * themselves through that mapping, and a fixed `subject` address applies to
184
- * everyone. Without an owner, an agent's Workspace calls fail
185
- * `auth_required`.
184
+ * everyone. Without an owner, a mapping-subject Workspace connector is
185
+ * absent for this pool's agents, as a personal connector is; people in the
186
+ * pool still see it.
186
187
  */
187
188
  owner: import("./config-schema.js").Field<string, string | undefined, true>;
189
+ /**
190
+ * What this pool is for, in plain words for the model: appended to the MCP
191
+ * `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
192
+ * caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
193
+ * 500 characters, no line breaks or control characters. It grants and hides nothing; plain
194
+ * `/mcp` never shows it.
195
+ */
196
+ description: import("./config-schema.js").Field<string, string | undefined, true>;
188
197
  };
189
198
  declare const connectaConfig: {
190
199
  connectors: import("./config-schema.js").Field<Connector[], readonly Connector[], false>;
@@ -210,6 +219,7 @@ declare const connectaConfig: {
210
219
  pools: import("./config-schema.js").Field<Record<string, {
211
220
  readOnly?: boolean | undefined;
212
221
  owner?: string | undefined;
222
+ description?: string | undefined;
213
223
  tools: readonly string[];
214
224
  members: readonly MemberRef[];
215
225
  }>, Readonly<Record<string, {
@@ -217,6 +227,7 @@ declare const connectaConfig: {
217
227
  readonly tools: readonly string[];
218
228
  readonly members: readonly MemberRef[];
219
229
  readonly owner: string | undefined;
230
+ readonly description: string | undefined;
220
231
  }>> | undefined, true>;
221
232
  /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
222
233
  classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;