@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.
- package/CHANGELOG.md +98 -0
- package/README.md +95 -116
- package/SECURITY.md +1 -1
- package/bin/connecta.mjs +24 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/activity.d.ts +3 -0
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/branding.d.ts +7 -0
- package/dist/branding.js +76 -9
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +15 -1
- package/dist/config-value-sources.js +16 -9
- package/dist/config.d.ts +48 -75
- package/dist/config.js +144 -36
- package/dist/connector-access.d.ts +4 -34
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +4 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +31 -7
- package/dist/describe-config.js +8 -17
- package/dist/execute.js +1 -11
- package/dist/fix-prompt.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +53 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- package/dist/oauth-callback-outcome.js +2 -2
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -4
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/config-summary.js +562 -0
- package/dist/operator-ui/connection-map.js +375 -0
- package/dist/operator-ui/contract.d.ts +149 -28
- package/dist/operator-ui/endpoints.js +221 -0
- package/dist/operator-ui/fix-prompts.js +5 -5
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +97 -0
- package/dist/operator-ui/model.d.ts +14 -6
- package/dist/operator-ui/overview.js +134 -0
- package/dist/operator-ui/view.d.ts +107 -37
- package/dist/operator-ui/view.js +163 -86
- package/dist/page-styles.js +5 -3
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +56 -17
- package/dist/providers/_shared/google/workspace.js +34 -8
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/ccb/index.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +4 -4
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +4 -4
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +11 -9
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- package/dist/providers/mixpanel/skill.generated.js +3 -3
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +100 -11
- package/dist/providers/notion/rest.js +45 -5
- package/dist/providers/notion/skill.generated.js +2 -2
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +2 -2
- package/dist/providers/overflow/skill.generated.js +2 -2
- package/dist/providers/planning-center/index.js +2 -2
- package/dist/providers/planning-center/skill.generated.js +4 -4
- package/dist/providers/revenuecat/skill.generated.js +2 -2
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +7 -4
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +4 -4
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/index.js +1 -1
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +23 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +161 -104
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +5 -11
- package/dist/routes/operator.js +24 -23
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +5 -0
- package/dist/skills.js +10 -0
- package/dist/storage/keys.js +9 -1
- package/dist/storage/sql.js +5 -1
- package/dist/types.d.ts +29 -7
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +23 -16
- package/dist/usage-guide.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +47 -23
- package/documentation/architecture.md +371 -61
- package/documentation/auth.md +897 -249
- package/documentation/code-mode.md +37 -16
- package/documentation/deploying.md +126 -25
- package/documentation/integrating.md +97 -23
- package/documentation/meta-tools.md +50 -31
- package/documentation/migration-0.33.md +352 -0
- package/documentation/migration-0.34.md +67 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +147 -34
- package/documentation/operator-ui.md +519 -64
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +37 -9
- package/examples/worker/README.md +127 -61
- package/examples/worker/src/connecta.config.ts +40 -23
- package/examples/worker/wrangler.jsonc +4 -1
- package/package.json +19 -18
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +80 -52
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +28 -12
- package/templates/node/src/rotate-token.ts +17 -0
- 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 {
|
|
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
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
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
|
-
|
|
247
|
-
/** Named tool pools, each served at `/mcp/<name>` to
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
587
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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
|
-
|
|
287
|
-
/** Named tool pools, each served at `/mcp/<name>` to
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
33
|
-
*
|
|
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
|
|
48
|
-
export {};
|
|
18
|
+
export declare function parseConnectorAccess(value: unknown): ConnectorAccess;
|
package/dist/connector-access.js
CHANGED
|
@@ -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
|
|
6
|
-
// legitimately named tool
|
|
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
|
|
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.
|
|
13
|
-
*
|
|
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
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|