@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.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/agent-harness.d.ts +108 -0
- package/dist/agent-harness.d.ts.map +1 -0
- package/dist/agent-harness.js +2 -0
- package/dist/agent-harness.js.map +1 -0
- package/dist/attachments-legacy.d.ts +17 -0
- package/dist/attachments-legacy.d.ts.map +1 -0
- package/dist/attachments-legacy.js +273 -0
- package/dist/attachments-legacy.js.map +1 -0
- package/dist/attachments.d.ts +49 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +832 -0
- package/dist/attachments.js.map +1 -0
- package/dist/builtin-tool-executors.d.ts +96 -0
- package/dist/builtin-tool-executors.d.ts.map +1 -0
- package/dist/builtin-tool-executors.js +501 -0
- package/dist/builtin-tool-executors.js.map +1 -0
- package/dist/builtin-tools.d.ts +30 -0
- package/dist/builtin-tools.d.ts.map +1 -0
- package/dist/builtin-tools.js +690 -0
- package/dist/builtin-tools.js.map +1 -0
- package/dist/capability-catalog.d.ts +27 -0
- package/dist/capability-catalog.d.ts.map +1 -0
- package/dist/capability-catalog.js +193 -0
- package/dist/capability-catalog.js.map +1 -0
- package/dist/channel-http-util.d.ts +34 -0
- package/dist/channel-http-util.d.ts.map +1 -0
- package/dist/channel-http-util.js +93 -0
- package/dist/channel-http-util.js.map +1 -0
- package/dist/channel-ingress.d.ts +56 -0
- package/dist/channel-ingress.d.ts.map +1 -0
- package/dist/channel-ingress.js +273 -0
- package/dist/channel-ingress.js.map +1 -0
- package/dist/channel.d.ts +146 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +2 -0
- package/dist/channel.js.map +1 -0
- package/dist/checkpoints.d.ts +34 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +86 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/code-mode.d.ts +20 -0
- package/dist/code-mode.d.ts.map +1 -0
- package/dist/code-mode.js +658 -0
- package/dist/code-mode.js.map +1 -0
- package/dist/computer-relay-protocol.d.ts +36 -0
- package/dist/computer-relay-protocol.d.ts.map +1 -0
- package/dist/computer-relay-protocol.js +127 -0
- package/dist/computer-relay-protocol.js.map +1 -0
- package/dist/concurrency.d.ts +68 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +193 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connection-auth-util.d.ts +56 -0
- package/dist/connection-auth-util.d.ts.map +1 -0
- package/dist/connection-auth-util.js +154 -0
- package/dist/connection-auth-util.js.map +1 -0
- package/dist/connection-clients.d.ts +50 -0
- package/dist/connection-clients.d.ts.map +1 -0
- package/dist/connection-clients.js +402 -0
- package/dist/connection-clients.js.map +1 -0
- package/dist/connection-stores.d.ts +108 -0
- package/dist/connection-stores.d.ts.map +1 -0
- package/dist/connection-stores.js +127 -0
- package/dist/connection-stores.js.map +1 -0
- package/dist/connection-util.d.ts +27 -0
- package/dist/connection-util.d.ts.map +1 -0
- package/dist/connection-util.js +118 -0
- package/dist/connection-util.js.map +1 -0
- package/dist/connections.d.ts +152 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +838 -0
- package/dist/connections.js.map +1 -0
- package/dist/content-capture.d.ts +32 -0
- package/dist/content-capture.d.ts.map +1 -0
- package/dist/content-capture.js +77 -0
- package/dist/content-capture.js.map +1 -0
- package/dist/context-policy-util.d.ts +45 -0
- package/dist/context-policy-util.d.ts.map +1 -0
- package/dist/context-policy-util.js +120 -0
- package/dist/context-policy-util.js.map +1 -0
- package/dist/conversation-context.d.ts +55 -0
- package/dist/conversation-context.d.ts.map +1 -0
- package/dist/conversation-context.js +179 -0
- package/dist/conversation-context.js.map +1 -0
- package/dist/conversation-transcript.d.ts +49 -0
- package/dist/conversation-transcript.d.ts.map +1 -0
- package/dist/conversation-transcript.js +77 -0
- package/dist/conversation-transcript.js.map +1 -0
- package/dist/conversation-turn-dispatch.d.ts +46 -0
- package/dist/conversation-turn-dispatch.d.ts.map +1 -0
- package/dist/conversation-turn-dispatch.js +201 -0
- package/dist/conversation-turn-dispatch.js.map +1 -0
- package/dist/delivery-queue.d.ts +89 -0
- package/dist/delivery-queue.d.ts.map +1 -0
- package/dist/delivery-queue.js +244 -0
- package/dist/delivery-queue.js.map +1 -0
- package/dist/delivery-send.d.ts +44 -0
- package/dist/delivery-send.d.ts.map +1 -0
- package/dist/delivery-send.js +494 -0
- package/dist/delivery-send.js.map +1 -0
- package/dist/env-config.d.ts +6 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +27 -0
- package/dist/env-config.js.map +1 -0
- package/dist/eval-run-options.d.ts +37 -0
- package/dist/eval-run-options.d.ts.map +1 -0
- package/dist/eval-run-options.js +106 -0
- package/dist/eval-run-options.js.map +1 -0
- package/dist/harness-continuation.d.ts +54 -0
- package/dist/harness-continuation.d.ts.map +1 -0
- package/dist/harness-continuation.js +58 -0
- package/dist/harness-continuation.js.map +1 -0
- package/dist/harness-loop.d.ts +110 -0
- package/dist/harness-loop.d.ts.map +1 -0
- package/dist/harness-loop.js +575 -0
- package/dist/harness-loop.js.map +1 -0
- package/dist/hook-dispatch.d.ts +19 -0
- package/dist/hook-dispatch.d.ts.map +1 -0
- package/dist/hook-dispatch.js +37 -0
- package/dist/hook-dispatch.js.map +1 -0
- package/dist/http.d.ts +26 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +114 -0
- package/dist/http.js.map +1 -0
- package/dist/in-memory-conversation-turn-store.d.ts +29 -0
- package/dist/in-memory-conversation-turn-store.d.ts.map +1 -0
- package/dist/in-memory-conversation-turn-store.js +166 -0
- package/dist/in-memory-conversation-turn-store.js.map +1 -0
- package/dist/in-memory-run-store.d.ts +39 -0
- package/dist/in-memory-run-store.d.ts.map +1 -0
- package/dist/in-memory-run-store.js +332 -0
- package/dist/in-memory-run-store.js.map +1 -0
- package/dist/in-memory-sandbox-session-store.d.ts +33 -0
- package/dist/in-memory-sandbox-session-store.d.ts.map +1 -0
- package/dist/in-memory-sandbox-session-store.js +186 -0
- package/dist/in-memory-sandbox-session-store.js.map +1 -0
- package/dist/in-memory-small-stores.d.ts +117 -0
- package/dist/in-memory-small-stores.d.ts.map +1 -0
- package/dist/in-memory-small-stores.js +340 -0
- package/dist/in-memory-small-stores.js.map +1 -0
- package/dist/in-memory-usage-store.d.ts +15 -0
- package/dist/in-memory-usage-store.d.ts.map +1 -0
- package/dist/in-memory-usage-store.js +49 -0
- package/dist/in-memory-usage-store.js.map +1 -0
- package/dist/index.d.ts +467 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2046 -0
- package/dist/index.js.map +1 -0
- package/dist/json-util.d.ts +19 -0
- package/dist/json-util.d.ts.map +1 -0
- package/dist/json-util.js +59 -0
- package/dist/json-util.js.map +1 -0
- package/dist/local-sandbox.d.ts +16 -0
- package/dist/local-sandbox.d.ts.map +1 -0
- package/dist/local-sandbox.js +327 -0
- package/dist/local-sandbox.js.map +1 -0
- package/dist/logging.d.ts +16 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +41 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp-client.d.ts +27 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +169 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-harnesses.d.ts +4 -0
- package/dist/model-harnesses.d.ts.map +1 -0
- package/dist/model-harnesses.js +6 -0
- package/dist/model-harnesses.js.map +1 -0
- package/dist/model-media.d.ts +36 -0
- package/dist/model-media.d.ts.map +1 -0
- package/dist/model-media.js +234 -0
- package/dist/model-media.js.map +1 -0
- package/dist/operator-controls.d.ts +31 -0
- package/dist/operator-controls.d.ts.map +1 -0
- package/dist/operator-controls.js +71 -0
- package/dist/operator-controls.js.map +1 -0
- package/dist/pi-agent-harness.d.ts +40 -0
- package/dist/pi-agent-harness.d.ts.map +1 -0
- package/dist/pi-agent-harness.js +222 -0
- package/dist/pi-agent-harness.js.map +1 -0
- package/dist/pi-harness.d.ts +42 -0
- package/dist/pi-harness.d.ts.map +1 -0
- package/dist/pi-harness.js +142 -0
- package/dist/pi-harness.js.map +1 -0
- package/dist/relay-mcp-client.d.ts +17 -0
- package/dist/relay-mcp-client.d.ts.map +1 -0
- package/dist/relay-mcp-client.js +119 -0
- package/dist/relay-mcp-client.js.map +1 -0
- package/dist/replay-resume.d.ts +107 -0
- package/dist/replay-resume.d.ts.map +1 -0
- package/dist/replay-resume.js +495 -0
- package/dist/replay-resume.js.map +1 -0
- package/dist/resource-projection-util.d.ts +58 -0
- package/dist/resource-projection-util.d.ts.map +1 -0
- package/dist/resource-projection-util.js +298 -0
- package/dist/resource-projection-util.js.map +1 -0
- package/dist/resource-projection.d.ts +45 -0
- package/dist/resource-projection.d.ts.map +1 -0
- package/dist/resource-projection.js +481 -0
- package/dist/resource-projection.js.map +1 -0
- package/dist/run-heartbeat.d.ts +34 -0
- package/dist/run-heartbeat.d.ts.map +1 -0
- package/dist/run-heartbeat.js +62 -0
- package/dist/run-heartbeat.js.map +1 -0
- package/dist/run-outcome.d.ts +47 -0
- package/dist/run-outcome.d.ts.map +1 -0
- package/dist/run-outcome.js +92 -0
- package/dist/run-outcome.js.map +1 -0
- package/dist/run-recovery.d.ts +117 -0
- package/dist/run-recovery.d.ts.map +1 -0
- package/dist/run-recovery.js +466 -0
- package/dist/run-recovery.js.map +1 -0
- package/dist/run-resolver.d.ts +35 -0
- package/dist/run-resolver.d.ts.map +1 -0
- package/dist/run-resolver.js +84 -0
- package/dist/run-resolver.js.map +1 -0
- package/dist/run-stream.d.ts +37 -0
- package/dist/run-stream.d.ts.map +1 -0
- package/dist/run-stream.js +78 -0
- package/dist/run-stream.js.map +1 -0
- package/dist/runs-query.d.ts +29 -0
- package/dist/runs-query.d.ts.map +1 -0
- package/dist/runs-query.js +142 -0
- package/dist/runs-query.js.map +1 -0
- package/dist/runtime-bundle.d.ts +3 -0
- package/dist/runtime-bundle.d.ts.map +1 -0
- package/dist/runtime-bundle.js +18 -0
- package/dist/runtime-bundle.js.map +1 -0
- package/dist/runtime-types.d.ts +397 -0
- package/dist/runtime-types.d.ts.map +1 -0
- package/dist/runtime-types.js +2 -0
- package/dist/runtime-types.js.map +1 -0
- package/dist/runtime-util.d.ts +45 -0
- package/dist/runtime-util.d.ts.map +1 -0
- package/dist/runtime-util.js +116 -0
- package/dist/runtime-util.js.map +1 -0
- package/dist/sandbox-cli-client.d.ts +20 -0
- package/dist/sandbox-cli-client.d.ts.map +1 -0
- package/dist/sandbox-cli-client.js +97 -0
- package/dist/sandbox-cli-client.js.map +1 -0
- package/dist/sandbox-fs.d.ts +56 -0
- package/dist/sandbox-fs.d.ts.map +1 -0
- package/dist/sandbox-fs.js +276 -0
- package/dist/sandbox-fs.js.map +1 -0
- package/dist/sandbox-session-key.d.ts +8 -0
- package/dist/sandbox-session-key.d.ts.map +1 -0
- package/dist/sandbox-session-key.js +32 -0
- package/dist/sandbox-session-key.js.map +1 -0
- package/dist/sandbox-snapshot-policy.d.ts +27 -0
- package/dist/sandbox-snapshot-policy.d.ts.map +1 -0
- package/dist/sandbox-snapshot-policy.js +94 -0
- package/dist/sandbox-snapshot-policy.js.map +1 -0
- package/dist/sandbox-sync-worker.d.ts +63 -0
- package/dist/sandbox-sync-worker.d.ts.map +1 -0
- package/dist/sandbox-sync-worker.js +448 -0
- package/dist/sandbox-sync-worker.js.map +1 -0
- package/dist/schedule-execution.d.ts +59 -0
- package/dist/schedule-execution.d.ts.map +1 -0
- package/dist/schedule-execution.js +951 -0
- package/dist/schedule-execution.js.map +1 -0
- package/dist/scheduler.d.ts +19 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +38 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +20 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +145 -0
- package/dist/security.js.map +1 -0
- package/dist/self-improvement.d.ts +245 -0
- package/dist/self-improvement.d.ts.map +1 -0
- package/dist/self-improvement.js +554 -0
- package/dist/self-improvement.js.map +1 -0
- package/dist/source-loader.d.ts +19 -0
- package/dist/source-loader.d.ts.map +1 -0
- package/dist/source-loader.js +160 -0
- package/dist/source-loader.js.map +1 -0
- package/dist/state-store-helpers.d.ts +28 -0
- package/dist/state-store-helpers.d.ts.map +1 -0
- package/dist/state-store-helpers.js +164 -0
- package/dist/state-store-helpers.js.map +1 -0
- package/dist/state-stores.d.ts +25 -0
- package/dist/state-stores.d.ts.map +1 -0
- package/dist/state-stores.js +157 -0
- package/dist/state-stores.js.map +1 -0
- package/dist/state.d.ts +736 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +46 -0
- package/dist/state.js.map +1 -0
- package/dist/storage.d.ts +188 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +752 -0
- package/dist/storage.js.map +1 -0
- package/dist/subagent-execution.d.ts +80 -0
- package/dist/subagent-execution.d.ts.map +1 -0
- package/dist/subagent-execution.js +135 -0
- package/dist/subagent-execution.js.map +1 -0
- package/dist/telemetry.d.ts +49 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +60 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tool-apis.d.ts +22 -0
- package/dist/tool-apis.d.ts.map +1 -0
- package/dist/tool-apis.js +344 -0
- package/dist/tool-apis.js.map +1 -0
- package/dist/tool-execution.d.ts +143 -0
- package/dist/tool-execution.d.ts.map +1 -0
- package/dist/tool-execution.js +573 -0
- package/dist/tool-execution.js.map +1 -0
- package/dist/trusted-attachments.d.ts +20 -0
- package/dist/trusted-attachments.d.ts.map +1 -0
- package/dist/trusted-attachments.js +276 -0
- package/dist/trusted-attachments.js.map +1 -0
- package/dist/usage-accounting.d.ts +37 -0
- package/dist/usage-accounting.d.ts.map +1 -0
- package/dist/usage-accounting.js +181 -0
- package/dist/usage-accounting.js.map +1 -0
- package/dist/usage-budget.d.ts +29 -0
- package/dist/usage-budget.d.ts.map +1 -0
- package/dist/usage-budget.js +145 -0
- package/dist/usage-budget.js.map +1 -0
- package/dist/usage-reconciliation.d.ts +76 -0
- package/dist/usage-reconciliation.d.ts.map +1 -0
- package/dist/usage-reconciliation.js +463 -0
- package/dist/usage-reconciliation.js.map +1 -0
- package/dist/usage-records.d.ts +24 -0
- package/dist/usage-records.d.ts.map +1 -0
- package/dist/usage-records.js +445 -0
- package/dist/usage-records.js.map +1 -0
- 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
|