@zackbart/connecta 0.28.1 → 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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,69 @@
1
+ import { vettedSchemaDigest } from "./catalog-drift.js";
2
+ // Only built-in factories register local credential checks. This hook is not
3
+ // part of Connector, and a replacement callTool cannot inherit its proof.
4
+ const checks = new WeakMap();
5
+ export function registerInvocationAuth(connector, check) {
6
+ checks.set(connector, { callTool: connector.callTool, check });
7
+ }
8
+ /** Local credential resolution only, before entering any connector call. */
9
+ export async function resolveInvocationAuth(connector, ctx) {
10
+ const entry = checks.get(connector);
11
+ if (entry?.callTool === connector.callTool)
12
+ await entry.check(ctx);
13
+ }
14
+ const requests = new WeakMap();
15
+ function requestFor(scope) {
16
+ let facts = requests.get(scope);
17
+ if (!facts) {
18
+ facts = { entered: [], authFailures: new Map(), retryReads: new Map() };
19
+ requests.set(scope, facts);
20
+ }
21
+ return facts;
22
+ }
23
+ /** Bind the verdict, annotation facts and schemas used for this call. */
24
+ export async function classificationDigest(definition) {
25
+ try {
26
+ const schema = await vettedSchemaDigest(definition);
27
+ const annotations = Object.entries(definition.annotations ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
28
+ const bytes = new TextEncoder().encode(JSON.stringify({ classification: definition.classification, annotations, schema }));
29
+ return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
30
+ }
31
+ catch {
32
+ // A digest refusal blocks recovery, not an ordinary call.
33
+ return "";
34
+ }
35
+ }
36
+ /** Called by the host immediately before callTool, using its private scope. */
37
+ export function recordCallEntry(scope, call, definition) {
38
+ // Snapshot synchronously. Recovery hashes this private copy only when it
39
+ // needs a replay binding, without changing ordinary admission or completion.
40
+ requestFor(scope).entered.push({ ...call, definition: structuredClone(definition) });
41
+ }
42
+ export function bindReplayReads(scope, reads) {
43
+ requestFor(scope).retryReads = new Map(reads.map((read) => [read.address, read.digest]));
44
+ }
45
+ export function replayClassificationDigest(scope, address) {
46
+ return requestFor(scope).retryReads.get(address);
47
+ }
48
+ /** Eligibility comes from invocation control flow, never error fields. */
49
+ export function recordAuthFailure(scope, connector, eligible) {
50
+ const failures = requestFor(scope).authFailures;
51
+ failures.set(connector, eligible && failures.get(connector) !== false);
52
+ }
53
+ export async function authRecoveryFacts(scope, connector) {
54
+ const facts = requestFor(scope);
55
+ const writeEntered = facts.entered.some((call) => call.classification === "write");
56
+ const reads = new Map();
57
+ let unsafeEntered = writeEntered;
58
+ for (const call of facts.entered) {
59
+ const digest = await classificationDigest(call.definition);
60
+ unsafeEntered ||= !call.fresh || !digest || (reads.has(call.address) && reads.get(call.address) !== digest);
61
+ reads.set(call.address, digest);
62
+ }
63
+ return {
64
+ writeEntered,
65
+ unsafeEntered,
66
+ eligible: !unsafeEntered && facts.authFailures.get(connector) === true,
67
+ reads: [...reads].map(([address, digest]) => ({ address, digest })),
68
+ };
69
+ }
@@ -1,11 +1,17 @@
1
+ import { withAuthorizationHandoff } from "./authorization-handoff.js";
2
+ import { hasControlCharacters } from "./tool-name.js";
3
+ import { surfaceAllowsTool } from "./tool-safety.js";
1
4
  import { Cause, Effect, Exit } from "effect";
2
- import { isCallAdmissionError, } from "./call-admission.js";
3
- import { boundedEchoText, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
4
- import { unwrapMcpResult } from "./mcp-result.js";
5
+ import { isCallAdmissionError } from "./call-admission.js";
6
+ import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
7
+ import { downstreamValue } from "./mcp-result.js";
8
+ import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
5
9
  import { splitAddress } from "./registry.js";
6
10
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
7
- import { isExplicitlyReadOnly } from "./tool-safety.js";
8
- import { validateToolInput } from "./validate.js";
11
+ import { validateCatalogToolInput } from "./validate.js";
12
+ import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
13
+ import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
14
+ import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
9
15
  function defined(values) {
10
16
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
11
17
  }
@@ -38,8 +44,7 @@ function admitted(registry, target, args, signal) {
38
44
  args: args ?? {},
39
45
  ...defined({ signal }),
40
46
  });
41
- return Effect.tryPromise({ try: () => pending, catch: (error) => error })
42
- .pipe(Effect.onInterrupt(() => Effect.sync(() => {
47
+ return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
43
48
  pending.then((permit) => permit.release(), () => { });
44
49
  })));
45
50
  }), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
@@ -54,23 +59,29 @@ function callerCancelledDetails() {
54
59
  function recoveryMode(registry, connector, baseUrl) {
55
60
  if (connector.startAuth)
56
61
  return "oauth";
57
- if (registry.credentialUiAvailable() && connector.credential &&
62
+ if (registry.credentialUiAvailable() &&
63
+ connector.credential &&
58
64
  registry.contextFor(connector.id, baseUrl).credential) {
59
65
  return "operator_config";
60
66
  }
61
67
  return "unavailable";
62
68
  }
63
69
  function isCallerCancellation(error, signal) {
64
- return (signal?.aborted === true ||
65
- (isCallAdmissionError(error) && error.admissionKind === "cancelled"));
70
+ return signal?.aborted === true || (isCallAdmissionError(error) && error.admissionKind === "cancelled");
66
71
  }
67
72
  /**
68
73
  * A downstream MCP tool answered with `isError`. Classified exactly as the
69
- * plain `Error` it used to be — same message, same heuristics — and marked so
74
+ * plain `Error`: untyped, non-retryable, and marked so
70
75
  * a write's outcome can tell "the service said no" from "nobody answered".
71
76
  */
72
77
  class DownstreamToolError extends Error {
73
78
  }
79
+ function isTimeoutFailure(error) {
80
+ const code = error.details?.code;
81
+ return (error.code === "timeout" ||
82
+ (error.code === "unavailable" &&
83
+ (code === "timeout" || code === "ETIMEDOUT" || code?.endsWith("_TIMEOUT") === true)));
84
+ }
74
85
  function assertRawMcpSuccess(kind, result) {
75
86
  if (kind !== "mcp" || result == null || typeof result !== "object")
76
87
  return;
@@ -101,6 +112,7 @@ export class InvocationFailure extends Error {
101
112
  recovery;
102
113
  nextAction;
103
114
  retry;
115
+ data;
104
116
  constructor(details) {
105
117
  super(details.message);
106
118
  this.details = details;
@@ -113,6 +125,7 @@ export class InvocationFailure extends Error {
113
125
  this.recovery = details.recovery;
114
126
  this.nextAction = details.nextAction;
115
127
  this.retry = details.retry;
128
+ this.data = details;
116
129
  }
117
130
  }
118
131
  /**
@@ -145,13 +158,18 @@ export class InvocationService {
145
158
  pipeline(address, args, context) {
146
159
  return Effect.gen({ self: this }, function* () {
147
160
  const started = Date.now();
161
+ let argumentEcho = {};
148
162
  let catalogMs = 0;
149
163
  let admissionMs = 0;
150
164
  let connectorMs = 0;
151
165
  let resultProcessingMs = 0;
152
166
  let attempts = 0;
167
+ let resultBytes;
153
168
  let dispatchedToConnector = false;
169
+ let preInvocationAuthFailure = false;
154
170
  let answered = false;
171
+ const sentSecrets = sentSecretsForRequest(this.catalog.requestScope);
172
+ context.sentSecrets?.include(sentSecrets);
155
173
  // A write gate's refusal that is no attempt; see WriteGateDecision.
156
174
  let unrecorded = false;
157
175
  let resolved;
@@ -171,20 +189,26 @@ export class InvocationService {
171
189
  const identity = activityTarget
172
190
  ? {
173
191
  connectorId: activityTarget.connector.id,
174
- toolName: activityTarget.toolName,
192
+ // A resolved tool's name is the downstream's choice, recorded
193
+ // only when it fits the tool-name grammar (INV-6).
194
+ toolName: resolved ? recordedToolName(resolved.definition) : activityTarget.toolName,
175
195
  }
176
196
  : attempted;
177
197
  if (!identity)
178
198
  return;
199
+ const toolName = hasControlCharacters(identity.toolName) ? "<unlisted>" : identity.toolName;
179
200
  this.activity?.recordTool?.(this.activity, {
180
201
  connectorId: identity.connectorId,
181
- toolName: identity.toolName,
182
- address: `${identity.connectorId}.${identity.toolName}`,
202
+ toolName,
203
+ address: `${identity.connectorId}.${toolName}`,
183
204
  source: context.source,
184
205
  outcome,
206
+ ...(activityTarget?.connector.authScope === "personal" ? { personal: true } : {}),
185
207
  durationMs: Date.now() - started,
186
208
  attempts,
187
209
  ...defined({
210
+ classification: resolved?.definition.classification,
211
+ resultBytes,
188
212
  errorCode: classification.errorCode,
189
213
  friction: classification.friction,
190
214
  }),
@@ -193,9 +217,51 @@ export class InvocationService {
193
217
  const enrich = (error, target) => {
194
218
  if (!target)
195
219
  return error;
220
+ if (isTimeoutFailure(error) && dispatchedToConnector && resolved?.definition.classification === "write") {
221
+ const echoed = argumentEcho;
222
+ return {
223
+ ...error,
224
+ code: "write_outcome_unknown",
225
+ message: `${error.message} Its outcome is unknown. Check the target before repeating this call.`,
226
+ retryable: false,
227
+ connector: target.connector.id,
228
+ operation: `${target.connector.id}.${target.toolName}`,
229
+ uncertainCall: {
230
+ address: `${target.connector.id}.${target.toolName}`,
231
+ ...echoed,
232
+ ...("args" in echoed ? {} : { argsOmitted: true }),
233
+ },
234
+ retry: "Do not retry automatically. Check whether the write took effect first." +
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."),
240
+ };
241
+ }
242
+ if (error.code === "auth_required" && target.connector.startAuth) {
243
+ error = { ...error, code: "downstream_oauth_required" };
244
+ }
245
+ const enteredWrite = resolved?.definition.classification === "write" && dispatchedToConnector;
196
246
  switch (error.code) {
247
+ case "input_required_unsupported":
248
+ if (context.processInputRequired)
249
+ return error;
250
+ return {
251
+ ...error,
252
+ nextAction: {
253
+ tool: resolved?.definition.classification === "read" ? "call_tool" : "call_destructive_tool",
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."),
261
+ },
262
+ };
197
263
  case "destructive_tool_requires_approval": {
198
- const echoed = echoedCallArgs(args);
264
+ const echoed = argumentEcho;
199
265
  return {
200
266
  ...error,
201
267
  nextAction: {
@@ -205,25 +271,37 @@ export class InvocationService {
205
271
  ...echoed,
206
272
  },
207
273
  purpose: "Ask the MCP host to approve this consequential call. " +
208
- ("args" in echoed
209
- ? "Re-send these arguments and add a short reason for the human reviewer."
210
- : "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."),
211
279
  },
212
280
  };
213
281
  }
214
282
  case "auth_required":
283
+ case "downstream_oauth_required":
215
284
  return {
216
285
  ...error,
217
286
  connector: target.connector.id,
218
287
  operation: `${target.connector.id}.${target.toolName}`,
219
288
  recovery: recoveryMode(this.registry, target.connector, this.catalog.baseUrl),
289
+ ...(enteredWrite ? { reconciliationRequired: true, retryable: false } : {}),
220
290
  nextAction: {
221
291
  tool: "authorize_connector",
222
292
  arguments: { connector: target.connector.id },
223
293
  operatorHandoff: "Give the URL and instructions it returns to the operator.",
224
294
  },
225
- retry: `Retry ${target.connector.id}.${target.toolName} after ` +
226
- "the operator completes recovery.",
295
+ retry: enteredWrite
296
+ ? "This write may have partially run. Reconcile its target before retrying after the operator completes recovery."
297
+ : `Retry ${target.connector.id}.${target.toolName} after the operator completes recovery.`,
298
+ };
299
+ case "provider_permission_denied":
300
+ return {
301
+ ...error,
302
+ connector: target.connector.id,
303
+ operation: `${target.connector.id}.${target.toolName}`,
304
+ retry: "Ask the provider's resource owner or administrator to grant the required permission or scope, then retry. Repeating the call or reconnecting alone will not grant access.",
227
305
  };
228
306
  case "invalid_args":
229
307
  if (!error.validation)
@@ -236,17 +314,33 @@ export class InvocationService {
236
314
  query: target.toolName,
237
315
  connector: target.connector.id,
238
316
  }, "Inspect the current input shape if the validation findings are not sufficient."),
239
- retry: `Correct the listed arguments and retry ` +
240
- `${target.connector.id}.${target.toolName}.`,
317
+ retry: `Correct the listed arguments and retry ` + `${target.connector.id}.${target.toolName}.`,
241
318
  };
242
319
  default:
243
320
  return error;
244
321
  }
245
322
  };
323
+ let stage = "catalog";
246
324
  const failed = (error) => {
247
325
  const diagnostics = timing();
248
326
  const target = resolved ?? activityTarget;
249
- const details = enrich(error, target);
327
+ if (target && (error.code === "auth_required" || error.code === "downstream_oauth_required")) {
328
+ recordAuthFailure(this.catalog.requestScope, target.connector.id, (resolved?.definition.classification === "read" && resolved.classificationFresh === true) ||
329
+ preInvocationAuthFailure ||
330
+ (!resolved && context.source === "call_tool"));
331
+ }
332
+ // Attach host deadline facts before write recovery changes the code.
333
+ if (isTimeoutFailure(error)) {
334
+ const elapsedMs = Date.now() - started;
335
+ const operation = boundedEchoText(address, 512);
336
+ error = {
337
+ ...error,
338
+ message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${context.timeoutMs}ms).`,
339
+ details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
340
+ };
341
+ }
342
+ // Downstream diagnostics were redacted under payload rules where they entered.
343
+ const details = sentSecrets.redact(enrich(error, target), "envelope");
250
344
  const outcome = () => ({
251
345
  ok: false,
252
346
  durationMs: Date.now() - started,
@@ -261,47 +355,50 @@ export class InvocationService {
261
355
  // leaves no event and nothing in the operator's log.
262
356
  if (unrecorded)
263
357
  return outcome();
264
- // Activity rows stay payload-free by construction; the operator's log is
265
- // where the downstream reason goes, bounded and without arguments.
358
+ // Activity rows and this log line are both payload-free by
359
+ // construction: the line is the failure's typed record, never its
360
+ // message, which may be the downstream's own words (INV-6).
266
361
  if (target && details.code !== "destructive_tool_requires_approval") {
267
- this.registry
268
- .contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
269
- .logger.warn("[connecta] call failed", {
362
+ logFailure(this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger, "call failed", failureRecord({
270
363
  connector: target.connector.id,
271
- tool: target.toolName,
364
+ // Named only when the catalog listed it (src/operator-record.ts).
365
+ tool: resolved?.definition,
272
366
  source: context.source,
273
- code: details.code,
274
- // Sanitized transport diagnostics (origin and errno only, #539).
275
- ...(details.details ? { details: details.details } : {}),
276
367
  attempts,
277
368
  durationMs: Date.now() - started,
278
- message: String(details.message ?? "").slice(0, 300),
279
- });
369
+ // Every refusal reaching `failed` is one connecta built: a
370
+ // thrown value's classification, a framing refusal, or a
371
+ // cancellation. Nothing else is read as a classification.
372
+ }, classifiedFailure(error)));
280
373
  }
281
- record(details.code === "timeout"
282
- ? "timeout"
283
- : details.code === "cancelled"
284
- ? "cancelled"
285
- : "error", { errorCode: details.code });
374
+ record(details.code === "timeout" ? "timeout" : details.code === "cancelled" ? "cancelled" : "error", defined({ errorCode: classificationCode(details.code) }));
286
375
  return outcome();
287
376
  };
288
- if (context.requestSignal?.aborted) {
377
+ if (context.requestSignal?.aborted || context.dispatchSignal?.aborted) {
289
378
  // Preserve the cancelled admission-attempt count without starting discovery.
290
379
  attempts = 1;
291
380
  return failed(callerCancelledDetails());
292
381
  }
293
382
  let result;
383
+ let rawResult;
384
+ let inputRequiredValue;
294
385
  let observedResult;
386
+ let valueFormat = "json";
295
387
  // Everything up to the downstream result: resolution, the safety and
296
388
  // schema refusals, admission, and the one attempt. A refusal is its
297
389
  // success value. Its failure is an abort reason or something nobody
298
390
  // expected, and a throw anywhere in it — a defect, to Effect — is
299
391
  // classified like a failure, as the async body this replaced did.
300
392
  const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
301
- const resolution = yield* this.catalog.resolve(address, defined({ signal: callSignal }));
393
+ const admissionSignal = context.dispatchSignal
394
+ ? AbortSignal.any([context.dispatchSignal, ...(callSignal ? [callSignal] : [])])
395
+ : callSignal;
396
+ const resolution = yield* this.catalog.resolve(address, defined({ signal: admissionSignal }));
302
397
  catalogMs += resolution.catalogMs;
303
398
  if (callSignal?.aborted)
304
399
  return yield* Effect.fail(callSignal.reason);
400
+ if (admissionSignal?.aborted)
401
+ return callerCancelledDetails();
305
402
  if (!resolution.ok) {
306
403
  if (resolution.connector && resolution.toolName) {
307
404
  activityTarget = {
@@ -317,30 +414,44 @@ export class InvocationService {
317
414
  const target = resolution.resolved;
318
415
  resolved = target;
319
416
  activityTarget = target;
320
- const consequential = !isExplicitlyReadOnly(target.definition) && !context.allowDestructive;
321
- const gate = consequential && (context.gates?.(target) ?? true)
322
- ? context.writeGate
417
+ argumentEcho = echoedCallArgs(args ?? {}, target.definition.inputSchema);
418
+ sentSecrets.arguments(args ?? {}, target.definition.inputSchema);
419
+ const write = target.definition.classification !== "read";
420
+ const canonicalAddress = `${target.connector.id}.${target.toolName}`;
421
+ const input = context.source !== "execute_code"
422
+ ? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
323
423
  : undefined;
324
- if (consequential && !gate) {
424
+ if (input)
425
+ recordDownstreamArgumentEcho(this.catalog.requestScope, argumentEcho);
426
+ const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
427
+ if (expectedDigest !== undefined &&
428
+ (target.classificationFresh !== true ||
429
+ (yield* Effect.promise(() => classificationDigest(target.definition))) !== expectedDigest)) {
430
+ return {
431
+ ...framingError("auth_replay_refused", "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request."),
432
+ reconciliationRequired: true,
433
+ };
434
+ }
435
+ if (!surfaceAllowsTool(target.definition.classification, context.source, context.trust)) {
325
436
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
326
- return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`);
437
+ return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
327
438
  }
328
439
  // Remote MCP tools advertise their input schema in the catalog. Validate
329
440
  // against that same request-local definition before admission or provider
330
441
  // dispatch, so a predictable mismatch stays structured instead of being
331
442
  // flattened into provider-specific error prose. Unsupported schemas retain
332
443
  // validateToolInput's fail-open behavior and reach the downstream normally.
333
- if (target.connector.kind === "mcp" &&
334
- target.definition.inputSchema) {
335
- const invalid = validateToolInput(target.definition.inputSchema, args ?? {}, {
444
+ if (target.connector.kind === "mcp" && target.definition.inputSchema) {
445
+ const invalid = validateCatalogToolInput(target.definition.inputSchema, args ?? {}, {
336
446
  address: `${target.connector.id}.${target.toolName}`,
337
- logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger,
338
- });
447
+ logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
448
+ .logger,
449
+ }, { connector: target.connector.id, tool: target.definition });
339
450
  if (invalid)
340
451
  return classifyCallError(invalid);
341
452
  }
342
- if (gate) {
343
- const decision = yield* gate(target, args ?? {});
453
+ if (write && context.beforeWrite) {
454
+ const decision = yield* context.beforeWrite(target, args ?? {});
344
455
  if (decision.kind === "refuse") {
345
456
  unrecorded = decision.unrecorded === true;
346
457
  return decision.error;
@@ -349,46 +460,104 @@ export class InvocationService {
349
460
  if (callSignal?.aborted)
350
461
  return yield* Effect.fail(callSignal.reason);
351
462
  attempts = 1;
352
- const call = () => {
353
- const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
354
- if (target.connector.credential &&
355
- !connectorContext.credential) {
356
- throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
357
- `authorize_connector({ connector: "${target.connector.id}" }).`);
463
+ const call = async () => {
464
+ const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
465
+ trackCredentialReads(connectorContext);
466
+ sentSecrets.include(sentSecretsFor(connectorContext));
467
+ try {
468
+ if (target.connector.credential) {
469
+ if (!connectorContext.credential) {
470
+ throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
471
+ `authorize_connector({ connector: "${target.connector.id}" }).`);
472
+ }
473
+ if (!(await connectorContext.credential.getAll())) {
474
+ throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. Call authorize_connector.`);
475
+ }
476
+ }
477
+ await resolveInvocationAuth(target.connector, connectorContext);
478
+ }
479
+ catch (error) {
480
+ const code = classifyCallError(error).code;
481
+ preInvocationAuthFailure = code === "auth_required" || code === "downstream_oauth_required";
482
+ throw error;
358
483
  }
359
484
  // Cancellation can arrive during admission or context construction.
360
- if (callSignal?.aborted)
361
- throw callSignal.reason;
485
+ if (admissionSignal?.aborted)
486
+ throw admissionSignal.reason;
362
487
  dispatchedToConnector = true;
363
- return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
488
+ recordCallEntry(this.catalog.requestScope, {
489
+ address: canonicalAddress,
490
+ classification: write ? "write" : "read",
491
+ fresh: target.classificationFresh === true,
492
+ }, target.definition);
493
+ return await target.connector.callTool(target.toolName, args ?? {}, connectorContext,
494
+ // The connector may retain or mutate its definition. Keep the
495
+ // invocation's classification and schema private, even during
496
+ // this call, and give every dispatch its own deep copy.
497
+ { definition: structuredClone(target.definition), ...defined({ input }) });
364
498
  };
365
499
  // The permit belongs to this scope, so success, failure, and the
366
500
  // deadline's interruption all release it. Interruption stops the
367
501
  // wait instead of waiting for the connector, so an uncooperative one
368
502
  // cannot hold its permit past the deadline.
369
503
  const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
370
- yield* timed((elapsed) => { admissionMs += elapsed; }, admitted(this.registry, target, args, callSignal));
371
- const raw = yield* timed((elapsed) => { connectorMs += elapsed; }, Effect.tryPromise({
504
+ stage = "admission";
505
+ yield* timed((elapsed) => {
506
+ admissionMs += elapsed;
507
+ }, admitted(this.registry, target, args, admissionSignal));
508
+ stage = "downstream";
509
+ const reply = yield* timed((elapsed) => {
510
+ connectorMs += elapsed;
511
+ }, Effect.tryPromise({
372
512
  try: () => Promise.resolve(call()),
373
513
  catch: (error) => error,
374
514
  }));
515
+ if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
516
+ if (!context.processInputRequired)
517
+ throw new ConnectorCallError("input_required_unsupported", "The downstream returned input_required. Use the equivalent direct MCP call to provide input.");
518
+ const value = yield* Effect.tryPromise({
519
+ try: () => Promise.resolve(context.processInputRequired(reply, target, sentSecrets)),
520
+ catch: (error) => error,
521
+ });
522
+ return { inputRequired: true, value };
523
+ }
524
+ assertDownstreamOutputSafe(this.catalog.requestScope, reply);
525
+ const raw = sentSecrets.redact(reply);
375
526
  // isError is checked here for BOTH result shapes so every adapter
376
527
  // reports the same downstream-failure wording, and the throw lands
377
528
  // inside the attempt where it feeds health.
378
529
  assertRawMcpSuccess(target.connector.kind, raw);
379
- return { raw, observed: unwrapMcpResult(target.connector.kind, raw) };
530
+ return { raw, observed: sentSecrets.redact(downstreamValue(target.connector.kind, raw)) };
380
531
  })));
381
532
  if (Exit.isFailure(attempt)) {
382
533
  if (callSignal?.aborted)
383
534
  return yield* Effect.fail(callSignal.reason);
535
+ if (!dispatchedToConnector && admissionSignal?.aborted)
536
+ return callerCancelledDetails();
384
537
  const attemptError = Cause.squash(attempt.cause);
385
538
  answered = answeredFailure(attemptError);
386
539
  return isCallerCancellation(attemptError, context.requestSignal)
387
540
  ? callerCancelledDetails()
388
- : classifyCallError(attemptError);
541
+ : carryFailureFacts(attemptError, classifyCallError(sentSecrets.redact(attemptError)));
542
+ }
543
+ if ("inputRequired" in attempt.value) {
544
+ inputRequiredValue = { value: attempt.value.value };
545
+ return undefined;
546
+ }
547
+ observedResult = attempt.value.observed.data;
548
+ try {
549
+ const serialized = attempt.value.observed.format === "text" && typeof observedResult === "string"
550
+ ? observedResult
551
+ : JSON.stringify(observedResult);
552
+ if (serialized !== undefined)
553
+ resultBytes = new TextEncoder().encode(serialized).byteLength;
554
+ }
555
+ catch {
556
+ /* Unserializable values have no measurable byte count. */
389
557
  }
390
- observedResult = attempt.value.observed;
391
- result = context.unwrapResult ? observedResult : attempt.value.raw;
558
+ valueFormat = attempt.value.observed.format;
559
+ rawResult = attempt.value.raw;
560
+ result = context.unwrapResult ? observedResult : rawResult;
392
561
  return undefined;
393
562
  });
394
563
  // One deadline owns discovery, queue admission, and provider dispatch.
@@ -405,9 +574,11 @@ export class InvocationService {
405
574
  })
406
575
  : dispatch());
407
576
  if (Exit.isFailure(dispatched)) {
408
- return failed(context.requestSignal?.aborted
577
+ const failure = Cause.squash(dispatched.cause);
578
+ const details = context.requestSignal?.aborted
409
579
  ? callerCancelledDetails()
410
- : classifyCallError(Cause.squash(dispatched.cause)));
580
+ : carryFailureFacts(failure, classifyCallError(sentSecrets.redact(failure)));
581
+ return failed(details);
411
582
  }
412
583
  if (dispatched.value)
413
584
  return failed(dispatched.value);
@@ -417,13 +588,19 @@ export class InvocationService {
417
588
  return yield* Effect.die(new Error("Invocation completed without a resolved tool"));
418
589
  }
419
590
  const processResult = context.processResult;
420
- const processing = processResult
421
- ? Effect.tryPromise({
422
- try: () => Promise.resolve(processResult(result, completed)),
423
- catch: (error) => error,
424
- })
425
- : Effect.succeed(result);
426
- const processed = yield* Effect.exit(timed((elapsed) => { resultProcessingMs += elapsed; }, Effect.tap(processing, () => Effect.sync(() => {
591
+ const processing = inputRequiredValue
592
+ ? Effect.succeed(inputRequiredValue.value)
593
+ : processResult
594
+ ? Effect.tryPromise({
595
+ try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat, rawResult)),
596
+ catch: (error) => error,
597
+ })
598
+ : Effect.succeed(result);
599
+ const processed = yield* Effect.exit(timed((elapsed) => {
600
+ resultProcessingMs += elapsed;
601
+ }, Effect.tap(processing, () => Effect.sync(() => {
602
+ if (inputRequiredValue)
603
+ return;
427
604
  try {
428
605
  this.registry.observeOutputShape(completed.connector.id, completed.definition, observedResult);
429
606
  }
@@ -436,13 +613,14 @@ export class InvocationService {
436
613
  if (Exit.isFailure(processed))
437
614
  return unprocessable();
438
615
  try {
439
- const value = processed.value;
616
+ const value = sentSecrets.redact(processed.value, "envelope");
440
617
  const diagnostics = timing();
441
618
  const friction = context.activityFriction?.(value);
442
619
  record("success", friction ? { friction } : {});
443
620
  return {
444
621
  ok: true,
445
622
  value,
623
+ format: valueFormat,
446
624
  resolved: completed,
447
625
  durationMs: Date.now() - started,
448
626
  attempts,
@@ -453,6 +631,11 @@ export class InvocationService {
453
631
  catch {
454
632
  return unprocessable();
455
633
  }
456
- });
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
+ }))));
457
640
  }
458
641
  }