@zackbart/connecta 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. package/templates/node/src/provision-token.ts +0 -17
@@ -1,10 +1,10 @@
1
1
  import type { ActivityActor } from "../activity.js";
2
2
  import type { DeferredWork } from "../connector-scope.js";
3
3
  import type { AdmissionController } from "../executor-admission.js";
4
- import type { Registry, ToolAccess } from "../registry.js";
5
- import type { ResolvedPool } from "../connector-access.js";
6
- import type { AuthenticatedIdentity, Executor, InboundAuth, InboundAuthRuntimeContext } from "../types.js";
7
- import type { ConnectorPermission, ConnectaIdentityConfig, ResolvedConfig } from "../config.js";
4
+ import type { Registry } from "../registry.js";
5
+ import { type ResolvedPool } from "../membership.js";
6
+ import type { AuthenticatedIdentity, Executor, IdentityReference, InboundAuth, InboundAuthRuntimeContext } from "../types.js";
7
+ import type { ResolvedConfig } from "../config.js";
8
8
  export { msg } from "../errors.js";
9
9
  export interface ServerOptions {
10
10
  /**
@@ -17,6 +17,8 @@ export interface ServerOptions {
17
17
  configDescription: import("../describe-config.js").ConnectaConfigDescription;
18
18
  configValueSources?: Readonly<Record<string, "default" | "config">>;
19
19
  registry: Registry;
20
+ /** Resolved `maintainers`: the only principals plain `/mcp` and operator routes admit. */
21
+ maintainers: readonly IdentityReference[];
20
22
  /** Validated named pools served at `/mcp/<name>`; empty when none declared. */
21
23
  pools: ReadonlyMap<string, ResolvedPool>;
22
24
  /** Required sandbox backing execute_code, wrapped in fallback admission if needed. */
@@ -39,6 +41,14 @@ export interface RouteContext {
39
41
  runtimeContext: RuntimeExecutionContext | undefined;
40
42
  }
41
43
  export declare function privateJson(body: unknown, init?: ResponseInit): Response;
44
+ /**
45
+ * The body as text, or undefined once it is longer than `maxChars`. No UTF-16
46
+ * unit costs more than three bytes, and a malformed sequence decodes to one
47
+ * U+FFFD per at most three bytes, so a body past three bytes per character is
48
+ * past the limit too — and reading stops there, instead of buffering whatever
49
+ * a client cares to send before measuring it.
50
+ */
51
+ export declare function boundedText(request: Request, maxChars: number): Promise<string | undefined>;
42
52
  /**
43
53
  * Bounded, escaped form of a caller-influenced value (an identity id or OAuth
44
54
  * callback connector id) for the operator log. Goes
@@ -55,40 +65,70 @@ export declare function authMetadata(request: Request, baseUrl: string, auth: re
55
65
  provider: InboundAuth;
56
66
  response: Response;
57
67
  } | null>;
58
- export declare function authorize(request: Request, baseUrl: string, auth: readonly InboundAuth[], runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig, partitionIdentity?: boolean, interactiveOnly?: boolean): Promise<{
68
+ export declare function authorize(request: Request, baseUrl: string, auth: readonly InboundAuth[], runtimeContext?: RuntimeExecutionContext, partitionIdentity?: boolean, interactiveOnly?: boolean): Promise<{
59
69
  ok: true;
60
70
  actor: ActivityActor;
61
71
  identity: AuthenticatedIdentity;
62
72
  sessionCookies?: readonly string[];
63
73
  subjectKey?: string;
64
74
  principalKey?: string;
65
- connectorIds: "all" | readonly string[];
66
- /** Per-connector tool allowlist for connectors granted by address only. */
67
- toolAccess?: ToolAccess;
68
- /** Granted addresses that still require the catalog's read-only hint. */
69
- guardedToolAccess?: ToolAccess;
70
- operator: boolean;
71
- accessTokenManagement: boolean;
72
- credentialAdministration: ConnectorPermission;
73
- personalConnection: ConnectorPermission;
74
- /** Backward-compatible name used by operator views. */
75
- uiAdminEligible?: boolean;
76
75
  } | {
77
76
  ok: false;
78
77
  response: Response;
79
78
  }>;
79
+ /**
80
+ * Admission at an agent endpoint, `/agent/<pool>` (#837). Only the built-in
81
+ * `cta_` adapter is consulted: Clerk and Access never run, cookies and Clerk
82
+ * handshake parameters are stripped, and the runtime context, which carries
83
+ * Access's `ctx.access` even on a bypassed path, is never passed. Anything but
84
+ * an admitted declared agent gets the machine-only challenge.
85
+ */
86
+ export declare function authorizeAgent(request: Request, baseUrl: string, auth: readonly InboundAuth[]): Promise<Awaited<ReturnType<typeof authorize>>>;
80
87
  /** Human routes reject machine credentials before any verification or storage I/O. */
81
- export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Awaited<ReturnType<typeof authorize>>>;
88
+ export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext): Promise<Awaited<ReturnType<typeof authorize>>>;
82
89
  export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
83
90
  export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
84
91
  type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
85
92
  ok: true;
86
93
  }>;
87
- /** Unknown configured ids refuse the complete view, including management rights. */
88
- export declare function validateAuthPermissions(authz: AuthorizedIdentity, registry: Registry): void;
89
- export declare function mayManageConnector(authz: AuthorizedIdentity, connector: {
94
+ /**
95
+ * Plain `/mcp` and every operator route admit a maintainer (#837). An open
96
+ * deployment has no inbound auth and so no maintainers: its one anonymous
97
+ * caller keeps the whole default endpoint, as the startup warning says.
98
+ */
99
+ export declare function admitsMaintainer(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
100
+ /**
101
+ * The declared pool `/mcp/<name>` serves to this person, or undefined for the
102
+ * same 404. An agent is never admitted here, even when listed (#837).
103
+ */
104
+ export declare function memberPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
105
+ /** The declared pool `/agent/<name>` serves to this agent, or undefined for the same 404. */
106
+ export declare function agentPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
107
+ /**
108
+ * The connectors of every pool this identity is a member of (#837): what a
109
+ * pool member may ever manage, and only their personal ones among these.
110
+ */
111
+ export declare function memberConnectors(opts: ServerOptions, identity: Readonly<AuthenticatedIdentity>): "all" | ReadonlySet<string>;
112
+ /**
113
+ * Whether this identity may start, finish, or remove a connector's auth
114
+ * (#837). Only an interactive person ever may; an agent or any other machine
115
+ * credential manages nothing. Shared auth is deployment-wide, so it takes a
116
+ * maintainer. A personal connector takes a stable principal that is a
117
+ * maintainer, whose plain `/mcp` includes it, or a member of a pool that
118
+ * contains it; either way it is always the caller's own partition, which
119
+ * callers bind by principal. Every path that issues a link, starts,
120
+ * completes, or disconnects auth, or writes a credential, asks this.
121
+ */
122
+ export declare function mayManageConnector(opts: ServerOptions, authz: AuthorizedIdentity, connector: {
90
123
  id: string;
91
124
  authScope?: "shared" | "personal";
92
125
  }): boolean;
126
+ /** Agent token list, issue, rotate, and revoke: an interactive maintainer only (#837). */
127
+ export declare function managesAgentTokens(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
128
+ /**
129
+ * Activity reads: a maintainer principal, interactive or not (#837). Members
130
+ * and agents read none, and an open deployment has no maintainer.
131
+ */
132
+ export declare function readsActivity(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
93
133
  /** Preserve refreshed browser cookies on the redirect or completion page. */
94
134
  export declare function withSessionCookies(response: Response, cookies?: readonly string[]): Response;
@@ -1,7 +1,8 @@
1
1
  import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
2
2
  import { htmlSecurityHeaders } from "../html-security.js";
3
- import { parseConnectorAccess } from "../connector-access.js";
3
+ import { isMaintainer, isMember } from "../membership.js";
4
4
  import { identityStorageKey, validIdentityReference } from "../identity.js";
5
+ import { assertsAgents, isAgentName } from "../agent-identity.js";
5
6
  export { msg } from "../errors.js";
6
7
  export function privateJson(body, init = {}) {
7
8
  const headers = new Headers(init.headers);
@@ -10,6 +11,40 @@ export function privateJson(body, init = {}) {
10
11
  headers.set("Referrer-Policy", "no-referrer");
11
12
  return new Response(JSON.stringify(body), { ...init, headers });
12
13
  }
14
+ /**
15
+ * The body as text, or undefined once it is longer than `maxChars`. No UTF-16
16
+ * unit costs more than three bytes, and a malformed sequence decodes to one
17
+ * U+FFFD per at most three bytes, so a body past three bytes per character is
18
+ * past the limit too — and reading stops there, instead of buffering whatever
19
+ * a client cares to send before measuring it.
20
+ */
21
+ export async function boundedText(request, maxChars) {
22
+ if (!request.body)
23
+ return "";
24
+ const maxBytes = maxChars * 3;
25
+ const reader = request.body.getReader();
26
+ const chunks = [];
27
+ let size = 0;
28
+ for (;;) {
29
+ const { done, value } = await reader.read();
30
+ if (done)
31
+ break;
32
+ size += value.byteLength;
33
+ if (size > maxBytes) {
34
+ reader.cancel().catch(() => { });
35
+ return undefined;
36
+ }
37
+ chunks.push(value);
38
+ }
39
+ const bytes = new Uint8Array(size);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ bytes.set(chunk, offset);
43
+ offset += chunk.byteLength;
44
+ }
45
+ const text = new TextDecoder().decode(bytes);
46
+ return text.length > maxChars ? undefined : text;
47
+ }
13
48
  const MAX_LOGGED_VALUE_LENGTH = 64;
14
49
  /**
15
50
  * Bounded, escaped form of a caller-influenced value (an identity id or OAuth
@@ -42,6 +77,13 @@ export async function authMetadata(request, baseUrl, auth) {
42
77
  return null;
43
78
  }
44
79
  async function providerChallenge(response, request, baseUrl, auth, refusingProvider) {
80
+ const privateHeaders = new Headers(response.headers);
81
+ privateHeaders.set("Cache-Control", "no-store");
82
+ response = new Response(response.body, {
83
+ status: response.status,
84
+ statusText: response.statusText,
85
+ headers: privateHeaders,
86
+ });
45
87
  if (response.status !== 401)
46
88
  return response;
47
89
  if (!response.headers.has("WWW-Authenticate")) {
@@ -80,7 +122,7 @@ function recognizedProvider(request, auth, runtimeContext) {
80
122
  }
81
123
  return undefined;
82
124
  }
83
- export async function authorize(request, baseUrl, auth, runtimeContext, identityConfig, partitionIdentity = true, interactiveOnly = false) {
125
+ export async function authorize(request, baseUrl, auth, runtimeContext, partitionIdentity = true, interactiveOnly = false) {
84
126
  const credential = authorizationCredential(request);
85
127
  const explicit = credential.kind !== "absent";
86
128
  if (credential.kind === "invalid") {
@@ -100,56 +142,53 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
100
142
  if (interactiveOnly && machineCredential) {
101
143
  return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
102
144
  }
145
+ // Every caller of this function is a human route. Agents reach pools only at
146
+ // `/agent/<pool>` (#837), so a `cta_` token refuses here without consulting
147
+ // token storage, and never falls back to a cookie, session, or Access context.
148
+ if (machineCredential) {
149
+ return {
150
+ ok: false,
151
+ response: await providerChallenge(privateJson({ error: "unauthorized" }, {
152
+ status: 401,
153
+ headers: { "WWW-Authenticate": "Bearer" },
154
+ }), request, baseUrl, auth),
155
+ };
156
+ }
103
157
  if (auth.length === 0 && !interactiveOnly && !explicit) {
104
158
  const actor = { kind: "anonymous" };
105
159
  const identity = { actor, interactive: false };
106
- let access;
107
- try {
108
- access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
109
- }
110
- catch {
111
- return {
112
- ok: false,
113
- response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
114
- };
115
- }
116
- return {
117
- ok: true,
118
- actor,
119
- identity,
120
- ...access,
121
- operator: false,
122
- accessTokenManagement: false,
123
- credentialAdministration: "none",
124
- personalConnection: "none",
125
- };
160
+ return { ok: true, actor, identity };
126
161
  }
162
+ // The built-in agent adapter answers only agent endpoints. A deployment
163
+ // whose only provider is that adapter still refuses here, never opens.
164
+ const humanAuth = auth.filter((provider) => !assertsAgents(provider));
127
165
  let recognized;
128
166
  try {
129
- recognized = recognizedProvider(request, machineCredential ? auth.filter((provider) => provider.kind === "access_token") : auth, runtimeContext);
167
+ recognized = recognizedProvider(request, humanAuth, runtimeContext);
130
168
  }
131
169
  catch {
132
170
  return { ok: false, response: privateJson({ error: "credential recognition failed" }, { status: 403 }) };
133
171
  }
134
- if (machineCredential && !recognized) {
135
- return {
136
- ok: false,
137
- response: await providerChallenge(privateJson({ error: "unauthorized" }, {
138
- status: 401,
139
- headers: { "WWW-Authenticate": "Bearer" },
140
- }), request, baseUrl, auth),
141
- };
142
- }
143
172
  if (interactiveOnly && recognized && !recognized.interactiveOperator) {
144
173
  return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
145
174
  }
146
175
  let lastResponse = null;
147
- const eligible = auth.filter((provider) => (!interactiveOnly || provider.interactiveOperator) &&
148
- (!explicit || machineCredential || provider.kind !== "access_token" || !provider.recognizesCredential));
176
+ const eligible = humanAuth.filter((provider) => (!interactiveOnly || provider.interactiveOperator) &&
177
+ (!explicit || provider.kind !== "access_token" || !provider.recognizesCredential));
149
178
  // A header owns one verdict even for adapters without a recognition hook.
150
179
  const candidates = recognized ? [recognized] : explicit ? eligible.slice(0, 1) : eligible;
151
180
  for (const provider of candidates) {
152
181
  const result = await provider.authorize(request, baseUrl, runtimeContext);
182
+ // Only the built-in access-token adapter asserts an agent, and it never
183
+ // answers here. Any `agent` claim refuses like an unknown token.
184
+ const agent = result.ok ? result.agent : undefined;
185
+ if (result.ok && agent !== undefined) {
186
+ lastResponse = privateJson({ error: "unauthorized" }, {
187
+ status: 401,
188
+ headers: { "WWW-Authenticate": "Bearer" },
189
+ });
190
+ break;
191
+ }
153
192
  if (result.ok) {
154
193
  if (interactiveOnly && !result.userId) {
155
194
  return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
@@ -164,7 +203,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
164
203
  const interactive = Boolean(result.userId && provider.interactiveOperator);
165
204
  // Principal-only API callers still need a stable typed actor for history
166
205
  // ownership. Never collapse distinct personal partitions into { kind }.
167
- const actorId = subjectId ?? principal?.id;
206
+ const actorId = agent ?? subjectId ?? principal?.id;
168
207
  const recordNamespace = actorNamespace ?? (subjectId === undefined ? principal?.namespace : undefined);
169
208
  const actor = {
170
209
  kind: provider.kind,
@@ -175,48 +214,9 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
175
214
  actor,
176
215
  ...(subject ? { subject } : {}),
177
216
  ...(principal ? { principal } : {}),
217
+ ...(agent !== undefined ? { agent } : {}),
178
218
  interactive,
179
219
  };
180
- let operator = interactive;
181
- let accessTokenManagement = false;
182
- let credentialAdministration = "none";
183
- let personalConnection = "none";
184
- let access;
185
- try {
186
- if (identityConfig?.activityAccess) {
187
- operator = principal ? await identityConfig.activityAccess(principal) : false;
188
- }
189
- access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
190
- if (interactive) {
191
- accessTokenManagement = identityConfig?.accessTokenManagement
192
- ? await identityConfig.accessTokenManagement(identity)
193
- : false;
194
- if (typeof accessTokenManagement !== "boolean")
195
- throw new Error("invalid token management permission");
196
- credentialAdministration = identityConfig?.credentialAdministration
197
- ? await identityConfig.credentialAdministration(identity)
198
- : "none";
199
- personalConnection =
200
- principal && identityConfig?.personalConnection
201
- ? await identityConfig.personalConnection(identity)
202
- : "none";
203
- }
204
- if (typeof operator !== "boolean")
205
- throw new Error("invalid activity permission");
206
- for (const permission of [credentialAdministration, personalConnection]) {
207
- if (permission !== "all" &&
208
- permission !== "none" &&
209
- (!Array.isArray(permission) ||
210
- !permission.every((id) => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
211
- throw new Error("invalid identity permission");
212
- }
213
- }
214
- catch {
215
- return {
216
- ok: false,
217
- response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
218
- };
219
- }
220
220
  return {
221
221
  ok: true,
222
222
  actor,
@@ -224,12 +224,6 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
224
224
  ...(result.sessionCookies?.length ? { sessionCookies: result.sessionCookies } : {}),
225
225
  ...(subject && partitionIdentity ? { subjectKey: await identityStorageKey(subject) } : {}),
226
226
  ...(principal && partitionIdentity ? { principalKey: await identityStorageKey(principal) } : {}),
227
- ...access,
228
- accessTokenManagement,
229
- credentialAdministration,
230
- personalConnection,
231
- operator,
232
- ...(interactive && operator ? { uiAdminEligible: true } : {}),
233
227
  };
234
228
  }
235
229
  lastResponse = result.response;
@@ -245,15 +239,57 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
245
239
  }), request, baseUrl, auth, explicit ? candidates[0] : undefined),
246
240
  };
247
241
  }
242
+ /** The machine-only challenge: Bearer, with no authorization server to discover. */
243
+ function agentChallenge() {
244
+ return {
245
+ ok: false,
246
+ response: privateJson({ error: "unauthorized" }, { status: 401, headers: { "WWW-Authenticate": "Bearer" } }),
247
+ };
248
+ }
249
+ /**
250
+ * Admission at an agent endpoint, `/agent/<pool>` (#837). Only the built-in
251
+ * `cta_` adapter is consulted: Clerk and Access never run, cookies and Clerk
252
+ * handshake parameters are stripped, and the runtime context, which carries
253
+ * Access's `ctx.access` even on a bypassed path, is never passed. Anything but
254
+ * an admitted declared agent gets the machine-only challenge.
255
+ */
256
+ export async function authorizeAgent(request, baseUrl, auth) {
257
+ const provider = auth.find(assertsAgents);
258
+ if (!provider || !isMachineCredential(request))
259
+ return agentChallenge();
260
+ let result;
261
+ try {
262
+ result = await provider.authorize(authorizationRequest(request), baseUrl, undefined);
263
+ }
264
+ catch {
265
+ return agentChallenge();
266
+ }
267
+ if (!result.ok) {
268
+ void result.response.body?.cancel().catch(() => { });
269
+ return agentChallenge();
270
+ }
271
+ const { agent } = result;
272
+ if (!isAgentName(agent) || result.userId !== undefined || result.principal !== undefined)
273
+ return agentChallenge();
274
+ const namespace = activityActorNamespace(provider);
275
+ const subject = { namespace: namespace ?? `connecta:auth:${provider.kind}`, id: result.subjectId ?? agent };
276
+ const actor = { kind: provider.kind, id: agent, ...(namespace ? { namespace } : {}) };
277
+ return {
278
+ ok: true,
279
+ actor,
280
+ identity: { actor, subject, agent, interactive: false },
281
+ subjectKey: await identityStorageKey(subject),
282
+ };
283
+ }
248
284
  /** Human routes reject machine credentials before any verification or storage I/O. */
249
- export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig) {
285
+ export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext) {
250
286
  if (!auth.some((provider) => provider.interactiveOperator)) {
251
287
  return {
252
288
  ok: false,
253
289
  response: privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
254
290
  };
255
291
  }
256
- return authorize(request, baseUrl, auth, runtimeContext, identityConfig, true, true);
292
+ return authorize(request, baseUrl, auth, runtimeContext, true, true);
257
293
  }
258
294
  export function isSameOrigin(request, baseUrl) {
259
295
  const origin = request.headers.get("origin");
@@ -281,24 +317,79 @@ export function withSecurityHeaders(response, requestUrl, _path) {
281
317
  headers,
282
318
  });
283
319
  }
284
- /** Unknown configured ids refuse the complete view, including management rights. */
285
- export function validateAuthPermissions(authz, registry) {
286
- for (const value of [authz.connectorIds, authz.credentialAdministration, authz.personalConnection]) {
287
- if (value === "all" || value === "none")
320
+ /**
321
+ * Plain `/mcp` and every operator route admit a maintainer (#837). An open
322
+ * deployment has no inbound auth and so no maintainers: its one anonymous
323
+ * caller keeps the whole default endpoint, as the startup warning says.
324
+ */
325
+ export function admitsMaintainer(opts, authz) {
326
+ return opts.config.auth.length === 0 || isMaintainer(opts.maintainers, authz.identity);
327
+ }
328
+ /**
329
+ * The declared pool `/mcp/<name>` serves to this person, or undefined for the
330
+ * same 404. An agent is never admitted here, even when listed (#837).
331
+ */
332
+ export function memberPool(opts, name, identity) {
333
+ if (identity.agent !== undefined)
334
+ return undefined;
335
+ const pool = opts.pools.get(name);
336
+ return pool?.members.some((member) => isMember(member, identity)) ? pool : undefined;
337
+ }
338
+ /** The declared pool `/agent/<name>` serves to this agent, or undefined for the same 404. */
339
+ export function agentPool(opts, name, identity) {
340
+ if (identity.agent === undefined)
341
+ return undefined;
342
+ const pool = opts.pools.get(name);
343
+ return pool?.members.some((member) => member.kind === "agent" && isMember(member, identity)) ? pool : undefined;
344
+ }
345
+ /**
346
+ * The connectors of every pool this identity is a member of (#837): what a
347
+ * pool member may ever manage, and only their personal ones among these.
348
+ */
349
+ export function memberConnectors(opts, identity) {
350
+ const ids = new Set();
351
+ for (const name of opts.pools.keys()) {
352
+ const pool = memberPool(opts, name, identity);
353
+ if (!pool)
288
354
  continue;
289
- if (!Array.isArray(value) || value.some((id) => !registry.getConnector(id))) {
290
- throw new Error("invalid identity permission connector ids");
291
- }
355
+ if (pool.access.connectorIds === "all")
356
+ return "all";
357
+ for (const id of pool.access.connectorIds)
358
+ ids.add(id);
292
359
  }
360
+ return ids;
293
361
  }
294
- export function mayManageConnector(authz, connector) {
295
- if (!authz.identity.interactive)
362
+ /**
363
+ * Whether this identity may start, finish, or remove a connector's auth
364
+ * (#837). Only an interactive person ever may; an agent or any other machine
365
+ * credential manages nothing. Shared auth is deployment-wide, so it takes a
366
+ * maintainer. A personal connector takes a stable principal that is a
367
+ * maintainer, whose plain `/mcp` includes it, or a member of a pool that
368
+ * contains it; either way it is always the caller's own partition, which
369
+ * callers bind by principal. Every path that issues a link, starts,
370
+ * completes, or disconnects auth, or writes a credential, asks this.
371
+ */
372
+ export function mayManageConnector(opts, authz, connector) {
373
+ const identity = authz.identity;
374
+ if (!identity.interactive || identity.agent !== undefined || !identity.principal)
296
375
  return false;
297
- if (authz.connectorIds !== "all" && !authz.connectorIds.includes(connector.id)) {
376
+ if (isMaintainer(opts.maintainers, identity))
377
+ return true;
378
+ if (connector.authScope !== "personal")
298
379
  return false;
299
- }
300
- const permission = connector.authScope === "personal" ? authz.personalConnection : authz.credentialAdministration;
301
- return permission === "all" || (permission !== "none" && permission.includes(connector.id));
380
+ const pools = memberConnectors(opts, identity);
381
+ return pools === "all" || pools.has(connector.id);
382
+ }
383
+ /** Agent token list, issue, rotate, and revoke: an interactive maintainer only (#837). */
384
+ export function managesAgentTokens(opts, authz) {
385
+ return (authz.identity.interactive && authz.identity.agent === undefined && isMaintainer(opts.maintainers, authz.identity));
386
+ }
387
+ /**
388
+ * Activity reads: a maintainer principal, interactive or not (#837). Members
389
+ * and agents read none, and an open deployment has no maintainer.
390
+ */
391
+ export function readsActivity(opts, authz) {
392
+ return isMaintainer(opts.maintainers, authz.identity);
302
393
  }
303
394
  /** Preserve refreshed browser cookies on the redirect or completion page. */
304
395
  export function withSessionCookies(response, cookies) {