@zackbart/connecta 0.30.0 → 0.32.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 +66 -0
- package/README.md +21 -16
- package/bin/connecta.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-service.js +7 -2
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config-value-sources.js +6 -3
- package/dist/config.d.ts +26 -8
- package/dist/config.js +38 -16
- package/dist/connector-access.d.ts +2 -1
- package/dist/connectors/guarded-fetch.d.ts +18 -1
- package/dist/connectors/guarded-fetch.js +21 -2
- package/dist/describe-config.d.ts +6 -3
- package/dist/describe-config.js +3 -2
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.js +4 -3
- package/dist/invocation.js +39 -4
- package/dist/meta-tools.js +17 -19
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -0
- package/dist/operator-ui/contract.d.ts +5 -4
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -1
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +12 -0
- package/dist/program-writes.js +4 -49
- package/dist/provider.d.ts +6 -5
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.js +1 -1
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.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 +274 -23
- 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 +35 -31
- package/dist/providers/notion/index.js +76 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1186 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +286 -24
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +1 -1
- package/dist/providers/sheets/index.js +3 -3
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -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/routes/mcp.js +30 -10
- package/dist/routes/operator.js +14 -7
- package/dist/routes/ui-config.js +2 -2
- package/dist/server.js +3 -0
- package/dist/skills.js +5 -4
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +155 -28
- package/documentation/auth.md +86 -27
- package/documentation/code-mode.md +81 -72
- package/documentation/deploying.md +10 -5
- package/documentation/integrating.md +72 -9
- package/documentation/meta-tools.md +52 -38
- package/documentation/operating.md +11 -8
- package/documentation/operator-tests.md +2 -3
- package/documentation/operator-ui.md +9 -3
- package/documentation/provider-auth-migration.md +308 -0
- package/documentation/provider-migration-0.29.md +18 -6
- package/examples/worker/AGENTS.md +5 -0
- package/examples/worker/README.md +2 -2
- package/examples/worker/src/connecta.config.ts +5 -0
- package/package.json +7 -2
- package/templates/node/AGENTS.md +6 -4
- package/templates/node/README.md +6 -4
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +5 -0
- package/dist/providers/vercel/mcp-ownership.js +0 -212
- package/dist/tool-safety.d.ts +0 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConnectorContext } from "../types.js";
|
|
2
2
|
/** The methods a hand-written provider surface actually uses. */
|
|
3
|
-
type GuardedMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
3
|
+
type GuardedMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
4
|
export interface GuardedRequest {
|
|
5
5
|
method: GuardedMethod;
|
|
6
6
|
/**
|
|
@@ -21,6 +21,14 @@ export interface GuardedRequest {
|
|
|
21
21
|
* shadow it.
|
|
22
22
|
*/
|
|
23
23
|
headers?: Record<string, string | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Headers whose values the tool returns to its caller by contract, such as
|
|
26
|
+
* an idempotency key. They are added after the request's sent secrets are
|
|
27
|
+
* registered, so a name like `Idempotency-Key` does not mark the value a
|
|
28
|
+
* credential and redact it from the result that must carry it. They may not
|
|
29
|
+
* collide with any other header.
|
|
30
|
+
*/
|
|
31
|
+
visibleHeaders?: Record<string, string>;
|
|
24
32
|
/** JSON request body. Serialized here, with the `Content-Type` to match. */
|
|
25
33
|
body?: unknown;
|
|
26
34
|
/**
|
|
@@ -48,6 +56,8 @@ interface GuardedResponse {
|
|
|
48
56
|
readonly status: number;
|
|
49
57
|
readonly ok: boolean;
|
|
50
58
|
readonly headers: Headers;
|
|
59
|
+
/** Cancel an unread body without waiting for the vendor to emit a chunk. */
|
|
60
|
+
cancel(): Promise<void>;
|
|
51
61
|
/** The body as bytes. */
|
|
52
62
|
bytes(): Promise<Uint8Array>;
|
|
53
63
|
/** The body decoded as UTF-8. */
|
|
@@ -116,6 +126,13 @@ export interface GuardedFetchOptions {
|
|
|
116
126
|
* absurd response is a fact about the API, not about HTTP.
|
|
117
127
|
*/
|
|
118
128
|
maxResponseBytes: number;
|
|
129
|
+
/**
|
|
130
|
+
* Inspect the exact URL about to be fetched — confined, normalized, with its
|
|
131
|
+
* query encoded — and throw to refuse it. A policy that reads this URL reads
|
|
132
|
+
* what the downstream server will route, not a spelling of it. Runs before
|
|
133
|
+
* authentication, so a refused request never resolves a credential.
|
|
134
|
+
*/
|
|
135
|
+
admit?: (url: URL, request: GuardedRequest) => void;
|
|
119
136
|
}
|
|
120
137
|
/** Sends one already-guarded request on behalf of one call's context. */
|
|
121
138
|
type GuardedFetcher = (url: string, init: RequestInit, ctx: ConnectorContext) => Promise<Response>;
|
|
@@ -181,6 +181,9 @@ function boundedResponse(provider, response, limit) {
|
|
|
181
181
|
status: response.status,
|
|
182
182
|
ok: response.ok,
|
|
183
183
|
headers: response.headers,
|
|
184
|
+
cancel: async () => {
|
|
185
|
+
await response.body?.cancel();
|
|
186
|
+
},
|
|
184
187
|
bytes: async () => new Uint8Array(),
|
|
185
188
|
text: async () => "",
|
|
186
189
|
json: emptyJson,
|
|
@@ -228,6 +231,9 @@ function boundedResponse(provider, response, limit) {
|
|
|
228
231
|
status: response.status,
|
|
229
232
|
ok: response.ok,
|
|
230
233
|
headers: response.headers,
|
|
234
|
+
cancel: async () => {
|
|
235
|
+
await response.body?.cancel();
|
|
236
|
+
},
|
|
231
237
|
bytes,
|
|
232
238
|
text,
|
|
233
239
|
json,
|
|
@@ -307,6 +313,7 @@ export function guardedFetch(options) {
|
|
|
307
313
|
throw new Error(`A ${provider} request cannot carry both a JSON body and a raw body.`);
|
|
308
314
|
}
|
|
309
315
|
const url = confinedUrl(provider, base, basePath, request);
|
|
316
|
+
options.admit?.(url, request);
|
|
310
317
|
const headers = { ...constantHeaders };
|
|
311
318
|
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
312
319
|
if (value !== undefined)
|
|
@@ -343,6 +350,13 @@ export function guardedFetch(options) {
|
|
|
343
350
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
344
351
|
};
|
|
345
352
|
sentSecretsFor(ctx).request(url, init);
|
|
353
|
+
for (const [name, value] of Object.entries(request.visibleHeaders ?? {})) {
|
|
354
|
+
const shadow = hasHeader(headers, name);
|
|
355
|
+
if (shadow) {
|
|
356
|
+
throw new ConnectorCallError("invalid_args", `A ${provider} request sets the ${shadow} header twice.`);
|
|
357
|
+
}
|
|
358
|
+
headers[name] = value;
|
|
359
|
+
}
|
|
346
360
|
response = await send(url.toString(), init, ctx);
|
|
347
361
|
}
|
|
348
362
|
catch (cause) {
|
|
@@ -356,8 +370,13 @@ export function guardedFetch(options) {
|
|
|
356
370
|
}
|
|
357
371
|
const declared = Number(response.headers.get("content-length"));
|
|
358
372
|
// A prefix read takes only what it was asked for, so a long body is not
|
|
359
|
-
// a reason to refuse it; the ceiling still bounds what is read.
|
|
360
|
-
|
|
373
|
+
// a reason to refuse it; the ceiling still bounds what is read. A HEAD
|
|
374
|
+
// response has no body: its Content-Length describes the GET
|
|
375
|
+
// representation, which is exactly what a HEAD asks about.
|
|
376
|
+
if (Number.isFinite(declared) &&
|
|
377
|
+
declared > limit &&
|
|
378
|
+
request.method !== "HEAD" &&
|
|
379
|
+
!(request.prefixOnly && response.ok)) {
|
|
361
380
|
await response.body?.cancel().catch(() => { });
|
|
362
381
|
throw oversized(provider, limit, `a declared ${declared} bytes`);
|
|
363
382
|
}
|
|
@@ -86,8 +86,10 @@ export interface ConnectaConfigDescription {
|
|
|
86
86
|
execute: Record<keyof typeof CONFIG_DEFAULTS.execute, DescribedLimit>;
|
|
87
87
|
requests: Record<keyof typeof CONFIG_DEFAULTS.admission.requests, DescribedLimit>;
|
|
88
88
|
};
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
|
|
90
|
+
readOnly: boolean;
|
|
91
|
+
/** Whether deployment code configures a grant for plain `/mcp`. */
|
|
92
|
+
hasGrant: boolean;
|
|
91
93
|
classification: Record<string, Record<string, "read" | "write">>;
|
|
92
94
|
/** Inbound auth providers in the order they are tried. */
|
|
93
95
|
auth: Array<{
|
|
@@ -96,12 +98,13 @@ export interface ConnectaConfigDescription {
|
|
|
96
98
|
ui?: string;
|
|
97
99
|
}>;
|
|
98
100
|
identity: Record<"connectorAccess" | "activityAccess" | "credentialAdministration" | "accessTokenManagement" | "personalConnection", "default" | "custom">;
|
|
101
|
+
/** `readOnly` is resolved: the pool's own value, else the deployment's. */
|
|
99
102
|
pools: Array<{
|
|
100
103
|
name: string;
|
|
101
104
|
path: string;
|
|
102
105
|
tools: string[];
|
|
103
106
|
hasGrant: boolean;
|
|
104
|
-
|
|
107
|
+
readOnly: boolean;
|
|
105
108
|
}>;
|
|
106
109
|
modules: {
|
|
107
110
|
ui: {
|
package/dist/describe-config.js
CHANGED
|
@@ -240,7 +240,8 @@ export function describeConfig(input) {
|
|
|
240
240
|
execute: limitGroup(config.execute, ownValue(raw, "execute"), Object.keys(D.execute)),
|
|
241
241
|
requests: limitGroup(config.admission.requests, ownValue(ownValue(raw, "admission"), "requests"), Object.keys(D.admission.requests)),
|
|
242
242
|
},
|
|
243
|
-
|
|
243
|
+
readOnly: config.readOnly,
|
|
244
|
+
hasGrant: typeof config.grant === "function",
|
|
244
245
|
classification: structuredClone(config.classification ?? {}),
|
|
245
246
|
auth: config.auth.map((provider) => ({
|
|
246
247
|
kind: String(provider.kind),
|
|
@@ -249,7 +250,7 @@ export function describeConfig(input) {
|
|
|
249
250
|
})),
|
|
250
251
|
identity: Object.fromEntries(IDENTITY_KEYS.map((key) => [key, typeof config.identity[key] === "function" ? "custom" : "default"])),
|
|
251
252
|
pools: Object.entries(config.pools ?? {}).map(([name, pool]) => ({
|
|
252
|
-
|
|
253
|
+
readOnly: pool.readOnly ?? config.readOnly,
|
|
253
254
|
name,
|
|
254
255
|
path: `/mcp/${name}`,
|
|
255
256
|
tools: pool.tools.map(String),
|
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. */
|
|
@@ -282,6 +282,8 @@ export interface CallErrorDetails extends Partial<AuthorizationHandoff> {
|
|
|
282
282
|
args?: unknown;
|
|
283
283
|
argsOmitted?: true;
|
|
284
284
|
argsRedacted?: true;
|
|
285
|
+
/** Facts the connector recorded before dispatch, such as a generated `idempotencyKey`. */
|
|
286
|
+
recovery?: Readonly<Record<string, string>>;
|
|
285
287
|
};
|
|
286
288
|
/** Explicit retry guidance; recovery never retries or mutates by itself. */
|
|
287
289
|
retry?: string;
|
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.js
CHANGED
|
@@ -16,7 +16,7 @@ export { defineConfig } from "./config.js";
|
|
|
16
16
|
* catalog lacks it. Remote catalogs load lazily, so their addresses are
|
|
17
17
|
* checked at catalog load instead and stay unreachable until they match.
|
|
18
18
|
*/
|
|
19
|
-
function resolvePools(pools, registry) {
|
|
19
|
+
function resolvePools(pools, readOnly, registry) {
|
|
20
20
|
const resolved = new Map();
|
|
21
21
|
if (!pools)
|
|
22
22
|
return resolved;
|
|
@@ -52,7 +52,8 @@ function resolvePools(pools, registry) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// A pool inherits the deployment value unless it sets its own, either way.
|
|
56
|
+
resolved.set(name, { access, readOnly: pool.readOnly ?? readOnly, grant: pool.grant ?? (() => false) });
|
|
56
57
|
}
|
|
57
58
|
return resolved;
|
|
58
59
|
}
|
|
@@ -190,7 +191,7 @@ export function createConnecta(config) {
|
|
|
190
191
|
results: resolved.results,
|
|
191
192
|
classification: resolved.classification,
|
|
192
193
|
});
|
|
193
|
-
const pools = resolvePools(resolved.pools, registry);
|
|
194
|
+
const pools = resolvePools(resolved.pools, resolved.readOnly, registry);
|
|
194
195
|
warnInsecureConfig(resolved);
|
|
195
196
|
const requestAdmission = new AdmissionController(resolved.admission.requests);
|
|
196
197
|
let codeAdmission;
|
package/dist/invocation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
@@ -10,6 +10,8 @@ import { splitAddress } from "./registry.js";
|
|
|
10
10
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
11
11
|
import { validateCatalogToolInput } from "./validate.js";
|
|
12
12
|
import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
|
|
13
|
+
import { recoveryFor } from "./call-recovery.js";
|
|
14
|
+
import { classifyWriteOutcome } from "./write-outcome.js";
|
|
13
15
|
import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
|
|
14
16
|
import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
|
|
15
17
|
function defined(values) {
|
|
@@ -166,6 +168,9 @@ export class InvocationService {
|
|
|
166
168
|
let attempts = 0;
|
|
167
169
|
let resultBytes;
|
|
168
170
|
let dispatchedToConnector = false;
|
|
171
|
+
// The context a dispatched call ran with, read for recovery facts the
|
|
172
|
+
// connector recorded before an interrupting deadline (call-recovery.ts).
|
|
173
|
+
let dispatchContext;
|
|
169
174
|
let preInvocationAuthFailure = false;
|
|
170
175
|
let answered = false;
|
|
171
176
|
const sentSecrets = sentSecretsForRequest(this.catalog.requestScope);
|
|
@@ -217,6 +222,10 @@ export class InvocationService {
|
|
|
217
222
|
const enrich = (error, target) => {
|
|
218
223
|
if (!target)
|
|
219
224
|
return error;
|
|
225
|
+
const recovery = recoveryFor(dispatchContext);
|
|
226
|
+
const recoveryRetry = recovery?.["idempotencyKey"]
|
|
227
|
+
? " A deliberate retry with uncertainCall.recovery.idempotencyKey and the exact original arguments cannot repeat the write while the vendor retains that key (Stripe keeps v1 keys at least 24 hours); after that, look the object up before retrying."
|
|
228
|
+
: "";
|
|
220
229
|
if (isTimeoutFailure(error) && dispatchedToConnector && resolved?.definition.classification === "write") {
|
|
221
230
|
const echoed = argumentEcho;
|
|
222
231
|
return {
|
|
@@ -230,8 +239,10 @@ export class InvocationService {
|
|
|
230
239
|
address: `${target.connector.id}.${target.toolName}`,
|
|
231
240
|
...echoed,
|
|
232
241
|
...("args" in echoed ? {} : { argsOmitted: true }),
|
|
242
|
+
...(recovery ? { recovery } : {}),
|
|
233
243
|
},
|
|
234
244
|
retry: "Do not retry automatically. Check whether the write took effect first." +
|
|
245
|
+
recoveryRetry +
|
|
235
246
|
(echoed.argsRedacted
|
|
236
247
|
? " Sensitive fields are omitted; use the original arguments if reconciliation requires another call."
|
|
237
248
|
: "args" in echoed
|
|
@@ -239,6 +250,26 @@ export class InvocationService {
|
|
|
239
250
|
: " The arguments could not be echoed safely; use the exact arguments you sent."),
|
|
240
251
|
};
|
|
241
252
|
}
|
|
253
|
+
// Any other dispatched write whose outcome is unknown (a lost response
|
|
254
|
+
// body, a 5xx, an unreadable reply) keeps the facts its connector
|
|
255
|
+
// recorded before dispatch, here and through program write accounting.
|
|
256
|
+
if (recovery &&
|
|
257
|
+
!error.uncertainCall &&
|
|
258
|
+
dispatchedToConnector &&
|
|
259
|
+
resolved?.definition.classification === "write" &&
|
|
260
|
+
classifyWriteOutcome({ ok: false, dispatched: true, answered, error }) === "unknown") {
|
|
261
|
+
const echoed = argumentEcho;
|
|
262
|
+
error = {
|
|
263
|
+
...error,
|
|
264
|
+
uncertainCall: {
|
|
265
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
266
|
+
...echoed,
|
|
267
|
+
...("args" in echoed ? {} : { argsOmitted: true }),
|
|
268
|
+
recovery,
|
|
269
|
+
},
|
|
270
|
+
retry: error.retry ?? `Do not retry automatically. Check whether the write took effect first.${recoveryRetry}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
242
273
|
if (error.code === "auth_required" && target.connector.startAuth) {
|
|
243
274
|
error = { ...error, code: "downstream_oauth_required" };
|
|
244
275
|
}
|
|
@@ -358,7 +389,7 @@ export class InvocationService {
|
|
|
358
389
|
// Activity rows and this log line are both payload-free by
|
|
359
390
|
// construction: the line is the failure's typed record, never its
|
|
360
391
|
// message, which may be the downstream's own words (INV-6).
|
|
361
|
-
if (target && details.code !== "destructive_tool_requires_approval") {
|
|
392
|
+
if (target && details.code !== "destructive_tool_requires_approval" && details.code !== "read_only_endpoint") {
|
|
362
393
|
logFailure(this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger, "call failed", failureRecord({
|
|
363
394
|
connector: target.connector.id,
|
|
364
395
|
// Named only when the catalog listed it (src/operator-record.ts).
|
|
@@ -432,8 +463,11 @@ export class InvocationService {
|
|
|
432
463
|
reconciliationRequired: true,
|
|
433
464
|
};
|
|
434
465
|
}
|
|
435
|
-
|
|
436
|
-
|
|
466
|
+
const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly);
|
|
467
|
+
if (refusal === "read_only_endpoint") {
|
|
468
|
+
return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and this endpoint is read-only. Nothing here can send it.`);
|
|
469
|
+
}
|
|
470
|
+
if (refusal === "destructive_tool_requires_approval") {
|
|
437
471
|
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
|
|
438
472
|
}
|
|
439
473
|
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
@@ -464,6 +498,7 @@ export class InvocationService {
|
|
|
464
498
|
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
|
|
465
499
|
trackCredentialReads(connectorContext);
|
|
466
500
|
sentSecrets.include(sentSecretsFor(connectorContext));
|
|
501
|
+
dispatchContext = connectorContext;
|
|
467
502
|
try {
|
|
468
503
|
if (target.connector.credential) {
|
|
469
504
|
if (!connectorContext.credential) {
|
package/dist/meta-tools.js
CHANGED
|
@@ -210,12 +210,6 @@ async function stashResult(bytes, results, preview, rawText) {
|
|
|
210
210
|
totalBytes,
|
|
211
211
|
hint: `${results.write ? WRITE_ALREADY_RAN + " " : ""}Paging is unavailable: text contains unpaired surrogates and can't be paged as text; request the value as JSON (value mode or a JSON-returning call) to recover it.`,
|
|
212
212
|
};
|
|
213
|
-
if (!results.pageable)
|
|
214
|
-
return {
|
|
215
|
-
truncated: true,
|
|
216
|
-
totalBytes,
|
|
217
|
-
hint: `${WRITE_ALREADY_RAN} Paging is unavailable for write results on read-only pools.`,
|
|
218
|
-
};
|
|
219
213
|
const id = crypto.randomUUID();
|
|
220
214
|
try {
|
|
221
215
|
// Base64 permits byte-range decoding, and splitting the envelope across
|
|
@@ -270,7 +264,7 @@ async function stashResult(bytes, results, preview, rawText) {
|
|
|
270
264
|
};
|
|
271
265
|
}
|
|
272
266
|
/** Program calls use the direct-call envelope, ledger, expiry and authority. */
|
|
273
|
-
export async function pageProgramResult(registry, baseUrl, resolved, value, format, secrets,
|
|
267
|
+
export async function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
|
|
274
268
|
const data = secrets.redact(value);
|
|
275
269
|
const wire = JSON.stringify({ data, format });
|
|
276
270
|
if (enc.encode(wire).length <= wireBytes)
|
|
@@ -279,7 +273,6 @@ export async function pageProgramResult(registry, baseUrl, resolved, value, form
|
|
|
279
273
|
const bytes = enc.encode(text);
|
|
280
274
|
const write = resolved.definition.classification !== "read";
|
|
281
275
|
const notice = await stashResult(bytes, {
|
|
282
|
-
pageable: !write || trust === "trusted",
|
|
283
276
|
binding: {
|
|
284
277
|
identity: registry.resultIdentity(),
|
|
285
278
|
baseUrl,
|
|
@@ -304,7 +297,7 @@ export async function pageProgramResult(registry, baseUrl, resolved, value, form
|
|
|
304
297
|
};
|
|
305
298
|
}
|
|
306
299
|
/** Program returns share the direct stash, with all host-admitted call dependencies. */
|
|
307
|
-
export async function guardProgramReturn(value, registry, baseUrl, dependencies, secrets,
|
|
300
|
+
export async function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite) {
|
|
308
301
|
const serialized = serializeResultText(value);
|
|
309
302
|
if (serialized.length <= maxChars)
|
|
310
303
|
return value;
|
|
@@ -314,7 +307,6 @@ export async function guardProgramReturn(value, registry, baseUrl, dependencies,
|
|
|
314
307
|
const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
|
|
315
308
|
const results = {
|
|
316
309
|
secrets,
|
|
317
|
-
pageable: !write || trust === "trusted",
|
|
318
310
|
binding: {
|
|
319
311
|
identity: registry.resultIdentity(),
|
|
320
312
|
baseUrl,
|
|
@@ -522,6 +514,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
522
514
|
});
|
|
523
515
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
524
516
|
requestScope,
|
|
517
|
+
// search_tools never shows a read-only endpoint's writes.
|
|
518
|
+
readOnly: opts.readOnly,
|
|
525
519
|
probeTimeoutMs,
|
|
526
520
|
concurrency: discoveryConcurrency,
|
|
527
521
|
defer: opts.defer,
|
|
@@ -536,6 +530,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
536
530
|
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
537
531
|
source,
|
|
538
532
|
sentSecrets,
|
|
533
|
+
readOnly: opts.readOnly,
|
|
539
534
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
540
535
|
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
541
536
|
unwrapResult: call.resultMode === "value",
|
|
@@ -557,7 +552,6 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
557
552
|
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
558
553
|
const results = {
|
|
559
554
|
secrets,
|
|
560
|
-
pageable: resolved.definition.classification === "read" || opts.trust === "trusted",
|
|
561
555
|
binding: {
|
|
562
556
|
identity: registry.resultIdentity(),
|
|
563
557
|
baseUrl,
|
|
@@ -788,7 +782,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
788
782
|
if (binding.program &&
|
|
789
783
|
!(await registry.recheckResultAccess(undefined, binding.classification, options.signal)))
|
|
790
784
|
return false;
|
|
791
|
-
if (binding.classification === "write" && opts.
|
|
785
|
+
if (binding.classification === "write" && opts.readOnly === true)
|
|
792
786
|
return false;
|
|
793
787
|
const dependencies = binding.program ?? [binding];
|
|
794
788
|
for (const dependency of dependencies) {
|
|
@@ -809,7 +803,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
809
803
|
if (!tool ||
|
|
810
804
|
!(dependency.access === "catalog" ||
|
|
811
805
|
(dependency.classification === "read" && tool.classification === "read") ||
|
|
812
|
-
opts.
|
|
806
|
+
opts.readOnly !== true))
|
|
813
807
|
return false;
|
|
814
808
|
}
|
|
815
809
|
return true;
|
|
@@ -915,9 +909,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
915
909
|
},
|
|
916
910
|
};
|
|
917
911
|
}
|
|
918
|
-
const SEARCH_DESC =
|
|
919
|
-
const CALL_DESC =
|
|
912
|
+
const SEARCH_DESC = (readOnly) => `${readOnly ? "Use top-level search for catalog inspection; this read-only endpoint omits write tools." : "Use top-level search for catalog inspection or approval-required work before call_destructive_tool."} Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses. Returns { catalogErrors, tools, total, offset, limit, hasMore }; read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
|
|
913
|
+
const CALL_DESC = (readOnly) => `Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. ${readOnly ? "This endpoint is read-only and refuses every write." : "Unannotated or write-capable tools fail closed to call_destructive_tool."} A truncated result carries a connecta.result action.`;
|
|
920
914
|
const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
|
|
915
|
+
/** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
|
|
916
|
+
const CALL_DESTRUCTIVE_READ_ONLY_DESC = "This endpoint is read-only: search and describe omit write tools, and every write call here is refused. Reads still run; prefer call_tool or execute_code for them.";
|
|
921
917
|
const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
|
|
922
918
|
const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
|
|
923
919
|
/**
|
|
@@ -1001,7 +997,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1001
997
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1002
998
|
downstreamInput: Boolean(ctx.authElicitation),
|
|
1003
999
|
requestScope: ctx.requestScope,
|
|
1004
|
-
|
|
1000
|
+
readOnly: ctx.readOnly,
|
|
1005
1001
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1006
1002
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1007
1003
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
@@ -1028,7 +1024,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1028
1024
|
return mt.skills(args);
|
|
1029
1025
|
});
|
|
1030
1026
|
server.registerTool("search_tools", {
|
|
1031
|
-
description: describedFor(registry, SEARCH_DESC, "search"),
|
|
1027
|
+
description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true), "search"),
|
|
1032
1028
|
inputSchema: SEARCH_INPUT,
|
|
1033
1029
|
outputSchema: SEARCH_OUTPUT,
|
|
1034
1030
|
annotations: READ_ONLY_REMOTE,
|
|
@@ -1037,7 +1033,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1037
1033
|
return mt.searchTools(args);
|
|
1038
1034
|
});
|
|
1039
1035
|
server.registerTool("call_tool", {
|
|
1040
|
-
description: CALL_DESC,
|
|
1036
|
+
description: CALL_DESC(ctx.readOnly === true),
|
|
1041
1037
|
inputSchema: CALL_INPUT,
|
|
1042
1038
|
// Default mode returns native content, so no structured result is promised.
|
|
1043
1039
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
@@ -1050,7 +1046,9 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1050
1046
|
: mt.callTool(args);
|
|
1051
1047
|
});
|
|
1052
1048
|
server.registerTool("call_destructive_tool", {
|
|
1053
|
-
description:
|
|
1049
|
+
description: ctx.readOnly === true
|
|
1050
|
+
? CALL_DESTRUCTIVE_READ_ONLY_DESC
|
|
1051
|
+
: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
|
|
1054
1052
|
inputSchema: CALL_DESTRUCTIVE_INPUT,
|
|
1055
1053
|
annotations: {
|
|
1056
1054
|
destructiveHint: true,
|
|
@@ -8,7 +8,7 @@ type FailureStep = "MCP handshake" | "tools/list" | "tools/call" | "resources/re
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const OAUTH_ERROR_CODES: ReadonlySet<string>;
|
|
10
10
|
/** Inbound denial facts, never identity text or provider diagnostics. */
|
|
11
|
-
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "pool_not_declared" | "pool_grant_denied" | "pool_grant_threw";
|
|
11
|
+
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "default_grant_denied" | "default_grant_threw" | "pool_not_declared" | "pool_grant_denied" | "pool_grant_threw";
|
|
12
12
|
/**
|
|
13
13
|
* Name `ctor`'s instances `label` in records and fixed text. Call once, at
|
|
14
14
|
* module scope, beside the class. An error whose prototype chain reaches no
|
|
@@ -104,7 +104,7 @@ export declare function failureRecord(subject: FailureSubject, failure?: unknown
|
|
|
104
104
|
* The events an operator log reports failures as. Closed: a new one is a
|
|
105
105
|
* reviewed line here, never a string assembled at the call site.
|
|
106
106
|
*/
|
|
107
|
-
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "connectorAccess grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP pool request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "result paging unavailable" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
|
|
107
|
+
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "connectorAccess grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP default endpoint request denied" | "MCP pool request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "result paging unavailable" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
|
|
108
108
|
/**
|
|
109
109
|
* Log one failure as `[connecta] <event>` and its record. A record
|
|
110
110
|
* `failureRecord` did not build is replaced, whatever its shape, by a fixed
|
package/dist/operator-record.js
CHANGED
|
@@ -2,7 +2,7 @@ import type { ActivityOutcome } from "../activity.js";
|
|
|
2
2
|
import type { ConnectaConfigDescription } from "../describe-config.js";
|
|
3
3
|
import type { ConnectorStatus, ConnectorToolDescription } from "../types.js";
|
|
4
4
|
import type { UiProblem } from "./model.js";
|
|
5
|
-
/** The registry's final verdict, independent of the viewer's
|
|
5
|
+
/** The registry's final verdict, independent of whether the viewer's endpoint is read-only. */
|
|
6
6
|
export interface OperatorTool extends ConnectorToolDescription {
|
|
7
7
|
address: string;
|
|
8
8
|
classification: "read" | "write";
|
|
@@ -36,15 +36,16 @@ export interface OperatorGrant {
|
|
|
36
36
|
export interface OperatorPool {
|
|
37
37
|
name: string;
|
|
38
38
|
path: string;
|
|
39
|
-
|
|
39
|
+
/** Whether this pool's endpoint refuses every write. */
|
|
40
|
+
readOnly: boolean;
|
|
40
41
|
/** Pool access intersected with the caller's grants. */
|
|
41
42
|
grants: OperatorGrant[];
|
|
42
43
|
}
|
|
43
44
|
export interface OperatorViewer {
|
|
44
45
|
interactive: boolean;
|
|
45
46
|
grants: OperatorGrant[];
|
|
46
|
-
/**
|
|
47
|
-
|
|
47
|
+
/** Whether the root /mcp endpoint refuses every write. */
|
|
48
|
+
readOnly: boolean;
|
|
48
49
|
pools: OperatorPool[];
|
|
49
50
|
permissions: {
|
|
50
51
|
activity: boolean;
|