@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +126 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/call-recovery.js +25 -0
  9. package/dist/catalog-cache.js +2 -2
  10. package/dist/catalog-service.js +103 -63
  11. package/dist/config-schema.d.ts +14 -7
  12. package/dist/config-schema.js +29 -10
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +12 -7
  15. package/dist/connectors/guarded-fetch.d.ts +9 -1
  16. package/dist/connectors/guarded-fetch.js +17 -5
  17. package/dist/connectors/negotiation-cache.js +1 -1
  18. package/dist/downstream-input-context.js +9 -0
  19. package/dist/downstream-input.js +13 -11
  20. package/dist/errors.d.ts +38 -12
  21. package/dist/errors.js +19 -9
  22. package/dist/execute.js +375 -43
  23. package/dist/executor-result.js +11 -9
  24. package/dist/executors/quickjs-child.js +12 -5
  25. package/dist/executors/quickjs-protocol.js +5 -6
  26. package/dist/executors/quickjs-runtime.js +6 -1
  27. package/dist/executors/quickjs.js +20 -4
  28. package/dist/guest-runtime.js +52 -2
  29. package/dist/guest-types.d.ts +47 -5
  30. package/dist/index.d.ts +1 -1
  31. package/dist/invocation.js +72 -14
  32. package/dist/meta-output.js +0 -15
  33. package/dist/meta-tools.js +166 -95
  34. package/dist/oauth-handoff.js +88 -11
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +9 -0
  37. package/dist/operator-ui/view.js +11 -0
  38. package/dist/program-result.js +5 -0
  39. package/dist/program-writes.js +7 -48
  40. package/dist/provider.d.ts +8 -6
  41. package/dist/provider.js +3 -4
  42. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  43. package/dist/providers/_shared/request-token.js +83 -0
  44. package/dist/providers/_shared/rest/dispatch.js +41 -0
  45. package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
  46. package/dist/providers/_shared/rest/operation-index.js +527 -0
  47. package/dist/providers/_shared/rest/tools.d.ts +141 -0
  48. package/dist/providers/_shared/rest/tools.js +569 -0
  49. package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
  50. package/dist/providers/_shared/rest/value-safety.js +580 -0
  51. package/dist/providers/cloudflare/graphql.js +385 -0
  52. package/dist/providers/cloudflare/index.d.ts +47 -51
  53. package/dist/providers/cloudflare/index.js +188 -1997
  54. package/dist/providers/cloudflare/openapi.generated.js +3347 -0
  55. package/dist/providers/cloudflare/rest.d.ts +37 -0
  56. package/dist/providers/cloudflare/rest.js +1056 -0
  57. package/dist/providers/cloudflare/skill.generated.js +7 -4
  58. package/dist/providers/cloudflare/value-safety.js +1247 -0
  59. package/dist/providers/infisical/index.d.ts +7 -0
  60. package/dist/providers/infisical/index.js +713 -0
  61. package/dist/providers/infisical/skill.generated.js +8 -0
  62. package/dist/providers/notion/index.d.ts +35 -31
  63. package/dist/providers/notion/index.js +60 -1499
  64. package/dist/providers/notion/openapi.generated.js +75 -0
  65. package/dist/providers/notion/rest.d.ts +24 -0
  66. package/dist/providers/notion/rest.js +1067 -0
  67. package/dist/providers/notion/skill.generated.js +5 -4
  68. package/dist/providers/notion/value-safety.js +31 -0
  69. package/dist/providers/stripe/index.d.ts +26 -20
  70. package/dist/providers/stripe/index.js +103 -130
  71. package/dist/providers/stripe/openapi.generated.js +649 -0
  72. package/dist/providers/stripe/rest.d.ts +14 -0
  73. package/dist/providers/stripe/rest.js +395 -0
  74. package/dist/providers/stripe/skill.generated.js +7 -5
  75. package/dist/providers/stripe/value-safety.js +705 -0
  76. package/dist/providers/vercel/index.d.ts +31 -22
  77. package/dist/providers/vercel/index.js +96 -853
  78. package/dist/providers/vercel/openapi.generated.js +456 -0
  79. package/dist/providers/vercel/rest.d.ts +20 -0
  80. package/dist/providers/vercel/rest.js +1002 -0
  81. package/dist/providers/vercel/skill.generated.js +4 -5
  82. package/dist/providers/vercel/value-safety.js +328 -0
  83. package/dist/registry.d.ts +8 -6
  84. package/dist/registry.js +134 -61
  85. package/dist/resource-read.js +4 -1
  86. package/dist/routes/mcp.js +11 -2
  87. package/dist/sent-secrets.d.ts +54 -13
  88. package/dist/sent-secrets.js +770 -231
  89. package/dist/skills.js +26 -16
  90. package/dist/sqlite.js +21 -6
  91. package/dist/storage/keys.js +20 -5
  92. package/dist/storage/memory.js +9 -4
  93. package/dist/storage/sql.js +18 -9
  94. package/dist/types.d.ts +17 -3
  95. package/dist/usage-guide.js +49 -40
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/worker.js +2 -1
  99. package/dist/write-outcome.js +47 -0
  100. package/documentation/README.md +64 -0
  101. package/documentation/architecture.md +241 -97
  102. package/documentation/auth.md +138 -5
  103. package/documentation/code-mode.md +224 -107
  104. package/documentation/deploying.md +146 -0
  105. package/documentation/integrating.md +170 -0
  106. package/documentation/meta-tools.md +142 -30
  107. package/documentation/operating.md +135 -0
  108. package/documentation/operator-tests.md +4 -0
  109. package/documentation/operator-ui.md +132 -0
  110. package/documentation/provider-auth-migration.md +307 -0
  111. package/documentation/provider-migration-0.29.md +22 -5
  112. package/examples/worker/README.md +9 -2
  113. package/package.json +9 -2
  114. package/templates/node/README.md +9 -2
  115. package/templates/node/package.json +1 -1
  116. package/dist/providers/vercel/mcp-ownership.js +0 -212
@@ -25,9 +25,13 @@ type AnyField = Field<any, any, boolean>;
25
25
  export type Shape = {
26
26
  readonly [key: string]: AnyField;
27
27
  };
28
- /** A discriminated union of closed objects: `cases[value[key]]`, else `cases[otherwise]`. */
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 } })`. An omitted
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 its path and
122
- * the valid values, so no union falls through to "no case". Only the walk
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
@@ -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 } })`. An omitted
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 its path and
189
- * the valid values, so no union falls through to "no case". Only the walk
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
- const descriptor = inspected.own.find(([key]) => key === variants.key)?.[1];
317
- if (descriptor && isAccessor(descriptor))
318
- fail(`${at} must be a plain value, not a getter or setter.`);
319
- const discriminant = descriptor?.value;
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 === 0)
303
- return;
304
- throw new ConfigError(`${path} must implement ${missing.join(" and ")}. ` +
305
- "Use d1Storage(env.CONNECTA_DB) from @zackbart/connecta/d1 on Workers, " +
306
- "sqliteStorage(path) from @zackbart/connecta/sqlite on Node, or " +
307
- "memoryStorage() in tests. Workers KV is not supported: it is eventually " +
308
- "consistent and cannot compare-and-set.");
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: ${baseUrl}`);
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): ${baseUrl}`);
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: ${baseUrl}`);
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
- if (Number.isFinite(declared) && declared > limit && !(request.prefixOnly && response.ok)) {
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).redact(prior);
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
  }
@@ -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, echoedCallArgs } from "./errors.js";
3
- import { redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
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
- redactAgentOutput(secrets, params.url) !== params.url)
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 (redactAgentOutput(secrets, parsed.value.requestedSchema) !== parsed.value.requestedSchema)
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 = echoedCallArgs(args.args);
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
- (args.args !== undefined && !("args" in echoed)
253
- ? " Use the exact arguments you sent; they exceed the echo budget."
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
- // Prompt messages take the same #736 agent boundary as ordinary outputs.
318
- return redactAgentOutput(sentSecretsForRequest(scope), inputRequired({
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
- * `{ args }` when the caller's arguments fit the shared echo budget,
81
- * `{}` when they do not. All or nothing: a clipped echo would be a *different*
82
- * call than the one that was refused, and the routes this feeds end at a human
83
- * approving one. Unserializable arguments are treated the same way as oversized
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
- * The caller's own arguments, echoed only when they fit
228
- * the shared echo budget — and then whole, never clipped. Absent
229
- * means "re-send exactly what you sent": a half-copied argument object
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
- /** Agent-only uncertain write, never copied to an operator record. */
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
- * `{ args }` when the caller's arguments fit the shared echo budget,
120
- * `{}` when they do not. All or nothing: a clipped echo would be a *different*
121
- * call than the one that was refused, and the routes this feeds end at a human
122
- * approving one. Unserializable arguments are treated the same way as oversized
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
- text = JSON.stringify(args);
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 ? { args: JSON.parse(text) } : {};
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)