@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
@@ -9,8 +9,14 @@ export interface ConnectorCaller {
9
9
  * with or without a subject, a Connecta-issued access token.
10
10
  */
11
11
  authenticated: boolean;
12
- /** The `/mcp/<pool>` the call arrived on, when it was a pool. */
12
+ /** The pool the call arrived on, at `/mcp/<pool>` or `/agent/<pool>`. */
13
13
  pool?: string;
14
+ /**
15
+ * That pool's configured `owner`, the Workspace address an agent acts as
16
+ * (decisions/0006). Core copies it from the admitted pool's config; no
17
+ * request can set it, and humans never act as it.
18
+ */
19
+ poolOwner?: string;
14
20
  }
15
21
  export declare function attachCaller(ctx: ConnectorContext, caller: ConnectorCaller | undefined): ConnectorContext;
16
22
  export declare function callerOf(ctx: ConnectorContext): ConnectorCaller | undefined;
@@ -8,8 +8,10 @@
8
8
  // - maintained providers that act *as* the caller downstream — Google
9
9
  // Workspace through domain-wide delegation (`src/providers/_shared/google/`) — which
10
10
  // hand the identity to a deployment-config function that names the
11
- // downstream subject (PRINCIPLES.md INV-3, #678). The
12
- // provider never decides whose account to open; config does, from this.
11
+ // downstream subject (PRINCIPLES.md INV-3, #678), or, for an agent, use the
12
+ // admitted pool's configured owner (decisions/0006), and the scoped registry
13
+ // view hides the connector from an agent whose pool has none. The provider never
14
+ // decides whose account to open; config does, from this.
13
15
  //
14
16
  // Core sets it from the authorization that admitted the request, on the scoped
15
17
  // registry view that request receives, and `api()` hands handlers that same
@@ -146,10 +146,33 @@ export interface ApiOptions {
146
146
  /** The closed options api() accepts; see `assertKnownOptions`. */
147
147
  export declare const API_OPTIONS: import("../config-schema.js").Field<unknown, unknown, true> & {
148
148
  readonly shape: {
149
- callAdmission: import("../config-schema.js").Field<unknown, unknown, true> & {
150
- readonly shape: {
151
- rules: import("../config-schema.js").Field<unknown[], readonly unknown[] | undefined, true>;
152
- maxPartitions: import("../config-schema.js").Field<unknown, unknown, true>;
149
+ callAdmission: Omit<import("../config-schema.js").Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
150
+ parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
151
+ readonly shape: import("../config-schema.js").Shape & {
152
+ maxPartitions: import("../config-schema.js").Field<number, number | undefined, true>;
153
+ rules: import("../config-schema.js").Field<{
154
+ maxConcurrency?: number | undefined;
155
+ maxQueueSize?: number | undefined;
156
+ queueTimeoutMs?: number | undefined;
157
+ retryAfterMs?: number | undefined;
158
+ partitionKey?: ((input: Readonly<import("../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
159
+ budget?: {
160
+ kind: "rolling-window";
161
+ maxCalls: number;
162
+ windowMs: number;
163
+ } | undefined;
164
+ }[], readonly {
165
+ readonly maxConcurrency: number | undefined;
166
+ readonly maxQueueSize: number | undefined;
167
+ readonly queueTimeoutMs: number | undefined;
168
+ readonly retryAfterMs: number | undefined;
169
+ readonly partitionKey: ((input: Readonly<import("../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
170
+ readonly budget: {
171
+ readonly kind: "rolling-window";
172
+ readonly maxCalls: number;
173
+ readonly windowMs: number;
174
+ } | undefined;
175
+ }[], false>;
153
176
  };
154
177
  };
155
178
  usageGuide: import("../config-schema.js").Field<unknown, unknown, true> & {
@@ -90,6 +90,8 @@ function describedApiAuth(opts) {
90
90
  */
91
91
  export function apiConnector(id, opts, oauth) {
92
92
  opts = assertKnownOptions(opts, `api(${JSON.stringify(id)})`, API_OPTIONS);
93
+ if (opts.callAdmission !== undefined)
94
+ CALL_ADMISSION.parse(opts.callAdmission, `api(${JSON.stringify(id)}).callAdmission`);
93
95
  assertStaticToolNames(opts.tools, `api(${JSON.stringify(id)}).tools`);
94
96
  if (opts.oauth !== undefined && oauth === undefined) {
95
97
  throw new Error(`api() connector "${id}" declares oauth but was built without its grant; construct it with api().`);
@@ -1024,6 +1024,8 @@ function registrationErrorCode(body) {
1024
1024
  */
1025
1025
  export function remoteMcp(id, opts) {
1026
1026
  opts = assertKnownOptions(opts, `remoteMcp(${JSON.stringify(id)})`, REMOTE_MCP_OPTIONS);
1027
+ if (opts.callAdmission !== undefined)
1028
+ CALL_ADMISSION.parse(opts.callAdmission, `remoteMcp(${JSON.stringify(id)}).callAdmission`);
1027
1029
  const skillsEnabled = opts.skills;
1028
1030
  if (skillsEnabled !== undefined && typeof skillsEnabled !== "boolean") {
1029
1031
  throw new Error(`[connecta] connector "${id}" skills must be a boolean.`);
@@ -1,6 +1,7 @@
1
1
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
2
2
  import type { ConnectaConfig, ResolvedConfig } from "./config.js";
3
- import type { ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, DescribedEndpoint } from "./types.js";
3
+ import type { ResolvedPool } from "./membership.js";
4
+ import type { ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, DescribedEndpoint, IdentityReference } from "./types.js";
4
5
  /** Where a limit's value came from. */
5
6
  export type ConfigValueSource = "default" | "config";
6
7
  /** One limit and where its value came from. `null` means unset (no bound). */
@@ -8,6 +9,15 @@ export interface DescribedLimit<T = number> {
8
9
  value: T | null;
9
10
  source: ConfigValueSource;
10
11
  }
12
+ /** A resolved pool member: a principal, or a declared agent by name. */
13
+ export type DescribedMember = {
14
+ kind: "human";
15
+ namespace: string;
16
+ id: string;
17
+ } | {
18
+ kind: "agent";
19
+ name: string;
20
+ };
11
21
  /** One connector as `describeConfig()` reports it. */
12
22
  export interface DescribedConnector {
13
23
  id: string;
@@ -88,8 +98,11 @@ export interface ConnectaConfigDescription {
88
98
  };
89
99
  /** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
90
100
  readOnly: boolean;
91
- /** Whether deployment code configures a grant for plain `/mcp`. */
92
- hasGrant: boolean;
101
+ /** The principals plain `/mcp` admits, resolved, in declaration order. Empty on an open deployment. */
102
+ maintainers: Array<{
103
+ namespace: string;
104
+ id: string;
105
+ }>;
93
106
  classification: Record<string, Record<string, "read" | "write">>;
94
107
  /** Inbound auth providers in the order they are tried. */
95
108
  auth: Array<{
@@ -97,14 +110,22 @@ export interface ConnectaConfigDescription {
97
110
  interactive: boolean;
98
111
  ui?: string;
99
112
  }>;
100
- identity: Record<"connectorAccess" | "activityAccess" | "credentialAdministration" | "accessTokenManagement" | "personalConnection", "default" | "custom">;
101
- /** `readOnly` is resolved: the pool's own value, else the deployment's. */
113
+ /** Agents declared in code, by name, in declaration order. Never token state. */
114
+ agents: string[];
115
+ /**
116
+ * `readOnly` is resolved: the pool's own value, else the deployment's.
117
+ * `members` are resolved references in declaration order. `owner` is the
118
+ * Workspace address agents act as, or null. `description` is the trimmed
119
+ * text the pool's endpoints add to their MCP instructions, or null.
120
+ */
102
121
  pools: Array<{
103
122
  name: string;
104
123
  path: string;
105
124
  tools: string[];
106
- hasGrant: boolean;
125
+ members: DescribedMember[];
107
126
  readOnly: boolean;
127
+ owner: string | null;
128
+ description: string | null;
108
129
  }>;
109
130
  modules: {
110
131
  ui: {
@@ -125,7 +146,6 @@ export interface ConnectaConfigDescription {
125
146
  };
126
147
  accessTokens: {
127
148
  enabled: boolean;
128
- maxActive?: number;
129
149
  };
130
150
  };
131
151
  /** Every accepted store implements `list` and `compareAndSet`, so only its kind is told. */
@@ -157,6 +177,10 @@ export interface DescribeConfigInput {
157
177
  registry: import("./registry.js").Registry;
158
178
  raw: ConnectaConfig;
159
179
  config: ResolvedConfig;
180
+ /** Resolved `maintainers`. */
181
+ maintainers: readonly IdentityReference[];
182
+ /** Resolved pools; members are read from here, never from raw references. */
183
+ pools: ReadonlyMap<string, ResolvedPool>;
160
184
  executorName: string | undefined;
161
185
  /** True when the executor owns admission, so `admission.code` is unused. */
162
186
  executorAdmits: boolean;
@@ -183,13 +183,6 @@ function describeConnector(connector, deploymentCap, registry) {
183
183
  ...(connector.staticTools ? { tools: describedTools(registry.describeStaticTools(connector.id) ?? []) } : {}),
184
184
  };
185
185
  }
186
- const IDENTITY_KEYS = [
187
- "connectorAccess",
188
- "activityAccess",
189
- "credentialAdministration",
190
- "accessTokenManagement",
191
- "personalConnection",
192
- ];
193
186
  const STORE_KINDS = new Set(["memory", "d1", "sqlite"]);
194
187
  const storeKind = (described) => typeof described?.kind === "string" && STORE_KINDS.has(described.kind)
195
188
  ? described.kind
@@ -198,7 +191,6 @@ const storeKind = (described) => typeof described?.kind === "string" && STORE_KI
198
191
  export function describeConfig(input) {
199
192
  const { raw, config } = input;
200
193
  const brand = resolveBranding(config.ui?.branding);
201
- const accessTokens = config.accessTokens?.describe?.();
202
194
  const storage = config.storage;
203
195
  const activityStore = config.activity?.store.describe?.();
204
196
  const D = CONFIG_DEFAULTS;
@@ -241,20 +233,24 @@ export function describeConfig(input) {
241
233
  requests: limitGroup(config.admission.requests, ownValue(ownValue(raw, "admission"), "requests"), Object.keys(D.admission.requests)),
242
234
  },
243
235
  readOnly: config.readOnly,
244
- hasGrant: typeof config.grant === "function",
236
+ maintainers: input.maintainers.map(({ namespace, id }) => ({ namespace: String(namespace), id: String(id) })),
245
237
  classification: structuredClone(config.classification ?? {}),
246
238
  auth: config.auth.map((provider) => ({
247
239
  kind: String(provider.kind),
248
240
  interactive: provider.interactiveOperator === true,
249
241
  ...(str(provider.uiAuth?.kind) !== undefined ? { ui: provider.uiAuth.kind } : {}),
250
242
  })),
251
- identity: Object.fromEntries(IDENTITY_KEYS.map((key) => [key, typeof config.identity[key] === "function" ? "custom" : "default"])),
243
+ agents: [...config.agents],
252
244
  pools: Object.entries(config.pools ?? {}).map(([name, pool]) => ({
253
245
  readOnly: pool.readOnly ?? config.readOnly,
254
246
  name,
255
247
  path: `/mcp/${name}`,
256
248
  tools: pool.tools.map(String),
257
- hasGrant: typeof pool.grant === "function",
249
+ members: (input.pools.get(name)?.members ?? []).map((member) => member.kind === "agent"
250
+ ? { kind: "agent", name: member.name }
251
+ : { kind: "human", namespace: member.namespace, id: member.id }),
252
+ owner: input.pools.get(name)?.owner ?? null,
253
+ description: input.pools.get(name)?.description ?? null,
258
254
  })),
259
255
  modules: {
260
256
  ui: { enabled: config.ui !== undefined },
@@ -277,12 +273,7 @@ export function describeConfig(input) {
277
273
  },
278
274
  }
279
275
  : { enabled: false },
280
- accessTokens: config.accessTokens
281
- ? {
282
- enabled: true,
283
- ...(num(accessTokens?.maxActive) !== undefined ? { maxActive: accessTokens.maxActive } : {}),
284
- }
285
- : { enabled: false },
276
+ accessTokens: { enabled: config.accessTokens !== undefined },
286
277
  },
287
278
  storage: {
288
279
  configured: raw.storage !== undefined,
package/dist/execute.js CHANGED
@@ -798,18 +798,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
798
798
  ...args,
799
799
  includeSchemas: args.includeSchemas ?? "json",
800
800
  includeSchemaKeys: args.includeSchemaKeys !== false,
801
- }));
801
+ }, catalogDependency));
802
802
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
803
- for (const tool of result.tools)
804
- catalogAddressDependency(tool.address);
805
- for (const error of result.catalogErrors)
806
- catalogDependency(error.connector);
807
- for (const connector of result.absence?.configuredConnectors ?? [])
808
- catalogDependency(connector);
809
- for (const connector of result.queryAnalysis?.configuredConnectors ?? [])
810
- catalogDependency(connector);
811
- if (result.queryAnalysis?.connectorScope)
812
- catalogDependency(result.queryAnalysis.connectorScope);
813
803
  return result;
814
804
  }),
815
805
  describe: (raw) => discovery("describe", async () => {
@@ -29,7 +29,7 @@ export function renderFixPrompt(spec, connectorId) {
29
29
  "own logs (lines prefixed [connecta]) or by reproducing the failure locally. Never put a secret in source, " +
30
30
  "a log line, or your reply: secrets belong in the deployment's environment or its credential vault.",
31
31
  "Make the smallest change that fixes it, then verify it by redeploying and refreshing the connection on the " +
32
- "operator Connections page. If the fix needs something you cannot do yourself — a provider console setting, " +
33
- "a secret only the operator holds — name that exact step instead.",
32
+ "operator Connectors page. If the fix needs a step you cannot do yourself, such as a provider console setting " +
33
+ "or a secret only the operator holds, name that exact step instead.",
34
34
  ].join("\n\n");
35
35
  }
package/dist/index.d.ts CHANGED
@@ -5,8 +5,9 @@ export { customExecutor, type CustomExecutorOptions } from "./executor-contract.
5
5
  export type { AccessTokensModule, ActivityModule, OperatorSurface } from "./module-contracts.js";
6
6
  export type { CredentialVault, CredentialMetadata } from "./credential-contract.js";
7
7
  export { defineConfig } from "./config.js";
8
- export type { AdmissionPoolConfig, ConnectaAdmissionConfig, ConnectaCallsConfig, ConnectaConfig, ConnectaDiscoveryConfig, ConnectaExecuteConfig, ConnectaIdentityConfig, ConnectaPoolConfig, ConnectaResultsConfig, ConnectorPermission, RequestAdmissionConfig, } from "./config.js";
9
- export type { ConfigValueSource, ConnectaConfigDescription, DescribedConnector, DescribedLimit, } from "./describe-config.js";
8
+ export type { AdmissionPoolConfig, ConnectaAdmissionConfig, ConnectaCallsConfig, ConnectaConfig, ConnectaDiscoveryConfig, ConnectaExecuteConfig, ConnectaPoolConfig, ConnectaResultsConfig, RequestAdmissionConfig, } from "./config.js";
9
+ export type { MemberRef } from "./membership.js";
10
+ export type { ConfigValueSource, ConnectaConfigDescription, DescribedConnector, DescribedMember, DescribedLimit, } from "./describe-config.js";
10
11
  export interface Connecta {
11
12
  /** Web-standard fetch handler. Usable as `export default { fetch: connecta.fetch }`. */
12
13
  fetch: (request: Request, env?: unknown, ctx?: unknown) => Promise<Response>;
@@ -14,7 +15,7 @@ export interface Connecta {
14
15
  /**
15
16
  * A secret-free snapshot of the configuration this deployment runs with,
16
17
  * built once at construction: limits and where each came from, auth
17
- * providers, identity rules, pools, modules, and every connector's source,
18
+ * providers, maintainers, agents, pools, modules, and every connector's source,
18
19
  * endpoint, auth mode, and static tools. Header values, keys, client
19
20
  * secrets, credentials, and functions never appear. The same object is
20
21
  * returned on every call.
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@ import { describeConfigSources } from "./config-value-sources.js";
2
2
  import { credentialTestRule, describeCredentialTestMismatch } from "./credential-rules.js";
3
3
  import { Registry } from "./registry.js";
4
4
  import { parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
5
+ import { assertsAgents } from "./agent-identity.js";
6
+ import { ConfigError } from "./config-schema.js";
7
+ import { memberContext, resolveMaintainers, resolveMembers, } from "./membership.js";
5
8
  import { createFetchHandler } from "./server.js";
6
9
  import { droppedBrandingUrls, droppedThemeTokens, droppedUiAuthUrls } from "./branding.js";
7
10
  import { readConfig } from "./config.js";
@@ -10,19 +13,21 @@ export { customExecutor } from "./executor-contract.js";
10
13
  import { AdmissionController, executorName, isAdmittingExecutor, withExecutorAdmission } from "./executor-admission.js";
11
14
  export { defineConfig } from "./config.js";
12
15
  /**
13
- * Validate declared pools against the connector set. Everything checkable at
14
- * construction throws here: a malformed name, an unparseable grant, an
15
- * unknown connector id, a tool address on an `api()` connector whose static
16
- * catalog lacks it. Remote catalogs load lazily, so their addresses are
17
- * checked at catalog load instead and stay unreachable until they match.
16
+ * Validate declared pools against the connector set and the members against
17
+ * auth and agents. Everything checkable at construction throws here: a
18
+ * malformed name, an unparseable grant, an unknown connector id, a tool
19
+ * address on an `api()` connector whose static catalog lacks it, and an empty,
20
+ * ambiguous, unknown, or repeated member. Remote catalogs load lazily, so
21
+ * their addresses are checked at catalog load instead and stay unreachable
22
+ * until they match.
18
23
  */
19
- function resolvePools(pools, readOnly, registry) {
24
+ function resolvePools(pools, readOnly, registry, members) {
20
25
  const resolved = new Map();
21
26
  if (!pools)
22
27
  return resolved;
23
28
  // The schema has already refused a pool that is not an object, a missing or
24
- // non-array `tools`, a non-function `grant`, and a misspelled key — a
25
- // misspelled `grant` would otherwise boot as a deny-all pool.
29
+ // non-array `tools` or `members`, and a misspelled key; the retired `grant`
30
+ // is refused there with its replacement named.
26
31
  for (const [name, pool] of Object.entries(pools)) {
27
32
  if (!POOL_NAME_RE.test(name)) {
28
33
  throw new Error(`ConnectaConfig.pools: pool name "${name}" must match [a-z0-9_-]+`);
@@ -52,8 +57,18 @@ function resolvePools(pools, readOnly, registry) {
52
57
  }
53
58
  }
54
59
  }
60
+ const path = `ConnectaConfig.pools.${name}.members`;
61
+ if (pool.members.length === 0) {
62
+ throw new ConfigError(`${path} is empty; a pool serves only its members, so list at least one.`);
63
+ }
55
64
  // A pool inherits the deployment value unless it sets its own, either way.
56
- resolved.set(name, { access, readOnly: pool.readOnly ?? readOnly, grant: pool.grant ?? (() => false) });
65
+ resolved.set(name, Object.freeze({
66
+ access,
67
+ readOnly: pool.readOnly ?? readOnly,
68
+ members: Object.freeze(resolveMembers(pool.members, path, members)),
69
+ ...(pool.owner !== undefined ? { owner: pool.owner.trim() } : {}),
70
+ ...(pool.description !== undefined ? { description: pool.description.trim() } : {}),
71
+ }));
57
72
  }
58
73
  return resolved;
59
74
  }
@@ -191,7 +206,9 @@ export function createConnecta(config) {
191
206
  results: resolved.results,
192
207
  classification: resolved.classification,
193
208
  });
194
- const pools = resolvePools(resolved.pools, resolved.readOnly, registry);
209
+ const members = memberContext(resolved.auth, resolved.agents);
210
+ const maintainers = resolveMaintainers(input.maintainers, resolved.auth.some((provider) => !assertsAgents(provider)), members);
211
+ const pools = resolvePools(resolved.pools, resolved.readOnly, registry, members);
195
212
  warnInsecureConfig(resolved);
196
213
  const requestAdmission = new AdmissionController(resolved.admission.requests);
197
214
  let codeAdmission;
@@ -214,6 +231,8 @@ export function createConnecta(config) {
214
231
  registry,
215
232
  raw: input,
216
233
  config: resolved,
234
+ maintainers,
235
+ pools,
217
236
  executorName: configuredExecutorName,
218
237
  executorAdmits,
219
238
  });
@@ -222,6 +241,7 @@ export function createConnecta(config) {
222
241
  configDescription: description,
223
242
  configValueSources: describeConfigSources(description, input),
224
243
  registry,
244
+ maintainers,
225
245
  pools,
226
246
  executor,
227
247
  executorName: configuredExecutorName,
@@ -3,6 +3,7 @@ import { hasControlCharacters } from "./tool-name.js";
3
3
  import { writeRefusal } from "./tool-safety.js";
4
4
  import { Cause, Effect, Exit } from "effect";
5
5
  import { isCallAdmissionError } from "./call-admission.js";
6
+ import { attachCallAdmission } from "./call-admission-context.js";
6
7
  import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
7
8
  import { downstreamValue } from "./mcp-result.js";
8
9
  import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
@@ -494,8 +495,9 @@ export class InvocationService {
494
495
  if (callSignal?.aborted)
495
496
  return yield* Effect.fail(callSignal.reason);
496
497
  attempts = 1;
497
- const call = async () => {
498
+ const call = async (permit) => {
498
499
  const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
500
+ attachCallAdmission(connectorContext, permit);
499
501
  trackCredentialReads(connectorContext);
500
502
  sentSecrets.include(sentSecretsFor(connectorContext));
501
503
  dispatchContext = connectorContext;
@@ -537,14 +539,14 @@ export class InvocationService {
537
539
  // cannot hold its permit past the deadline.
538
540
  const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
539
541
  stage = "admission";
540
- yield* timed((elapsed) => {
542
+ const permit = yield* timed((elapsed) => {
541
543
  admissionMs += elapsed;
542
544
  }, admitted(this.registry, target, args, admissionSignal));
543
545
  stage = "downstream";
544
546
  const reply = yield* timed((elapsed) => {
545
547
  connectorMs += elapsed;
546
548
  }, Effect.tryPromise({
547
- try: () => Promise.resolve(call()),
549
+ try: () => Promise.resolve(call(permit)),
548
550
  catch: (error) => error,
549
551
  }));
550
552
  if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
@@ -0,0 +1,53 @@
1
+ import type { ConnectorAccess } from "./connector-access.js";
2
+ import type { AuthenticatedIdentity, IdentityReference, InboundAuth } from "./types.js";
3
+ /**
4
+ * A person or an agent, as deployment code lists it: a bare principal id in
5
+ * the namespace of the deployment's one interactive auth provider,
6
+ * `{ namespace, id }` when that is ambiguous or the id contains `:`, or
7
+ * `"agent:<name>"` for an agent declared in `agents`.
8
+ */
9
+ export type MemberRef = string | Readonly<IdentityReference>;
10
+ /** A resolved member reference. Humans match by principal, agents by name. */
11
+ export type Member = {
12
+ kind: "human";
13
+ namespace: string;
14
+ id: string;
15
+ } | {
16
+ kind: "agent";
17
+ name: string;
18
+ };
19
+ /** A declared pool after construction-time validation. */
20
+ export interface ResolvedPool {
21
+ /** Resolved: the pool's own `readOnly`, else the deployment's. */
22
+ readOnly: boolean;
23
+ /** Exactly the pool's `tools`; nothing intersects or widens it. */
24
+ access: ConnectorAccess;
25
+ members: readonly Member[];
26
+ /** Trimmed `owner`: the Workspace address agents in this pool act as. */
27
+ owner?: string;
28
+ /** Trimmed `description`, appended to this pool's MCP instructions. */
29
+ description?: string;
30
+ }
31
+ /** What resolving a reference needs to know about the deployment. */
32
+ export interface MemberContext {
33
+ /** `activityActorNamespace` of each interactive provider, `undefined` where it has none. */
34
+ interactiveNamespaces: ReadonlyArray<string | undefined>;
35
+ agents: readonly string[];
36
+ }
37
+ /** The namespaces bare ids may resolve in, read from the resolved providers. */
38
+ export declare function memberContext(auth: readonly InboundAuth[], agents: readonly string[]): MemberContext;
39
+ /**
40
+ * Resolve one list of references, throwing on any that is malformed,
41
+ * ambiguous, unknown, or repeated (INV-11).
42
+ */
43
+ export declare function resolveMembers(value: unknown, path: string, context: MemberContext): Member[];
44
+ /**
45
+ * `maintainers`: people only. Required whenever an inbound provider can admit
46
+ * a person, which is any provider but the built-in agent-token adapter. With
47
+ * agents alone nobody can be a maintainer, so plain `/mcp` admits nobody.
48
+ */
49
+ export declare function resolveMaintainers(value: unknown, admitsPeople: boolean, context: MemberContext): readonly IdentityReference[];
50
+ /** Whether an admitted identity is this member. An agent never matches a person. */
51
+ export declare function isMember(member: Member, identity: Readonly<AuthenticatedIdentity>): boolean;
52
+ /** Whether an admitted identity is a listed maintainer. */
53
+ export declare function isMaintainer(maintainers: readonly IdentityReference[], identity: Readonly<AuthenticatedIdentity>): boolean;
@@ -0,0 +1,121 @@
1
+ // Who may open which endpoint (#837, decisions/0006): `maintainers` open plain
2
+ // `/mcp`, and each pool's static `members` open `/mcp/<pool>`. References are
3
+ // resolved once at construction; a request is matched against them and never
4
+ // names its own scope (INV-4). Pure, so core reads it without any auth module.
5
+ import { isAgentName } from "./agent-identity.js";
6
+ import { ConfigError } from "./config-schema.js";
7
+ import { validIdentityReference } from "./identity.js";
8
+ const AGENT_PREFIX = "agent:";
9
+ /** The namespaces bare ids may resolve in, read from the resolved providers. */
10
+ export function memberContext(auth, agents) {
11
+ return {
12
+ interactiveNamespaces: auth
13
+ .filter((provider) => provider.interactiveOperator === true)
14
+ .map((provider) => typeof provider.activityActorNamespace === "string" &&
15
+ validIdentityReference({ namespace: provider.activityActorNamespace, id: "x" })
16
+ ? provider.activityActorNamespace
17
+ : undefined),
18
+ agents,
19
+ };
20
+ }
21
+ function resolveRef(value, path, context) {
22
+ if (typeof value === "string") {
23
+ if (value.startsWith(AGENT_PREFIX)) {
24
+ const name = value.slice(AGENT_PREFIX.length);
25
+ if (!isAgentName(name) || !context.agents.includes(name)) {
26
+ throw new ConfigError(`${path} names agent "${name}", which ConnectaConfig.agents does not declare.`);
27
+ }
28
+ return { kind: "agent", name };
29
+ }
30
+ if (value.includes(":")) {
31
+ throw new ConfigError(`${path} contains ":". Write a principal whose id contains ":" as { namespace, id }; ` +
32
+ 'agents are "agent:<name>".');
33
+ }
34
+ if (!validIdentityReference({ namespace: "x", id: value })) {
35
+ throw new ConfigError(`${path} must be a principal id of 1 to 256 printable, non-space ASCII characters.`);
36
+ }
37
+ const namespaces = context.interactiveNamespaces;
38
+ if (namespaces.length !== 1) {
39
+ throw new ConfigError(`${path} is a bare principal id, which resolves only when exactly one interactive auth provider is ` +
40
+ `configured (found ${namespaces.length}). Write it as { namespace, id }.`);
41
+ }
42
+ const namespace = namespaces[0];
43
+ if (namespace === undefined) {
44
+ throw new ConfigError(`${path} is a bare principal id, but the interactive auth provider declares no activityActorNamespace. ` +
45
+ "Write it as { namespace, id }.");
46
+ }
47
+ return { kind: "human", namespace, id: value };
48
+ }
49
+ if (typeof value !== "object" ||
50
+ value === null ||
51
+ Array.isArray(value) ||
52
+ Object.keys(value).length !== 2 ||
53
+ !Object.hasOwn(value, "namespace") ||
54
+ !Object.hasOwn(value, "id")) {
55
+ throw new ConfigError(`${path} must be a principal id string, { namespace, id }, or "agent:<name>".`);
56
+ }
57
+ const { namespace, id } = value;
58
+ if (typeof namespace !== "string" || typeof id !== "string" || !validIdentityReference({ namespace, id })) {
59
+ throw new ConfigError(`${path} must have a namespace and id of 1 to 256 printable, non-space ASCII characters each.`);
60
+ }
61
+ return { kind: "human", namespace, id };
62
+ }
63
+ const memberKey = (member) => member.kind === "agent" ? `agent\0${member.name}` : `human\0${member.namespace}\0${member.id}`;
64
+ /**
65
+ * Resolve one list of references, throwing on any that is malformed,
66
+ * ambiguous, unknown, or repeated (INV-11).
67
+ */
68
+ export function resolveMembers(value, path, context) {
69
+ if (!Array.isArray(value)) {
70
+ throw new ConfigError(`${path} must be an array of principal ids, { namespace, id }, or "agent:<name>".`);
71
+ }
72
+ const seen = new Set();
73
+ return value.map((entry, index) => {
74
+ const member = resolveRef(entry, `${path}[${index}]`, context);
75
+ const key = memberKey(member);
76
+ if (seen.has(key))
77
+ throw new ConfigError(`${path}[${index}] repeats an earlier member.`);
78
+ seen.add(key);
79
+ return Object.freeze(member);
80
+ });
81
+ }
82
+ /**
83
+ * `maintainers`: people only. Required whenever an inbound provider can admit
84
+ * a person, which is any provider but the built-in agent-token adapter. With
85
+ * agents alone nobody can be a maintainer, so plain `/mcp` admits nobody.
86
+ */
87
+ export function resolveMaintainers(value, admitsPeople, context) {
88
+ const path = "ConnectaConfig.maintainers";
89
+ if (value === undefined || (Array.isArray(value) && value.length === 0)) {
90
+ if (admitsPeople) {
91
+ throw new ConfigError(`${path} is required with inbound auth: list the people who may use plain /mcp, administer shared ` +
92
+ "credentials, manage agent tokens, and read activity.");
93
+ }
94
+ return Object.freeze([]);
95
+ }
96
+ if (!admitsPeople) {
97
+ throw new ConfigError(`${path} needs an inbound auth provider that admits people, such as clerkAuth or cloudflareAccessAuth. ` +
98
+ "Without one no maintainer can be admitted; agents are never maintainers.");
99
+ }
100
+ const members = resolveMembers(value, path, context);
101
+ return Object.freeze(members.map((member, index) => {
102
+ if (member.kind === "agent") {
103
+ throw new ConfigError(`${path}[${index}] is an agent. Agents are never maintainers; add them to a pool's members.`);
104
+ }
105
+ return Object.freeze({ namespace: member.namespace, id: member.id });
106
+ }));
107
+ }
108
+ /** Whether an admitted identity is this member. An agent never matches a person. */
109
+ export function isMember(member, identity) {
110
+ if (member.kind === "agent")
111
+ return identity.agent !== undefined && identity.agent === member.name;
112
+ const principal = identity.principal;
113
+ return (identity.agent === undefined &&
114
+ principal !== undefined &&
115
+ principal.namespace === member.namespace &&
116
+ principal.id === member.id);
117
+ }
118
+ /** Whether an admitted identity is a listed maintainer. */
119
+ export function isMaintainer(maintainers, identity) {
120
+ return maintainers.some((maintainer) => isMember({ kind: "human", ...maintainer }, identity));
121
+ }
@@ -17,13 +17,30 @@ export interface ActivityModule {
17
17
  readonly recordTool: typeof recordToolActivity;
18
18
  readonly recordChange: typeof recordCatalogChangeActivity;
19
19
  }
20
- /** Optional client-token authentication and operator lifecycle. */
20
+ /** Optional agent-token authentication and operator lifecycle. */
21
21
  export interface AccessTokensModule {
22
22
  readonly auth: import("./types.js").InboundAuth;
23
23
  handle(context: RouteContext): Promise<Response | null>;
24
- /** Token policy for `describeConfig()`; never token material. */
25
- describe?(): {
26
- maxActive: number;
27
- optionSources?: Readonly<Record<string, "default" | "config">>;
28
- };
24
+ /**
25
+ * Every declared agent's token slot, in declaration order: the list GET
26
+ * /ui/access-tokens serves, which the maintainer's operator contract
27
+ * repeats field by field. Never secret material.
28
+ */
29
+ slots(): Promise<Array<{
30
+ name: string;
31
+ state: "none";
32
+ revision: string;
33
+ } | {
34
+ name: string;
35
+ state: "issued";
36
+ issuedAt: string;
37
+ revision: string;
38
+ } | {
39
+ name: string;
40
+ state: "revoked";
41
+ revokedAt: string;
42
+ revision: string;
43
+ }>>;
44
+ /** The same module bound to the deployment's declared `agents`; createConnecta calls it once. */
45
+ forAgents(agents: readonly string[]): AccessTokensModule;
29
46
  }