@zackbart/connecta 0.32.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 +63 -0
- package/README.md +15 -7
- 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/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/config-value-sources.js +11 -9
- package/dist/config.d.ts +37 -75
- package/dist/config.js +110 -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 +3 -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 +29 -7
- package/dist/describe-config.js +7 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +29 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- 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/contract.d.ts +149 -28
- 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 +14 -6
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +82 -21
- package/dist/page-styles.js +1 -1
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +30 -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 +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 +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +3 -3
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +3 -3
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +3 -3
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +10 -8
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- 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 +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +6 -3
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +3 -3
- 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/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/value-safety.js +22 -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 +145 -95
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- 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/storage/keys.js +9 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +16 -14
- 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 +355 -50
- package/documentation/auth.md +775 -243
- package/documentation/code-mode.md +14 -11
- package/documentation/deploying.md +121 -24
- package/documentation/integrating.md +92 -21
- package/documentation/meta-tools.md +14 -9
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +115 -28
- package/documentation/operator-ui.md +326 -61
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +30 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +36 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +3 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +27 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +67 -42
- 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.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,12 @@ function assertFlag(value, path) {
|
|
|
202
184
|
if (typeof value !== "boolean")
|
|
203
185
|
throw new ConfigError(`${path} must be true or false.`);
|
|
204
186
|
}
|
|
187
|
+
/** Shape only; each reference resolves against auth and agents in createConnecta. */
|
|
188
|
+
function assertMemberList(value, path) {
|
|
189
|
+
if (!Array.isArray(value)) {
|
|
190
|
+
throw new ConfigError(`${path} must be an array of principal ids, { namespace, id }, or "agent:<name>".`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
205
193
|
const pool = {
|
|
206
194
|
/**
|
|
207
195
|
* Whether this pool's endpoint refuses every write: write tools are absent
|
|
@@ -219,11 +207,26 @@ const pool = {
|
|
|
219
207
|
},
|
|
220
208
|
})),
|
|
221
209
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
210
|
+
* Who may open `/mcp/<name>`: principal ids in the namespace of the one
|
|
211
|
+
* interactive auth provider, `{ namespace, id }`, or `"agent:<name>"` for a
|
|
212
|
+
* declared agent. Required and non-empty; the pool serves exactly its
|
|
213
|
+
* `tools` to these members and the same 404 to everyone else.
|
|
214
|
+
*/
|
|
215
|
+
members: required(opaque({ check: assertMemberList }), (path) => `${path} is required: list who may open this pool, as principal ids, { namespace, id }, or "agent:<name>".`),
|
|
216
|
+
/**
|
|
217
|
+
* The Google Workspace address agents in this pool act as, on a Workspace
|
|
218
|
+
* connector whose `subject` is a mapping function. Humans always act as
|
|
219
|
+
* themselves through that mapping, and a fixed `subject` address applies to
|
|
220
|
+
* everyone. Without an owner, an agent's Workspace calls fail
|
|
221
|
+
* `auth_required`.
|
|
225
222
|
*/
|
|
226
|
-
|
|
223
|
+
owner: opaque({
|
|
224
|
+
check: (value, path) => {
|
|
225
|
+
if (typeof value !== "string" || !isWorkspaceAddress(value.trim())) {
|
|
226
|
+
throw new ConfigError(`${path} must be a Google Workspace email address.`);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
227
230
|
};
|
|
228
231
|
const icon = {
|
|
229
232
|
src: required(text()),
|
|
@@ -276,15 +279,26 @@ const connectaConfig = {
|
|
|
276
279
|
},
|
|
277
280
|
resolve: (value) => normalizeAuth(value === undefined ? [] : Array.isArray(value) ? value : [value]),
|
|
278
281
|
}),
|
|
279
|
-
/** Code-derived connection visibility and independent management permissions. */
|
|
280
|
-
identity: object(identity),
|
|
281
282
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
283
|
+
* Agents, declared by name: lowercase letters, digits, `_` and `-`, each
|
|
284
|
+
* unique. Each has one `cta_` token slot that operators issue, rotate, and
|
|
285
|
+
* revoke; nothing outside code adds, renames, or rebinds an agent. Removing
|
|
286
|
+
* a name stops its token on the next request. Requires `accessTokens`.
|
|
287
|
+
* An admitted agent carries `identity.agent` and has no principal, so
|
|
288
|
+
* personal connectors are absent for it. Defaults to none.
|
|
289
|
+
*/
|
|
290
|
+
agents: opaque({
|
|
291
|
+
check: (value, path) => assertAgentNames(value, path),
|
|
292
|
+
resolve: (value) => Object.freeze([...(value ?? [])]),
|
|
293
|
+
}),
|
|
294
|
+
/**
|
|
295
|
+
* The people who may use plain `/mcp`, which serves the whole catalog with
|
|
296
|
+
* the deployment's `readOnly`, and who administer the deployment. Same
|
|
297
|
+
* reference forms as pool `members`, people only. Required with inbound
|
|
298
|
+
* auth; a maintainer opens no pool unless listed among its members.
|
|
285
299
|
*/
|
|
286
|
-
|
|
287
|
-
/** Named tool pools, each served at `/mcp/<name>` to
|
|
300
|
+
maintainers: opaque({ check: assertMemberList }),
|
|
301
|
+
/** Named tool pools, each served at `/mcp/<name>` to its members only. */
|
|
288
302
|
pools: record(object(pool)),
|
|
289
303
|
/** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
|
|
290
304
|
classification: record(record(opaque({
|
|
@@ -364,11 +378,11 @@ const connectaConfig = {
|
|
|
364
378
|
}
|
|
365
379
|
},
|
|
366
380
|
}),
|
|
367
|
-
/** Optional
|
|
381
|
+
/** Optional agent tokens for the declared `agents`, created by accessTokens() from /auth/access-tokens. */
|
|
368
382
|
accessTokens: opaque({
|
|
369
383
|
check: (value) => {
|
|
370
384
|
const auth = isObject(value) ? value.auth : undefined;
|
|
371
|
-
if (!hasMethods(value, ["handle"]) ||
|
|
385
|
+
if (!hasMethods(value, ["handle", "slots", "forAgents"]) ||
|
|
372
386
|
!isObject(auth) ||
|
|
373
387
|
typeof auth.authorize !== "function" ||
|
|
374
388
|
auth.interactiveOperator) {
|
|
@@ -436,8 +450,46 @@ const connectaConfig = {
|
|
|
436
450
|
const CONFIG = object(connectaConfig);
|
|
437
451
|
/** Pause-only options #672 removed with `resume_execution`. */
|
|
438
452
|
const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
|
|
453
|
+
/** Access options pools replaced (#837, decisions/0006). */
|
|
454
|
+
const isRetiredPoolGrant = (path) => /^ConnectaConfig\.pools\.[^.]+\.grant$/.test(path);
|
|
455
|
+
/** Management resolvers maintainers and pool membership replaced (#837, decisions/0006). */
|
|
456
|
+
const RETIRED_IDENTITY = [
|
|
457
|
+
["credentialAdministration", "maintainers administer shared credentials and OAuth grants. List them in maintainers."],
|
|
458
|
+
[
|
|
459
|
+
"personalConnection",
|
|
460
|
+
"pool members connect and disconnect their own personal connectors in their pools, and maintainers their own. Add people to a pool's members.",
|
|
461
|
+
],
|
|
462
|
+
[
|
|
463
|
+
"accessTokenManagement",
|
|
464
|
+
"maintainers issue, rotate, and revoke agent tokens from an interactive session. List them in maintainers.",
|
|
465
|
+
],
|
|
466
|
+
["activityAccess", "maintainers read activity; activity.readGate can still narrow it. List them in maintainers."],
|
|
467
|
+
];
|
|
439
468
|
/** `trust`, on the deployment or a pool, was replaced by `readOnly` (#820). */
|
|
440
469
|
const isRetiredTrust = (path) => path === "ConnectaConfig.trust" || (path.startsWith("ConnectaConfig.pools.") && path.endsWith(".trust"));
|
|
470
|
+
/**
|
|
471
|
+
* `identity` was removed whole (#837), so the walk reports only the section.
|
|
472
|
+
* Name each key it still holds, read by descriptor without running a getter
|
|
473
|
+
* or echoing a value, so a retired resolver's message names its replacement.
|
|
474
|
+
*/
|
|
475
|
+
function expandRetiredIdentity(config, paths) {
|
|
476
|
+
if (!paths.includes("ConnectaConfig.identity"))
|
|
477
|
+
return paths;
|
|
478
|
+
let keys = [];
|
|
479
|
+
try {
|
|
480
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(config, "identity");
|
|
481
|
+
const value = descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
482
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
483
|
+
keys = Reflect.ownKeys(value).filter((key) => typeof key === "string");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
keys = [];
|
|
488
|
+
}
|
|
489
|
+
if (keys.length === 0)
|
|
490
|
+
return paths;
|
|
491
|
+
return paths.flatMap((path) => path === "ConnectaConfig.identity" ? keys.map((key) => `ConnectaConfig.identity.${key}`) : [path]);
|
|
492
|
+
}
|
|
441
493
|
function rejectUnknownOptions(paths) {
|
|
442
494
|
if (paths.length === 0)
|
|
443
495
|
return;
|
|
@@ -455,6 +507,21 @@ function rejectUnknownOptions(paths) {
|
|
|
455
507
|
(paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
|
|
456
508
|
? "\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
509
|
: "") +
|
|
510
|
+
(paths.includes("ConnectaConfig.identity.connectorAccess")
|
|
511
|
+
? "\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."
|
|
512
|
+
: "") +
|
|
513
|
+
RETIRED_IDENTITY.filter(([key]) => paths.includes(`ConnectaConfig.identity.${key}`))
|
|
514
|
+
.map(([key, replacement]) => `\nidentity.${key} was removed (#837): ${replacement}`)
|
|
515
|
+
.join("") +
|
|
516
|
+
(paths.some((path) => path === "ConnectaConfig.identity" || path.startsWith("ConnectaConfig.identity."))
|
|
517
|
+
? "\nThe identity section was removed (#837): maintainers and pool members decide access and management. Delete it."
|
|
518
|
+
: "") +
|
|
519
|
+
(paths.includes("ConnectaConfig.grant")
|
|
520
|
+
? "\nThe top-level grant was removed (#837); list the people who may use plain /mcp in maintainers."
|
|
521
|
+
: "") +
|
|
522
|
+
(paths.some(isRetiredPoolGrant)
|
|
523
|
+
? "\nPool grant was removed (#837); list who may open the pool in its members."
|
|
524
|
+
: "") +
|
|
458
525
|
(paths.some(isRetiredTrust)
|
|
459
526
|
? '\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
527
|
: ""));
|
|
@@ -465,6 +532,10 @@ function assertCoherent(config) {
|
|
|
465
532
|
config.discovery.catalogMaxTtlSeconds > 86_400) {
|
|
466
533
|
throw new ConfigError("ConnectaConfig.discovery requires catalogMinTtlSeconds <= catalogMaxTtlSeconds <= 86400.");
|
|
467
534
|
}
|
|
535
|
+
if (config.agents.length > 0 && !config.accessTokens) {
|
|
536
|
+
throw new ConfigError("ConnectaConfig.agents needs accessTokens: accessTokens(storage) from @zackbart/connecta/auth/access-tokens " +
|
|
537
|
+
"holds each agent's token slot.");
|
|
538
|
+
}
|
|
468
539
|
}
|
|
469
540
|
/**
|
|
470
541
|
* Validate a configuration and apply every default. Unknown keys are refused
|
|
@@ -483,7 +554,7 @@ export function resolveConfig(config) {
|
|
|
483
554
|
*/
|
|
484
555
|
export function readConfig(config) {
|
|
485
556
|
const read = readPlain(config, "ConnectaConfig", CONFIG);
|
|
486
|
-
rejectUnknownOptions(read.unknown);
|
|
557
|
+
rejectUnknownOptions(expandRetiredIdentity(config, read.unknown));
|
|
487
558
|
const input = read.value;
|
|
488
559
|
const parsed = CONFIG.parse(input, "ConnectaConfig");
|
|
489
560
|
assertCoherent(parsed);
|
|
@@ -492,10 +563,13 @@ export function readConfig(config) {
|
|
|
492
563
|
name: parsed.serverInfo.name ?? "connecta",
|
|
493
564
|
version: parsed.serverInfo.version ?? CONNECTA_VERSION,
|
|
494
565
|
};
|
|
566
|
+
// The token module admits exactly the agents this configuration declares.
|
|
567
|
+
const accessTokens = parsed.accessTokens?.forAgents(parsed.agents);
|
|
495
568
|
const resolved = Object.freeze({
|
|
496
569
|
...parsed,
|
|
570
|
+
accessTokens,
|
|
497
571
|
// A managed-token module contributes one more provider, ordered with the rest.
|
|
498
|
-
auth:
|
|
572
|
+
auth: accessTokens ? normalizeAuth([accessTokens.auth, ...parsed.auth]) : parsed.auth,
|
|
499
573
|
serverInfo: Object.freeze(info),
|
|
500
574
|
});
|
|
501
575
|
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
|
}
|
|
@@ -9,8 +9,14 @@ export interface ConnectorCaller {
|
|
|
9
9
|
* with or without a subject, a Connecta-issued access token.
|
|
10
10
|
*/
|
|
11
11
|
authenticated: boolean;
|
|
12
|
-
/** The
|
|
12
|
+
/** The pool the call arrived on, at `/mcp/<pool>` or `/agent/<pool>`. */
|
|
13
13
|
pool?: string;
|
|
14
|
+
/**
|
|
15
|
+
* That pool's configured `owner`, the Workspace address an agent acts as
|
|
16
|
+
* (decisions/0006). Core copies it from the admitted pool's config; no
|
|
17
|
+
* request can set it, and humans never act as it.
|
|
18
|
+
*/
|
|
19
|
+
poolOwner?: string;
|
|
14
20
|
}
|
|
15
21
|
export declare function attachCaller(ctx: ConnectorContext, caller: ConnectorCaller | undefined): ConnectorContext;
|
|
16
22
|
export declare function callerOf(ctx: ConnectorContext): ConnectorCaller | undefined;
|
package/dist/connector-caller.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// - maintained providers that act *as* the caller downstream — Google
|
|
9
9
|
// Workspace through domain-wide delegation (`src/providers/_shared/google/`) — which
|
|
10
10
|
// hand the identity to a deployment-config function that names the
|
|
11
|
-
// downstream subject (PRINCIPLES.md INV-3, #678)
|
|
12
|
-
//
|
|
11
|
+
// downstream subject (PRINCIPLES.md INV-3, #678), or, for an agent, use the
|
|
12
|
+
// admitted pool's configured owner (decisions/0006). The provider never
|
|
13
|
+
// decides whose account to open; config does, from this.
|
|
13
14
|
//
|
|
14
15
|
// Core sets it from the authorization that admitted the request, on the scoped
|
|
15
16
|
// registry view that request receives, and `api()` hands handlers that same
|
|
@@ -146,10 +146,33 @@ export interface ApiOptions {
|
|
|
146
146
|
/** The closed options api() accepts; see `assertKnownOptions`. */
|
|
147
147
|
export declare const API_OPTIONS: import("../config-schema.js").Field<unknown, unknown, true> & {
|
|
148
148
|
readonly shape: {
|
|
149
|
-
callAdmission: import("../config-schema.js").Field<unknown,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
maxPartitions: import("../config-schema.js").Field<
|
|
149
|
+
callAdmission: Omit<import("../config-schema.js").Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
|
|
150
|
+
parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
|
|
151
|
+
readonly shape: import("../config-schema.js").Shape & {
|
|
152
|
+
maxPartitions: import("../config-schema.js").Field<number, number | undefined, true>;
|
|
153
|
+
rules: import("../config-schema.js").Field<{
|
|
154
|
+
maxConcurrency?: number | undefined;
|
|
155
|
+
maxQueueSize?: number | undefined;
|
|
156
|
+
queueTimeoutMs?: number | undefined;
|
|
157
|
+
retryAfterMs?: number | undefined;
|
|
158
|
+
partitionKey?: ((input: Readonly<import("../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
|
|
159
|
+
budget?: {
|
|
160
|
+
kind: "rolling-window";
|
|
161
|
+
maxCalls: number;
|
|
162
|
+
windowMs: number;
|
|
163
|
+
} | undefined;
|
|
164
|
+
}[], readonly {
|
|
165
|
+
readonly maxConcurrency: number | undefined;
|
|
166
|
+
readonly maxQueueSize: number | undefined;
|
|
167
|
+
readonly queueTimeoutMs: number | undefined;
|
|
168
|
+
readonly retryAfterMs: number | undefined;
|
|
169
|
+
readonly partitionKey: ((input: Readonly<import("../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
|
|
170
|
+
readonly budget: {
|
|
171
|
+
readonly kind: "rolling-window";
|
|
172
|
+
readonly maxCalls: number;
|
|
173
|
+
readonly windowMs: number;
|
|
174
|
+
} | undefined;
|
|
175
|
+
}[], false>;
|
|
153
176
|
};
|
|
154
177
|
};
|
|
155
178
|
usageGuide: import("../config-schema.js").Field<unknown, unknown, true> & {
|
|
@@ -90,6 +90,8 @@ function describedApiAuth(opts) {
|
|
|
90
90
|
*/
|
|
91
91
|
export function apiConnector(id, opts, oauth) {
|
|
92
92
|
opts = assertKnownOptions(opts, `api(${JSON.stringify(id)})`, API_OPTIONS);
|
|
93
|
+
if (opts.callAdmission !== undefined)
|
|
94
|
+
CALL_ADMISSION.parse(opts.callAdmission, `api(${JSON.stringify(id)}).callAdmission`);
|
|
93
95
|
assertStaticToolNames(opts.tools, `api(${JSON.stringify(id)}).tools`);
|
|
94
96
|
if (opts.oauth !== undefined && oauth === undefined) {
|
|
95
97
|
throw new Error(`api() connector "${id}" declares oauth but was built without its grant; construct it with api().`);
|
|
@@ -1024,6 +1024,8 @@ function registrationErrorCode(body) {
|
|
|
1024
1024
|
*/
|
|
1025
1025
|
export function remoteMcp(id, opts) {
|
|
1026
1026
|
opts = assertKnownOptions(opts, `remoteMcp(${JSON.stringify(id)})`, REMOTE_MCP_OPTIONS);
|
|
1027
|
+
if (opts.callAdmission !== undefined)
|
|
1028
|
+
CALL_ADMISSION.parse(opts.callAdmission, `remoteMcp(${JSON.stringify(id)}).callAdmission`);
|
|
1027
1029
|
const skillsEnabled = opts.skills;
|
|
1028
1030
|
if (skillsEnabled !== undefined && typeof skillsEnabled !== "boolean") {
|
|
1029
1031
|
throw new Error(`[connecta] connector "${id}" skills must be a boolean.`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
2
2
|
import type { ConnectaConfig, ResolvedConfig } from "./config.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResolvedPool } from "./membership.js";
|
|
4
|
+
import type { ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, DescribedEndpoint, IdentityReference } from "./types.js";
|
|
4
5
|
/** Where a limit's value came from. */
|
|
5
6
|
export type ConfigValueSource = "default" | "config";
|
|
6
7
|
/** One limit and where its value came from. `null` means unset (no bound). */
|
|
@@ -8,6 +9,15 @@ export interface DescribedLimit<T = number> {
|
|
|
8
9
|
value: T | null;
|
|
9
10
|
source: ConfigValueSource;
|
|
10
11
|
}
|
|
12
|
+
/** A resolved pool member: a principal, or a declared agent by name. */
|
|
13
|
+
export type DescribedMember = {
|
|
14
|
+
kind: "human";
|
|
15
|
+
namespace: string;
|
|
16
|
+
id: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "agent";
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
11
21
|
/** One connector as `describeConfig()` reports it. */
|
|
12
22
|
export interface DescribedConnector {
|
|
13
23
|
id: string;
|
|
@@ -88,8 +98,11 @@ export interface ConnectaConfigDescription {
|
|
|
88
98
|
};
|
|
89
99
|
/** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
|
|
90
100
|
readOnly: boolean;
|
|
91
|
-
/**
|
|
92
|
-
|
|
101
|
+
/** The principals plain `/mcp` admits, resolved, in declaration order. Empty on an open deployment. */
|
|
102
|
+
maintainers: Array<{
|
|
103
|
+
namespace: string;
|
|
104
|
+
id: string;
|
|
105
|
+
}>;
|
|
93
106
|
classification: Record<string, Record<string, "read" | "write">>;
|
|
94
107
|
/** Inbound auth providers in the order they are tried. */
|
|
95
108
|
auth: Array<{
|
|
@@ -97,14 +110,20 @@ export interface ConnectaConfigDescription {
|
|
|
97
110
|
interactive: boolean;
|
|
98
111
|
ui?: string;
|
|
99
112
|
}>;
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
*/
|
|
102
120
|
pools: Array<{
|
|
103
121
|
name: string;
|
|
104
122
|
path: string;
|
|
105
123
|
tools: string[];
|
|
106
|
-
|
|
124
|
+
members: DescribedMember[];
|
|
107
125
|
readOnly: boolean;
|
|
126
|
+
owner: string | null;
|
|
108
127
|
}>;
|
|
109
128
|
modules: {
|
|
110
129
|
ui: {
|
|
@@ -125,7 +144,6 @@ export interface ConnectaConfigDescription {
|
|
|
125
144
|
};
|
|
126
145
|
accessTokens: {
|
|
127
146
|
enabled: boolean;
|
|
128
|
-
maxActive?: number;
|
|
129
147
|
};
|
|
130
148
|
};
|
|
131
149
|
/** Every accepted store implements `list` and `compareAndSet`, so only its kind is told. */
|
|
@@ -157,6 +175,10 @@ export interface DescribeConfigInput {
|
|
|
157
175
|
registry: import("./registry.js").Registry;
|
|
158
176
|
raw: ConnectaConfig;
|
|
159
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>;
|
|
160
182
|
executorName: string | undefined;
|
|
161
183
|
/** True when the executor owns admission, so `admission.code` is unused. */
|
|
162
184
|
executorAdmits: boolean;
|