@assemblyline-agents/runtime 0.1.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 (331) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/agent-harness.d.ts +108 -0
  4. package/dist/agent-harness.d.ts.map +1 -0
  5. package/dist/agent-harness.js +2 -0
  6. package/dist/agent-harness.js.map +1 -0
  7. package/dist/attachments-legacy.d.ts +17 -0
  8. package/dist/attachments-legacy.d.ts.map +1 -0
  9. package/dist/attachments-legacy.js +273 -0
  10. package/dist/attachments-legacy.js.map +1 -0
  11. package/dist/attachments.d.ts +49 -0
  12. package/dist/attachments.d.ts.map +1 -0
  13. package/dist/attachments.js +832 -0
  14. package/dist/attachments.js.map +1 -0
  15. package/dist/builtin-tool-executors.d.ts +96 -0
  16. package/dist/builtin-tool-executors.d.ts.map +1 -0
  17. package/dist/builtin-tool-executors.js +501 -0
  18. package/dist/builtin-tool-executors.js.map +1 -0
  19. package/dist/builtin-tools.d.ts +30 -0
  20. package/dist/builtin-tools.d.ts.map +1 -0
  21. package/dist/builtin-tools.js +690 -0
  22. package/dist/builtin-tools.js.map +1 -0
  23. package/dist/capability-catalog.d.ts +27 -0
  24. package/dist/capability-catalog.d.ts.map +1 -0
  25. package/dist/capability-catalog.js +193 -0
  26. package/dist/capability-catalog.js.map +1 -0
  27. package/dist/channel-http-util.d.ts +34 -0
  28. package/dist/channel-http-util.d.ts.map +1 -0
  29. package/dist/channel-http-util.js +93 -0
  30. package/dist/channel-http-util.js.map +1 -0
  31. package/dist/channel-ingress.d.ts +56 -0
  32. package/dist/channel-ingress.d.ts.map +1 -0
  33. package/dist/channel-ingress.js +273 -0
  34. package/dist/channel-ingress.js.map +1 -0
  35. package/dist/channel.d.ts +146 -0
  36. package/dist/channel.d.ts.map +1 -0
  37. package/dist/channel.js +2 -0
  38. package/dist/channel.js.map +1 -0
  39. package/dist/checkpoints.d.ts +34 -0
  40. package/dist/checkpoints.d.ts.map +1 -0
  41. package/dist/checkpoints.js +86 -0
  42. package/dist/checkpoints.js.map +1 -0
  43. package/dist/code-mode.d.ts +20 -0
  44. package/dist/code-mode.d.ts.map +1 -0
  45. package/dist/code-mode.js +658 -0
  46. package/dist/code-mode.js.map +1 -0
  47. package/dist/computer-relay-protocol.d.ts +36 -0
  48. package/dist/computer-relay-protocol.d.ts.map +1 -0
  49. package/dist/computer-relay-protocol.js +127 -0
  50. package/dist/computer-relay-protocol.js.map +1 -0
  51. package/dist/concurrency.d.ts +68 -0
  52. package/dist/concurrency.d.ts.map +1 -0
  53. package/dist/concurrency.js +193 -0
  54. package/dist/concurrency.js.map +1 -0
  55. package/dist/connection-auth-util.d.ts +56 -0
  56. package/dist/connection-auth-util.d.ts.map +1 -0
  57. package/dist/connection-auth-util.js +154 -0
  58. package/dist/connection-auth-util.js.map +1 -0
  59. package/dist/connection-clients.d.ts +50 -0
  60. package/dist/connection-clients.d.ts.map +1 -0
  61. package/dist/connection-clients.js +402 -0
  62. package/dist/connection-clients.js.map +1 -0
  63. package/dist/connection-stores.d.ts +108 -0
  64. package/dist/connection-stores.d.ts.map +1 -0
  65. package/dist/connection-stores.js +127 -0
  66. package/dist/connection-stores.js.map +1 -0
  67. package/dist/connection-util.d.ts +27 -0
  68. package/dist/connection-util.d.ts.map +1 -0
  69. package/dist/connection-util.js +118 -0
  70. package/dist/connection-util.js.map +1 -0
  71. package/dist/connections.d.ts +152 -0
  72. package/dist/connections.d.ts.map +1 -0
  73. package/dist/connections.js +838 -0
  74. package/dist/connections.js.map +1 -0
  75. package/dist/content-capture.d.ts +32 -0
  76. package/dist/content-capture.d.ts.map +1 -0
  77. package/dist/content-capture.js +77 -0
  78. package/dist/content-capture.js.map +1 -0
  79. package/dist/context-policy-util.d.ts +45 -0
  80. package/dist/context-policy-util.d.ts.map +1 -0
  81. package/dist/context-policy-util.js +120 -0
  82. package/dist/context-policy-util.js.map +1 -0
  83. package/dist/conversation-context.d.ts +55 -0
  84. package/dist/conversation-context.d.ts.map +1 -0
  85. package/dist/conversation-context.js +179 -0
  86. package/dist/conversation-context.js.map +1 -0
  87. package/dist/conversation-transcript.d.ts +49 -0
  88. package/dist/conversation-transcript.d.ts.map +1 -0
  89. package/dist/conversation-transcript.js +77 -0
  90. package/dist/conversation-transcript.js.map +1 -0
  91. package/dist/conversation-turn-dispatch.d.ts +46 -0
  92. package/dist/conversation-turn-dispatch.d.ts.map +1 -0
  93. package/dist/conversation-turn-dispatch.js +201 -0
  94. package/dist/conversation-turn-dispatch.js.map +1 -0
  95. package/dist/delivery-queue.d.ts +89 -0
  96. package/dist/delivery-queue.d.ts.map +1 -0
  97. package/dist/delivery-queue.js +244 -0
  98. package/dist/delivery-queue.js.map +1 -0
  99. package/dist/delivery-send.d.ts +44 -0
  100. package/dist/delivery-send.d.ts.map +1 -0
  101. package/dist/delivery-send.js +494 -0
  102. package/dist/delivery-send.js.map +1 -0
  103. package/dist/env-config.d.ts +6 -0
  104. package/dist/env-config.d.ts.map +1 -0
  105. package/dist/env-config.js +27 -0
  106. package/dist/env-config.js.map +1 -0
  107. package/dist/eval-run-options.d.ts +37 -0
  108. package/dist/eval-run-options.d.ts.map +1 -0
  109. package/dist/eval-run-options.js +106 -0
  110. package/dist/eval-run-options.js.map +1 -0
  111. package/dist/harness-continuation.d.ts +54 -0
  112. package/dist/harness-continuation.d.ts.map +1 -0
  113. package/dist/harness-continuation.js +58 -0
  114. package/dist/harness-continuation.js.map +1 -0
  115. package/dist/harness-loop.d.ts +110 -0
  116. package/dist/harness-loop.d.ts.map +1 -0
  117. package/dist/harness-loop.js +575 -0
  118. package/dist/harness-loop.js.map +1 -0
  119. package/dist/hook-dispatch.d.ts +19 -0
  120. package/dist/hook-dispatch.d.ts.map +1 -0
  121. package/dist/hook-dispatch.js +37 -0
  122. package/dist/hook-dispatch.js.map +1 -0
  123. package/dist/http.d.ts +26 -0
  124. package/dist/http.d.ts.map +1 -0
  125. package/dist/http.js +114 -0
  126. package/dist/http.js.map +1 -0
  127. package/dist/in-memory-conversation-turn-store.d.ts +29 -0
  128. package/dist/in-memory-conversation-turn-store.d.ts.map +1 -0
  129. package/dist/in-memory-conversation-turn-store.js +166 -0
  130. package/dist/in-memory-conversation-turn-store.js.map +1 -0
  131. package/dist/in-memory-run-store.d.ts +39 -0
  132. package/dist/in-memory-run-store.d.ts.map +1 -0
  133. package/dist/in-memory-run-store.js +332 -0
  134. package/dist/in-memory-run-store.js.map +1 -0
  135. package/dist/in-memory-sandbox-session-store.d.ts +33 -0
  136. package/dist/in-memory-sandbox-session-store.d.ts.map +1 -0
  137. package/dist/in-memory-sandbox-session-store.js +186 -0
  138. package/dist/in-memory-sandbox-session-store.js.map +1 -0
  139. package/dist/in-memory-small-stores.d.ts +117 -0
  140. package/dist/in-memory-small-stores.d.ts.map +1 -0
  141. package/dist/in-memory-small-stores.js +340 -0
  142. package/dist/in-memory-small-stores.js.map +1 -0
  143. package/dist/in-memory-usage-store.d.ts +15 -0
  144. package/dist/in-memory-usage-store.d.ts.map +1 -0
  145. package/dist/in-memory-usage-store.js +49 -0
  146. package/dist/in-memory-usage-store.js.map +1 -0
  147. package/dist/index.d.ts +467 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +2046 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/json-util.d.ts +19 -0
  152. package/dist/json-util.d.ts.map +1 -0
  153. package/dist/json-util.js +59 -0
  154. package/dist/json-util.js.map +1 -0
  155. package/dist/local-sandbox.d.ts +16 -0
  156. package/dist/local-sandbox.d.ts.map +1 -0
  157. package/dist/local-sandbox.js +327 -0
  158. package/dist/local-sandbox.js.map +1 -0
  159. package/dist/logging.d.ts +16 -0
  160. package/dist/logging.d.ts.map +1 -0
  161. package/dist/logging.js +41 -0
  162. package/dist/logging.js.map +1 -0
  163. package/dist/mcp-client.d.ts +27 -0
  164. package/dist/mcp-client.d.ts.map +1 -0
  165. package/dist/mcp-client.js +169 -0
  166. package/dist/mcp-client.js.map +1 -0
  167. package/dist/model-harnesses.d.ts +4 -0
  168. package/dist/model-harnesses.d.ts.map +1 -0
  169. package/dist/model-harnesses.js +6 -0
  170. package/dist/model-harnesses.js.map +1 -0
  171. package/dist/model-media.d.ts +36 -0
  172. package/dist/model-media.d.ts.map +1 -0
  173. package/dist/model-media.js +234 -0
  174. package/dist/model-media.js.map +1 -0
  175. package/dist/operator-controls.d.ts +31 -0
  176. package/dist/operator-controls.d.ts.map +1 -0
  177. package/dist/operator-controls.js +71 -0
  178. package/dist/operator-controls.js.map +1 -0
  179. package/dist/pi-agent-harness.d.ts +40 -0
  180. package/dist/pi-agent-harness.d.ts.map +1 -0
  181. package/dist/pi-agent-harness.js +222 -0
  182. package/dist/pi-agent-harness.js.map +1 -0
  183. package/dist/pi-harness.d.ts +42 -0
  184. package/dist/pi-harness.d.ts.map +1 -0
  185. package/dist/pi-harness.js +142 -0
  186. package/dist/pi-harness.js.map +1 -0
  187. package/dist/relay-mcp-client.d.ts +17 -0
  188. package/dist/relay-mcp-client.d.ts.map +1 -0
  189. package/dist/relay-mcp-client.js +119 -0
  190. package/dist/relay-mcp-client.js.map +1 -0
  191. package/dist/replay-resume.d.ts +107 -0
  192. package/dist/replay-resume.d.ts.map +1 -0
  193. package/dist/replay-resume.js +495 -0
  194. package/dist/replay-resume.js.map +1 -0
  195. package/dist/resource-projection-util.d.ts +58 -0
  196. package/dist/resource-projection-util.d.ts.map +1 -0
  197. package/dist/resource-projection-util.js +298 -0
  198. package/dist/resource-projection-util.js.map +1 -0
  199. package/dist/resource-projection.d.ts +45 -0
  200. package/dist/resource-projection.d.ts.map +1 -0
  201. package/dist/resource-projection.js +481 -0
  202. package/dist/resource-projection.js.map +1 -0
  203. package/dist/run-heartbeat.d.ts +34 -0
  204. package/dist/run-heartbeat.d.ts.map +1 -0
  205. package/dist/run-heartbeat.js +62 -0
  206. package/dist/run-heartbeat.js.map +1 -0
  207. package/dist/run-outcome.d.ts +47 -0
  208. package/dist/run-outcome.d.ts.map +1 -0
  209. package/dist/run-outcome.js +92 -0
  210. package/dist/run-outcome.js.map +1 -0
  211. package/dist/run-recovery.d.ts +117 -0
  212. package/dist/run-recovery.d.ts.map +1 -0
  213. package/dist/run-recovery.js +466 -0
  214. package/dist/run-recovery.js.map +1 -0
  215. package/dist/run-resolver.d.ts +35 -0
  216. package/dist/run-resolver.d.ts.map +1 -0
  217. package/dist/run-resolver.js +84 -0
  218. package/dist/run-resolver.js.map +1 -0
  219. package/dist/run-stream.d.ts +37 -0
  220. package/dist/run-stream.d.ts.map +1 -0
  221. package/dist/run-stream.js +78 -0
  222. package/dist/run-stream.js.map +1 -0
  223. package/dist/runs-query.d.ts +29 -0
  224. package/dist/runs-query.d.ts.map +1 -0
  225. package/dist/runs-query.js +142 -0
  226. package/dist/runs-query.js.map +1 -0
  227. package/dist/runtime-bundle.d.ts +3 -0
  228. package/dist/runtime-bundle.d.ts.map +1 -0
  229. package/dist/runtime-bundle.js +18 -0
  230. package/dist/runtime-bundle.js.map +1 -0
  231. package/dist/runtime-types.d.ts +397 -0
  232. package/dist/runtime-types.d.ts.map +1 -0
  233. package/dist/runtime-types.js +2 -0
  234. package/dist/runtime-types.js.map +1 -0
  235. package/dist/runtime-util.d.ts +45 -0
  236. package/dist/runtime-util.d.ts.map +1 -0
  237. package/dist/runtime-util.js +116 -0
  238. package/dist/runtime-util.js.map +1 -0
  239. package/dist/sandbox-cli-client.d.ts +20 -0
  240. package/dist/sandbox-cli-client.d.ts.map +1 -0
  241. package/dist/sandbox-cli-client.js +97 -0
  242. package/dist/sandbox-cli-client.js.map +1 -0
  243. package/dist/sandbox-fs.d.ts +56 -0
  244. package/dist/sandbox-fs.d.ts.map +1 -0
  245. package/dist/sandbox-fs.js +276 -0
  246. package/dist/sandbox-fs.js.map +1 -0
  247. package/dist/sandbox-session-key.d.ts +8 -0
  248. package/dist/sandbox-session-key.d.ts.map +1 -0
  249. package/dist/sandbox-session-key.js +32 -0
  250. package/dist/sandbox-session-key.js.map +1 -0
  251. package/dist/sandbox-snapshot-policy.d.ts +27 -0
  252. package/dist/sandbox-snapshot-policy.d.ts.map +1 -0
  253. package/dist/sandbox-snapshot-policy.js +94 -0
  254. package/dist/sandbox-snapshot-policy.js.map +1 -0
  255. package/dist/sandbox-sync-worker.d.ts +63 -0
  256. package/dist/sandbox-sync-worker.d.ts.map +1 -0
  257. package/dist/sandbox-sync-worker.js +448 -0
  258. package/dist/sandbox-sync-worker.js.map +1 -0
  259. package/dist/schedule-execution.d.ts +59 -0
  260. package/dist/schedule-execution.d.ts.map +1 -0
  261. package/dist/schedule-execution.js +951 -0
  262. package/dist/schedule-execution.js.map +1 -0
  263. package/dist/scheduler.d.ts +19 -0
  264. package/dist/scheduler.d.ts.map +1 -0
  265. package/dist/scheduler.js +38 -0
  266. package/dist/scheduler.js.map +1 -0
  267. package/dist/security.d.ts +20 -0
  268. package/dist/security.d.ts.map +1 -0
  269. package/dist/security.js +145 -0
  270. package/dist/security.js.map +1 -0
  271. package/dist/self-improvement.d.ts +245 -0
  272. package/dist/self-improvement.d.ts.map +1 -0
  273. package/dist/self-improvement.js +554 -0
  274. package/dist/self-improvement.js.map +1 -0
  275. package/dist/source-loader.d.ts +19 -0
  276. package/dist/source-loader.d.ts.map +1 -0
  277. package/dist/source-loader.js +160 -0
  278. package/dist/source-loader.js.map +1 -0
  279. package/dist/state-store-helpers.d.ts +28 -0
  280. package/dist/state-store-helpers.d.ts.map +1 -0
  281. package/dist/state-store-helpers.js +164 -0
  282. package/dist/state-store-helpers.js.map +1 -0
  283. package/dist/state-stores.d.ts +25 -0
  284. package/dist/state-stores.d.ts.map +1 -0
  285. package/dist/state-stores.js +157 -0
  286. package/dist/state-stores.js.map +1 -0
  287. package/dist/state.d.ts +736 -0
  288. package/dist/state.d.ts.map +1 -0
  289. package/dist/state.js +46 -0
  290. package/dist/state.js.map +1 -0
  291. package/dist/storage.d.ts +188 -0
  292. package/dist/storage.d.ts.map +1 -0
  293. package/dist/storage.js +752 -0
  294. package/dist/storage.js.map +1 -0
  295. package/dist/subagent-execution.d.ts +80 -0
  296. package/dist/subagent-execution.d.ts.map +1 -0
  297. package/dist/subagent-execution.js +135 -0
  298. package/dist/subagent-execution.js.map +1 -0
  299. package/dist/telemetry.d.ts +49 -0
  300. package/dist/telemetry.d.ts.map +1 -0
  301. package/dist/telemetry.js +60 -0
  302. package/dist/telemetry.js.map +1 -0
  303. package/dist/tool-apis.d.ts +22 -0
  304. package/dist/tool-apis.d.ts.map +1 -0
  305. package/dist/tool-apis.js +344 -0
  306. package/dist/tool-apis.js.map +1 -0
  307. package/dist/tool-execution.d.ts +143 -0
  308. package/dist/tool-execution.d.ts.map +1 -0
  309. package/dist/tool-execution.js +573 -0
  310. package/dist/tool-execution.js.map +1 -0
  311. package/dist/trusted-attachments.d.ts +20 -0
  312. package/dist/trusted-attachments.d.ts.map +1 -0
  313. package/dist/trusted-attachments.js +276 -0
  314. package/dist/trusted-attachments.js.map +1 -0
  315. package/dist/usage-accounting.d.ts +37 -0
  316. package/dist/usage-accounting.d.ts.map +1 -0
  317. package/dist/usage-accounting.js +181 -0
  318. package/dist/usage-accounting.js.map +1 -0
  319. package/dist/usage-budget.d.ts +29 -0
  320. package/dist/usage-budget.d.ts.map +1 -0
  321. package/dist/usage-budget.js +145 -0
  322. package/dist/usage-budget.js.map +1 -0
  323. package/dist/usage-reconciliation.d.ts +76 -0
  324. package/dist/usage-reconciliation.d.ts.map +1 -0
  325. package/dist/usage-reconciliation.js +463 -0
  326. package/dist/usage-reconciliation.js.map +1 -0
  327. package/dist/usage-records.d.ts +24 -0
  328. package/dist/usage-records.d.ts.map +1 -0
  329. package/dist/usage-records.js +445 -0
  330. package/dist/usage-records.js.map +1 -0
  331. package/package.json +51 -0
@@ -0,0 +1,838 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, isConnectionAuthorizationFailedError, isConnectionAuthorizationRequiredError } from "@assemblyline-agents/core";
3
+ import { OAUTH_STANDARD_TOKEN_FIELDS, buildOAuthChallenge, connectionPrincipalFor, connectionPrincipalKey, grantToTokenResult, isTokenExpired, normalizeAuthorization, randomUrlToken, resolveHeadersDefinition, storedGrantExpired, tokenCacheKey } from "./connection-auth-util.js";
4
+ import { HttpApiConnectionClient, McpConnectionClient, OpenApiConnectionClient, RelayMcpConnectionClient, readResponseBody } from "./connection-clients.js";
5
+ import { SandboxCliConnectionClient } from "./sandbox-cli-client.js";
6
+ import { InMemoryConnectionAuthorizationSessionStore, InMemoryConnectionGrantStore } from "./connection-stores.js";
7
+ import { connectionToolAccess, isLiveConnection, isRecord, liveDefinitionUrl, parseQualifiedName, scoreMatch, stringValue, timestamp, tokenize } from "./connection-util.js";
8
+ export * from "./connection-auth-util.js";
9
+ export * from "./connection-clients.js";
10
+ export * from "./connection-stores.js";
11
+ export * from "./connection-util.js";
12
+ export * from "./computer-relay-protocol.js";
13
+ // Compose the channel-visible response for a run parked on connection
14
+ // authorization. The generic "authorization required" string is useless to the
15
+ // human — the challenge URL is the thing they need to click.
16
+ export async function connectionAuthorizationPrompt(store, authorizationSessionId, baseResponse) {
17
+ const session = authorizationSessionId ? await store.getSession(authorizationSessionId) : undefined;
18
+ const url = session && typeof session.challenge.url === "string" ? session.challenge.url : undefined;
19
+ if (!url)
20
+ return baseResponse || "Connection authorization required.";
21
+ const prompt = `Authorization required for ${session?.connectionName ?? "a connection"} — approve here: ${url}`;
22
+ return baseResponse ? `${baseResponse}\n\n${prompt}` : prompt;
23
+ }
24
+ /**
25
+ * Project declared connection credentials into a freshly acquired sandbox.
26
+ * Tokens resolve host-side (cached grant or refresh exchange — the grant
27
+ * store is updated before anything reaches the sandbox), so recycling the
28
+ * sandbox never loses a durable credential and never holds one. Subagent
29
+ * sandboxes are excluded: they receive only their engine harness credential,
30
+ * never the parent's connections. The emitted event carries paths only —
31
+ * credential contents never reach the event log.
32
+ */
33
+ export async function materializeConnectionCredentialsIntoSandbox(run, registry, sandbox, appendEvent) {
34
+ if (run.input.channel === "subagent" || typeof run.input.parentRunId === "string")
35
+ return;
36
+ const { materialized, pending } = await registry.materializeCredentials();
37
+ if (materialized.length === 0 && pending.length === 0)
38
+ return;
39
+ for (const credential of materialized) {
40
+ for (const file of credential.files) {
41
+ await sandbox.writeFile(file.path, file.content);
42
+ await sandbox.shell(`chmod ${file.mode.toString(8)} '${file.path.replaceAll("'", String.raw `'\''`)}'`);
43
+ }
44
+ }
45
+ await appendEvent("connections.credentials_materialized", {
46
+ materialized: materialized.map((credential) => ({
47
+ connection: credential.connection,
48
+ files: credential.files.map((file) => file.path)
49
+ })),
50
+ pending
51
+ });
52
+ }
53
+ export const CONNECTION_PAIR_TOOL_NAME = "pair";
54
+ // The synthetic host-side pairing tool advertised for connections that define
55
+ // redeemPairingCode. The input field is named `secret` so tool-call event
56
+ // logging redacts the pasted one-time code automatically.
57
+ function pairingToolItem(connection) {
58
+ return {
59
+ connection: connection.name,
60
+ qualifiedName: `${connection.name}__${CONNECTION_PAIR_TOOL_NAME}`,
61
+ tool: CONNECTION_PAIR_TOOL_NAME,
62
+ description: `Pair ${connection.name} by redeeming a one-time pairing packet or claim code. When the user pastes binding/pairing instructions for ${connection.name}, call this with the FULL pasted text as \`secret\`; the host exchanges it and stores the credential durably. Works while the connection is not yet authorized.`,
63
+ inputSchema: {
64
+ type: "object",
65
+ additionalProperties: false,
66
+ properties: {
67
+ secret: {
68
+ type: "string",
69
+ description: "The full pasted pairing packet, binding instructions, or one-time claim code."
70
+ }
71
+ },
72
+ required: ["secret"]
73
+ }
74
+ };
75
+ }
76
+ const CREDENTIAL_TEMPLATE_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/gu;
77
+ function templateUsesToken(template) {
78
+ return [...template.matchAll(CREDENTIAL_TEMPLATE_PATTERN)].some((match) => match[1] === "token");
79
+ }
80
+ // An unrendered placeholder in a credentials file is a silent misconfiguration
81
+ // (the CLI reads garbage), so unknown keys fail loudly instead of passing through.
82
+ function renderCredentialTemplate(template, values, connectionName) {
83
+ return template.replace(CREDENTIAL_TEMPLATE_PATTERN, (_match, key) => {
84
+ const value = values[key];
85
+ if (value === undefined) {
86
+ throw new Error(`Connection ${connectionName} materialize template references unknown placeholder {{${key}}}.`);
87
+ }
88
+ return value;
89
+ });
90
+ }
91
+ export class RuntimeConnectionRegistry {
92
+ options;
93
+ resolved = new Map();
94
+ clients = new Map();
95
+ tokenCache = new Map();
96
+ executionContext;
97
+ grantStore;
98
+ authorizationSessionStore;
99
+ constructor(options) {
100
+ this.options = options;
101
+ this.grantStore = options.grantStore ?? new InMemoryConnectionGrantStore();
102
+ this.authorizationSessionStore = options.authorizationSessionStore ?? new InMemoryConnectionAuthorizationSessionStore();
103
+ }
104
+ hasLiveConnections() {
105
+ return this.options.connections.some(isLiveConnection);
106
+ }
107
+ liveConnectionNames() {
108
+ return this.options.connections
109
+ .filter(isLiveConnection)
110
+ .map((connection) => connection.name);
111
+ }
112
+ async search(input, execution) {
113
+ return this.withExecutionContext(execution, () => this.searchWithContext(input));
114
+ }
115
+ async searchWithContext(input) {
116
+ const keywords = input.keywords ?? "";
117
+ const limit = input.limit ?? 10;
118
+ const target = this.targetConnections(input.connection);
119
+ const queryTokens = tokenize(keywords);
120
+ const scored = [];
121
+ const failures = [];
122
+ // Pairing tools surface even when the connection cannot authenticate yet —
123
+ // redeeming a pasted packet is exactly how an unauthorized connection
124
+ // becomes authorized, so it must not hide behind the failure path.
125
+ const pairable = [];
126
+ for (const connection of target) {
127
+ if (await this.supportsPairing(connection))
128
+ pairable.push(pairingToolItem(connection));
129
+ let metadata;
130
+ try {
131
+ metadata = await this.getClient(connection.name).getToolMetadata();
132
+ }
133
+ catch (error) {
134
+ failures.push(this.connectionFailure(connection, error));
135
+ continue;
136
+ }
137
+ for (const tool of metadata) {
138
+ const score = keywords.trim() ? scoreMatch(queryTokens, tool) : 1;
139
+ if (score <= 0)
140
+ continue;
141
+ const item = {
142
+ connection: tool.connection,
143
+ description: tool.description,
144
+ inputSchema: tool.inputSchema,
145
+ qualifiedName: tool.qualifiedName,
146
+ tool: tool.name
147
+ };
148
+ if (tool.outputSchema)
149
+ item.outputSchema = tool.outputSchema;
150
+ scored.push({
151
+ score,
152
+ item
153
+ });
154
+ }
155
+ }
156
+ scored.sort((a, b) => b.score - a.score);
157
+ const matches = scored.slice(0, limit).map((entry) => entry.item);
158
+ if (matches.length > 0)
159
+ return [...pairable, ...matches, ...failures];
160
+ return [
161
+ ...pairable,
162
+ ...target.map((connection) => ({
163
+ connection: connection.name,
164
+ description: connection.description ?? `${connection.name} connection`
165
+ })),
166
+ ...failures
167
+ ];
168
+ }
169
+ async supportsPairing(connection) {
170
+ try {
171
+ return typeof (await this.resolveConnection(connection)).definition.redeemPairingCode === "function";
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }
177
+ /**
178
+ * Redeem a user-provided one-time pairing code host-side and persist the
179
+ * resulting credentials in the grant store. The durable credential never
180
+ * enters a sandbox; the model only relays the single-use code.
181
+ */
182
+ async redeemPairingCode(connectionName, code) {
183
+ const compiled = this.options.connections.find((connection) => connection.name === connectionName);
184
+ if (!compiled || !isLiveConnection(compiled)) {
185
+ throw new Error(`Connection ${connectionName} is not registered as a live connection.`);
186
+ }
187
+ const definition = (await this.resolveConnection(compiled)).definition;
188
+ const redeem = definition.redeemPairingCode;
189
+ if (!redeem)
190
+ throw new Error(`Connection ${connectionName} does not support pairing codes.`);
191
+ if (!code.trim())
192
+ throw new Error("A pairing code is required.");
193
+ const context = this.resolverContext();
194
+ const token = await redeem({ code }, context);
195
+ const authorization = definition.auth
196
+ ? await normalizeAuthorization(definition.auth, context)
197
+ : { kind: "token", principalType: "user", getToken: async () => token };
198
+ const principal = connectionPrincipalFor(compiled, authorization, context);
199
+ const principalKey = connectionPrincipalKey(principal);
200
+ await this.saveGrantFromToken(compiled.name, principal, principalKey, token);
201
+ this.tokenCache.delete(tokenCacheKey(compiled.name, principal));
202
+ return {
203
+ connected: true,
204
+ connection: compiled.name,
205
+ message: `${compiled.name} is paired. The credential is stored durably on the host; no further setup is needed.`
206
+ };
207
+ }
208
+ async metadataForQualifiedName(qualifiedName) {
209
+ const parsed = parseQualifiedName(qualifiedName);
210
+ if (!parsed)
211
+ return undefined;
212
+ const metadata = await this.getClient(parsed.connectionName).getToolMetadata();
213
+ return metadata.find((tool) => tool.name === parsed.toolName);
214
+ }
215
+ async executeQualifiedTool(qualifiedName, input, execution) {
216
+ const parsed = parseQualifiedName(qualifiedName);
217
+ if (!parsed)
218
+ throw new Error(`Invalid connection tool name: ${qualifiedName}`);
219
+ // "<connection>__pair" is the synthetic host-side pairing tool; it must
220
+ // run before any client/auth resolution (the whole point is that the
221
+ // connection is not authorized yet).
222
+ if (parsed.toolName === CONNECTION_PAIR_TOOL_NAME) {
223
+ const compiled = this.options.connections.find((connection) => connection.name === parsed.connectionName);
224
+ if (compiled && await this.supportsPairing(compiled)) {
225
+ const secret = isRecord(input) && typeof input.secret === "string" ? input.secret : "";
226
+ return this.withExecutionContext(execution, () => this.redeemPairingCode(parsed.connectionName, secret));
227
+ }
228
+ }
229
+ return this.withExecutionContext(execution, () => this.getClient(parsed.connectionName).executeTool(parsed.toolName, input));
230
+ }
231
+ async getConnectionApproval(connectionName, toolName) {
232
+ const compiled = this.options.connections.find((connection) => connection.name === connectionName);
233
+ if (!compiled || !isLiveConnection(compiled))
234
+ return undefined;
235
+ const resolved = await this.resolveConnection(compiled);
236
+ const access = connectionToolAccess(toolName, resolved.definition.access);
237
+ if (access === "none")
238
+ throw new Error(`Tool ${toolName} is not allowed by connection ${connectionName}.`);
239
+ if (access === "write" && resolved.definition.access.write !== false) {
240
+ return resolved.definition.access.write.approval;
241
+ }
242
+ return resolved.definition.approval;
243
+ }
244
+ getClient(connectionName) {
245
+ const existing = this.clients.get(connectionName);
246
+ if (existing)
247
+ return existing;
248
+ const compiled = this.options.connections.find((connection) => connection.name === connectionName);
249
+ if (!compiled || !isLiveConnection(compiled)) {
250
+ throw new Error(`Connection ${connectionName} is not registered as a live connection.`);
251
+ }
252
+ const resolved = () => this.resolveConnection(compiled);
253
+ const client = compiled.protocol === "cli"
254
+ ? new SandboxCliConnectionClient(compiled, resolved, this)
255
+ : compiled.protocol === "mcp"
256
+ ? compiled.transport === "relay"
257
+ ? new RelayMcpConnectionClient(compiled, resolved, this)
258
+ : new McpConnectionClient(compiled, resolved, this)
259
+ : compiled.protocol === "openapi"
260
+ ? new OpenApiConnectionClient(compiled, resolved, this)
261
+ : new HttpApiConnectionClient(compiled, resolved, this);
262
+ this.clients.set(connectionName, client);
263
+ return client;
264
+ }
265
+ async executeSandboxCli(invocation) {
266
+ if (!this.options.executeSandboxCli) {
267
+ throw new Error("Sandbox CLI connection execution requires an active agent run sandbox.");
268
+ }
269
+ return this.options.executeSandboxCli(invocation, this.executionContext);
270
+ }
271
+ async resolveHeaders(connection, definition) {
272
+ const headers = {};
273
+ const context = this.resolverContext();
274
+ if (definition.auth !== undefined) {
275
+ const token = await this.resolveToken(connection, definition.auth, context, liveDefinitionUrl(definition));
276
+ const tokenType = token.tokenType?.trim() || "Bearer";
277
+ headers.Authorization = `${tokenType} ${token.token}`;
278
+ }
279
+ if (definition.headers !== undefined) {
280
+ const resolved = await resolveHeadersDefinition(definition.headers, context);
281
+ for (const [key, value] of Object.entries(resolved)) {
282
+ if (key.toLowerCase() === "authorization" && headers.Authorization !== undefined) {
283
+ throw new Error(`Connection ${connection.name} headers must not include Authorization when auth is configured.`);
284
+ }
285
+ headers[key] = value;
286
+ }
287
+ }
288
+ return headers;
289
+ }
290
+ /**
291
+ * Start (or resume) the authorization flow for a named connection and
292
+ * report where to send the user's browser. Backs the host's authenticated
293
+ * GET /openeve/connections/authorize endpoint (agent-control route class,
294
+ * JSON — deliberately not a public 302, so unauthenticated callers cannot
295
+ * trigger consent flows or enumerate connection names): the dashboard
296
+ * fetches the URL and opens it; the provider redirects back to the public,
297
+ * idempotency-claimed callback. Hitting it when a grant already exists
298
+ * reports "connected" instead of minting a redundant session; a pending
299
+ * session's challenge is reused.
300
+ */
301
+ async beginAuthorization(connectionName) {
302
+ const compiled = this.options.connections.find((connection) => connection.name === connectionName);
303
+ if (!compiled || !isLiveConnection(compiled))
304
+ return { status: "unavailable", reason: "unknown_connection" };
305
+ let definition;
306
+ try {
307
+ definition = (await this.resolveConnection(compiled)).definition;
308
+ }
309
+ catch (error) {
310
+ return { status: "unavailable", reason: error instanceof Error ? error.message : String(error) };
311
+ }
312
+ if (!definition.auth)
313
+ return { status: "unavailable", reason: "connection_has_no_auth" };
314
+ try {
315
+ await this.resolveToken(compiled, definition.auth, this.resolverContext(), liveDefinitionUrl(definition));
316
+ return { status: "connected" };
317
+ }
318
+ catch (error) {
319
+ if (isConnectionAuthorizationRequiredError(error)) {
320
+ const url = typeof error.challenge?.url === "string" ? error.challenge.url : undefined;
321
+ return url ? { status: "authorize", url } : { status: "unavailable", reason: error.reason };
322
+ }
323
+ return { status: "unavailable", reason: error instanceof Error ? error.message : String(error) };
324
+ }
325
+ }
326
+ /**
327
+ * Resolve every connection that declares `materialize` and render its
328
+ * credential files for injection into the agent sandbox. Tokens resolve
329
+ * through the normal host-side flow — cached grant, or a refresh exchange
330
+ * that persists the rotated grant to the grant store BEFORE anything is
331
+ * rendered — so the sandbox only ever sees the short-lived access token.
332
+ *
333
+ * A connection that cannot resolve (missing grant, pending authorization,
334
+ * failed refresh) is reported as pending, never thrown: missing credentials
335
+ * are a recoverable connection-required state, not a boot crash.
336
+ */
337
+ async materializeCredentials() {
338
+ const materialized = [];
339
+ const pending = [];
340
+ const context = this.resolverContext();
341
+ for (const compiled of this.options.connections) {
342
+ if (!isLiveConnection(compiled))
343
+ continue;
344
+ let definition;
345
+ try {
346
+ definition = (await this.resolveConnection(compiled)).definition;
347
+ }
348
+ catch (error) {
349
+ pending.push({ connection: compiled.name, reason: error instanceof Error ? error.message : String(error) });
350
+ continue;
351
+ }
352
+ const spec = definition.materialize;
353
+ if (!spec || spec.files.length === 0)
354
+ continue;
355
+ const needsToken = spec.files.some((file) => templateUsesToken(file.path) || templateUsesToken(file.content));
356
+ let resolvedToken = { token: "" };
357
+ if (needsToken) {
358
+ if (!definition.auth) {
359
+ pending.push({ connection: compiled.name, reason: "materialize templates use {{token}} but the connection has no auth" });
360
+ continue;
361
+ }
362
+ try {
363
+ resolvedToken = await this.resolveToken(compiled, definition.auth, context, liveDefinitionUrl(definition));
364
+ }
365
+ catch (error) {
366
+ if (isConnectionAuthorizationRequiredError(error)) {
367
+ const authorizeUrl = typeof error.challenge?.url === "string" ? error.challenge.url : undefined;
368
+ pending.push({ connection: compiled.name, reason: `authorization_required: ${error.reason}`, ...(authorizeUrl ? { authorizeUrl } : {}) });
369
+ continue;
370
+ }
371
+ const reason = isConnectionAuthorizationFailedError(error)
372
+ ? `${error.reason}: ${error.message}`
373
+ : error instanceof Error
374
+ ? error.message
375
+ : String(error);
376
+ pending.push({ connection: compiled.name, reason });
377
+ continue;
378
+ }
379
+ }
380
+ const values = { token: resolvedToken.token };
381
+ // Extra token-response fields (captured into the grant's credential bag)
382
+ // are addressable as {{credential.<key>}} — e.g. a provider-issued user
383
+ // or account id a sandbox-side CLI needs to build API paths.
384
+ for (const [key, value] of Object.entries(resolvedToken.credential ?? {})) {
385
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
386
+ values[`credential.${key}`] = String(value);
387
+ }
388
+ }
389
+ try {
390
+ materialized.push({
391
+ connection: compiled.name,
392
+ files: spec.files.map((file) => ({
393
+ path: renderCredentialTemplate(file.path, values, compiled.name),
394
+ content: renderCredentialTemplate(file.content, values, compiled.name),
395
+ mode: file.mode ?? 0o600
396
+ }))
397
+ });
398
+ }
399
+ catch (error) {
400
+ pending.push({ connection: compiled.name, reason: error instanceof Error ? error.message : String(error) });
401
+ }
402
+ }
403
+ return { materialized, pending };
404
+ }
405
+ async evictToken(connection, definition) {
406
+ if (!definition.auth)
407
+ return;
408
+ const context = this.resolverContext();
409
+ const authorization = await normalizeAuthorization(definition.auth, context);
410
+ const principal = connectionPrincipalFor(connection, authorization, context);
411
+ const cacheKey = tokenCacheKey(connection.name, principal);
412
+ const principalKey = connectionPrincipalKey(principal);
413
+ const cached = this.tokenCache.get(cacheKey);
414
+ this.tokenCache.delete(cacheKey);
415
+ if (cached?.authorization.kind === "oauth" || authorization.kind === "oauth") {
416
+ await this.grantStore.deleteGrant(connection.name, principalKey);
417
+ }
418
+ await (cached?.authorization ?? authorization).evict?.({ principal, connection: { url: connection.url ?? "" } });
419
+ }
420
+ async completeAuthorization(callback) {
421
+ const state = callback.params.state;
422
+ if (!state)
423
+ throw new ConnectionAuthorizationFailedError("unknown", { reason: "missing_state", retryable: false });
424
+ const session = await this.authorizationSessionStore.getSessionByState(state);
425
+ if (!session)
426
+ throw new ConnectionAuthorizationFailedError("unknown", { reason: "unknown_state", retryable: false });
427
+ if (session.expiresAt <= Date.now()) {
428
+ await this.authorizationSessionStore.updateSession(session.id, { status: "expired" });
429
+ throw new ConnectionAuthorizationFailedError(session.connectionName, { reason: "authorization_expired", retryable: true });
430
+ }
431
+ if (callback.params.error) {
432
+ await this.authorizationSessionStore.updateSession(session.id, { status: "denied" });
433
+ throw new ConnectionAuthorizationFailedError(session.connectionName, {
434
+ reason: callback.params.error,
435
+ retryable: true,
436
+ message: callback.params.error_description ?? `Connection ${session.connectionName} authorization was denied.`
437
+ });
438
+ }
439
+ const compiled = this.options.connections.find((connection) => connection.name === session.connectionName);
440
+ if (!compiled || !isLiveConnection(compiled)) {
441
+ throw new ConnectionAuthorizationFailedError(session.connectionName, { reason: "connection_not_found", retryable: false });
442
+ }
443
+ const resolved = await this.resolveConnection(compiled);
444
+ if (!resolved.definition.auth) {
445
+ throw new ConnectionAuthorizationFailedError(session.connectionName, { reason: "auth_not_configured", retryable: false });
446
+ }
447
+ const authorization = await normalizeAuthorization(resolved.definition.auth, { env: this.options.env, session: session.session });
448
+ const connectionContext = { url: compiled.url ?? "" };
449
+ let token;
450
+ if (authorization.kind === "oauth" && authorization.oauth) {
451
+ token = await this.exchangeOAuthAuthorizationCode(authorization.oauth, session, callback);
452
+ }
453
+ else if (authorization.completeAuthorization) {
454
+ const completeOptions = {
455
+ principal: session.principal,
456
+ connection: connectionContext,
457
+ callbackUrl: session.callbackUrl,
458
+ callback
459
+ };
460
+ if (session.resume !== undefined)
461
+ completeOptions.resume = session.resume;
462
+ token = await authorization.completeAuthorization(completeOptions);
463
+ }
464
+ else {
465
+ throw new ConnectionAuthorizationFailedError(session.connectionName, { reason: "callback_not_supported", retryable: false });
466
+ }
467
+ await this.saveGrantFromToken(compiled.name, session.principal, session.principalKey, token);
468
+ return this.authorizationSessionStore.updateSession(session.id, { status: "completed" });
469
+ }
470
+ async getAuthorizationSession(id) {
471
+ return this.authorizationSessionStore.getSession(id);
472
+ }
473
+ async updateAuthorizationSession(id, patch) {
474
+ return this.authorizationSessionStore.updateSession(id, patch);
475
+ }
476
+ async close() {
477
+ await Promise.allSettled([...this.clients.values()].map((client) => client.close()));
478
+ this.clients.clear();
479
+ }
480
+ request(input, init) {
481
+ return this.options.fetch(input, init);
482
+ }
483
+ environmentValue(name) {
484
+ return this.options.env[name];
485
+ }
486
+ async resolveConnection(compiled) {
487
+ const existing = this.resolved.get(compiled.name);
488
+ if (existing)
489
+ return existing;
490
+ const promise = this.loadResolvedConnection(compiled);
491
+ this.resolved.set(compiled.name, promise);
492
+ return promise;
493
+ }
494
+ async loadResolvedConnection(compiled) {
495
+ const platform = this.options.platform ?? process.platform;
496
+ if (compiled.hostRequirements?.platforms?.length && !compiled.hostRequirements.platforms.includes(platform)) {
497
+ throw new Error(`${compiled.hostRequirements.message} Current runtime platform: ${platform}.`);
498
+ }
499
+ const raw = await this.options.loadDefinition(compiled);
500
+ if (!isRecord(raw))
501
+ throw new Error(`Connection ${compiled.name} did not export an object.`);
502
+ if (compiled.protocol === "mcp") {
503
+ if (raw.transport === "stdio") {
504
+ if (compiled.source.path.startsWith("dynamic://")) {
505
+ throw new Error(`Dynamic MCP connection ${compiled.name} cannot launch a stdio process.`);
506
+ }
507
+ if (typeof raw.command !== "string" || !raw.command.trim()) {
508
+ throw new Error(`Stdio MCP connection ${compiled.name} must export command.`);
509
+ }
510
+ if (raw.args !== undefined && (!Array.isArray(raw.args) || raw.args.some((argument) => typeof argument !== "string"))) {
511
+ throw new Error(`Stdio MCP connection ${compiled.name} args must contain only strings.`);
512
+ }
513
+ }
514
+ else if (raw.transport === "relay") {
515
+ if (compiled.source.path.startsWith("dynamic://")) {
516
+ throw new Error(`Dynamic MCP connection ${compiled.name} cannot use a device relay.`);
517
+ }
518
+ if (typeof raw.url !== "string" || typeof raw.credentialEnv !== "string") {
519
+ throw new Error(`Relay MCP connection ${compiled.name} must export url and credentialEnv.`);
520
+ }
521
+ }
522
+ else if (typeof raw.url !== "string") {
523
+ throw new Error(`HTTP MCP connection ${compiled.name} must export url.`);
524
+ }
525
+ if (typeof raw.description !== "string")
526
+ throw new Error(`MCP connection ${compiled.name} must export description.`);
527
+ return { compiled, definition: raw };
528
+ }
529
+ if (compiled.protocol === "openapi") {
530
+ if (raw.spec === undefined)
531
+ throw new Error(`OpenAPI connection ${compiled.name} must export spec.`);
532
+ if (typeof raw.description !== "string")
533
+ throw new Error(`OpenAPI connection ${compiled.name} must export description.`);
534
+ return { compiled, definition: raw };
535
+ }
536
+ if (compiled.protocol === "http") {
537
+ if (typeof raw.baseUrl !== "string")
538
+ throw new Error(`HTTP API connection ${compiled.name} must export baseUrl.`);
539
+ if (typeof raw.description !== "string")
540
+ throw new Error(`HTTP API connection ${compiled.name} must export description.`);
541
+ if (!Array.isArray(raw.operations))
542
+ throw new Error(`HTTP API connection ${compiled.name} must export operations.`);
543
+ return { compiled, definition: raw };
544
+ }
545
+ if (compiled.protocol === "cli") {
546
+ if (compiled.source.path.startsWith("dynamic://")) {
547
+ throw new Error(`Dynamic CLI connection ${compiled.name} is not allowed.`);
548
+ }
549
+ if (raw.transport !== "sandbox" || typeof raw.command !== "string" || !raw.command.trim()) {
550
+ throw new Error(`Sandbox CLI connection ${compiled.name} must export transport "sandbox" and command.`);
551
+ }
552
+ if (typeof raw.description !== "string" || !Array.isArray(raw.tools)) {
553
+ throw new Error(`Sandbox CLI connection ${compiled.name} must export description and tools.`);
554
+ }
555
+ return { compiled, definition: raw };
556
+ }
557
+ throw new Error(`Connection ${compiled.name} is declaration-only.`);
558
+ }
559
+ targetConnections(name) {
560
+ return this.options.connections.filter((connection) => {
561
+ if (!isLiveConnection(connection))
562
+ return false;
563
+ return !name || connection.name === name;
564
+ });
565
+ }
566
+ connectionFailure(connection, error) {
567
+ if (isConnectionAuthorizationRequiredError(error)) {
568
+ return {
569
+ connection: connection.name,
570
+ description: connection.description ?? `${connection.name} connection`,
571
+ needsAuthorization: true,
572
+ error: error.reason
573
+ };
574
+ }
575
+ if (isConnectionAuthorizationFailedError(error)) {
576
+ return {
577
+ connection: connection.name,
578
+ description: connection.description ?? `${connection.name} connection`,
579
+ error: `${error.reason}: ${error.message}`
580
+ };
581
+ }
582
+ return {
583
+ connection: connection.name,
584
+ description: connection.description ?? `${connection.name} connection`,
585
+ error: error instanceof Error ? error.message : String(error)
586
+ };
587
+ }
588
+ async resolveToken(connection, auth, context, resolvedUrl) {
589
+ const authorization = await normalizeAuthorization(auth, context);
590
+ const principal = connectionPrincipalFor(connection, authorization, context);
591
+ const principalKey = connectionPrincipalKey(principal);
592
+ const cacheKey = tokenCacheKey(connection.name, principal);
593
+ const cached = this.tokenCache.get(cacheKey);
594
+ if (cached && !isTokenExpired(cached.token))
595
+ return cached.token;
596
+ const connectionContext = { url: resolvedUrl ?? connection.url ?? "" };
597
+ if (authorization.kind === "oauth") {
598
+ const stored = await this.grantStore.getGrant(connection.name, principalKey);
599
+ if (stored && !storedGrantExpired(stored)) {
600
+ const token = grantToTokenResult(stored);
601
+ this.tokenCache.set(cacheKey, { token, authorization, principal, connection: connectionContext });
602
+ return token;
603
+ }
604
+ if (stored?.refreshToken && authorization.oauth) {
605
+ const refreshed = await this.refreshOAuthGrant(authorization.oauth, connection.name, principal, principalKey, stored.refreshToken);
606
+ // saveGrantFromToken preserved the credential bag on the grant; mirror
607
+ // that on the returned token so template rendering sees it too.
608
+ if (!refreshed.credential && stored.credential)
609
+ refreshed.credential = stored.credential;
610
+ this.tokenCache.set(cacheKey, { token: refreshed, authorization, principal, connection: connectionContext });
611
+ return refreshed;
612
+ }
613
+ throw await this.createAuthorizationRequiredError(connection, authorization, principal, principalKey, context, connectionContext);
614
+ }
615
+ try {
616
+ const token = await authorization.getToken({ principal, connection: connectionContext });
617
+ this.tokenCache.set(cacheKey, { token, authorization, principal, connection: connectionContext });
618
+ return token;
619
+ }
620
+ catch (error) {
621
+ if (isConnectionAuthorizationRequiredError(error) && authorization.kind === "interactive") {
622
+ throw await this.createAuthorizationRequiredError(connection, authorization, principal, principalKey, context, connectionContext, error);
623
+ }
624
+ if (isConnectionAuthorizationRequiredError(error) || isConnectionAuthorizationFailedError(error))
625
+ throw error;
626
+ throw error;
627
+ }
628
+ }
629
+ resolverContext() {
630
+ return {
631
+ env: this.options.env,
632
+ session: this.options.session
633
+ };
634
+ }
635
+ async withExecutionContext(execution, fn) {
636
+ const previous = this.executionContext;
637
+ if (execution)
638
+ this.executionContext = execution;
639
+ try {
640
+ return await fn();
641
+ }
642
+ finally {
643
+ this.executionContext = previous;
644
+ }
645
+ }
646
+ async createAuthorizationRequiredError(connection, authorization, principal, principalKey, context, connectionContext, cause) {
647
+ const existing = await this.authorizationSessionStore.findPendingSession(connection.name, principalKey);
648
+ if (existing) {
649
+ const options = {
650
+ reason: cause?.reason ?? "authorization_required",
651
+ challenge: existing.challenge
652
+ };
653
+ if (cause?.message)
654
+ options.message = cause.message;
655
+ return new ConnectionAuthorizationRequiredError(connection.name, {
656
+ ...options
657
+ });
658
+ }
659
+ const callbackUrl = this.callbackUrl();
660
+ const id = randomUUID();
661
+ const state = randomUrlToken();
662
+ const expiresAt = Date.now() + 10 * 60_000;
663
+ let challenge;
664
+ let resume;
665
+ let codeVerifier;
666
+ if (authorization.kind === "oauth" && authorization.oauth) {
667
+ codeVerifier = randomUrlToken(48);
668
+ const oauthChallengeOptions = {
669
+ state,
670
+ callbackUrl,
671
+ codeVerifier
672
+ };
673
+ if (authorization.displayName)
674
+ oauthChallengeOptions.displayName = authorization.displayName;
675
+ challenge = buildOAuthChallenge(authorization.oauth, oauthChallengeOptions);
676
+ }
677
+ else if (authorization.startAuthorization) {
678
+ const started = await authorization.startAuthorization({ principal, connection: connectionContext, callbackUrl });
679
+ challenge = started.challenge;
680
+ resume = started.resume;
681
+ if (!challenge.url && cause?.challenge?.url)
682
+ challenge.url = cause.challenge.url;
683
+ if (!challenge.instructions && cause?.challenge?.instructions)
684
+ challenge.instructions = cause.challenge.instructions;
685
+ }
686
+ else {
687
+ challenge = cause?.challenge ?? { instructions: `Authorize connection ${connection.name} and retry.` };
688
+ if (authorization.displayName)
689
+ challenge.displayName = authorization.displayName;
690
+ }
691
+ challenge.sessionId = id;
692
+ challenge.state = state;
693
+ challenge.expiresAt = expiresAt;
694
+ const session = {
695
+ id,
696
+ state,
697
+ connectionName: connection.name,
698
+ principal,
699
+ principalKey,
700
+ session: context.session,
701
+ callbackUrl,
702
+ challenge,
703
+ status: "pending",
704
+ expiresAt,
705
+ createdAt: timestamp(),
706
+ updatedAt: timestamp()
707
+ };
708
+ if (codeVerifier)
709
+ session.codeVerifier = codeVerifier;
710
+ if (resume !== undefined)
711
+ session.resume = resume;
712
+ if (this.executionContext?.runId)
713
+ session.runId = this.executionContext.runId;
714
+ if (this.executionContext?.toolCallId)
715
+ session.toolCallId = this.executionContext.toolCallId;
716
+ if (this.executionContext?.toolName)
717
+ session.toolName = this.executionContext.toolName;
718
+ await this.authorizationSessionStore.createSession(session);
719
+ const errorOptions = {
720
+ reason: cause?.reason ?? "authorization_required",
721
+ challenge
722
+ };
723
+ if (cause?.message)
724
+ errorOptions.message = cause.message;
725
+ return new ConnectionAuthorizationRequiredError(connection.name, errorOptions);
726
+ }
727
+ callbackUrl() {
728
+ const base = (this.options.callbackBaseUrl ?? this.options.env.OPENEVE_PUBLIC_URL ?? "http://localhost").replace(/\/+$/u, "");
729
+ return `${base}/openeve/connections/callback`;
730
+ }
731
+ async exchangeOAuthAuthorizationCode(oauth, session, callback) {
732
+ const code = callback.params.code;
733
+ if (!code) {
734
+ throw new ConnectionAuthorizationFailedError(session.connectionName, { reason: "missing_code", retryable: true });
735
+ }
736
+ return this.requestOAuthToken(oauth, {
737
+ grant_type: "authorization_code",
738
+ code,
739
+ redirect_uri: oauth.redirectUri ?? session.callbackUrl,
740
+ code_verifier: session.codeVerifier
741
+ });
742
+ }
743
+ async refreshOAuthGrant(oauth, connectionName, principal, principalKey, refreshToken) {
744
+ const token = await this.requestOAuthToken(oauth, {
745
+ grant_type: "refresh_token",
746
+ refresh_token: refreshToken
747
+ });
748
+ if (!token.refreshToken)
749
+ token.refreshToken = refreshToken;
750
+ await this.saveGrantFromToken(connectionName, principal, principalKey, token);
751
+ return token;
752
+ }
753
+ async requestOAuthToken(oauth, fields) {
754
+ const body = new URLSearchParams();
755
+ body.set("client_id", oauth.clientId);
756
+ if (oauth.clientSecret)
757
+ body.set("client_secret", oauth.clientSecret);
758
+ for (const [key, value] of Object.entries(oauth.tokenParams ?? {}))
759
+ body.set(key, value);
760
+ for (const [key, value] of Object.entries(fields)) {
761
+ if (value !== undefined)
762
+ body.set(key, value);
763
+ }
764
+ const response = await this.request(oauth.tokenUrl, {
765
+ method: "POST",
766
+ headers: {
767
+ accept: "application/json",
768
+ "content-type": "application/x-www-form-urlencoded"
769
+ },
770
+ body
771
+ });
772
+ const payload = await readResponseBody(response);
773
+ if (!response.ok || !isRecord(payload)) {
774
+ throw new ConnectionAuthorizationFailedError("oauth", {
775
+ reason: "token_exchange_failed",
776
+ message: `OAuth token exchange failed with HTTP ${response.status}.`
777
+ });
778
+ }
779
+ const accessToken = stringValue(payload.access_token);
780
+ if (!accessToken) {
781
+ throw new ConnectionAuthorizationFailedError("oauth", { reason: "missing_access_token" });
782
+ }
783
+ const token = { token: accessToken };
784
+ const refreshToken = stringValue(payload.refresh_token);
785
+ const tokenType = oauth.tokenType?.trim() || stringValue(payload.token_type);
786
+ const scope = stringValue(payload.scope);
787
+ const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : Number(payload.expires_in);
788
+ if (refreshToken)
789
+ token.refreshToken = refreshToken;
790
+ if (tokenType)
791
+ token.tokenType = tokenType;
792
+ if (scope)
793
+ token.scopes = scope.split(/\s+/u).filter(Boolean);
794
+ if (Number.isFinite(expiresIn) && expiresIn > 0)
795
+ token.expiresAt = Date.now() + expiresIn * 1000;
796
+ // Non-standard response fields (provider identity ids and the like) ride
797
+ // along as the grant's credential bag; RFC 6749 §5.1 permits extras.
798
+ const credential = {};
799
+ for (const [key, value] of Object.entries(payload)) {
800
+ if (OAUTH_STANDARD_TOKEN_FIELDS.has(key))
801
+ continue;
802
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
803
+ credential[key] = value;
804
+ }
805
+ if (Object.keys(credential).length > 0)
806
+ token.credential = credential;
807
+ return token;
808
+ }
809
+ async saveGrantFromToken(connectionName, principal, principalKey, token) {
810
+ const now = timestamp();
811
+ const existing = await this.grantStore.getGrant(connectionName, principalKey);
812
+ const grant = {
813
+ connectionName,
814
+ principalKey,
815
+ principal,
816
+ token: token.token,
817
+ createdAt: existing?.createdAt ?? now,
818
+ updatedAt: now
819
+ };
820
+ if (token.refreshToken)
821
+ grant.refreshToken = token.refreshToken;
822
+ else if (existing?.refreshToken)
823
+ grant.refreshToken = existing.refreshToken;
824
+ if (token.tokenType)
825
+ grant.tokenType = token.tokenType;
826
+ if (token.scopes)
827
+ grant.scopes = token.scopes;
828
+ if (token.expiresAt)
829
+ grant.expiresAt = token.expiresAt;
830
+ // A response that omits extras must not erase identity captured earlier.
831
+ if (token.credential)
832
+ grant.credential = token.credential;
833
+ else if (existing?.credential)
834
+ grant.credential = existing.credential;
835
+ await this.grantStore.saveGrant(grant);
836
+ }
837
+ }
838
+ //# sourceMappingURL=connections.js.map