@zackbart/connecta 0.32.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 (158) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +95 -116
  3. package/SECURITY.md +1 -1
  4. package/bin/connecta.mjs +24 -6
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +167 -23
  7. package/dist/access-tokens.js +676 -140
  8. package/dist/activity.d.ts +3 -0
  9. package/dist/agent-identity.js +47 -0
  10. package/dist/auth/cloudflare-access.js +13 -5
  11. package/dist/branding.d.ts +7 -0
  12. package/dist/branding.js +76 -9
  13. package/dist/call-admission-context.js +15 -0
  14. package/dist/call-admission.js +30 -43
  15. package/dist/call-recovery.js +6 -2
  16. package/dist/catalog-service.js +15 -1
  17. package/dist/config-value-sources.js +16 -9
  18. package/dist/config.d.ts +48 -75
  19. package/dist/config.js +144 -36
  20. package/dist/connector-access.d.ts +4 -34
  21. package/dist/connector-access.js +15 -99
  22. package/dist/connector-caller.d.ts +7 -1
  23. package/dist/connector-caller.js +4 -2
  24. package/dist/connectors/api-connector.d.ts +27 -4
  25. package/dist/connectors/api-connector.js +2 -0
  26. package/dist/connectors/remote-mcp.js +2 -0
  27. package/dist/describe-config.d.ts +31 -7
  28. package/dist/describe-config.js +8 -17
  29. package/dist/execute.js +1 -11
  30. package/dist/fix-prompt.js +2 -2
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +5 -3
  34. package/dist/membership.d.ts +53 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/module-contracts.d.ts +23 -6
  37. package/dist/oauth-callback-outcome.js +2 -2
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -4
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/config-summary.js +562 -0
  43. package/dist/operator-ui/connection-map.js +375 -0
  44. package/dist/operator-ui/contract.d.ts +149 -28
  45. package/dist/operator-ui/endpoints.js +221 -0
  46. package/dist/operator-ui/fix-prompts.js +5 -5
  47. package/dist/operator-ui/generated.js +5 -4
  48. package/dist/operator-ui/member-page.js +97 -0
  49. package/dist/operator-ui/model.d.ts +14 -6
  50. package/dist/operator-ui/overview.js +134 -0
  51. package/dist/operator-ui/view.d.ts +107 -37
  52. package/dist/operator-ui/view.js +163 -86
  53. package/dist/page-styles.js +5 -3
  54. package/dist/provider.d.ts +61 -8
  55. package/dist/provider.js +46 -7
  56. package/dist/providers/_shared/google/workspace.d.ts +56 -17
  57. package/dist/providers/_shared/google/workspace.js +34 -8
  58. package/dist/providers/_shared/rest/tools.js +2 -2
  59. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  60. package/dist/providers/_shared/rest/value-safety.js +699 -7
  61. package/dist/providers/ccb/index.js +2 -2
  62. package/dist/providers/cloudflare/index.js +3 -3
  63. package/dist/providers/cloudflare/rest.js +25 -3
  64. package/dist/providers/cloudflare/skill.generated.js +1 -1
  65. package/dist/providers/cloudflare/value-safety.js +32 -0
  66. package/dist/providers/docs/index.js +4 -4
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.js +4 -4
  69. package/dist/providers/drive/skill.generated.js +2 -2
  70. package/dist/providers/forms/index.js +4 -4
  71. package/dist/providers/forms/skill.generated.js +2 -2
  72. package/dist/providers/gmail/index.js +11 -9
  73. package/dist/providers/gmail/skill.generated.js +2 -2
  74. package/dist/providers/mixpanel/index.js +1 -1
  75. package/dist/providers/mixpanel/skill.generated.js +3 -3
  76. package/dist/providers/notion/index.d.ts +7 -0
  77. package/dist/providers/notion/index.js +100 -11
  78. package/dist/providers/notion/rest.js +45 -5
  79. package/dist/providers/notion/skill.generated.js +2 -2
  80. package/dist/providers/notion/value-safety.js +30 -5
  81. package/dist/providers/overflow/index.js +2 -2
  82. package/dist/providers/overflow/skill.generated.js +2 -2
  83. package/dist/providers/planning-center/index.js +2 -2
  84. package/dist/providers/planning-center/skill.generated.js +4 -4
  85. package/dist/providers/revenuecat/skill.generated.js +2 -2
  86. package/dist/providers/sheets/index.d.ts +3 -0
  87. package/dist/providers/sheets/index.js +7 -4
  88. package/dist/providers/sheets/skill.generated.js +2 -2
  89. package/dist/providers/slides/index.js +4 -4
  90. package/dist/providers/slides/skill.generated.js +2 -2
  91. package/dist/providers/stripe/rest.js +9 -2
  92. package/dist/providers/stripe/skill.generated.js +1 -1
  93. package/dist/providers/stripe/value-safety.js +22 -0
  94. package/dist/providers/tithely/index.js +1 -1
  95. package/dist/providers/vercel/index.js +1 -1
  96. package/dist/providers/vercel/rest.js +20 -2
  97. package/dist/providers/vercel/skill.generated.js +2 -2
  98. package/dist/providers/vercel/value-safety.js +22 -0
  99. package/dist/registry.d.ts +2 -3
  100. package/dist/registry.js +23 -9
  101. package/dist/routes/access-tokens.js +82 -75
  102. package/dist/routes/activity-disclosure.js +17 -18
  103. package/dist/routes/activity.js +34 -54
  104. package/dist/routes/actor-labels.js +33 -4
  105. package/dist/routes/agent-path.js +43 -0
  106. package/dist/routes/connect.js +16 -12
  107. package/dist/routes/credentials.js +16 -40
  108. package/dist/routes/mcp.js +161 -104
  109. package/dist/routes/oauth-management.js +11 -5
  110. package/dist/routes/oauth.js +5 -11
  111. package/dist/routes/operator.js +24 -23
  112. package/dist/routes/shared.d.ts +60 -20
  113. package/dist/routes/shared.js +187 -96
  114. package/dist/routes/ui-config.js +437 -97
  115. package/dist/routes/ui-data.js +2 -2
  116. package/dist/routes/ui.js +9 -16
  117. package/dist/runtime/call-admission.js +9 -0
  118. package/dist/server.js +5 -0
  119. package/dist/skills.js +10 -0
  120. package/dist/storage/keys.js +9 -1
  121. package/dist/storage/sql.js +5 -1
  122. package/dist/types.d.ts +29 -7
  123. package/dist/ui.d.ts +1 -1
  124. package/dist/ui.js +23 -16
  125. package/dist/usage-guide.js +1 -1
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/dist/workspace-address.js +9 -0
  129. package/dist/workspace-subject.js +23 -0
  130. package/documentation/README.md +47 -23
  131. package/documentation/architecture.md +371 -61
  132. package/documentation/auth.md +897 -249
  133. package/documentation/code-mode.md +37 -16
  134. package/documentation/deploying.md +126 -25
  135. package/documentation/integrating.md +97 -23
  136. package/documentation/meta-tools.md +50 -31
  137. package/documentation/migration-0.33.md +352 -0
  138. package/documentation/migration-0.34.md +67 -0
  139. package/documentation/operating.md +3 -1
  140. package/documentation/operator-tests.md +147 -34
  141. package/documentation/operator-ui.md +519 -64
  142. package/documentation/provider-auth-migration.md +37 -23
  143. package/documentation/provider-migration-0.29.md +2 -2
  144. package/examples/worker/AGENTS.md +37 -9
  145. package/examples/worker/README.md +127 -61
  146. package/examples/worker/src/connecta.config.ts +40 -23
  147. package/examples/worker/wrangler.jsonc +4 -1
  148. package/package.json +19 -18
  149. package/templates/node/.env.example +3 -0
  150. package/templates/node/AGENTS.md +33 -6
  151. package/templates/node/Dockerfile +2 -2
  152. package/templates/node/README.md +80 -52
  153. package/templates/node/docker-compose.yml +3 -2
  154. package/templates/node/package.json +4 -3
  155. package/templates/node/src/adopt-token.ts +23 -0
  156. package/templates/node/src/connecta.config.ts +28 -12
  157. package/templates/node/src/rotate-token.ts +17 -0
  158. package/templates/node/src/provision-token.ts +0 -17
package/dist/config.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- import type { ConnectorGrant } from "./connector-access.js";
2
1
  import type { CredentialVault } from "./credential-contract.js";
3
2
  import { type ConfigInput, type ConfigOutput } from "./config-schema.js";
3
+ import type { MemberRef } from "./membership.js";
4
4
  import type { AccessTokensModule, ActivityModule, OperatorSurface } from "./module-contracts.js";
5
- import type { AuthenticatedIdentity, Connector, Executor, IdentityReference, InboundAuth, KVStorage, Logger } from "./types.js";
6
- /** Config-owned identity rules for one deployment and tenant. */
7
- export type ConnectorPermission = "all" | "none" | readonly string[];
5
+ import type { Connector, Executor, InboundAuth, KVStorage, Logger } from "./types.js";
8
6
  declare const discovery: {
9
7
  /**
10
8
  * Maximum connector catalogs/status probes fetched at once by discovery
@@ -162,26 +160,6 @@ declare const admission: {
162
160
  };
163
161
  };
164
162
  };
165
- declare const identity: {
166
- /**
167
- * What this admitted identity may discover and call: `"all"`, or a list
168
- * whose entries are connector ids (the whole connector) and `connector.tool`
169
- * addresses (that tool only), or `{ tool: "connector.tool",
170
- * requireReadOnly: true }` for an exact tool only while its loaded catalog
171
- * explicitly classifies it read-only. Grants are additive: an unrestricted
172
- * connector or address grant wins over the guarded form. An address naming
173
- * a tool the catalog lacks is unreachable and warned once, never widened.
174
- */
175
- connectorAccess: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>, ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined, true>;
176
- /** Global payload-free activity reads. Defaults to interactive humans. */
177
- activityAccess: import("./config-schema.js").Field<(principal: Readonly<IdentityReference>) => boolean | Promise<boolean>, ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined, true>;
178
- /** Shared credential and OAuth administration. Defaults to none. */
179
- credentialAdministration: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
180
- /** Client-token lifecycle management by interactive humans. Defaults to false. */
181
- accessTokenManagement: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
182
- /** Connecting or changing the caller's personal account. Defaults to none. */
183
- personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
184
- };
185
163
  declare const pool: {
186
164
  /**
187
165
  * Whether this pool's endpoint refuses every write: write tools are absent
@@ -193,66 +171,63 @@ declare const pool: {
193
171
  /** Connector ids and exact `connector.tool` addresses in this pool. */
194
172
  tools: import("./config-schema.js").Field<readonly string[], readonly string[], false>;
195
173
  /**
196
- * Whether this admitted identity may open the pool. Denied by default:
197
- * a pool with no grant serves nobody. A false return, a throw, and an
198
- * undeclared pool name are the same 404.
174
+ * Who may open `/mcp/<name>`: principal ids in the namespace of the one
175
+ * interactive auth provider, `{ namespace, id }`, or `"agent:<name>"` for a
176
+ * declared agent. Required and non-empty; the pool serves exactly its
177
+ * `tools` to these members and the same 404 to everyone else.
178
+ */
179
+ members: import("./config-schema.js").Field<readonly MemberRef[], readonly MemberRef[], false>;
180
+ /**
181
+ * The Google Workspace address agents in this pool act as, on a Workspace
182
+ * connector whose `subject` is a mapping function. Humans always act as
183
+ * themselves through that mapping, and a fixed `subject` address applies to
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.
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.
199
195
  */
200
- grant: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
196
+ description: import("./config-schema.js").Field<string, string | undefined, true>;
201
197
  };
202
198
  declare const connectaConfig: {
203
199
  connectors: import("./config-schema.js").Field<Connector[], readonly Connector[], false>;
204
200
  /** Inbound auth adapters, such as clerkAuth(...); omit for open (dev). */
205
201
  auth: import("./config-schema.js").Field<InboundAuth | InboundAuth[], readonly InboundAuth[], true>;
206
- /** Code-derived connection visibility and independent management permissions. */
207
- identity: import("./config-schema.js").Field<{
208
- connectorAccess?: ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined;
209
- activityAccess?: ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined;
210
- credentialAdministration?: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
211
- accessTokenManagement?: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
212
- personalConnection?: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
213
- }, {
214
- readonly connectorAccess: ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined;
215
- readonly activityAccess: ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined;
216
- readonly credentialAdministration: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
217
- readonly accessTokenManagement: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
218
- readonly personalConnection: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
219
- }, true> & {
220
- readonly shape: {
221
- /**
222
- * What this admitted identity may discover and call: `"all"`, or a list
223
- * whose entries are connector ids (the whole connector) and `connector.tool`
224
- * addresses (that tool only), or `{ tool: "connector.tool",
225
- * requireReadOnly: true }` for an exact tool only while its loaded catalog
226
- * explicitly classifies it read-only. Grants are additive: an unrestricted
227
- * connector or address grant wins over the guarded form. An address naming
228
- * a tool the catalog lacks is unreachable and warned once, never widened.
229
- */
230
- connectorAccess: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>, ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined, true>;
231
- /** Global payload-free activity reads. Defaults to interactive humans. */
232
- activityAccess: import("./config-schema.js").Field<(principal: Readonly<IdentityReference>) => boolean | Promise<boolean>, ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined, true>;
233
- /** Shared credential and OAuth administration. Defaults to none. */
234
- credentialAdministration: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
235
- /** Client-token lifecycle management by interactive humans. Defaults to false. */
236
- accessTokenManagement: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
237
- /** Connecting or changing the caller's personal account. Defaults to none. */
238
- personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
239
- };
240
- };
241
202
  /**
242
- * Whether this admitted identity may open plain `/mcp`. Omitted admits
243
- * everyone; only literal true admits when configured. Other returns and
244
- * throws receive the same 404 as a refused or undeclared pool.
203
+ * Agents, declared by name: lowercase letters, digits, `_` and `-`, each
204
+ * unique. Each has one `cta_` token slot that operators issue, rotate, and
205
+ * revoke; nothing outside code adds, renames, or rebinds an agent. Removing
206
+ * a name stops its token on the next request. Requires `accessTokens`.
207
+ * An admitted agent carries `identity.agent` and has no principal, so
208
+ * personal connectors are absent for it. Defaults to none.
209
+ */
210
+ agents: import("./config-schema.js").Field<readonly string[], readonly string[], true>;
211
+ /**
212
+ * The people who may use plain `/mcp`, which serves the whole catalog with
213
+ * the deployment's `readOnly`, and who administer the deployment. Same
214
+ * reference forms as pool `members`, people only. Required with inbound
215
+ * auth; a maintainer opens no pool unless listed among its members.
245
216
  */
246
- grant: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
247
- /** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
217
+ maintainers: import("./config-schema.js").Field<readonly MemberRef[], readonly MemberRef[] | undefined, true>;
218
+ /** Named tool pools, each served at `/mcp/<name>` to its members only. */
248
219
  pools: import("./config-schema.js").Field<Record<string, {
249
220
  readOnly?: boolean | undefined;
250
- grant?: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
221
+ owner?: string | undefined;
222
+ description?: string | undefined;
251
223
  tools: readonly string[];
224
+ members: readonly MemberRef[];
252
225
  }>, Readonly<Record<string, {
253
226
  readonly readOnly: boolean | undefined;
254
227
  readonly tools: readonly string[];
255
- readonly grant: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
228
+ readonly members: readonly MemberRef[];
229
+ readonly owner: string | undefined;
230
+ readonly description: string | undefined;
256
231
  }>> | undefined, true>;
257
232
  /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
258
233
  classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;
@@ -281,7 +256,7 @@ declare const connectaConfig: {
281
256
  vault: import("./config-schema.js").Field<CredentialVault, CredentialVault | undefined, true>;
282
257
  /** Optional connection UI, created by operatorUi() from /ui. */
283
258
  ui: import("./config-schema.js").Field<OperatorSurface, OperatorSurface | undefined, true>;
284
- /** Optional managed client tokens, created by accessTokens() from /auth/access-tokens. */
259
+ /** Optional agent tokens for the declared `agents`, created by accessTokens() from /auth/access-tokens. */
285
260
  accessTokens: import("./config-schema.js").Field<AccessTokensModule, AccessTokensModule | undefined, true>;
286
261
  /** Tool-catalog TTL bounds and probe deadlines. */
287
262
  discovery: import("./config-schema.js").Field<{
@@ -581,11 +556,9 @@ export type AdmissionPoolConfig = ConfigInput<typeof codeAdmission>;
581
556
  export type RequestAdmissionConfig = ConfigInput<typeof requestAdmission>;
582
557
  /** Runtime-portable server-memory boundaries for `/mcp` and fallback code-mode work. */
583
558
  export type ConnectaAdmissionConfig = ConfigInput<typeof admission>;
584
- export type ConnectaIdentityConfig = ConfigInput<typeof identity>;
585
559
  /**
586
- * A named tool pool served at `/mcp/<name>`. The pool is the slice a client
587
- * pointed at that endpoint may see; the identity's own `connectorAccess`
588
- * remains its ceiling and the pool can only narrow it.
560
+ * A named tool pool served at `/mcp/<name>`: exactly its `tools`, to its
561
+ * `members` only.
589
562
  */
590
563
  export type ConnectaPoolConfig = ConfigInput<typeof pool>;
591
564
  type Parsed = ConfigOutput<typeof connectaConfig>;
package/dist/config.js CHANGED
@@ -12,10 +12,12 @@
12
12
  // reads, with defaults applied once, so no later layer re-derives a value or
13
13
  // re-plumbs a field.
14
14
  import { CONFIG_DEFAULTS as D } from "./config-defaults.js";
15
- import { array, ConfigError, fn, object, opaque, record, required, seconds, readPlain, text, whole, } from "./config-schema.js";
15
+ import { array, ConfigError, object, opaque, record, required, seconds, readPlain, text, whole, } from "./config-schema.js";
16
+ import { assertAgentNames } from "./agent-identity.js";
16
17
  import { assertExecutor } from "./executor-contract.js";
17
18
  import { memoryStorage } from "./storage/memory.js";
18
19
  import { CONNECTA_VERSION } from "./version.js";
20
+ import { isWorkspaceAddress } from "./workspace-address.js";
19
21
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
20
22
  const STORAGE_METHODS = ["get", "set", "delete", "list", "compareAndSet"];
21
23
  const hasMethods = (value, methods) => isObject(value) && methods.every((method) => typeof value[method] === "function");
@@ -175,26 +177,6 @@ const admission = {
175
177
  */
176
178
  code: object(codeAdmission),
177
179
  };
178
- const identity = {
179
- /**
180
- * What this admitted identity may discover and call: `"all"`, or a list
181
- * whose entries are connector ids (the whole connector) and `connector.tool`
182
- * addresses (that tool only), or `{ tool: "connector.tool",
183
- * requireReadOnly: true }` for an exact tool only while its loaded catalog
184
- * explicitly classifies it read-only. Grants are additive: an unrestricted
185
- * connector or address grant wins over the guarded form. An address naming
186
- * a tool the catalog lacks is unreachable and warned once, never widened.
187
- */
188
- connectorAccess: fn(),
189
- /** Global payload-free activity reads. Defaults to interactive humans. */
190
- activityAccess: fn(),
191
- /** Shared credential and OAuth administration. Defaults to none. */
192
- credentialAdministration: fn(),
193
- /** Client-token lifecycle management by interactive humans. Defaults to false. */
194
- accessTokenManagement: fn(),
195
- /** Connecting or changing the caller's personal account. Defaults to none. */
196
- personalConnection: fn(),
197
- };
198
180
  /** The choice every deployment makes explicitly while `trust` migrates (#820). */
199
181
  const READ_ONLY_REQUIRED = (path) => `${path} is required. Choose readOnly: true to refuse every write on /mcp, or readOnly: false to let ` +
200
182
  "call_destructive_tool and programs write with MCP client approval. Pools inherit it unless they set their own.";
@@ -202,6 +184,37 @@ function assertFlag(value, path) {
202
184
  if (typeof value !== "boolean")
203
185
  throw new ConfigError(`${path} must be true or false.`);
204
186
  }
187
+ /** Longest pool `description`, in characters, after trimming. */
188
+ const MAX_POOL_DESCRIPTION_LENGTH = 500;
189
+ /**
190
+ * A pool description is one line of prose for the model. C0 and C1 controls
191
+ * (including NEL), plus Unicode line and paragraph separators, are refused.
192
+ * A scan, as in remote-mcp.ts,
193
+ * because a character class over the control range trips `no-control-regex`.
194
+ */
195
+ function hasLineBreakOrControlCharacter(value) {
196
+ for (let index = 0; index < value.length; index++) {
197
+ const code = value.charCodeAt(index);
198
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029)
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+ function assertPoolDescription(value, path) {
204
+ const trimmed = typeof value === "string" ? value.trim() : undefined;
205
+ if (trimmed === undefined ||
206
+ trimmed.length === 0 ||
207
+ [...trimmed].length > MAX_POOL_DESCRIPTION_LENGTH ||
208
+ hasLineBreakOrControlCharacter(trimmed)) {
209
+ throw new ConfigError(`${path} must be text of 1 to ${MAX_POOL_DESCRIPTION_LENGTH} characters after trimming, with no line breaks or control characters.`);
210
+ }
211
+ }
212
+ /** Shape only; each reference resolves against auth and agents in createConnecta. */
213
+ function assertMemberList(value, path) {
214
+ if (!Array.isArray(value)) {
215
+ throw new ConfigError(`${path} must be an array of principal ids, { namespace, id }, or "agent:<name>".`);
216
+ }
217
+ }
205
218
  const pool = {
206
219
  /**
207
220
  * Whether this pool's endpoint refuses every write: write tools are absent
@@ -219,11 +232,35 @@ const pool = {
219
232
  },
220
233
  })),
221
234
  /**
222
- * Whether this admitted identity may open the pool. Denied by default:
223
- * a pool with no grant serves nobody. A false return, a throw, and an
224
- * undeclared pool name are the same 404.
235
+ * Who may open `/mcp/<name>`: principal ids in the namespace of the one
236
+ * interactive auth provider, `{ namespace, id }`, or `"agent:<name>"` for a
237
+ * declared agent. Required and non-empty; the pool serves exactly its
238
+ * `tools` to these members and the same 404 to everyone else.
239
+ */
240
+ members: required(opaque({ check: assertMemberList }), (path) => `${path} is required: list who may open this pool, as principal ids, { namespace, id }, or "agent:<name>".`),
241
+ /**
242
+ * The Google Workspace address agents in this pool act as, on a Workspace
243
+ * connector whose `subject` is a mapping function. Humans always act as
244
+ * themselves through that mapping, and a fixed `subject` address applies to
245
+ * everyone. Without an owner, a mapping-subject Workspace connector is
246
+ * absent for this pool's agents, as a personal connector is; people in the
247
+ * pool still see it.
248
+ */
249
+ owner: opaque({
250
+ check: (value, path) => {
251
+ if (typeof value !== "string" || !isWorkspaceAddress(value.trim())) {
252
+ throw new ConfigError(`${path} must be a Google Workspace email address.`);
253
+ }
254
+ },
255
+ }),
256
+ /**
257
+ * What this pool is for, in plain words for the model: appended to the MCP
258
+ * `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
259
+ * caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
260
+ * 500 characters, no line breaks or control characters. It grants and hides nothing; plain
261
+ * `/mcp` never shows it.
225
262
  */
226
- grant: fn(),
263
+ description: opaque({ check: assertPoolDescription }),
227
264
  };
228
265
  const icon = {
229
266
  src: required(text()),
@@ -276,15 +313,26 @@ const connectaConfig = {
276
313
  },
277
314
  resolve: (value) => normalizeAuth(value === undefined ? [] : Array.isArray(value) ? value : [value]),
278
315
  }),
279
- /** Code-derived connection visibility and independent management permissions. */
280
- identity: object(identity),
281
316
  /**
282
- * Whether this admitted identity may open plain `/mcp`. Omitted admits
283
- * everyone; only literal true admits when configured. Other returns and
284
- * throws receive the same 404 as a refused or undeclared pool.
317
+ * Agents, declared by name: lowercase letters, digits, `_` and `-`, each
318
+ * unique. Each has one `cta_` token slot that operators issue, rotate, and
319
+ * revoke; nothing outside code adds, renames, or rebinds an agent. Removing
320
+ * a name stops its token on the next request. Requires `accessTokens`.
321
+ * An admitted agent carries `identity.agent` and has no principal, so
322
+ * personal connectors are absent for it. Defaults to none.
323
+ */
324
+ agents: opaque({
325
+ check: (value, path) => assertAgentNames(value, path),
326
+ resolve: (value) => Object.freeze([...(value ?? [])]),
327
+ }),
328
+ /**
329
+ * The people who may use plain `/mcp`, which serves the whole catalog with
330
+ * the deployment's `readOnly`, and who administer the deployment. Same
331
+ * reference forms as pool `members`, people only. Required with inbound
332
+ * auth; a maintainer opens no pool unless listed among its members.
285
333
  */
286
- grant: fn(),
287
- /** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
334
+ maintainers: opaque({ check: assertMemberList }),
335
+ /** Named tool pools, each served at `/mcp/<name>` to its members only. */
288
336
  pools: record(object(pool)),
289
337
  /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
290
338
  classification: record(record(opaque({
@@ -364,11 +412,11 @@ const connectaConfig = {
364
412
  }
365
413
  },
366
414
  }),
367
- /** Optional managed client tokens, created by accessTokens() from /auth/access-tokens. */
415
+ /** Optional agent tokens for the declared `agents`, created by accessTokens() from /auth/access-tokens. */
368
416
  accessTokens: opaque({
369
417
  check: (value) => {
370
418
  const auth = isObject(value) ? value.auth : undefined;
371
- if (!hasMethods(value, ["handle"]) ||
419
+ if (!hasMethods(value, ["handle", "slots", "forAgents"]) ||
372
420
  !isObject(auth) ||
373
421
  typeof auth.authorize !== "function" ||
374
422
  auth.interactiveOperator) {
@@ -436,8 +484,46 @@ const connectaConfig = {
436
484
  const CONFIG = object(connectaConfig);
437
485
  /** Pause-only options #672 removed with `resume_execution`. */
438
486
  const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
487
+ /** Access options pools replaced (#837, decisions/0006). */
488
+ const isRetiredPoolGrant = (path) => /^ConnectaConfig\.pools\.[^.]+\.grant$/.test(path);
489
+ /** Management resolvers maintainers and pool membership replaced (#837, decisions/0006). */
490
+ const RETIRED_IDENTITY = [
491
+ ["credentialAdministration", "maintainers administer shared credentials and OAuth grants. List them in maintainers."],
492
+ [
493
+ "personalConnection",
494
+ "pool members connect and disconnect their own personal connectors in their pools, and maintainers their own. Add people to a pool's members.",
495
+ ],
496
+ [
497
+ "accessTokenManagement",
498
+ "maintainers issue, rotate, and revoke agent tokens from an interactive session. List them in maintainers.",
499
+ ],
500
+ ["activityAccess", "maintainers read activity; activity.readGate can still narrow it. List them in maintainers."],
501
+ ];
439
502
  /** `trust`, on the deployment or a pool, was replaced by `readOnly` (#820). */
440
503
  const isRetiredTrust = (path) => path === "ConnectaConfig.trust" || (path.startsWith("ConnectaConfig.pools.") && path.endsWith(".trust"));
504
+ /**
505
+ * `identity` was removed whole (#837), so the walk reports only the section.
506
+ * Name each key it still holds, read by descriptor without running a getter
507
+ * or echoing a value, so a retired resolver's message names its replacement.
508
+ */
509
+ function expandRetiredIdentity(config, paths) {
510
+ if (!paths.includes("ConnectaConfig.identity"))
511
+ return paths;
512
+ let keys = [];
513
+ try {
514
+ const descriptor = Reflect.getOwnPropertyDescriptor(config, "identity");
515
+ const value = descriptor && "value" in descriptor ? descriptor.value : undefined;
516
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
517
+ keys = Reflect.ownKeys(value).filter((key) => typeof key === "string");
518
+ }
519
+ }
520
+ catch {
521
+ keys = [];
522
+ }
523
+ if (keys.length === 0)
524
+ return paths;
525
+ return paths.flatMap((path) => path === "ConnectaConfig.identity" ? keys.map((key) => `ConnectaConfig.identity.${key}`) : [path]);
526
+ }
441
527
  function rejectUnknownOptions(paths) {
442
528
  if (paths.length === 0)
443
529
  return;
@@ -455,6 +541,21 @@ function rejectUnknownOptions(paths) {
455
541
  (paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
456
542
  ? "\nPrograms no longer pause at writes, so there is nothing to configure: writable endpoints let programs and call_destructive_tool write; readOnly endpoints refuse every write (issue #672). Delete the option."
457
543
  : "") +
544
+ (paths.includes("ConnectaConfig.identity.connectorAccess")
545
+ ? "\nidentity.connectorAccess was removed (#837): pools are the access model. List who may use plain /mcp in maintainers, and give each pool its tools and members. A guarded { tool, requireReadOnly: true } grant becomes a pool with readOnly: true."
546
+ : "") +
547
+ RETIRED_IDENTITY.filter(([key]) => paths.includes(`ConnectaConfig.identity.${key}`))
548
+ .map(([key, replacement]) => `\nidentity.${key} was removed (#837): ${replacement}`)
549
+ .join("") +
550
+ (paths.some((path) => path === "ConnectaConfig.identity" || path.startsWith("ConnectaConfig.identity."))
551
+ ? "\nThe identity section was removed (#837): maintainers and pool members decide access and management. Delete it."
552
+ : "") +
553
+ (paths.includes("ConnectaConfig.grant")
554
+ ? "\nThe top-level grant was removed (#837); list the people who may use plain /mcp in maintainers."
555
+ : "") +
556
+ (paths.some(isRetiredPoolGrant)
557
+ ? "\nPool grant was removed (#837); list who may open the pool in its members."
558
+ : "") +
458
559
  (paths.some(isRetiredTrust)
459
560
  ? '\ntrust was removed (#820); set readOnly, which every deployment now chooses explicitly. trust: "read-only" becomes readOnly: true, which now also refuses call_destructive_tool. trust: "trusted" becomes readOnly: false. A pool inherits the deployment readOnly unless it sets its own.'
460
561
  : ""));
@@ -465,6 +566,10 @@ function assertCoherent(config) {
465
566
  config.discovery.catalogMaxTtlSeconds > 86_400) {
466
567
  throw new ConfigError("ConnectaConfig.discovery requires catalogMinTtlSeconds <= catalogMaxTtlSeconds <= 86400.");
467
568
  }
569
+ if (config.agents.length > 0 && !config.accessTokens) {
570
+ throw new ConfigError("ConnectaConfig.agents needs accessTokens: accessTokens(storage) from @zackbart/connecta/auth/access-tokens " +
571
+ "holds each agent's token slot.");
572
+ }
468
573
  }
469
574
  /**
470
575
  * Validate a configuration and apply every default. Unknown keys are refused
@@ -483,7 +588,7 @@ export function resolveConfig(config) {
483
588
  */
484
589
  export function readConfig(config) {
485
590
  const read = readPlain(config, "ConnectaConfig", CONFIG);
486
- rejectUnknownOptions(read.unknown);
591
+ rejectUnknownOptions(expandRetiredIdentity(config, read.unknown));
487
592
  const input = read.value;
488
593
  const parsed = CONFIG.parse(input, "ConnectaConfig");
489
594
  assertCoherent(parsed);
@@ -492,10 +597,13 @@ export function readConfig(config) {
492
597
  name: parsed.serverInfo.name ?? "connecta",
493
598
  version: parsed.serverInfo.version ?? CONNECTA_VERSION,
494
599
  };
600
+ // The token module admits exactly the agents this configuration declares.
601
+ const accessTokens = parsed.accessTokens?.forAgents(parsed.agents);
495
602
  const resolved = Object.freeze({
496
603
  ...parsed,
604
+ accessTokens,
497
605
  // A managed-token module contributes one more provider, ordered with the rest.
498
- auth: parsed.accessTokens ? normalizeAuth([parsed.accessTokens.auth, ...parsed.auth]) : parsed.auth,
606
+ auth: accessTokens ? normalizeAuth([accessTokens.auth, ...parsed.auth]) : parsed.auth,
499
607
  serverInfo: Object.freeze(info),
500
608
  });
501
609
  return { input, resolved };
@@ -1,48 +1,18 @@
1
1
  import type { ToolAccess } from "./registry.js";
2
- import type { AuthenticatedIdentity } from "./types.js";
3
- /** A declared pool after construction-time validation. */
4
- export interface ResolvedPool {
5
- /** Resolved: the pool's own `readOnly`, else the deployment's. */
6
- readOnly: boolean;
7
- access: ConnectorAccess;
8
- grant(identity: Readonly<AuthenticatedIdentity>): boolean | Promise<boolean>;
9
- }
10
2
  export declare const POOL_NAME_RE: RegExp;
11
3
  /**
12
4
  * One derived view: which connectors, and for connectors granted by address
13
5
  * only, which tools. A connector absent from `toolAccess` is visible whole.
14
- * `guardedToolAccess` is a subset of `toolAccess` checked against the loaded
15
- * catalog each time tools are discovered or invoked.
16
6
  */
17
7
  export interface ConnectorAccess {
18
8
  connectorIds: "all" | readonly string[];
19
9
  toolAccess?: ToolAccess;
20
- /** Exact tool grants that also require a current explicit read-only annotation. */
21
- guardedToolAccess?: ToolAccess;
22
- }
23
- interface ReadOnlyToolGrant {
24
- tool: string;
25
- requireReadOnly: true;
26
10
  }
27
- export type ConnectorGrant = string | ReadOnlyToolGrant;
28
11
  /**
29
- * Normalize a grant list. A bare connector id grants every tool on that
12
+ * Normalize a pool's `tools`. A bare connector id grants every tool on that
30
13
  * connector; a `connector.tool` address grants one tool. Grants are additive,
31
14
  * so a bare id beside addresses for the same connector means the whole
32
- * connector. Identity resolution can opt into `{ tool, requireReadOnly: true }`
33
- * for a fixed address that also needs a current explicit read-only annotation;
34
- * pool declarations remain strings. Anything else throws, and the caller
35
- * decides whether that is a construction failure or a 403: a grant that
36
- * cannot be parsed must never fail open.
37
- */
38
- export declare function parseConnectorAccess(value: unknown, options?: {
39
- allowReadOnly?: boolean;
40
- }): ConnectorAccess;
41
- /**
42
- * The view a pool endpoint serves: the pool's grants, never wider than the
43
- * identity's own. A connector or tool outside either side is gone; a
44
- * connector whose tool intersection is empty is gone too, so the pool can
45
- * only narrow what the identity resolver already allowed.
15
+ * connector. Anything else throws: a grant that cannot be parsed must never
16
+ * fail open.
46
17
  */
47
- export declare function intersectAccess(ceiling: ConnectorAccess, pool: ConnectorAccess): ConnectorAccess;
48
- export {};
18
+ export declare function parseConnectorAccess(value: unknown): ConnectorAccess;
@@ -2,121 +2,37 @@ import { hasControlCharacters } from "./tool-name.js";
2
2
  export const POOL_NAME_RE = /^[a-z0-9_-]+$/;
3
3
  const CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;
4
4
  // MCP does not restrict tool names, and remote servers ship spaced and
5
- // non-ASCII ones. Only control characters are refused, so a grant for a
6
- // legitimately named tool cannot 403 the whole identity at request time.
5
+ // non-ASCII ones. Only control characters are refused, so a pool naming a
6
+ // legitimately named tool still constructs.
7
7
  const TOOL_ADDRESS_RE = /^[a-z0-9_-]+\..{1,256}$/su;
8
8
  /**
9
- * Normalize a grant list. A bare connector id grants every tool on that
9
+ * Normalize a pool's `tools`. A bare connector id grants every tool on that
10
10
  * connector; a `connector.tool` address grants one tool. Grants are additive,
11
11
  * so a bare id beside addresses for the same connector means the whole
12
- * connector. Identity resolution can opt into `{ tool, requireReadOnly: true }`
13
- * for a fixed address that also needs a current explicit read-only annotation;
14
- * pool declarations remain strings. Anything else throws, and the caller
15
- * decides whether that is a construction failure or a 403: a grant that
16
- * cannot be parsed must never fail open.
12
+ * connector. Anything else throws: a grant that cannot be parsed must never
13
+ * fail open.
17
14
  */
18
- export function parseConnectorAccess(value, options = {}) {
19
- if (value === "all")
20
- return { connectorIds: "all" };
15
+ export function parseConnectorAccess(value) {
21
16
  if (!Array.isArray(value))
22
17
  throw new Error("invalid connector permission");
23
18
  const whole = new Set();
24
- const plain = new Map();
25
- const guarded = new Map();
26
- for (const entry of value) {
27
- if (typeof entry === "string" && CONNECTOR_ID_RE.test(entry)) {
28
- whole.add(entry);
19
+ const partial = new Map();
20
+ for (const address of value) {
21
+ if (typeof address === "string" && CONNECTOR_ID_RE.test(address)) {
22
+ whole.add(address);
29
23
  continue;
30
24
  }
31
- const isGuarded = typeof entry !== "string";
32
- if (isGuarded &&
33
- (!options.allowReadOnly ||
34
- entry === null ||
35
- typeof entry !== "object" ||
36
- Array.isArray(entry) ||
37
- !Object.hasOwn(entry, "tool") ||
38
- !Object.hasOwn(entry, "requireReadOnly") ||
39
- Object.keys(entry).length !== 2 ||
40
- entry.requireReadOnly !== true)) {
41
- throw new Error("invalid connector permission");
42
- }
43
- const address = isGuarded ? entry.tool : entry;
44
25
  if (typeof address !== "string" || !TOOL_ADDRESS_RE.test(address) || hasControlCharacters(address)) {
45
26
  throw new Error("invalid connector permission");
46
27
  }
47
28
  const dot = address.indexOf(".");
48
29
  const connectorId = address.slice(0, dot);
49
- const target = isGuarded ? guarded : plain;
50
- const tools = target.get(connectorId) ?? new Set();
30
+ const tools = partial.get(connectorId) ?? new Set();
51
31
  tools.add(address.slice(dot + 1));
52
- target.set(connectorId, tools);
53
- }
54
- for (const id of whole) {
55
- plain.delete(id);
56
- guarded.delete(id);
57
- }
58
- for (const [id, names] of plain) {
59
- const restricted = guarded.get(id);
60
- if (!restricted)
61
- continue;
62
- for (const name of names)
63
- restricted.delete(name);
64
- if (restricted.size === 0)
65
- guarded.delete(id);
66
- }
67
- const partial = new Map(plain);
68
- for (const [id, names] of guarded) {
69
- const combined = partial.get(id) ?? new Set();
70
- for (const name of names)
71
- combined.add(name);
72
- partial.set(id, combined);
32
+ partial.set(connectorId, tools);
73
33
  }
34
+ for (const id of whole)
35
+ partial.delete(id);
74
36
  const connectorIds = [...new Set([...whole, ...partial.keys()])];
75
- return {
76
- connectorIds,
77
- ...(partial.size > 0 ? { toolAccess: partial } : {}),
78
- ...(guarded.size > 0 ? { guardedToolAccess: guarded } : {}),
79
- };
80
- }
81
- /**
82
- * The view a pool endpoint serves: the pool's grants, never wider than the
83
- * identity's own. A connector or tool outside either side is gone; a
84
- * connector whose tool intersection is empty is gone too, so the pool can
85
- * only narrow what the identity resolver already allowed.
86
- */
87
- export function intersectAccess(ceiling, pool) {
88
- if (pool.connectorIds === "all")
89
- return ceiling;
90
- const allowedIds = ceiling.connectorIds === "all" ? null : new Set(ceiling.connectorIds);
91
- const connectorIds = [];
92
- const toolAccess = new Map();
93
- const guardedToolAccess = new Map();
94
- for (const id of pool.connectorIds) {
95
- if (allowedIds && !allowedIds.has(id))
96
- continue;
97
- const fromPool = pool.toolAccess?.get(id);
98
- const fromCeiling = ceiling.toolAccess?.get(id);
99
- if (fromPool && fromCeiling) {
100
- const both = new Set([...fromPool].filter((name) => fromCeiling.has(name)));
101
- if (both.size === 0)
102
- continue;
103
- toolAccess.set(id, both);
104
- }
105
- else if (fromPool ?? fromCeiling) {
106
- toolAccess.set(id, (fromPool ?? fromCeiling));
107
- }
108
- const guarded = ceiling.guardedToolAccess?.get(id);
109
- if (guarded) {
110
- const allowed = toolAccess.get(id);
111
- const retained = allowed ? new Set([...guarded].filter((name) => allowed.has(name))) : guarded;
112
- if (retained.size > 0)
113
- guardedToolAccess.set(id, retained);
114
- }
115
- connectorIds.push(id);
116
- }
117
- return {
118
- connectorIds,
119
- ...(toolAccess.size > 0 ? { toolAccess } : {}),
120
- ...(guardedToolAccess.size > 0 ? { guardedToolAccess } : {}),
121
- };
37
+ return { connectorIds, ...(partial.size > 0 ? { toolAccess: partial } : {}) };
122
38
  }