@zackbart/connecta 0.29.0 → 0.30.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 (69) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +123 -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/catalog-cache.js +2 -2
  9. package/dist/catalog-service.js +103 -63
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +12 -7
  12. package/dist/connectors/guarded-fetch.js +3 -3
  13. package/dist/connectors/negotiation-cache.js +1 -1
  14. package/dist/downstream-input-context.js +9 -0
  15. package/dist/downstream-input.js +13 -11
  16. package/dist/errors.d.ts +36 -12
  17. package/dist/errors.js +19 -9
  18. package/dist/execute.js +375 -43
  19. package/dist/executor-result.js +11 -9
  20. package/dist/executors/quickjs-child.js +12 -5
  21. package/dist/executors/quickjs-protocol.js +5 -6
  22. package/dist/executors/quickjs-runtime.js +6 -1
  23. package/dist/executors/quickjs.js +20 -4
  24. package/dist/guest-runtime.js +52 -2
  25. package/dist/guest-types.d.ts +47 -5
  26. package/dist/index.d.ts +1 -1
  27. package/dist/invocation.js +40 -14
  28. package/dist/meta-output.js +0 -15
  29. package/dist/meta-tools.js +166 -95
  30. package/dist/oauth-handoff.js +88 -11
  31. package/dist/program-result.js +5 -0
  32. package/dist/program-writes.js +5 -1
  33. package/dist/provider.d.ts +1 -1
  34. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  35. package/dist/providers/_shared/request-token.js +83 -0
  36. package/dist/providers/infisical/index.d.ts +7 -0
  37. package/dist/providers/infisical/index.js +713 -0
  38. package/dist/providers/infisical/skill.generated.js +8 -0
  39. package/dist/registry.d.ts +8 -6
  40. package/dist/registry.js +134 -61
  41. package/dist/resource-read.js +4 -1
  42. package/dist/routes/mcp.js +11 -2
  43. package/dist/sent-secrets.d.ts +54 -13
  44. package/dist/sent-secrets.js +770 -231
  45. package/dist/skills.js +26 -16
  46. package/dist/sqlite.js +21 -6
  47. package/dist/storage/keys.js +20 -5
  48. package/dist/storage/memory.js +9 -4
  49. package/dist/storage/sql.js +18 -9
  50. package/dist/types.d.ts +17 -3
  51. package/dist/usage-guide.js +49 -40
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/worker.js +2 -1
  55. package/documentation/README.md +63 -0
  56. package/documentation/architecture.md +121 -89
  57. package/documentation/auth.md +138 -5
  58. package/documentation/code-mode.md +224 -107
  59. package/documentation/deploying.md +146 -0
  60. package/documentation/integrating.md +159 -0
  61. package/documentation/meta-tools.md +135 -30
  62. package/documentation/operating.md +135 -0
  63. package/documentation/operator-tests.md +4 -0
  64. package/documentation/operator-ui.md +132 -0
  65. package/documentation/provider-migration-0.29.md +5 -0
  66. package/examples/worker/README.md +7 -0
  67. package/package.json +7 -1
  68. package/templates/node/README.md +7 -0
  69. package/templates/node/package.json +1 -1
@@ -18,12 +18,12 @@ const TRUNCATION_HINT = "filter/map/slice data inside execute_code and return on
18
18
  * of what the program returned, and truncation happens exactly once no matter
19
19
  * how many hops the value takes.
20
20
  */
21
- function truncationEnvelope(text, maxChars, totalChars = text.length) {
21
+ function truncationEnvelope(text, maxChars, totalChars = text.length, hint = TRUNCATION_HINT) {
22
22
  const base = {
23
23
  truncated: true,
24
24
  preview: "",
25
25
  totalChars,
26
- hint: TRUNCATION_HINT,
26
+ hint,
27
27
  };
28
28
  let budget = Math.max(0, maxChars - JSON.stringify(base).length);
29
29
  for (let attempt = 0; attempt < 8 && budget > 0; attempt += 1) {
@@ -37,11 +37,12 @@ function truncationEnvelope(text, maxChars, totalChars = text.length) {
37
37
  }
38
38
  return { ...base, preview: text.slice(0, budget) };
39
39
  }
40
- export function guardExecuteResultValue(value, maxChars = MAX_EXECUTE_RESULT_CHARS) {
40
+ export function guardExecuteResultValue(value, maxChars = MAX_EXECUTE_RESULT_CHARS, hintPrefix) {
41
+ const hint = hintPrefix ? `${hintPrefix} ${TRUNCATION_HINT}` : TRUNCATION_HINT;
41
42
  const text = serializeResultText(value);
42
43
  if (text.length <= maxChars)
43
44
  return value;
44
- // A child may already have guarded this value at a larger transport cap.
45
+ // A custom executor may already have guarded this value at a larger cap.
45
46
  // Shrink its preview once more without nesting notices or losing original size.
46
47
  if (value !== null && typeof value === "object") {
47
48
  const prior = value;
@@ -52,10 +53,10 @@ export function guardExecuteResultValue(value, maxChars = MAX_EXECUTE_RESULT_CHA
52
53
  Number.isSafeInteger(prior.totalChars) &&
53
54
  prior.totalChars >= prior.preview.length &&
54
55
  Object.keys(value).length === 4) {
55
- return truncationEnvelope(prior.preview, maxChars, prior.totalChars);
56
+ return truncationEnvelope(prior.preview, maxChars, prior.totalChars, hint);
56
57
  }
57
58
  }
58
- return truncationEnvelope(text, maxChars);
59
+ return truncationEnvelope(text, maxChars, text.length, hint);
59
60
  }
60
61
  export function truncateExecuteText(text, max) {
61
62
  if (text.length <= max)
@@ -63,8 +64,8 @@ export function truncateExecuteText(text, max) {
63
64
  return `${text.slice(0, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
64
65
  }
65
66
  /**
66
- * Apply the public execute_code result/log policy before a child result enters
67
- * IPC. The parent repeats the guard for third-party Executor implementations.
67
+ * Validate serialization before IPC without discarding the program return.
68
+ * The host applies the presentation cap after it can stash the full value.
68
69
  */
69
70
  export function prepareExecuteResultForTransport(outcome) {
70
71
  // QuickJS already bounds captured logs at source (entries + cumulative
@@ -82,8 +83,9 @@ export function prepareExecuteResultForTransport(outcome) {
82
83
  };
83
84
  }
84
85
  try {
86
+ serializeResultText(outcome.result);
85
87
  return {
86
- result: guardExecuteResultValue(outcome.result),
88
+ result: outcome.result,
87
89
  ...(logs ? { logs } : {}),
88
90
  };
89
91
  }
@@ -1,7 +1,7 @@
1
1
  import { prepareExecuteResultForTransport } from "../executor-result.js";
2
2
  import { InvocationFailure } from "../invocation.js";
3
3
  import { msg } from "../errors.js";
4
- import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
4
+ import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_RESULT_BYTES, QUICKJS_RESULT_CHUNK_CHARS, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
5
5
  import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
6
6
  let activeJobId;
7
7
  let nextCallId = 1;
@@ -51,6 +51,13 @@ function fixedTransportFailure(message) {
51
51
  },
52
52
  };
53
53
  }
54
+ function sendResult(jobId, payloadJson) {
55
+ for (let offset = 0; offset + QUICKJS_RESULT_CHUNK_CHARS < payloadJson.length; offset += QUICKJS_RESULT_CHUNK_CHARS) {
56
+ send({ type: "result-chunk", jobId, payloadJson: payloadJson.slice(offset, offset + QUICKJS_RESULT_CHUNK_CHARS) });
57
+ }
58
+ const tail = Math.floor((payloadJson.length - 1) / QUICKJS_RESULT_CHUNK_CHARS) * QUICKJS_RESULT_CHUNK_CHARS;
59
+ send({ type: "result", jobId, payloadJson: payloadJson.slice(tail) });
60
+ }
54
61
  async function run(payload) {
55
62
  if (activeJobId !== undefined) {
56
63
  throw new Error("QuickJS child received overlapping executions.");
@@ -69,12 +76,12 @@ async function run(payload) {
69
76
  };
70
77
  let payloadJson;
71
78
  try {
72
- payloadJson = stringifyBounded(prepared, "QuickJS execution result");
79
+ payloadJson = stringifyBounded(prepared, "QuickJS execution result", MAX_QUICKJS_RESULT_BYTES);
73
80
  }
74
81
  catch (err) {
75
- payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS execution result exceeded the ${MAX_QUICKJS_IPC_BYTES}-byte IPC limit: ${msg(err)}`));
82
+ payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS execution result exceeded the ${MAX_QUICKJS_RESULT_BYTES}-byte aggregate transport limit: ${msg(err)}`));
76
83
  }
77
- send({ type: "result", jobId: payload.id, payloadJson });
84
+ sendResult(payload.id, payloadJson);
78
85
  }
79
86
  finally {
80
87
  activeJobId = undefined;
@@ -118,7 +125,7 @@ process.on("message", (message) => {
118
125
  const payload = JSON.parse(message.payloadJson);
119
126
  void run(payload).catch((err) => {
120
127
  const payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS child failed: ${msg(err)}`));
121
- send({ type: "result", jobId: payload.id, payloadJson });
128
+ sendResult(payload.id, payloadJson);
122
129
  activeJobId = undefined;
123
130
  });
124
131
  });
@@ -1,10 +1,9 @@
1
- // An execution result is serialized once into payloadJson and again as that
2
- // string is embedded in ChildToParentMessage. Captured logs therefore get a
3
- // separate 512 KiB budget measured after both JSON encodings. The final result
4
- // is shaped to 24k characters; even if every character takes its worst-case
5
- // seven transport bytes, that leaves ample structural overhead below this hard
6
- // process.send ceiling.
1
+ // Result messages remain bounded even when a full return travels in chunks.
2
+ // The host's presentation guard stashes the completed value after assembly.
7
3
  export const MAX_QUICKJS_IPC_BYTES = 1024 * 1024;
4
+ /** Aggregate result transport bound, independent of per-message IPC and log caps. */
5
+ export const MAX_QUICKJS_RESULT_BYTES = 64 * 1024 * 1024;
6
+ export const QUICKJS_RESULT_CHUNK_CHARS = 65_536;
8
7
  export const MAX_QUICKJS_LOG_TRANSPORT_BYTES = 512 * 1024;
9
8
  /** Preserve #84's stopgap before a host value enters the child/WASM process. */
10
9
  export const MAX_QUICKJS_HOST_RPC_BYTES = 256 * 1024;
@@ -253,7 +253,12 @@ function installBridge(ctx, providers, logs, onLog) {
253
253
  // The error identity and typed facts stay in host state. A guest
254
254
  // may edit public fields without changing the retained failure.
255
255
  bridge.failures.push({ error: error.dup(), call });
256
- for (const [key, value] of Object.entries({ code: call.code, retryable: call.retryable, details: call })) {
256
+ for (const [key, value] of Object.entries({
257
+ code: call.code,
258
+ retryable: call.retryable,
259
+ details: call,
260
+ data: call,
261
+ })) {
257
262
  const property = ctx.unwrapResult(ctx.evalCode(`(${JSON.stringify(value)})`));
258
263
  ctx.setProp(error, key, property);
259
264
  property.dispose();
@@ -21,7 +21,7 @@ import { InvocationFailure } from "../invocation.js";
21
21
  import { brandExecutor } from "../executor-contract.js";
22
22
  import { MAX_EXECUTE_LOG_CHARS } from "../executor-result.js";
23
23
  import { detach, fromSignal, runEdge } from "../runtime/run.js";
24
- import { hostCallLabel, MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
24
+ import { hostCallLabel, MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_RESULT_BYTES, QUICKJS_RESULT_CHUNK_CHARS, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
25
25
  export { normalizeCode } from "./quickjs-runtime.js";
26
26
  const DEFAULT_TIMEOUT_MS = 30_000;
27
27
  const DEFAULT_CPU_TIME_MS = 250;
@@ -244,6 +244,8 @@ class QuickJsChildPool {
244
244
  id,
245
245
  logs: [],
246
246
  logChars: 0,
247
+ resultChunks: [],
248
+ resultBytes: 0,
247
249
  providers: providerMap,
248
250
  outcome: Deferred.makeUnsafe(),
249
251
  };
@@ -456,15 +458,29 @@ class QuickJsChildPool {
456
458
  await this.handleHostCall(slot, child.process, active, message);
457
459
  return;
458
460
  }
459
- if (message.type !== "result" || message.jobId !== active.id)
461
+ if ((message.type !== "result" && message.type !== "result-chunk") || message.jobId !== active.id)
460
462
  return;
461
- if (serializedBytes(message.payloadJson) > MAX_QUICKJS_IPC_BYTES) {
463
+ if (message.payloadJson.length > QUICKJS_RESULT_CHUNK_CHARS ||
464
+ (message.type === "result-chunk" && message.payloadJson.length !== QUICKJS_RESULT_CHUNK_CHARS) ||
465
+ message.payloadJson.length === 0) {
466
+ this.rejectActive(slot, new Error("QuickJS execution result exceeded the IPC limit."));
467
+ this.recycle(slot);
468
+ return;
469
+ }
470
+ const bytes = serializedBytes(message.payloadJson);
471
+ if (active.resultBytes + bytes > MAX_QUICKJS_RESULT_BYTES) {
462
472
  this.rejectActive(slot, new Error("QuickJS execution result exceeded the IPC limit."));
463
473
  this.recycle(slot);
464
474
  return;
465
475
  }
466
476
  try {
467
- const payload = JSON.parse(message.payloadJson);
477
+ stringifyBounded(message, "QuickJS result IPC envelope");
478
+ active.resultBytes += bytes;
479
+ active.resultChunks.push(message.payloadJson);
480
+ if (message.type === "result-chunk")
481
+ return;
482
+ const payload = JSON.parse(active.resultChunks.join(""));
483
+ active.resultChunks.length = 0;
468
484
  // A result ends any crash streak: the next crash backs off from 100 ms.
469
485
  slot.crashes.length = 0;
470
486
  slot.backoff = undefined;
@@ -80,10 +80,57 @@ export function guestInitializer() {
80
80
  const promiseCatch = Function.prototype.call.bind(NativePromise.prototype.catch);
81
81
  const promiseFinally = Function.prototype.call.bind(NativePromise.prototype.finally);
82
82
  const push = Function.prototype.call.bind(Array.prototype.push);
83
+ const apply = Function.prototype.call.bind(Function.prototype.apply);
83
84
  const NativeError = Error;
85
+ const NativeWeakMap = WeakMap;
86
+ const isArray = Array.isArray;
87
+ const weakGet = Function.prototype.call.bind(WeakMap.prototype.get);
88
+ const weakSet = Function.prototype.call.bind(WeakMap.prototype.set);
84
89
  return freeze((provider) => {
85
90
  const pending = [];
86
91
  const emit = provider.emit;
92
+ const hostCall = provider.call;
93
+ const hostResult = provider.result;
94
+ const nativeRefs = new NativeWeakMap();
95
+ async function inline(value) {
96
+ if (!value || typeof value.transfer !== "string" || !value.handle) return value;
97
+ // A completed downstream call stays successful if transfer storage fails.
98
+ // Preserve its handle so the caller can page later, without repeating a write.
99
+ try {
100
+ let text = "", page;
101
+ do {
102
+ page = await hostResult(value.transfer);
103
+ text += page.text;
104
+ } while (page.hasMore);
105
+ return { data: value.handle.valueFormat === "json" ? JSON.parse(text) : text,
106
+ format: value.handle.valueFormat };
107
+ } catch { return value.handle; }
108
+ }
109
+ async function call() {
110
+ const reply = await apply(hostCall, undefined, arguments);
111
+ if (!reply || !reply.native) return inline(reply);
112
+ const native = await inline(reply.native.value);
113
+ if (native.format === "paged" && !reply.value) return native;
114
+ const value = reply.value ? await inline(reply.value) : {
115
+ data: reply.native.dataKey ? native.data[reply.native.dataKey] : native.data, format: "json"
116
+ };
117
+ if (value.format === "paged") return value;
118
+ if (native.format === "paged") {
119
+ value.contentResult = native;
120
+ return value;
121
+ }
122
+ if (native.format === "json" && native.data && isArray(native.data.content)) {
123
+ const content = native.data.content;
124
+ // Only host-issued refs cross back. Binary data never needs a second
125
+ // guest-to-host transfer, and guest edits cannot mint native pointers.
126
+ for (let i = 0; i < content.length; i++) {
127
+ weakSet(nativeRefs, content[i], { ref: reply.native.ref, index: i });
128
+ }
129
+ value.content = content;
130
+ weakSet(nativeRefs, value, { ref: reply.native.ref });
131
+ }
132
+ return value;
133
+ }
87
134
  function trackEmission(task) {
88
135
  const entry = { handled: false, settled: promiseThen(task,
89
136
  () => ({ ok: true }), error => ({ ok: false, error })) };
@@ -109,11 +156,14 @@ export function guestInitializer() {
109
156
  __proto__: null,
110
157
  search: provider.search,
111
158
  describe: provider.describe,
112
- call: provider.call,
159
+ call,
113
160
  read: provider.read,
114
161
  result: provider.result,
115
162
  skill: provider.skill,
116
- emit(block) { return trackEmission(emit(block)); }
163
+ emit(block) {
164
+ const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
165
+ return trackEmission(emit(ref || block));
166
+ }
117
167
  });
118
168
  globalThis.connecta = namespace;
119
169
  defineProperties(globalThis, {
@@ -15,7 +15,8 @@ export interface GuestApi {
15
15
  timeoutMs?: number;
16
16
  }): Promise<GuestResult>;
17
17
  read(uri: string): Promise<GuestResourceResult>;
18
- result(id: string, options?: {
18
+ result(id: string | GuestResultHandle, options?: {
19
+ page?: number;
19
20
  offset?: number;
20
21
  maxBytes?: number;
21
22
  }): Promise<GuestResultPage>;
@@ -24,7 +25,7 @@ export interface GuestApi {
24
25
  text: string;
25
26
  format: "text";
26
27
  }>;
27
- emit(block: GuestBlock): PromiseLike<void>;
28
+ emit(block: GuestNativeBlock | GuestResult): PromiseLike<void>;
28
29
  }
29
30
  export interface GuestResourceResult {
30
31
  contents: Array<{
@@ -36,12 +37,52 @@ export interface GuestResourceResult {
36
37
  blob: string;
37
38
  })>;
38
39
  }
39
- export type GuestResult = {
40
+ export interface GuestResultHandle {
41
+ format: "paged";
42
+ valueFormat: "json" | "text";
43
+ resultId?: string;
44
+ totalBytes: number;
45
+ truncated: true;
46
+ hint: string;
47
+ }
48
+ /** Inline MCP rich results expose original blocks; emit(result) forwards all of them. */
49
+ export type GuestResult = ({
40
50
  data: unknown;
41
51
  format: "json";
42
52
  } | {
43
53
  data: string;
44
54
  format: "text";
55
+ } | GuestResultHandle) & {
56
+ content?: GuestNativeBlock[];
57
+ /** Paging handle when native content exceeds the inline cap or transfer fails. */
58
+ contentResult?: GuestResultHandle;
59
+ };
60
+ /** Native call blocks can be forwarded unchanged, including annotations and metadata. */
61
+ export type GuestNativeBlock = (GuestBlock | {
62
+ type: "resource";
63
+ resource: {
64
+ uri: string;
65
+ mimeType?: string;
66
+ } & ({
67
+ text: string;
68
+ } | {
69
+ blob: string;
70
+ });
71
+ } | {
72
+ type: "resource_link";
73
+ uri: string;
74
+ name: string;
75
+ title?: string;
76
+ description?: string;
77
+ mimeType?: string;
78
+ size?: number;
79
+ }) & {
80
+ annotations?: {
81
+ audience?: Array<"user" | "assistant">;
82
+ priority?: number;
83
+ lastModified?: string;
84
+ };
85
+ _meta?: Record<string, unknown>;
45
86
  };
46
87
  export interface GuestResultPage {
47
88
  resultId: string;
@@ -79,7 +120,9 @@ export interface CatalogDescribeArgs {
79
120
  fullDescriptions?: boolean;
80
121
  }
81
122
  type GuideRequiredReason = "connector_required" | "approval_required" | "schema_truncated";
82
- interface CatalogFailureDetail {
123
+ interface CatalogFailureDetail extends Partial<import("./errors.js").AuthorizationHandoff> {
124
+ nextAction?: CallErrorDetails["nextAction"];
125
+ retry?: string;
83
126
  code: string;
84
127
  message: string;
85
128
  retryable: boolean;
@@ -92,7 +135,6 @@ interface CatalogDescriptionFailureDetail extends CatalogFailureDetail {
92
135
  }
93
136
  interface CatalogSearchFailure extends CatalogFailureDetail {
94
137
  connector: string;
95
- recovery?: CallErrorDetails["recovery"];
96
138
  nextAction?: Extract<NonNullable<CallErrorDetails["nextAction"]>, {
97
139
  tool: "authorize_connector";
98
140
  }>;
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ export { validateToolInput } from "./validate.js";
35
35
  export type { ValidateToolInputOptions } from "./validate.js";
36
36
  export { memoryStorage } from "./storage/memory.js";
37
37
  export { CONNECTA_VERSION } from "./version.js";
38
- export type { GuestApi, GuestResourceResult, GuestResult, GuestResultPage, GuestBlock } from "./guest-types.js";
38
+ export type { GuestApi, GuestResourceResult, GuestResult, GuestResultPage, GuestBlock, GuestNativeBlock, } from "./guest-types.js";
39
39
  export type { Registry } from "./registry.js";
40
40
  export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRedirectPolicy } from "./connectors/remote-mcp.js";
41
41
  export type { ApiHandlerContext, ApiOAuthAccess, ApiOAuthClientAuthentication, ApiOAuthConfig, ApiOptions, ApiTool, } from "./connectors/api.js";
@@ -1,3 +1,4 @@
1
+ import { withAuthorizationHandoff } from "./authorization-handoff.js";
1
2
  import { hasControlCharacters } from "./tool-name.js";
2
3
  import { surfaceAllowsTool } from "./tool-safety.js";
3
4
  import { Cause, Effect, Exit } from "effect";
@@ -10,7 +11,7 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
10
11
  import { validateCatalogToolInput } from "./validate.js";
11
12
  import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
12
13
  import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
13
- import { downstreamContinuation, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
14
+ import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
14
15
  function defined(values) {
15
16
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
16
17
  }
@@ -111,6 +112,7 @@ export class InvocationFailure extends Error {
111
112
  recovery;
112
113
  nextAction;
113
114
  retry;
115
+ data;
114
116
  constructor(details) {
115
117
  super(details.message);
116
118
  this.details = details;
@@ -123,6 +125,7 @@ export class InvocationFailure extends Error {
123
125
  this.recovery = details.recovery;
124
126
  this.nextAction = details.nextAction;
125
127
  this.retry = details.retry;
128
+ this.data = details;
126
129
  }
127
130
  }
128
131
  /**
@@ -155,7 +158,7 @@ export class InvocationService {
155
158
  pipeline(address, args, context) {
156
159
  return Effect.gen({ self: this }, function* () {
157
160
  const started = Date.now();
158
- const argumentEcho = echoedCallArgs(args ?? {});
161
+ let argumentEcho = {};
159
162
  let catalogMs = 0;
160
163
  let admissionMs = 0;
161
164
  let connectorMs = 0;
@@ -229,7 +232,11 @@ export class InvocationService {
229
232
  ...("args" in echoed ? {} : { argsOmitted: true }),
230
233
  },
231
234
  retry: "Do not retry automatically. Check whether the write took effect first." +
232
- ("args" in echoed ? "" : " The arguments exceed the echo budget; use the exact arguments you sent."),
235
+ (echoed.argsRedacted
236
+ ? " Sensitive fields are omitted; use the original arguments if reconciliation requires another call."
237
+ : "args" in echoed
238
+ ? ""
239
+ : " The arguments could not be echoed safely; use the exact arguments you sent."),
233
240
  };
234
241
  }
235
242
  if (error.code === "auth_required" && target.connector.startAuth) {
@@ -244,12 +251,17 @@ export class InvocationService {
244
251
  ...error,
245
252
  nextAction: {
246
253
  tool: resolved?.definition.classification === "read" ? "call_tool" : "call_destructive_tool",
247
- arguments: { address: `${target.connector.id}.${target.toolName}`, ...echoedCallArgs(args) },
248
- purpose: "Use this direct call so the host can fulfill the downstream input request.",
254
+ arguments: { address: `${target.connector.id}.${target.toolName}`, ...argumentEcho },
255
+ purpose: "Use this direct call so the host can fulfill the downstream input request." +
256
+ (argumentEcho.argsRedacted
257
+ ? " Re-send the original arguments; sensitive fields are omitted."
258
+ : "args" in argumentEcho
259
+ ? ""
260
+ : " Re-send the original arguments; they could not be echoed safely."),
249
261
  },
250
262
  };
251
263
  case "destructive_tool_requires_approval": {
252
- const echoed = echoedCallArgs(args);
264
+ const echoed = argumentEcho;
253
265
  return {
254
266
  ...error,
255
267
  nextAction: {
@@ -259,9 +271,11 @@ export class InvocationService {
259
271
  ...echoed,
260
272
  },
261
273
  purpose: "Ask the MCP host to approve this consequential call. " +
262
- ("args" in echoed
263
- ? "Re-send these arguments and add a short reason for the human reviewer."
264
- : "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
274
+ (echoed.argsRedacted
275
+ ? "Re-send the original arguments; sensitive fields are omitted from this hint. Add a short reason for the human reviewer."
276
+ : "args" in echoed
277
+ ? "Re-send these arguments and add a short reason for the human reviewer."
278
+ : "Re-send the arguments you just sent; they could not be echoed safely. Add a short reason for the human reviewer."),
265
279
  },
266
280
  };
267
281
  }
@@ -325,7 +339,8 @@ export class InvocationService {
325
339
  details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
326
340
  };
327
341
  }
328
- const details = sentSecrets.redact(enrich(error, target));
342
+ // Downstream diagnostics were redacted under payload rules where they entered.
343
+ const details = sentSecrets.redact(enrich(error, target), "envelope");
329
344
  const outcome = () => ({
330
345
  ok: false,
331
346
  durationMs: Date.now() - started,
@@ -365,6 +380,7 @@ export class InvocationService {
365
380
  return failed(callerCancelledDetails());
366
381
  }
367
382
  let result;
383
+ let rawResult;
368
384
  let inputRequiredValue;
369
385
  let observedResult;
370
386
  let valueFormat = "json";
@@ -398,11 +414,15 @@ export class InvocationService {
398
414
  const target = resolution.resolved;
399
415
  resolved = target;
400
416
  activityTarget = target;
417
+ argumentEcho = echoedCallArgs(args ?? {}, target.definition.inputSchema);
418
+ sentSecrets.arguments(args ?? {}, target.definition.inputSchema);
401
419
  const write = target.definition.classification !== "read";
402
420
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
403
421
  const input = context.source !== "execute_code"
404
422
  ? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
405
423
  : undefined;
424
+ if (input)
425
+ recordDownstreamArgumentEcho(this.catalog.requestScope, argumentEcho);
406
426
  const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
407
427
  if (expectedDigest !== undefined &&
408
428
  (target.classificationFresh !== true ||
@@ -536,7 +556,8 @@ export class InvocationService {
536
556
  /* Unserializable values have no measurable byte count. */
537
557
  }
538
558
  valueFormat = attempt.value.observed.format;
539
- result = context.unwrapResult ? observedResult : attempt.value.raw;
559
+ rawResult = attempt.value.raw;
560
+ result = context.unwrapResult ? observedResult : rawResult;
540
561
  return undefined;
541
562
  });
542
563
  // One deadline owns discovery, queue admission, and provider dispatch.
@@ -571,7 +592,7 @@ export class InvocationService {
571
592
  ? Effect.succeed(inputRequiredValue.value)
572
593
  : processResult
573
594
  ? Effect.tryPromise({
574
- try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat)),
595
+ try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat, rawResult)),
575
596
  catch: (error) => error,
576
597
  })
577
598
  : Effect.succeed(result);
@@ -592,7 +613,7 @@ export class InvocationService {
592
613
  if (Exit.isFailure(processed))
593
614
  return unprocessable();
594
615
  try {
595
- const value = sentSecrets.redact(processed.value);
616
+ const value = sentSecrets.redact(processed.value, "envelope");
596
617
  const diagnostics = timing();
597
618
  const friction = context.activityFriction?.(value);
598
619
  record("success", friction ? { friction } : {});
@@ -610,6 +631,11 @@ export class InvocationService {
610
631
  catch {
611
632
  return unprocessable();
612
633
  }
613
- });
634
+ }).pipe(Effect.flatMap((outcome) => outcome.ok
635
+ ? Effect.succeed(outcome)
636
+ : Effect.promise(async () => ({
637
+ ...outcome,
638
+ error: await withAuthorizationHandoff(this.catalog.requestScope, outcome.error),
639
+ }))));
614
640
  }
615
641
  }
@@ -51,21 +51,6 @@ export const SEARCH_OUTPUT = {
51
51
  },
52
52
  },
53
53
  };
54
- export const CALL_OUTPUT = advertisedSchema(z
55
- .object({
56
- ok: z.boolean().optional(),
57
- data: z.unknown().optional(),
58
- format: format.optional(),
59
- error: error.optional(),
60
- durationMs: z.number().nonnegative().optional(),
61
- attempts: z.number().int().nonnegative().optional(),
62
- truncated: z.boolean().optional(),
63
- resultId: z.string().optional(),
64
- totalBytes: z.number().int().nonnegative().optional(),
65
- hint: z.string().optional(),
66
- nextAction: z.record(z.string(), z.unknown()).optional(),
67
- })
68
- .passthrough());
69
54
  export const AUTHORIZE_OUTPUT = advertisedSchema(z
70
55
  .object({
71
56
  connector: z.string().optional(),