@zackbart/connecta 0.29.0 → 0.31.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 +99 -0
- package/README.md +126 -134
- package/bin/connecta.mjs +13 -2
- package/bin/version.mjs +1 -1
- package/dist/argument-redaction.js +230 -0
- package/dist/auth-elicitation.js +14 -9
- package/dist/authorization-handoff.js +111 -0
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-cache.js +2 -2
- package/dist/catalog-service.js +103 -63
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -7
- package/dist/connectors/guarded-fetch.d.ts +9 -1
- package/dist/connectors/guarded-fetch.js +17 -5
- package/dist/connectors/negotiation-cache.js +1 -1
- package/dist/downstream-input-context.js +9 -0
- package/dist/downstream-input.js +13 -11
- package/dist/errors.d.ts +38 -12
- package/dist/errors.js +19 -9
- package/dist/execute.js +375 -43
- package/dist/executor-result.js +11 -9
- package/dist/executors/quickjs-child.js +12 -5
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +6 -1
- package/dist/executors/quickjs.js +20 -4
- package/dist/guest-runtime.js +52 -2
- package/dist/guest-types.d.ts +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/invocation.js +72 -14
- package/dist/meta-output.js +0 -15
- package/dist/meta-tools.js +166 -95
- package/dist/oauth-handoff.js +88 -11
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +11 -0
- package/dist/program-result.js +5 -0
- package/dist/program-writes.js +7 -48
- package/dist/provider.d.ts +8 -6
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/request-token.js +83 -0
- 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/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/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +60 -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 +1067 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- 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/registry.d.ts +8 -6
- package/dist/registry.js +134 -61
- package/dist/resource-read.js +4 -1
- package/dist/routes/mcp.js +11 -2
- package/dist/sent-secrets.d.ts +54 -13
- package/dist/sent-secrets.js +770 -231
- package/dist/skills.js +26 -16
- package/dist/sqlite.js +21 -6
- package/dist/storage/keys.js +20 -5
- package/dist/storage/memory.js +9 -4
- package/dist/storage/sql.js +18 -9
- package/dist/types.d.ts +17 -3
- package/dist/usage-guide.js +49 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +2 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +64 -0
- package/documentation/architecture.md +241 -97
- package/documentation/auth.md +138 -5
- package/documentation/code-mode.md +224 -107
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +170 -0
- package/documentation/meta-tools.md +142 -30
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +4 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-auth-migration.md +307 -0
- package/documentation/provider-migration-0.29.md +22 -5
- package/examples/worker/README.md +9 -2
- package/package.json +9 -2
- package/templates/node/README.md +9 -2
- package/templates/node/package.json +1 -1
- package/dist/providers/vercel/mcp-ownership.js +0 -212
package/dist/config-schema.d.ts
CHANGED
|
@@ -25,9 +25,13 @@ type AnyField = Field<any, any, boolean>;
|
|
|
25
25
|
export type Shape = {
|
|
26
26
|
readonly [key: string]: AnyField;
|
|
27
27
|
};
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* A discriminated union of closed objects: `cases[value[key[0]][key[1]]…]`,
|
|
30
|
+
* else `cases[otherwise]`. Each case is the whole object's shape, including
|
|
31
|
+
* the nested objects on the key path.
|
|
32
|
+
*/
|
|
29
33
|
interface Variants {
|
|
30
|
-
readonly key: string;
|
|
34
|
+
readonly key: readonly string[];
|
|
31
35
|
readonly cases: Readonly<Record<string, Shape>>;
|
|
32
36
|
readonly otherwise?: string;
|
|
33
37
|
}
|
|
@@ -116,13 +120,16 @@ export declare function instance<F extends AnyField>(field: F): F & {
|
|
|
116
120
|
};
|
|
117
121
|
/**
|
|
118
122
|
* A closed object whose allowed keys depend on a discriminant, such as
|
|
119
|
-
* `remoteMcp({ auth: { type: "headers", headers } })`.
|
|
123
|
+
* `remoteMcp({ auth: { type: "headers", headers } })`. The discriminant may
|
|
124
|
+
* be nested: `variants(["auth", "type"], cases)` selects the whole options
|
|
125
|
+
* shape from `options.auth.type`, so `{ auth: { type: "apiKey" }, mode }` and
|
|
126
|
+
* `{ auth: { type: "oauth" } }` accept different top-level keys. An omitted
|
|
120
127
|
* discriminant selects `otherwise` when given; an omitted one without it, an
|
|
121
|
-
* unrecognized one, or a value that is not an object throws with
|
|
122
|
-
* the valid values, so no union falls through to "no case". Only
|
|
123
|
-
* reads variants; the factory validates the remaining values itself.
|
|
128
|
+
* unrecognized one, or a value on the path that is not an object throws with
|
|
129
|
+
* its path and the valid values, so no union falls through to "no case". Only
|
|
130
|
+
* the walk reads variants; the factory validates the remaining values itself.
|
|
124
131
|
*/
|
|
125
|
-
export declare function variants(key: string, cases: Readonly<Record<string, Shape>>, otherwise?: string): Field<unknown, unknown, true>;
|
|
132
|
+
export declare function variants(key: string | readonly string[], cases: Readonly<Record<string, Shape>>, otherwise?: string): Field<unknown, unknown, true>;
|
|
126
133
|
/**
|
|
127
134
|
* A closed object for the unknown-key walk only: each named key may hold
|
|
128
135
|
* anything, which its factory validates. For the options of a built-in
|
package/dist/config-schema.js
CHANGED
|
@@ -183,16 +183,22 @@ export function instance(field) {
|
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
185
|
* A closed object whose allowed keys depend on a discriminant, such as
|
|
186
|
-
* `remoteMcp({ auth: { type: "headers", headers } })`.
|
|
186
|
+
* `remoteMcp({ auth: { type: "headers", headers } })`. The discriminant may
|
|
187
|
+
* be nested: `variants(["auth", "type"], cases)` selects the whole options
|
|
188
|
+
* shape from `options.auth.type`, so `{ auth: { type: "apiKey" }, mode }` and
|
|
189
|
+
* `{ auth: { type: "oauth" } }` accept different top-level keys. An omitted
|
|
187
190
|
* discriminant selects `otherwise` when given; an omitted one without it, an
|
|
188
|
-
* unrecognized one, or a value that is not an object throws with
|
|
189
|
-
* the valid values, so no union falls through to "no case". Only
|
|
190
|
-
* reads variants; the factory validates the remaining values itself.
|
|
191
|
+
* unrecognized one, or a value on the path that is not an object throws with
|
|
192
|
+
* its path and the valid values, so no union falls through to "no case". Only
|
|
193
|
+
* the walk reads variants; the factory validates the remaining values itself.
|
|
191
194
|
*/
|
|
192
195
|
export function variants(key, cases, otherwise) {
|
|
196
|
+
const path = typeof key === "string" ? [key] : [...key];
|
|
197
|
+
if (path.length === 0)
|
|
198
|
+
throw new Error("variants() requires a discriminant key.");
|
|
193
199
|
return {
|
|
194
200
|
...opaque(),
|
|
195
|
-
variants: { key, cases, ...(otherwise !== undefined ? { otherwise } : {}) },
|
|
201
|
+
variants: { key: path, cases, ...(otherwise !== undefined ? { otherwise } : {}) },
|
|
196
202
|
};
|
|
197
203
|
}
|
|
198
204
|
/**
|
|
@@ -309,14 +315,27 @@ function inspectRecord(value, path, field) {
|
|
|
309
315
|
* is never echoed.
|
|
310
316
|
*/
|
|
311
317
|
function selectShape(inspected, path, variants) {
|
|
312
|
-
const at = `${path}.${variants.key}`;
|
|
318
|
+
const at = `${path}.${variants.key.join(".")}`;
|
|
313
319
|
const valid = Object.keys(variants.cases)
|
|
314
320
|
.map((name) => JSON.stringify(name))
|
|
315
321
|
.join(", ");
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
322
|
+
// Walk the key path one plain object at a time, reading descriptors only.
|
|
323
|
+
let holder = inspected;
|
|
324
|
+
let discriminant;
|
|
325
|
+
for (const [depth, key] of variants.key.entries()) {
|
|
326
|
+
const step = `${path}.${variants.key.slice(0, depth + 1).join(".")}`;
|
|
327
|
+
const descriptor = holder.own.find(([own]) => own === key)?.[1];
|
|
328
|
+
if (descriptor && isAccessor(descriptor))
|
|
329
|
+
fail(`${step} must be a plain value, not a getter or setter.`);
|
|
330
|
+
discriminant = descriptor?.value;
|
|
331
|
+
if (depth === variants.key.length - 1 || discriminant === undefined)
|
|
332
|
+
break;
|
|
333
|
+
if (typeof discriminant !== "object" || discriminant === null || isArrayAt(discriminant, step)) {
|
|
334
|
+
fail(`${step} must be an object.`);
|
|
335
|
+
}
|
|
336
|
+
holder = inspectRecord(discriminant, step, OPAQUE);
|
|
337
|
+
discriminant = undefined;
|
|
338
|
+
}
|
|
320
339
|
if (discriminant === undefined) {
|
|
321
340
|
if (variants.otherwise !== undefined)
|
|
322
341
|
return variants.cases[variants.otherwise];
|
package/dist/config.d.ts
CHANGED
|
@@ -252,6 +252,8 @@ declare const connectaConfig: {
|
|
|
252
252
|
* `@zackbart/connecta/d1` on Workers, `sqliteStorage(path)` from
|
|
253
253
|
* `@zackbart/connecta/sqlite` on Node. Defaults to memoryStorage(), which
|
|
254
254
|
* forgets everything on restart.
|
|
255
|
+
* Custom adapters must declare `capabilities.absoluteExpiry: true` and honor
|
|
256
|
+
* `expiresAtMs` on set and compareAndSet; older adapters fail construction.
|
|
255
257
|
*/
|
|
256
258
|
storage: import("./config-schema.js").Field<KVStorage, KVStorage, true>;
|
|
257
259
|
/**
|
package/dist/config.js
CHANGED
|
@@ -291,6 +291,8 @@ const connectaConfig = {
|
|
|
291
291
|
* `@zackbart/connecta/d1` on Workers, `sqliteStorage(path)` from
|
|
292
292
|
* `@zackbart/connecta/sqlite` on Node. Defaults to memoryStorage(), which
|
|
293
293
|
* forgets everything on restart.
|
|
294
|
+
* Custom adapters must declare `capabilities.absoluteExpiry: true` and honor
|
|
295
|
+
* `expiresAtMs` on set and compareAndSet; older adapters fail construction.
|
|
294
296
|
*/
|
|
295
297
|
storage: opaque({
|
|
296
298
|
// Every subsystem relies on the atomic claim and enumeration, so storage
|
|
@@ -299,13 +301,16 @@ const connectaConfig = {
|
|
|
299
301
|
// adapter, which could never offer either guarantee.
|
|
300
302
|
check: (value, path) => {
|
|
301
303
|
const missing = STORAGE_METHODS.filter((method) => !hasMethods(value, [method]));
|
|
302
|
-
if (missing.length
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
if (missing.length > 0)
|
|
305
|
+
throw new ConfigError(`${path} must implement ${missing.join(" and ")}. ` +
|
|
306
|
+
"Use d1Storage(env.CONNECTA_DB) from @zackbart/connecta/d1 on Workers, " +
|
|
307
|
+
"sqliteStorage(path) from @zackbart/connecta/sqlite on Node, or " +
|
|
308
|
+
"memoryStorage() in tests. Workers KV is not supported: it is eventually " +
|
|
309
|
+
"consistent and cannot compare-and-set.");
|
|
310
|
+
if (!isObject(value) || !isObject(value.capabilities) || value.capabilities.absoluteExpiry !== true) {
|
|
311
|
+
throw new ConfigError(`${path} must declare capabilities.absoluteExpiry: true and honor expiresAtMs on set and compareAndSet. ` +
|
|
312
|
+
"Upgrade the storage adapter before declaring this capability.");
|
|
313
|
+
}
|
|
309
314
|
},
|
|
310
315
|
resolve: (value) => value ?? memoryStorage(),
|
|
311
316
|
}),
|
|
@@ -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
|
/**
|
|
@@ -70,17 +70,17 @@ function confinementBase(provider, baseUrl) {
|
|
|
70
70
|
base = new URL(baseUrl);
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
73
|
-
throw new Error(`${provider} baseUrl must be an absolute URL
|
|
73
|
+
throw new Error(`${provider} baseUrl must be an absolute URL.`);
|
|
74
74
|
}
|
|
75
75
|
const loopback = base.hostname === "localhost" || base.hostname === "127.0.0.1" || base.hostname === "[::1]";
|
|
76
76
|
if (base.protocol !== "https:" && !(base.protocol === "http:" && loopback)) {
|
|
77
|
-
throw new Error(`${provider} baseUrl must be https (http is allowed only for a loopback proxy or test double)
|
|
77
|
+
throw new Error(`${provider} baseUrl must be https (http is allowed only for a loopback proxy or test double).`);
|
|
78
78
|
}
|
|
79
79
|
if (base.username || base.password) {
|
|
80
80
|
throw new Error(`${provider} baseUrl must not embed URL credentials; authentication belongs in headers.`);
|
|
81
81
|
}
|
|
82
82
|
if (base.search || base.hash) {
|
|
83
|
-
throw new Error(`${provider} baseUrl must not carry a query or fragment
|
|
83
|
+
throw new Error(`${provider} baseUrl must not carry a query or fragment.`);
|
|
84
84
|
}
|
|
85
85
|
return base;
|
|
86
86
|
}
|
|
@@ -343,6 +343,13 @@ export function guardedFetch(options) {
|
|
|
343
343
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
344
344
|
};
|
|
345
345
|
sentSecretsFor(ctx).request(url, init);
|
|
346
|
+
for (const [name, value] of Object.entries(request.visibleHeaders ?? {})) {
|
|
347
|
+
const shadow = hasHeader(headers, name);
|
|
348
|
+
if (shadow) {
|
|
349
|
+
throw new ConnectorCallError("invalid_args", `A ${provider} request sets the ${shadow} header twice.`);
|
|
350
|
+
}
|
|
351
|
+
headers[name] = value;
|
|
352
|
+
}
|
|
346
353
|
response = await send(url.toString(), init, ctx);
|
|
347
354
|
}
|
|
348
355
|
catch (cause) {
|
|
@@ -356,8 +363,13 @@ export function guardedFetch(options) {
|
|
|
356
363
|
}
|
|
357
364
|
const declared = Number(response.headers.get("content-length"));
|
|
358
365
|
// 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
|
-
|
|
366
|
+
// a reason to refuse it; the ceiling still bounds what is read. A HEAD
|
|
367
|
+
// response has no body: its Content-Length describes the GET
|
|
368
|
+
// representation, which is exactly what a HEAD asks about.
|
|
369
|
+
if (Number.isFinite(declared) &&
|
|
370
|
+
declared > limit &&
|
|
371
|
+
request.method !== "HEAD" &&
|
|
372
|
+
!(request.prefixOnly && response.ok)) {
|
|
361
373
|
await response.body?.cancel().catch(() => { });
|
|
362
374
|
throw oversized(provider, limit, `a declared ${declared} bytes`);
|
|
363
375
|
}
|
|
@@ -31,7 +31,7 @@ export async function storeNegotiation(ctx, digest, prior) {
|
|
|
31
31
|
return;
|
|
32
32
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
|
|
33
33
|
// DiscoverResult instructions, metadata and capability keys can echo auth.
|
|
34
|
-
const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).
|
|
34
|
+
const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redactCredentials(prior);
|
|
35
35
|
if (clean.kind === "modern" && !isSpecType.DiscoverResult(clean.discover))
|
|
36
36
|
return;
|
|
37
37
|
try {
|
|
@@ -18,6 +18,15 @@ export function bindDownstreamContinuation(scope, target) {
|
|
|
18
18
|
export function clearDownstreamContinuation(scope) {
|
|
19
19
|
continuations.delete(scope);
|
|
20
20
|
}
|
|
21
|
+
/** The invocation's schema-filtered snapshot, never raw continuation arguments. */
|
|
22
|
+
export function recordDownstreamArgumentEcho(scope, echo) {
|
|
23
|
+
const continuation = continuations.get(scope);
|
|
24
|
+
if (continuation)
|
|
25
|
+
continuation.argumentEcho = echo;
|
|
26
|
+
}
|
|
27
|
+
export function downstreamArgumentEcho(scope) {
|
|
28
|
+
return continuations.get(scope)?.argumentEcho ?? {};
|
|
29
|
+
}
|
|
21
30
|
export function downstreamWriteContinuation(scope) {
|
|
22
31
|
return continuations.get(scope)?.write === true;
|
|
23
32
|
}
|
package/dist/downstream-input.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { inputRequired, specTypeSchemas, ProtocolError, INVALID_PARAMS, CLIENT_CAPABILITIES_META_KEY, PROTOCOL_VERSION_META_KEY, } from "@modelcontextprotocol/server";
|
|
2
|
-
import { ConnectorCallError
|
|
3
|
-
import {
|
|
2
|
+
import { ConnectorCallError } from "./errors.js";
|
|
3
|
+
import { sentSecretsForRequest } from "./sent-secrets.js";
|
|
4
4
|
import { inputRetryKeys } from "./storage/keys.js";
|
|
5
5
|
import { REQUEST_STATE_TTL_MS, MAX_INPUT_ROUNDS, invalidRequestState, requestDigest, stateObject, } from "./request-state.js";
|
|
6
|
-
import { bindDownstreamCapabilities, downstreamInputCapabilities, bindDownstreamContinuation, clearDownstreamContinuation, assertNoPrivateStateEchoes, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
|
|
6
|
+
import { bindDownstreamCapabilities, downstreamInputCapabilities, bindDownstreamContinuation, clearDownstreamContinuation, downstreamArgumentEcho, assertNoPrivateStateEchoes, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
|
|
7
7
|
const MAX_PAYLOAD_BYTES = 64 * 1024;
|
|
8
8
|
export const MAX_RELAY_STATE_CHARS = 128 * 1024;
|
|
9
9
|
const MAX_REQUESTS = 16;
|
|
@@ -67,7 +67,7 @@ export async function captureDownstreamInput(scope, connector, address, raw, sec
|
|
|
67
67
|
url.password ||
|
|
68
68
|
[...params.url].some((char) => char.charCodeAt(0) <= 0x20 || char.charCodeAt(0) === 0x7f) ||
|
|
69
69
|
secrets.containsUrl(params.url) ||
|
|
70
|
-
|
|
70
|
+
secrets.redact(params.url) !== params.url)
|
|
71
71
|
return invalidInput();
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
@@ -84,7 +84,7 @@ export async function captureDownstreamInput(scope, connector, address, raw, sec
|
|
|
84
84
|
return invalidInput();
|
|
85
85
|
// A schema controls the accepted answer. Refuse redaction changes rather
|
|
86
86
|
// than asking the host to submit different field names or enum values.
|
|
87
|
-
if (
|
|
87
|
+
if (secrets.redact(parsed.value.requestedSchema) !== parsed.value.requestedSchema)
|
|
88
88
|
return invalidInput();
|
|
89
89
|
inputRequests[namespace(connector, index)] = inputRequired.elicit({
|
|
90
90
|
message: `Downstream ${connector}: ${parsed.value.message}`,
|
|
@@ -237,7 +237,7 @@ export class DownstreamElicitation {
|
|
|
237
237
|
return result;
|
|
238
238
|
// The nonce is spent even when the downstream failure would normally
|
|
239
239
|
// allow a retry. Preserve write reconciliation and auth prerequisites.
|
|
240
|
-
const echoed =
|
|
240
|
+
const echoed = downstreamArgumentEcho(this.options.requestScope);
|
|
241
241
|
const structuredContent = {
|
|
242
242
|
...result.structuredContent,
|
|
243
243
|
error: {
|
|
@@ -249,9 +249,11 @@ export class DownstreamElicitation {
|
|
|
249
249
|
tool: state.tool,
|
|
250
250
|
arguments: { address: state.address, ...echoed },
|
|
251
251
|
purpose: "Re-issue the original direct call without requestState or inputResponses to start a fresh input round. Do not resend this continuation." +
|
|
252
|
-
(
|
|
253
|
-
? "
|
|
254
|
-
: "")
|
|
252
|
+
(echoed.argsRedacted
|
|
253
|
+
? " Re-send the original arguments; sensitive fields are omitted."
|
|
254
|
+
: args.args !== undefined && !("args" in echoed)
|
|
255
|
+
? " Use the exact arguments you sent; they could not be echoed safely."
|
|
256
|
+
: ""),
|
|
255
257
|
},
|
|
256
258
|
}
|
|
257
259
|
: {}),
|
|
@@ -314,8 +316,8 @@ export class DownstreamElicitation {
|
|
|
314
316
|
if (requestState.length > MAX_RELAY_STATE_CHARS)
|
|
315
317
|
return failure("input_required_limit", "The sealed downstream state exceeds 128 KiB.");
|
|
316
318
|
// Opaque echoes and mutable schemas/URLs were refused during capture.
|
|
317
|
-
//
|
|
318
|
-
return
|
|
319
|
+
// Downstream prompt messages are prose under the payload rules.
|
|
320
|
+
return sentSecretsForRequest(scope).redact(inputRequired({
|
|
319
321
|
...(Object.keys(pending.inputRequests).length ? { inputRequests: pending.inputRequests } : {}),
|
|
320
322
|
requestState,
|
|
321
323
|
}));
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutorAdmissionErrorCode } from "./executor-admission.js";
|
|
2
|
+
import type { JsonSchema } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Machine-readable classification of a failed connector tool call.
|
|
4
5
|
*
|
|
@@ -77,14 +78,14 @@ export declare const MAX_ARGUMENT_VALIDATION_ISSUES = 3;
|
|
|
77
78
|
*/
|
|
78
79
|
export declare function boundedEchoText(value: string, maxBytes?: number): string;
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* ones — there is nothing honest to put in the field.
|
|
81
|
+
* Snapshot the caller's arguments after bounded schema-aware writeOnly omission,
|
|
82
|
+
* then apply the shared byte budget without clipping. Unresolved sensitivity
|
|
83
|
+
* omits the entire echo. argsRedacted marks partial or withheld arguments as
|
|
84
|
+
* reconciliation context; any retry must use the caller's original arguments.
|
|
85
85
|
*/
|
|
86
|
-
export declare function echoedCallArgs(args: unknown): {
|
|
86
|
+
export declare function echoedCallArgs(args: unknown, schema?: JsonSchema): {
|
|
87
87
|
args?: unknown;
|
|
88
|
+
argsRedacted?: true;
|
|
88
89
|
};
|
|
89
90
|
/** Optional transport diagnostics; never a URL path or raw runtime message. */
|
|
90
91
|
interface UnavailableDetails {
|
|
@@ -182,8 +183,25 @@ export declare class ConnectorCallError extends Error {
|
|
|
182
183
|
current?: Readonly<Record<string, number>>;
|
|
183
184
|
});
|
|
184
185
|
}
|
|
186
|
+
/** Secret-free payload returned by authorize_connector and auth failures. */
|
|
187
|
+
export interface AuthorizationHandoff {
|
|
188
|
+
connector: string;
|
|
189
|
+
recovery: AuthRecoveryMode;
|
|
190
|
+
message?: string;
|
|
191
|
+
status?: "auth_required";
|
|
192
|
+
authorizationUrl?: string;
|
|
193
|
+
operatorUrl?: string;
|
|
194
|
+
instructions?: string;
|
|
195
|
+
credential?: {
|
|
196
|
+
label: string;
|
|
197
|
+
fields: {
|
|
198
|
+
name: string;
|
|
199
|
+
guidance?: string;
|
|
200
|
+
}[];
|
|
201
|
+
};
|
|
202
|
+
}
|
|
185
203
|
/** The `error` object surfaced in value-mode call results and rejected promises. */
|
|
186
|
-
export interface CallErrorDetails {
|
|
204
|
+
export interface CallErrorDetails extends Partial<AuthorizationHandoff> {
|
|
187
205
|
/** Sanitized transport diagnostics, absent when the runtime supplies none. */
|
|
188
206
|
details?: UnavailableDetails;
|
|
189
207
|
code: string;
|
|
@@ -224,12 +242,13 @@ export interface CallErrorDetails {
|
|
|
224
242
|
arguments: {
|
|
225
243
|
address: string;
|
|
226
244
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* routed into a human approval prompt would describe a call nobody made.
|
|
245
|
+
* A schema-filtered snapshot within the shared byte budget, never
|
|
246
|
+
* clipped. With argsRedacted, this is partial reconciliation context.
|
|
247
|
+
* If absent or redacted, retry only with the original arguments.
|
|
231
248
|
*/
|
|
232
249
|
args?: unknown;
|
|
250
|
+
/** Sensitive fields were omitted, or sensitivity could not be resolved. */
|
|
251
|
+
argsRedacted?: true;
|
|
233
252
|
};
|
|
234
253
|
purpose: string;
|
|
235
254
|
} | {
|
|
@@ -253,11 +272,18 @@ export interface CallErrorDetails {
|
|
|
253
272
|
};
|
|
254
273
|
/** Configured connectors in this request's registry view only. */
|
|
255
274
|
configuredConnectors?: string[];
|
|
256
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Agent-only uncertain write, never copied to an operator record. argsRedacted
|
|
277
|
+
* marks partial or withheld schema-filtered arguments; argsOmitted means args
|
|
278
|
+
* is absent. Reconciliation and any explicit retry require the original input.
|
|
279
|
+
*/
|
|
257
280
|
uncertainCall?: {
|
|
258
281
|
address: string;
|
|
259
282
|
args?: unknown;
|
|
260
283
|
argsOmitted?: true;
|
|
284
|
+
argsRedacted?: true;
|
|
285
|
+
/** Facts the connector recorded before dispatch, such as a generated `idempotencyKey`. */
|
|
286
|
+
recovery?: Readonly<Record<string, string>>;
|
|
261
287
|
};
|
|
262
288
|
/** Explicit retry guidance; recovery never retries or mutates by itself. */
|
|
263
289
|
retry?: string;
|
package/dist/errors.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Typed failure contract for connector tool calls. Web-API only — no node:
|
|
2
2
|
// imports here.
|
|
3
|
+
import { redactCallArguments } from "./argument-redaction.js";
|
|
3
4
|
const CLASSIFICATION_CODE_TABLE = {
|
|
4
5
|
timeout: true,
|
|
5
6
|
auth_required: true,
|
|
@@ -116,25 +117,34 @@ export function boundedEchoText(value, maxBytes = MAX_ECHOED_BYTES) {
|
|
|
116
117
|
return `${echoDecoder.decode(bytes.slice(0, end))}…`;
|
|
117
118
|
}
|
|
118
119
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* ones — there is nothing honest to put in the field.
|
|
120
|
+
* Snapshot the caller's arguments after bounded schema-aware writeOnly omission,
|
|
121
|
+
* then apply the shared byte budget without clipping. Unresolved sensitivity
|
|
122
|
+
* omits the entire echo. argsRedacted marks partial or withheld arguments as
|
|
123
|
+
* reconciliation context; any retry must use the caller's original arguments.
|
|
124
124
|
*/
|
|
125
|
-
export function echoedCallArgs(args) {
|
|
125
|
+
export function echoedCallArgs(args, schema) {
|
|
126
126
|
if (args === undefined)
|
|
127
127
|
return {};
|
|
128
128
|
let text;
|
|
129
|
+
let redacted = false;
|
|
129
130
|
try {
|
|
130
|
-
|
|
131
|
+
const filtered = schema ? redactCallArguments(args, schema) : { value: args, redacted: false };
|
|
132
|
+
redacted = filtered.redacted;
|
|
133
|
+
if (!("value" in filtered))
|
|
134
|
+
return { argsRedacted: true };
|
|
135
|
+
const safeArgs = filtered.value;
|
|
136
|
+
text = JSON.stringify(safeArgs);
|
|
131
137
|
}
|
|
132
138
|
catch {
|
|
133
|
-
return {};
|
|
139
|
+
return redacted ? { argsRedacted: true } : {};
|
|
134
140
|
}
|
|
135
141
|
if (text === undefined)
|
|
136
142
|
return {};
|
|
137
|
-
return echoEncoder.encode(text).length <= MAX_ECHOED_BYTES
|
|
143
|
+
return echoEncoder.encode(text).length <= MAX_ECHOED_BYTES
|
|
144
|
+
? { args: JSON.parse(text), ...(redacted ? { argsRedacted: true } : {}) }
|
|
145
|
+
: redacted
|
|
146
|
+
? { argsRedacted: true }
|
|
147
|
+
: {};
|
|
138
148
|
}
|
|
139
149
|
function boundedValidation(details) {
|
|
140
150
|
if (!details)
|