@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
@@ -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";
@@ -9,8 +10,10 @@ import { splitAddress } from "./registry.js";
9
10
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
10
11
  import { validateCatalogToolInput } from "./validate.js";
11
12
  import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
13
+ import { recoveryFor } from "./call-recovery.js";
14
+ import { classifyWriteOutcome } from "./write-outcome.js";
12
15
  import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
13
- import { downstreamContinuation, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
16
+ import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
14
17
  function defined(values) {
15
18
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
16
19
  }
@@ -111,6 +114,7 @@ export class InvocationFailure extends Error {
111
114
  recovery;
112
115
  nextAction;
113
116
  retry;
117
+ data;
114
118
  constructor(details) {
115
119
  super(details.message);
116
120
  this.details = details;
@@ -123,6 +127,7 @@ export class InvocationFailure extends Error {
123
127
  this.recovery = details.recovery;
124
128
  this.nextAction = details.nextAction;
125
129
  this.retry = details.retry;
130
+ this.data = details;
126
131
  }
127
132
  }
128
133
  /**
@@ -155,7 +160,7 @@ export class InvocationService {
155
160
  pipeline(address, args, context) {
156
161
  return Effect.gen({ self: this }, function* () {
157
162
  const started = Date.now();
158
- const argumentEcho = echoedCallArgs(args ?? {});
163
+ let argumentEcho = {};
159
164
  let catalogMs = 0;
160
165
  let admissionMs = 0;
161
166
  let connectorMs = 0;
@@ -163,6 +168,9 @@ export class InvocationService {
163
168
  let attempts = 0;
164
169
  let resultBytes;
165
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;
166
174
  let preInvocationAuthFailure = false;
167
175
  let answered = false;
168
176
  const sentSecrets = sentSecretsForRequest(this.catalog.requestScope);
@@ -214,6 +222,10 @@ export class InvocationService {
214
222
  const enrich = (error, target) => {
215
223
  if (!target)
216
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
+ : "";
217
229
  if (isTimeoutFailure(error) && dispatchedToConnector && resolved?.definition.classification === "write") {
218
230
  const echoed = argumentEcho;
219
231
  return {
@@ -227,9 +239,35 @@ export class InvocationService {
227
239
  address: `${target.connector.id}.${target.toolName}`,
228
240
  ...echoed,
229
241
  ...("args" in echoed ? {} : { argsOmitted: true }),
242
+ ...(recovery ? { recovery } : {}),
230
243
  },
231
244
  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."),
245
+ recoveryRetry +
246
+ (echoed.argsRedacted
247
+ ? " Sensitive fields are omitted; use the original arguments if reconciliation requires another call."
248
+ : "args" in echoed
249
+ ? ""
250
+ : " The arguments could not be echoed safely; use the exact arguments you sent."),
251
+ };
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}`,
233
271
  };
234
272
  }
235
273
  if (error.code === "auth_required" && target.connector.startAuth) {
@@ -244,12 +282,17 @@ export class InvocationService {
244
282
  ...error,
245
283
  nextAction: {
246
284
  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.",
285
+ arguments: { address: `${target.connector.id}.${target.toolName}`, ...argumentEcho },
286
+ purpose: "Use this direct call so the host can fulfill the downstream input request." +
287
+ (argumentEcho.argsRedacted
288
+ ? " Re-send the original arguments; sensitive fields are omitted."
289
+ : "args" in argumentEcho
290
+ ? ""
291
+ : " Re-send the original arguments; they could not be echoed safely."),
249
292
  },
250
293
  };
251
294
  case "destructive_tool_requires_approval": {
252
- const echoed = echoedCallArgs(args);
295
+ const echoed = argumentEcho;
253
296
  return {
254
297
  ...error,
255
298
  nextAction: {
@@ -259,9 +302,11 @@ export class InvocationService {
259
302
  ...echoed,
260
303
  },
261
304
  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."),
305
+ (echoed.argsRedacted
306
+ ? "Re-send the original arguments; sensitive fields are omitted from this hint. Add a short reason for the human reviewer."
307
+ : "args" in echoed
308
+ ? "Re-send these arguments and add a short reason for the human reviewer."
309
+ : "Re-send the arguments you just sent; they could not be echoed safely. Add a short reason for the human reviewer."),
265
310
  },
266
311
  };
267
312
  }
@@ -325,7 +370,8 @@ export class InvocationService {
325
370
  details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
326
371
  };
327
372
  }
328
- const details = sentSecrets.redact(enrich(error, target));
373
+ // Downstream diagnostics were redacted under payload rules where they entered.
374
+ const details = sentSecrets.redact(enrich(error, target), "envelope");
329
375
  const outcome = () => ({
330
376
  ok: false,
331
377
  durationMs: Date.now() - started,
@@ -365,6 +411,7 @@ export class InvocationService {
365
411
  return failed(callerCancelledDetails());
366
412
  }
367
413
  let result;
414
+ let rawResult;
368
415
  let inputRequiredValue;
369
416
  let observedResult;
370
417
  let valueFormat = "json";
@@ -398,11 +445,15 @@ export class InvocationService {
398
445
  const target = resolution.resolved;
399
446
  resolved = target;
400
447
  activityTarget = target;
448
+ argumentEcho = echoedCallArgs(args ?? {}, target.definition.inputSchema);
449
+ sentSecrets.arguments(args ?? {}, target.definition.inputSchema);
401
450
  const write = target.definition.classification !== "read";
402
451
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
403
452
  const input = context.source !== "execute_code"
404
453
  ? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
405
454
  : undefined;
455
+ if (input)
456
+ recordDownstreamArgumentEcho(this.catalog.requestScope, argumentEcho);
406
457
  const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
407
458
  if (expectedDigest !== undefined &&
408
459
  (target.classificationFresh !== true ||
@@ -444,6 +495,7 @@ export class InvocationService {
444
495
  const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
445
496
  trackCredentialReads(connectorContext);
446
497
  sentSecrets.include(sentSecretsFor(connectorContext));
498
+ dispatchContext = connectorContext;
447
499
  try {
448
500
  if (target.connector.credential) {
449
501
  if (!connectorContext.credential) {
@@ -536,7 +588,8 @@ export class InvocationService {
536
588
  /* Unserializable values have no measurable byte count. */
537
589
  }
538
590
  valueFormat = attempt.value.observed.format;
539
- result = context.unwrapResult ? observedResult : attempt.value.raw;
591
+ rawResult = attempt.value.raw;
592
+ result = context.unwrapResult ? observedResult : rawResult;
540
593
  return undefined;
541
594
  });
542
595
  // One deadline owns discovery, queue admission, and provider dispatch.
@@ -571,7 +624,7 @@ export class InvocationService {
571
624
  ? Effect.succeed(inputRequiredValue.value)
572
625
  : processResult
573
626
  ? Effect.tryPromise({
574
- try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat)),
627
+ try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat, rawResult)),
575
628
  catch: (error) => error,
576
629
  })
577
630
  : Effect.succeed(result);
@@ -592,7 +645,7 @@ export class InvocationService {
592
645
  if (Exit.isFailure(processed))
593
646
  return unprocessable();
594
647
  try {
595
- const value = sentSecrets.redact(processed.value);
648
+ const value = sentSecrets.redact(processed.value, "envelope");
596
649
  const diagnostics = timing();
597
650
  const friction = context.activityFriction?.(value);
598
651
  record("success", friction ? { friction } : {});
@@ -610,6 +663,11 @@ export class InvocationService {
610
663
  catch {
611
664
  return unprocessable();
612
665
  }
613
- });
666
+ }).pipe(Effect.flatMap((outcome) => outcome.ok
667
+ ? Effect.succeed(outcome)
668
+ : Effect.promise(async () => ({
669
+ ...outcome,
670
+ error: await withAuthorizationHandoff(this.catalog.requestScope, outcome.error),
671
+ }))));
614
672
  }
615
673
  }
@@ -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(),