@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Code-declared agents (#837): the names deployment code lists in `agents`,
|
|
2
|
+
// the identity an admitted agent carries, and the one provider trusted to
|
|
3
|
+
// assert it. Pure, so core reads it without reaching the token module.
|
|
4
|
+
import { ConfigError } from "./config-schema.js";
|
|
5
|
+
/** Agent names: lowercase letters, digits, `_` and `-`. */
|
|
6
|
+
const AGENT_NAME_RE = /^[a-z0-9_-]+$/u;
|
|
7
|
+
/** Longest accepted agent name, so a name always fits a storage key. */
|
|
8
|
+
const MAX_AGENT_NAME_LENGTH = 64;
|
|
9
|
+
/** Directory namespace of agent subjects and activity actors. */
|
|
10
|
+
export const AGENT_NAMESPACE = "connecta:access-tokens:v1";
|
|
11
|
+
/** Whether `value` is an acceptable agent name. */
|
|
12
|
+
export function isAgentName(value) {
|
|
13
|
+
return typeof value === "string" && value.length <= MAX_AGENT_NAME_LENGTH && AGENT_NAME_RE.test(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The subject id that partitions an agent's per-caller state, such as result
|
|
17
|
+
* handles. The `agent:` prefix keeps it apart from the UUID subjects older
|
|
18
|
+
* tokens used in the same namespace.
|
|
19
|
+
*/
|
|
20
|
+
export function agentSubjectId(name) {
|
|
21
|
+
return `agent:${name}`;
|
|
22
|
+
}
|
|
23
|
+
/** Validate a declared agent list: an array of unique, well-formed names. */
|
|
24
|
+
export function assertAgentNames(value, path) {
|
|
25
|
+
if (!Array.isArray(value))
|
|
26
|
+
throw new ConfigError(`${path} must be an array of agent names.`);
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
value.forEach((name, index) => {
|
|
29
|
+
if (!isAgentName(name)) {
|
|
30
|
+
throw new ConfigError(`${path}[${index}] must be an agent name: 1 to ${MAX_AGENT_NAME_LENGTH} characters of ` +
|
|
31
|
+
"lowercase letters, digits, _ and -.");
|
|
32
|
+
}
|
|
33
|
+
if (seen.has(name))
|
|
34
|
+
throw new ConfigError(`${path}[${index}] repeats the agent name "${name}".`);
|
|
35
|
+
seen.add(name);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const agentProviders = new WeakSet();
|
|
39
|
+
/** Mark the built-in access-token adapter as the one provider whose `agent` is trusted. */
|
|
40
|
+
export function trustAgentProvider(provider) {
|
|
41
|
+
agentProviders.add(provider);
|
|
42
|
+
return provider;
|
|
43
|
+
}
|
|
44
|
+
/** Whether `provider` is the built-in access-token adapter. */
|
|
45
|
+
export function assertsAgents(provider) {
|
|
46
|
+
return agentProviders.has(provider);
|
|
47
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withDeadline } from "../timeout.js";
|
|
1
2
|
import { authorizationCredential } from "../inbound-credential.js";
|
|
2
3
|
import { validIdentityReference } from "../identity.js";
|
|
3
4
|
function identityString(identity, field) {
|
|
@@ -9,7 +10,7 @@ function identityString(identity, field) {
|
|
|
9
10
|
function unauthorized() {
|
|
10
11
|
return {
|
|
11
12
|
ok: false,
|
|
12
|
-
response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"' } }),
|
|
13
|
+
response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"', "Cache-Control": "no-store" } }),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
@@ -34,17 +35,24 @@ export function cloudflareAccessAuth() {
|
|
|
34
35
|
return unauthorized();
|
|
35
36
|
let identity;
|
|
36
37
|
try {
|
|
37
|
-
identity = await access.getIdentity()
|
|
38
|
+
identity = await withDeadline(() => access.getIdentity(), {
|
|
39
|
+
timeoutMs: 5000,
|
|
40
|
+
signal: request.signal,
|
|
41
|
+
timeoutError: new Error("Access identity lookup timed out"),
|
|
42
|
+
});
|
|
38
43
|
}
|
|
39
44
|
catch {
|
|
40
|
-
return
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
response: Response.json({ error: "Cloudflare Access identity unavailable", problem: "identity_unavailable" }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }),
|
|
48
|
+
};
|
|
41
49
|
}
|
|
42
50
|
// Access binds ctx.access.aud to its Worker application at the edge.
|
|
43
51
|
// Service credentials can pass that edge, but connecta machines must
|
|
44
52
|
// present a cta_ token to the machine provider instead.
|
|
45
53
|
const forbidden = () => ({
|
|
46
54
|
ok: false,
|
|
47
|
-
response: Response.json({ error: "Cloudflare Access human identity required" }, { status: 403 }),
|
|
55
|
+
response: Response.json({ error: "Cloudflare Access human identity required" }, { status: 403, headers: { "Cache-Control": "no-store" } }),
|
|
48
56
|
});
|
|
49
57
|
if (!identity || typeof identity !== "object" || Array.isArray(identity))
|
|
50
58
|
return forbidden();
|
|
@@ -56,7 +64,7 @@ export function cloudflareAccessAuth() {
|
|
|
56
64
|
if (!userId) {
|
|
57
65
|
return {
|
|
58
66
|
ok: false,
|
|
59
|
-
response: Response.json({ error: "Cloudflare Access user identity required" }, { status: 403 }),
|
|
67
|
+
response: Response.json({ error: "Cloudflare Access user identity required" }, { status: 403, headers: { "Cache-Control": "no-store" } }),
|
|
60
68
|
};
|
|
61
69
|
}
|
|
62
70
|
return { ok: true, userId, subjectId: userId };
|
package/dist/auth-elicitation.js
CHANGED
|
@@ -148,7 +148,7 @@ export class AuthElicitation {
|
|
|
148
148
|
requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
|
|
149
149
|
});
|
|
150
150
|
// Recheck every entered read before restarting any part of a program.
|
|
151
|
-
// A
|
|
151
|
+
// A writable endpoint cannot authorize replay of a newly classified write.
|
|
152
152
|
for (const read of previous.reads) {
|
|
153
153
|
const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
|
|
154
154
|
if (!current.ok ||
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { chargeCallBudget } from "./runtime/call-admission.js";
|
|
2
|
+
import { runEdge } from "./runtime/run.js";
|
|
3
|
+
const permits = new WeakMap();
|
|
4
|
+
export function attachCallAdmission(ctx, permit) {
|
|
5
|
+
permits.set(ctx, permit);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Charge one sequential auxiliary request using the enclosing concurrency slot.
|
|
9
|
+
* Await before dispatch and propagate refusal. Contexts outside an invocation
|
|
10
|
+
* have no registry policy to charge.
|
|
11
|
+
*/
|
|
12
|
+
export function chargeConnectorCall(ctx) {
|
|
13
|
+
const permit = permits.get(ctx);
|
|
14
|
+
return permit ? runEdge(chargeCallBudget(permit, ctx.signal)) : Promise.resolve();
|
|
15
|
+
}
|
package/dist/call-admission.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Deferred, Duration, Effect } from "effect";
|
|
2
|
+
import { CALL_ADMISSION } from "./provider.js";
|
|
2
3
|
import { ConnectorCallError } from "./errors.js";
|
|
3
|
-
import { provideCallAdmissionProgram, startCallAdmission } from "./runtime/call-admission.js";
|
|
4
|
+
import { provideCallAdmissionProgram, provideCallBudgetCharge, startCallAdmission } from "./runtime/call-admission.js";
|
|
4
5
|
import { fromSignal, runEdge } from "./runtime/run.js";
|
|
5
6
|
const DEFAULT_MAX_QUEUE_SIZE = 32;
|
|
6
7
|
const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
|
|
@@ -50,18 +51,6 @@ export function aggregateCallAdmissionSnapshots(snapshots) {
|
|
|
50
51
|
}
|
|
51
52
|
return aggregate;
|
|
52
53
|
}
|
|
53
|
-
function positiveWhole(value, name) {
|
|
54
|
-
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
|
|
55
|
-
throw new TypeError(`${name} must be a positive whole number.`);
|
|
56
|
-
}
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
function nonNegativeWhole(value, name) {
|
|
60
|
-
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
61
|
-
throw new TypeError(`${name} must be a non-negative whole number.`);
|
|
62
|
-
}
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
54
|
/**
|
|
66
55
|
* Per-runtime, per-connector call admission. State contains only bounded
|
|
67
56
|
* partition keys, counters, timestamps, signals, and promise continuations;
|
|
@@ -88,36 +77,14 @@ export class ConnectorCallAdmissionController {
|
|
|
88
77
|
queueWaitMaxMs = 0;
|
|
89
78
|
constructor(connectorId, policy) {
|
|
90
79
|
this.connectorId = connectorId;
|
|
91
|
-
|
|
92
|
-
throw new TypeError(`connector "${connectorId}" callAdmission.rules must contain exactly one rule in this release.`);
|
|
93
|
-
}
|
|
80
|
+
policy = CALL_ADMISSION.parse(policy, `connector "${connectorId}" callAdmission`);
|
|
94
81
|
const rule = policy.rules[0];
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
: positiveWhole(rule.maxConcurrency, `connector "${connectorId}" callAdmission maxConcurrency`);
|
|
102
|
-
if (this.maxConcurrency === undefined &&
|
|
103
|
-
(rule.maxQueueSize !== undefined || rule.queueTimeoutMs !== undefined || rule.retryAfterMs !== undefined)) {
|
|
104
|
-
throw new TypeError(`connector "${connectorId}" callAdmission queue settings require maxConcurrency.`);
|
|
105
|
-
}
|
|
106
|
-
this.maxQueueSize = nonNegativeWhole(rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, `connector "${connectorId}" callAdmission maxQueueSize`);
|
|
107
|
-
this.queueTimeoutMs = positiveWhole(rule.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS, `connector "${connectorId}" callAdmission queueTimeoutMs`);
|
|
108
|
-
this.retryAfterMs = nonNegativeWhole(rule.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS, `connector "${connectorId}" callAdmission retryAfterMs`);
|
|
109
|
-
this.maxPartitions = positiveWhole(policy.maxPartitions ?? DEFAULT_MAX_PARTITIONS, `connector "${connectorId}" callAdmission maxPartitions`);
|
|
110
|
-
if (rule.budget) {
|
|
111
|
-
if (rule.budget.kind !== "rolling-window") {
|
|
112
|
-
throw new TypeError(`connector "${connectorId}" callAdmission budget kind must be "rolling-window".`);
|
|
113
|
-
}
|
|
114
|
-
const maxCalls = positiveWhole(rule.budget.maxCalls, `connector "${connectorId}" callAdmission budget.maxCalls`);
|
|
115
|
-
const windowMs = positiveWhole(rule.budget.windowMs, `connector "${connectorId}" callAdmission budget.windowMs`);
|
|
116
|
-
this.budget = { maxCalls, windowMs };
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
this.budget = undefined;
|
|
120
|
-
}
|
|
82
|
+
this.maxConcurrency = rule.maxConcurrency;
|
|
83
|
+
this.maxQueueSize = rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
84
|
+
this.queueTimeoutMs = rule.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS;
|
|
85
|
+
this.retryAfterMs = rule.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS;
|
|
86
|
+
this.maxPartitions = policy.maxPartitions ?? DEFAULT_MAX_PARTITIONS;
|
|
87
|
+
this.budget = rule.budget ? { maxCalls: rule.budget.maxCalls, windowMs: rule.budget.windowMs } : undefined;
|
|
121
88
|
this.partitionKey = rule.partitionKey;
|
|
122
89
|
}
|
|
123
90
|
acquire(input) {
|
|
@@ -179,7 +146,7 @@ export class ConnectorCallAdmissionController {
|
|
|
179
146
|
this.queueWaitMaxMs = Math.max(this.queueWaitMaxMs, waitMs);
|
|
180
147
|
}
|
|
181
148
|
let released = false;
|
|
182
|
-
|
|
149
|
+
const permit = {
|
|
183
150
|
waitMs,
|
|
184
151
|
release: () => {
|
|
185
152
|
if (released)
|
|
@@ -190,6 +157,26 @@ export class ConnectorCallAdmissionController {
|
|
|
190
157
|
this.pump(state);
|
|
191
158
|
},
|
|
192
159
|
};
|
|
160
|
+
provideCallBudgetCharge(permit, (chargedAt, signal) => {
|
|
161
|
+
if (this.closed || released) {
|
|
162
|
+
return Effect.fail(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission ${this.closed ? "is closed" : "permit is released"}.`));
|
|
163
|
+
}
|
|
164
|
+
if (signal?.aborted) {
|
|
165
|
+
this.cancelledTotal++;
|
|
166
|
+
return Effect.fail(this.cancelled(signal));
|
|
167
|
+
}
|
|
168
|
+
this.pruneBudget(state, chargedAt);
|
|
169
|
+
const retryAfterMs = this.budgetRetryAfterMs(state, chargedAt);
|
|
170
|
+
if (retryAfterMs !== undefined) {
|
|
171
|
+
this.rateLimitedTotal++;
|
|
172
|
+
return Effect.fail(this.budgetLimited(retryAfterMs));
|
|
173
|
+
}
|
|
174
|
+
if (this.budget)
|
|
175
|
+
state.admittedAt.push(chargedAt);
|
|
176
|
+
this.admittedTotal++;
|
|
177
|
+
return Effect.void;
|
|
178
|
+
});
|
|
179
|
+
return permit;
|
|
193
180
|
}
|
|
194
181
|
pump(state) {
|
|
195
182
|
if (this.closed || this.maxConcurrency === undefined)
|
package/dist/call-recovery.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
const MAX_FACTS = 4;
|
|
2
2
|
const MAX_TEXT = 128;
|
|
3
|
+
// Shared with the REST write schema so every accepted key survives recovery.
|
|
4
|
+
export const IDEMPOTENCY_KEY_MAX_LENGTH = 255;
|
|
3
5
|
const NAME = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
4
6
|
const facts = new WeakMap();
|
|
5
7
|
/**
|
|
6
8
|
* Record facts that let the caller reconcile or safely retry this call's
|
|
7
9
|
* write. Bounded: at most four string facts with short camelCase names; a fact
|
|
8
|
-
* outside the bound is dropped rather than truncated.
|
|
10
|
+
* outside the bound is dropped rather than truncated. Idempotency keys allow
|
|
11
|
+
* 255 characters to match the REST write schema; other values allow 128.
|
|
9
12
|
*/
|
|
10
13
|
export function recordRecovery(ctx, recovery) {
|
|
11
14
|
const kept = { ...facts.get(ctx) };
|
|
12
15
|
for (const [name, value] of Object.entries(recovery)) {
|
|
13
16
|
if (Object.keys(kept).length >= MAX_FACTS && !Object.hasOwn(kept, name))
|
|
14
17
|
break;
|
|
15
|
-
|
|
18
|
+
const maxLength = name === "idempotencyKey" ? IDEMPOTENCY_KEY_MAX_LENGTH : MAX_TEXT;
|
|
19
|
+
if (!NAME.test(name) || name.length > 32 || typeof value !== "string" || value.length > maxLength)
|
|
16
20
|
continue;
|
|
17
21
|
kept[name] = value;
|
|
18
22
|
}
|
package/dist/catalog-service.js
CHANGED
|
@@ -377,6 +377,7 @@ export class CatalogService {
|
|
|
377
377
|
concurrency;
|
|
378
378
|
searchRoute;
|
|
379
379
|
requestSignal;
|
|
380
|
+
readOnly;
|
|
380
381
|
// The request-scoped catalog cache: one shared read per connector asked
|
|
381
382
|
// about, started by the first asker and joined by every later one. A
|
|
382
383
|
// success stays for the rest of the request, so a search and the call after
|
|
@@ -401,6 +402,7 @@ export class CatalogService {
|
|
|
401
402
|
this.searchRoute = options.searchRoute ?? "search_tools";
|
|
402
403
|
this.requestSignal = options.requestSignal;
|
|
403
404
|
this.defer = options.defer;
|
|
405
|
+
this.readOnly = options.readOnly === true;
|
|
404
406
|
}
|
|
405
407
|
/**
|
|
406
408
|
* Send a caller back to discovery through the surface it can actually reach.
|
|
@@ -460,9 +462,12 @@ export class CatalogService {
|
|
|
460
462
|
// each settling in its input slot: an unavailable catalog is a Failure
|
|
461
463
|
// there, not a failed discovery. `route` names the surface in a timeout.
|
|
462
464
|
// A probe also ends with its request, so no listing outlives the caller
|
|
463
|
-
// that asked for it.
|
|
465
|
+
// that asked for it. On a read-only endpoint a write is absent from search
|
|
466
|
+
// and describe, as if the connector never had it. Guides are not filtered.
|
|
464
467
|
discoveryCatalogs(ids, route) {
|
|
465
|
-
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.
|
|
468
|
+
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.readOnly
|
|
469
|
+
? Effect.map(this.catalog(id, { signal }), (tools) => tools.filter((tool) => tool.classification === "read"))
|
|
470
|
+
: this.catalog(id, { signal }), {
|
|
466
471
|
...(this.requestSignal ? { signal: this.requestSignal } : {}),
|
|
467
472
|
timeoutMs: this.probeTimeoutMs,
|
|
468
473
|
timeoutError: new ConnectorCallError("timeout", `${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
|
|
@@ -44,7 +44,6 @@ function factorySource(sources, parts) {
|
|
|
44
44
|
export function describeConfigSources(snapshot, raw) {
|
|
45
45
|
const sources = {};
|
|
46
46
|
const connectorSources = new Map(raw.connectors.map((connector) => [connector.id, factorySources(connector)]));
|
|
47
|
-
const moduleSources = { accessTokens: factorySources(raw.accessTokens) };
|
|
48
47
|
const provided = (value) => (value === undefined ? "default" : "config");
|
|
49
48
|
for (const path of configValuePaths(snapshot)) {
|
|
50
49
|
const parts = path.split(".").slice(1);
|
|
@@ -56,10 +55,12 @@ export function describeConfigSources(snapshot, raw) {
|
|
|
56
55
|
source = provided(raw.serverInfo?.[key]);
|
|
57
56
|
else if (group === "urls")
|
|
58
57
|
source = key === "mcpPath" ? "default" : provided(raw[key]);
|
|
59
|
-
else if (group === "
|
|
60
|
-
source = provided(raw.
|
|
61
|
-
else if (group === "
|
|
62
|
-
source = provided(raw.
|
|
58
|
+
else if (group === "maintainers")
|
|
59
|
+
source = provided(raw.maintainers);
|
|
60
|
+
else if (group === "readOnly")
|
|
61
|
+
source = provided(raw.readOnly);
|
|
62
|
+
else if (group === "agents")
|
|
63
|
+
source = provided(raw.agents);
|
|
63
64
|
else if (group === "storage")
|
|
64
65
|
source = provided(raw.storage);
|
|
65
66
|
else if (group === "deploymentInfo")
|
|
@@ -67,11 +68,15 @@ export function describeConfigSources(snapshot, raw) {
|
|
|
67
68
|
else if (group === "classification")
|
|
68
69
|
source = provided(raw.classification);
|
|
69
70
|
else if (group === "pools")
|
|
70
|
-
|
|
71
|
+
// An inherited value comes from wherever the deployment's came from.
|
|
72
|
+
source =
|
|
73
|
+
field === "readOnly"
|
|
74
|
+
? provided(raw.pools?.[key]?.readOnly ?? raw.readOnly)
|
|
75
|
+
: field === "owner"
|
|
76
|
+
? provided(raw.pools?.[key]?.owner)
|
|
77
|
+
: provided(raw.pools);
|
|
71
78
|
else if (group === "modules") {
|
|
72
|
-
|
|
73
|
-
const factory = moduleSources[key];
|
|
74
|
-
source = (factory ? factorySource(factory, parts.slice(2)) : undefined) ?? provided(module);
|
|
79
|
+
source = provided(key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens);
|
|
75
80
|
}
|
|
76
81
|
else if (group === "branding") {
|
|
77
82
|
const brand = raw.ui?.branding;
|
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
|
|
@@ -76,7 +74,7 @@ declare const execute: {
|
|
|
76
74
|
*/
|
|
77
75
|
watchdogMs: import("./config-schema.js").Field<number, number, true>;
|
|
78
76
|
/**
|
|
79
|
-
*
|
|
77
|
+
* Writes one program may send on a writable endpoint, on top of the host-call
|
|
80
78
|
* budget every call already spends. Default 10.
|
|
81
79
|
*/
|
|
82
80
|
maxWrites: import("./config-schema.js").Field<number, number, true>;
|
|
@@ -162,89 +160,64 @@ declare const admission: {
|
|
|
162
160
|
};
|
|
163
161
|
};
|
|
164
162
|
};
|
|
165
|
-
declare const
|
|
163
|
+
declare const pool: {
|
|
166
164
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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.
|
|
165
|
+
* Whether this pool's endpoint refuses every write: write tools are absent
|
|
166
|
+
* from search_tools, tool listings, and describe, and every write call is
|
|
167
|
+
* refused. Inherits the deployment's `readOnly` unless set here, in either
|
|
168
|
+
* direction.
|
|
174
169
|
*/
|
|
175
|
-
|
|
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
|
-
declare const pool: {
|
|
186
|
-
/** Programs may write only in trusted pools. Default read-only. */
|
|
187
|
-
trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
|
|
170
|
+
readOnly: import("./config-schema.js").Field<boolean, boolean | undefined, true>;
|
|
188
171
|
/** Connector ids and exact `connector.tool` addresses in this pool. */
|
|
189
172
|
tools: import("./config-schema.js").Field<readonly string[], readonly string[], false>;
|
|
190
173
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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, an agent's Workspace calls fail
|
|
185
|
+
* `auth_required`.
|
|
194
186
|
*/
|
|
195
|
-
|
|
187
|
+
owner: import("./config-schema.js").Field<string, string | undefined, true>;
|
|
196
188
|
};
|
|
197
189
|
declare const connectaConfig: {
|
|
198
190
|
connectors: import("./config-schema.js").Field<Connector[], readonly Connector[], false>;
|
|
199
191
|
/** Inbound auth adapters, such as clerkAuth(...); omit for open (dev). */
|
|
200
192
|
auth: import("./config-schema.js").Field<InboundAuth | InboundAuth[], readonly InboundAuth[], true>;
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
* whose entries are connector ids (the whole connector) and `connector.tool`
|
|
219
|
-
* addresses (that tool only), or `{ tool: "connector.tool",
|
|
220
|
-
* requireReadOnly: true }` for an exact tool only while its loaded catalog
|
|
221
|
-
* explicitly classifies it read-only. Grants are additive: an unrestricted
|
|
222
|
-
* connector or address grant wins over the guarded form. An address naming
|
|
223
|
-
* a tool the catalog lacks is unreachable and warned once, never widened.
|
|
224
|
-
*/
|
|
225
|
-
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>;
|
|
226
|
-
/** Global payload-free activity reads. Defaults to interactive humans. */
|
|
227
|
-
activityAccess: import("./config-schema.js").Field<(principal: Readonly<IdentityReference>) => boolean | Promise<boolean>, ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined, true>;
|
|
228
|
-
/** Shared credential and OAuth administration. Defaults to none. */
|
|
229
|
-
credentialAdministration: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
|
|
230
|
-
/** Client-token lifecycle management by interactive humans. Defaults to false. */
|
|
231
|
-
accessTokenManagement: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
|
|
232
|
-
/** Connecting or changing the caller's personal account. Defaults to none. */
|
|
233
|
-
personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
|
|
234
|
-
};
|
|
235
|
-
};
|
|
236
|
-
/** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
|
|
193
|
+
/**
|
|
194
|
+
* Agents, declared by name: lowercase letters, digits, `_` and `-`, each
|
|
195
|
+
* unique. Each has one `cta_` token slot that operators issue, rotate, and
|
|
196
|
+
* revoke; nothing outside code adds, renames, or rebinds an agent. Removing
|
|
197
|
+
* a name stops its token on the next request. Requires `accessTokens`.
|
|
198
|
+
* An admitted agent carries `identity.agent` and has no principal, so
|
|
199
|
+
* personal connectors are absent for it. Defaults to none.
|
|
200
|
+
*/
|
|
201
|
+
agents: import("./config-schema.js").Field<readonly string[], readonly string[], true>;
|
|
202
|
+
/**
|
|
203
|
+
* The people who may use plain `/mcp`, which serves the whole catalog with
|
|
204
|
+
* the deployment's `readOnly`, and who administer the deployment. Same
|
|
205
|
+
* reference forms as pool `members`, people only. Required with inbound
|
|
206
|
+
* auth; a maintainer opens no pool unless listed among its members.
|
|
207
|
+
*/
|
|
208
|
+
maintainers: import("./config-schema.js").Field<readonly MemberRef[], readonly MemberRef[] | undefined, true>;
|
|
209
|
+
/** Named tool pools, each served at `/mcp/<name>` to its members only. */
|
|
237
210
|
pools: import("./config-schema.js").Field<Record<string, {
|
|
238
|
-
|
|
239
|
-
|
|
211
|
+
readOnly?: boolean | undefined;
|
|
212
|
+
owner?: string | undefined;
|
|
240
213
|
tools: readonly string[];
|
|
214
|
+
members: readonly MemberRef[];
|
|
241
215
|
}>, Readonly<Record<string, {
|
|
242
|
-
readonly
|
|
216
|
+
readonly readOnly: boolean | undefined;
|
|
243
217
|
readonly tools: readonly string[];
|
|
244
|
-
readonly
|
|
218
|
+
readonly members: readonly MemberRef[];
|
|
219
|
+
readonly owner: string | undefined;
|
|
245
220
|
}>> | undefined, true>;
|
|
246
|
-
/** Trust of the default /mcp endpoint. Default read-only. */
|
|
247
|
-
trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
|
|
248
221
|
/** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
|
|
249
222
|
classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;
|
|
250
223
|
/**
|
|
@@ -272,7 +245,7 @@ declare const connectaConfig: {
|
|
|
272
245
|
vault: import("./config-schema.js").Field<CredentialVault, CredentialVault | undefined, true>;
|
|
273
246
|
/** Optional connection UI, created by operatorUi() from /ui. */
|
|
274
247
|
ui: import("./config-schema.js").Field<OperatorSurface, OperatorSurface | undefined, true>;
|
|
275
|
-
/** Optional
|
|
248
|
+
/** Optional agent tokens for the declared `agents`, created by accessTokens() from /auth/access-tokens. */
|
|
276
249
|
accessTokens: import("./config-schema.js").Field<AccessTokensModule, AccessTokensModule | undefined, true>;
|
|
277
250
|
/** Tool-catalog TTL bounds and probe deadlines. */
|
|
278
251
|
discovery: import("./config-schema.js").Field<{
|
|
@@ -394,7 +367,7 @@ declare const connectaConfig: {
|
|
|
394
367
|
*/
|
|
395
368
|
watchdogMs: import("./config-schema.js").Field<number, number, true>;
|
|
396
369
|
/**
|
|
397
|
-
*
|
|
370
|
+
* Writes one program may send on a writable endpoint, on top of the host-call
|
|
398
371
|
* budget every call already spends. Default 10.
|
|
399
372
|
*/
|
|
400
373
|
maxWrites: import("./config-schema.js").Field<number, number, true>;
|
|
@@ -548,6 +521,15 @@ declare const connectaConfig: {
|
|
|
548
521
|
* `customExecutor(myExecutor, { lifecycle: "self-managed" })`.
|
|
549
522
|
*/
|
|
550
523
|
executor: import("./config-schema.js").Field<Executor, Executor, false>;
|
|
524
|
+
/**
|
|
525
|
+
* Required. Whether the default /mcp endpoint refuses every write. `true`:
|
|
526
|
+
* write tools are absent from search_tools, tool listings, and describe,
|
|
527
|
+
* and call_tool, call_destructive_tool, and program writes are refused.
|
|
528
|
+
* `false`: call_destructive_tool and programs may write, and the MCP
|
|
529
|
+
* client owns approval. Pools inherit it unless they set their own. It has
|
|
530
|
+
* no default while deployments migrate from `trust` (#820).
|
|
531
|
+
*/
|
|
532
|
+
readOnly: import("./config-schema.js").Field<boolean, boolean, false>;
|
|
551
533
|
};
|
|
552
534
|
/** A deployment's whole configuration, passed to `createConnecta`. */
|
|
553
535
|
export type ConnectaConfig = ConfigInput<typeof connectaConfig>;
|
|
@@ -563,11 +545,9 @@ export type AdmissionPoolConfig = ConfigInput<typeof codeAdmission>;
|
|
|
563
545
|
export type RequestAdmissionConfig = ConfigInput<typeof requestAdmission>;
|
|
564
546
|
/** Runtime-portable server-memory boundaries for `/mcp` and fallback code-mode work. */
|
|
565
547
|
export type ConnectaAdmissionConfig = ConfigInput<typeof admission>;
|
|
566
|
-
export type ConnectaIdentityConfig = ConfigInput<typeof identity>;
|
|
567
548
|
/**
|
|
568
|
-
* A named tool pool served at `/mcp/<name
|
|
569
|
-
*
|
|
570
|
-
* remains its ceiling and the pool can only narrow it.
|
|
549
|
+
* A named tool pool served at `/mcp/<name>`: exactly its `tools`, to its
|
|
550
|
+
* `members` only.
|
|
571
551
|
*/
|
|
572
552
|
export type ConnectaPoolConfig = ConfigInput<typeof pool>;
|
|
573
553
|
type Parsed = ConfigOutput<typeof connectaConfig>;
|