@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.
- package/CHANGELOG.md +97 -0
- package/README.md +30 -20
- package/bin/connecta.mjs +25 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/auth-elicitation.js +1 -1
- 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 +7 -2
- package/dist/config-value-sources.js +14 -9
- package/dist/config.d.ts +58 -78
- package/dist/config.js +143 -47
- package/dist/connector-access.d.ts +4 -33
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +32 -7
- package/dist/describe-config.js +9 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +12 -7
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/meta-tools.js +17 -19
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -2
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +151 -29
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +15 -7
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +83 -21
- package/dist/page-styles.js +1 -1
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +63 -11
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +31 -9
- 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/breeze/skill.generated.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +5 -5
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +5 -5
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +279 -26
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +116 -11
- package/dist/providers/notion/rest.js +183 -24
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +287 -25
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +4 -1
- package/dist/providers/sheets/index.js +9 -6
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +6 -6
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/index.js +2 -2
- 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/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -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 +155 -85
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +25 -17
- 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 +8 -0
- package/dist/skills.js +5 -4
- package/dist/storage/keys.js +9 -1
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +12 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +17 -15
- package/dist/usage-guide.js +3 -3
- 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 +46 -22
- package/documentation/architecture.md +378 -58
- package/documentation/auth.md +806 -215
- package/documentation/code-mode.md +93 -81
- package/documentation/deploying.md +128 -26
- package/documentation/integrating.md +145 -22
- package/documentation/meta-tools.md +58 -46
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +14 -9
- package/documentation/operator-tests.md +117 -31
- package/documentation/operator-ui.md +326 -55
- package/documentation/provider-auth-migration.md +42 -27
- package/documentation/provider-migration-0.29.md +3 -3
- package/examples/worker/AGENTS.md +35 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +41 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +7 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -10
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +71 -44
- 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 +33 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/dist/tool-safety.d.ts +0 -9
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -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 {
|
|
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;
|
|
@@ -86,8 +96,13 @@ export interface ConnectaConfigDescription {
|
|
|
86
96
|
execute: Record<keyof typeof CONFIG_DEFAULTS.execute, DescribedLimit>;
|
|
87
97
|
requests: Record<keyof typeof CONFIG_DEFAULTS.admission.requests, DescribedLimit>;
|
|
88
98
|
};
|
|
89
|
-
/**
|
|
90
|
-
|
|
99
|
+
/** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
|
|
100
|
+
readOnly: 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
|
+
}>;
|
|
91
106
|
classification: Record<string, Record<string, "read" | "write">>;
|
|
92
107
|
/** Inbound auth providers in the order they are tried. */
|
|
93
108
|
auth: Array<{
|
|
@@ -95,13 +110,20 @@ export interface ConnectaConfigDescription {
|
|
|
95
110
|
interactive: boolean;
|
|
96
111
|
ui?: string;
|
|
97
112
|
}>;
|
|
98
|
-
|
|
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.
|
|
119
|
+
*/
|
|
99
120
|
pools: Array<{
|
|
100
121
|
name: string;
|
|
101
122
|
path: string;
|
|
102
123
|
tools: string[];
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
members: DescribedMember[];
|
|
125
|
+
readOnly: boolean;
|
|
126
|
+
owner: string | null;
|
|
105
127
|
}>;
|
|
106
128
|
modules: {
|
|
107
129
|
ui: {
|
|
@@ -122,7 +144,6 @@ export interface ConnectaConfigDescription {
|
|
|
122
144
|
};
|
|
123
145
|
accessTokens: {
|
|
124
146
|
enabled: boolean;
|
|
125
|
-
maxActive?: number;
|
|
126
147
|
};
|
|
127
148
|
};
|
|
128
149
|
/** Every accepted store implements `list` and `compareAndSet`, so only its kind is told. */
|
|
@@ -154,6 +175,10 @@ export interface DescribeConfigInput {
|
|
|
154
175
|
registry: import("./registry.js").Registry;
|
|
155
176
|
raw: ConnectaConfig;
|
|
156
177
|
config: ResolvedConfig;
|
|
178
|
+
/** Resolved `maintainers`. */
|
|
179
|
+
maintainers: readonly IdentityReference[];
|
|
180
|
+
/** Resolved pools; members are read from here, never from raw references. */
|
|
181
|
+
pools: ReadonlyMap<string, ResolvedPool>;
|
|
157
182
|
executorName: string | undefined;
|
|
158
183
|
/** True when the executor owns admission, so `admission.code` is unused. */
|
|
159
184
|
executorAdmits: boolean;
|
package/dist/describe-config.js
CHANGED
|
@@ -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;
|
|
@@ -240,20 +232,24 @@ export function describeConfig(input) {
|
|
|
240
232
|
execute: limitGroup(config.execute, ownValue(raw, "execute"), Object.keys(D.execute)),
|
|
241
233
|
requests: limitGroup(config.admission.requests, ownValue(ownValue(raw, "admission"), "requests"), Object.keys(D.admission.requests)),
|
|
242
234
|
},
|
|
243
|
-
|
|
235
|
+
readOnly: config.readOnly,
|
|
236
|
+
maintainers: input.maintainers.map(({ namespace, id }) => ({ namespace: String(namespace), id: String(id) })),
|
|
244
237
|
classification: structuredClone(config.classification ?? {}),
|
|
245
238
|
auth: config.auth.map((provider) => ({
|
|
246
239
|
kind: String(provider.kind),
|
|
247
240
|
interactive: provider.interactiveOperator === true,
|
|
248
241
|
...(str(provider.uiAuth?.kind) !== undefined ? { ui: provider.uiAuth.kind } : {}),
|
|
249
242
|
})),
|
|
250
|
-
|
|
243
|
+
agents: [...config.agents],
|
|
251
244
|
pools: Object.entries(config.pools ?? {}).map(([name, pool]) => ({
|
|
252
|
-
|
|
245
|
+
readOnly: pool.readOnly ?? config.readOnly,
|
|
253
246
|
name,
|
|
254
247
|
path: `/mcp/${name}`,
|
|
255
248
|
tools: pool.tools.map(String),
|
|
256
|
-
|
|
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,
|
|
257
253
|
})),
|
|
258
254
|
modules: {
|
|
259
255
|
ui: { enabled: config.ui !== undefined },
|
|
@@ -276,12 +272,7 @@ export function describeConfig(input) {
|
|
|
276
272
|
},
|
|
277
273
|
}
|
|
278
274
|
: { enabled: false },
|
|
279
|
-
accessTokens: config.accessTokens
|
|
280
|
-
? {
|
|
281
|
-
enabled: true,
|
|
282
|
-
...(num(accessTokens?.maxActive) !== undefined ? { maxActive: accessTokens.maxActive } : {}),
|
|
283
|
-
}
|
|
284
|
-
: { enabled: false },
|
|
275
|
+
accessTokens: { enabled: config.accessTokens !== undefined },
|
|
285
276
|
},
|
|
286
277
|
storage: {
|
|
287
278
|
configured: raw.storage !== undefined,
|
package/dist/errors.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "downstream_o
|
|
|
33
33
|
* Adding a member fails typecheck until {@link CLASSIFICATION_CODE_TABLE}
|
|
34
34
|
* lists it.
|
|
35
35
|
*/
|
|
36
|
-
export type ClassificationCode = ConnectorCallErrorCode | ExecutorAdmissionErrorCode | "cancelled" | "unknown_address" | "unknown_tool" | "ambiguous_tool_alias" | "catalog_lookup_failed" | "result_too_large" | "destructive_tool_requires_approval" | "auth_replay_refused" | "write_outcome_unknown" | "result_processing_failed" | "budget_exceeded" | "executor_failed" | "program_error";
|
|
36
|
+
export type ClassificationCode = ConnectorCallErrorCode | ExecutorAdmissionErrorCode | "cancelled" | "unknown_address" | "unknown_tool" | "ambiguous_tool_alias" | "catalog_lookup_failed" | "result_too_large" | "destructive_tool_requires_approval" | "read_only_endpoint" | "auth_replay_refused" | "write_outcome_unknown" | "result_processing_failed" | "budget_exceeded" | "executor_failed" | "program_error";
|
|
37
37
|
/** `value` when it is a code connecta assigns, else undefined. */
|
|
38
38
|
export declare function classificationCode(value: unknown): ClassificationCode | undefined;
|
|
39
39
|
/** One bounded, payload-free explanation of an input-schema mismatch. */
|
package/dist/errors.js
CHANGED
|
@@ -28,6 +28,7 @@ const CLASSIFICATION_CODE_TABLE = {
|
|
|
28
28
|
catalog_lookup_failed: true,
|
|
29
29
|
result_too_large: true,
|
|
30
30
|
destructive_tool_requires_approval: true,
|
|
31
|
+
read_only_endpoint: true,
|
|
31
32
|
auth_replay_refused: true,
|
|
32
33
|
write_outcome_unknown: true,
|
|
33
34
|
result_processing_failed: true,
|
package/dist/execute.js
CHANGED
|
@@ -412,6 +412,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
412
412
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
413
413
|
requestScope,
|
|
414
414
|
requestSignal: hostAccessSignal,
|
|
415
|
+
// connecta.search and connecta.describe never show a read-only endpoint's writes.
|
|
416
|
+
readOnly: limits.readOnly,
|
|
415
417
|
// A program that just missed an address cannot call search_tools.
|
|
416
418
|
searchRoute: "connecta.search",
|
|
417
419
|
concurrency: limits.discoveryConcurrency,
|
|
@@ -455,7 +457,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
455
457
|
}
|
|
456
458
|
return value;
|
|
457
459
|
};
|
|
458
|
-
/** Budget and account for writes
|
|
460
|
+
/** Budget and account for writes a writable endpoint admits. */
|
|
459
461
|
const invocationContext = (sending, timeoutMs) => ({
|
|
460
462
|
source: "execute_code",
|
|
461
463
|
sentSecrets,
|
|
@@ -463,9 +465,9 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
463
465
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
464
466
|
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
465
467
|
unwrapResult: true,
|
|
466
|
-
|
|
468
|
+
readOnly: limits.readOnly,
|
|
467
469
|
processResult: async (value, resolved, secrets, format, raw) => {
|
|
468
|
-
const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets,
|
|
470
|
+
const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, requestScope, wireBytes);
|
|
469
471
|
const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
|
|
470
472
|
? raw
|
|
471
473
|
: undefined;
|
|
@@ -605,7 +607,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
605
607
|
normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
|
|
606
608
|
hostCallTimeoutMs;
|
|
607
609
|
const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
608
|
-
// A
|
|
610
|
+
// A program write settles here — unknown if the call never returned an
|
|
609
611
|
// outcome. Credentials are filtered from arguments; submitted private values are not.
|
|
610
612
|
const sending = {};
|
|
611
613
|
const outcome = yield* invocation
|
|
@@ -656,7 +658,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
656
658
|
: Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
657
659
|
});
|
|
658
660
|
const meta = createMetaTools(registry, baseUrl, {
|
|
659
|
-
|
|
661
|
+
readOnly: limits.readOnly,
|
|
660
662
|
requestScope,
|
|
661
663
|
onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
|
|
662
664
|
});
|
|
@@ -1077,7 +1079,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1077
1079
|
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
1078
1080
|
programDeadlineAt: Date.now() + watchdog.ms,
|
|
1079
1081
|
defer: config.defer,
|
|
1080
|
-
|
|
1082
|
+
readOnly: config.readOnly,
|
|
1081
1083
|
maxWrites: config.maxWrites,
|
|
1082
1084
|
programWrites,
|
|
1083
1085
|
resultDependencies,
|
|
@@ -1087,7 +1089,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1087
1089
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
1088
1090
|
}
|
|
1089
1091
|
const admitted = lease;
|
|
1090
|
-
// A
|
|
1092
|
+
// A program write may still be on the wire when the program settles:
|
|
1091
1093
|
// close, so a write gated after this is not sent, then let the ones
|
|
1092
1094
|
// already dispatched finish before the scope aborts them — an
|
|
1093
1095
|
// abandoned write would be one whose outcome nobody knows.
|
|
@@ -1114,7 +1116,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1114
1116
|
emitted,
|
|
1115
1117
|
diagnostics,
|
|
1116
1118
|
program,
|
|
1117
|
-
guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets,
|
|
1119
|
+
guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, logger, maxChars, programWrites.hasSucceeded),
|
|
1118
1120
|
};
|
|
1119
1121
|
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
1120
1122
|
// Releasing a QuickJS lease ends its child and rejects execute() with
|
|
@@ -1336,7 +1338,7 @@ function connectorInventory(connectors) {
|
|
|
1336
1338
|
return `${prefix}${shown.join(", ")}.`;
|
|
1337
1339
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
1338
1340
|
}
|
|
1339
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites,
|
|
1341
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly) => `${readOnly ? "One known read: call_tool. Everything else: execute_code. Read-only endpoint: search and describe omit write tools, and every write call is refused." : "One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. Writable endpoint: programs may read and write; the client approves."} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
1340
1342
|
${connectorInventory(connectors)}
|
|
1341
1343
|
|
|
1342
1344
|
Use async () => { ... }, no arguments. No portable ambient capabilities. API:
|
|
@@ -1383,7 +1385,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1383
1385
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
1384
1386
|
watchdogMs: ctx.watchdogMs,
|
|
1385
1387
|
defer: ctx.defer,
|
|
1386
|
-
|
|
1388
|
+
readOnly: ctx.readOnly,
|
|
1387
1389
|
maxWrites,
|
|
1388
1390
|
requestScope: ctx.requestScope,
|
|
1389
1391
|
});
|
|
@@ -1391,13 +1393,13 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1391
1393
|
description: executeDescription(emitBudgets, {
|
|
1392
1394
|
...hostLimits,
|
|
1393
1395
|
hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1394
|
-
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.
|
|
1396
|
+
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true),
|
|
1395
1397
|
inputSchema: EXECUTE_INPUT,
|
|
1396
1398
|
outputSchema: EXECUTE_OUTPUT,
|
|
1397
|
-
// The
|
|
1399
|
+
// The client sees execute_code as a write exactly when the endpoint writes.
|
|
1398
1400
|
annotations: {
|
|
1399
|
-
readOnlyHint: ctx.
|
|
1400
|
-
destructiveHint: ctx.
|
|
1401
|
+
readOnlyHint: ctx.readOnly === true,
|
|
1402
|
+
destructiveHint: ctx.readOnly !== true,
|
|
1401
1403
|
openWorldHint: true,
|
|
1402
1404
|
},
|
|
1403
1405
|
}, (args, extra) => {
|
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,
|
|
9
|
-
export type {
|
|
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,
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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, 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
|
|
25
|
-
//
|
|
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,7 +57,17 @@ function resolvePools(pools, registry) {
|
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
|
-
|
|
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
|
+
}
|
|
64
|
+
// A pool inherits the deployment value unless it sets its own, either way.
|
|
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
|
+
}));
|
|
56
71
|
}
|
|
57
72
|
return resolved;
|
|
58
73
|
}
|
|
@@ -190,7 +205,9 @@ export function createConnecta(config) {
|
|
|
190
205
|
results: resolved.results,
|
|
191
206
|
classification: resolved.classification,
|
|
192
207
|
});
|
|
193
|
-
const
|
|
208
|
+
const members = memberContext(resolved.auth, resolved.agents);
|
|
209
|
+
const maintainers = resolveMaintainers(input.maintainers, resolved.auth.some((provider) => !assertsAgents(provider)), members);
|
|
210
|
+
const pools = resolvePools(resolved.pools, resolved.readOnly, registry, members);
|
|
194
211
|
warnInsecureConfig(resolved);
|
|
195
212
|
const requestAdmission = new AdmissionController(resolved.admission.requests);
|
|
196
213
|
let codeAdmission;
|
|
@@ -213,6 +230,8 @@ export function createConnecta(config) {
|
|
|
213
230
|
registry,
|
|
214
231
|
raw: input,
|
|
215
232
|
config: resolved,
|
|
233
|
+
maintainers,
|
|
234
|
+
pools,
|
|
216
235
|
executorName: configuredExecutorName,
|
|
217
236
|
executorAdmits,
|
|
218
237
|
});
|
|
@@ -221,6 +240,7 @@ export function createConnecta(config) {
|
|
|
221
240
|
configDescription: description,
|
|
222
241
|
configValueSources: describeConfigSources(description, input),
|
|
223
242
|
registry,
|
|
243
|
+
maintainers,
|
|
224
244
|
pools,
|
|
225
245
|
executor,
|
|
226
246
|
executorName: configuredExecutorName,
|
package/dist/invocation.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { withAuthorizationHandoff } from "./authorization-handoff.js";
|
|
2
2
|
import { hasControlCharacters } from "./tool-name.js";
|
|
3
|
-
import {
|
|
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";
|
|
@@ -389,7 +390,7 @@ export class InvocationService {
|
|
|
389
390
|
// Activity rows and this log line are both payload-free by
|
|
390
391
|
// construction: the line is the failure's typed record, never its
|
|
391
392
|
// message, which may be the downstream's own words (INV-6).
|
|
392
|
-
if (target && details.code !== "destructive_tool_requires_approval") {
|
|
393
|
+
if (target && details.code !== "destructive_tool_requires_approval" && details.code !== "read_only_endpoint") {
|
|
393
394
|
logFailure(this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger, "call failed", failureRecord({
|
|
394
395
|
connector: target.connector.id,
|
|
395
396
|
// Named only when the catalog listed it (src/operator-record.ts).
|
|
@@ -463,8 +464,11 @@ export class InvocationService {
|
|
|
463
464
|
reconciliationRequired: true,
|
|
464
465
|
};
|
|
465
466
|
}
|
|
466
|
-
|
|
467
|
-
|
|
467
|
+
const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly);
|
|
468
|
+
if (refusal === "read_only_endpoint") {
|
|
469
|
+
return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and this endpoint is read-only. Nothing here can send it.`);
|
|
470
|
+
}
|
|
471
|
+
if (refusal === "destructive_tool_requires_approval") {
|
|
468
472
|
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
|
|
469
473
|
}
|
|
470
474
|
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
@@ -491,8 +495,9 @@ export class InvocationService {
|
|
|
491
495
|
if (callSignal?.aborted)
|
|
492
496
|
return yield* Effect.fail(callSignal.reason);
|
|
493
497
|
attempts = 1;
|
|
494
|
-
const call = async () => {
|
|
498
|
+
const call = async (permit) => {
|
|
495
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);
|
|
496
501
|
trackCredentialReads(connectorContext);
|
|
497
502
|
sentSecrets.include(sentSecretsFor(connectorContext));
|
|
498
503
|
dispatchContext = connectorContext;
|
|
@@ -534,14 +539,14 @@ export class InvocationService {
|
|
|
534
539
|
// cannot hold its permit past the deadline.
|
|
535
540
|
const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
|
|
536
541
|
stage = "admission";
|
|
537
|
-
yield* timed((elapsed) => {
|
|
542
|
+
const permit = yield* timed((elapsed) => {
|
|
538
543
|
admissionMs += elapsed;
|
|
539
544
|
}, admitted(this.registry, target, args, admissionSignal));
|
|
540
545
|
stage = "downstream";
|
|
541
546
|
const reply = yield* timed((elapsed) => {
|
|
542
547
|
connectorMs += elapsed;
|
|
543
548
|
}, Effect.tryPromise({
|
|
544
|
-
try: () => Promise.resolve(call()),
|
|
549
|
+
try: () => Promise.resolve(call(permit)),
|
|
545
550
|
catch: (error) => error,
|
|
546
551
|
}));
|
|
547
552
|
if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
}
|
|
29
|
+
/** What resolving a reference needs to know about the deployment. */
|
|
30
|
+
export interface MemberContext {
|
|
31
|
+
/** `activityActorNamespace` of each interactive provider, `undefined` where it has none. */
|
|
32
|
+
interactiveNamespaces: ReadonlyArray<string | undefined>;
|
|
33
|
+
agents: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/** The namespaces bare ids may resolve in, read from the resolved providers. */
|
|
36
|
+
export declare function memberContext(auth: readonly InboundAuth[], agents: readonly string[]): MemberContext;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve one list of references, throwing on any that is malformed,
|
|
39
|
+
* ambiguous, unknown, or repeated (INV-11).
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveMembers(value: unknown, path: string, context: MemberContext): Member[];
|
|
42
|
+
/**
|
|
43
|
+
* `maintainers`: people only. Required whenever an inbound provider can admit
|
|
44
|
+
* a person, which is any provider but the built-in agent-token adapter. With
|
|
45
|
+
* agents alone nobody can be a maintainer, so plain `/mcp` admits nobody.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveMaintainers(value: unknown, admitsPeople: boolean, context: MemberContext): readonly IdentityReference[];
|
|
48
|
+
/** Whether an admitted identity is this member. An agent never matches a person. */
|
|
49
|
+
export declare function isMember(member: Member, identity: Readonly<AuthenticatedIdentity>): boolean;
|
|
50
|
+
/** Whether an admitted identity is a listed maintainer. */
|
|
51
|
+
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
|
+
}
|