diff --git a/config/openclaw.json5 b/config/openclaw.json5 new file mode 100644 index 0000000000000..324965ae2075b --- /dev/null +++ b/config/openclaw.json5 @@ -0,0 +1,43 @@ +// Hyperion Assistant Service — OC Gateway base config. +// Loaded via OC_GATEWAY_CONFIG secret in ECS container. +// Per-tenant config (model, tools, personality) is loaded from DynamoDB at runtime +// by the Hyperion plugin; this file only defines platform-level gateway settings. +{ + // Gateway binds to 0.0.0.0 on port 18789 (overridden by CMD in ECS task def). + gateway: { + mode: "local", + port: 18789, + bind: "lan", + auth: { + // ALB handles TLS termination; no gateway-level auth needed. + // WAF rate-limiting + ALB security group restrict access. + mode: "none", + }, + // ALB is a trusted reverse proxy — trust x-forwarded-for for client IP. + trustedProxies: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], + }, + + // ACP runtime — route agent turns to AgentCore (registered by agentcore extension). + acp: { + enabled: true, + backend: "agentcore", + }, + + // Extensions loaded at startup (built into the container image via OPENCLAW_EXTENSIONS). + plugins: { + enabled: true, + }, + + // Logging — structured JSON for CloudWatch ingestion. + logging: { + format: "json", + }, + + // Disable features not needed in headless server mode. + update: { + checkOnStart: false, + }, + discovery: { + mdns: { mode: "off" }, + }, +} diff --git a/extensions/agentcore/index.ts b/extensions/agentcore/index.ts new file mode 100644 index 0000000000000..5441a522b65ae --- /dev/null +++ b/extensions/agentcore/index.ts @@ -0,0 +1,45 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/acpx"; +import { createAgentCoreRuntimeService } from "./src/service.js"; + +type AgentCorePluginConfig = { + ssmPrefix?: string; + region?: string; + endpoint?: string; + invokeTimeoutMs?: number; +}; + +const plugin = { + id: "agentcore", + name: "AgentCore Runtime", + description: "ACP runtime backend powered by AWS Bedrock AgentCore.", + register(api: OpenClawPluginApi) { + const pluginConfig = (api.pluginConfig ?? {}) as AgentCorePluginConfig; + + // Derive SSM prefix from HYPERION_STAGE env var if not configured. + const stage = process.env.HYPERION_STAGE; + const ssmPrefix = + pluginConfig.ssmPrefix ?? (stage ? `/hyperion/${stage}/agentcore` : undefined); + + if (!ssmPrefix) { + api.logger.warn( + "AgentCore plugin: no ssmPrefix configured and HYPERION_STAGE not set. " + + "Set plugins.agentcore.ssmPrefix in config or HYPERION_STAGE env var.", + ); + return; + } + + const region = pluginConfig.region ?? process.env.AWS_REGION ?? "us-west-2"; + + api.registerService( + createAgentCoreRuntimeService({ + configSource: { + ssmPrefix, + region, + localOverride: pluginConfig.endpoint ? { endpoint: pluginConfig.endpoint } : undefined, + }, + }), + ); + }, +}; + +export default plugin; diff --git a/extensions/agentcore/openclaw.plugin.json b/extensions/agentcore/openclaw.plugin.json new file mode 100644 index 0000000000000..b0873f8af94f8 --- /dev/null +++ b/extensions/agentcore/openclaw.plugin.json @@ -0,0 +1,48 @@ +{ + "id": "agentcore", + "name": "AgentCore Runtime", + "description": "ACP runtime backend powered by AWS Bedrock AgentCore. Replaces embedded Pi Agent with per-tenant Firecracker microVMs.", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "ssmPrefix": { + "type": "string", + "description": "SSM parameter path prefix (e.g. /hyperion/beta/agentcore)" + }, + "region": { + "type": "string", + "description": "AWS region for AgentCore API calls" + }, + "endpoint": { + "type": "string", + "description": "AgentCore endpoint override (for local testing)" + }, + "invokeTimeoutMs": { + "type": "number", + "minimum": 1000, + "description": "Timeout for agent invocations in milliseconds" + } + } + }, + "uiHints": { + "ssmPrefix": { + "label": "SSM Parameter Prefix", + "help": "SSM path prefix for AgentCore config (e.g. /hyperion/beta/agentcore). Runtime ARNs, memory config, and default model are read from sub-parameters." + }, + "region": { + "label": "AWS Region", + "help": "AWS region for AgentCore. Defaults to AWS_REGION env var or us-west-2." + }, + "endpoint": { + "label": "Endpoint Override", + "help": "Custom AgentCore endpoint for local development.", + "advanced": true + }, + "invokeTimeoutMs": { + "label": "Invoke Timeout (ms)", + "help": "Maximum time to wait for an agent invocation (default: 300000 = 5 minutes).", + "advanced": true + } + } +} diff --git a/extensions/agentcore/package.json b/extensions/agentcore/package.json new file mode 100644 index 0000000000000..0d83820639f56 --- /dev/null +++ b/extensions/agentcore/package.json @@ -0,0 +1,18 @@ +{ + "name": "@openclaw/agentcore", + "version": "2026.3.10", + "description": "OpenClaw ACP runtime backend via AWS Bedrock AgentCore", + "type": "module", + "dependencies": { + "@aws-sdk/client-bedrock-agentcore": "^3.0.0", + "@aws-sdk/client-ssm": "^3.0.0" + }, + "devDependencies": { + "openclaw": "workspace:*" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/agentcore/src/config.test.ts b/extensions/agentcore/src/config.test.ts new file mode 100644 index 0000000000000..cb0a467b405cc --- /dev/null +++ b/extensions/agentcore/src/config.test.ts @@ -0,0 +1,229 @@ +// @vitest-pool threads +// ↑ vi.mock for external packages requires threads pool. + +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const mockSsmSend = vi.fn(); +vi.mock("@aws-sdk/client-ssm", () => ({ + SSMClient: class { + send = mockSsmSend; + }, + GetParameterCommand: class { + input: unknown; + constructor(input: unknown) { + this.input = input; + } + }, +})); + +import { loadAgentCoreConfig } from "./config.js"; + +describe("loadAgentCoreConfig", () => { + beforeEach(() => { + mockSsmSend.mockReset(); + }); + + // ── localOverride path ────────────────────────────────────────────── + + describe("localOverride path", () => { + it("returns override values directly and does NOT call SSM", async () => { + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + localOverride: { + runtimeArns: ["arn:aws:agentcore:us-west-2:123:runtime/local"], + memoryNamespacePrefix: "local_", + defaultModel: "anthropic.claude-haiku-3", + }, + }); + + expect(config.runtimeArns).toEqual(["arn:aws:agentcore:us-west-2:123:runtime/local"]); + expect(config.memoryNamespacePrefix).toBe("local_"); + expect(config.defaultModel).toBe("anthropic.claude-haiku-3"); + expect(mockSsmSend).not.toHaveBeenCalled(); + }); + + it("fills in defaults for missing fields", async () => { + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + localOverride: {}, + }); + + expect(config.runtimeArns).toEqual([]); + expect(config.memoryNamespacePrefix).toBe("tenant_"); + expect(config.defaultModel).toBe("anthropic.claude-sonnet-4-20250514"); + expect(config.region).toBe("us-west-2"); + expect(mockSsmSend).not.toHaveBeenCalled(); + }); + + it("preserves provided endpoint and invokeTimeoutMs", async () => { + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + region: "eu-west-1", + localOverride: { + endpoint: "https://localhost:9999", + invokeTimeoutMs: 60_000, + }, + }); + + expect(config.endpoint).toBe("https://localhost:9999"); + expect(config.invokeTimeoutMs).toBe(60_000); + expect(config.region).toBe("eu-west-1"); + }); + }); + + // ── SSM path ──────────────────────────────────────────────────────── + + describe("SSM path", () => { + it("parses runtime-arns as JSON array of strings", async () => { + mockSsmSend.mockImplementation((cmd: any) => { + const name = cmd.input?.Name; + if (name?.endsWith("/runtime-arns")) { + return { + Parameter: { + Value: + '["arn:aws:agentcore:us-west-2:123:runtime/a","arn:aws:agentcore:us-west-2:123:runtime/b"]', + }, + }; + } + return { Parameter: { Value: null } }; + }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.runtimeArns).toEqual([ + "arn:aws:agentcore:us-west-2:123:runtime/a", + "arn:aws:agentcore:us-west-2:123:runtime/b", + ]); + }); + + it("parses memory-config JSON and extracts memoryNamespacePrefix", async () => { + mockSsmSend.mockImplementation((cmd: any) => { + const name = cmd.input?.Name; + if (name?.endsWith("/memory-config")) { + return { + Parameter: { Value: '{"memoryEnabled":true,"memoryNamespacePrefix":"custom_prefix_"}' }, + }; + } + return { Parameter: { Value: null } }; + }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.memoryNamespacePrefix).toBe("custom_prefix_"); + }); + + it("uses default-model string value and trims whitespace", async () => { + mockSsmSend.mockImplementation((cmd: any) => { + const name = cmd.input?.Name; + if (name?.endsWith("/default-model")) { + return { Parameter: { Value: " anthropic.claude-haiku-3 " } }; + } + return { Parameter: { Value: null } }; + }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.defaultModel).toBe("anthropic.claude-haiku-3"); + }); + + it("falls back to defaults when SSM returns null for all params", async () => { + mockSsmSend.mockResolvedValue({ Parameter: { Value: null } }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.runtimeArns).toEqual([]); + expect(config.memoryNamespacePrefix).toBe("tenant_"); + expect(config.defaultModel).toBe("anthropic.claude-sonnet-4-20250514"); + expect(config.region).toBe("us-west-2"); + }); + + it("falls back to defaults when SSM params contain invalid JSON", async () => { + mockSsmSend.mockImplementation((cmd: any) => { + const name = cmd.input?.Name; + if (name?.endsWith("/runtime-arns")) { + return { Parameter: { Value: "not-valid-json{[" } }; + } + if (name?.endsWith("/memory-config")) { + return { Parameter: { Value: "{broken" } }; + } + if (name?.endsWith("/default-model")) { + return { Parameter: { Value: "" } }; + } + return {}; + }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.runtimeArns).toEqual([]); + expect(config.memoryNamespacePrefix).toBe("tenant_"); + expect(config.defaultModel).toBe("anthropic.claude-sonnet-4-20250514"); + }); + + it("uses DEFAULT_REGION when region not specified in source", async () => { + mockSsmSend.mockResolvedValue({ Parameter: { Value: null } }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.region).toBe("us-west-2"); + }); + }); + + // ── SSM error handling ────────────────────────────────────────────── + + describe("SSM error handling", () => { + it("handles SSM GetParameter failures gracefully and returns defaults", async () => { + mockSsmSend.mockRejectedValue(new Error("ParameterNotFound")); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.runtimeArns).toEqual([]); + expect(config.memoryNamespacePrefix).toBe("tenant_"); + expect(config.defaultModel).toBe("anthropic.claude-sonnet-4-20250514"); + expect(config.region).toBe("us-west-2"); + }); + + it("filters out empty and non-string entries from runtimeArns", async () => { + mockSsmSend.mockImplementation((cmd: any) => { + const name = cmd.input?.Name; + if (name?.endsWith("/runtime-arns")) { + return { + Parameter: { + Value: JSON.stringify([ + "arn:aws:agentcore:us-west-2:123:runtime/valid", + "", + " ", + 42, + null, + "arn:aws:agentcore:us-west-2:123:runtime/also-valid", + ]), + }, + }; + } + return { Parameter: { Value: null } }; + }); + + const config = await loadAgentCoreConfig({ + ssmPrefix: "/hyperion/beta/agentcore", + }); + + expect(config.runtimeArns).toEqual([ + "arn:aws:agentcore:us-west-2:123:runtime/valid", + "arn:aws:agentcore:us-west-2:123:runtime/also-valid", + ]); + }); + }); +}); diff --git a/extensions/agentcore/src/config.ts b/extensions/agentcore/src/config.ts new file mode 100644 index 0000000000000..159e150bc77da --- /dev/null +++ b/extensions/agentcore/src/config.ts @@ -0,0 +1,86 @@ +import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import type { AgentCoreRuntimeConfig } from "./types.js"; + +const DEFAULT_REGION = "us-west-2"; +const DEFAULT_MEMORY_NAMESPACE_PREFIX = "tenant_"; +const DEFAULT_MODEL = "anthropic.claude-sonnet-4-20250514"; + +export type AgentCoreConfigSource = { + /** SSM parameter path prefix (e.g. "/hyperion/beta/agentcore"). */ + ssmPrefix: string; + /** AWS region for SSM and AgentCore. */ + region?: string; + /** Override for local development (skip SSM, use provided config). */ + localOverride?: Partial; +}; + +/** + * Load AgentCore config from SSM parameters written by CDK (AgentCoreConstruct): + * + * /hyperion/{stage}/agentcore/runtime-arns — JSON array of Runtime ARNs + * /hyperion/{stage}/agentcore/memory-config — JSON { memoryEnabled, memoryNamespacePrefix } + * /hyperion/{stage}/agentcore/default-model — model ID string + */ +export async function loadAgentCoreConfig( + source: AgentCoreConfigSource, +): Promise { + const region = source.region || DEFAULT_REGION; + + if (source.localOverride) { + return { + region, + runtimeArns: source.localOverride.runtimeArns ?? [], + memoryNamespacePrefix: + source.localOverride.memoryNamespacePrefix ?? DEFAULT_MEMORY_NAMESPACE_PREFIX, + defaultModel: source.localOverride.defaultModel ?? DEFAULT_MODEL, + endpoint: source.localOverride.endpoint, + invokeTimeoutMs: source.localOverride.invokeTimeoutMs, + }; + } + + const ssm = new SSMClient({ region }); + + const [runtimeArnsParam, memoryConfigParam, defaultModelParam] = await Promise.all([ + ssmGet(ssm, `${source.ssmPrefix}/runtime-arns`), + ssmGet(ssm, `${source.ssmPrefix}/memory-config`), + ssmGet(ssm, `${source.ssmPrefix}/default-model`), + ]); + + let runtimeArns: string[] = []; + try { + const parsed = JSON.parse(runtimeArnsParam ?? "[]"); + if (Array.isArray(parsed)) { + runtimeArns = parsed.filter((v): v is string => typeof v === "string" && v.trim() !== ""); + } + } catch { + // Fall through to empty array + } + + let memoryNamespacePrefix = DEFAULT_MEMORY_NAMESPACE_PREFIX; + try { + const parsed = JSON.parse(memoryConfigParam ?? "{}"); + if (parsed && typeof parsed.memoryNamespacePrefix === "string") { + memoryNamespacePrefix = parsed.memoryNamespacePrefix; + } + } catch { + // Fall through to default + } + + const defaultModel = defaultModelParam?.trim() || DEFAULT_MODEL; + + return { + region, + runtimeArns, + memoryNamespacePrefix, + defaultModel, + }; +} + +async function ssmGet(ssm: SSMClient, name: string): Promise { + try { + const resp = await ssm.send(new GetParameterCommand({ Name: name })); + return resp.Parameter?.Value ?? null; + } catch { + return null; + } +} diff --git a/extensions/agentcore/src/index.ts b/extensions/agentcore/src/index.ts new file mode 100644 index 0000000000000..d4a2fff1a8345 --- /dev/null +++ b/extensions/agentcore/src/index.ts @@ -0,0 +1,4 @@ +export { AGENTCORE_BACKEND_ID, AgentCoreRuntime } from "./runtime.js"; +export { createAgentCoreRuntimeService, type CreateAgentCoreServiceParams } from "./service.js"; +export { loadAgentCoreConfig, type AgentCoreConfigSource } from "./config.js"; +export type { AgentCoreRuntimeConfig, AgentCoreHandleState } from "./types.js"; diff --git a/extensions/agentcore/src/runtime.test.ts b/extensions/agentcore/src/runtime.test.ts new file mode 100644 index 0000000000000..2aa7e6d5184be --- /dev/null +++ b/extensions/agentcore/src/runtime.test.ts @@ -0,0 +1,952 @@ +// @vitest-pool threads +// ↑ vi.mock for external packages requires threads pool (forks doesn't intercept). + +import { describe, expect, it, vi, beforeEach } from "vitest"; + +// --------------------------------------------------------------------------- +// Mocks — must be declared before importing the module under test. +// --------------------------------------------------------------------------- + +const mockSend = vi.fn(); + +vi.mock("@aws-sdk/client-bedrock-agentcore", () => ({ + BedrockAgentCoreClient: vi.fn().mockImplementation(function () { + return { send: mockSend }; + }), + InvokeAgentRuntimeCommand: vi.fn().mockImplementation(function (input: unknown) { + return { input }; + }), + StopRuntimeSessionCommand: vi.fn().mockImplementation(function (input: unknown) { + return { input }; + }), + RetrieveMemoryRecordsCommand: vi.fn().mockImplementation(function (input: unknown) { + return { input }; + }), + StartMemoryExtractionJobCommand: vi.fn().mockImplementation(function (input: unknown) { + return { input }; + }), +})); + +vi.mock("openclaw/plugin-sdk/acpx", () => { + class AcpRuntimeError extends Error { + code: string; + constructor(code: string, message: string) { + super(message); + this.code = code; + this.name = "AcpRuntimeError"; + } + } + return { AcpRuntimeError }; +}); + +vi.mock("../../hyperion/src/globals.js", () => ({ + hasHyperionRuntime: vi.fn().mockReturnValue(false), + getHyperionRuntime: vi.fn(), +})); + +vi.mock("../../../src/hyperion/session-manager.js", () => ({ + extractTenantId: vi.fn((key: string) => { + if (!key.startsWith("tenant_")) return null; + const afterPrefix = key.slice(7); + const sep = afterPrefix.indexOf(":"); + if (sep < 0) return null; + return afterPrefix.slice(0, sep); + }), + extractAgentId: vi.fn((key: string) => { + if (!key.startsWith("tenant_")) return "main"; + const afterPrefix = key.slice(7); + const firstSep = afterPrefix.indexOf(":"); + if (firstSep < 0) return "main"; + const afterUserId = afterPrefix.slice(firstSep + 1); + const secondSep = afterUserId.indexOf(":"); + if (secondSep < 0) return afterUserId || "main"; + return afterUserId.slice(0, secondSep) || "main"; + }), +})); + +vi.mock("../../../src/hyperion/types.js", () => ({ + DEFAULT_AGENT_ID: "main", +})); + +// --------------------------------------------------------------------------- +// Imports (after mocks) +// --------------------------------------------------------------------------- + +import type { + AcpRuntimeEnsureInput, + AcpRuntimeEvent, + AcpRuntimeHandle, + AcpRuntimeTurnInput, +} from "openclaw/plugin-sdk/acpx"; +import { hasHyperionRuntime, getHyperionRuntime } from "../../hyperion/src/globals.js"; +import { AGENTCORE_BACKEND_ID, AgentCoreRuntime } from "./runtime.js"; +import type { AgentCoreRuntimeConfig, AgentCoreHandleState } from "./types.js"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const RUNTIME_ARN = "arn:aws:bedrock:us-east-1:123456789012:agent-runtime/test-runtime"; + +function makeConfig(overrides?: Partial): AgentCoreRuntimeConfig { + return { + region: "us-east-1", + runtimeArns: [RUNTIME_ARN], + memoryNamespacePrefix: "tenant_", + defaultModel: "anthropic.claude-sonnet-4-20250514", + ...overrides, + }; +} + +function createRuntime(overrides?: Partial): AgentCoreRuntime { + return new AgentCoreRuntime(makeConfig(overrides)); +} + +function makeEnsureInput(overrides?: Partial): AcpRuntimeEnsureInput { + return { + agent: "user1", + sessionKey: "tenant_user1:main:main", + mode: "persistent", + ...overrides, + }; +} + +function makeTurnInput( + handle: AcpRuntimeHandle, + overrides?: Partial, +): AcpRuntimeTurnInput { + return { + handle, + text: "Hi there", + mode: "prompt", + requestId: "test-request-id", + ...overrides, + }; +} + +/** Decode the base64url state from a handle's runtimeSessionName. */ +function decodeState(handle: AcpRuntimeHandle): AgentCoreHandleState { + const prefix = "agentcore:v1:"; + const encoded = handle.runtimeSessionName.slice(prefix.length); + return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); +} + +const SAMPLE_STATE: AgentCoreHandleState = { + runtimeArn: RUNTIME_ARN, + sessionId: "test-session-id", + tenantId: "user123", + agentId: "main", + agent: "user123", + mode: "persistent", +}; + +/** Collect all events from an async iterable. */ +async function collectEvents(iter: AsyncIterable): Promise { + const results: T[] = []; + for await (const item of iter) { + results.push(item); + } + return results; +} + +/** Narrow an AcpRuntimeEvent to the error variant. */ +function expectErrorEvent(event: AcpRuntimeEvent): AcpRuntimeEvent & { type: "error" } { + expect(event.type).toBe("error"); + return event as AcpRuntimeEvent & { type: "error" }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("AGENTCORE_BACKEND_ID", () => { + it("equals 'agentcore'", () => { + expect(AGENTCORE_BACKEND_ID).toBe("agentcore"); + }); +}); + +describe("AgentCoreRuntime", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSend.mockReset(); + }); + + // ----------------------------------------------------------------------- + // ensureSession + // ----------------------------------------------------------------------- + + describe("ensureSession", () => { + it("throws AcpRuntimeError when agent is missing", async () => { + const runtime = createRuntime(); + const input = { + sessionKey: "tenant_u1:main:main", + mode: "persistent", + } as Partial; + await expect(runtime.ensureSession(input as AcpRuntimeEnsureInput)).rejects.toThrow( + "Agent ID is required.", + ); + }); + + it("throws AcpRuntimeError when agent is empty/whitespace", async () => { + const runtime = createRuntime(); + await expect(runtime.ensureSession(makeEnsureInput({ agent: " " }))).rejects.toThrow( + "Agent ID is required.", + ); + }); + + it("throws AcpRuntimeError when sessionKey is missing", async () => { + const runtime = createRuntime(); + const input = { agent: "user1", mode: "persistent" } as Partial; + await expect(runtime.ensureSession(input as AcpRuntimeEnsureInput)).rejects.toThrow( + "Session key is required.", + ); + }); + + it("throws AcpRuntimeError when sessionKey is empty/whitespace", async () => { + const runtime = createRuntime(); + await expect(runtime.ensureSession(makeEnsureInput({ sessionKey: " " }))).rejects.toThrow( + "Session key is required.", + ); + }); + + it("returns handle with correct sessionKey and backend", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + expect(handle.sessionKey).toBe("tenant_user1:main:main"); + expect(handle.backend).toBe("agentcore"); + }); + + it("returns handle with runtimeSessionName starting with 'agentcore:v1:'", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + expect(handle.runtimeSessionName).toMatch(/^agentcore:v1:/); + }); + + it("encodes handle state that roundtrips correctly", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + // Verify roundtrip: decode the encoded state and check fields + const state = decodeState(handle); + expect(state.runtimeArn).toBe(RUNTIME_ARN); + expect(state.tenantId).toBe("user1"); + expect(state.agent).toBe("user1"); + expect(state.mode).toBe("persistent"); + expect(state.sessionId).toBeTruthy(); + + // Also verify via getStatus (the public API roundtrip) + const status = await runtime.getStatus({ handle }); + expect(status.summary).toContain(`session=${state.sessionId}`); + expect(status.summary).toContain("tenant=user1"); + expect(status.backendSessionId).toBe(state.sessionId); + }); + + it("uses resumeSessionId when provided", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession( + makeEnsureInput({ resumeSessionId: "existing-session-id-123" }), + ); + + const state = decodeState(handle); + expect(state.sessionId).toBe("existing-session-id-123"); + expect(handle.backendSessionId).toBe("existing-session-id-123"); + }); + + it("generates new UUID sessionId when no resumeSessionId", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + const state = decodeState(handle); + // UUID v4 format: 8-4-4-4-12 hex chars + expect(state.sessionId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ); + }); + + it("extracts agentId from session key", async () => { + const runtime = createRuntime(); + // Session key "tenant_user1:work:main" => agentId = "work" + const handle = await runtime.ensureSession( + makeEnsureInput({ sessionKey: "tenant_user1:work:main" }), + ); + + const state = decodeState(handle); + expect(state.agentId).toBe("work"); + }); + }); + + // ----------------------------------------------------------------------- + // cancel + // ----------------------------------------------------------------------- + + describe("cancel", () => { + it("calls StopRuntimeSessionCommand via client.send", async () => { + const runtime = createRuntime(); + mockSend.mockResolvedValue({}); + + const handle = await runtime.ensureSession(makeEnsureInput()); + await runtime.cancel({ handle }); + + expect(mockSend).toHaveBeenCalledTimes(1); + const sentCommand = mockSend.mock.calls[0][0]; + expect(sentCommand.input.agentRuntimeArn).toBe(RUNTIME_ARN); + expect(sentCommand.input.runtimeSessionId).toBeTruthy(); + }); + + it("swallows errors (best-effort)", async () => { + const runtime = createRuntime(); + mockSend.mockRejectedValue(new Error("Network error")); + + const handle = await runtime.ensureSession(makeEnsureInput()); + + // Should not throw + await expect(runtime.cancel({ handle })).resolves.toBeUndefined(); + }); + }); + + // ----------------------------------------------------------------------- + // close + // ----------------------------------------------------------------------- + + describe("close", () => { + it("calls StopRuntimeSessionCommand for oneshot mode", async () => { + const runtime = createRuntime(); + mockSend.mockResolvedValue({}); + + const handle = await runtime.ensureSession(makeEnsureInput({ mode: "oneshot" })); + await runtime.close({ handle, reason: "done" }); + + expect(mockSend).toHaveBeenCalledTimes(1); + const sentCommand = mockSend.mock.calls[0][0]; + expect(sentCommand.input.agentRuntimeArn).toBe(RUNTIME_ARN); + }); + + it("does NOT call StopRuntimeSessionCommand for persistent mode", async () => { + const runtime = createRuntime(); + + const handle = await runtime.ensureSession(makeEnsureInput()); + await runtime.close({ handle, reason: "done" }); + + expect(mockSend).not.toHaveBeenCalled(); + }); + }); + + // ----------------------------------------------------------------------- + // doctor + // ----------------------------------------------------------------------- + + describe("doctor", () => { + it("returns ok:false when no runtimeArns configured", async () => { + const runtime = createRuntime({ runtimeArns: [] }); + + const report = await runtime.doctor(); + + expect(report.ok).toBe(false); + expect(report.code).toBe("ACP_BACKEND_UNAVAILABLE"); + expect(report.message).toContain("No AgentCore Runtime ARNs configured"); + }); + + it("returns ok:true with message when runtimeArns are present", async () => { + const runtime = createRuntime(); + + const report = await runtime.doctor(); + + expect(report.ok).toBe(true); + expect(report.message).toContain("AgentCore backend configured"); + expect(report.message).toContain("us-east-1"); + expect(report.message).toContain("runtimes: 1"); + }); + }); + + // ----------------------------------------------------------------------- + // getCapabilities + // ----------------------------------------------------------------------- + + describe("getCapabilities", () => { + it("returns capabilities with empty controls array", () => { + const runtime = createRuntime(); + const caps = runtime.getCapabilities(); + expect(caps).toEqual({ controls: [] }); + }); + }); + + // ----------------------------------------------------------------------- + // getStatus + // ----------------------------------------------------------------------- + + describe("getStatus", () => { + it("returns summary with session, runtime, and tenant info", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput({ resumeSessionId: "sess-abc" })); + + const status = await runtime.getStatus({ handle }); + + expect(status.summary).toContain("session=sess-abc"); + expect(status.summary).toContain(`runtime=${RUNTIME_ARN}`); + expect(status.summary).toContain("tenant=user1"); + expect(status.backendSessionId).toBe("sess-abc"); + }); + }); + + // ----------------------------------------------------------------------- + // isHealthy / setHealthy + // ----------------------------------------------------------------------- + + describe("isHealthy / setHealthy", () => { + it("is initially false", () => { + const runtime = createRuntime(); + expect(runtime.isHealthy()).toBe(false); + }); + + it("setHealthy(true) makes it true", () => { + const runtime = createRuntime(); + runtime.setHealthy(true); + expect(runtime.isHealthy()).toBe(true); + }); + }); + + // ----------------------------------------------------------------------- + // resolveHandleState (tested indirectly via getStatus) + // ----------------------------------------------------------------------- + + describe("resolveHandleState (indirect)", () => { + it("throws AcpRuntimeError for handle with wrong prefix", async () => { + const runtime = createRuntime(); + const badHandle: AcpRuntimeHandle = { + sessionKey: "test", + backend: "agentcore", + runtimeSessionName: "wrong-prefix:eyJ0ZXN0IjoxfQ", + }; + + await expect(runtime.getStatus({ handle: badHandle })).rejects.toThrow( + "Invalid AgentCore runtime handle", + ); + }); + + it("throws AcpRuntimeError for handle with corrupted base64", async () => { + const runtime = createRuntime(); + const badHandle: AcpRuntimeHandle = { + sessionKey: "test", + backend: "agentcore", + runtimeSessionName: "agentcore:v1:!!!not-valid-base64!!!", + }; + + await expect(runtime.getStatus({ handle: badHandle })).rejects.toThrow("could not decode"); + }); + }); + + // ----------------------------------------------------------------------- + // pickRuntimeArn (tested indirectly via ensureSession) + // ----------------------------------------------------------------------- + + describe("pickRuntimeArn (indirect)", () => { + it("throws when runtimeArns is empty", async () => { + const runtime = createRuntime({ runtimeArns: [] }); + + await expect(runtime.ensureSession(makeEnsureInput())).rejects.toThrow( + "No AgentCore Runtime ARNs configured", + ); + }); + + it("selects from multiple ARNs without error", async () => { + const arns = [ + "arn:aws:bedrock:us-east-1:123456789012:agent-runtime/rt-1", + "arn:aws:bedrock:us-east-1:123456789012:agent-runtime/rt-2", + ]; + const runtime = createRuntime({ runtimeArns: arns }); + + const handle = await runtime.ensureSession(makeEnsureInput()); + + const state = decodeState(handle); + expect(arns).toContain(state.runtimeArn); + }); + }); + + // ----------------------------------------------------------------------- + // runTurn + // ----------------------------------------------------------------------- + + describe("runTurn", () => { + it("yields text_delta and done events for a successful invocation", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Hello, user!" }), + }, + }) // InvokeAgentRuntime + .mockResolvedValueOnce({}); // StartMemoryExtractionJob + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events).toHaveLength(2); + expect(events[0]).toEqual({ + type: "text_delta", + text: "Hello, user!", + stream: "output", + }); + expect(events[1]).toEqual({ type: "done" }); + }); + + it("yields done event when response body is empty", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockResolvedValueOnce({ + response: { transformToString: async () => " " }, + }); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events).toHaveLength(1); + expect(events[0]).toEqual({ type: "done" }); + }); + + it("yields error event on invocation failure", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockRejectedValueOnce(new Error("Connection refused")); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events).toHaveLength(1); + const errEvent = expectErrorEvent(events[0]); + expect(errEvent.message).toContain("Connection refused"); + }); + + it("yields retryable error on ThrottlingException", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + const throttleErr = new Error("Rate exceeded"); + throttleErr.name = "ThrottlingException"; + + mockSend.mockResolvedValueOnce({ records: [] }).mockRejectedValueOnce(throttleErr); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events).toHaveLength(1); + const errEvent = expectErrorEvent(events[0]); + expect(errEvent.code).toBe("RATE_LIMITED"); + expect(errEvent.retryable).toBe(true); + }); + + it("sets healthy=false on ResourceNotFoundException", async () => { + const runtime = createRuntime(); + runtime.setHealthy(true); + expect(runtime.isHealthy()).toBe(true); + + const handle = await runtime.ensureSession(makeEnsureInput()); + + const notFoundErr = new Error("Runtime not found"); + notFoundErr.name = "ResourceNotFoundException"; + + mockSend.mockResolvedValueOnce({ records: [] }).mockRejectedValueOnce(notFoundErr); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + const errEvent = expectErrorEvent(events[0]); + expect(errEvent.code).toBe("RESOURCE_NOT_FOUND"); + expect(runtime.isHealthy()).toBe(false); + }); + + it("silently returns when signal is aborted during invocation", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + const abortController = new AbortController(); + abortController.abort(); + + mockSend + .mockResolvedValueOnce({ records: [] }) + .mockRejectedValueOnce(new DOMException("Aborted", "AbortError")); + + const events = await collectEvents( + runtime.runTurn(makeTurnInput(handle, { signal: abortController.signal })), + ); + + expect(events).toHaveLength(0); + }); + + it("loads tenant context when Hyperion runtime is available", async () => { + const mockDbClient = { + getTenantConfig: vi.fn().mockResolvedValue({ + user_id: "user1", + display_name: "Test User", + model: "anthropic.claude-sonnet-4-20250514", + custom_instructions: "Be helpful", + tools: [], + profile: {}, + plan: "pro", + }), + }; + + vi.mocked(hasHyperionRuntime).mockReturnValue(true); + vi.mocked(getHyperionRuntime).mockReturnValue({ dbClient: mockDbClient } as ReturnType< + typeof getHyperionRuntime + >); + + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Hi!" }), + }, + }) // InvokeAgentRuntime + .mockResolvedValueOnce({}); // extractMemory + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle, { text: "Hello" }))); + + expect(mockDbClient.getTenantConfig).toHaveBeenCalledWith("user1", "main"); + expect(events[0].type).toBe("text_delta"); + }); + + it("includes memory records in invocation payload when available", async () => { + vi.mocked(hasHyperionRuntime).mockReturnValue(false); + + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ + records: [ + { content: { text: "User likes coffee" }, score: 0.95 }, + { content: { text: "User is a developer" }, score: 0.88 }, + ], + }) // retrieveMemory + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Got it!" }), + }, + }) // InvokeAgentRuntime + .mockResolvedValueOnce({}); // extractMemory + + await collectEvents(runtime.runTurn(makeTurnInput(handle, { text: "Tell me about myself" }))); + + // Verify InvokeAgentRuntimeCommand was called (second send call) + expect(mockSend).toHaveBeenCalledTimes(3); + const invokeCall = mockSend.mock.calls[1][0]; + const payload = JSON.parse(new TextDecoder().decode(invokeCall.input.payload)); + expect(payload.memory_context).toHaveLength(2); + expect(payload.memory_context[0].content).toBe("User likes coffee"); + }); + + it("fires memory extraction after a turn with response text", async () => { + vi.mocked(hasHyperionRuntime).mockReturnValue(false); + + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Memory-worthy response" }), + }, + }) // InvokeAgentRuntime + .mockResolvedValueOnce({}); // extractMemory + + await collectEvents(runtime.runTurn(makeTurnInput(handle, { text: "Important message" }))); + + // Wait a tick for fire-and-forget to execute + await new Promise((r) => setTimeout(r, 10)); + + // Third send call should be StartMemoryExtractionJobCommand + expect(mockSend).toHaveBeenCalledTimes(3); + const extractCall = mockSend.mock.calls[2][0]; + expect(extractCall.input.namespace).toBe("tenant_user1:main"); + expect(extractCall.input.content.text).toContain("Important message"); + expect(extractCall.input.content.text).toContain("Memory-worthy response"); + }); + + it("uses correct memory namespace with agentId from session key", async () => { + vi.mocked(hasHyperionRuntime).mockReturnValue(false); + + const runtime = createRuntime(); + // Session key with agentId "work" + const handle = await runtime.ensureSession( + makeEnsureInput({ sessionKey: "tenant_user1:work:slack:U111" }), + ); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Reply" }), + }, + }) + .mockResolvedValueOnce({}); // extractMemory + + await collectEvents(runtime.runTurn(makeTurnInput(handle, { text: "test" }))); + + await new Promise((r) => setTimeout(r, 10)); + + // Memory namespace should be "tenant_user1:work" + const extractCall = mockSend.mock.calls[2][0]; + expect(extractCall.input.namespace).toBe("tenant_user1:work"); + }); + + it("handles non-JSON response body as raw text", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) + .mockResolvedValueOnce({ + response: { + transformToString: async () => "Plain text response", + }, + }) + .mockResolvedValueOnce({}); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events[0]).toEqual({ + type: "text_delta", + text: "Plain text response", + stream: "output", + }); + }); + + it("handles response with no response body (yields done)", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend.mockResolvedValueOnce({ records: [] }).mockResolvedValueOnce({ + response: undefined, + }); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events).toEqual([{ type: "done" }]); + }); + + it("emits retryable error for 5xx status code", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend.mockResolvedValueOnce({ records: [] }).mockResolvedValueOnce({ + response: { transformToString: async () => "error" }, + statusCode: 503, + }); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events[0]).toMatchObject({ + type: "error", + retryable: true, + }); + const errEvent = expectErrorEvent(events[0]); + expect(errEvent.message).toContain("503"); + }); + + it("emits non-retryable error for 4xx status code", async () => { + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend.mockResolvedValueOnce({ records: [] }).mockResolvedValueOnce({ + response: { transformToString: async () => "error" }, + statusCode: 400, + }); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events[0]).toMatchObject({ + type: "error", + retryable: false, + }); + }); + + it("continues without tenant context when Hyperion runtime throws", async () => { + vi.mocked(hasHyperionRuntime).mockReturnValue(true); + vi.mocked(getHyperionRuntime).mockReturnValue({ + dbClient: { + getTenantConfig: vi.fn().mockRejectedValue(new Error("DDB timeout")), + }, + } as ReturnType); + + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockResolvedValueOnce({ records: [] }) // retrieveMemory + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Still works" }), + }, + }) + .mockResolvedValueOnce({}); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + // Should still get a response despite tenant context failure + expect(events[0]).toMatchObject({ type: "text_delta", text: "Still works" }); + }); + + it("continues without memory when retrieveMemory throws", async () => { + vi.mocked(hasHyperionRuntime).mockReturnValue(false); + + const runtime = createRuntime(); + const handle = await runtime.ensureSession(makeEnsureInput()); + + mockSend + .mockRejectedValueOnce(new Error("Memory service down")) // retrieveMemory fails + .mockResolvedValueOnce({ + response: { + transformToString: async () => JSON.stringify({ response: "Works anyway" }), + }, + }) + .mockResolvedValueOnce({}); + + const events = await collectEvents(runtime.runTurn(makeTurnInput(handle))); + + expect(events[0]).toMatchObject({ type: "text_delta", text: "Works anyway" }); + }); + }); + + // ----------------------------------------------------------------------- + // handleInvocationError — error classification (via private access) + // ----------------------------------------------------------------------- + + describe("handleInvocationError (error classification)", () => { + function collectErrors( + runtime: AgentCoreRuntime, + err: unknown, + ): Array<{ type: string; code?: string; message?: string; retryable?: boolean }> { + const events: Array<{ type: string; code?: string; message?: string; retryable?: boolean }> = + []; + // Access private method via bracket notation for testing + for (const event of runtime["handleInvocationError"](err)) { + events.push(event); + } + return events; + } + + it("classifies ThrottlingException as RATE_LIMITED and retryable", () => { + const runtime = createRuntime(); + const err = new Error("Rate exceeded"); + err.name = "ThrottlingException"; + + const events = collectErrors(runtime, err); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "error", + code: "RATE_LIMITED", + retryable: true, + }); + }); + + it("classifies 'Too Many Requests' message as RATE_LIMITED", () => { + const runtime = createRuntime(); + const events = collectErrors(runtime, new Error("Too Many Requests")); + expect(events[0]).toMatchObject({ code: "RATE_LIMITED", retryable: true }); + }); + + it("classifies ServiceUnavailableException as SERVICE_UNAVAILABLE", () => { + const runtime = createRuntime(); + const err = new Error("Service unavailable"); + err.name = "ServiceUnavailableException"; + + const events = collectErrors(runtime, err); + expect(events[0]).toMatchObject({ code: "SERVICE_UNAVAILABLE", retryable: true }); + }); + + it("classifies ResourceNotFoundException and sets unhealthy", () => { + const runtime = createRuntime(); + runtime.setHealthy(true); + const err = new Error("Not found"); + err.name = "ResourceNotFoundException"; + + const events = collectErrors(runtime, err); + expect(events[0]).toMatchObject({ code: "RESOURCE_NOT_FOUND" }); + expect(runtime.isHealthy()).toBe(false); + }); + + it("classifies unknown errors as generic (no code, not retryable)", () => { + const runtime = createRuntime(); + const events = collectErrors(runtime, new Error("Something unexpected")); + expect(events).toHaveLength(1); + expect(events[0].type).toBe("error"); + expect(events[0].code).toBeUndefined(); + expect(events[0].retryable).toBeUndefined(); + expect(events[0].message).toContain("Something unexpected"); + }); + }); + + // ----------------------------------------------------------------------- + // processResponse (via private access) + // ----------------------------------------------------------------------- + + describe("processResponse (indirect)", () => { + interface MockResponse { + response?: { transformToString: () => Promise }; + statusCode?: number; + } + + async function collectResponseEvents( + runtime: AgentCoreRuntime, + response: MockResponse, + state: AgentCoreHandleState, + ) { + return collectEvents(runtime["processResponse"](response, state)); + } + + it("parses JSON with 'text' field", async () => { + const runtime = createRuntime(); + const events = await collectResponseEvents( + runtime, + { response: { transformToString: async () => JSON.stringify({ text: "Text reply" }) } }, + SAMPLE_STATE, + ); + expect(events).toEqual([ + { type: "text_delta", text: "Text reply", stream: "output" }, + { type: "done" }, + ]); + }); + + it("parses JSON with 'message' field", async () => { + const runtime = createRuntime(); + const events = await collectResponseEvents( + runtime, + { response: { transformToString: async () => JSON.stringify({ message: "Msg reply" }) } }, + SAMPLE_STATE, + ); + expect(events).toEqual([ + { type: "text_delta", text: "Msg reply", stream: "output" }, + { type: "done" }, + ]); + }); + + it("emits error when transformToString throws", async () => { + const runtime = createRuntime(); + const events = await collectResponseEvents( + runtime, + { + response: { + transformToString: async () => { + throw new Error("Stream interrupted"); + }, + }, + }, + SAMPLE_STATE, + ); + expect(events[0]).toMatchObject({ + type: "error", + message: expect.stringContaining("Stream interrupted"), + }); + }); + }); +}); diff --git a/extensions/agentcore/src/runtime.ts b/extensions/agentcore/src/runtime.ts new file mode 100644 index 0000000000000..8ecfeea69d10f --- /dev/null +++ b/extensions/agentcore/src/runtime.ts @@ -0,0 +1,509 @@ +import crypto from "node:crypto"; +import { + BedrockAgentCoreClient, + InvokeAgentRuntimeCommand, + StopRuntimeSessionCommand, + RetrieveMemoryRecordsCommand, + StartMemoryExtractionJobCommand, +} from "@aws-sdk/client-bedrock-agentcore"; +import type { + AcpRuntime, + AcpRuntimeCapabilities, + AcpRuntimeDoctorReport, + AcpRuntimeEnsureInput, + AcpRuntimeEvent, + AcpRuntimeHandle, + AcpRuntimeStatus, + AcpRuntimeTurnInput, +} from "openclaw/plugin-sdk/acpx"; +import { AcpRuntimeError } from "openclaw/plugin-sdk/acpx"; +import { extractTenantId, extractAgentId } from "../../../src/hyperion/session-manager.js"; +import { DEFAULT_AGENT_ID } from "../../../src/hyperion/types.js"; +import { hasHyperionRuntime, getHyperionRuntime } from "../../hyperion/src/globals.js"; +import type { AgentCoreHandleState, AgentCoreRuntimeConfig } from "./types.js"; + +export const AGENTCORE_BACKEND_ID = "agentcore"; + +const HANDLE_PREFIX = "agentcore:v1:"; +const DEFAULT_INVOKE_TIMEOUT_MS = 300_000; // 5 minutes + +const AGENTCORE_CAPABILITIES: AcpRuntimeCapabilities = { + // AgentCore doesn't expose OC-style session controls + controls: [], +}; + +// --------------------------------------------------------------------------- +// Handle state encoding (persisted in AcpRuntimeHandle.runtimeSessionName) +// --------------------------------------------------------------------------- + +function encodeHandleState(state: AgentCoreHandleState): string { + return `${HANDLE_PREFIX}${Buffer.from(JSON.stringify(state), "utf8").toString("base64url")}`; +} + +function decodeHandleState(runtimeSessionName: string): AgentCoreHandleState | null { + if (!runtimeSessionName.startsWith(HANDLE_PREFIX)) { + return null; + } + try { + const encoded = runtimeSessionName.slice(HANDLE_PREFIX.length); + return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as AgentCoreHandleState; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// Runtime ARN selection +// --------------------------------------------------------------------------- + +/** + * Pick a runtime ARN from the configured pool. + * Simple random selection; can be replaced with health-aware routing. + */ +function pickRuntimeArn(arns: string[]): string { + if (arns.length === 0) { + throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "No AgentCore Runtime ARNs configured."); + } + if (arns.length === 1) { + return arns[0]!; + } + return arns[Math.floor(Math.random() * arns.length)]!; +} + +// --------------------------------------------------------------------------- +// AgentCore AcpRuntime implementation +// --------------------------------------------------------------------------- + +export class AgentCoreRuntime implements AcpRuntime { + private healthy = false; + private readonly client: BedrockAgentCoreClient; + private readonly config: AgentCoreRuntimeConfig; + + constructor(config: AgentCoreRuntimeConfig) { + this.config = config; + this.client = new BedrockAgentCoreClient({ + region: config.region, + ...(config.endpoint ? { endpoint: config.endpoint } : {}), + }); + } + + isHealthy(): boolean { + return this.healthy; + } + + setHealthy(value: boolean): void { + this.healthy = value; + } + + // ------------------------------------------------------------------------- + // ensureSession — creates session state for subsequent runTurn calls + // ------------------------------------------------------------------------- + + async ensureSession(input: AcpRuntimeEnsureInput): Promise { + const agent = input.agent?.trim(); + if (!agent) { + throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Agent ID is required."); + } + const sessionKey = input.sessionKey?.trim(); + if (!sessionKey) { + throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Session key is required."); + } + + const runtimeArn = pickRuntimeArn(this.config.runtimeArns); + + // For Hyperion, the OC agentId IS the tenant user_id. + const tenantId = agent; + // [claude-infra] Multi-instance: extract agent instance ID from session key. + const agentId = extractAgentId(sessionKey); + + // AgentCore sessions are created implicitly on first InvokeAgentRuntime. + // We generate a stable session ID here. For resumed sessions, reuse the + // provided ID so AgentCore picks up the existing conversation. + const sessionId = input.resumeSessionId || crypto.randomUUID(); + + const state: AgentCoreHandleState = { + runtimeArn, + sessionId, + tenantId, + agentId, + agent, + mode: input.mode, + }; + + return { + sessionKey, + backend: AGENTCORE_BACKEND_ID, + runtimeSessionName: encodeHandleState(state), + cwd: input.cwd, + backendSessionId: sessionId, + }; + } + + // ------------------------------------------------------------------------- + // runTurn — invokes AgentCore and streams AcpRuntimeEvents back + // ------------------------------------------------------------------------- + + async *runTurn(input: AcpRuntimeTurnInput): AsyncIterable { + const state = this.resolveHandleState(input.handle); + // [claude-infra] Multi-instance: memory namespace includes agentId for isolation. + // Uses configurable prefix (memoryNamespacePrefix from SSM) rather than + // buildTenantMemoryNamespace() which hardcodes "tenant_". + const memoryNamespace = `${this.config.memoryNamespacePrefix}${state.tenantId}:${state.agentId}`; + + // Load tenant config and retrieve memory in parallel. + // Tenant config provides model, tools, custom_instructions, profile. + // Memory provides prior conversation context for the agent. + const [tenantContext, memoryRecords] = await Promise.all([ + this.loadTenantContext(state.tenantId, state.agentId), + this.retrieveMemory(memoryNamespace, input.text), + ]); + + const payload: Record = { + sessionId: state.sessionId, + tenant_id: state.tenantId, + message: input.text, + ...(tenantContext ? { tenant_config: tenantContext } : {}), + ...(memoryRecords.length > 0 ? { memory_context: memoryRecords } : {}), + }; + + const payloadBytes = new TextEncoder().encode(JSON.stringify(payload)); + + let response; + try { + response = await this.client.send( + new InvokeAgentRuntimeCommand({ + agentRuntimeArn: state.runtimeArn, + runtimeSessionId: state.sessionId, + runtimeUserId: state.tenantId, + contentType: "application/json", + accept: "application/json", + payload: payloadBytes, + }), + { + abortSignal: input.signal, + requestTimeout: this.config.invokeTimeoutMs ?? DEFAULT_INVOKE_TIMEOUT_MS, + }, + ); + } catch (err) { + if (input.signal?.aborted) { + return; + } + yield* this.handleInvocationError(err); + return; + } + + // Process the response stream and collect the full text for memory extraction. + let fullResponseText = ""; + for await (const event of this.processResponse(response, state)) { + if (event.type === "text_delta" && event.text) { + fullResponseText += event.text; + } + yield event; + } + + // Fire-and-forget: extract memories from this turn for future context. + if (fullResponseText) { + void this.extractMemory(memoryNamespace, input.text, fullResponseText); + } + } + + // ------------------------------------------------------------------------- + // cancel / close — session lifecycle + // ------------------------------------------------------------------------- + + async cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise { + // Best-effort: stop the runtime session so AgentCore tears down the microVM. + const state = this.resolveHandleState(input.handle); + try { + await this.client.send( + new StopRuntimeSessionCommand({ + agentRuntimeArn: state.runtimeArn, + runtimeSessionId: state.sessionId, + }), + ); + } catch { + // Swallow errors — cancel is best-effort + } + } + + async close(input: { handle: AcpRuntimeHandle; reason: string }): Promise { + // For oneshot sessions, stop the runtime session. + // For persistent sessions, leave it alive for future turns. + const state = this.resolveHandleState(input.handle); + if (state.mode === "oneshot") { + try { + await this.client.send( + new StopRuntimeSessionCommand({ + agentRuntimeArn: state.runtimeArn, + runtimeSessionId: state.sessionId, + }), + ); + } catch { + // Best-effort cleanup + } + } + } + + // ------------------------------------------------------------------------- + // getCapabilities / getStatus / doctor + // ------------------------------------------------------------------------- + + getCapabilities(): AcpRuntimeCapabilities { + return AGENTCORE_CAPABILITIES; + } + + async getStatus(input: { + handle: AcpRuntimeHandle; + signal?: AbortSignal; + }): Promise { + const state = this.resolveHandleState(input.handle); + return { + summary: `agentcore session=${state.sessionId} runtime=${state.runtimeArn} tenant=${state.tenantId}`, + backendSessionId: state.sessionId, + }; + } + + async doctor(): Promise { + if (this.config.runtimeArns.length === 0) { + return { + ok: false, + code: "ACP_BACKEND_UNAVAILABLE", + message: + "No AgentCore Runtime ARNs configured. " + + "Populate SSM parameter /hyperion/{stage}/agentcore/runtime-arns.", + }; + } + + // Lightweight check: try to describe the first runtime + try { + // TODO: replace with a proper health ping when AgentCore exposes one. + // For now, we just validate config is present. + return { + ok: true, + message: + `AgentCore backend configured ` + + `(region: ${this.config.region}, runtimes: ${this.config.runtimeArns.length})`, + }; + } catch (err) { + return { + ok: false, + code: "ACP_BACKEND_UNAVAILABLE", + message: err instanceof Error ? err.message : String(err), + }; + } + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + /** + * Load tenant config from the Hyperion runtime (DynamoDB). + * Returns a subset of the config relevant to the agent container: + * model, custom_instructions, tools, profile, display_name. + */ + // [claude-infra] Multi-instance: loads config for specific agent instance. + private async loadTenantContext( + tenantId: string, + agentId: string = DEFAULT_AGENT_ID, + ): Promise | null> { + if (!hasHyperionRuntime()) return null; + try { + const runtime = getHyperionRuntime(); + const tenantConfig = await runtime.dbClient.getTenantConfig(tenantId, agentId); + if (!tenantConfig) return null; + return { + user_id: tenantConfig.user_id, + display_name: tenantConfig.display_name, + model: tenantConfig.model ?? this.config.defaultModel, + custom_instructions: tenantConfig.custom_instructions, + tools: tenantConfig.tools, + profile: tenantConfig.profile, + plan: tenantConfig.plan, + }; + } catch { + // Non-fatal: agent can still run without tenant context + return null; + } + } + + /** + * Retrieve relevant memory records for this tenant before the turn. + * Uses the user's message as a semantic query to find related memories. + */ + private async retrieveMemory( + namespace: string, + query: string, + ): Promise> { + try { + const resp = await this.client.send( + new RetrieveMemoryRecordsCommand({ + namespace, + query: { text: query }, + maxResults: 10, + }), + ); + if (!resp.records || resp.records.length === 0) return []; + return resp.records.map((r) => ({ + content: r.content?.text ?? "", + score: r.score, + })); + } catch { + // Non-fatal: agent runs without memory context on failure + return []; + } + } + + /** + * Extract and persist memories from a completed turn (fire-and-forget). + * AgentCore Memory will extract salient facts from the conversation. + */ + private async extractMemory( + namespace: string, + userMessage: string, + agentResponse: string, + ): Promise { + try { + await this.client.send( + new StartMemoryExtractionJobCommand({ + namespace, + content: { + text: `User: ${userMessage}\nAssistant: ${agentResponse}`, + }, + }), + ); + } catch { + // Best-effort: memory extraction failure is not user-facing + } + } + + private resolveHandleState(handle: AcpRuntimeHandle): AgentCoreHandleState { + const state = decodeHandleState(handle.runtimeSessionName); + if (!state) { + throw new AcpRuntimeError( + "ACP_SESSION_INIT_FAILED", + "Invalid AgentCore runtime handle: could not decode state.", + ); + } + return state; + } + + /** + * Process the InvokeAgentRuntime response. + * + * The response.response is a StreamingBlob — we consume it as text and + * parse the agent's output. The agent container returns JSON with a + * "response" field containing the agent's reply text. + * + * For streaming: the response blob may arrive in chunks. We emit + * text_delta events as data arrives, then a done event at the end. + */ + private async *processResponse( + response: { + response?: { transformToString(): Promise } | undefined; + runtimeSessionId?: string; + statusCode?: number; + }, + state: AgentCoreHandleState, + ): AsyncIterable { + if (!response.response) { + yield { type: "done" }; + return; + } + + if (response.statusCode && response.statusCode >= 400) { + yield { + type: "error", + message: `AgentCore returned status ${response.statusCode}`, + retryable: response.statusCode >= 500, + }; + return; + } + + try { + const body = await response.response.transformToString(); + + if (!body.trim()) { + yield { type: "done" }; + return; + } + + // Try to parse as JSON (agent container format: { response: "..." }) + let text: string; + try { + const parsed = JSON.parse(body); + text = + typeof parsed.response === "string" + ? parsed.response + : typeof parsed.text === "string" + ? parsed.text + : typeof parsed.message === "string" + ? parsed.message + : body; + } catch { + // Not JSON — treat the raw body as the agent's text response + text = body; + } + + if (text) { + yield { + type: "text_delta", + text, + stream: "output", + }; + } + + yield { type: "done" }; + } catch (err) { + yield { + type: "error", + message: `Failed to read AgentCore response: ${err instanceof Error ? err.message : String(err)}`, + }; + } + } + + /** + * Map AgentCore invocation errors to AcpRuntimeEvents. + */ + private *handleInvocationError(err: unknown): Iterable { + const message = err instanceof Error ? err.message : String(err); + const errName = err instanceof Error ? err.name : ""; + + const isThrottled = + errName === "ThrottlingException" || + message.includes("ThrottlingException") || + message.includes("Too Many Requests"); + const isTransient = + errName === "ServiceUnavailableException" || + errName === "InternalServerException" || + message.includes("ServiceUnavailable") || + message.includes("InternalServer"); + const isNotFound = + errName === "ResourceNotFoundException" || message.includes("ResourceNotFound"); + + if (isNotFound) { + this.healthy = false; + yield { + type: "error", + message: `AgentCore Runtime not found. Check runtime ARN configuration. ${message}`, + code: "RESOURCE_NOT_FOUND", + }; + return; + } + + if (isThrottled || isTransient) { + yield { + type: "error", + message: `AgentCore invocation failed: ${message}`, + code: isThrottled ? "RATE_LIMITED" : "SERVICE_UNAVAILABLE", + retryable: true, + }; + return; + } + + yield { + type: "error", + message: `AgentCore invocation failed: ${message}`, + }; + } +} diff --git a/extensions/agentcore/src/service.ts b/extensions/agentcore/src/service.ts new file mode 100644 index 0000000000000..9bbfd2f7db107 --- /dev/null +++ b/extensions/agentcore/src/service.ts @@ -0,0 +1,90 @@ +import type { + AcpRuntime, + OpenClawPluginService, + OpenClawPluginServiceContext, +} from "openclaw/plugin-sdk/acpx"; +import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/acpx"; +import { loadAgentCoreConfig, type AgentCoreConfigSource } from "./config.js"; +import { AGENTCORE_BACKEND_ID, AgentCoreRuntime } from "./runtime.js"; + +type AgentCoreRuntimeLike = AcpRuntime & { + isHealthy(): boolean; + setHealthy(value: boolean): void; + doctor(): Promise<{ ok: boolean; message: string }>; +}; + +export type CreateAgentCoreServiceParams = { + configSource: AgentCoreConfigSource; +}; + +/** + * OpenClaw plugin service that registers the AgentCore ACP backend. + * + * Usage in gateway startup: + * const service = createAgentCoreRuntimeService({ + * configSource: { + * ssmPrefix: `/hyperion/${stage}/agentcore`, + * region: "us-west-2", + * }, + * }); + * await service.start(ctx); + * + * This registers "agentcore" as an ACP runtime backend. When OC dispatches + * a message via ACP (e.g. from external channel webhooks), it flows through + * AgentCoreRuntime.runTurn() which invokes Bedrock AgentCore. + */ +export function createAgentCoreRuntimeService( + params: CreateAgentCoreServiceParams, +): OpenClawPluginService { + let runtime: AgentCoreRuntimeLike | null = null; + + return { + id: "agentcore-runtime", + + async start(ctx: OpenClawPluginServiceContext): Promise { + const config = await loadAgentCoreConfig(params.configSource); + + if (config.runtimeArns.length === 0) { + ctx.logger.warn( + "AgentCore runtime backend has no runtime ARNs configured. " + + "Backend will be registered but unhealthy until SSM parameter is populated.", + ); + } + + runtime = new AgentCoreRuntime(config); + + registerAcpRuntimeBackend({ + id: AGENTCORE_BACKEND_ID, + runtime, + healthy: () => runtime?.isHealthy() ?? false, + }); + + ctx.logger.info( + `AgentCore runtime backend registered (region: ${config.region}, ` + + `runtimes: ${config.runtimeArns.length}, model: ${config.defaultModel})`, + ); + + // Probe health in background + void (async () => { + try { + const report = await runtime?.doctor(); + if (report?.ok) { + runtime?.setHealthy(true); + ctx.logger.info("AgentCore runtime backend ready"); + } else { + ctx.logger.warn(`AgentCore runtime backend probe failed: ${report?.message}`); + } + } catch (err) { + ctx.logger.warn( + `AgentCore runtime health check failed: ${err instanceof Error ? err.message : String(err)}`, + ); + } + })(); + }, + + async stop(_ctx: OpenClawPluginServiceContext): Promise { + unregisterAcpRuntimeBackend(AGENTCORE_BACKEND_ID); + runtime = null; + }, + }; +} diff --git a/extensions/agentcore/src/types.ts b/extensions/agentcore/src/types.ts new file mode 100644 index 0000000000000..ecbe8808d6f0c --- /dev/null +++ b/extensions/agentcore/src/types.ts @@ -0,0 +1,37 @@ +/** + * Configuration for the AgentCore runtime backend. + * Loaded from SSM parameters at startup (see CDK AgentCoreConstruct). + */ +export type AgentCoreRuntimeConfig = { + /** AWS region for AgentCore API calls. */ + region: string; + /** AgentCore Runtime ARNs for load distribution. */ + runtimeArns: string[]; + /** Prefix for per-tenant memory namespacing (e.g. "tenant_"). */ + memoryNamespacePrefix: string; + /** Default Bedrock model ID (e.g. "anthropic.claude-sonnet-4-20250514"). */ + defaultModel: string; + /** AgentCore endpoint override (for local testing). */ + endpoint?: string; + /** Timeout for agent invocations in milliseconds. Default 300_000 (5 min). */ + invokeTimeoutMs?: number; +}; + +/** + * Internal handle state encoded into AcpRuntimeHandle.runtimeSessionName. + * Tracks the AgentCore session identity for subsequent turns. + */ +export type AgentCoreHandleState = { + /** AgentCore Runtime ARN used for this session. */ + runtimeArn: string; + /** Session ID passed as runtimeSessionId to AgentCore. */ + sessionId: string; + /** Tenant/user ID — used as runtimeUserId for per-tenant isolation. */ + tenantId: string; + /** Agent instance ID within the tenant. Default: "main". [claude-infra] */ + agentId: string; + /** Agent identifier from OC's session key. */ + agent: string; + /** Session mode. */ + mode: "persistent" | "oneshot"; +}; diff --git a/extensions/hyperion/index.ts b/extensions/hyperion/index.ts new file mode 100644 index 0000000000000..cf2021c920b73 --- /dev/null +++ b/extensions/hyperion/index.ts @@ -0,0 +1,15 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import { createHyperionPluginService, type HyperionPluginConfig } from "./src/service.js"; + +const plugin = { + id: "hyperion", + name: "Hyperion Multi-Tenant Runtime", + description: "Multi-tenant DynamoDB integration for the Nova Personal Assistant Platform.", + register(api: OpenClawPluginApi) { + const pluginConfig = (api.pluginConfig ?? {}) as HyperionPluginConfig; + + api.registerService(createHyperionPluginService(pluginConfig)); + }, +}; + +export default plugin; diff --git a/extensions/hyperion/openclaw.plugin.json b/extensions/hyperion/openclaw.plugin.json new file mode 100644 index 0000000000000..73aeac442a4e8 --- /dev/null +++ b/extensions/hyperion/openclaw.plugin.json @@ -0,0 +1,24 @@ +{ + "id": "hyperion", + "name": "Hyperion Multi-Tenant Runtime", + "description": "Multi-tenant DynamoDB integration for the Nova Personal Assistant Platform. Provides per-tenant config loading, channel identity resolution, credential encryption, and pairing.", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "stage": { + "type": "string", + "enum": ["beta", "gamma", "prod"], + "description": "Deployment stage (derives table names and KMS key alias)" + }, + "region": { + "type": "string", + "description": "AWS region for DynamoDB and KMS" + }, + "dynamoEndpoint": { + "type": "string", + "description": "DynamoDB endpoint override (for local development)" + } + } + } +} diff --git a/extensions/hyperion/package.json b/extensions/hyperion/package.json new file mode 100644 index 0000000000000..f2f934b476707 --- /dev/null +++ b/extensions/hyperion/package.json @@ -0,0 +1,19 @@ +{ + "name": "@openclaw/hyperion", + "version": "2026.3.10", + "description": "Hyperion multi-tenant runtime for Nova Personal Assistant Platform", + "type": "module", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.0.0", + "@aws-sdk/client-kms": "^3.0.0", + "@aws-sdk/lib-dynamodb": "^3.0.0" + }, + "devDependencies": { + "openclaw": "workspace:*" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/hyperion/src/env.ts b/extensions/hyperion/src/env.ts new file mode 100644 index 0000000000000..68feb5ad05742 --- /dev/null +++ b/extensions/hyperion/src/env.ts @@ -0,0 +1,41 @@ +import type { HyperionDynamoDBConfig } from "../../../src/hyperion/types.js"; + +/** + * Resolve the Hyperion stage from environment or plugin config. + * + * Priority: + * 1. Plugin config `stage` field + * 2. `HYPERION_STAGE` env var + * 3. Derived from `STACK_NAME` env var (e.g. "Hyperion-beta" → "beta") + */ +export function resolveStage(pluginStage?: string): string | null { + if (pluginStage) return pluginStage; + if (process.env.HYPERION_STAGE) return process.env.HYPERION_STAGE; + const stackName = process.env.STACK_NAME; + if (stackName?.startsWith("Hyperion-")) { + return stackName.replace("Hyperion-", ""); + } + return null; +} + +/** + * Build DynamoDB config from stage name. + * Table names and KMS key alias follow the CDK naming convention. + */ +export function buildDynamoConfig(params: { + stage: string; + region: string; + endpoint?: string; +}): HyperionDynamoDBConfig { + const { stage, region, endpoint } = params; + return { + region, + tenantConfigTableName: `Hyperion-${stage}-tenant-config`, + channelConfigTableName: `Hyperion-${stage}-channel-config`, + pairingCodesTableName: `Hyperion-${stage}-pairing-codes`, + userCredentialsTableName: `Hyperion-${stage}-user-credentials`, + credentialsKmsKeyId: `alias/hyperion-${stage}-credentials`, + channelConfigUserIdIndexName: "user_id-index", + ...(endpoint ? { endpoint } : {}), + }; +} diff --git a/extensions/hyperion/src/globals.ts b/extensions/hyperion/src/globals.ts new file mode 100644 index 0000000000000..0d49818feaa04 --- /dev/null +++ b/extensions/hyperion/src/globals.ts @@ -0,0 +1,38 @@ +import type { HyperionRuntime } from "../../../src/hyperion/index.js"; + +let hyperionRuntime: HyperionRuntime | null = null; + +/** + * Set the global Hyperion runtime instance. + * Called by the Hyperion plugin service on startup. + */ +export function setHyperionRuntime(runtime: HyperionRuntime): void { + hyperionRuntime = runtime; +} + +/** + * Get the global Hyperion runtime instance. + * Throws if the Hyperion plugin has not started yet. + */ +export function getHyperionRuntime(): HyperionRuntime { + if (!hyperionRuntime) { + throw new Error( + "Hyperion runtime not initialized. Ensure the hyperion plugin is enabled and started.", + ); + } + return hyperionRuntime; +} + +/** + * Check if the Hyperion runtime is available (non-throwing). + */ +export function hasHyperionRuntime(): boolean { + return hyperionRuntime !== null; +} + +/** + * Clear the global Hyperion runtime (for shutdown/testing). + */ +export function clearHyperionRuntime(): void { + hyperionRuntime = null; +} diff --git a/extensions/hyperion/src/index.ts b/extensions/hyperion/src/index.ts new file mode 100644 index 0000000000000..c771ca330e4f5 --- /dev/null +++ b/extensions/hyperion/src/index.ts @@ -0,0 +1,8 @@ +export { + getHyperionRuntime, + hasHyperionRuntime, + setHyperionRuntime, + clearHyperionRuntime, +} from "./globals.js"; +export { createHyperionPluginService, type HyperionPluginConfig } from "./service.js"; +export { resolveStage, buildDynamoConfig } from "./env.js"; diff --git a/extensions/hyperion/src/service.ts b/extensions/hyperion/src/service.ts new file mode 100644 index 0000000000000..ade87eab3d4d2 --- /dev/null +++ b/extensions/hyperion/src/service.ts @@ -0,0 +1,81 @@ +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { KMSClient } from "@aws-sdk/client-kms"; +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import type { OpenClawPluginService, OpenClawPluginServiceContext } from "openclaw/plugin-sdk"; +import { createHyperionRuntime } from "../../../src/hyperion/index.js"; +import { buildDynamoConfig, resolveStage } from "./env.js"; +import { clearHyperionRuntime, setHyperionRuntime } from "./globals.js"; + +export type HyperionPluginConfig = { + stage?: string; + region?: string; + dynamoEndpoint?: string; +}; + +/** + * OC plugin service that creates the Hyperion multi-tenant runtime on startup. + * + * On start: + * 1. Resolves stage from plugin config / env vars + * 2. Creates AWS SDK clients (DynamoDB, KMS) + * 3. Calls createHyperionRuntime() to wire all services + * 4. Stores the runtime globally via setHyperionRuntime() + * + * On stop: + * Clears the global runtime reference. + * + * Other plugins and channel handlers access it via: + * import { getHyperionRuntime } from "extensions/hyperion/src/globals.js"; + */ +export function createHyperionPluginService( + pluginConfig: HyperionPluginConfig, +): OpenClawPluginService { + return { + id: "hyperion-runtime", + + async start(ctx: OpenClawPluginServiceContext): Promise { + const stage = resolveStage(pluginConfig.stage); + if (!stage) { + ctx.logger.warn( + "Hyperion plugin: cannot determine stage. " + + "Set plugins.hyperion.stage, HYPERION_STAGE, or STACK_NAME env var.", + ); + return; + } + + const region = pluginConfig.region ?? process.env.AWS_REGION ?? "us-west-2"; + + const dynamoConfig = buildDynamoConfig({ + stage, + region, + endpoint: pluginConfig.dynamoEndpoint, + }); + + const ddbClient = new DynamoDBClient({ + region, + ...(pluginConfig.dynamoEndpoint ? { endpoint: pluginConfig.dynamoEndpoint } : {}), + }); + const docClient = DynamoDBDocumentClient.from(ddbClient, { + marshallOptions: { removeUndefinedValues: true }, + }); + const kmsClient = new KMSClient({ region }); + + const runtime = createHyperionRuntime({ + dynamoConfig, + docClient, + kmsClient, + }); + + setHyperionRuntime(runtime); + + ctx.logger.info( + `Hyperion runtime initialized (stage: ${stage}, region: ${region}, ` + + `tables: ${dynamoConfig.tenantConfigTableName}, ...)`, + ); + }, + + async stop(_ctx: OpenClawPluginServiceContext): Promise { + clearHyperionRuntime(); + }, + }; +} diff --git a/extensions/nova/src/monitor.ts b/extensions/nova/src/monitor.ts index 7fcdeec744526..13f969f6c7a29 100644 --- a/extensions/nova/src/monitor.ts +++ b/extensions/nova/src/monitor.ts @@ -6,12 +6,13 @@ import { type RuntimeEnv, } from "openclaw/plugin-sdk"; import WebSocket from "ws"; -import type { NovaConfig } from "./types.js"; +import { getHyperionRuntime, hasHyperionRuntime } from "../../hyperion/src/globals.js"; import { setActiveNovaConnection } from "./connection.js"; import { resolveNovaCredentials } from "./credentials.js"; import { parseNovaInboundMessage } from "./inbound.js"; import { getNovaRuntime } from "./runtime.js"; import { sendNovaMessage } from "./send.js"; +import type { NovaConfig } from "./types.js"; export type MonitorNovaOpts = { cfg: OpenClawConfig; @@ -158,7 +159,7 @@ export async function monitorNovaProvider(opts: MonitorNovaOpts): Promise async function handleInboundMessage( raw: string, - msgCfg: OpenClawConfig, + gatewayCfg: OpenClawConfig, msgRuntime: RuntimeEnv, ): Promise { const msg = parseNovaInboundMessage(raw); @@ -167,21 +168,37 @@ export async function monitorNovaProvider(opts: MonitorNovaOpts): Promise return; } - const dmPolicy = novaCfg?.dmPolicy ?? "allowlist"; - const allowFrom = (novaCfg?.allowFrom ?? []).map((entry) => - String(entry).trim().toLowerCase(), - ); - - // Enforce allowlist policy — an empty allowlist blocks everyone - if (dmPolicy === "allowlist" && !allowFrom.includes("*")) { - if (allowFrom.length === 0) { - logger.info(`nova: message from ${msg.userId} dropped (allowlist is empty)`); + // In multi-tenant mode (Hyperion runtime available), load per-tenant config. + // The tenant user_id is the msg.userId from the authenticated WebSocket. + // HWS has already authenticated the user, so no allowlist check needed. + let msgCfg: OpenClawConfig; + if (hasHyperionRuntime()) { + try { + const hyperion = getHyperionRuntime(); + msgCfg = await hyperion.configLoader.loadTenantConfig(msg.userId); + } catch (err) { + logger.error(`nova: failed to load tenant config for ${msg.userId}: ${String(err)}`); return; } - const senderId = msg.userId.trim().toLowerCase(); - if (!allowFrom.includes(senderId)) { - logger.info(`nova: message from ${msg.userId} dropped (not in allowlist)`); - return; + } else { + // Single-tenant fallback: use the static gateway config with allowlist check. + msgCfg = gatewayCfg; + + const dmPolicy = novaCfg?.dmPolicy ?? "allowlist"; + const allowFrom = (novaCfg?.allowFrom ?? []).map((entry) => + String(entry).trim().toLowerCase(), + ); + + if (dmPolicy === "allowlist" && !allowFrom.includes("*")) { + if (allowFrom.length === 0) { + logger.info(`nova: message from ${msg.userId} dropped (allowlist is empty)`); + return; + } + const senderId = msg.userId.trim().toLowerCase(); + if (!allowFrom.includes(senderId)) { + logger.info(`nova: message from ${msg.userId} dropped (not in allowlist)`); + return; + } } } @@ -195,7 +212,7 @@ export async function monitorNovaProvider(opts: MonitorNovaOpts): Promise peer: { kind: "user", id: msg.userId }, }); - const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { + const storePath = core.channel.session.resolveStorePath(msgCfg.session?.store, { agentId: route.agentId, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b3028f61eb10..fc1dc9d873548 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,6 +257,19 @@ importers: specifier: 0.1.16 version: 0.1.16(zod@4.3.6) + extensions/agentcore: + dependencies: + '@aws-sdk/client-bedrock-agentcore': + specifier: ^3.0.0 + version: 3.1006.0 + '@aws-sdk/client-ssm': + specifier: ^3.0.0 + version: 3.1006.0 + devDependencies: + openclaw: + specifier: workspace:* + version: link:../.. + extensions/bluebubbles: dependencies: zod: @@ -341,6 +354,22 @@ importers: specifier: '>=2026.3.7' version: 2026.3.8(@discordjs/opus@0.10.0)(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.12.5)(node-llama-cpp@3.16.2(typescript@5.9.3)) + extensions/hyperion: + dependencies: + '@aws-sdk/client-dynamodb': + specifier: ^3.0.0 + version: 3.1006.0 + '@aws-sdk/client-kms': + specifier: ^3.0.0 + version: 3.1006.0 + '@aws-sdk/lib-dynamodb': + specifier: ^3.0.0 + version: 3.1006.0(@aws-sdk/client-dynamodb@3.1006.0) + devDependencies: + openclaw: + specifier: workspace:* + version: link:../.. + extensions/imessage: {} extensions/irc: @@ -440,6 +469,19 @@ importers: specifier: ^4.3.6 version: 4.3.6 + extensions/nova: + dependencies: + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@types/ws': + specifier: ^8.5.14 + version: 8.18.1 + openclaw: + specifier: workspace:* + version: link:../.. + extensions/open-prose: {} extensions/signal: {} @@ -613,6 +655,10 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + '@aws-sdk/client-bedrock-agentcore@3.1006.0': + resolution: {integrity: sha512-9zAdV97zTUOels4Ziu9BqJPX5965QHxT5r2xR1TVihJIl3BVnxH3WyFgdEP2iqKrS2stiAgP3Dxs0/VoVNLWCw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-bedrock-runtime@3.1004.0': resolution: {integrity: sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw==} engines: {node: '>=20.0.0'} @@ -621,94 +667,124 @@ packages: resolution: {integrity: sha512-JbfZSV85IL+43S7rPBmeMbvoOYXs1wmrfbEpHkDBjkvbukRQWtoetiPAXNSKDfFq1qVsoq8sWPdoerDQwlUO8w==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-dynamodb@3.1006.0': + resolution: {integrity: sha512-PTGnTtTdOezfg7UsICBeS8BtxAxYfWI2/JNLflWjjryiJdHIl7aAprsHrIzlsD6qwQOloxhmwqp7C9JFY5BT2Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-kms@3.1006.0': + resolution: {integrity: sha512-6y8W0iE6VNP2QmHVdD8yOxOd1eolgimiKkyUrkeYCJ668u02kO4KVjXmS+uvUQ1vjpdHmP144whnYRrJBe1nEw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-s3@3.1000.0': resolution: {integrity: sha512-7kPy33qNGq3NfwHC0412T6LDK1bp4+eiPzetX0sVd9cpTSXuQDKpoOFnB0Njj6uZjJDcLS3n2OeyarwwgkQ0Ow==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.15': - resolution: {integrity: sha512-AlC0oQ1/mdJ8vCIqu524j5RB7M8i8E24bbkZmya1CuiQxkY7SdIZAyw7NDNMGaNINQFq/8oGRMX0HeOfCVsl/A==} + '@aws-sdk/client-ssm@3.1006.0': + resolution: {integrity: sha512-B+t09zsPL7/YJ6mcTTnBY1hMHkjGDGF4zefj77wWvIEOxs6sxR4echFmobSt6imtmxVvKo4n3mFGm3nRquTZ3g==} engines: {node: '>=20.0.0'} '@aws-sdk/core@3.973.18': resolution: {integrity: sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==} engines: {node: '>=20.0.0'} - '@aws-sdk/crc64-nvme@3.972.3': - resolution: {integrity: sha512-UExeK+EFiq5LAcbHm96CQLSia+5pvpUVSAsVApscBzayb7/6dJBJKwV4/onsk4VbWSmqxDMcfuTD+pC4RxgZHg==} + '@aws-sdk/core@3.973.19': + resolution: {integrity: sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.13': - resolution: {integrity: sha512-6ljXKIQ22WFKyIs1jbORIkGanySBHaPPTOI4OxACP5WXgbcR0nDYfqNJfXEGwCK7IzHdNbCSFsNKKs0qCexR8Q==} + '@aws-sdk/crc64-nvme@3.972.3': + resolution: {integrity: sha512-UExeK+EFiq5LAcbHm96CQLSia+5pvpUVSAsVApscBzayb7/6dJBJKwV4/onsk4VbWSmqxDMcfuTD+pC4RxgZHg==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-env@3.972.16': resolution: {integrity: sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.15': - resolution: {integrity: sha512-dJuSTreu/T8f24SHDNTjd7eQ4rabr0TzPh2UTCwYexQtzG3nTDKm1e5eIdhiroTMDkPEJeY+WPkA6F9wod/20A==} + '@aws-sdk/credential-provider-env@3.972.17': + resolution: {integrity: sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-http@3.972.18': resolution: {integrity: sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.13': - resolution: {integrity: sha512-JKSoGb7XeabZLBJptpqoZIFbROUIS65NuQnEHGOpuT9GuuZwag2qciKANiDLFiYk4u8nSrJC9JIOnWKVvPVjeA==} + '@aws-sdk/credential-provider-http@3.972.19': + resolution: {integrity: sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-ini@3.972.17': resolution: {integrity: sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.13': - resolution: {integrity: sha512-RtYcrxdnJHKY8MFQGLltCURcjuMjnaQpAxPE6+/QEdDHHItMKZgabRe/KScX737F9vJMQsmJy9EmMOkCnoC1JQ==} + '@aws-sdk/credential-provider-ini@3.972.18': + resolution: {integrity: sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-login@3.972.17': resolution: {integrity: sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.14': - resolution: {integrity: sha512-WqoC2aliIjQM/L3oFf6j+op/enT2i9Cc4UTxxMEKrJNECkq4/PlKE5BOjSYFcq6G9mz65EFbXJh7zOU4CvjSKQ==} + '@aws-sdk/credential-provider-login@3.972.18': + resolution: {integrity: sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-node@3.972.18': resolution: {integrity: sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.13': - resolution: {integrity: sha512-rsRG0LQA4VR+jnDyuqtXi2CePYSmfm5GNL9KxiW8DSe25YwJSr06W8TdUfONAC+rjsTI+aIH2rBGG5FjMeANrw==} + '@aws-sdk/credential-provider-node@3.972.19': + resolution: {integrity: sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-process@3.972.16': resolution: {integrity: sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.13': - resolution: {integrity: sha512-fr0UU1wx8kNHDhTQBXioc/YviSW8iXuAxHvnH7eQUtn8F8o/FU3uu6EUMvAQgyvn7Ne5QFnC0Cj0BFlwCk+RFw==} + '@aws-sdk/credential-provider-process@3.972.17': + resolution: {integrity: sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-sso@3.972.17': resolution: {integrity: sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.13': - resolution: {integrity: sha512-a6iFMh1pgUH0TdcouBppLJUfPM7Yd3R9S1xFodPtCRoLqCz2RQFA3qjA8x4112PVYXEd4/pHX2eihapq39w0rA==} + '@aws-sdk/credential-provider-sso@3.972.18': + resolution: {integrity: sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-web-identity@3.972.17': resolution: {integrity: sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.18': + resolution: {integrity: sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/dynamodb-codec@3.972.20': + resolution: {integrity: sha512-MQ2W0zeBMNaQYgHcQ7aul7g5783qFdP2AKcJnpaID0ekl2QbiKF+St1JMx5lgOXHlnERD9X3exr2B0SIg35oOA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/endpoint-cache@3.972.4': + resolution: {integrity: sha512-GdASDnWanLnHxKK0hqV97xz23QmfA/C8yGe0PiuEmWiHSe+x+x+mFEj4sXqx9IbfyPncWz8f4EhNwBSG9cgYCg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/eventstream-handler-node@3.972.10': resolution: {integrity: sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA==} engines: {node: '>=20.0.0'} + '@aws-sdk/lib-dynamodb@3.1006.0': + resolution: {integrity: sha512-fn/OeYKdf0Z+4+0zHLllm1q/O4O8dYbt/sE3tHj1YBaCmZqVi8ydvnjJkAZEbejM/BBMaxKJH7VJdFKSoc0mOA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-dynamodb': ^3.1006.0 + '@aws-sdk/middleware-bucket-endpoint@3.972.6': resolution: {integrity: sha512-3H2bhvb7Cb/S6WFsBy/Dy9q2aegC9JmGH1inO8Lb2sWirSqpLJlZmvQHPE29h2tIxzv6el/14X/tLCQ8BQU6ZQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-endpoint-discovery@3.972.7': + resolution: {integrity: sha512-ZeFfgAVOGR+fDq/JAPsVA3P07ba74hIppoGfmQyfzZMfAQAzc9Lbg5pndZU8EanzfKnlXbv6y09OMrSkTsUuOg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-eventstream@3.972.7': resolution: {integrity: sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g==} engines: {node: '>=20.0.0'} @@ -721,10 +797,6 @@ packages: resolution: {integrity: sha512-QLXsxsI6VW8LuGK+/yx699wzqP/NMCGk/hSGP+qtB+Lcff+23UlbahyouLlk+nfT7Iu021SkXBhnAuVd6IZcPw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.6': - resolution: {integrity: sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==} - engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.7': resolution: {integrity: sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==} engines: {node: '>=20.0.0'} @@ -733,18 +805,10 @@ packages: resolution: {integrity: sha512-XdZ2TLwyj3Am6kvUc67vquQvs6+D8npXvXgyEUJAdkUDx5oMFJKOqpK+UpJhVDsEL068WAJl2NEGzbSik7dGJQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.972.6': - resolution: {integrity: sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==} - engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.972.7': resolution: {integrity: sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.6': - resolution: {integrity: sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==} - engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.7': resolution: {integrity: sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==} engines: {node: '>=20.0.0'} @@ -757,28 +821,24 @@ packages: resolution: {integrity: sha512-acvMUX9jF4I2Ew+Z/EA6gfaFaz9ehci5wxBmXCZeulLuv8m+iGf6pY9uKz8TPjg39bdAz3hxoE0eLP8Qz+IYlA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.15': - resolution: {integrity: sha512-ABlFVcIMmuRAwBT+8q5abAxOr7WmaINirDJBnqGY5b5jSDo00UMlg/G4a0xoAgwm6oAECeJcwkvDlxDwKf58fQ==} - engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.19': resolution: {integrity: sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-user-agent@3.972.20': + resolution: {integrity: sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-websocket@3.972.12': resolution: {integrity: sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q==} engines: {node: '>= 14.0.0'} - '@aws-sdk/nested-clients@3.996.3': - resolution: {integrity: sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==} - engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.996.7': resolution: {integrity: sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==} engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.972.6': - resolution: {integrity: sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==} + '@aws-sdk/nested-clients@3.996.8': + resolution: {integrity: sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==} engines: {node: '>=20.0.0'} '@aws-sdk/region-config-resolver@3.972.7': @@ -797,12 +857,8 @@ packages: resolution: {integrity: sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.999.0': - resolution: {integrity: sha512-cx0hHUlgXULfykx4rdu/ciNAJaa3AL5xz3rieCz7NKJ68MJwlj3664Y8WR5MGgxfyYJBdamnkjNSx5Kekuc0cg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.973.4': - resolution: {integrity: sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==} + '@aws-sdk/token-providers@3.1005.0': + resolution: {integrity: sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw==} engines: {node: '>=20.0.0'} '@aws-sdk/types@3.973.5': @@ -813,9 +869,11 @@ packages: resolution: {integrity: sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.996.3': - resolution: {integrity: sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==} + '@aws-sdk/util-dynamodb@3.996.2': + resolution: {integrity: sha512-ddpwaZmjBzcApYN7lgtAXjk+u+GO8fiPsxzuc59UqP+zqdxI1gsenPvkyiHiF9LnYnyRGijz6oN2JylnN561qQ==} engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-dynamodb': ^3.1003.0 '@aws-sdk/util-endpoints@3.996.4': resolution: {integrity: sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==} @@ -833,14 +891,11 @@ packages: resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.972.6': - resolution: {integrity: sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==} - '@aws-sdk/util-user-agent-browser@3.972.7': resolution: {integrity: sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==} - '@aws-sdk/util-user-agent-node@3.973.0': - resolution: {integrity: sha512-A9J2G4Nf236e9GpaC1JnA8wRn6u6GjnOXiTwBLA6NUJhlBTIGfrTy+K1IazmF8y+4OFdW3O5TZlhyspJMqiqjA==} + '@aws-sdk/util-user-agent-node@3.973.4': + resolution: {integrity: sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -848,8 +903,8 @@ packages: aws-crt: optional: true - '@aws-sdk/util-user-agent-node@3.973.4': - resolution: {integrity: sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==} + '@aws-sdk/util-user-agent-node@3.973.5': + resolution: {integrity: sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -861,10 +916,6 @@ packages: resolution: {integrity: sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.8': - resolution: {integrity: sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==} - engines: {node: '>=20.0.0'} - '@aws/lambda-invoke-store@0.2.3': resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} @@ -2741,10 +2792,6 @@ packages: resolution: {integrity: sha512-RoygyteJeFswxDPJjUMESn9dldWVMD2xUcHHd9DenVavSfVC6FeVnSdDerOO7m8LLvw4Q132nQM4hX8JiF7dng==} engines: {node: '>= 18', npm: '>= 8.6.0'} - '@smithy/abort-controller@4.2.10': - resolution: {integrity: sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==} - engines: {node: '>=18.0.0'} - '@smithy/abort-controller@4.2.11': resolution: {integrity: sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==} engines: {node: '>=18.0.0'} @@ -2761,70 +2808,34 @@ packages: resolution: {integrity: sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==} engines: {node: '>=18.0.0'} - '@smithy/config-resolver@4.4.9': - resolution: {integrity: sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.23.6': - resolution: {integrity: sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg==} - engines: {node: '>=18.0.0'} - '@smithy/core@3.23.9': resolution: {integrity: sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.10': - resolution: {integrity: sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==} - engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.11': resolution: {integrity: sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.2.10': - resolution: {integrity: sha512-A4ynrsFFfSXUHicfTcRehytppFBcY3HQxEGYiyGktPIOye3Ot7fxpiy4VR42WmtGI4Wfo6OXt/c1Ky1nUFxYYQ==} - engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.2.11': resolution: {integrity: sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.10': - resolution: {integrity: sha512-0xupsu9yj9oDVuQ50YCTS9nuSYhGlrwqdaKQel9y2Fz7LU9fNErVlw9N0o4pm4qqvWEGbSTI4HKc6XJfB30MVw==} - engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.11': resolution: {integrity: sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.3.10': - resolution: {integrity: sha512-8kn6sinrduk0yaYHMJDsNuiFpXwQwibR7n/4CDUqn4UgaG+SeBHu5jHGFdU9BLFAM7Q4/gvr9RYxBHz9/jKrhA==} - engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.3.11': resolution: {integrity: sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.2.10': - resolution: {integrity: sha512-uUrxPGgIffnYfvIOUmBM5i+USdEBRTdh7mLPttjphgtooxQ8CtdO1p6K5+Q4BBAZvKlvtJ9jWyrWpBJYzBKsyQ==} - engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.2.11': resolution: {integrity: sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.10': - resolution: {integrity: sha512-aArqzOEvcs2dK+xQVCgLbpJQGfZihw8SD4ymhkwNTtwKbnrzdhJsFDKuMQnam2kF69WzgJYOU5eJlCx+CA32bw==} - engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.11': resolution: {integrity: sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.11': - resolution: {integrity: sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==} - engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.13': resolution: {integrity: sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==} engines: {node: '>=18.0.0'} @@ -2833,10 +2844,6 @@ packages: resolution: {integrity: sha512-DrcAx3PM6AEbWZxsKl6CWAGnVwiz28Wp1ZhNu+Hi4uI/6C1PIZBIaPM2VoqBDAsOWbM6ZVzOEQMxFLLdmb4eBQ==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.2.10': - resolution: {integrity: sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==} - engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.2.11': resolution: {integrity: sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==} engines: {node: '>=18.0.0'} @@ -2845,10 +2852,6 @@ packages: resolution: {integrity: sha512-w78xsYrOlwXKwN5tv1GnKIRbHb1HygSpeZMP6xDxCPGf1U/xDHjCpJu64c5T35UKyEPwa0bPeIcvU69VY3khUA==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.2.10': - resolution: {integrity: sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==} - engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.2.11': resolution: {integrity: sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==} engines: {node: '>=18.0.0'} @@ -2857,10 +2860,6 @@ packages: resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.2.1': - resolution: {integrity: sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==} - engines: {node: '>=18.0.0'} - '@smithy/is-array-buffer@4.2.2': resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} engines: {node: '>=18.0.0'} @@ -2869,122 +2868,62 @@ packages: resolution: {integrity: sha512-Op+Dh6dPLWTjWITChFayDllIaCXRofOed8ecpggTC5fkh8yXes0vAEX7gRUfjGK+TlyxoCAA05gHbZW/zB9JwQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.10': - resolution: {integrity: sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==} - engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.11': resolution: {integrity: sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.20': - resolution: {integrity: sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw==} - engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.23': resolution: {integrity: sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.37': - resolution: {integrity: sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA==} - engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.40': resolution: {integrity: sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.11': - resolution: {integrity: sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg==} - engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.12': resolution: {integrity: sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.10': - resolution: {integrity: sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA==} - engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.11': resolution: {integrity: sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.10': - resolution: {integrity: sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w==} - engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.11': resolution: {integrity: sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.4.12': - resolution: {integrity: sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w==} - engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.4.14': resolution: {integrity: sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.10': - resolution: {integrity: sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw==} - engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.11': resolution: {integrity: sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.10': - resolution: {integrity: sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A==} - engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.11': resolution: {integrity: sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.10': - resolution: {integrity: sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA==} - engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.11': resolution: {integrity: sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.10': - resolution: {integrity: sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q==} - engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.11': resolution: {integrity: sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.10': - resolution: {integrity: sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg==} - engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.11': resolution: {integrity: sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.4.5': - resolution: {integrity: sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug==} - engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.4.6': resolution: {integrity: sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.10': - resolution: {integrity: sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==} - engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.11': resolution: {integrity: sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.12.0': - resolution: {integrity: sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ==} - engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.12.3': resolution: {integrity: sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==} engines: {node: '>=18.0.0'} @@ -2993,34 +2932,18 @@ packages: resolution: {integrity: sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.10': - resolution: {integrity: sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==} - engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.11': resolution: {integrity: sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==} engines: {node: '>=18.0.0'} - '@smithy/util-base64@4.3.1': - resolution: {integrity: sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==} - engines: {node: '>=18.0.0'} - '@smithy/util-base64@4.3.2': resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-browser@4.2.1': - resolution: {integrity: sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==} - engines: {node: '>=18.0.0'} - '@smithy/util-body-length-browser@4.2.2': resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-node@4.2.2': - resolution: {integrity: sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw==} - engines: {node: '>=18.0.0'} - '@smithy/util-body-length-node@4.2.3': resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} engines: {node: '>=18.0.0'} @@ -3029,82 +2952,42 @@ packages: resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.2.1': - resolution: {integrity: sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==} - engines: {node: '>=18.0.0'} - '@smithy/util-buffer-from@4.2.2': resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} engines: {node: '>=18.0.0'} - '@smithy/util-config-provider@4.2.1': - resolution: {integrity: sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==} - engines: {node: '>=18.0.0'} - '@smithy/util-config-provider@4.2.2': resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.36': - resolution: {integrity: sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew==} - engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.39': resolution: {integrity: sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.39': - resolution: {integrity: sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg==} - engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.42': resolution: {integrity: sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.3.1': - resolution: {integrity: sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg==} - engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.3.2': resolution: {integrity: sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==} engines: {node: '>=18.0.0'} - '@smithy/util-hex-encoding@4.2.1': - resolution: {integrity: sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==} - engines: {node: '>=18.0.0'} - '@smithy/util-hex-encoding@4.2.2': resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.2.10': - resolution: {integrity: sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==} - engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.2.11': resolution: {integrity: sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.10': - resolution: {integrity: sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A==} - engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.11': resolution: {integrity: sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.15': - resolution: {integrity: sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw==} - engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.17': resolution: {integrity: sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==} engines: {node: '>=18.0.0'} - '@smithy/util-uri-escape@4.2.1': - resolution: {integrity: sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q==} - engines: {node: '>=18.0.0'} - '@smithy/util-uri-escape@4.2.2': resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} engines: {node: '>=18.0.0'} @@ -3113,10 +2996,6 @@ packages: resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.2.1': - resolution: {integrity: sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==} - engines: {node: '>=18.0.0'} - '@smithy/util-utf8@4.2.2': resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} engines: {node: '>=18.0.0'} @@ -3125,8 +3004,8 @@ packages: resolution: {integrity: sha512-4eTWph/Lkg1wZEDAyObwme0kmhEb7J/JjibY2znJdrYRgKbKqB7YoEhhJVJ4R1g/SYih4zuwX7LpJaM8RsnTVg==} engines: {node: '>=18.0.0'} - '@smithy/uuid@1.1.1': - resolution: {integrity: sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==} + '@smithy/util-waiter@4.2.12': + resolution: {integrity: sha512-ek5hyDrzS6mBFsNCEX8LpM+EWSLq6b9FdmPRlkpXXhiJE6aIZehKT9clC6+nFpZAA+i/Yg0xlaPeWGNbf5rzQA==} engines: {node: '>=18.0.0'} '@smithy/uuid@1.1.2': @@ -5122,6 +5001,9 @@ packages: engines: {node: '>=10'} hasBin: true + mnemonist@0.38.3: + resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} @@ -5260,6 +5142,9 @@ packages: resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} engines: {node: '>= 10.12.0'} + obliterator@1.6.1: + resolution: {integrity: sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -6592,20 +6477,20 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-locate-window': 3.965.4 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -6636,6 +6521,54 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 + '@aws-sdk/client-bedrock-agentcore@3.1006.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-node': 3.972.19 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/eventstream-serde-browser': 4.2.11 + '@smithy/eventstream-serde-config-resolver': 4.3.11 + '@smithy/eventstream-serde-node': 4.2.11 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/util-stream': 4.5.17 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/client-bedrock-runtime@3.1004.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -6733,108 +6666,236 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-s3@3.1000.0': + '@aws-sdk/client-dynamodb@3.1006.0': dependencies: - '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.15 - '@aws-sdk/credential-provider-node': 3.972.14 - '@aws-sdk/middleware-bucket-endpoint': 3.972.6 - '@aws-sdk/middleware-expect-continue': 3.972.6 - '@aws-sdk/middleware-flexible-checksums': 3.973.1 - '@aws-sdk/middleware-host-header': 3.972.6 - '@aws-sdk/middleware-location-constraint': 3.972.6 - '@aws-sdk/middleware-logger': 3.972.6 - '@aws-sdk/middleware-recursion-detection': 3.972.6 - '@aws-sdk/middleware-sdk-s3': 3.972.15 - '@aws-sdk/middleware-ssec': 3.972.6 - '@aws-sdk/middleware-user-agent': 3.972.15 - '@aws-sdk/region-config-resolver': 3.972.6 - '@aws-sdk/signature-v4-multi-region': 3.996.3 - '@aws-sdk/types': 3.973.4 - '@aws-sdk/util-endpoints': 3.996.3 - '@aws-sdk/util-user-agent-browser': 3.972.6 - '@aws-sdk/util-user-agent-node': 3.973.0 - '@smithy/config-resolver': 4.4.9 - '@smithy/core': 3.23.6 - '@smithy/eventstream-serde-browser': 4.2.10 - '@smithy/eventstream-serde-config-resolver': 4.3.10 - '@smithy/eventstream-serde-node': 4.2.10 - '@smithy/fetch-http-handler': 5.3.11 - '@smithy/hash-blob-browser': 4.2.11 - '@smithy/hash-node': 4.2.10 - '@smithy/hash-stream-node': 4.2.10 - '@smithy/invalid-dependency': 4.2.10 - '@smithy/md5-js': 4.2.10 - '@smithy/middleware-content-length': 4.2.10 - '@smithy/middleware-endpoint': 4.4.20 - '@smithy/middleware-retry': 4.4.37 - '@smithy/middleware-serde': 4.2.11 - '@smithy/middleware-stack': 4.2.10 - '@smithy/node-config-provider': 4.3.10 - '@smithy/node-http-handler': 4.4.12 - '@smithy/protocol-http': 5.3.10 - '@smithy/smithy-client': 4.12.0 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.10 - '@smithy/util-base64': 4.3.1 - '@smithy/util-body-length-browser': 4.2.1 - '@smithy/util-body-length-node': 4.2.2 - '@smithy/util-defaults-mode-browser': 4.3.36 - '@smithy/util-defaults-mode-node': 4.2.39 - '@smithy/util-endpoints': 3.3.1 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-retry': 4.2.10 - '@smithy/util-stream': 4.5.15 - '@smithy/util-utf8': 4.2.1 - '@smithy/util-waiter': 4.2.10 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.973.15': - dependencies: - '@aws-sdk/types': 3.973.4 - '@aws-sdk/xml-builder': 3.972.8 - '@smithy/core': 3.23.6 - '@smithy/node-config-provider': 4.3.10 - '@smithy/property-provider': 4.2.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/signature-v4': 5.3.10 - '@smithy/smithy-client': 4.12.0 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.1 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - - '@aws-sdk/core@3.973.18': - dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-node': 3.972.19 + '@aws-sdk/dynamodb-codec': 3.972.20 + '@aws-sdk/middleware-endpoint-discovery': 3.972.7 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 '@aws-sdk/types': 3.973.5 - '@aws-sdk/xml-builder': 3.972.10 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 '@smithy/core': 3.23.9 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 '@smithy/node-config-provider': 4.3.11 - '@smithy/property-provider': 4.2.11 + '@smithy/node-http-handler': 4.4.14 '@smithy/protocol-http': 5.3.11 - '@smithy/signature-v4': 5.3.11 '@smithy/smithy-client': 4.12.3 '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.12 tslib: 2.8.1 - - '@aws-sdk/crc64-nvme@3.972.3': + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-kms@3.1006.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-node': 3.972.19 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-s3@3.1000.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.18 + '@aws-sdk/credential-provider-node': 3.972.18 + '@aws-sdk/middleware-bucket-endpoint': 3.972.6 + '@aws-sdk/middleware-expect-continue': 3.972.6 + '@aws-sdk/middleware-flexible-checksums': 3.973.1 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-location-constraint': 3.972.6 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-sdk-s3': 3.972.15 + '@aws-sdk/middleware-ssec': 3.972.6 + '@aws-sdk/middleware-user-agent': 3.972.19 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/signature-v4-multi-region': 3.996.3 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.4 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/eventstream-serde-browser': 4.2.11 + '@smithy/eventstream-serde-config-resolver': 4.3.11 + '@smithy/eventstream-serde-node': 4.2.11 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-blob-browser': 4.2.11 + '@smithy/hash-node': 4.2.11 + '@smithy/hash-stream-node': 4.2.10 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/md5-js': 4.2.10 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/util-stream': 4.5.17 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.10 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-ssm@3.1006.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-node': 3.972.19 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.12 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.18': dependencies: + '@aws-sdk/types': 3.973.5 + '@aws-sdk/xml-builder': 3.972.10 + '@smithy/core': 3.23.9 + '@smithy/node-config-provider': 4.3.11 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 + '@smithy/smithy-client': 4.12.3 '@smithy/types': 4.13.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.13': + '@aws-sdk/core@3.973.19': + dependencies: + '@aws-sdk/types': 3.973.5 + '@aws-sdk/xml-builder': 3.972.10 + '@smithy/core': 3.23.9 + '@smithy/node-config-provider': 4.3.11 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.3': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 '@smithy/types': 4.13.0 tslib: 2.8.1 @@ -6846,17 +6907,12 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.15': + '@aws-sdk/credential-provider-env@3.972.17': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/types': 3.973.4 - '@smithy/fetch-http-handler': 5.3.11 - '@smithy/node-http-handler': 4.4.12 - '@smithy/property-provider': 4.2.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/smithy-client': 4.12.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 '@smithy/types': 4.13.0 - '@smithy/util-stream': 4.5.15 tslib: 2.8.1 '@aws-sdk/credential-provider-http@3.972.18': @@ -6872,24 +6928,18 @@ snapshots: '@smithy/util-stream': 4.5.17 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.13': - dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/credential-provider-env': 3.972.13 - '@aws-sdk/credential-provider-http': 3.972.15 - '@aws-sdk/credential-provider-login': 3.972.13 - '@aws-sdk/credential-provider-process': 3.972.13 - '@aws-sdk/credential-provider-sso': 3.972.13 - '@aws-sdk/credential-provider-web-identity': 3.972.13 - '@aws-sdk/nested-clients': 3.996.3 - '@aws-sdk/types': 3.973.4 - '@smithy/credential-provider-imds': 4.2.10 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/credential-provider-http@3.972.19': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/node-http-handler': 4.4.14 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 '@smithy/types': 4.13.0 + '@smithy/util-stream': 4.5.17 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/credential-provider-ini@3.972.17': dependencies: @@ -6910,14 +6960,20 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.972.13': + '@aws-sdk/credential-provider-ini@3.972.18': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/nested-clients': 3.996.3 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-env': 3.972.17 + '@aws-sdk/credential-provider-http': 3.972.19 + '@aws-sdk/credential-provider-login': 3.972.18 + '@aws-sdk/credential-provider-process': 3.972.17 + '@aws-sdk/credential-provider-sso': 3.972.18 + '@aws-sdk/credential-provider-web-identity': 3.972.18 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/credential-provider-imds': 4.2.11 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 '@smithy/types': 4.13.0 tslib: 2.8.1 transitivePeerDependencies: @@ -6936,18 +6992,14 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.972.14': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.13 - '@aws-sdk/credential-provider-http': 3.972.15 - '@aws-sdk/credential-provider-ini': 3.972.13 - '@aws-sdk/credential-provider-process': 3.972.13 - '@aws-sdk/credential-provider-sso': 3.972.13 - '@aws-sdk/credential-provider-web-identity': 3.972.13 - '@aws-sdk/types': 3.973.4 - '@smithy/credential-provider-imds': 4.2.10 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/credential-provider-login@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/shared-ini-file-loader': 4.4.6 '@smithy/types': 4.13.0 tslib: 2.8.1 transitivePeerDependencies: @@ -6970,14 +7022,22 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.972.13': + '@aws-sdk/credential-provider-node@3.972.19': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/credential-provider-env': 3.972.17 + '@aws-sdk/credential-provider-http': 3.972.19 + '@aws-sdk/credential-provider-ini': 3.972.18 + '@aws-sdk/credential-provider-process': 3.972.17 + '@aws-sdk/credential-provider-sso': 3.972.18 + '@aws-sdk/credential-provider-web-identity': 3.972.18 + '@aws-sdk/types': 3.973.5 + '@smithy/credential-provider-imds': 4.2.11 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 '@smithy/types': 4.13.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt '@aws-sdk/credential-provider-process@3.972.16': dependencies: @@ -6988,18 +7048,14 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.13': + '@aws-sdk/credential-provider-process@3.972.17': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/nested-clients': 3.996.3 - '@aws-sdk/token-providers': 3.999.0 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 '@smithy/types': 4.13.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/credential-provider-sso@3.972.17': dependencies: @@ -7014,13 +7070,14 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.13': + '@aws-sdk/credential-provider-sso@3.972.18': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/nested-clients': 3.996.3 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/token-providers': 3.1005.0 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 '@smithy/types': 4.13.0 tslib: 2.8.1 transitivePeerDependencies: @@ -7038,6 +7095,32 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-web-identity@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/dynamodb-codec@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.19 + '@smithy/core': 3.23.9 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@aws-sdk/endpoint-cache@3.972.4': + dependencies: + mnemonist: 0.38.3 + tslib: 2.8.1 + '@aws-sdk/eventstream-handler-node@3.972.10': dependencies: '@aws-sdk/types': 3.973.5 @@ -7045,14 +7128,33 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 + '@aws-sdk/lib-dynamodb@3.1006.0(@aws-sdk/client-dynamodb@3.1006.0)': + dependencies: + '@aws-sdk/client-dynamodb': 3.1006.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/util-dynamodb': 3.996.2(@aws-sdk/client-dynamodb@3.1006.0) + '@smithy/core': 3.23.9 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/middleware-bucket-endpoint@3.972.6': dependencies: - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-arn-parser': 3.972.2 - '@smithy/node-config-provider': 4.3.10 - '@smithy/protocol-http': 5.3.10 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-endpoint-discovery@3.972.7': + dependencies: + '@aws-sdk/endpoint-cache': 3.972.4 + '@aws-sdk/types': 3.973.5 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 '@smithy/types': 4.13.0 - '@smithy/util-config-provider': 4.2.1 tslib: 2.8.1 '@aws-sdk/middleware-eventstream@3.972.7': @@ -7064,8 +7166,8 @@ snapshots: '@aws-sdk/middleware-expect-continue@3.972.6': dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/protocol-http': 5.3.10 + '@aws-sdk/types': 3.973.5 + '@smithy/protocol-http': 5.3.11 '@smithy/types': 4.13.0 tslib: 2.8.1 @@ -7074,23 +7176,16 @@ snapshots: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.973.15 + '@aws-sdk/core': 3.973.18 '@aws-sdk/crc64-nvme': 3.972.3 - '@aws-sdk/types': 3.973.4 - '@smithy/is-array-buffer': 4.2.1 - '@smithy/node-config-provider': 4.3.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-stream': 4.5.15 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-host-header@3.972.6': - dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/protocol-http': 5.3.10 + '@aws-sdk/types': 3.973.5 + '@smithy/is-array-buffer': 4.2.2 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 '@smithy/types': 4.13.0 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-stream': 4.5.17 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 '@aws-sdk/middleware-host-header@3.972.7': @@ -7102,13 +7197,7 @@ snapshots: '@aws-sdk/middleware-location-constraint@3.972.6': dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.972.6': - dependencies: - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 '@smithy/types': 4.13.0 tslib: 2.8.1 @@ -7118,14 +7207,6 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.6': - dependencies: - '@aws-sdk/types': 3.973.4 - '@aws/lambda-invoke-store': 0.2.3 - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.7': dependencies: '@aws-sdk/types': 3.973.5 @@ -7136,40 +7217,41 @@ snapshots: '@aws-sdk/middleware-sdk-s3@3.972.15': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/types': 3.973.4 + '@aws-sdk/core': 3.973.18 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-arn-parser': 3.972.2 - '@smithy/core': 3.23.6 - '@smithy/node-config-provider': 4.3.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/signature-v4': 5.3.10 - '@smithy/smithy-client': 4.12.0 + '@smithy/core': 3.23.9 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 + '@smithy/smithy-client': 4.12.3 '@smithy/types': 4.13.0 - '@smithy/util-config-provider': 4.2.1 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-stream': 4.5.15 - '@smithy/util-utf8': 4.2.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-stream': 4.5.17 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 '@aws-sdk/middleware-ssec@3.972.6': dependencies: - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.15': + '@aws-sdk/middleware-user-agent@3.972.19': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/types': 3.973.4 - '@aws-sdk/util-endpoints': 3.996.3 - '@smithy/core': 3.23.6 - '@smithy/protocol-http': 5.3.10 + '@aws-sdk/core': 3.973.18 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@smithy/core': 3.23.9 + '@smithy/protocol-http': 5.3.11 '@smithy/types': 4.13.0 + '@smithy/util-retry': 4.2.11 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.19': + '@aws-sdk/middleware-user-agent@3.972.20': dependencies: - '@aws-sdk/core': 3.973.18 + '@aws-sdk/core': 3.973.19 '@aws-sdk/types': 3.973.5 '@aws-sdk/util-endpoints': 3.996.4 '@smithy/core': 3.23.9 @@ -7193,49 +7275,6 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.996.3': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.15 - '@aws-sdk/middleware-host-header': 3.972.6 - '@aws-sdk/middleware-logger': 3.972.6 - '@aws-sdk/middleware-recursion-detection': 3.972.6 - '@aws-sdk/middleware-user-agent': 3.972.15 - '@aws-sdk/region-config-resolver': 3.972.6 - '@aws-sdk/types': 3.973.4 - '@aws-sdk/util-endpoints': 3.996.3 - '@aws-sdk/util-user-agent-browser': 3.972.6 - '@aws-sdk/util-user-agent-node': 3.973.0 - '@smithy/config-resolver': 4.4.9 - '@smithy/core': 3.23.6 - '@smithy/fetch-http-handler': 5.3.11 - '@smithy/hash-node': 4.2.10 - '@smithy/invalid-dependency': 4.2.10 - '@smithy/middleware-content-length': 4.2.10 - '@smithy/middleware-endpoint': 4.4.20 - '@smithy/middleware-retry': 4.4.37 - '@smithy/middleware-serde': 4.2.11 - '@smithy/middleware-stack': 4.2.10 - '@smithy/node-config-provider': 4.3.10 - '@smithy/node-http-handler': 4.4.12 - '@smithy/protocol-http': 5.3.10 - '@smithy/smithy-client': 4.12.0 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.10 - '@smithy/util-base64': 4.3.1 - '@smithy/util-body-length-browser': 4.2.1 - '@smithy/util-body-length-node': 4.2.2 - '@smithy/util-defaults-mode-browser': 4.3.36 - '@smithy/util-defaults-mode-node': 4.2.39 - '@smithy/util-endpoints': 3.3.1 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-retry': 4.2.10 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/nested-clients@3.996.7': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -7279,13 +7318,48 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.972.6': + '@aws-sdk/nested-clients@3.996.8': dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/config-resolver': 4.4.9 - '@smithy/node-config-provider': 4.3.10 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt '@aws-sdk/region-config-resolver@3.972.7': dependencies: @@ -7298,20 +7372,20 @@ snapshots: '@aws-sdk/s3-request-presigner@3.1000.0': dependencies: '@aws-sdk/signature-v4-multi-region': 3.996.3 - '@aws-sdk/types': 3.973.4 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-format-url': 3.972.6 - '@smithy/middleware-endpoint': 4.4.20 - '@smithy/protocol-http': 5.3.10 - '@smithy/smithy-client': 4.12.0 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 '@smithy/types': 4.13.0 tslib: 2.8.1 '@aws-sdk/signature-v4-multi-region@3.996.3': dependencies: '@aws-sdk/middleware-sdk-s3': 3.972.15 - '@aws-sdk/types': 3.973.4 - '@smithy/protocol-http': 5.3.10 - '@smithy/signature-v4': 5.3.10 + '@aws-sdk/types': 3.973.5 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 '@smithy/types': 4.13.0 tslib: 2.8.1 @@ -7327,23 +7401,18 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/token-providers@3.999.0': + '@aws-sdk/token-providers@3.1005.0': dependencies: - '@aws-sdk/core': 3.973.15 - '@aws-sdk/nested-clients': 3.996.3 - '@aws-sdk/types': 3.973.4 - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 '@smithy/types': 4.13.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.973.4': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@aws-sdk/types@3.973.5': dependencies: '@smithy/types': 4.13.0 @@ -7353,12 +7422,9 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.996.3': + '@aws-sdk/util-dynamodb@3.996.2(@aws-sdk/client-dynamodb@3.1006.0)': dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.10 - '@smithy/util-endpoints': 3.3.1 + '@aws-sdk/client-dynamodb': 3.1006.0 tslib: 2.8.1 '@aws-sdk/util-endpoints@3.996.4': @@ -7371,8 +7437,8 @@ snapshots: '@aws-sdk/util-format-url@3.972.6': dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/querystring-builder': 4.2.10 + '@aws-sdk/types': 3.973.5 + '@smithy/querystring-builder': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 @@ -7387,13 +7453,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.6': - dependencies: - '@aws-sdk/types': 3.973.4 - '@smithy/types': 4.13.0 - bowser: 2.14.1 - tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.7': dependencies: '@aws-sdk/types': 3.973.5 @@ -7401,14 +7460,6 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.0': - dependencies: - '@aws-sdk/middleware-user-agent': 3.972.15 - '@aws-sdk/types': 3.973.4 - '@smithy/node-config-provider': 4.3.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.4': dependencies: '@aws-sdk/middleware-user-agent': 3.972.19 @@ -7417,13 +7468,15 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.10': + '@aws-sdk/util-user-agent-node@3.973.5': dependencies: + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/types': 3.973.5 + '@smithy/node-config-provider': 4.3.11 '@smithy/types': 4.13.0 - fast-xml-parser: 5.3.8 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.8': + '@aws-sdk/xml-builder@3.972.10': dependencies: '@smithy/types': 4.13.0 fast-xml-parser: 5.3.8 @@ -9234,11 +9287,6 @@ snapshots: transitivePeerDependencies: - debug - '@smithy/abort-controller@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/abort-controller@4.2.11': dependencies: '@smithy/types': 4.13.0 @@ -9246,7 +9294,7 @@ snapshots: '@smithy/chunked-blob-reader-native@4.2.2': dependencies: - '@smithy/util-base64': 4.3.1 + '@smithy/util-base64': 4.3.2 tslib: 2.8.1 '@smithy/chunked-blob-reader@5.2.1': @@ -9262,28 +9310,6 @@ snapshots: '@smithy/util-middleware': 4.2.11 tslib: 2.8.1 - '@smithy/config-resolver@4.4.9': - dependencies: - '@smithy/node-config-provider': 4.3.10 - '@smithy/types': 4.13.0 - '@smithy/util-config-provider': 4.2.1 - '@smithy/util-endpoints': 3.3.1 - '@smithy/util-middleware': 4.2.10 - tslib: 2.8.1 - - '@smithy/core@3.23.6': - dependencies: - '@smithy/middleware-serde': 4.2.11 - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.1 - '@smithy/util-body-length-browser': 4.2.1 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-stream': 4.5.15 - '@smithy/util-utf8': 4.2.1 - '@smithy/uuid': 1.1.1 - tslib: 2.8.1 - '@smithy/core@3.23.9': dependencies: '@smithy/middleware-serde': 4.2.12 @@ -9297,14 +9323,6 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.10': - dependencies: - '@smithy/node-config-provider': 4.3.10 - '@smithy/property-provider': 4.2.10 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.10 - tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.11': dependencies: '@smithy/node-config-provider': 4.3.11 @@ -9313,13 +9331,6 @@ snapshots: '@smithy/url-parser': 4.2.11 tslib: 2.8.1 - '@smithy/eventstream-codec@4.2.10': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.13.0 - '@smithy/util-hex-encoding': 4.2.1 - tslib: 2.8.1 - '@smithy/eventstream-codec@4.2.11': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -9327,60 +9338,29 @@ snapshots: '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 - '@smithy/eventstream-serde-browser@4.2.10': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/eventstream-serde-browser@4.2.11': dependencies: '@smithy/eventstream-serde-universal': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/eventstream-serde-config-resolver@4.3.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/eventstream-serde-config-resolver@4.3.11': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/eventstream-serde-node@4.2.10': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/eventstream-serde-node@4.2.11': dependencies: '@smithy/eventstream-serde-universal': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/eventstream-serde-universal@4.2.10': - dependencies: - '@smithy/eventstream-codec': 4.2.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/eventstream-serde-universal@4.2.11': dependencies: '@smithy/eventstream-codec': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.11': - dependencies: - '@smithy/protocol-http': 5.3.10 - '@smithy/querystring-builder': 4.2.10 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.1 - tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.13': dependencies: '@smithy/protocol-http': 5.3.11 @@ -9396,13 +9376,6 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/hash-node@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - '@smithy/util-buffer-from': 4.2.1 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - '@smithy/hash-node@4.2.11': dependencies: '@smithy/types': 4.13.0 @@ -9413,12 +9386,7 @@ snapshots: '@smithy/hash-stream-node@4.2.10': dependencies: '@smithy/types': 4.13.0 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.10': - dependencies: - '@smithy/types': 4.13.0 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 '@smithy/invalid-dependency@4.2.11': @@ -9430,10 +9398,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/is-array-buffer@4.2.1': - dependencies: - tslib: 2.8.1 - '@smithy/is-array-buffer@4.2.2': dependencies: tslib: 2.8.1 @@ -9441,13 +9405,7 @@ snapshots: '@smithy/md5-js@4.2.10': dependencies: '@smithy/types': 4.13.0 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.10': - dependencies: - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 '@smithy/middleware-content-length@4.2.11': @@ -9456,17 +9414,6 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.20': - dependencies: - '@smithy/core': 3.23.6 - '@smithy/middleware-serde': 4.2.11 - '@smithy/node-config-provider': 4.3.10 - '@smithy/shared-ini-file-loader': 4.4.5 - '@smithy/types': 4.13.0 - '@smithy/url-parser': 4.2.10 - '@smithy/util-middleware': 4.2.10 - tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.23': dependencies: '@smithy/core': 3.23.9 @@ -9478,18 +9425,6 @@ snapshots: '@smithy/util-middleware': 4.2.11 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.37': - dependencies: - '@smithy/node-config-provider': 4.3.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/service-error-classification': 4.2.10 - '@smithy/smithy-client': 4.12.0 - '@smithy/types': 4.13.0 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-retry': 4.2.10 - '@smithy/uuid': 1.1.1 - tslib: 2.8.1 - '@smithy/middleware-retry@4.4.40': dependencies: '@smithy/node-config-provider': 4.3.11 @@ -9502,35 +9437,17 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.11': - dependencies: - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/middleware-serde@4.2.12': dependencies: '@smithy/protocol-http': 5.3.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/middleware-stack@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/middleware-stack@4.2.11': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/node-config-provider@4.3.10': - dependencies: - '@smithy/property-provider': 4.2.10 - '@smithy/shared-ini-file-loader': 4.4.5 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/node-config-provider@4.3.11': dependencies: '@smithy/property-provider': 4.2.11 @@ -9538,14 +9455,6 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.4.12': - dependencies: - '@smithy/abort-controller': 4.2.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/querystring-builder': 4.2.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/node-http-handler@4.4.14': dependencies: '@smithy/abort-controller': 4.2.11 @@ -9554,77 +9463,36 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/property-provider@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/property-provider@4.2.11': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/protocol-http@5.3.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/protocol-http@5.3.11': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/querystring-builder@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - '@smithy/util-uri-escape': 4.2.1 - tslib: 2.8.1 - '@smithy/querystring-builder@4.2.11': dependencies: '@smithy/types': 4.13.0 '@smithy/util-uri-escape': 4.2.2 tslib: 2.8.1 - '@smithy/querystring-parser@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/querystring-parser@4.2.11': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/service-error-classification@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - '@smithy/service-error-classification@4.2.11': dependencies: '@smithy/types': 4.13.0 - '@smithy/shared-ini-file-loader@4.4.5': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/shared-ini-file-loader@4.4.6': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/signature-v4@5.3.10': - dependencies: - '@smithy/is-array-buffer': 4.2.1 - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 - '@smithy/util-hex-encoding': 4.2.1 - '@smithy/util-middleware': 4.2.10 - '@smithy/util-uri-escape': 4.2.1 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - '@smithy/signature-v4@5.3.11': dependencies: '@smithy/is-array-buffer': 4.2.2 @@ -9636,16 +9504,6 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/smithy-client@4.12.0': - dependencies: - '@smithy/core': 3.23.6 - '@smithy/middleware-endpoint': 4.4.20 - '@smithy/middleware-stack': 4.2.10 - '@smithy/protocol-http': 5.3.10 - '@smithy/types': 4.13.0 - '@smithy/util-stream': 4.5.15 - tslib: 2.8.1 - '@smithy/smithy-client@4.12.3': dependencies: '@smithy/core': 3.23.9 @@ -9660,42 +9518,22 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.2.10': - dependencies: - '@smithy/querystring-parser': 4.2.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/url-parser@4.2.11': dependencies: '@smithy/querystring-parser': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/util-base64@4.3.1': - dependencies: - '@smithy/util-buffer-from': 4.2.1 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - '@smithy/util-base64@4.3.2': dependencies: '@smithy/util-buffer-from': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/util-body-length-browser@4.2.1': - dependencies: - tslib: 2.8.1 - '@smithy/util-body-length-browser@4.2.2': dependencies: tslib: 2.8.1 - '@smithy/util-body-length-node@4.2.2': - dependencies: - tslib: 2.8.1 - '@smithy/util-body-length-node@4.2.3': dependencies: tslib: 2.8.1 @@ -9705,31 +9543,15 @@ snapshots: '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 - '@smithy/util-buffer-from@4.2.1': - dependencies: - '@smithy/is-array-buffer': 4.2.1 - tslib: 2.8.1 - '@smithy/util-buffer-from@4.2.2': dependencies: '@smithy/is-array-buffer': 4.2.2 tslib: 2.8.1 - '@smithy/util-config-provider@4.2.1': - dependencies: - tslib: 2.8.1 - '@smithy/util-config-provider@4.2.2': dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.36': - dependencies: - '@smithy/property-provider': 4.2.10 - '@smithy/smithy-client': 4.12.0 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.39': dependencies: '@smithy/property-provider': 4.2.11 @@ -9737,16 +9559,6 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.39': - dependencies: - '@smithy/config-resolver': 4.4.9 - '@smithy/credential-provider-imds': 4.2.10 - '@smithy/node-config-provider': 4.3.10 - '@smithy/property-provider': 4.2.10 - '@smithy/smithy-client': 4.12.0 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.42': dependencies: '@smithy/config-resolver': 4.4.10 @@ -9757,59 +9569,27 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/util-endpoints@3.3.1': - dependencies: - '@smithy/node-config-provider': 4.3.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/util-endpoints@3.3.2': dependencies: '@smithy/node-config-provider': 4.3.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/util-hex-encoding@4.2.1': - dependencies: - tslib: 2.8.1 - '@smithy/util-hex-encoding@4.2.2': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.2.10': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/util-middleware@4.2.11': dependencies: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/util-retry@4.2.10': - dependencies: - '@smithy/service-error-classification': 4.2.10 - '@smithy/types': 4.13.0 - tslib: 2.8.1 - '@smithy/util-retry@4.2.11': dependencies: '@smithy/service-error-classification': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/util-stream@4.5.15': - dependencies: - '@smithy/fetch-http-handler': 5.3.11 - '@smithy/node-http-handler': 4.4.12 - '@smithy/types': 4.13.0 - '@smithy/util-base64': 4.3.1 - '@smithy/util-buffer-from': 4.2.1 - '@smithy/util-hex-encoding': 4.2.1 - '@smithy/util-utf8': 4.2.1 - tslib: 2.8.1 - '@smithy/util-stream@4.5.17': dependencies: '@smithy/fetch-http-handler': 5.3.13 @@ -9821,10 +9601,6 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/util-uri-escape@4.2.1': - dependencies: - tslib: 2.8.1 - '@smithy/util-uri-escape@4.2.2': dependencies: tslib: 2.8.1 @@ -9834,11 +9610,6 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.2.1': - dependencies: - '@smithy/util-buffer-from': 4.2.1 - tslib: 2.8.1 - '@smithy/util-utf8@4.2.2': dependencies: '@smithy/util-buffer-from': 4.2.2 @@ -9846,12 +9617,14 @@ snapshots: '@smithy/util-waiter@4.2.10': dependencies: - '@smithy/abort-controller': 4.2.10 + '@smithy/abort-controller': 4.2.11 '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/uuid@1.1.1': + '@smithy/util-waiter@4.2.12': dependencies: + '@smithy/abort-controller': 4.2.11 + '@smithy/types': 4.13.0 tslib: 2.8.1 '@smithy/uuid@1.1.2': @@ -12062,6 +11835,10 @@ snapshots: mkdirp@3.0.1: {} + mnemonist@0.38.3: + dependencies: + obliterator: 1.6.1 + module-details-from-path@1.0.4: {} morgan@1.10.1: @@ -12243,6 +12020,8 @@ snapshots: object-path@0.11.8: {} + obliterator@1.6.1: {} + obug@2.1.1: {} octokit@5.0.5: diff --git a/src/hyperion/channel-identity-resolver.ts b/src/hyperion/channel-identity-resolver.ts new file mode 100644 index 0000000000000..b5b921580bceb --- /dev/null +++ b/src/hyperion/channel-identity-resolver.ts @@ -0,0 +1,132 @@ +import type { HyperionDynamoDBClient } from "./dynamodb-client.js"; +import { TenantConfigLoader } from "./tenant-config-loader.js"; +import { + DEFAULT_AGENT_ID, + type CachedChannelIdentity, + type ChannelIdentityResolution, + type ChannelLink, + type HyperionPlatform, +} from "./types.js"; + +/** Identity cache TTL: 5 minutes. */ +const IDENTITY_CACHE_TTL_MS = 5 * 60_000; + +/** Maximum identity cache entries. */ +const IDENTITY_CACHE_MAX_SIZE = 50_000; + +/** + * Resolves external channel messages to internal tenant identities. + * + * This is the critical path for inbound webhook messages: + * External message arrives → resolve platform_user_id → get user_id → load config + * + * Replaces OpenClaw's in-memory allowFrom/pairing matching with DynamoDB lookups. + * The channel_config table maps (platform, platform_user_id) → user_id. + * + * Caching: in-memory with 5-minute TTL per (platform, platform_user_id). + */ +export class ChannelIdentityResolver { + private readonly dbClient: HyperionDynamoDBClient; + private readonly configLoader: TenantConfigLoader; + private readonly cache = new Map(); + + constructor(dbClient: HyperionDynamoDBClient, configLoader: TenantConfigLoader) { + this.dbClient = dbClient; + this.configLoader = configLoader; + } + + /** + * Resolve an inbound channel message to a tenant. + * + * Flow: + * 1. Look up channel_config by (platform, platform_user_id) + * 2. Extract user_id from the link + * 3. Load the full tenant config (with all channel configs assembled) + * + * @returns ChannelIdentityResolution or null if not paired. + */ + async resolve( + platform: HyperionPlatform, + platformUserId: string, + ): Promise { + const channelLink = await this.getChannelLink(platform, platformUserId); + if (!channelLink) { + return null; + } + + // [claude-infra] Multi-instance: load config for the specific agent instance + // the channel is bound to. + const agentId = channelLink.agent_id || DEFAULT_AGENT_ID; + const config = await this.configLoader.loadTenantConfig(channelLink.user_id, agentId); + + return { + user_id: channelLink.user_id, + agent_id: agentId, + channelLink, + config, + }; + } + + /** + * Resolve only the user_id for a given platform identity. + * Lighter-weight than full resolve() when you don't need the config. + */ + async resolveUserId(platform: HyperionPlatform, platformUserId: string): Promise { + const channelLink = await this.getChannelLink(platform, platformUserId); + return channelLink?.user_id ?? null; + } + + /** + * Get all channel links for a specific user. + * Useful for the portal "Connected Channels" UI. + */ + async getLinksForUser(userId: string): Promise { + return this.dbClient.getChannelLinksForUser(userId); + } + + /** + * Invalidate the identity cache for a specific channel. + * Call this when a channel is paired or unpaired. + */ + invalidateCache(platform: HyperionPlatform, platformUserId: string): void { + this.cache.delete(this.cacheKey(platform, platformUserId)); + } + + /** + * Clear the entire identity cache. + */ + clearCache(): void { + this.cache.clear(); + } + + private async getChannelLink( + platform: HyperionPlatform, + platformUserId: string, + ): Promise { + const key = this.cacheKey(platform, platformUserId); + const cached = this.cache.get(key); + if (cached && Date.now() - cached.cachedAt < IDENTITY_CACHE_TTL_MS) { + return cached.channelLink; + } + + const channelLink = await this.dbClient.getChannelLink(platform, platformUserId); + if (!channelLink) { + return null; + } + + // Evict oldest if full. + if (this.cache.size >= IDENTITY_CACHE_MAX_SIZE) { + const oldestKey = this.cache.keys().next().value; + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + this.cache.set(key, { channelLink, cachedAt: Date.now() }); + return channelLink; + } + + private cacheKey(platform: HyperionPlatform, platformUserId: string): string { + return `${platform}:${platformUserId}`; + } +} diff --git a/src/hyperion/dynamodb-client.test.ts b/src/hyperion/dynamodb-client.test.ts new file mode 100644 index 0000000000000..dfdbcf4a26af1 --- /dev/null +++ b/src/hyperion/dynamodb-client.test.ts @@ -0,0 +1,577 @@ +// @vitest-pool threads +// ↑ vi.mock for dynamic `await import()` requires threads pool (forks doesn't intercept). +import { describe, expect, it, vi, beforeEach } from "vitest"; + +// Mock AWS SDK commands — the source uses dynamic imports which fail without the package installed. +// Each command class just stores its input for assertion. +class MockCommand { + input: unknown; + constructor(input: unknown) { + this.input = input; + } +} +vi.mock("@aws-sdk/lib-dynamodb", () => ({ + GetCommand: class extends MockCommand {}, + PutCommand: class extends MockCommand {}, + DeleteCommand: class extends MockCommand {}, + QueryCommand: class extends MockCommand {}, +})); + +import { HyperionDynamoDBClient, type DynamoDBDocClient } from "./dynamodb-client.js"; +import type { + HyperionDynamoDBConfig, + TenantConfig, + PairingCode, + UserCredentialsRecord, +} from "./types.js"; +import { DEFAULT_AGENT_ID } from "./types.js"; + +const TEST_CONFIG: HyperionDynamoDBConfig = { + region: "us-west-2", + tenantConfigTableName: "hyperion-test-tenant-config", + channelConfigTableName: "hyperion-test-channel-config", + pairingCodesTableName: "hyperion-test-pairing-codes", + userCredentialsTableName: "hyperion-test-user-credentials", + credentialsKmsKeyId: "arn:aws:kms:us-west-2:123456789012:key/test-key-id", + channelConfigUserIdIndexName: "user-id-index", +}; + +function createMockDocClient(): DynamoDBDocClient & { send: ReturnType } { + return { send: vi.fn() }; +} + +describe("HyperionDynamoDBClient", () => { + let mockDocClient: ReturnType; + let client: HyperionDynamoDBClient; + + beforeEach(() => { + mockDocClient = createMockDocClient(); + client = new HyperionDynamoDBClient(TEST_CONFIG, mockDocClient); + }); + + // -- getTenantConfig -- + + describe("getTenantConfig", () => { + it("returns the item when found", async () => { + const tenantConfig: TenantConfig = { + user_id: "user-1", + agent_id: "main", + display_name: "Test User", + plan: "pro", + }; + mockDocClient.send.mockResolvedValueOnce({ Item: tenantConfig }); + + const result = await client.getTenantConfig("user-1"); + + expect(result).toEqual(tenantConfig); + expect(mockDocClient.send).toHaveBeenCalledOnce(); + }); + + it("returns null when item is not found", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.getTenantConfig("nonexistent-user"); + + expect(result).toBeNull(); + }); + + it("uses the correct table name and composite key", async () => { + mockDocClient.send.mockResolvedValueOnce({ Item: { user_id: "u1", agent_id: "helper" } }); + + await client.getTenantConfig("u1", "helper"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-tenant-config", + Key: { user_id: "u1", agent_id: "helper" }, + }); + }); + + it("defaults agentId to DEFAULT_AGENT_ID when not provided", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.getTenantConfig("u1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Key).toEqual({ user_id: "u1", agent_id: DEFAULT_AGENT_ID }); + }); + }); + + // -- listTenantAgents -- + + describe("listTenantAgents", () => { + it("returns items array from query", async () => { + const agents: TenantConfig[] = [ + { user_id: "u1", agent_id: "main" }, + { user_id: "u1", agent_id: "work" }, + ]; + mockDocClient.send.mockResolvedValueOnce({ Items: agents }); + + const result = await client.listTenantAgents("u1"); + + expect(result).toEqual(agents); + expect(result).toHaveLength(2); + }); + + it("returns empty array when no items found", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.listTenantAgents("u1"); + + expect(result).toEqual([]); + }); + + it("queries the correct table with user_id", async () => { + mockDocClient.send.mockResolvedValueOnce({ Items: [] }); + + await client.listTenantAgents("u1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-tenant-config", + KeyConditionExpression: "user_id = :uid", + ExpressionAttributeValues: { ":uid": "u1" }, + }); + }); + }); + + // -- putTenantConfig -- + + describe("putTenantConfig", () => { + it("sets updated_at timestamp", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + const before = new Date().toISOString(); + + await client.putTenantConfig({ user_id: "u1", agent_id: "main" }); + + const command = mockDocClient.send.mock.calls[0][0]; + const item = command.input.Item; + expect(item.updated_at).toBeDefined(); + // updated_at should be a recent ISO timestamp + const updatedAt = new Date(item.updated_at).getTime(); + expect(updatedAt).toBeGreaterThanOrEqual(new Date(before).getTime()); + expect(updatedAt).toBeLessThanOrEqual(Date.now()); + }); + + it("defaults agent_id to DEFAULT_AGENT_ID when falsy", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.putTenantConfig({ user_id: "u1", agent_id: "" }); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Item.agent_id).toBe(DEFAULT_AGENT_ID); + }); + + it("preserves explicit agent_id", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.putTenantConfig({ user_id: "u1", agent_id: "work-helper" }); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Item.agent_id).toBe("work-helper"); + }); + + it("writes to the correct table", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.putTenantConfig({ user_id: "u1", agent_id: "main" }); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.TableName).toBe("hyperion-test-tenant-config"); + }); + }); + + // -- deleteTenantConfig -- + + describe("deleteTenantConfig", () => { + it("deletes with correct key", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.deleteTenantConfig("u1", "work"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-tenant-config", + Key: { user_id: "u1", agent_id: "work" }, + }); + }); + + it("defaults agentId to DEFAULT_AGENT_ID", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.deleteTenantConfig("u1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Key).toEqual({ user_id: "u1", agent_id: DEFAULT_AGENT_ID }); + }); + }); + + // -- getChannelLink -- + + describe("getChannelLink", () => { + it("returns channel link when found", async () => { + const link = { + platform: "telegram" as const, + platform_user_id: "tg-123", + user_id: "u1", + agent_id: "main", + paired_at: "2025-01-01T00:00:00Z", + channel_account_id: "bot-1", + channel_config: {}, + }; + mockDocClient.send.mockResolvedValueOnce({ Item: link }); + + const result = await client.getChannelLink("telegram", "tg-123"); + + expect(result).toEqual(link); + }); + + it("returns null when not found", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.getChannelLink("slack", "unknown"); + + expect(result).toBeNull(); + }); + + it("uses correct table and composite key", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.getChannelLink("discord", "disc-456"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-channel-config", + Key: { platform: "discord", platform_user_id: "disc-456" }, + }); + }); + }); + + // -- getChannelLinksForUser -- + + describe("getChannelLinksForUser", () => { + it("queries GSI with correct index name", async () => { + mockDocClient.send.mockResolvedValueOnce({ Items: [] }); + + await client.getChannelLinksForUser("u1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-channel-config", + IndexName: "user-id-index", + KeyConditionExpression: "user_id = :uid", + ExpressionAttributeValues: { ":uid": "u1" }, + }); + }); + + it("returns empty array when no links found", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.getChannelLinksForUser("u1"); + + expect(result).toEqual([]); + }); + }); + + // -- putChannelLink -- + + describe("putChannelLink", () => { + it("writes channel link to correct table", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + const link = { + platform: "telegram" as const, + platform_user_id: "tg-123", + user_id: "u1", + agent_id: "main", + paired_at: "2025-01-01T00:00:00Z", + channel_account_id: "bot-1", + channel_config: {}, + }; + + await client.putChannelLink(link); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.TableName).toBe("hyperion-test-channel-config"); + expect(command.input.Item).toEqual(link); + }); + }); + + // -- deleteChannelLink -- + + describe("deleteChannelLink", () => { + it("deletes with correct key", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.deleteChannelLink("whatsapp", "wa-789"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-channel-config", + Key: { platform: "whatsapp", platform_user_id: "wa-789" }, + }); + }); + }); + + // -- getPairingCode -- + + describe("getPairingCode", () => { + it("returns code when not expired", async () => { + const futureExpiry = Math.floor(Date.now() / 1000) + 300; // 5 min from now + const pairingCode: PairingCode = { + code: "ABC123", + user_id: "u1", + agent_id: "main", + platform: "telegram", + created_at: "2025-01-01T00:00:00Z", + expires_at: futureExpiry, + }; + mockDocClient.send.mockResolvedValueOnce({ Item: pairingCode }); + + const result = await client.getPairingCode("ABC123"); + + expect(result).toEqual(pairingCode); + }); + + it("returns null for expired codes", async () => { + const pastExpiry = Math.floor(Date.now() / 1000) - 60; // 1 min ago + const pairingCode: PairingCode = { + code: "EXPIRED1", + user_id: "u1", + agent_id: "main", + platform: "telegram", + created_at: "2025-01-01T00:00:00Z", + expires_at: pastExpiry, + }; + mockDocClient.send.mockResolvedValueOnce({ Item: pairingCode }); + + const result = await client.getPairingCode("EXPIRED1"); + + expect(result).toBeNull(); + }); + + it("returns null when code not found in DynamoDB", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.getPairingCode("NONEXIST"); + + expect(result).toBeNull(); + }); + + it("uses correct table and key", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.getPairingCode("CODE1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-pairing-codes", + Key: { code: "CODE1" }, + }); + }); + }); + + // -- putPairingCode -- + + describe("putPairingCode", () => { + it("writes with ConditionExpression to prevent overwrites", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + const pairingCode: PairingCode = { + code: "NEW123", + user_id: "u1", + agent_id: "main", + platform: "slack", + created_at: "2025-01-01T00:00:00Z", + expires_at: Math.floor(Date.now() / 1000) + 300, + }; + + await client.putPairingCode(pairingCode); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.TableName).toBe("hyperion-test-pairing-codes"); + expect(command.input.Item).toEqual(pairingCode); + expect(command.input.ConditionExpression).toBe("attribute_not_exists(code)"); + }); + }); + + // -- deletePairingCode -- + + describe("deletePairingCode", () => { + it("deletes with correct key", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.deletePairingCode("CODE1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-pairing-codes", + Key: { code: "CODE1" }, + }); + }); + }); + + // -- getUserCredentials -- + + describe("getUserCredentials", () => { + const credRecord: UserCredentialsRecord = { + user_id: "u1", + agent_id: "work", + credentials_blob: "encrypted-blob", + kms_key_id: "key-1", + updated_at: "2025-01-01T00:00:00Z", + }; + + it("returns agent-specific credentials when found", async () => { + mockDocClient.send.mockResolvedValueOnce({ Item: credRecord }); + + const result = await client.getUserCredentials("u1", "work"); + + expect(result).toEqual(credRecord); + // Should only call send once (no fallback needed) + expect(mockDocClient.send).toHaveBeenCalledOnce(); + }); + + it("falls back to __shared__ when agent-specific not found", async () => { + const sharedRecord: UserCredentialsRecord = { + user_id: "u1", + agent_id: "__shared__", + credentials_blob: "shared-blob", + kms_key_id: "key-1", + updated_at: "2025-01-01T00:00:00Z", + }; + // First call: agent-specific not found + mockDocClient.send.mockResolvedValueOnce({}); + // Second call: __shared__ found + mockDocClient.send.mockResolvedValueOnce({ Item: sharedRecord }); + + const result = await client.getUserCredentials("u1", "work"); + + expect(result).toEqual(sharedRecord); + expect(mockDocClient.send).toHaveBeenCalledTimes(2); + + // Verify first call was for agent-specific + const firstCommand = mockDocClient.send.mock.calls[0][0]; + expect(firstCommand.input.Key).toEqual({ user_id: "u1", agent_id: "work" }); + + // Verify second call was for __shared__ + const secondCommand = mockDocClient.send.mock.calls[1][0]; + expect(secondCommand.input.Key).toEqual({ user_id: "u1", agent_id: "__shared__" }); + }); + + it("returns null when neither agent-specific nor __shared__ found", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.getUserCredentials("u1", "work"); + + expect(result).toBeNull(); + expect(mockDocClient.send).toHaveBeenCalledTimes(2); + }); + + it("does NOT fall back when agentId is already __shared__", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + const result = await client.getUserCredentials("u1", "__shared__"); + + expect(result).toBeNull(); + // Should only call send once — no fallback to __shared__ when already querying __shared__ + expect(mockDocClient.send).toHaveBeenCalledOnce(); + }); + + it("defaults agentId to DEFAULT_AGENT_ID", async () => { + mockDocClient.send.mockResolvedValueOnce({ + Item: { ...credRecord, agent_id: DEFAULT_AGENT_ID }, + }); + + await client.getUserCredentials("u1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Key).toEqual({ user_id: "u1", agent_id: DEFAULT_AGENT_ID }); + }); + + it("uses the correct table for all calls", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + mockDocClient.send.mockResolvedValueOnce({}); + + await client.getUserCredentials("u1", "custom-agent"); + + const firstCommand = mockDocClient.send.mock.calls[0][0]; + const secondCommand = mockDocClient.send.mock.calls[1][0]; + expect(firstCommand.input.TableName).toBe("hyperion-test-user-credentials"); + expect(secondCommand.input.TableName).toBe("hyperion-test-user-credentials"); + }); + }); + + // -- putUserCredentials -- + + describe("putUserCredentials", () => { + it("defaults agent_id when falsy", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.putUserCredentials({ + user_id: "u1", + agent_id: "", + credentials_blob: "blob", + kms_key_id: "key-1", + updated_at: "2025-01-01T00:00:00Z", + }); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Item.agent_id).toBe(DEFAULT_AGENT_ID); + }); + + it("preserves explicit agent_id", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.putUserCredentials({ + user_id: "u1", + agent_id: "custom", + credentials_blob: "blob", + kms_key_id: "key-1", + updated_at: "2025-01-01T00:00:00Z", + }); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Item.agent_id).toBe("custom"); + }); + + it("writes to the correct table", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.putUserCredentials({ + user_id: "u1", + agent_id: "main", + credentials_blob: "blob", + kms_key_id: "key-1", + updated_at: "2025-01-01T00:00:00Z", + }); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.TableName).toBe("hyperion-test-user-credentials"); + }); + }); + + // -- deleteUserCredentials -- + + describe("deleteUserCredentials", () => { + it("deletes with correct key", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.deleteUserCredentials("u1", "work"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input).toEqual({ + TableName: "hyperion-test-user-credentials", + Key: { user_id: "u1", agent_id: "work" }, + }); + }); + + it("defaults agentId to DEFAULT_AGENT_ID", async () => { + mockDocClient.send.mockResolvedValueOnce({}); + + await client.deleteUserCredentials("u1"); + + const command = mockDocClient.send.mock.calls[0][0]; + expect(command.input.Key).toEqual({ user_id: "u1", agent_id: DEFAULT_AGENT_ID }); + }); + }); +}); diff --git a/src/hyperion/dynamodb-client.ts b/src/hyperion/dynamodb-client.ts new file mode 100644 index 0000000000000..1786063f1f035 --- /dev/null +++ b/src/hyperion/dynamodb-client.ts @@ -0,0 +1,232 @@ +import { + DEFAULT_AGENT_ID, + type ChannelLink, + type HyperionDynamoDBConfig, + type HyperionPlatform, + type PairingCode, + type TenantConfig, + type UserCredentialsRecord, +} from "./types.js"; + +/** + * Minimal DynamoDB document client interface. + * Accepts any AWS SDK v3 DynamoDBDocumentClient-compatible implementation. + */ +export type DynamoDBDocClient = { + send(command: unknown): Promise; +}; + +/** + * Wraps DynamoDB operations for Hyperion's three tables. + * Designed to work with AWS SDK v3 DynamoDBDocumentClient. + */ +export class HyperionDynamoDBClient { + private readonly config: HyperionDynamoDBConfig; + private readonly docClient: DynamoDBDocClient; + + constructor(config: HyperionDynamoDBConfig, docClient: DynamoDBDocClient) { + this.config = config; + this.docClient = docClient; + } + + // -- Tenant Config -- [claude-infra] composite key: user_id + agent_id + + async getTenantConfig( + userId: string, + agentId: string = DEFAULT_AGENT_ID, + ): Promise { + const { GetCommand } = await import("@aws-sdk/lib-dynamodb"); + const result = await this.docClient.send( + new GetCommand({ + TableName: this.config.tenantConfigTableName, + Key: { user_id: userId, agent_id: agentId }, + }), + ); + const item = (result as { Item?: TenantConfig }).Item; + return item ?? null; + } + + async listTenantAgents(userId: string): Promise { + const { QueryCommand } = await import("@aws-sdk/lib-dynamodb"); + const result = await this.docClient.send( + new QueryCommand({ + TableName: this.config.tenantConfigTableName, + KeyConditionExpression: "user_id = :uid", + ExpressionAttributeValues: { ":uid": userId }, + }), + ); + return (result as { Items?: TenantConfig[] }).Items ?? []; + } + + async putTenantConfig(tenantConfig: TenantConfig): Promise { + const { PutCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new PutCommand({ + TableName: this.config.tenantConfigTableName, + Item: { + ...tenantConfig, + agent_id: tenantConfig.agent_id || DEFAULT_AGENT_ID, + updated_at: new Date().toISOString(), + }, + }), + ); + } + + async deleteTenantConfig(userId: string, agentId: string = DEFAULT_AGENT_ID): Promise { + const { DeleteCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new DeleteCommand({ + TableName: this.config.tenantConfigTableName, + Key: { user_id: userId, agent_id: agentId }, + }), + ); + } + + // -- Channel Config -- + + async getChannelLink( + platform: HyperionPlatform, + platformUserId: string, + ): Promise { + const { GetCommand } = await import("@aws-sdk/lib-dynamodb"); + const result = await this.docClient.send( + new GetCommand({ + TableName: this.config.channelConfigTableName, + Key: { platform, platform_user_id: platformUserId }, + }), + ); + const item = (result as { Item?: ChannelLink }).Item; + return item ?? null; + } + + async getChannelLinksForUser(userId: string): Promise { + const { QueryCommand } = await import("@aws-sdk/lib-dynamodb"); + const result = await this.docClient.send( + new QueryCommand({ + TableName: this.config.channelConfigTableName, + IndexName: this.config.channelConfigUserIdIndexName, + KeyConditionExpression: "user_id = :uid", + ExpressionAttributeValues: { ":uid": userId }, + }), + ); + const items = (result as { Items?: ChannelLink[] }).Items; + return items ?? []; + } + + async putChannelLink(channelLink: ChannelLink): Promise { + const { PutCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new PutCommand({ + TableName: this.config.channelConfigTableName, + Item: channelLink, + }), + ); + } + + async deleteChannelLink(platform: HyperionPlatform, platformUserId: string): Promise { + const { DeleteCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new DeleteCommand({ + TableName: this.config.channelConfigTableName, + Key: { platform, platform_user_id: platformUserId }, + }), + ); + } + + // -- Pairing Codes -- + + async getPairingCode(code: string): Promise { + const { GetCommand } = await import("@aws-sdk/lib-dynamodb"); + const result = await this.docClient.send( + new GetCommand({ + TableName: this.config.pairingCodesTableName, + Key: { code }, + }), + ); + const item = (result as { Item?: PairingCode }).Item; + if (!item) { + return null; + } + // DynamoDB TTL is eventually consistent — check expiry explicitly. + if (item.expires_at <= Math.floor(Date.now() / 1000)) { + return null; + } + return item; + } + + async putPairingCode(pairingCode: PairingCode): Promise { + const { PutCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new PutCommand({ + TableName: this.config.pairingCodesTableName, + Item: pairingCode, + ConditionExpression: "attribute_not_exists(code)", + }), + ); + } + + async deletePairingCode(code: string): Promise { + const { DeleteCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new DeleteCommand({ + TableName: this.config.pairingCodesTableName, + Key: { code }, + }), + ); + } + + // -- User Credentials -- [claude-infra] composite key: user_id + agent_id + + async getUserCredentials( + userId: string, + agentId: string = DEFAULT_AGENT_ID, + ): Promise { + const { GetCommand } = await import("@aws-sdk/lib-dynamodb"); + // Try agent-specific credentials first, then shared. + const result = await this.docClient.send( + new GetCommand({ + TableName: this.config.userCredentialsTableName, + Key: { user_id: userId, agent_id: agentId }, + }), + ); + const item = (result as { Item?: UserCredentialsRecord }).Item; + if (item) { + return item; + } + + // Fall back to shared credentials if agent-specific not found. + if (agentId !== "__shared__") { + const sharedResult = await this.docClient.send( + new GetCommand({ + TableName: this.config.userCredentialsTableName, + Key: { user_id: userId, agent_id: "__shared__" }, + }), + ); + return (sharedResult as { Item?: UserCredentialsRecord }).Item ?? null; + } + return null; + } + + async putUserCredentials(record: UserCredentialsRecord): Promise { + const { PutCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new PutCommand({ + TableName: this.config.userCredentialsTableName, + Item: { + ...record, + agent_id: record.agent_id || DEFAULT_AGENT_ID, + }, + }), + ); + } + + async deleteUserCredentials(userId: string, agentId: string = DEFAULT_AGENT_ID): Promise { + const { DeleteCommand } = await import("@aws-sdk/lib-dynamodb"); + await this.docClient.send( + new DeleteCommand({ + TableName: this.config.userCredentialsTableName, + Key: { user_id: userId, agent_id: agentId }, + }), + ); + } +} diff --git a/src/hyperion/index.ts b/src/hyperion/index.ts new file mode 100644 index 0000000000000..5a6cc74307df8 --- /dev/null +++ b/src/hyperion/index.ts @@ -0,0 +1,68 @@ +/** + * Hyperion Integration Layer for OpenClaw + * + * This module replaces OpenClaw's single-tenant filesystem-based configuration + * with a multi-tenant DynamoDB-backed implementation for the Nova Personal + * Assistant Platform (assistant.nova.amazon.com). + * + * Architecture: + * + * OpenClaw (single-tenant) Hyperion (multi-tenant) + * ───────────────────────── ─────────────────────────── + * openclaw.json5 on disk → tenant_config DynamoDB table + * {channel}-pairing.json → pairing_codes DynamoDB table (TTL) + * {channel}-allowFrom.json → channel_config DynamoDB table + * session keys: "main" → session keys: "tenant_{userId}:{agentId}:main" + * in-memory config cache → in-memory LRU with 1-min TTL + * file lock concurrency → DynamoDB conditional writes + * + * Entry points: + * - TenantConfigLoader: loadConfig() replacement (per-tenant from DynamoDB) + * - ChannelIdentityResolver: webhook identity resolution (platform_user_id → user_id) + * - HyperionPairingStore: pairing-store.ts replacement (DynamoDB-backed) + * - Session helpers: tenant-scoped session key management + * - HyperionDynamoDBClient: DynamoDB operations for all three tables + * - createHyperionRuntime: one-call setup of the full integration layer + */ + +// Types +export { DEFAULT_AGENT_ID } from "./types.js"; +export type { + ChannelIdentityResolution, + ChannelLink, + ChannelRuntimeConfig, + CachedChannelIdentity, + CachedTenantConfig, + HyperionDynamoDBConfig, + HyperionPlatform, + PairingCode, + TenantConfig, +} from "./types.js"; + +// DynamoDB client +export { HyperionDynamoDBClient } from "./dynamodb-client.js"; +export type { DynamoDBDocClient } from "./dynamodb-client.js"; + +// Config loader (replaces io.ts loadConfig) +export { TenantConfigLoader, TenantNotFoundError } from "./tenant-config-loader.js"; + +// Identity resolution (replaces channel-config.ts resolution + pairing allowFrom) +export { ChannelIdentityResolver } from "./channel-identity-resolver.js"; + +// Pairing store (replaces pairing-store.ts file-based store) +export { HyperionPairingStore } from "./pairing-store.js"; + +// Session management (replaces session.ts with tenant-scoped keys) +export { + buildPortalSessionKey, + buildChannelSessionKey, + buildTenantMemoryNamespace, + extractAgentId, + extractInnerSessionKey, + extractTenantId, + isSessionForAgent, + isSessionForTenant, +} from "./session-manager.js"; + +// Runtime factory +export { createHyperionRuntime, type HyperionRuntime } from "./runtime.js"; diff --git a/src/hyperion/pairing-store.test.ts b/src/hyperion/pairing-store.test.ts new file mode 100644 index 0000000000000..a6c53fbbe2ca9 --- /dev/null +++ b/src/hyperion/pairing-store.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import type { HyperionDynamoDBClient } from "./dynamodb-client.js"; +import { HyperionPairingStore } from "./pairing-store.js"; +import { DEFAULT_AGENT_ID } from "./types.js"; + +function createMockDbClient() { + return { + putPairingCode: vi.fn(), + getPairingCode: vi.fn(), + deletePairingCode: vi.fn(), + putChannelLink: vi.fn(), + deleteChannelLink: vi.fn(), + } as unknown as HyperionDynamoDBClient & { + putPairingCode: ReturnType; + getPairingCode: ReturnType; + deletePairingCode: ReturnType; + putChannelLink: ReturnType; + deleteChannelLink: ReturnType; + }; +} + +describe("HyperionPairingStore", () => { + let dbClient: ReturnType; + let store: HyperionPairingStore; + + beforeEach(() => { + dbClient = createMockDbClient(); + store = new HyperionPairingStore(dbClient); + }); + + describe("generatePairingCode", () => { + it("returns a code on success", async () => { + dbClient.putPairingCode.mockResolvedValueOnce(undefined); + + const code = await store.generatePairingCode("user-1", "telegram"); + + expect(code).toBeTruthy(); + expect(typeof code).toBe("string"); + expect(code!.length).toBe(8); + expect(dbClient.putPairingCode).toHaveBeenCalledOnce(); + const savedCode = dbClient.putPairingCode.mock.calls[0][0]; + expect(savedCode.user_id).toBe("user-1"); + expect(savedCode.platform).toBe("telegram"); + expect(savedCode.agent_id).toBe(DEFAULT_AGENT_ID); + }); + + it("retries on ConditionalCheckFailedException", async () => { + const conditionalError = new Error("Conditional check failed"); + conditionalError.name = "ConditionalCheckFailedException"; + + dbClient.putPairingCode + .mockRejectedValueOnce(conditionalError) + .mockRejectedValueOnce(conditionalError) + .mockResolvedValueOnce(undefined); + + const code = await store.generatePairingCode("user-1", "telegram"); + + expect(code).toBeTruthy(); + expect(dbClient.putPairingCode).toHaveBeenCalledTimes(3); + }); + + it("returns null after 5 failed attempts", async () => { + const conditionalError = new Error("Conditional check failed"); + conditionalError.name = "ConditionalCheckFailedException"; + + dbClient.putPairingCode.mockRejectedValue(conditionalError); + + const code = await store.generatePairingCode("user-1", "telegram"); + + expect(code).toBeNull(); + expect(dbClient.putPairingCode).toHaveBeenCalledTimes(5); + }); + + it("passes agentId through to PairingCode", async () => { + dbClient.putPairingCode.mockResolvedValueOnce(undefined); + + const code = await store.generatePairingCode("user-1", "slack", "work-agent"); + + expect(code).toBeTruthy(); + const savedCode = dbClient.putPairingCode.mock.calls[0][0]; + expect(savedCode.agent_id).toBe("work-agent"); + }); + + it("throws on non-conditional errors", async () => { + const genericError = new Error("DynamoDB is down"); + genericError.name = "InternalServerError"; + + dbClient.putPairingCode.mockRejectedValueOnce(genericError); + + await expect(store.generatePairingCode("user-1", "telegram")).rejects.toThrow( + "DynamoDB is down", + ); + expect(dbClient.putPairingCode).toHaveBeenCalledOnce(); + }); + }); + + describe("redeemPairingCode", () => { + const basePairingCode = { + code: "ABCD1234", + user_id: "user-1", + agent_id: "work-agent", + platform: "telegram" as const, + created_at: "2026-01-01T00:00:00.000Z", + expires_at: Math.floor(Date.now() / 1000) + 300, + }; + + it("creates ChannelLink with correct data, inherits agent_id from pairing code", async () => { + dbClient.getPairingCode.mockResolvedValueOnce(basePairingCode); + dbClient.putChannelLink.mockResolvedValueOnce(undefined); + dbClient.deletePairingCode.mockResolvedValueOnce(undefined); + + const link = await store.redeemPairingCode({ + code: "ABCD1234", + platform: "telegram", + platformUserId: "tg-user-99", + channelAccountId: "bot-account", + channelConfig: { some: "config" }, + }); + + expect(link).not.toBeNull(); + expect(link!.platform).toBe("telegram"); + expect(link!.platform_user_id).toBe("tg-user-99"); + expect(link!.user_id).toBe("user-1"); + expect(link!.agent_id).toBe("work-agent"); + expect(link!.channel_account_id).toBe("bot-account"); + expect(link!.channel_config).toEqual({ some: "config" }); + expect(link!.paired_at).toBeTruthy(); + expect(dbClient.putChannelLink).toHaveBeenCalledOnce(); + }); + + it("normalizes code to uppercase", async () => { + dbClient.getPairingCode.mockResolvedValueOnce(basePairingCode); + dbClient.putChannelLink.mockResolvedValueOnce(undefined); + dbClient.deletePairingCode.mockResolvedValueOnce(undefined); + + await store.redeemPairingCode({ + code: " abcd1234 ", + platform: "telegram", + platformUserId: "tg-user-99", + }); + + expect(dbClient.getPairingCode).toHaveBeenCalledWith("ABCD1234"); + }); + + it("returns null for empty code", async () => { + const link = await store.redeemPairingCode({ + code: " ", + platform: "telegram", + platformUserId: "tg-user-99", + }); + + expect(link).toBeNull(); + expect(dbClient.getPairingCode).not.toHaveBeenCalled(); + }); + + it("returns null if pairing code not found", async () => { + dbClient.getPairingCode.mockResolvedValueOnce(null); + + const link = await store.redeemPairingCode({ + code: "NONEXIST", + platform: "telegram", + platformUserId: "tg-user-99", + }); + + expect(link).toBeNull(); + expect(dbClient.putChannelLink).not.toHaveBeenCalled(); + }); + + it("returns null if platform doesn't match", async () => { + dbClient.getPairingCode.mockResolvedValueOnce(basePairingCode); + + const link = await store.redeemPairingCode({ + code: "ABCD1234", + platform: "slack", + platformUserId: "slack-user-1", + }); + + expect(link).toBeNull(); + expect(dbClient.putChannelLink).not.toHaveBeenCalled(); + }); + + it("deletes consumed code (best effort)", async () => { + dbClient.getPairingCode.mockResolvedValueOnce(basePairingCode); + dbClient.putChannelLink.mockResolvedValueOnce(undefined); + dbClient.deletePairingCode.mockRejectedValueOnce(new Error("Delete failed")); + + const link = await store.redeemPairingCode({ + code: "ABCD1234", + platform: "telegram", + platformUserId: "tg-user-99", + }); + + // Link should still be returned even though delete failed + expect(link).not.toBeNull(); + expect(dbClient.deletePairingCode).toHaveBeenCalledWith("ABCD1234"); + }); + }); + + describe("validatePairingCode", () => { + it("returns pairing code when valid", async () => { + const pairingCode = { + code: "ABCD1234", + user_id: "user-1", + agent_id: DEFAULT_AGENT_ID, + platform: "telegram" as const, + created_at: "2026-01-01T00:00:00.000Z", + expires_at: Math.floor(Date.now() / 1000) + 300, + }; + dbClient.getPairingCode.mockResolvedValueOnce(pairingCode); + + const result = await store.validatePairingCode("abcd1234", "telegram"); + + expect(result).toEqual(pairingCode); + expect(dbClient.getPairingCode).toHaveBeenCalledWith("ABCD1234"); + }); + + it("returns null on platform mismatch", async () => { + const pairingCode = { + code: "ABCD1234", + user_id: "user-1", + agent_id: DEFAULT_AGENT_ID, + platform: "telegram" as const, + created_at: "2026-01-01T00:00:00.000Z", + expires_at: Math.floor(Date.now() / 1000) + 300, + }; + dbClient.getPairingCode.mockResolvedValueOnce(pairingCode); + + const result = await store.validatePairingCode("ABCD1234", "discord"); + + expect(result).toBeNull(); + }); + }); + + describe("disconnectChannel", () => { + it("calls deleteChannelLink", async () => { + dbClient.deleteChannelLink.mockResolvedValueOnce(undefined); + + await store.disconnectChannel("telegram", "tg-user-99"); + + expect(dbClient.deleteChannelLink).toHaveBeenCalledWith("telegram", "tg-user-99"); + }); + }); +}); diff --git a/src/hyperion/pairing-store.ts b/src/hyperion/pairing-store.ts new file mode 100644 index 0000000000000..f8bc4b6ba8a65 --- /dev/null +++ b/src/hyperion/pairing-store.ts @@ -0,0 +1,164 @@ +import crypto from "node:crypto"; +import type { HyperionDynamoDBClient } from "./dynamodb-client.js"; +import { + DEFAULT_AGENT_ID, + type ChannelLink, + type ChannelRuntimeConfig, + type HyperionPlatform, + type PairingCode, +} from "./types.js"; + +const PAIRING_CODE_LENGTH = 8; +const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; +const PAIRING_CODE_TTL_SECONDS = 5 * 60; // 5 minutes + +/** + * DynamoDB-backed pairing store for Hyperion. + * + * Replaces OpenClaw's file-based pairing store (src/pairing/pairing-store.ts) with + * a serverless implementation using the pairing_codes DynamoDB table. + * + * Flow: + * 1. User clicks "Connect Telegram" in portal → generatePairingCode() + * 2. User sends `/connect ` to bot on Telegram → redeemPairingCode() + * 3. Code is validated, channel link is created, code is deleted + * + * Key differences from OpenClaw's file-based store: + * - No file locks needed — DynamoDB provides atomic conditional writes + * - No pruning needed — DynamoDB TTL auto-deletes expired codes + * - No max-pending limit needed — TTL-based expiry prevents unbounded growth + * - Pairing is user-initiated (portal → external), not external-initiated + */ +export class HyperionPairingStore { + private readonly dbClient: HyperionDynamoDBClient; + + constructor(dbClient: HyperionDynamoDBClient) { + this.dbClient = dbClient; + } + + /** + * Generate a pairing code for a user to connect a specific channel. + * Called from the portal when user clicks "Connect ". + * + * @returns The generated code, or null if code generation failed after retries. + */ + // [claude-infra] Multi-instance: agentId specifies which agent the channel binds to. + async generatePairingCode( + userId: string, + platform: HyperionPlatform, + agentId: string = DEFAULT_AGENT_ID, + meta?: Record, + ): Promise { + const maxAttempts = 5; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const code = randomCode(); + const pairingCode: PairingCode = { + code, + user_id: userId, + agent_id: agentId, + platform, + created_at: new Date().toISOString(), + expires_at: Math.floor(Date.now() / 1000) + PAIRING_CODE_TTL_SECONDS, + ...(meta ? { meta } : {}), + }; + + try { + await this.dbClient.putPairingCode(pairingCode); + return code; + } catch (err) { + // ConditionalCheckFailedException means code already exists — retry. + if ((err as { name?: string }).name === "ConditionalCheckFailedException") { + continue; + } + throw err; + } + } + return null; + } + + /** + * Redeem a pairing code and create the channel link. + * Called when a webhook arrives with `/connect `. + * + * @returns The created ChannelLink, or null if the code is invalid/expired. + */ + async redeemPairingCode(params: { + code: string; + platform: HyperionPlatform; + platformUserId: string; + channelAccountId?: string; + channelConfig?: ChannelRuntimeConfig; + }): Promise { + const normalizedCode = params.code.trim().toUpperCase(); + if (!normalizedCode) { + return null; + } + + // Fetch and validate the pairing code. + const pairingCode = await this.dbClient.getPairingCode(normalizedCode); + if (!pairingCode) { + return null; + } + + // Verify platform matches. + if (pairingCode.platform !== params.platform) { + return null; + } + + // [claude-infra] Multi-instance: channel link inherits agent_id from pairing code. + const channelLink: ChannelLink = { + platform: params.platform, + platform_user_id: params.platformUserId, + user_id: pairingCode.user_id, + agent_id: pairingCode.agent_id || DEFAULT_AGENT_ID, + paired_at: new Date().toISOString(), + channel_account_id: params.channelAccountId ?? "default", + channel_config: params.channelConfig ?? {}, + }; + + await this.dbClient.putChannelLink(channelLink); + + // Delete the consumed code (best-effort — TTL will clean up regardless). + await this.dbClient.deletePairingCode(normalizedCode).catch(() => {}); + + return channelLink; + } + + /** + * Validate a pairing code without consuming it. + * Useful for showing confirmation before completing pairing. + */ + async validatePairingCode(code: string, platform: HyperionPlatform): Promise { + const normalizedCode = code.trim().toUpperCase(); + if (!normalizedCode) { + return null; + } + + const pairingCode = await this.dbClient.getPairingCode(normalizedCode); + if (!pairingCode) { + return null; + } + if (pairingCode.platform !== platform) { + return null; + } + + return pairingCode; + } + + /** + * Disconnect a channel link. + * Called from the portal when user clicks "Disconnect ". + */ + async disconnectChannel(platform: HyperionPlatform, platformUserId: string): Promise { + await this.dbClient.deleteChannelLink(platform, platformUserId); + } +} + +function randomCode(): string { + let out = ""; + for (let i = 0; i < PAIRING_CODE_LENGTH; i++) { + const idx = crypto.randomInt(0, PAIRING_CODE_ALPHABET.length); + out += PAIRING_CODE_ALPHABET[idx]; + } + return out; +} diff --git a/src/hyperion/runtime.ts b/src/hyperion/runtime.ts new file mode 100644 index 0000000000000..fa71222d41cbf --- /dev/null +++ b/src/hyperion/runtime.ts @@ -0,0 +1,90 @@ +import type { OpenClawConfig } from "../config/types.js"; +import { ChannelIdentityResolver } from "./channel-identity-resolver.js"; +import { HyperionDynamoDBClient, type DynamoDBDocClient } from "./dynamodb-client.js"; +import { HyperionPairingStore } from "./pairing-store.js"; +import { TenantConfigLoader } from "./tenant-config-loader.js"; +import type { HyperionDynamoDBConfig } from "./types.js"; +import { UserCredentialStore, type KMSClient } from "./user-credential-store.js"; + +/** + * The complete Hyperion runtime — all services wired together. + */ +export type HyperionRuntime = { + /** DynamoDB operations for all tables. */ + dbClient: HyperionDynamoDBClient; + /** Loads per-tenant OpenClawConfig from DynamoDB (replaces loadConfig). */ + configLoader: TenantConfigLoader; + /** Resolves inbound webhooks to tenant identities (replaces allowFrom/pairing match). */ + identityResolver: ChannelIdentityResolver; + /** Manages pairing codes and channel linking (replaces file-based pairing store). */ + pairingStore: HyperionPairingStore; + /** Manages per-user encrypted credentials (API keys, bot tokens). */ + credentialStore: UserCredentialStore; +}; + +/** + * Create the full Hyperion runtime with a single call. + * + * Usage: + * ```ts + * import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; + * import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; + * import { KMSClient } from "@aws-sdk/client-kms"; + * import { createHyperionRuntime } from "./hyperion/index.js"; + * + * const ddbClient = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" })); + * const kmsClient = new KMSClient({ region: "us-east-1" }); + * const runtime = createHyperionRuntime({ + * dynamoConfig: { + * region: "us-east-1", + * tenantConfigTableName: "Hyperion-prod-tenant-config", + * channelConfigTableName: "Hyperion-prod-channel-config", + * pairingCodesTableName: "Hyperion-prod-pairing-codes", + * userCredentialsTableName: "Hyperion-prod-user-credentials", + * credentialsKmsKeyId: "alias/hyperion-prod-credentials", + * channelConfigUserIdIndexName: "user_id-index", + * }, + * docClient: ddbClient, + * kmsClient: kmsClient, + * }); + * + * // Portal SSE path (credentials auto-injected into config): + * const config = await runtime.configLoader.loadTenantConfig(userId); + * + * // Store user credentials (encrypted at rest via KMS): + * await runtime.credentialStore.putCredentials(userId, { + * model_keys: { openai: "sk-..." }, + * tool_keys: { brave_search: "BSA..." }, + * }); + * + * // Webhook path: + * const identity = await runtime.identityResolver.resolve("telegram", "12345"); + * + * // Pairing: + * const code = await runtime.pairingStore.generatePairingCode(userId, "telegram"); + * ``` + */ +export function createHyperionRuntime(params: { + dynamoConfig: HyperionDynamoDBConfig; + docClient: DynamoDBDocClient; + kmsClient: KMSClient; + defaultConfig?: Partial; +}): HyperionRuntime { + const dbClient = new HyperionDynamoDBClient(params.dynamoConfig, params.docClient); + const credentialStore = new UserCredentialStore( + dbClient, + params.kmsClient, + params.dynamoConfig.credentialsKmsKeyId, + ); + const configLoader = new TenantConfigLoader(dbClient, params.defaultConfig, credentialStore); + const identityResolver = new ChannelIdentityResolver(dbClient, configLoader); + const pairingStore = new HyperionPairingStore(dbClient); + + return { + dbClient, + configLoader, + identityResolver, + pairingStore, + credentialStore, + }; +} diff --git a/src/hyperion/session-manager.test.ts b/src/hyperion/session-manager.test.ts new file mode 100644 index 0000000000000..4673b2eca6c8b --- /dev/null +++ b/src/hyperion/session-manager.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, it } from "vitest"; +import { + buildPortalSessionKey, + buildChannelSessionKey, + extractTenantId, + extractAgentId, + extractInnerSessionKey, + isSessionForTenant, + isSessionForAgent, + buildTenantMemoryNamespace, +} from "./session-manager.js"; + +describe("session-manager", () => { + describe("buildPortalSessionKey", () => { + it("uses default agentId", () => { + expect(buildPortalSessionKey("user123")).toBe("tenant_user123:main:main"); + }); + + it("uses custom agentId", () => { + expect(buildPortalSessionKey("user123", "work")).toBe("tenant_user123:work:main"); + }); + }); + + describe("buildChannelSessionKey", () => { + it("builds key without threadId", () => { + expect(buildChannelSessionKey("user123", "main", "telegram", "98765")).toBe( + "tenant_user123:main:telegram:98765", + ); + }); + + it("builds key with threadId", () => { + expect(buildChannelSessionKey("user123", "main", "slack", "U111", "T999")).toBe( + "tenant_user123:main:slack:U111:T999", + ); + }); + + it("defaults agentId to main", () => { + expect(buildChannelSessionKey("user123", undefined, "discord", "D555")).toBe( + "tenant_user123:main:discord:D555", + ); + }); + + it("uses custom agentId", () => { + expect(buildChannelSessionKey("user123", "personal", "whatsapp", "W777")).toBe( + "tenant_user123:personal:whatsapp:W777", + ); + }); + }); + + describe("extractTenantId", () => { + it("extracts userId from portal key", () => { + expect(extractTenantId("tenant_user123:main:main")).toBe("user123"); + }); + + it("extracts userId from channel key", () => { + expect(extractTenantId("tenant_abc:work:telegram:98765")).toBe("abc"); + }); + + it("returns null for non-tenant key", () => { + expect(extractTenantId("main")).toBeNull(); + }); + + it("returns null for empty string", () => { + expect(extractTenantId("")).toBeNull(); + }); + + it("returns null for tenant_ prefix with no separator", () => { + expect(extractTenantId("tenant_user123")).toBeNull(); + }); + + it("handles tenant_ prefix with immediate separator", () => { + expect(extractTenantId("tenant_:main:main")).toBe(""); + }); + }); + + describe("extractAgentId", () => { + it("extracts agentId from portal key", () => { + expect(extractAgentId("tenant_user123:main:main")).toBe("main"); + }); + + it("extracts custom agentId", () => { + expect(extractAgentId("tenant_user123:work:telegram:98765")).toBe("work"); + }); + + it("returns default for non-tenant key", () => { + expect(extractAgentId("some:other:key")).toBe("main"); + }); + + it("returns default when no separator after prefix", () => { + expect(extractAgentId("tenant_user123")).toBe("main"); + }); + + it("returns agentId when only userId and agentId present", () => { + expect(extractAgentId("tenant_user123:work")).toBe("work"); + }); + + it("returns default for empty agentId segment", () => { + expect(extractAgentId("tenant_user123::rest")).toBe("main"); + }); + }); + + describe("extractInnerSessionKey", () => { + it("extracts inner key from portal session", () => { + expect(extractInnerSessionKey("tenant_user123:main:main")).toBe("main"); + }); + + it("extracts inner key from channel session", () => { + expect(extractInnerSessionKey("tenant_user123:work:telegram:98765")).toBe("telegram:98765"); + }); + + it("extracts inner key with threadId", () => { + expect(extractInnerSessionKey("tenant_user123:main:slack:U111:T999")).toBe("slack:U111:T999"); + }); + + it("returns original key for non-tenant key", () => { + expect(extractInnerSessionKey("telegram:12345")).toBe("telegram:12345"); + }); + + it("returns original when no separator after prefix", () => { + expect(extractInnerSessionKey("tenant_user123")).toBe("tenant_user123"); + }); + + it("returns agentId when no second separator", () => { + expect(extractInnerSessionKey("tenant_user123:work")).toBe("work"); + }); + }); + + describe("isSessionForTenant", () => { + it("returns true for matching userId", () => { + expect(isSessionForTenant("tenant_user123:main:main", "user123")).toBe(true); + }); + + it("returns false for different userId", () => { + expect(isSessionForTenant("tenant_user123:main:main", "user456")).toBe(false); + }); + + it("returns false for non-tenant key", () => { + expect(isSessionForTenant("main", "user123")).toBe(false); + }); + + it("does not match partial userId prefix", () => { + expect(isSessionForTenant("tenant_user123:main:main", "user12")).toBe(false); + }); + }); + + describe("isSessionForAgent", () => { + it("returns true for matching userId and default agentId", () => { + expect(isSessionForAgent("tenant_user123:main:main", "user123")).toBe(true); + }); + + it("returns true for matching userId and custom agentId", () => { + expect(isSessionForAgent("tenant_user123:work:telegram:98765", "user123", "work")).toBe(true); + }); + + it("returns false for wrong agentId", () => { + expect(isSessionForAgent("tenant_user123:work:main", "user123", "personal")).toBe(false); + }); + + it("returns false for wrong userId", () => { + expect(isSessionForAgent("tenant_user123:main:main", "user456")).toBe(false); + }); + + it("returns false for non-tenant key", () => { + expect(isSessionForAgent("main", "user123")).toBe(false); + }); + }); + + describe("buildTenantMemoryNamespace", () => { + it("builds namespace with default agentId", () => { + expect(buildTenantMemoryNamespace("user123")).toBe("tenant_user123:main"); + }); + + it("builds namespace with custom agentId", () => { + expect(buildTenantMemoryNamespace("user123", "work")).toBe("tenant_user123:work"); + }); + }); +}); diff --git a/src/hyperion/session-manager.ts b/src/hyperion/session-manager.ts new file mode 100644 index 0000000000000..4aab48d39f538 --- /dev/null +++ b/src/hyperion/session-manager.ts @@ -0,0 +1,158 @@ +import { DEFAULT_AGENT_ID, type HyperionPlatform } from "./types.js"; + +/** + * Tenant-scoped session key management for Hyperion. + * [claude-infra] Multi-instance: session keys now include agent_id. + * + * OpenClaw's sessions are identified by session keys (src/channels/session.ts, + * src/routing/session-key.ts). In single-tenant mode, session keys are simple + * channel identifiers like "telegram:12345" or "main". + * + * For multi-tenant Hyperion, all session keys are namespaced with the tenant's + * user_id and agent_id to ensure complete isolation between tenants and agents: + * + * Single-tenant OpenClaw: "main" + * Multi-tenant Hyperion: "tenant_user123:main:main" + * + * Single-tenant OpenClaw: "telegram:12345" + * Multi-tenant Hyperion: "tenant_user123:main:telegram:12345" + * + * Format: tenant_{userId}:{agentId}:{rest} + */ + +const TENANT_PREFIX = "tenant_"; +const SEPARATOR = ":"; + +/** + * Build a tenant-scoped session key for portal (synchronous) interactions. + * [claude-infra] Multi-instance: includes agentId. + * + * @param userId - Internal Hyperion user ID + * @param agentId - Agent instance ID (default: "main") + * @returns Scoped session key like "tenant_user123:main:main" + */ +export function buildPortalSessionKey(userId: string, agentId: string = DEFAULT_AGENT_ID): string { + return `${TENANT_PREFIX}${userId}${SEPARATOR}${agentId}${SEPARATOR}main`; +} + +/** + * Build a tenant-scoped session key for an external channel interaction. + * [claude-infra] Multi-instance: includes agentId. + * + * @param userId - Internal Hyperion user ID + * @param agentId - Agent instance ID (default: "main") + * @param platform - External platform identifier + * @param platformUserId - User's ID on the external platform + * @param threadId - Optional thread/conversation ID for threaded sessions + * @returns Scoped session key like "tenant_user123:main:telegram:98765" + */ +export function buildChannelSessionKey( + userId: string, + agentId: string = DEFAULT_AGENT_ID, + platform: HyperionPlatform, + platformUserId: string, + threadId?: string, +): string { + const base = `${TENANT_PREFIX}${userId}${SEPARATOR}${agentId}${SEPARATOR}${platform}${SEPARATOR}${platformUserId}`; + if (threadId) { + return `${base}${SEPARATOR}${threadId}`; + } + return base; +} + +/** + * Extract the tenant user_id from a scoped session key. + * + * @returns The user_id, or null if the key is not tenant-scoped. + */ +export function extractTenantId(sessionKey: string): string | null { + if (!sessionKey.startsWith(TENANT_PREFIX)) { + return null; + } + const afterPrefix = sessionKey.slice(TENANT_PREFIX.length); + const separatorIdx = afterPrefix.indexOf(SEPARATOR); + if (separatorIdx < 0) { + return null; + } + return afterPrefix.slice(0, separatorIdx); +} + +/** + * Extract the agent_id from a scoped session key. + * [claude-infra] Multi-instance support. + * + * Format: tenant_{userId}:{agentId}:{rest} + * @returns The agent_id, or DEFAULT_AGENT_ID if not found. + */ +export function extractAgentId(sessionKey: string): string { + if (!sessionKey.startsWith(TENANT_PREFIX)) { + return DEFAULT_AGENT_ID; + } + const afterPrefix = sessionKey.slice(TENANT_PREFIX.length); + const firstSep = afterPrefix.indexOf(SEPARATOR); + if (firstSep < 0) { + return DEFAULT_AGENT_ID; + } + const afterUserId = afterPrefix.slice(firstSep + 1); + const secondSep = afterUserId.indexOf(SEPARATOR); + if (secondSep < 0) { + return afterUserId || DEFAULT_AGENT_ID; + } + return afterUserId.slice(0, secondSep) || DEFAULT_AGENT_ID; +} + +/** + * Extract the inner session key (without tenant prefix and agent_id). + * This is what gets passed to OpenClaw's session internals. + * [claude-infra] Multi-instance: strips both tenant_ prefix and agentId. + * + * @returns The inner key, or the original key if not tenant-scoped. + */ +export function extractInnerSessionKey(sessionKey: string): string { + if (!sessionKey.startsWith(TENANT_PREFIX)) { + return sessionKey; + } + const afterPrefix = sessionKey.slice(TENANT_PREFIX.length); + // Skip userId + const firstSep = afterPrefix.indexOf(SEPARATOR); + if (firstSep < 0) { + return sessionKey; + } + const afterUserId = afterPrefix.slice(firstSep + 1); + // Skip agentId + const secondSep = afterUserId.indexOf(SEPARATOR); + if (secondSep < 0) { + return afterUserId; + } + return afterUserId.slice(secondSep + 1); +} + +/** + * Check if a session key belongs to a specific tenant. + */ +export function isSessionForTenant(sessionKey: string, userId: string): boolean { + return sessionKey.startsWith(`${TENANT_PREFIX}${userId}${SEPARATOR}`); +} + +/** + * Check if a session key belongs to a specific tenant+agent. + * [claude-infra] Multi-instance support. + */ +export function isSessionForAgent( + sessionKey: string, + userId: string, + agentId: string = DEFAULT_AGENT_ID, +): boolean { + return sessionKey.startsWith(`${TENANT_PREFIX}${userId}${SEPARATOR}${agentId}${SEPARATOR}`); +} + +/** + * Build the AgentCore memory namespace for a tenant+agent. + * [claude-infra] Multi-instance: each agent instance has isolated memory. + */ +export function buildTenantMemoryNamespace( + userId: string, + agentId: string = DEFAULT_AGENT_ID, +): string { + return `${TENANT_PREFIX}${userId}${SEPARATOR}${agentId}`; +} diff --git a/src/hyperion/tenant-config-loader.test.ts b/src/hyperion/tenant-config-loader.test.ts new file mode 100644 index 0000000000000..9f64db8887727 --- /dev/null +++ b/src/hyperion/tenant-config-loader.test.ts @@ -0,0 +1,249 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import type { HyperionDynamoDBClient } from "./dynamodb-client.js"; +import { TenantConfigLoader, TenantNotFoundError } from "./tenant-config-loader.js"; +import type { ChannelLink, TenantConfig } from "./types.js"; +import type { UserCredentialStore } from "./user-credential-store.js"; + +function createMockFns() { + return { + getTenantConfig: vi.fn(), + listTenantAgents: vi.fn(), + putTenantConfig: vi.fn(), + deleteTenantConfig: vi.fn(), + getChannelLink: vi.fn(), + getChannelLinksForUser: vi.fn(), + putChannelLink: vi.fn(), + deleteChannelLink: vi.fn(), + getPairingCode: vi.fn(), + putPairingCode: vi.fn(), + deletePairingCode: vi.fn(), + getUserCredentials: vi.fn(), + putUserCredentials: vi.fn(), + deleteUserCredentials: vi.fn(), + }; +} + +function createMockCredFns() { + return { + getCredentials: vi.fn(), + putCredentials: vi.fn(), + deleteCredentials: vi.fn(), + invalidateCache: vi.fn(), + clearCache: vi.fn(), + }; +} + +const baseTenantConfig: TenantConfig = { + user_id: "user1", + agent_id: "main", + model: "anthropic.claude-sonnet-4-20250514", + custom_instructions: "Be helpful", + tools: ["brave_search", "calculator"], + skills: ["web"], +}; + +const channelLink: ChannelLink = { + platform: "telegram", + platform_user_id: "tg98765", + user_id: "user1", + agent_id: "main", + paired_at: "2026-01-01T00:00:00.000Z", + channel_account_id: "bot1", + channel_config: { streaming: "partial" }, +}; + +describe("TenantConfigLoader", () => { + let mockDb: ReturnType; + let mockCreds: ReturnType; + let loader: TenantConfigLoader; + + beforeEach(() => { + mockDb = createMockFns(); + mockCreds = createMockCredFns(); + loader = new TenantConfigLoader( + mockDb as unknown as HyperionDynamoDBClient, + {}, + mockCreds as unknown as UserCredentialStore, + ); + }); + + // -- loadTenantConfig -- + + describe("loadTenantConfig", () => { + test("builds config from DynamoDB data", async () => { + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([channelLink]); + mockCreds.getCredentials.mockResolvedValue(null); + + const config = await loader.loadTenantConfig("user1"); + + expect(config).toHaveProperty( + "agents.list.0.model.primary", + "anthropic.claude-sonnet-4-20250514", + ); + expect(config).toHaveProperty("tools.allow", ["brave_search", "calculator"]); + expect(config).toHaveProperty("channels.telegram"); + expect(config).toHaveProperty("channels.telegram.enabled", true); + }); + + test("throws TenantNotFoundError when config missing", async () => { + mockDb.getTenantConfig.mockResolvedValue(null); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + await expect(loader.loadTenantConfig("nonexistent")).rejects.toThrow(TenantNotFoundError); + }); + + test("filters channel links by agentId", async () => { + const workLink: ChannelLink = { ...channelLink, agent_id: "work" }; + const mainLink: ChannelLink = { + ...channelLink, + platform_user_id: "tg11111", + agent_id: "main", + }; + + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([workLink, mainLink]); + mockCreds.getCredentials.mockResolvedValue(null); + + const config = await loader.loadTenantConfig("user1", "main"); + + // Only mainLink should be included (filtered to agent_id=main) + expect(config).toHaveProperty("channels.telegram"); + const plain = JSON.parse(JSON.stringify(config)); + const accountKeys = Object.keys(plain.channels.telegram.accounts); + expect(accountKeys).toHaveLength(1); + // The account's allowFrom should reference mainLink's platform_user_id + expect(plain.channels.telegram.accounts[accountKeys[0]].allowFrom).toEqual(["tg11111"]); + }); + + test("injects model_keys from credentials", async () => { + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue({ + model_keys: { openai: "sk-test123" }, + }); + + const config = await loader.loadTenantConfig("user1"); + expect(config).toHaveProperty("models.providers.openai.apiKey", "sk-test123"); + }); + + test("injects channel_tokens into channel accounts", async () => { + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([channelLink]); + mockCreds.getCredentials.mockResolvedValue({ + channel_tokens: { telegram: "bot-token-123" }, + }); + + const config = await loader.loadTenantConfig("user1"); + expect(config).toHaveProperty("channels.telegram.accounts"); + const plain = JSON.parse(JSON.stringify(config)); + expect(Object.values(plain.channels.telegram.accounts)[0]).toHaveProperty( + "botToken", + "bot-token-123", + ); + }); + }); + + // -- caching -- + + describe("caching", () => { + test("returns cached config on second call", async () => { + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + const config1 = await loader.loadTenantConfig("user1"); + const config2 = await loader.loadTenantConfig("user1"); + + expect(config1).toBe(config2); // same reference + expect(mockDb.getTenantConfig).toHaveBeenCalledTimes(1); + }); + + test("separate cache keys for different agents", async () => { + const workConfig = { ...baseTenantConfig, agent_id: "work", model: "gpt-4" }; + mockDb.getTenantConfig + .mockResolvedValueOnce(baseTenantConfig) + .mockResolvedValueOnce(workConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + const main = await loader.loadTenantConfig("user1", "main"); + const work = await loader.loadTenantConfig("user1", "work"); + + expect(main).toHaveProperty( + "agents.list.0.model.primary", + "anthropic.claude-sonnet-4-20250514", + ); + expect(work).toHaveProperty("agents.list.0.model.primary", "gpt-4"); + expect(mockDb.getTenantConfig).toHaveBeenCalledTimes(2); + }); + + test("invalidateCache forces refetch", async () => { + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + await loader.loadTenantConfig("user1"); + loader.invalidateCache("user1"); + await loader.loadTenantConfig("user1"); + + expect(mockDb.getTenantConfig).toHaveBeenCalledTimes(2); + }); + + test("clearCache empties all entries", async () => { + mockDb.getTenantConfig.mockResolvedValue(baseTenantConfig); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + await loader.loadTenantConfig("user1"); + await loader.loadTenantConfig("user2"); + loader.clearCache(); + await loader.loadTenantConfig("user1"); + + // getTenantConfig called 3 times: user1, user2, user1 (after clear) + expect(mockDb.getTenantConfig).toHaveBeenCalledTimes(3); + }); + }); + + // -- custom_instructions / profile merging -- + + describe("agent config merging", () => { + test("applies custom_instructions to agents config", async () => { + mockDb.getTenantConfig.mockResolvedValue({ + ...baseTenantConfig, + custom_instructions: "Always respond in French", + }); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + const config = await loader.loadTenantConfig("user1"); + expect(config).toHaveProperty("agents.list.0.customInstructions", "Always respond in French"); + }); + + test("applies profile settings to agents config", async () => { + mockDb.getTenantConfig.mockResolvedValue({ + ...baseTenantConfig, + profile: { name: "TestBot", avatar: "robot" }, + }); + mockDb.getChannelLinksForUser.mockResolvedValue([]); + mockCreds.getCredentials.mockResolvedValue(null); + + const config = await loader.loadTenantConfig("user1"); + expect(config).toHaveProperty("agents.list.0.name", "TestBot"); + expect(config).toHaveProperty("agents.list.0.avatar", "robot"); + }); + }); + + // -- TenantNotFoundError -- + + describe("TenantNotFoundError", () => { + test("has correct name and tenantId", () => { + const err = new TenantNotFoundError("user999"); + expect(err.name).toBe("TenantNotFoundError"); + expect(err.tenantId).toBe("user999"); + expect(err.message).toBe("Tenant not found: user999"); + expect(err).toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/hyperion/tenant-config-loader.ts b/src/hyperion/tenant-config-loader.ts new file mode 100644 index 0000000000000..9ed5648e97cde --- /dev/null +++ b/src/hyperion/tenant-config-loader.ts @@ -0,0 +1,272 @@ +import type { ChannelsConfig } from "../config/types.channels.js"; +import type { OpenClawConfig } from "../config/types.js"; +import type { HyperionDynamoDBClient } from "./dynamodb-client.js"; +import { + DEFAULT_AGENT_ID, + type CachedTenantConfig, + type ChannelLink, + type TenantConfig, + type UserCredentials, +} from "./types.js"; +import type { UserCredentialStore } from "./user-credential-store.js"; + +/** Config cache TTL: 1 minute. */ +const CONFIG_CACHE_TTL_MS = 60_000; + +/** Maximum cache entries to prevent unbounded growth. */ +const CONFIG_CACHE_MAX_SIZE = 10_000; + +/** + * Loads and assembles OpenClawConfig for a specific tenant from DynamoDB. + * + * Replaces OpenClaw's filesystem-based `loadConfig()` (src/config/io.ts) with + * a multi-tenant DynamoDB-backed implementation: + * + * 1. Fetches tenant_config (profile, model, tools, skills, etc.) + * 2. Fetches all channel_config links for the user via GSI + * 3. Assembles the OpenClawConfig.channels section dynamically + * 4. Merges tenant base config with channel configs into a complete OpenClawConfig + * + * Caching: in-memory LRU with 1-minute TTL per tenant. + */ +export class TenantConfigLoader { + private readonly dbClient: HyperionDynamoDBClient; + private readonly credentialStore: UserCredentialStore | null; + private readonly cache = new Map(); + private readonly defaultConfig: Partial; + + constructor( + dbClient: HyperionDynamoDBClient, + defaultConfig?: Partial, + credentialStore?: UserCredentialStore, + ) { + this.dbClient = dbClient; + this.defaultConfig = defaultConfig ?? {}; + this.credentialStore = credentialStore ?? null; + } + + /** + * Load the full OpenClawConfig for a tenant+agent, with caching. + * [claude-infra] Multi-instance: agentId defaults to "main". + */ + async loadTenantConfig( + tenantId: string, + agentId: string = DEFAULT_AGENT_ID, + ): Promise { + const cacheKey = `${tenantId}:${agentId}`; + const cached = this.cache.get(cacheKey); + if (cached && Date.now() - cached.cachedAt < CONFIG_CACHE_TTL_MS) { + return cached.config; + } + + const config = await this.buildTenantConfig(tenantId, agentId); + + // Evict oldest entries if cache is full. + if (this.cache.size >= CONFIG_CACHE_MAX_SIZE) { + const oldestKey = this.cache.keys().next().value; + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + this.cache.set(cacheKey, { config, cachedAt: Date.now() }); + return config; + } + + /** + * Invalidate the cached config for a tenant+agent. + * Call this when tenant config or channel links are updated. + * [claude-infra] Multi-instance: agentId defaults to "main". + */ + invalidateCache(tenantId: string, agentId: string = DEFAULT_AGENT_ID): void { + this.cache.delete(`${tenantId}:${agentId}`); + } + + /** + * Clear the entire config cache. + */ + clearCache(): void { + this.cache.clear(); + } + + /** + * Build the full OpenClawConfig for a tenant by reading DynamoDB. + */ + private async buildTenantConfig( + tenantId: string, + agentId: string = DEFAULT_AGENT_ID, + ): Promise { + // Fetch tenant config, channel links, and credentials in parallel. + // [claude-infra] Multi-instance: config + credentials keyed by (userId, agentId), + // channel links filtered to matching agent_id. + const [tenantConfig, allChannelLinks, credentials] = await Promise.all([ + this.dbClient.getTenantConfig(tenantId, agentId), + this.dbClient.getChannelLinksForUser(tenantId), + this.credentialStore?.getCredentials(tenantId, agentId) ?? Promise.resolve(null), + ]); + // Filter channel links to only those bound to this agent instance. + const channelLinks = allChannelLinks.filter( + (link) => (link.agent_id || DEFAULT_AGENT_ID) === agentId, + ); + + if (!tenantConfig) { + throw new TenantNotFoundError(tenantId); + } + + const channels = this.assembleChannelsConfig(channelLinks); + return this.mergeConfig(tenantConfig, channels, credentials); + } + + /** + * Assemble OpenClaw's ChannelsConfig from the tenant's channel links. + * + * Each channel link produces an entry under the appropriate platform key. + * Multiple links for the same platform use the multi-account pattern: + * channels.telegram.accounts[accountId] = { ...config, allowFrom: [platformUserId] } + */ + private assembleChannelsConfig(channelLinks: ChannelLink[]): ChannelsConfig { + const channels: ChannelsConfig = {}; + + for (const link of channelLinks) { + const platform = link.platform; + const accountId = link.channel_account_id || "default"; + + if (!channels[platform]) { + channels[platform] = { + enabled: true, + accounts: {}, + defaultAccount: accountId, + }; + } + + channels[platform].accounts[accountId] = this.buildAccountConfig(link); + } + + return channels; + } + + /** + * Build a per-account channel config from a channel link. + * The platform_user_id is automatically added to allowFrom + * to authorize the linked external identity. + */ + private buildAccountConfig(link: ChannelLink): Record { + const runtimeConfig = link.channel_config ?? {}; + return { + ...runtimeConfig, + // Authorize this specific external user for DM access. + allowFrom: [link.platform_user_id], + // DM policy is "open" for paired users — they've already been verified. + dmPolicy: runtimeConfig.dmPolicy ?? "open", + }; + } + + /** + * Merge tenant-level settings with the assembled channel config + * and decrypted credentials into a complete OpenClawConfig. + */ + private mergeConfig( + tenant: TenantConfig, + channels: ChannelsConfig, + credentials: UserCredentials | null, + ): OpenClawConfig { + const config: OpenClawConfig = { + ...this.defaultConfig, + channels, + }; + + // Apply tenant-level agent configuration (model, profile, custom instructions). + // OC agents are under config.agents.list — each entry is an AgentConfig. + const baseAgent = config.agents?.list?.[0] ?? { id: "default" }; + const agentPatches: Record = {}; + + if (tenant.model) { + agentPatches.model = { primary: tenant.model }; + } + if (tenant.custom_instructions) { + agentPatches.customInstructions = tenant.custom_instructions; + } + if (tenant.profile) { + Object.assign(agentPatches, tenant.profile); + } + + if (Object.keys(agentPatches).length > 0) { + config.agents = { + ...config.agents, + list: [{ ...baseAgent, ...agentPatches }], + }; + } + + // Inject per-user model provider API keys from encrypted credential store. + if (credentials?.model_keys) { + const providers = { ...config.models?.providers }; + for (const [provider, apiKey] of Object.entries(credentials.model_keys)) { + providers[provider] = { ...providers[provider], apiKey }; + } + config.models = { ...config.models, providers }; + } + + // Apply tenant-level tool permissions. + if (tenant.tools) { + config.tools = { + ...config.tools, + allow: tenant.tools, + }; + } + + // Inject per-user tool API keys from encrypted credential store. + if (credentials?.tool_keys) { + const web = config.tools?.web ?? {}; + const search = web.search ?? {}; + for (const [toolName, apiKey] of Object.entries(credentials.tool_keys)) { + if ( + toolName === "brave_search" || + toolName === "gemini" || + toolName === "grok" || + toolName === "kimi" || + toolName === "perplexity" + ) { + config.tools = { + ...config.tools, + web: { ...web, search: { ...search, apiKey } }, + }; + } + } + } + + // Apply tenant-level skill permissions. + if (tenant.skills) { + config.skills = { + ...config.skills, + allowBundled: tenant.skills, + }; + } + + // Inject per-user channel bot tokens into assembled channel accounts. + if (credentials?.channel_tokens) { + for (const [platform, token] of Object.entries(credentials.channel_tokens)) { + const platformConfig = channels[platform]; + if (platformConfig?.accounts) { + for (const account of Object.values(platformConfig.accounts)) { + account.botToken = token; + } + } + } + } + + return config; + } +} + +/** + * Thrown when a tenant_id cannot be found in the tenant_config table. + */ +export class TenantNotFoundError extends Error { + public readonly tenantId: string; + + constructor(tenantId: string) { + super(`Tenant not found: ${tenantId}`); + this.name = "TenantNotFoundError"; + this.tenantId = tenantId; + } +} diff --git a/src/hyperion/types.ts b/src/hyperion/types.ts new file mode 100644 index 0000000000000..f5114e96ed71c --- /dev/null +++ b/src/hyperion/types.ts @@ -0,0 +1,204 @@ +import type { OpenClawConfig } from "../config/types.js"; + +/** + * Supported external channel platforms for Hyperion. + */ +export type HyperionPlatform = "telegram" | "slack" | "whatsapp" | "discord"; + +/** + * Default agent ID for single-instance users (backwards compatible). + * [claude-infra] Multi-instance support + */ +export const DEFAULT_AGENT_ID = "main"; + +/** + * A channel link record stored in the channel_config DynamoDB table. + * Maps an external platform identity to an internal Hyperion user_id. + * + * Table schema: + * PK: platform (HyperionPlatform) + * SK: platform_user_id (string) + * GSI1PK: user_id (string) + */ +export type ChannelLink = { + /** External platform identifier (e.g., "telegram", "slack"). */ + platform: HyperionPlatform; + /** The user's identity on the external platform (e.g., Telegram user ID, Slack team+user). */ + platform_user_id: string; + /** Internal Hyperion user ID this channel is linked to. */ + user_id: string; + /** Agent instance this channel is bound to. Default: "main". [claude-infra] */ + agent_id: string; + /** ISO timestamp when the channel was paired. */ + paired_at: string; + /** Account ID within the channel (e.g., bot token alias). */ + channel_account_id: string; + /** Platform-specific channel runtime configuration. */ + channel_config: ChannelRuntimeConfig; +}; + +/** + * Platform-specific runtime configuration stored per channel link. + * Subset of OpenClaw's per-account channel config, relevant for multi-tenant operation. + */ +export type ChannelRuntimeConfig = { + /** DM policy for this channel link. */ + dmPolicy?: "pairing" | "open" | "disabled"; + /** Streaming mode for message delivery. */ + streaming?: "off" | "partial" | "block" | "progress"; + /** Max text chunk size for message splitting. */ + textChunkLimit?: number; + /** Reply threading mode. */ + replyToMode?: string; + /** Group message policy. */ + groupPolicy?: Record; + /** Bot token or Secrets Manager ARN reference. */ + credentialRef?: string; + /** Additional platform-specific settings. */ + [key: string]: unknown; +}; + +/** + * Tenant configuration stored in the tenant_config DynamoDB table. + * + * Table schema: + * PK: user_id (string), SK: agent_id (string) [claude-infra] + */ +export type TenantConfig = { + /** Internal Hyperion user ID. */ + user_id: string; + /** Agent instance ID. Default: "main". [claude-infra] */ + agent_id: string; + /** User's display name. */ + display_name?: string; + /** User's preferred model configuration. */ + model?: string; + /** User's custom instructions for the agent. */ + custom_instructions?: string; + /** User's subscription plan. */ + plan?: "free" | "pro" | "enterprise"; + /** Usage limits for the tenant. */ + limits?: { + messages_per_day?: number; + messages_per_month?: number; + }; + /** Agent profile/persona settings. */ + profile?: Record; + /** Enabled tools for this tenant. */ + tools?: string[]; + /** Enabled skills for this tenant. */ + skills?: string[]; + /** ISO timestamp when config was last updated. */ + updated_at?: string; +}; + +/** + * A pairing code record stored in the pairing_codes DynamoDB table. + * + * Table schema: + * PK: code (string) + * TTL: expires_at (number, epoch seconds) + */ +export type PairingCode = { + /** The human-friendly pairing code. */ + code: string; + /** Internal user ID that initiated the pairing. */ + user_id: string; + /** Agent instance to bind the channel to. Default: "main". [claude-infra] */ + agent_id: string; + /** Target platform for the pairing. */ + platform: HyperionPlatform; + /** ISO timestamp when the code was created. */ + created_at: string; + /** TTL attribute — epoch seconds when this code expires. */ + expires_at: number; + /** Optional metadata from the pairing request. */ + meta?: Record; +}; + +/** + * Result of resolving an inbound channel message to a tenant. + */ +export type ChannelIdentityResolution = { + /** The resolved internal user ID. */ + user_id: string; + /** The resolved agent instance ID. [claude-infra] */ + agent_id: string; + /** The channel link record. */ + channelLink: ChannelLink; + /** The assembled OpenClawConfig for this tenant+agent. */ + config: OpenClawConfig; +}; + +/** + * Per-user credentials stored encrypted in the user_credentials DynamoDB table. + * Each field is optional — users only store the keys they actually use. + * + * Values are encrypted at rest via KMS envelope encryption with + * encryption context { user_id: "" } to ensure per-tenant isolation. + */ +export type UserCredentials = { + /** Model provider API keys (e.g., openai, anthropic, google). */ + model_keys?: Record; + /** Tool API keys (e.g., brave_search, firecrawl, perplexity). */ + tool_keys?: Record; + /** Channel bot tokens (e.g., telegram, discord, slack). */ + channel_tokens?: Record; + /** Arbitrary additional credentials for custom tools/plugins. */ + custom?: Record; +}; + +/** + * Raw record stored in the user_credentials DynamoDB table. + * The credentials_blob field contains KMS-encrypted JSON of UserCredentials. + */ +export type UserCredentialsRecord = { + /** Internal Hyperion user ID (PK). */ + user_id: string; + /** Agent instance ID (SK). Default: "main". [claude-infra] */ + agent_id: string; + /** KMS-encrypted credentials blob (base64-encoded ciphertext). */ + credentials_blob: string; + /** KMS key ID used for encryption (for key rotation tracking). */ + kms_key_id: string; + /** ISO timestamp when credentials were last updated. */ + updated_at: string; +}; + +/** + * Configuration for the Hyperion DynamoDB integration. + */ +export type HyperionDynamoDBConfig = { + /** AWS region for DynamoDB. */ + region: string; + /** Tenant config table name. */ + tenantConfigTableName: string; + /** Channel config table name. */ + channelConfigTableName: string; + /** Pairing codes table name. */ + pairingCodesTableName: string; + /** User credentials table name. */ + userCredentialsTableName: string; + /** KMS key ID (ARN or alias) for credential encryption. */ + credentialsKmsKeyId: string; + /** GSI name for user_id lookups on channel_config. */ + channelConfigUserIdIndexName: string; + /** Optional DynamoDB endpoint override (for local development). */ + endpoint?: string; +}; + +/** + * Cache entry for tenant configs with TTL. + */ +export type CachedTenantConfig = { + config: OpenClawConfig; + cachedAt: number; +}; + +/** + * Cache entry for channel identity resolution with TTL. + */ +export type CachedChannelIdentity = { + channelLink: ChannelLink; + cachedAt: number; +}; diff --git a/src/hyperion/user-credential-store.ts b/src/hyperion/user-credential-store.ts new file mode 100644 index 0000000000000..03a13b25ebe0f --- /dev/null +++ b/src/hyperion/user-credential-store.ts @@ -0,0 +1,186 @@ +import type { HyperionDynamoDBClient } from "./dynamodb-client.js"; +import { DEFAULT_AGENT_ID, type UserCredentials } from "./types.js"; + +/** + * Minimal KMS client interface. + * Accepts any AWS SDK v3 KMSClient-compatible implementation. + */ +export type KMSClient = { + send(command: unknown): Promise; +}; + +/** Credential cache TTL: 2 minutes (shorter than config cache for security). */ +const CREDENTIAL_CACHE_TTL_MS = 2 * 60_000; + +/** Maximum credential cache entries. */ +const CREDENTIAL_CACHE_MAX_SIZE = 10_000; + +type CachedCredentials = { + credentials: UserCredentials; + cachedAt: number; +}; + +/** + * Manages per-user API keys and credentials with KMS envelope encryption. + * + * Security model: + * - Credentials are encrypted client-side using KMS before writing to DynamoDB. + * - KMS encryption context includes { user_id } so one user's ciphertext + * cannot be decrypted with another user's context (cross-tenant isolation). + * - DynamoDB never stores plaintext credentials. + * - KMS key rotation is handled by AWS (enabled in CDK). + * - Decrypted values are cached in-memory with a short TTL to avoid + * excessive KMS calls during high-frequency request paths. + */ +export class UserCredentialStore { + private readonly dbClient: HyperionDynamoDBClient; + private readonly kmsClient: KMSClient; + private readonly kmsKeyId: string; + private readonly cache = new Map(); + + constructor(dbClient: HyperionDynamoDBClient, kmsClient: KMSClient, kmsKeyId: string) { + this.dbClient = dbClient; + this.kmsClient = kmsClient; + this.kmsKeyId = kmsKeyId; + } + + /** + * Retrieve and decrypt credentials for a user+agent. + * [claude-infra] Multi-instance: looks up agent-specific, falls back to shared. + * Returns null if the user has no stored credentials. + */ + async getCredentials( + userId: string, + agentId: string = DEFAULT_AGENT_ID, + ): Promise { + const cacheKey = `${userId}:${agentId}`; + const cached = this.cache.get(cacheKey); + if (cached && Date.now() - cached.cachedAt < CREDENTIAL_CACHE_TTL_MS) { + return cached.credentials; + } + + const record = await this.dbClient.getUserCredentials(userId, agentId); + if (!record) { + return null; + } + + const credentials = await this.decrypt(record.credentials_blob, userId); + + if (this.cache.size >= CREDENTIAL_CACHE_MAX_SIZE) { + const oldestKey = this.cache.keys().next().value; + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + this.cache.set(cacheKey, { credentials, cachedAt: Date.now() }); + return credentials; + } + + /** + * Encrypt and store credentials for a user+agent. + * [claude-infra] Multi-instance: stores with composite key. + */ + async putCredentials( + userId: string, + credentials: UserCredentials, + agentId: string = DEFAULT_AGENT_ID, + ): Promise { + const blob = await this.encrypt(credentials, userId); + + await this.dbClient.putUserCredentials({ + user_id: userId, + agent_id: agentId, + credentials_blob: blob, + kms_key_id: this.kmsKeyId, + updated_at: new Date().toISOString(), + }); + + const cacheKey = `${userId}:${agentId}`; + this.cache.set(cacheKey, { credentials, cachedAt: Date.now() }); + } + + /** + * Delete credentials for a user+agent. + * [claude-infra] Multi-instance: deletes specific agent's credentials. + */ + async deleteCredentials(userId: string, agentId: string = DEFAULT_AGENT_ID): Promise { + await this.dbClient.deleteUserCredentials(userId, agentId); + this.cache.delete(`${userId}:${agentId}`); + } + + /** + * Invalidate the cached credentials for a user+agent. + */ + invalidateCache(userId: string, agentId: string = DEFAULT_AGENT_ID): void { + this.cache.delete(`${userId}:${agentId}`); + } + + clearCache(): void { + this.cache.clear(); + } + + /** + * Encrypt a UserCredentials object using KMS. + * Returns base64-encoded ciphertext. + */ + private async encrypt(credentials: UserCredentials, userId: string): Promise { + const { EncryptCommand } = await import("@aws-sdk/client-kms"); + const plaintext = new TextEncoder().encode(JSON.stringify(credentials)); + + const result = await this.kmsClient.send( + new EncryptCommand({ + KeyId: this.kmsKeyId, + Plaintext: plaintext, + EncryptionContext: { user_id: userId }, + }), + ); + + const ciphertextBlob = (result as { CiphertextBlob?: Uint8Array }).CiphertextBlob; + if (!ciphertextBlob) { + throw new Error(`KMS encryption failed for user ${userId}`); + } + + return uint8ArrayToBase64(ciphertextBlob); + } + + /** + * Decrypt a base64-encoded ciphertext blob back to UserCredentials. + * The encryption context must match what was used during encryption. + */ + private async decrypt(blob: string, userId: string): Promise { + const { DecryptCommand } = await import("@aws-sdk/client-kms"); + const ciphertext = base64ToUint8Array(blob); + + const result = await this.kmsClient.send( + new DecryptCommand({ + CiphertextBlob: ciphertext, + EncryptionContext: { user_id: userId }, + }), + ); + + const plaintext = (result as { Plaintext?: Uint8Array }).Plaintext; + if (!plaintext) { + throw new Error(`KMS decryption failed for user ${userId}`); + } + + return JSON.parse(new TextDecoder().decode(plaintext)) as UserCredentials; + } +} + +function uint8ArrayToBase64(bytes: Uint8Array): string { + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +function base64ToUint8Array(base64: string): Uint8Array { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +}