diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5d2dfb..697bf20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Thanks for considering a contribution. Adrian is open-source under the [Apache 2 ## Local dev setup -The Python SDK lives at `sdk/python/` (the TypeScript SDK lives at `sdk/typescript/`). From the repo root: +The Python SDK lives at `sdk/python/`. From the repo root: ```sh make sdk-install # creates .venv and installs sdk + dev deps via uv @@ -29,6 +29,17 @@ source .venv/bin/activate pre-commit install # wires the git hook ``` +The TypeScript SDK lives at `sdk/typescript/` as an npm workspace with the core package (`@secureagentics/adrian`). From that directory: + +```sh +cd sdk/typescript +npm install +npm run build +npm test +``` + +See [`sdk/typescript/README.md`](sdk/typescript/README.md) for usage examples. + After `pre-commit install`, every `git commit` runs the configured hooks on staged files: `ruff format`, `ruff check --fix`, `basedpyright` on `sdk/python/adrian/`, plus the standard whitespace / YAML / TOML checks. Hooks that diff --git a/README.md b/README.md index 04ea25d..0b3aa48 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ --- -Adrian is an open-source, [AARM-aligned](https://aarm.dev) runtime security monitoring and control engine for AI agents. It analyses both agent activity logs (tool calls, actions, outputs) and reasoning traces to detect malicious, misaligned, or out-of-remit behaviour, and optionally intervene in-flight. Python SDK with a two-line install to LangChain agents. +Adrian is an open-source, [AARM-aligned](https://aarm.dev) runtime security monitoring and control engine for AI agents. It analyses both agent activity logs (tool calls, actions, outputs) and reasoning traces to detect malicious, misaligned, or out-of-remit behaviour, and optionally intervene in-flight. SDKs are available for Python (LangChain) and TypeScript (see [sdk/typescript/README.md](sdk/typescript/README.md)).

Documentation diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore new file mode 100644 index 0000000..7c96d6d --- /dev/null +++ b/sdk/typescript/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +coverage/ +.vite/ +.env diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md new file mode 100644 index 0000000..8ce007c --- /dev/null +++ b/sdk/typescript/README.md @@ -0,0 +1,160 @@ +# Adrian TypeScript SDK + +Monorepo for the Adrian TypeScript SDK. The core package owns the event pipeline: event pairing, PII redaction, JSONL logging, WebSocket streaming, policy verdicts, and shared capture helpers. + +## Packages + +| Package | npm name | Install | Import | +|---|---|---|---| +| Core | `@secureagentics/adrian` | `npm install @secureagentics/adrian` | `import { adrian } from "@secureagentics/adrian"` | + +## Quick start + +```ts +import { adrian } from "@secureagentics/adrian"; + +await adrian.init({ apiKey: process.env.ADRIAN_API_KEY, wsUrl: null }); +// Wire callbacks via adrian.getHandler() or custom handlers — see below. +await adrian.shutdown(); +``` + +Named exports (`init`, `shutdown`, etc.) remain available for compatibility. + +## Core exports + +| Export | Description | +|---|---| +| `adrian.init(options?)` | Initialise the SDK | +| `adrian.shutdown()` | Flush handlers and tear down | +| `adrian.getHandler()` | Access the callback handler for manual wiring | +| `adrian.getWebSocketClient()` | Access the WebSocket client | +| `AdrianCallbackHandler` | Event callback handler class | +| `JSONLHandler` | Local JSONL event sink | + +## Environment + +Explicit `init()` options take precedence over environment variables. + +| Variable | Description | +|---|---| +| `ADRIAN_API_KEY` | API key used for WebSocket authentication | +| `ADRIAN_LOG_FILE` | Local JSONL log path (default: `events.jsonl`) | +| `ADRIAN_WS_URL` | WebSocket endpoint (default: `ws://localhost:8080/ws`) | +| `ADRIAN_SESSION_ID` | Session identifier for grouping events | +| `ADRIAN_BLOCK_TIMEOUT` | Seconds to wait for a BLOCK-mode verdict before fail-open (default: `30`) | +| `ADRIAN_REPLAY_BUFFER_FRAMES` | WebSocket replay buffer size (default: `1000`) | + +Set `wsUrl: null` in `init()` for local JSONL logging without a WebSocket connection (even when `ADRIAN_WS_URL` is set): + +```ts +import { adrian } from "@secureagentics/adrian"; + +await adrian.init({ + wsUrl: null, + logFile: "events.jsonl", + onEvent: (eventType, data, runId, parentRunId, eventId) => { + console.log({ eventType, runId, parentRunId, eventId, data }); + }, +}); + +await adrian.shutdown(); +``` + +## Policy and BLOCK mode + +When connected over WebSocket and the dashboard policy is in **BLOCK** or **HITL** mode, the SDK waits for backend verdicts on tool calls proposed by an LLM turn. In **BLOCK** mode, if no verdict arrives within `blockTimeout` seconds, the SDK **fail-open** and allows execution (matching the Python SDK). Dashboard-configurable failure policy is planned for a later release. + +## Manual callback wiring + +```ts +import { adrian } from "@secureagentics/adrian"; + +await adrian.init(); +const handler = adrian.getHandler(); +// Pass handler into your framework's callback system. +``` + +For custom integrations, pair an LLM start and end with the same `runId`: + +```ts +import { randomUUID } from "node:crypto"; +import { adrian } from "@secureagentics/adrian"; + +await adrian.init({ wsUrl: null }); + +const handler = adrian.getHandler(); +const runId = randomUUID(); + +await handler?.handleChatModelStart( + { name: "custom-model" }, + [[{ role: "user", content: "Hello" }]], + runId, +); + +await handler?.handleLLMEnd( + { + output: "Hi there", + toolCalls: [], + usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 }, + }, + runId, +); + +await adrian.shutdown(); +``` + +Manual tool events work the same way: + +```ts +const toolRunId = randomUUID(); + +await handler?.handleToolStart( + { name: "lookup_user" }, + JSON.stringify({ userId: "user_123" }), + toolRunId, + undefined, + { tool_call_id: "call_123", metadata: { source: "custom-integration" } }, +); + +await handler?.handleToolEnd(JSON.stringify({ ok: true }), toolRunId); +``` + +## Custom event handlers + +Provide `handlers` when you want to replace the default JSONL/WebSocket sinks: + +```ts +import { adrian, type EventHandler, type PairedEvent } from "@secureagentics/adrian"; + +const handler: EventHandler = { + onPairedEvent(event: PairedEvent) { + console.log(event.pairType, event.eventId); + }, + close() { + // Flush resources if needed. + }, +}; + +await adrian.init({ handlers: [handler] }); +``` + +## Subpath export + +`@secureagentics/adrian/capture` exposes shared LLM capture helpers used internally by provider packages. + +## Development + +From this directory: + +```sh +npm install +npm run build +npm test +``` + +Build or test the core package only: + +```sh +npm run build -w @secureagentics/adrian +npm test -w @secureagentics/adrian +``` diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 0000000..94f38bb --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,2710 @@ +{ + "name": "@secureagentics/adrian-workspace", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@secureagentics/adrian-workspace", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@secureagentics/adrian": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/protobufjs": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.3.tgz", + "integrity": "sha512-alQyzT0j401LGBtwsqu6uprjR6pfNH1UJf9N6GBFMjIcd+HzTe0/HrjAbFCqun+zvnfLarrxAtMM2xvZ+kFZ5A==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "packages/core": { + "name": "@secureagentics/adrian", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index c9a6ace..cc273d4 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,5 +1,5 @@ { - "name": "adrian-typescript", + "name": "@secureagentics/adrian-workspace", "private": true, "description": "Adrian TypeScript SDK monorepo (npm workspaces).", "workspaces": [ @@ -12,6 +12,7 @@ }, "devDependencies": { "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.7" diff --git a/sdk/typescript/packages/core/package.json b/sdk/typescript/packages/core/package.json new file mode 100644 index 0000000..6c39e52 --- /dev/null +++ b/sdk/typescript/packages/core/package.json @@ -0,0 +1,52 @@ +{ + "name": "@secureagentics/adrian", + "version": "1.0.0", + "description": "Core SDK for Adrian multi-agent security monitoring in Node.js.", + "license": "Apache-2.0", + "author": "Secure Agentics ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./capture": { + "types": "./dist/capture/index.d.ts", + "import": "./dist/capture/index.js", + "require": "./dist/capture/index.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup src/index.ts src/capture/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "ai", + "agents", + "security", + "monitoring", + "observability", + "llm", + "multi-agent", + "prompt-injection" + ], + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/packages/core/src/capture/common.ts b/sdk/typescript/packages/core/src/capture/common.ts new file mode 100644 index 0000000..ebd18a2 --- /dev/null +++ b/sdk/typescript/packages/core/src/capture/common.ts @@ -0,0 +1,209 @@ +import { randomUUID } from "node:crypto"; +import { currentConfig } from "../config.js"; +import type { AdrianCallbackHandler } from "../handler.js"; +import { runWithInvocationId } from "../context.js"; +import { assertToolCallsAllowed } from "../policy.js"; +import { getWebSocketClient } from "../registry.js"; +import type { CallbackMetadata, ChatMessage, LlmEndData, TokenUsage, ToolArgs, ToolCallRecord } from "../types.js"; + +/** Gate tool calls after the paired LLM event has been emitted (maps tool-call ids on the WS client). */ +export async function gateLlmEndData(end: LlmEndData): Promise { + await assertToolCallsAllowed( + end.toolCalls.map((call) => call.id), + getWebSocketClient(), + currentConfig()?.blockTimeout ?? 30, + ); +} + +export interface LlmCaptureInput { + model: string; + messages: ChatMessage[]; + metadata?: CallbackMetadata | null; + parentRunId?: string; +} + +export async function captureLlmCall( + getHandler: () => AdrianCallbackHandler | null, + input: LlmCaptureInput, + execute: () => Promise, + extractOutput: (result: T) => LlmEndData | Promise, + afterPairedEmit?: (end: LlmEndData) => void | Promise, +): Promise { + const handler = getHandler(); + if (!handler) return execute(); + + const runId = randomUUID(); + return runWithInvocationId(randomUUID(), async () => { + await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata }); + try { + const result = await execute(); + const endData = await extractOutput(result); + await handler.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + return result; + } catch (error) { + await handler.handleLLMError(error, runId); + throw error; + } + }); +} + +export function captureLlmAsyncIterable( + getHandler: () => AdrianCallbackHandler | null, + input: LlmCaptureInput, + iterable: AsyncIterable, + aggregate: (chunk: T) => void, + extractOutput: () => LlmEndData | Promise, + afterPairedEmit?: (end: LlmEndData) => void | Promise, +): AsyncIterable { + const handler = getHandler(); + if (!handler) return iterable; + + const runId = randomUUID(); + const invocationId = randomUUID(); + + async function* wrapped(): AsyncGenerator { + await handler?.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata }); + yield* runWithInvocationId(invocationId, async function* () { + let emitted = false; + let failed = false; + try { + for await (const chunk of iterable) { + aggregate(chunk); + yield chunk; + } + emitted = true; + const endData = await extractOutput(); + await handler?.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + } catch (error) { + failed = true; + await handler?.handleLLMError(error, runId); + throw error; + } finally { + if (!emitted && !failed) { + const endData = await extractOutput(); + await handler?.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + } + } + }); + } + + return wrapped(); +} + +export function normalizeMessages(input: unknown): ChatMessage[] { + if (typeof input === "string") return [{ role: "user", content: input }]; + if (!Array.isArray(input)) return []; + return input.map((message) => { + const obj = message && typeof message === "object" ? message as Record : {}; + return { + role: String(obj.role), + content: stringifyContent(obj.content ?? obj.text), + }; + }); +} + +export function messagesFromPromptLike(args: Record): ChatMessage[] { + const system = args.instructions; + const messages = normalizeMessages(args.messages); + if (messages.length > 0) return prependSystem(system, messages); + if (typeof args.input === "string") return prependSystem(system, [{ role: "user", content: args.input }]); + const inputMessages = normalizeResponseInput(args.input); + if (inputMessages.length > 0) return prependSystem(system, inputMessages); + return prependSystem(system, []); +} + +/** Normalise OpenAI Responses API `input` arrays (roles, tool calls, tool outputs). */ +export function normalizeResponseInput(input: unknown): ChatMessage[] { + if (!Array.isArray(input)) return []; + const messages: ChatMessage[] = []; + for (const item of input) { + if (!item || typeof item !== "object") continue; + const obj = item as Record; + const type = String(obj.type); + + if (type === "function_call" || type === "tool_call") { + const name = String(obj.name); + const args = typeof obj.arguments === "string" ? obj.arguments : stringifyJson(obj.arguments); + messages.push({ role: "assistant", content: `[tool_call:${name}] ${args}` }); + continue; + } + if (type === "function_call_output") { + messages.push({ role: "tool", content: String(obj.output) }); + continue; + } + + const role = String(obj.role); + if (!role) continue; + messages.push({ + role: role === "developer" ? "system" : role, + content: stringifyContent(obj.content ?? obj.text), + }); + } + return messages; +} + +export function stringifyContent(value: unknown): string { + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value.map((part) => { + if (typeof part === "string") return part; + if (part && typeof part === "object") { + const obj = part as Record; + if (typeof obj.text === "string") return obj.text; + if (typeof obj.content === "string") return obj.content; + } + return stringifyJson(part); + }).join(""); + } + return stringifyJson(value); +} + +export function normalizeUsage(usage: unknown, promptKeys = ["promptTokens", "prompt_tokens", "input_tokens"], completionKeys = ["completionTokens", "completion_tokens", "output_tokens"]): TokenUsage | null { + if (!usage || typeof usage !== "object") return null; + const obj = usage as Record; + const promptTokens = numberFromKeys(obj, promptKeys); + const completionTokens = numberFromKeys(obj, completionKeys); + const totalTokens = numberFromKeys(obj, ["totalTokens", "total_tokens"]) ?? ((promptTokens ?? 0) + (completionTokens ?? 0)); + if (promptTokens === null && completionTokens === null && totalTokens === 0) return null; + return { promptTokens: promptTokens ?? 0, completionTokens: completionTokens ?? 0, totalTokens }; +} + +export function parseToolArgs(value: unknown): ToolArgs { + if (!value) return {}; + if (typeof value === "object" && !Array.isArray(value)) return value as ToolArgs; + if (typeof value !== "string") return {}; + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as ToolArgs : {}; + } catch { + return {}; + } +} + +export function emptyLlmEnd(output = "", toolCalls: ToolCallRecord[] = [], usage: TokenUsage | null = null): LlmEndData { + return { output, toolCalls, usage }; +} + +function prependSystem(system: unknown, messages: ChatMessage[]): ChatMessage[] { + return typeof system === "string" && system.length > 0 ? [{ role: "system", content: system }, ...messages] : messages; +} + +function numberFromKeys(obj: Record, keys: string[]): number | null { + for (const key of keys) { + const value = obj[key]; + if (typeof value === "number" && Number.isFinite(value)) return value; + } + return null; +} + +function stringifyJson(value: unknown): string { + if (value === null || value === undefined) return ""; + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} diff --git a/sdk/typescript/packages/core/src/capture/index.ts b/sdk/typescript/packages/core/src/capture/index.ts new file mode 100644 index 0000000..e9db7b2 --- /dev/null +++ b/sdk/typescript/packages/core/src/capture/index.ts @@ -0,0 +1 @@ +export * from "./common.js"; diff --git a/sdk/typescript/packages/core/src/config.ts b/sdk/typescript/packages/core/src/config.ts new file mode 100644 index 0000000..8a9e1dc --- /dev/null +++ b/sdk/typescript/packages/core/src/config.ts @@ -0,0 +1,116 @@ +import type { EventData, McpServer, VerdictContext } from "./types.js"; + +export type MaybePromise = T | Promise; +export type OnVerdictCallback = (ctx: VerdictContext) => MaybePromise; +export type OnBlockCallback = (ctx: VerdictContext) => MaybePromise; +export type OnAuditCallback = (ctx: VerdictContext) => MaybePromise; +export type OnEventCallback = ( + eventType: string, + data: EventData, + runId: string, + parentRunId: string | null, + eventId: string, +) => MaybePromise; +export type OnDisconnectCallback = (reason: string) => MaybePromise; +export type OnReconnectCallback = () => MaybePromise; +export type OnMcpServerCallback = (server: McpServer) => MaybePromise; + +export interface AdrianConfig { + apiKey: string | null; + logFile: string; + logLevel: string | null; + sessionId: string; + wsUrl: string | null; + blockTimeout: number; + onEvent: OnEventCallback | null; + onVerdict: OnVerdictCallback | null; + onBlock: OnBlockCallback | null; + onAudit: OnAuditCallback | null; + onDisconnect: OnDisconnectCallback | null; + onReconnect: OnReconnectCallback | null; + onMcpServer: OnMcpServerCallback | null; + replayBufferFrames: number; +} + +export interface InitOptions { + apiKey?: string | null; + logFile?: string; + handlers?: import("./types.js").EventHandler[] | null; + logLevel?: string | null; + wsUrl?: string | null; + sessionId?: string | null; + blockTimeout?: number; + onEvent?: OnEventCallback | null; + onVerdict?: OnVerdictCallback | null; + onBlock?: OnBlockCallback | null; + onAudit?: OnAuditCallback | null; + onDisconnect?: OnDisconnectCallback | null; + onReconnect?: OnReconnectCallback | null; + onMcpServer?: OnMcpServerCallback | null; + replayBufferFrames?: number; +} + +let config: AdrianConfig | null = null; + +export function getConfig(): AdrianConfig { + if (config === null) { + throw new Error("Adrian SDK has not been initialised. Call init() first."); + } + return config; +} + +export function currentConfig(): AdrianConfig | null { + return config; +} + +export function setConfig(next: AdrianConfig | null): void { + config = next; +} + +export function isInitialized(): boolean { + return config !== null; +} + +function envString(name: string): string | undefined { + const value = process.env[name]; + return value !== undefined && value !== "" ? value : undefined; +} + +export function resolveInitOptions(options: InitOptions): { + apiKey: string | null; + logFile: string; + wsUrl: string | null; + blockTimeout: number; + replayBufferFrames: number; +} { + const apiKey = options.apiKey !== undefined + ? options.apiKey + : (envString("ADRIAN_API_KEY") ?? null); + + const logFile = options.logFile !== undefined + ? options.logFile + : (envString("ADRIAN_LOG_FILE") ?? "events.jsonl"); + + const wsUrl = options.wsUrl !== undefined + ? options.wsUrl + : (envString("ADRIAN_WS_URL") ?? "ws://localhost:8080/ws"); + + const blockTimeout = options.blockTimeout !== undefined + ? options.blockTimeout + : Number(envString("ADRIAN_BLOCK_TIMEOUT") ?? 30); + + let replayBufferFrames = options.replayBufferFrames ?? 1000; + const envReplay = envString("ADRIAN_REPLAY_BUFFER_FRAMES"); + if (options.replayBufferFrames === undefined && envReplay !== undefined) { + const parsed = parseInt(envReplay, 10); + if (Number.isFinite(parsed)) replayBufferFrames = parsed; + } + + return { + apiKey, + logFile, + wsUrl, + blockTimeout: Number.isFinite(blockTimeout) ? blockTimeout : 30, + replayBufferFrames: Number.isFinite(replayBufferFrames) ? replayBufferFrames : 1000, + }; +} diff --git a/sdk/typescript/packages/core/src/context.ts b/sdk/typescript/packages/core/src/context.ts new file mode 100644 index 0000000..0ad59db --- /dev/null +++ b/sdk/typescript/packages/core/src/context.ts @@ -0,0 +1,84 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import type { AgentContext, ParentContext } from "./format/types.js"; + +const invocationStorage = new AsyncLocalStorage(); + +export function getInvocationId(): string | null { + return invocationStorage.getStore() ?? null; +} + +export function runWithInvocationId(invocationId: string, fn: () => T): T { + return invocationStorage.run(invocationId, fn); +} + +export class AgentContextTracker { + private contexts = new Map(); + private parentMap = new Map(); + private delegatedBy: string | null = null; + + markDelegated(agentId: string): void { + this.delegatedBy = agentId; + } + + update(agentId: string, systemPrompt: string, userInstruction: string): ParentContext | null { + this.contexts.set(agentId, { agentId, systemPrompt, userInstruction }); + + if (!this.parentMap.has(agentId)) { + let parent: ParentContext | null = null; + if (this.delegatedBy !== null) { + const previous = this.contexts.get(this.delegatedBy); + if (previous && previous.agentId !== agentId) { + parent = { ...previous }; + } + } + + if (parent === null) { + const newParts = normalize(agentId.split("|")); + let bestCandidate: string | null = null; + let bestCommon = 0; + for (const otherId of this.contexts.keys()) { + if (otherId === agentId) continue; + const otherParts = normalize(otherId.split("|")); + if (otherParts.length >= newParts.length) continue; + let common = 0; + for (let idx = 0; idx < Math.min(otherParts.length, newParts.length); idx += 1) { + if (otherParts[idx] !== newParts[idx]) break; + common += 1; + } + if (common > bestCommon) { + bestCommon = common; + bestCandidate = otherId; + } + } + if (bestCandidate !== null && bestCommon > 0) { + const previous = this.contexts.get(bestCandidate); + if (previous) parent = { ...previous }; + } + } + + this.parentMap.set(agentId, parent); + } + + if (agentId === this.delegatedBy) { + this.delegatedBy = null; + } + + return this.parentMap.get(agentId) ?? null; + } + + getParent(agentId: string): ParentContext | null { + return this.parentMap.get(agentId) ?? null; + } + + hasContext(agentId: string): boolean { + return this.contexts.has(agentId); + } + + getContext(agentId: string): AgentContext | null { + return this.contexts.get(agentId) ?? null; + } +} + +function normalize(parts: string[]): string[] { + return parts.filter((part) => !/^\d+$/.test(part)); +} diff --git a/sdk/typescript/packages/core/src/format/types.ts b/sdk/typescript/packages/core/src/format/types.ts new file mode 100644 index 0000000..cc262cf --- /dev/null +++ b/sdk/typescript/packages/core/src/format/types.ts @@ -0,0 +1,49 @@ +import type { CallbackMetadata, ChatMessage, ErrorData, TokenUsage, ToolCallRecord } from "../types.js"; + +export interface AgentContext { + agentId: string; + systemPrompt: string; + userInstruction: string; +} + +export interface ParentContext { + agentId: string; + systemPrompt: string; + userInstruction: string; +} + +export interface LlmPairData { + kind: "llm"; + model: string; + messages: ChatMessage[]; + output: string; + toolCalls: ToolCallRecord[]; + usage: TokenUsage | null; + error?: ErrorData; +} + +export interface ToolPairData { + kind: "tool"; + toolName: string; + toolCallId: string | null; + input: string; + output: string; + error?: ErrorData; +} + +export type PairType = "llm" | "tool"; +export type PairData = LlmPairData | ToolPairData; + +export interface PairedEvent { + eventId: string; + invocationId: string; + sessionId: string; + runId: string; + parentRunId: string; + timestamp: string; + pairType: PairType; + agent: AgentContext; + parent: ParentContext | null; + data: PairData; + metadata: CallbackMetadata | null; +} diff --git a/sdk/typescript/packages/core/src/handler.ts b/sdk/typescript/packages/core/src/handler.ts new file mode 100644 index 0000000..1c5310a --- /dev/null +++ b/sdk/typescript/packages/core/src/handler.ts @@ -0,0 +1,263 @@ +import { currentConfig, type AdrianConfig } from "./config.js"; +import { getInvocationId } from "./context.js"; +import { AgentContextTracker } from "./context.js"; +import type { PairedEvent } from "./format/types.js"; +import { HookRegistry } from "./hooks.js"; +import { deriveAgentId } from "./identity.js"; +import { EventPairBuffer } from "./pairing.js"; +import type { CallbackMetadata, ChatMessage, ErrorData, EventData, EventRecord, LlmEndData, ToolCallRecord, ToolEndData, VerdictContext } from "./types.js"; +import type { Verdict } from "./proto/schema.js"; + +export interface AdrianCallbackHandlerOptions { + pairBuffer: EventPairBuffer; + contextTracker: AgentContextTracker; + hooks: HookRegistry; + config: AdrianConfig; +} + +export class AdrianCallbackHandler { + name = "AdrianCallbackHandler"; + private pairBuffer: EventPairBuffer; + private contextTracker: AgentContextTracker; + private hooks: HookRegistry; + private config: AdrianConfig; + private eventMap = new Map(); + private currentAgentId = "default"; + + constructor(options: AdrianCallbackHandlerOptions) { + this.pairBuffer = options.pairBuffer; + this.contextTracker = options.contextTracker; + this.hooks = options.hooks; + this.config = options.config; + } + + async handleChatModelStart(llm: Record, messages: unknown[][], runId: string, parentRunId?: string, extraParams?: Record): Promise { + const flatMessages = messages.flat().map(messageToChatMessage); + const metadata = extractMetadata(extraParams); + const agentId = deriveAgentId(metadata, flatMessages); + this.currentAgentId = agentId; + const systemPrompt = flatMessages.find((msg) => msg.role === "system")?.content ?? ""; + const userInstruction = [...flatMessages].reverse().find((msg) => msg.role === "human" || msg.role === "user")?.content ?? ""; + const parent = this.contextTracker.update(agentId, systemPrompt, userInstruction); + this.pairBuffer.onStart({ + eventType: "chat_model_start", + data: { model: extractModelName(llm), messages: flatMessages, metadata }, + runId: String(runId), + agentId, + parent: parent ?? this.contextTracker.getParent(agentId), + metadata, + parentRunId: parentRunId ? String(parentRunId) : "", + }); + } + + async handleLLMStart(llm: Record, prompts: string[], runId: string, parentRunId?: string, extraParams?: Record): Promise { + const flatMessages = prompts.map((content) => ({ role: "human", content })); + const metadata = extractMetadata(extraParams); + const agentId = deriveAgentId(metadata, flatMessages); + this.currentAgentId = agentId; + const parent = this.contextTracker.update(agentId, "", prompts[0] ?? ""); + this.pairBuffer.onStart({ + eventType: "chat_model_start", + data: { model: extractModelName(llm), messages: flatMessages, metadata }, + runId: String(runId), + agentId, + parent: parent ?? this.contextTracker.getParent(agentId), + metadata, + parentRunId: parentRunId ? String(parentRunId) : "", + }); + } + + async handleLLMEnd(output: unknown, runId: string): Promise { + const data = extractLlmEndData(output); + const pair = this.pairBuffer.onEnd({ + eventType: "llm_end", + data, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) { + if (data.toolCalls.length > 0) this.contextTracker.markDelegated(pair.agent.agentId); + await this.emitPair(pair); + } + } + + async handleLLMError(error: unknown, runId: string): Promise { + const errorData = normalizeError(error); + const pair = this.pairBuffer.onEnd({ + eventType: "llm_end", + data: { output: errorOutput(errorData), toolCalls: [], usage: null, error: errorData }, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) await this.emitPair(pair); + } + + async handleToolStart(tool: Record, input: string, runId: string, parentRunId?: string, extraParams?: Record): Promise { + const metadata = extractMetadata(extraParams); + let agentId = this.currentAgentId; + if (metadata) { + const candidate = deriveAgentId(metadata); + if (this.contextTracker.hasContext(candidate)) agentId = candidate; + } + this.pairBuffer.onStart({ + eventType: "tool_start", + data: { + toolName: String(tool.name ?? tool.id ?? "unknown"), + toolCallId: typeof extraParams?.tool_call_id === "string" ? extraParams.tool_call_id : typeof extraParams?.toolCallId === "string" ? extraParams.toolCallId : null, + input: String(input ?? ""), + metadata, + }, + runId: String(runId), + agentId, + parent: this.contextTracker.getParent(agentId), + metadata, + agentContext: this.contextTracker.getContext(agentId), + parentRunId: parentRunId ? String(parentRunId) : "", + }); + } + + async handleToolEnd(output: unknown, runId: string): Promise { + const data: ToolEndData = { output: stringifyOutput(output) }; + const pair = this.pairBuffer.onEnd({ + eventType: "tool_end", + data, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) await this.emitPair(pair); + } + + async handleToolError(error: unknown, runId: string): Promise { + const errorData = normalizeError(error); + const pair = this.pairBuffer.onEnd({ + eventType: "tool_end", + data: { output: errorOutput(errorData), error: errorData }, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) await this.emitPair(pair); + } + + async handleVerdict(verdict: Verdict): Promise { + const record = this.eventMap.get(verdict.eventId); + this.eventMap.delete(verdict.eventId); + if (!record) return; + const ctx: VerdictContext = { + eventId: verdict.eventId, + sessionId: verdict.sessionId, + eventType: record.eventType, + eventData: record.data, + runId: record.runId, + parentRunId: record.parentRunId, + policy: verdict.policy, + madCode: verdict.madCode, + hitl: verdict.hitl, + }; + await this.config.onVerdict?.(ctx); + const prefix = verdict.madCode.slice(0, 2); + if ((prefix === "M3" || prefix === "M4") && this.config.onBlock) await this.config.onBlock(ctx); + if (prefix === "M2" && this.config.onAudit) await this.config.onAudit(ctx); + } + + private async emitPair(pair: PairedEvent): Promise { + await this.hooks.emit(pair); + this.eventMap.set(pair.eventId, { + eventType: pair.pairType, + data: pair.data as unknown as EventData, + runId: pair.runId, + parentRunId: pair.parentRunId || null, + }); + await this.config.onEvent?.(pair.pairType, pair.data as unknown as EventData, pair.runId, pair.parentRunId || null, pair.eventId); + } + + private resolveSessionId(): string { + const cfg = currentConfig() ?? this.config; + if (!cfg.sessionId) throw new Error("session_id is not set, init() must be called before capturing events"); + return cfg.sessionId; + } + + private resolveInvocationId(): string { + return getInvocationId() ?? "no_invocation"; + } +} + +export function extractModelName(serialized: Record | null | undefined): string { + if (!serialized) return "unknown"; + if (typeof serialized.name === "string") return serialized.name; + if (Array.isArray(serialized.id) && serialized.id.length > 0) return String(serialized.id.at(-1)); + const kwargs = serialized.kwargs; + if (kwargs && typeof kwargs === "object" && "model_name" in kwargs) return String((kwargs as Record).model_name); + return "unknown"; +} + +function extractMetadata(extraParams?: Record): CallbackMetadata | null { + const raw = extraParams?.metadata; + if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) return null; + return raw as CallbackMetadata; +} + +function messageToChatMessage(message: unknown): ChatMessage { + const obj = message && typeof message === "object" ? message as Record : {}; + const role = String(obj.type ?? obj.role ?? "unknown"); + const content = obj.content; + return { role, content: typeof content === "string" ? content : JSON.stringify(content ?? "") }; +} + +function extractLlmEndData(output: unknown): LlmEndData { + if (isLlmEndData(output)) return output; + const generations = (output as { generations?: unknown[][] })?.generations ?? []; + const first = generations[0]?.[0] as Record | undefined; + const text = typeof first?.text === "string" ? first.text : ""; + const message = first?.message as Record | undefined; + const rawCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : []; + const toolCalls: ToolCallRecord[] = rawCalls.map((call) => { + const obj = call && typeof call === "object" ? call as Record : {}; + return { id: String(obj.id ?? ""), name: String(obj.name ?? ""), args: (obj.args && typeof obj.args === "object" ? obj.args : {}) as ToolCallRecord["args"] }; + }); + const llmOutput = (output as { llmOutput?: Record; llm_output?: Record })?.llmOutput ?? (output as { llm_output?: Record })?.llm_output ?? {}; + const usageRaw = llmOutput.tokenUsage ?? llmOutput.token_usage; + const usageObj = usageRaw && typeof usageRaw === "object" ? usageRaw as Record : null; + return { + output: text, + toolCalls, + usage: usageObj ? { + promptTokens: Number(usageObj.promptTokens ?? usageObj.prompt_tokens ?? 0), + completionTokens: Number(usageObj.completionTokens ?? usageObj.completion_tokens ?? 0), + totalTokens: Number(usageObj.totalTokens ?? usageObj.total_tokens ?? 0), + } : null, + }; +} + +function isLlmEndData(output: unknown): output is LlmEndData { + if (!output || typeof output !== "object") return false; + const obj = output as Record; + return typeof obj.output === "string" && Array.isArray(obj.toolCalls) && ("usage" in obj); +} + +function normalizeError(error: unknown): ErrorData { + if (error instanceof Error) { + return { + name: error.name || "Error", + message: error.message, + ...(error.stack ? { stack: error.stack } : {}), + }; + } + return { name: "Error", message: stringifyOutput(error) }; +} + +function errorOutput(error: ErrorData): string { + return `[ERROR] ${error.name}: ${error.message}`; +} + +function stringifyOutput(output: unknown): string { + if (typeof output === "string") return output; + try { + return JSON.stringify(output); + } catch { + return String(output); + } +} diff --git a/sdk/typescript/packages/core/src/handlers/jsonl.ts b/sdk/typescript/packages/core/src/handlers/jsonl.ts new file mode 100644 index 0000000..25e3be5 --- /dev/null +++ b/sdk/typescript/packages/core/src/handlers/jsonl.ts @@ -0,0 +1,36 @@ +import { mkdir, open, type FileHandle } from "node:fs/promises"; +import { dirname } from "node:path"; +import type { PairedEvent } from "../format/types.js"; +import type { EventHandler } from "../types.js"; + +export class JSONLHandler implements EventHandler { + readonly path: string; + private filePromise: Promise; + private chain: Promise = Promise.resolve(); + + constructor(path: string) { + this.path = path; + this.filePromise = this.openFile(path); + } + + async onPairedEvent(event: PairedEvent): Promise { + const line = JSON.stringify(event) + "\n"; + this.chain = this.chain.then(async () => { + const file = await this.filePromise; + await file.write(line); + await file.sync(); + }); + return this.chain; + } + + async close(): Promise { + await this.chain; + const file = await this.filePromise; + await file.close(); + } + + private async openFile(path: string): Promise { + await mkdir(dirname(path), { recursive: true }); + return open(path, "w"); + } +} diff --git a/sdk/typescript/packages/core/src/hooks.ts b/sdk/typescript/packages/core/src/hooks.ts new file mode 100644 index 0000000..759b172 --- /dev/null +++ b/sdk/typescript/packages/core/src/hooks.ts @@ -0,0 +1,34 @@ +import type { PairedEvent } from "./format/types.js"; +import type { EventHandler } from "./types.js"; + +export class HookRegistry { + private handlers: EventHandler[] = []; + + register(handler: EventHandler): void { + this.handlers.push(handler); + } + + get size(): number { + return this.handlers.length; + } + + async emit(event: PairedEvent): Promise { + for (const handler of this.handlers) { + try { + await handler.onPairedEvent(event); + } catch (error) { + console.error("adrian handler failed", { handler: handler.constructor?.name, eventId: event.eventId, error }); + } + } + } + + async close(): Promise { + for (const handler of this.handlers) { + try { + await handler.close(); + } catch (error) { + console.error("adrian handler close failed", { handler: handler.constructor?.name, error }); + } + } + } +} diff --git a/sdk/typescript/packages/core/src/identity.ts b/sdk/typescript/packages/core/src/identity.ts new file mode 100644 index 0000000..019352b --- /dev/null +++ b/sdk/typescript/packages/core/src/identity.ts @@ -0,0 +1,5 @@ +import type { CallbackMetadata, ChatMessage } from "./types.js"; + +export function deriveAgentId(_metadata: CallbackMetadata | null, _messages?: ChatMessage[] | null): string { + return "default"; +} diff --git a/sdk/typescript/packages/core/src/index.ts b/sdk/typescript/packages/core/src/index.ts new file mode 100644 index 0000000..fb9da6c --- /dev/null +++ b/sdk/typescript/packages/core/src/index.ts @@ -0,0 +1,112 @@ +import { resolveInitOptions, setConfig, type AdrianConfig, type InitOptions } from "./config.js"; +import { AgentContextTracker } from "./context.js"; +import { AdrianCallbackHandler } from "./handler.js"; +import { JSONLHandler } from "./handlers/jsonl.js"; +import { HookRegistry } from "./hooks.js"; +import { patchMcpAdapters, mcpServers } from "./mcp.js"; +import { EventPairBuffer } from "./pairing.js"; +import { RedactingHandler } from "./pii/index.js"; +import { getHandler, getWebSocketClient, setRuntime } from "./registry.js"; +import { envAwareResolveSessionId } from "./sessionPersistence.js"; +import { WebSocketClient } from "./ws.js"; +import type { EventHandler, McpServer } from "./types.js"; + +export const version = "1.0.0"; +export const __version__ = version; + +let hooks: HookRegistry | null = null; + +export async function init(options: InitOptions = {}): Promise { + const resolved = resolveInitOptions(options); + const sessionId = await envAwareResolveSessionId(options.sessionId); + + const config: AdrianConfig = { + apiKey: resolved.apiKey, + logFile: resolved.logFile, + logLevel: options.logLevel ?? null, + sessionId, + wsUrl: resolved.wsUrl, + blockTimeout: resolved.blockTimeout, + onEvent: options.onEvent ?? null, + onVerdict: options.onVerdict ?? null, + onBlock: options.onBlock ?? null, + onAudit: options.onAudit ?? null, + onDisconnect: options.onDisconnect ?? null, + onReconnect: options.onReconnect ?? null, + onMcpServer: chainMcpServerCallback(options.onMcpServer ?? null), + replayBufferFrames: resolved.replayBufferFrames, + }; + setConfig(config); + + const handlerList: EventHandler[] = options.handlers ? [...options.handlers] : [new JSONLHandler(resolved.logFile)]; + let wsClient: WebSocketClient | null = null; + if (!options.handlers && resolved.wsUrl) { + if (!resolved.apiKey) console.warn("ADRIAN wsUrl is set but no apiKey was provided; the server will reject the connection."); + wsClient = new WebSocketClient({ + url: resolved.wsUrl, + sessionId, + apiKey: resolved.apiKey ?? "", + onDisconnect: config.onDisconnect, + onReconnect: config.onReconnect, + onLoginAck: sendMcpInventory, + replayBufferFrames: config.replayBufferFrames, + }); + handlerList.push(wsClient); + } + + hooks = new HookRegistry(); + for (const eventHandler of handlerList.map((h) => new RedactingHandler(h))) hooks.register(eventHandler); + const handler = new AdrianCallbackHandler({ pairBuffer: new EventPairBuffer(), contextTracker: new AgentContextTracker(), hooks, config }); + setRuntime(handler, wsClient); + if (wsClient) { + wsClient.handler = handler; + wsClient.scheduleConnect(); + } + + await patchMcpAdapters(); +} + +export const adrian = { + init, + shutdown, + getHandler, + getWebSocketClient, + version, + __version__: version, +}; + +export async function shutdown(): Promise { + await hooks?.close(); + hooks = null; + setRuntime(null, null); + setConfig(null); +} + +async function sendMcpInventory(): Promise { + await getWebSocketClient()?.sendMcpInventory(mcpServers()); +} + +function chainMcpServerCallback(userCallback: ((server: McpServer) => void | Promise) | null) { + return async (server: McpServer) => { + await sendMcpInventory(); + await userCallback?.(server); + }; +} + +export { AdrianCallbackHandler } from "./handler.js"; +export { JSONLHandler } from "./handlers/jsonl.js"; +export { HookRegistry } from "./hooks.js"; +export { EventPairBuffer } from "./pairing.js"; +export { AgentContextTracker, getInvocationId, runWithInvocationId } from "./context.js"; +export { deriveAgentId } from "./identity.js"; +export { WebSocketClient, shouldHalt } from "./ws.js"; +export { AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE, assertToolCallsAllowed, gateToolCallIds } from "./policy.js"; +export type { GateToolCallsReason, GateToolCallsResult } from "./policy.js"; +export { mcpServers, registerMcpServer, registerMcpConnection } from "./mcp.js"; +export { resolveSessionId, envAwareResolveSessionId } from "./sessionPersistence.js"; +export { getHandler, getWebSocketClient } from "./registry.js"; +export * from "./config.js"; +export * from "./types.js"; +export * from "./format/types.js"; +export * from "./pii/index.js"; +export * from "./proto/schema.js"; diff --git a/sdk/typescript/packages/core/src/mcp.ts b/sdk/typescript/packages/core/src/mcp.ts new file mode 100644 index 0000000..421519c --- /dev/null +++ b/sdk/typescript/packages/core/src/mcp.ts @@ -0,0 +1,85 @@ +import { currentConfig, isInitialized } from "./config.js"; +import type { McpServer } from "./types.js"; + +const servers = new Map(); + +export function mcpServers(): McpServer[] { + return [...servers.values()]; +} + +export function resetMcpServers(): void { + servers.clear(); +} + +export function registerMcpServer(server: McpServer): void { + const previous = servers.get(server.name); + if (previous?.transport === server.transport && previous.endpoint === server.endpoint) return; + servers.set(server.name, server); + if (isInitialized()) void currentConfig()?.onMcpServer?.(server); +} + +export function registerMcpConnection(name: string, connection: unknown): void { + if (!name) return; + registerMcpServer(serverFromConnection(name, connection)); +} + +export async function patchMcpAdapters(): Promise { + await patchMcpTransports(); +} + +function serverFromConnection(name: string, connection: unknown): McpServer { + if (!connection || typeof connection !== "object") return { name, transport: "unknown", endpoint: "" }; + const conn = connection as Record; + const transport = String(conn.transport ?? "unknown").toLowerCase(); + return { name, transport, endpoint: endpointFor(transport, conn) }; +} + +function endpointFor(transport: string, conn: Record): string { + if (transport === "stdio") { + const command = String(conn.command ?? ""); + const args = Array.isArray(conn.args) ? conn.args.map(String) : []; + return [command, ...args].filter(Boolean).join(" "); + } + if (["sse", "websocket", "streamable_http", "streamable-http", "http"].includes(transport)) return String(conn.url ?? ""); + return ""; +} + +async function patchMcpTransports(): Promise { + const targets: Array<[string, string, string]> = [ + ["@modelcontextprotocol/sdk/client/stdio.js", "stdio_client", "stdio"], + ["@modelcontextprotocol/sdk/client/sse.js", "sse_client", "sse"], + ["@modelcontextprotocol/sdk/client/websocket.js", "websocket_client", "websocket"], + ]; + for (const [specifier, attr, transport] of targets) { + const mod = await importOptional(specifier); + const original = mod?.[attr]; + if (!original || original._adrianMcpPatched) continue; + mod[attr] = function patchedTransport(...args: unknown[]) { + registerSynthesised(transport, endpointFromTransportArgs(transport, args)); + return original(...args); + }; + mod[attr]._adrianMcpPatched = true; + } +} + +function registerSynthesised(transport: string, endpoint: string): void { + if (!endpoint && transport === "unknown") return; + if ([...servers.values()].some((server) => server.transport === transport && server.endpoint === endpoint)) return; + registerMcpServer({ name: endpoint ? `${transport}:${endpoint}` : transport, transport, endpoint }); +} + +function endpointFromTransportArgs(transport: string, args: unknown[]): string { + const first = args[0]; + if (transport === "stdio" && first && typeof first === "object") { + const params = first as Record; + return [String(params.command ?? ""), ...(Array.isArray(params.args) ? params.args.map(String) : [])].filter(Boolean).join(" "); + } + if (typeof first === "string") return first; + if (first instanceof URL) return first.toString(); + if (first && typeof first === "object" && "url" in first) return String((first as Record).url ?? ""); + return ""; +} + +async function importOptional(specifier: string): Promise { + try { return await import(specifier); } catch { return null; } +} diff --git a/sdk/typescript/packages/core/src/pairing.ts b/sdk/typescript/packages/core/src/pairing.ts new file mode 100644 index 0000000..9e70f3d --- /dev/null +++ b/sdk/typescript/packages/core/src/pairing.ts @@ -0,0 +1,109 @@ +import { randomUUID } from "node:crypto"; +import type { AgentContext, PairedEvent, ParentContext } from "./format/types.js"; +import type { CallbackMetadata, ChatModelStartData, LlmEndData, ToolEndData, ToolStartData } from "./types.js"; + +interface StartEventRecord { + eventType: "chat_model_start" | "tool_start"; + data: ChatModelStartData | ToolStartData; + agentId: string; + parentRunId: string; + parent: ParentContext | null; + metadata: CallbackMetadata | null; + agentContext: AgentContext | null; +} + +export class EventPairBuffer { + private pending = new Map(); + + onStart(args: { + eventType: "chat_model_start" | "tool_start"; + data: ChatModelStartData | ToolStartData; + runId: string; + agentId: string; + parent: ParentContext | null; + metadata: CallbackMetadata | null; + agentContext?: AgentContext | null; + parentRunId?: string; + }): void { + this.pending.set(args.runId, { + eventType: args.eventType, + data: args.data, + agentId: args.agentId, + parentRunId: args.parentRunId ?? "", + parent: args.parent, + metadata: args.metadata, + agentContext: args.agentContext ?? null, + }); + } + + onEnd(args: { + eventType: "llm_end" | "tool_end"; + data: LlmEndData | ToolEndData; + runId: string; + invocationId: string; + sessionId: string; + }): PairedEvent | null { + const start = this.pending.get(args.runId); + this.pending.delete(args.runId); + if (!start) return null; + if (args.eventType === "llm_end" && start.eventType === "chat_model_start") { + return this.assembleLlmPair(start, args.data as LlmEndData, args.runId, args.invocationId, args.sessionId); + } + if (args.eventType === "tool_end" && start.eventType === "tool_start") { + return this.assembleToolPair(start, args.data as ToolEndData, args.runId, args.invocationId, args.sessionId); + } + return null; + } + + private assembleLlmPair(start: StartEventRecord, endData: LlmEndData, runId: string, invocationId: string, sessionId: string): PairedEvent { + const startData = start.data as ChatModelStartData; + const messages = startData.messages ?? []; + const systemPrompt = messages.find((msg) => msg.role === "system")?.content ?? ""; + const userInstruction = [...messages].reverse().find((msg) => msg.role === "human" || msg.role === "user")?.content ?? ""; + return { + eventId: randomUUID(), + invocationId, + sessionId, + runId, + parentRunId: start.parentRunId, + timestamp: new Date().toISOString(), + pairType: "llm", + agent: { agentId: start.agentId, systemPrompt, userInstruction }, + parent: start.parent, + data: { + kind: "llm", + model: startData.model ?? "unknown", + messages, + output: endData.output ?? "", + toolCalls: endData.toolCalls ?? [], + usage: endData.usage ?? null, + error: endData.error, + }, + metadata: start.metadata, + }; + } + + private assembleToolPair(start: StartEventRecord, endData: ToolEndData, runId: string, invocationId: string, sessionId: string): PairedEvent { + const startData = start.data as ToolStartData; + return { + eventId: randomUUID(), + invocationId, + sessionId, + runId, + parentRunId: start.parentRunId, + timestamp: new Date().toISOString(), + pairType: "tool", + agent: start.agentContext ?? { agentId: start.agentId, systemPrompt: "", userInstruction: "" }, + parent: start.parent, + data: { + kind: "tool", + toolName: startData.toolName ?? "unknown", + toolCallId: startData.toolCallId ?? null, + input: startData.input ?? "", + output: endData.output ?? "", + error: endData.error, + }, + metadata: start.metadata, + }; + } +} diff --git a/sdk/typescript/packages/core/src/pii/engine.ts b/sdk/typescript/packages/core/src/pii/engine.ts new file mode 100644 index 0000000..b580a47 --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/engine.ts @@ -0,0 +1,24 @@ +import { detect, type Detection, type PiiType } from "./patterns.js"; +import { applyStrategy, RedactionStrategy } from "./strategies.js"; + +export interface PiiConfig { + strategy?: RedactionStrategy; + enabledTypes?: ReadonlySet | PiiType[] | null; +} + +export interface RedactionResult { + text: string; + detections: Detection[]; +} + +export function redactText(text: string, config: PiiConfig = {}): RedactionResult { + if (!text) return { text, detections: [] }; + const enabledTypes = Array.isArray(config.enabledTypes) ? new Set(config.enabledTypes) : config.enabledTypes ?? null; + const detections = detect(text, enabledTypes); + if (detections.length === 0) return { text, detections }; + let result = text; + for (const detection of [...detections].reverse()) { + result = result.slice(0, detection.start) + applyStrategy(detection, config.strategy ?? RedactionStrategy.REPLACE) + result.slice(detection.end); + } + return { text: result, detections }; +} diff --git a/sdk/typescript/packages/core/src/pii/index.ts b/sdk/typescript/packages/core/src/pii/index.ts new file mode 100644 index 0000000..04b17c2 --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/index.ts @@ -0,0 +1,6 @@ +export { redactText } from "./engine.js"; +export type { PiiConfig, RedactionResult } from "./engine.js"; +export { PiiType, detect } from "./patterns.js"; +export type { Detection } from "./patterns.js"; +export { RedactionStrategy } from "./strategies.js"; +export { PiiRedactor, RedactingHandler } from "./redactor.js"; diff --git a/sdk/typescript/packages/core/src/pii/patterns.ts b/sdk/typescript/packages/core/src/pii/patterns.ts new file mode 100644 index 0000000..3f89ce0 --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/patterns.ts @@ -0,0 +1,183 @@ +import { isIP } from "node:net"; + +export enum PiiType { + EMAIL = "EMAIL", + PHONE = "PHONE", + SSN = "SSN", + CREDIT_CARD = "CREDIT_CARD", + IP_ADDRESS = "IP_ADDRESS", + DATE_OF_BIRTH = "DATE_OF_BIRTH", + IBAN = "IBAN", + PASSPORT = "PASSPORT", + STREET_ADDRESS = "STREET_ADDRESS", + POSTAL_CODE = "POSTAL_CODE", + DRIVER_LICENSE = "DRIVER_LICENSE", + AWS_KEY = "AWS_KEY", +} + +export interface Detection { + piiType: PiiType; + start: number; + end: number; + text: string; +} + +type PatternEntry = [PiiType, RegExp, boolean, string | null]; + +// Named regexes (mirror Python naming) +const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; +const PHONE_RE = /(? | null): Detection[] { + const detections: Detection[] = []; + for (const [piiType, pattern, needsGroup, postFilter] of PATTERNS) { + if (types && !types.has(piiType)) continue; + pattern.lastIndex = 0; + for (const match of text.matchAll(pattern)) { + // Determine matched text and indices; support patterns that capture group(1) + let matched: string; + let start = match.index ?? 0; + if (needsGroup && match[1] !== undefined) { + matched = match[1]; + const whole = match[0]; + const groupIndex = whole.indexOf(matched); + start = (match.index ?? 0) + (groupIndex >= 0 ? groupIndex : 0); + } else { + matched = match[0]; + start = match.index ?? 0; + } + + const end = start + matched.length; + + // Post-filter validations + switch (postFilter) { + case "luhn": { + if (!luhnCheck(matched.replace(/\D/g, ""))) continue; + break; + } + case "private_ip": { + if (!isPrivateIp(matched)) continue; + break; + } + case "dob_context": { + if (!hasDobContext(text, start, end)) continue; + break; + } + case "postal_context": { + if (!hasPostalContext(text, start, end)) continue; + break; + } + default: + break; + } + + detections.push({ piiType, start, end, text: matched }); + } + } + // Sort detections by start (asc) and length (desc) then resolve overlaps + detections.sort((a, b) => a.start - b.start || b.end - a.end); + return resolveOverlaps(detections); +} + +function luhnCheck(digits: string): boolean { + let total = 0; + const reverse = [...digits].reverse(); + for (let i = 0; i < reverse.length; i += 1) { + let n = Number(reverse[i]); + if (i % 2 === 1) { + n *= 2; + if (n > 9) n -= 9; + } + total += n; + } + return total % 10 === 0; +} + +function resolveOverlaps(detections: Detection[]): Detection[] { + if (detections.length === 0) return []; + + // Already sorted by start asc, longer matches first for equal starts. + const result: Detection[] = []; + let lastEnd = -1; + + for (const det of detections) { + if (det.start >= lastEnd) { + result.push(det); + lastEnd = det.end; + } + } + + return result; +} + +function isPrivateIp(input: string): boolean { + const text = input.split("%")[0]; + const v = isIP(text); + if (v === 4) { + const [a, b] = text.split(".").map(Number); + return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || a === 127 || (a === 169 && b === 254); + } + if (v === 6) { + if (text === "::1") return true; + const first = text.split(":")[0]; + if (!first) return false; + const h = parseInt(first, 16); + return (h & 0xfe00) === 0xfc00 || (h & 0xffc0) === 0xfe80; + } + return false; +} + +const DOB_CONTEXT_RE = /\b(?:born|dob|date\s+of\s+birth|birthday|birthdate|d\.o\.b)\b/i; + +function hasDobContext(text: string, start: number, end: number): boolean { + const before = 50; + const after = 30; + + if (before > 0) { + const regionStart = Math.max(0, start - before); + if (DOB_CONTEXT_RE.test(text.slice(regionStart, start))) return true; + } + + if (after > 0) { + if (DOB_CONTEXT_RE.test(text.slice(end, end + after))) return true; + } + + return false; +} + +function hasPostalContext(text: string, start: number, end: number): boolean { + const before = 30; + const regionStart = Math.max(0, start - before); + if (POSTAL_CONTEXT_RE.test(text.slice(regionStart, start))) return true; + return false; +} diff --git a/sdk/typescript/packages/core/src/pii/redactor.ts b/sdk/typescript/packages/core/src/pii/redactor.ts new file mode 100644 index 0000000..b59d559 --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/redactor.ts @@ -0,0 +1,82 @@ +import type { PairedEvent } from "../format/types.js"; +import type { EventHandler, JsonValue } from "../types.js"; +import { redactText, type PiiConfig } from "./engine.js"; +import { detect } from "./patterns.js"; + +export class PiiRedactor { + private config: PiiConfig; + constructor(config: PiiConfig = {}) { + this.config = config; + } + + redactEvent(event: PairedEvent, options: { inPlace?: boolean } = {}): PairedEvent { + const target = options.inPlace ? event : structuredClone(event); + target.agent.systemPrompt = this.redactString(target.agent.systemPrompt); + target.agent.userInstruction = this.redactString(target.agent.userInstruction); + if (target.parent) { + target.parent.systemPrompt = this.redactString(target.parent.systemPrompt); + target.parent.userInstruction = this.redactString(target.parent.userInstruction); + } + if (target.data.kind === "llm") { + for (const message of target.data.messages) message.content = this.redactString(message.content); + target.data.output = this.redactString(target.data.output); + for (const call of target.data.toolCalls) call.args = this.redactValue(call.args) as typeof call.args; + } else { + target.data.input = this.redactString(target.data.input); + target.data.output = this.redactString(target.data.output); + } + return target; + } + + eventHasPii(event: PairedEvent): boolean { + const enabledTypes = Array.isArray(this.config.enabledTypes) ? new Set(this.config.enabledTypes) : this.config.enabledTypes ?? null; + return this.iterEventText(event).some((text) => detect(text, enabledTypes).length > 0); + } + + private redactString(text: string): string { + return redactText(text, this.config).text; + } + + private redactValue(value: JsonValue): JsonValue { + if (typeof value === "string") return this.redactString(value); + if (Array.isArray(value)) return value.map((item) => this.redactValue(item)); + if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, this.redactValue(val)])); + return value; + } + + private iterEventText(event: PairedEvent): string[] { + const texts = [event.agent.systemPrompt, event.agent.userInstruction]; + if (event.parent) texts.push(event.parent.systemPrompt, event.parent.userInstruction); + if (event.data.kind === "llm") { + texts.push(...event.data.messages.map((msg) => msg.content), event.data.output); + for (const call of event.data.toolCalls) collectStrings(call.args, texts); + } else { + texts.push(event.data.input, event.data.output); + } + return texts; + } +} + +export class RedactingHandler implements EventHandler { + private inner: EventHandler; + private redactor: PiiRedactor; + constructor(inner: EventHandler, config: PiiConfig = {}) { + this.inner = inner; + this.redactor = new PiiRedactor(config); + } + + async onPairedEvent(event: PairedEvent): Promise { + const next = this.redactor.eventHasPii(event) ? this.redactor.redactEvent(event) : event; + await this.inner.onPairedEvent(next); + } + + async close(): Promise { + await this.inner.close(); + } +} + +function collectStrings(value: JsonValue, sink: string[]): void { + if (typeof value === "string") sink.push(value); + else if (Array.isArray(value)) value.forEach((item) => collectStrings(item, sink)); + else if (value && typeof value === "object") Object.values(value).forEach((item) => collectStrings(item, sink)); +} diff --git a/sdk/typescript/packages/core/src/pii/strategies.ts b/sdk/typescript/packages/core/src/pii/strategies.ts new file mode 100644 index 0000000..f735229 --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/strategies.ts @@ -0,0 +1,70 @@ +import { createHash } from "node:crypto"; +import { Detection, PiiType } from "./patterns.js"; + +export enum RedactionStrategy { + REPLACE = "replace", + MASK = "mask", + HASH = "hash", +} + +export function applyStrategy(detection: Detection, strategy: RedactionStrategy): string { + if (strategy === RedactionStrategy.HASH) { + const digest = createHash("sha256").update(detection.text).digest("hex").slice(0, 8); + return `[${detection.piiType}:${digest}]`; + } + if (strategy === RedactionStrategy.MASK) return mask(detection); + return `[${detection.piiType}_REDACTED]`; +} + +function mask(detection: Detection): string { + const text = detection.text; + switch (detection.piiType) { + case PiiType.EMAIL: { + return maskEmail(text); + } + case PiiType.PHONE: + return maskPhone(text); + case PiiType.SSN: + return maskSsn(text); + case PiiType.CREDIT_CARD: + return maskCreditCard(text); + case PiiType.IP_ADDRESS: + return maskIp(text); + default: + return text.length <= 2 ? "*".repeat(text.length) : text[0] + "*".repeat(text.length - 2) + text.at(-1); + } +} + +function maskEmail(text: string): string { + const [local, domain] = text.split("@"); + const suffix = domain?.split(".").at(-1) ?? ""; + return `${local?.[0] ?? "*"}***@***.${suffix}`; +} + +function maskPhone(text: string): string { + const digits = text.replace(/\D/g, ""); + if (digits.length < 4) return "***"; + return `***-***-${digits.slice(-4)}`; +} + +function maskSsn(text: string): string { + const digits = text.replace(/\D/g, ""); + if (digits.length < 4) return "***-**-****"; + return `***-**-${digits.slice(-4)}`; +} + +function maskCreditCard(text: string): string { + const digits = text.replace(/\D/g, ""); + if (digits.length < 4) return "****-****-****-****"; + return `****-****-****-${digits.slice(-4)}`; +} + +function maskIp(text: string): string { + if (text.includes(":")) { + const parts = text.split(":"); + const masked = Array(Math.max(0, parts.length - 1)).fill("****").concat(parts.slice(-1)); + return masked.join(":"); + } + const last = text.split(".").slice(-1)[0] ?? ""; + return `***.***.***.${last}`; +} diff --git a/sdk/typescript/packages/core/src/policy.ts b/sdk/typescript/packages/core/src/policy.ts new file mode 100644 index 0000000..9c05519 --- /dev/null +++ b/sdk/typescript/packages/core/src/policy.ts @@ -0,0 +1,56 @@ +import { currentConfig } from "./config.js"; +import { shouldHalt, type WebSocketClient } from "./ws.js"; + +/** Tool result content returned when tool execution is blocked by policy. */ +export const BLOCKED_TOOL_MESSAGE = "[BLOCKED by security policy]"; + +export type GateToolCallsReason = "policy_halt" | "verdict_timeout"; + +export type GateToolCallsResult = + | { action: "allow" } + | { action: "block"; reason: GateToolCallsReason }; + +export class AdrianPolicyBlockedError extends Error { + readonly reason: GateToolCallsReason; + + constructor(reason: GateToolCallsReason) { + super(`Adrian security policy blocked execution (${reason})`); + this.name = "AdrianPolicyBlockedError"; + this.reason = reason; + } +} + +/** + * Waits for backend verdicts on tool calls proposed by a prior LLM turn. + * No-ops when WebSocket is absent or policy mode is not BLOCK/HITL. + */ +export async function gateToolCallIds( + toolCallIds: string[], + ws: WebSocketClient | null, + blockTimeoutSeconds?: number, +): Promise { + if (toolCallIds.length === 0) return { action: "allow" }; + if (!ws) return { action: "allow" }; + + const timeoutSeconds = blockTimeoutSeconds ?? currentConfig()?.blockTimeout ?? 30; + const policyReady = await ws.waitForPolicyReady(timeoutSeconds); + if (!policyReady || !ws.policyActive()) return { action: "allow" }; + + const correlatableIds = toolCallIds.filter((id) => id); + if (correlatableIds.length === 0) return { action: "allow" }; + + const verdictTimeout = ws.blockTimeout(timeoutSeconds); + const verdicts = await Promise.all(correlatableIds.map((id) => ws.waitForToolCallVerdict(id, verdictTimeout))); + if (verdicts.some((verdict) => verdict !== null && shouldHalt(verdict))) return { action: "block", reason: "policy_halt" }; + return { action: "allow" }; +} + +/** Throws {@link AdrianPolicyBlockedError} when {@link gateToolCallIds} would block. */ +export async function assertToolCallsAllowed( + toolCallIds: string[], + ws: WebSocketClient | null, + blockTimeoutSeconds?: number, +): Promise { + const result = await gateToolCallIds(toolCallIds, ws, blockTimeoutSeconds); + if (result.action === "block") throw new AdrianPolicyBlockedError(result.reason); +} diff --git a/sdk/typescript/packages/core/src/proto/schema.ts b/sdk/typescript/packages/core/src/proto/schema.ts new file mode 100644 index 0000000..f5d886b --- /dev/null +++ b/sdk/typescript/packages/core/src/proto/schema.ts @@ -0,0 +1,161 @@ +import protobuf from "protobufjs"; +import type { PairedEvent } from "../format/types.js"; +import type { McpServer } from "../types.js"; + +export const SCHEMA_VERSION = 2; + +export enum PairTypeProto { + PAIR_TYPE_UNSPECIFIED = 0, + PAIR_TYPE_LLM = 1, + PAIR_TYPE_TOOL = 2, +} + +export enum Mode { + MODE_UNSPECIFIED = 0, + MODE_ALERT = 1, + MODE_HITL = 2, + MODE_BLOCK = 3, +} + +export interface PolicySnapshot { + mode: Mode; + policyM0: boolean; + policyM2: boolean; + policyM3: boolean; + policyM4: boolean; +} + +export interface HitlResponse { + continueExecution: boolean; +} + +export interface Verdict { + eventId: string; + sessionId: string; + madCode: string; + policy: PolicySnapshot; + hitl: HitlResponse | null; +} + +export interface LoginAck { + policy: PolicySnapshot; +} + +export type ClientFrame = + | { login: { sessionId: string; llmStack: { provider: string; model: string }; schemaVersion: number } } + | { pairedBatch: { events: PairedEvent[] } } + | { mcpInventory: { servers: McpServer[] } }; + +export type ServerFrame = + | { loginAck: LoginAck } + | { verdict: Verdict }; + +const protoSource = ` +syntax = "proto3"; +package adrian.core_api.v1; +enum PairType { PAIR_TYPE_UNSPECIFIED = 0; PAIR_TYPE_LLM = 1; PAIR_TYPE_TOOL = 2; } +message ChatMessage { string role = 1; string content = 2; } +message ToolCall { string name = 1; string args = 2; string id = 3; } +message TokenUsage { int32 prompt_tokens = 1; int32 completion_tokens = 2; int32 total_tokens = 3; } +message AgentContext { string agent_id = 1; string system_prompt = 2; string user_instruction = 3; } +message LlmPairData { string model = 1; repeated ChatMessage messages = 2; string output = 3; repeated ToolCall tool_calls = 4; TokenUsage usage = 5; } +message ToolPairData { string tool_name = 1; string tool_call_id = 2; string input = 3; string output = 4; } +message PairedEvent { string event_id = 1; string invocation_id = 2; string session_id = 3; string run_id = 4; string parent_run_id = 5; string timestamp = 6; PairType pair_type = 7; AgentContext agent = 8; AgentContext parent = 9; oneof data { LlmPairData llm = 10; ToolPairData tool = 11; } bytes metadata_json = 20; } +message PairedEventBatch { repeated PairedEvent events = 1; } +message McpServer { string name = 1; string transport = 2; string endpoint = 3; } +message McpInventory { repeated McpServer servers = 1; } +message LLMStack { string provider = 1; string model = 2; } +message SessionLogin { string session_id = 1; LLMStack llm_stack = 2; reserved 3; uint32 schema_version = 4; } +message ClientFrame { reserved 2; oneof frame { SessionLogin login = 1; PairedEventBatch paired_batch = 3; McpInventory mcp_inventory = 4; } } +enum Mode { MODE_UNSPECIFIED = 0; MODE_ALERT = 1; MODE_HITL = 2; MODE_BLOCK = 3; } +message PolicySnapshot { Mode mode = 1; bool policy_m0 = 2; bool policy_m2 = 3; bool policy_m3 = 4; bool policy_m4 = 5; } +message HitlResponse { bool continue_execution = 1; } +message LoginAck { PolicySnapshot policy = 1; } +message ServerFrame { oneof frame { LoginAck login_ack = 1; Verdict verdict = 2; } } +message Verdict { string event_id = 1; string session_id = 2; reserved 3; string mad_code = 4; reserved 5; PolicySnapshot policy = 6; HitlResponse hitl = 7; } +`; + +const root = protobuf.parse(protoSource, { keepCase: true }).root; +const ClientFrameType = root.lookupType("adrian.core_api.v1.ClientFrame"); +const ServerFrameType = root.lookupType("adrian.core_api.v1.ServerFrame"); + +export function encodeClientFrame(frame: ClientFrame): Uint8Array { + const message = toProtoClientFrame(frame); + const err = ClientFrameType.verify(message); + if (err) throw new Error(err); + return ClientFrameType.encode(ClientFrameType.create(message)).finish(); +} + +export function decodeServerFrame(bytes: Uint8Array): ServerFrame { + const decoded = ServerFrameType.toObject(ServerFrameType.decode(bytes), { defaults: true, bytes: Uint8Array }) as Record; + if (decoded.login_ack) return { loginAck: { policy: fromProtoPolicy((decoded.login_ack as Record).policy as Record) } }; + if (decoded.verdict) return { verdict: fromProtoVerdict(decoded.verdict as Record) }; + throw new Error("server frame did not contain login_ack or verdict"); +} + +export function pairedEventToProto(event: PairedEvent): Record { + const base: Record = { + event_id: event.eventId, + invocation_id: event.invocationId, + session_id: event.sessionId, + run_id: event.runId, + parent_run_id: event.parentRunId, + timestamp: event.timestamp, + pair_type: event.pairType === "llm" ? PairTypeProto.PAIR_TYPE_LLM : PairTypeProto.PAIR_TYPE_TOOL, + agent: agentToProto(event.agent), + parent: event.parent ? agentToProto(event.parent) : { agent_id: "", system_prompt: "", user_instruction: "" }, + }; + if (event.data.kind === "llm") { + base.llm = { + model: event.data.model, + messages: event.data.messages, + output: event.data.output, + tool_calls: event.data.toolCalls.map((call) => ({ name: call.name, args: JSON.stringify(call.args), id: call.id })), + usage: event.data.usage ? { + prompt_tokens: event.data.usage.promptTokens, + completion_tokens: event.data.usage.completionTokens, + total_tokens: event.data.usage.totalTokens, + } : undefined, + }; + } else { + base.tool = { tool_name: event.data.toolName, tool_call_id: event.data.toolCallId ?? "", input: event.data.input, output: event.data.output }; + } + if (event.metadata) base.metadata_json = new TextEncoder().encode(JSON.stringify(event.metadata)); + return base; +} + +function toProtoClientFrame(frame: ClientFrame): Record { + if ("login" in frame) { + return { login: { session_id: frame.login.sessionId, llm_stack: frame.login.llmStack, schema_version: frame.login.schemaVersion } }; + } + if ("pairedBatch" in frame) { + return { paired_batch: { events: frame.pairedBatch.events.map(pairedEventToProto) } }; + } + return { mcp_inventory: { servers: frame.mcpInventory.servers } }; +} + +function agentToProto(agent: { agentId: string; systemPrompt: string; userInstruction: string }): Record { + return { agent_id: agent.agentId, system_prompt: agent.systemPrompt, user_instruction: agent.userInstruction }; +} + +function fromProtoPolicy(policyRaw: Record | undefined): PolicySnapshot { + const policy = policyRaw ?? {}; + return { + mode: Number(policy.mode ?? 0) as Mode, + policyM0: Boolean(policy.policy_m0), + policyM2: Boolean(policy.policy_m2), + policyM3: Boolean(policy.policy_m3), + policyM4: Boolean(policy.policy_m4), + }; +} + +function fromProtoVerdict(raw: Record): Verdict { + const hitl = raw.hitl as Record | undefined; + return { + eventId: String(raw.event_id ?? ""), + sessionId: String(raw.session_id ?? ""), + madCode: String(raw.mad_code ?? ""), + policy: fromProtoPolicy(raw.policy as Record | undefined), + hitl: hitl ? { continueExecution: Boolean(hitl.continue_execution) } : null, + }; +} diff --git a/sdk/typescript/packages/core/src/registry.ts b/sdk/typescript/packages/core/src/registry.ts new file mode 100644 index 0000000..191c0bb --- /dev/null +++ b/sdk/typescript/packages/core/src/registry.ts @@ -0,0 +1,18 @@ +import type { AdrianCallbackHandler } from "./handler.js"; +import type { WebSocketClient } from "./ws.js"; + +let handler: AdrianCallbackHandler | null = null; +let wsClient: WebSocketClient | null = null; + +export function getHandler(): AdrianCallbackHandler | null { + return handler; +} + +export function getWebSocketClient(): WebSocketClient | null { + return wsClient; +} + +export function setRuntime(nextHandler: AdrianCallbackHandler | null, nextWsClient: WebSocketClient | null): void { + handler = nextHandler; + wsClient = nextWsClient; +} diff --git a/sdk/typescript/packages/core/src/sessionPersistence.ts b/sdk/typescript/packages/core/src/sessionPersistence.ts new file mode 100644 index 0000000..92d5dca --- /dev/null +++ b/sdk/typescript/packages/core/src/sessionPersistence.ts @@ -0,0 +1,54 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { resolve, sep } from "node:path"; + +const CONFIG_FILENAME = "config.json"; +const SESSION_KEY = "session_id"; + +export function cwdKey(cwd = process.cwd()): string { + return resolve(cwd).replaceAll("/", "-").replaceAll("\\", "-").replaceAll(":", "-"); +} + +export function configDir(cwd = process.cwd()): string { + return [homedir(), ".adrian", "projects", cwdKey(cwd)].join(sep); +} + +export function configPath(cwd = process.cwd()): string { + return [configDir(cwd), CONFIG_FILENAME].join(sep); +} + +export async function resolveSessionId(cwd = process.cwd()): Promise { + const existing = await readPersisted(cwd); + if (existing) return existing; + const next = randomUUID(); + await writePersisted(next, cwd); + return next; +} + +export async function envAwareResolveSessionId(explicit?: string | null, cwd = process.cwd()): Promise { + if (explicit !== undefined && explicit !== null) return explicit; + if (process.env.ADRIAN_SESSION_ID) return process.env.ADRIAN_SESSION_ID; + return resolveSessionId(cwd); +} + +async function readPersisted(cwd: string): Promise { + try { + const raw = await readFile(configPath(cwd), "utf8"); + const data = JSON.parse(raw) as Record; + const sessionId = data[SESSION_KEY]; + return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null; + } catch { + return null; + } +} + +async function writePersisted(sessionId: string, cwd: string): Promise { + try { + const path = configPath(cwd); + await mkdir(configDir(cwd), { recursive: true }); + await writeFile(path, JSON.stringify({ [SESSION_KEY]: sessionId }, null, 2) + "\n", "utf8"); + } catch { + // Persistence is best effort; init can still proceed with the generated id. + } +} diff --git a/sdk/typescript/packages/core/src/types.ts b/sdk/typescript/packages/core/src/types.ts new file mode 100644 index 0000000..c8c0ceb --- /dev/null +++ b/sdk/typescript/packages/core/src/types.ts @@ -0,0 +1,94 @@ +import type { PairedEvent } from "./format/types.js"; +import type { PolicySnapshot, HitlResponse } from "./proto/schema.js"; + +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; +export type MetadataValue = JsonPrimitive | string[]; +export type CallbackMetadata = Record; +export type ToolArgs = Record; + +export interface ChatMessage { + role: string; + content: string; +} + +export interface ToolCallRecord { + id: string; + name: string; + args: ToolArgs; +} + +export interface TokenUsage { + promptTokens: number; + completionTokens: number; + totalTokens: number; +} + +export interface ErrorData { + name: string; + message: string; + stack?: string; +} + +export interface ChatModelStartData { + model: string; + messages: ChatMessage[]; + metadata: CallbackMetadata | null; +} + +export interface LlmStartData { + model: string; + prompts: string[]; + metadata: CallbackMetadata | null; +} + +export interface LlmEndData { + output: string; + toolCalls: ToolCallRecord[]; + usage: TokenUsage | null; + error?: ErrorData; +} + +export interface ToolStartData { + toolName: string; + toolCallId: string | null; + input: string; + metadata: CallbackMetadata | null; +} + +export interface ToolEndData { + output: string; + error?: ErrorData; +} + +export type EventData = ChatModelStartData | LlmStartData | LlmEndData | ToolStartData | ToolEndData; + +export interface EventRecord { + eventType: string; + data: EventData; + runId: string; + parentRunId: string | null; +} + +export interface VerdictContext { + eventId: string; + sessionId: string; + eventType: string; + eventData: EventData; + runId: string; + parentRunId: string | null; + policy: PolicySnapshot; + madCode: string; + hitl: HitlResponse | null; +} + +export interface McpServer { + name: string; + transport: string; + endpoint: string; +} + +export interface EventHandler { + onPairedEvent(event: PairedEvent): Promise | void; + close(): Promise | void; +} diff --git a/sdk/typescript/packages/core/src/ws.ts b/sdk/typescript/packages/core/src/ws.ts new file mode 100644 index 0000000..e06221a --- /dev/null +++ b/sdk/typescript/packages/core/src/ws.ts @@ -0,0 +1,307 @@ +import WebSocket from "ws"; +import type { AdrianCallbackHandler } from "./handler.js"; +import type { PairedEvent } from "./format/types.js"; +import type { EventHandler, McpServer } from "./types.js"; +import { decodeServerFrame, encodeClientFrame, Mode, SCHEMA_VERSION, type PolicySnapshot, type Verdict } from "./proto/schema.js"; + +const INITIAL_BACKOFF_MS = 1000; +const MAX_BACKOFF_MS = 30_000; +const QUOTA_EXHAUSTED_CLOSE_CODE = 4003; +const QUOTA_RECONNECT_DELAY_MS = 60_000; +const MAX_RUN_ID_MAP = 1024; +const MAX_TOOL_CALL_MAP = 1024; +const MAX_VERDICT_CACHE = 1024; + +type VerdictWaiter = { resolve: (verdict: Verdict | null) => void; timer?: ReturnType }; +type LoginAckWaiter = { resolve: (acked: boolean) => void; timer?: ReturnType }; + +export class WebSocketClient implements EventHandler { + private url: string; + private sessionId: string; + private apiKey: string; + private onDisconnect?: ((reason: string) => void | Promise) | null; + private onReconnect?: (() => void | Promise) | null; + private onLoginAck?: (() => void | Promise) | null; + private ws: WebSocket | null = null; + private loggedIn = false; + private closing = false; + private replaying = false; + private hadConnection = false; + private provider = ""; + private model = ""; + private mode = Mode.MODE_UNSPECIFIED; + private policy: PolicySnapshot | null = null; + private replayBuffer: Uint8Array[] = []; + private replayLimit: number; + private droppedFrames = 0; + private nextReconnectDelay: number | null = null; + private runIdToEventId = new Map(); + private toolCallIdToEventId = new Map(); + private pendingVerdicts = new Map(); + private verdictCache = new Map(); + private loginAckWaiters = new Set(); + handler: AdrianCallbackHandler | null; + + constructor(options: { + url: string; + sessionId: string; + apiKey: string; + handler?: AdrianCallbackHandler | null; + onDisconnect?: ((reason: string) => void | Promise) | null; + onReconnect?: (() => void | Promise) | null; + onLoginAck?: (() => void | Promise) | null; + replayBufferFrames?: number; + }) { + this.url = options.url; + this.sessionId = options.sessionId; + this.apiKey = options.apiKey; + this.handler = options.handler ?? null; + this.onDisconnect = options.onDisconnect; + this.onReconnect = options.onReconnect; + this.onLoginAck = options.onLoginAck; + this.replayLimit = options.replayBufferFrames ?? 1000; + } + + scheduleConnect(): void { + void this.connectLoop(); + } + + async onPairedEvent(event: PairedEvent): Promise { + if (event.data.kind === "llm") { + if (!this.provider) this.provider = deriveProvider(event.data.model); + if (!this.model) this.model = event.data.model; + this.setLru(this.runIdToEventId, event.runId, event.eventId, MAX_RUN_ID_MAP); + for (const call of event.data.toolCalls) { + if (call.id) this.setLru(this.toolCallIdToEventId, call.id, event.eventId, MAX_TOOL_CALL_MAP); + } + } + await this.sendFrame(encodeClientFrame({ pairedBatch: { events: [event] } })); + } + + async sendMcpInventory(servers: McpServer[]): Promise { + if (servers.length === 0) return; + await this.sendFrame(encodeClientFrame({ mcpInventory: { servers } })); + } + + policyActive(): boolean { + return this.mode === Mode.MODE_BLOCK || this.mode === Mode.MODE_HITL; + } + + loginAcked(): boolean { + return this.loggedIn; + } + + async waitForPolicyReady(timeoutSeconds: number | null): Promise { + if (this.loggedIn) return true; + if (this.closing) return false; + return new Promise((resolve) => { + const waiter: LoginAckWaiter = { resolve }; + if (timeoutSeconds !== null) { + waiter.timer = setTimeout(() => { + this.loginAckWaiters.delete(waiter); + resolve(false); + }, timeoutSeconds * 1000); + } + this.loginAckWaiters.add(waiter); + }); + } + + blockTimeout(defaultSeconds: number): number | null { + if (this.mode === Mode.MODE_HITL) return null; + if (this.mode === Mode.MODE_BLOCK) return defaultSeconds; + return 0; + } + + async waitForToolCallVerdict(toolCallId: string, timeoutSeconds: number | null): Promise { + const eventId = this.toolCallIdToEventId.get(toolCallId); + if (!eventId) return null; + return this.waitForVerdict(eventId, timeoutSeconds); + } + + async waitForVerdict(eventId: string, timeoutSeconds: number | null): Promise { + const cached = this.verdictCache.get(eventId); + if (cached) return cached; + return new Promise((resolve) => { + const entry: VerdictWaiter = { resolve }; + if (timeoutSeconds !== null) { + entry.timer = setTimeout(() => this.resolveVerdict(eventId, null), timeoutSeconds * 1000); + } + const waiters = this.pendingVerdicts.get(eventId) ?? []; + waiters.push(entry); + this.pendingVerdicts.set(eventId, waiters); + }); + } + + async close(): Promise { + this.closing = true; + this.ws?.close(); + for (const [eventId] of this.pendingVerdicts) this.resolveVerdict(eventId, null); + this.resolveLoginAckWaiters(false); + } + + private async connectLoop(): Promise { + let backoff = INITIAL_BACKOFF_MS; + while (!this.closing) { + const initialDelay = this.nextReconnectDelay; + this.nextReconnectDelay = null; + if (initialDelay !== null) await sleep(initialDelay); + + try { + await this.connectOnce(); + backoff = INITIAL_BACKOFF_MS; + await this.waitForClose(); + } catch { + if (this.closing) return; + await sleep(backoff); + backoff = Math.min(backoff * 2, MAX_BACKOFF_MS); + continue; + } + if (this.closing) return; + } + } + + private async connectOnce(): Promise { + await new Promise((resolve, reject) => { + const ws = new WebSocket(this.url, { headers: { Authorization: `Bearer ${this.apiKey}` } }); + this.ws = ws; + ws.binaryType = "arraybuffer"; + ws.once("open", () => { + void this.sendRaw(encodeClientFrame({ login: { sessionId: this.sessionId, llmStack: { provider: this.provider, model: this.model }, schemaVersion: SCHEMA_VERSION } })); + resolve(); + }); + ws.on("message", (data) => void this.handleMessage(data)); + ws.once("error", reject); + ws.once("close", (code) => { + if (code === QUOTA_EXHAUSTED_CLOSE_CODE) this.nextReconnectDelay = QUOTA_RECONNECT_DELAY_MS; + this.loggedIn = false; + this.replaying = false; + this.policy = null; + this.mode = Mode.MODE_UNSPECIFIED; + this.resolveLoginAckWaiters(false); + if (!this.closing) { + const reason = code === QUOTA_EXHAUSTED_CLOSE_CODE + ? `quota_exhausted (close=${code})` + : "recv_loop_exit"; + void this.onDisconnect?.(reason); + } + }); + }); + } + + private waitForClose(): Promise { + const ws = this.ws; + if (!ws || ws.readyState === WebSocket.CLOSED) return Promise.resolve(); + return new Promise((resolve) => ws.once("close", () => resolve())); + } + + private async handleMessage(data: WebSocket.RawData): Promise { + const bytes = data instanceof Buffer ? data : Buffer.from(data as ArrayBuffer); + let frame: ReturnType; + try { + frame = decodeServerFrame(bytes); + } catch { + return; + } + if ("loginAck" in frame) { + this.policy = frame.loginAck.policy; + this.mode = frame.loginAck.policy.mode; + this.loggedIn = true; + this.resolveLoginAckWaiters(true); + if (this.hadConnection) await this.onReconnect?.(); + this.hadConnection = true; + await this.drainReplayBuffer(); + await this.onLoginAck?.(); + return; + } + const verdict = frame.verdict; + this.resolveVerdict(verdict.eventId, verdict); + await this.handler?.handleVerdict(verdict); + } + + private async sendFrame(frame: Uint8Array): Promise { + if (!this.loggedIn || this.replaying || !this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.bufferFrame(frame); + return; + } + try { + await this.sendRaw(frame); + } catch { + this.bufferFrame(frame); + await this.onDisconnect?.("send_failure"); + } + } + + private async sendRaw(frame: Uint8Array): Promise { + const ws = this.ws; + if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("websocket is not open"); + await new Promise((resolve, reject) => ws.send(frame, { binary: true }, (err) => err ? reject(err) : resolve())); + } + + private async drainReplayBuffer(): Promise { + this.replaying = true; + try { + while (this.replayBuffer.length > 0 && this.ws?.readyState === WebSocket.OPEN) { + const frame = this.replayBuffer.shift(); + if (frame) await this.sendRaw(frame); + } + } finally { + this.replaying = false; + } + this.droppedFrames = 0; + } + + private bufferFrame(frame: Uint8Array): void { + if (this.replayLimit <= 0) return; + if (this.replayBuffer.length >= this.replayLimit) { + this.replayBuffer.shift(); + this.droppedFrames += 1; + } + this.replayBuffer.push(frame); + } + + private resolveVerdict(eventId: string, verdict: Verdict | null): void { + if (verdict) this.setLru(this.verdictCache, eventId, verdict, MAX_VERDICT_CACHE); + const waiters = this.pendingVerdicts.get(eventId); + if (!waiters) return; + this.pendingVerdicts.delete(eventId); + for (const entry of waiters) { + if (entry.timer) clearTimeout(entry.timer); + entry.resolve(verdict); + } + } + + private resolveLoginAckWaiters(acked: boolean): void { + for (const waiter of this.loginAckWaiters) { + if (waiter.timer) clearTimeout(waiter.timer); + waiter.resolve(acked); + } + this.loginAckWaiters.clear(); + } + + private setLru(map: Map, key: string, value: V, limit: number): void { + if (map.has(key)) map.delete(key); + map.set(key, value); + while (map.size > limit) map.delete(map.keys().next().value as string); + } +} + +export function shouldHalt(verdict: Verdict): boolean { + if (verdict.hitl) return !verdict.hitl.continueExecution; + const prefix = verdict.madCode.slice(0, 2); + switch (prefix) { + case "M0": return verdict.policy.policyM0; + case "M2": return verdict.policy.policyM2; + case "M3": return verdict.policy.policyM3; + case "M4": return verdict.policy.policyM4; + default: return false; + } +} + +function deriveProvider(modelClassName: string): string { + const key = modelClassName.toLowerCase(); + return ({ chatanthropic: "anthropic", chatopenai: "openai", chatgooglegenai: "google", chatcohere: "cohere", chatmistralai: "mistral" } as Record)[key] ?? key; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/sdk/typescript/packages/core/tests/capture.test.ts b/sdk/typescript/packages/core/tests/capture.test.ts new file mode 100644 index 0000000..f6c0c04 --- /dev/null +++ b/sdk/typescript/packages/core/tests/capture.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { messagesFromPromptLike, normalizeResponseInput } from "../src/capture/common.js"; + +describe("normalizeResponseInput", () => { + it("maps role-based input items", () => { + expect(normalizeResponseInput([ + { role: "user", content: "hello" }, + ])).toEqual([{ role: "user", content: "hello" }]); + }); + + it("maps function calls and outputs", () => { + expect(normalizeResponseInput([ + { type: "function_call", name: "get_weather", arguments: '{"city":"SF"}' }, + { type: "function_call_output", output: '{"temp":58}' }, + ])).toEqual([ + { role: "assistant", content: '[tool_call:get_weather] {"city":"SF"}' }, + { role: "tool", content: '{"temp":58}' }, + ]); + }); +}); + +describe("messagesFromPromptLike", () => { + it("prepends instructions as system for string input", () => { + expect(messagesFromPromptLike({ + instructions: "You are helpful.", + input: "Run the task.", + })).toEqual([ + { role: "system", content: "You are helpful." }, + { role: "user", content: "Run the task." }, + ]); + }); + + it("prepends instructions for Responses API input arrays", () => { + expect(messagesFromPromptLike({ + instructions: "You are an autonomous assistant.", + input: [ + { role: "user", content: "Do the work." }, + { type: "function_call", name: "add_numbers", arguments: '{"a":1,"b":2}' }, + { type: "function_call_output", output: '{"result":3}' }, + ], + })).toEqual([ + { role: "system", content: "You are an autonomous assistant." }, + { role: "user", content: "Do the work." }, + { role: "assistant", content: '[tool_call:add_numbers] {"a":1,"b":2}' }, + { role: "tool", content: '{"result":3}' }, + ]); + }); +}); diff --git a/sdk/typescript/packages/core/tests/init.test.ts b/sdk/typescript/packages/core/tests/init.test.ts new file mode 100644 index 0000000..a90f66e --- /dev/null +++ b/sdk/typescript/packages/core/tests/init.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { currentConfig, resolveInitOptions } from "../src/config.js"; +import { getWebSocketClient, init, shutdown } from "../src/index.js"; + +describe("resolveInitOptions", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("honours explicit wsUrl: null over ADRIAN_WS_URL", () => { + vi.stubEnv("ADRIAN_WS_URL", "wss://env.example/ws"); + expect(resolveInitOptions({ wsUrl: null }).wsUrl).toBeNull(); + }); + + it("honours explicit wsUrl over ADRIAN_WS_URL", () => { + vi.stubEnv("ADRIAN_WS_URL", "wss://env.example/ws"); + expect(resolveInitOptions({ wsUrl: "wss://explicit.example/ws" }).wsUrl).toBe("wss://explicit.example/ws"); + }); + + it("honours explicit blockTimeout over ADRIAN_BLOCK_TIMEOUT", () => { + vi.stubEnv("ADRIAN_BLOCK_TIMEOUT", "99"); + expect(resolveInitOptions({ blockTimeout: 10 }).blockTimeout).toBe(10); + }); +}); + +describe("init option resolution", () => { + afterEach(async () => { + vi.unstubAllEnvs(); + await shutdown(); + }); + + it("does not create a WebSocket client when wsUrl is explicitly null", async () => { + vi.stubEnv("ADRIAN_WS_URL", "wss://env.example/ws"); + await init({ wsUrl: null, handlers: [] }); + expect(getWebSocketClient()).toBeNull(); + expect(currentConfig()?.wsUrl).toBeNull(); + }); + + it("stores explicit blockTimeout when env is set", async () => { + vi.stubEnv("ADRIAN_BLOCK_TIMEOUT", "99"); + await init({ blockTimeout: 10, handlers: [] }); + expect(currentConfig()?.blockTimeout).toBe(10); + }); +}); diff --git a/sdk/typescript/packages/core/tests/jsonl.test.ts b/sdk/typescript/packages/core/tests/jsonl.test.ts new file mode 100644 index 0000000..9c10c51 --- /dev/null +++ b/sdk/typescript/packages/core/tests/jsonl.test.ts @@ -0,0 +1,28 @@ +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { JSONLHandler } from "../src/handlers/jsonl.js"; +import type { PairedEvent } from "../src/format/types.js"; + +it("writes paired events as jsonl", async () => { + const dir = await mkdtemp(join(tmpdir(), "adrian-ts-")); + const path = join(dir, "events.jsonl"); + const handler = new JSONLHandler(path); + const event: PairedEvent = { + eventId: "evt", + invocationId: "inv", + sessionId: "sess", + runId: "run", + parentRunId: "", + timestamp: new Date(0).toISOString(), + pairType: "tool", + agent: { agentId: "agent", systemPrompt: "", userInstruction: "" }, + parent: null, + data: { kind: "tool", toolName: "search", toolCallId: null, input: "x", output: "y" }, + metadata: null, + }; + await handler.onPairedEvent(event); + await handler.close(); + expect(await readFile(path, "utf8")).toContain('"eventId":"evt"'); +}); diff --git a/sdk/typescript/packages/core/tests/pairing.test.ts b/sdk/typescript/packages/core/tests/pairing.test.ts new file mode 100644 index 0000000..9e50a06 --- /dev/null +++ b/sdk/typescript/packages/core/tests/pairing.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { EventPairBuffer } from "../src/pairing.js"; + +it("pairs chat model start and llm end events", () => { + const buffer = new EventPairBuffer(); + buffer.onStart({ + eventType: "chat_model_start", + data: { model: "ChatOpenAI", messages: [{ role: "system", content: "sys" }, { role: "human", content: "hi" }], metadata: null }, + runId: "run-1", + agentId: "agent", + parent: null, + metadata: null, + }); + const pair = buffer.onEnd({ eventType: "llm_end", data: { output: "hello", toolCalls: [], usage: null }, runId: "run-1", invocationId: "inv", sessionId: "sess" }); + expect(pair?.pairType).toBe("llm"); + expect(pair?.agent.systemPrompt).toBe("sys"); + expect(pair?.agent.userInstruction).toBe("hi"); +}); diff --git a/sdk/typescript/packages/core/tests/pii.test.ts b/sdk/typescript/packages/core/tests/pii.test.ts new file mode 100644 index 0000000..035d201 --- /dev/null +++ b/sdk/typescript/packages/core/tests/pii.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { redactText, PiiType, RedactionStrategy } from "../src/pii/index.js"; +import { applyStrategy } from "../src/pii/strategies.js"; + +describe("PII patterns parity with Python", () => { + it("preserves passport label and redacts only digits", () => { + const r = redactText("passport: 123456789"); + expect(r.text).toContain("passport"); + expect(r.text).toContain("[PASSPORT_REDACTED]"); + }); + + it("recognizes DL and D.L. forms and redacts only the id", () => { + const r1 = redactText("DL: A12345"); + expect(r1.text).toContain("DL"); + expect(r1.text).toContain("[DRIVER_LICENSE_REDACTED]"); + + const r2 = redactText("D.L.: Z99999"); + expect(r2.text).toContain("D.L."); + expect(r2.text).toContain("[DRIVER_LICENSE_REDACTED]"); + }); + + it("redacts private IPv6 but not public IPv6", () => { + const privateR = redactText("host fe80::1", { strategy: RedactionStrategy.REPLACE }); + expect(privateR.text).toContain("[IP_ADDRESS_REDACTED]"); + + const publicR = redactText("dns 2001:4860:4860::8888", { strategy: RedactionStrategy.REPLACE }); + expect(publicR.text).not.toContain("[IP_ADDRESS_REDACTED]"); + }); + + it("masks IPv6 preserving only last group", () => { + const masked = redactText("ip fe80::1", { strategy: RedactionStrategy.MASK }); + expect(masked.text).toContain(":1"); + }); + + it("requires postal context for US ZIP but not arbitrary 5-digit numbers", () => { + const zip = redactText("My zip is 02115"); + expect(zip.text).toContain("[POSTAL_CODE_REDACTED]"); + + const order = redactText("Order 12345"); + expect(order.text).not.toContain("[POSTAL_CODE_REDACTED]"); + }); + + it("requires DOB context for dates", () => { + const withContext = redactText("born 1 January 1970"); + expect(withContext.text).toContain("[DATE_OF_BIRTH_REDACTED]"); + + const without = redactText("1 January 1970"); + expect(without.text).not.toContain("[DATE_OF_BIRTH_REDACTED]"); + }); + + it("detects street addresses with secondary unit (Apt/Suite)", () => { + const r = redactText("123 Main Street Apt 4"); + expect(r.text).toContain("[STREET_ADDRESS_REDACTED]"); + }); + + it("driver license requires separator like [:.#]", () => { + const withSep = redactText("driver license: 12345"); + expect(withSep.text).toContain("[DRIVER_LICENSE_REDACTED]"); + + const withoutSep = redactText("driver license 12345"); + expect(withoutSep.text).not.toContain("[DRIVER_LICENSE_REDACTED]"); + }); + + it("masking functions return all-asterisks when fewer than 4 digits", () => { + const phoneDet = { piiType: PiiType.PHONE, start: 0, end: 3, text: "123" }; + const phoneMasked = applyStrategy(phoneDet as any, RedactionStrategy.MASK); + expect(phoneMasked).toBe("***"); + + const ssnDet = { piiType: PiiType.SSN, start: 0, end: 3, text: "123" }; + const ssnMasked = applyStrategy(ssnDet as any, RedactionStrategy.MASK); + expect(ssnMasked).toBe("***-**-****"); + + const ccDet = { piiType: PiiType.CREDIT_CARD, start: 0, end: 3, text: "123" }; + const ccMasked = applyStrategy(ccDet as any, RedactionStrategy.MASK); + expect(ccMasked).toBe("****-****-****-****"); + }); +}); diff --git a/sdk/typescript/packages/core/tests/policy.test.ts b/sdk/typescript/packages/core/tests/policy.test.ts new file mode 100644 index 0000000..d469b85 --- /dev/null +++ b/sdk/typescript/packages/core/tests/policy.test.ts @@ -0,0 +1,138 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { setConfig, Mode, type Verdict, type WebSocketClient } from "../src/index.js"; +import { assertToolCallsAllowed, gateToolCallIds } from "../src/policy.js"; + +function config(): Parameters[0] { + return { + apiKey: null, + logFile: "events.jsonl", + logLevel: null, + sessionId: "sess", + wsUrl: null, + blockTimeout: 5, + onEvent: null, + onVerdict: null, + onBlock: null, + onAudit: null, + onDisconnect: null, + onReconnect: null, + onMcpServer: null, + replayBufferFrames: 1000, + }; +} + +function verdict(eventId: string, halt: boolean): Verdict { + return { + eventId, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, + hitl: null, + }; +} + +describe("gateToolCallIds", () => { + afterEach(() => setConfig(null)); + + it("allows when WebSocket is absent", async () => { + expect(await gateToolCallIds(["call-1"], null)).toEqual({ action: "allow" }); + }); + + it("allows when policy is not active", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => false, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => verdict("evt", false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1"], ws)).toEqual({ action: "allow" }); + }); + + it("allows on missing tool call id (fail-open)", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => verdict("evt", false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1", ""], ws)).toEqual({ action: "allow" }); + }); + + it("allows when all tool call ids are empty (fail-open)", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => verdict("evt", false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["", ""], ws)).toEqual({ action: "allow" }); + }); + + it("does not wait for verdicts on empty tool call ids", async () => { + setConfig(config()); + const waitedFor: string[] = []; + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => { + waitedFor.push(toolCallId); + return verdict(`event-${toolCallId}`, false); + }, + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["", "call-1", ""], ws)).toEqual({ action: "allow" }); + expect(waitedFor).toEqual(["call-1"]); + }); + + it("blocks when a verdict requests halt", async () => { + setConfig(config()); + const waitedFor: string[] = []; + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => { + waitedFor.push(toolCallId); + return verdict(`event-${toolCallId}`, toolCallId === "call-2"); + }, + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1", "call-2"], ws)).toEqual({ action: "block", reason: "policy_halt" }); + expect(waitedFor).toEqual(["call-1", "call-2"]); + }); + + it("allows when all verdicts permit execution", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => verdict(`event-${toolCallId}`, false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1", "call-2"], ws)).toEqual({ action: "allow" }); + }); + + it("allows when verdicts time out (fail-open)", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => null, + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1"], ws)).toEqual({ action: "allow" }); + }); + + it("assertToolCallsAllowed allows on verdict timeout (fail-open)", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => null, + } as unknown as WebSocketClient; + await expect(assertToolCallsAllowed(["call-1"], ws)).resolves.toBeUndefined(); + }); +}); diff --git a/sdk/typescript/packages/core/tests/proto.test.ts b/sdk/typescript/packages/core/tests/proto.test.ts new file mode 100644 index 0000000..9776de1 --- /dev/null +++ b/sdk/typescript/packages/core/tests/proto.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { encodeClientFrame, SCHEMA_VERSION } from "../src/proto/schema.js"; +import type { PairedEvent } from "../src/format/types.js"; + +it("encodes login and paired event frames", () => { + const login = encodeClientFrame({ login: { sessionId: "sess", llmStack: { provider: "openai", model: "gpt" }, schemaVersion: SCHEMA_VERSION } }); + expect(login.length).toBeGreaterThan(0); + + const event: PairedEvent = { + eventId: "evt", + invocationId: "inv", + sessionId: "sess", + runId: "run", + parentRunId: "", + timestamp: new Date(0).toISOString(), + pairType: "llm", + agent: { agentId: "agent", systemPrompt: "", userInstruction: "" }, + parent: null, + data: { kind: "llm", model: "ChatOpenAI", messages: [], output: "ok", toolCalls: [], usage: null }, + metadata: null, + }; + const batch = encodeClientFrame({ pairedBatch: { events: [event] } }); + expect(batch.length).toBeGreaterThan(0); +}); diff --git a/sdk/typescript/packages/core/tests/session.test.ts b/sdk/typescript/packages/core/tests/session.test.ts new file mode 100644 index 0000000..2ba7bf5 --- /dev/null +++ b/sdk/typescript/packages/core/tests/session.test.ts @@ -0,0 +1,6 @@ +import { describe, expect, it } from "vitest"; +import { cwdKey } from "../src/sessionPersistence.js"; + +it("encodes cwd into a flat key", () => { + expect(cwdKey("/tmp/adrian")).toContain("-tmp-adrian"); +}); diff --git a/sdk/typescript/packages/core/tests/ws.test.ts b/sdk/typescript/packages/core/tests/ws.test.ts new file mode 100644 index 0000000..734db4f --- /dev/null +++ b/sdk/typescript/packages/core/tests/ws.test.ts @@ -0,0 +1,252 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { PairedEvent } from "../src/format/types.js"; +import { Mode, type Verdict } from "../src/proto/schema.js"; + +const wsMock = vi.hoisted(() => { + const { EventEmitter } = require("node:events") as typeof import("node:events"); + const created: InstanceType[] = []; + let autoCloseCode: number | null = 4003; + let autoCloseRemaining = 1; + let failNextConnections = 0; + + class MockWebSocket extends EventEmitter { + static readonly CONNECTING = 0; + static readonly OPEN = 1; + static readonly CLOSING = 2; + static readonly CLOSED = 3; + readyState = MockWebSocket.CONNECTING; + binaryType = "arraybuffer"; + + constructor( + public url: string, + public options?: unknown, + ) { + super(); + created.push(this); + queueMicrotask(() => { + if (this.readyState === MockWebSocket.CLOSED) return; + if (failNextConnections > 0) { + failNextConnections -= 1; + this.readyState = MockWebSocket.CLOSED; + this.emit("error", new Error("connect failed")); + return; + } + this.readyState = MockWebSocket.OPEN; + this.emit("open"); + if (autoCloseRemaining > 0 && autoCloseCode !== null) { + autoCloseRemaining -= 1; + queueMicrotask(() => { + this.readyState = MockWebSocket.CLOSED; + this.emit("close", autoCloseCode); + }); + } + }); + } + + send(_data: unknown, _opts: unknown, cb?: (err?: Error | null) => void): void { + cb?.(null); + } + + close(code?: number): void { + if (this.readyState === MockWebSocket.CLOSED) return; + this.readyState = MockWebSocket.CLOSED; + this.emit("close", code ?? 1000); + } + } + + return { + created, + get autoCloseCode() { + return autoCloseCode; + }, + set autoCloseCode(value: number | null) { + autoCloseCode = value; + }, + get autoCloseRemaining() { + return autoCloseRemaining; + }, + set autoCloseRemaining(value: number) { + autoCloseRemaining = value; + }, + get failNextConnections() { + return failNextConnections; + }, + set failNextConnections(value: number) { + failNextConnections = value; + }, + reset(): void { + created.length = 0; + autoCloseCode = 4003; + autoCloseRemaining = 1; + failNextConnections = 0; + }, + MockWebSocket, + }; +}); + +vi.mock("ws", () => ({ + default: wsMock.MockWebSocket, +})); + +import { WebSocketClient } from "../src/ws.js"; + +const QUOTA_RECONNECT_DELAY_MS = 60_000; + +function client(onDisconnect?: (reason: string) => void): WebSocketClient { + return new WebSocketClient({ + url: "ws://localhost:0", + sessionId: "sess", + apiKey: "key", + replayBufferFrames: 10, + onDisconnect, + }); +} + +function verdict(eventId: string): Verdict { + return { + eventId, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: true, policyM4: false }, + hitl: null, + }; +} + +function llmEvent(eventId: string): PairedEvent { + return { + eventId, + invocationId: "inv", + sessionId: "sess", + runId: "run", + parentRunId: "", + timestamp: new Date(0).toISOString(), + pairType: "llm", + agent: { agentId: "agent", systemPrompt: "", userInstruction: "" }, + parent: null, + data: { kind: "llm", model: "ChatOpenAI", messages: [], output: "", toolCalls: [{ id: "tool-1", name: "search", args: {} }], usage: null }, + metadata: null, + }; +} + +function nextReconnectDelay(ws: WebSocketClient): number | null { + return (ws as unknown as { nextReconnectDelay: number | null }).nextReconnectDelay; +} + +async function flushConnectionLifecycle(): Promise { + await vi.advanceTimersByTimeAsync(0); + await vi.advanceTimersByTimeAsync(0); +} + +describe("WebSocketClient verdict waiting", () => { + it("replays a verdict that arrives before a waiter is registered", async () => { + const ws = client(); + const early = verdict("evt-1"); + (ws as unknown as { resolveVerdict: (eventId: string, verdict: Verdict) => void }).resolveVerdict("evt-1", early); + + await expect(ws.waitForVerdict("evt-1", 1)).resolves.toBe(early); + }); + + it("resolves every waiter registered for the same event", async () => { + const ws = client(); + const expected = verdict("evt-2"); + const first = ws.waitForVerdict("evt-2", 1); + const second = ws.waitForVerdict("evt-2", 1); + + (ws as unknown as { resolveVerdict: (eventId: string, verdict: Verdict) => void }).resolveVerdict("evt-2", expected); + + await expect(Promise.all([first, second])).resolves.toEqual([expected, expected]); + }); + + it("uses cached event verdicts for correlated tool calls", async () => { + const ws = client(); + await ws.onPairedEvent(llmEvent("evt-3")); + const expected = verdict("evt-3"); + (ws as unknown as { resolveVerdict: (eventId: string, verdict: Verdict) => void }).resolveVerdict("evt-3", expected); + + await expect(ws.waitForToolCallVerdict("tool-1", 1)).resolves.toBe(expected); + }); +}); + +describe("WebSocketClient quota-exhausted reconnect", () => { + beforeEach(() => { + vi.useFakeTimers(); + wsMock.reset(); + }); + + afterEach(() => { + vi.useRealTimers(); + wsMock.reset(); + }); + + it("arms a 60s reconnect delay when the server closes with 4003", async () => { + const disconnects: string[] = []; + const ws = client((reason) => disconnects.push(reason)); + ws.scheduleConnect(); + + await flushConnectionLifecycle(); + + expect(disconnects).toEqual(["quota_exhausted (close=4003)"]); + expect(wsMock.created).toHaveLength(1); + expect(nextReconnectDelay(ws)).toBeNull(); + + await vi.advanceTimersByTimeAsync(QUOTA_RECONNECT_DELAY_MS - 1); + expect(wsMock.created).toHaveLength(1); + + await vi.advanceTimersByTimeAsync(1); + expect(wsMock.created).toHaveLength(2); + + await ws.close(); + }); + + it("reconnects immediately after a normal close", async () => { + wsMock.autoCloseCode = 1000; + wsMock.autoCloseRemaining = 1; + const disconnects: string[] = []; + const ws = client((reason) => disconnects.push(reason)); + ws.scheduleConnect(); + + await flushConnectionLifecycle(); + + expect(disconnects).toEqual(["recv_loop_exit"]); + expect(nextReconnectDelay(ws)).toBeNull(); + expect(wsMock.created).toHaveLength(2); + + await ws.close(); + }); + + it("backs off only when connect fails", async () => { + wsMock.autoCloseCode = null; + wsMock.failNextConnections = 1; + const ws = client(); + ws.scheduleConnect(); + + await vi.advanceTimersByTimeAsync(0); + expect(wsMock.created).toHaveLength(1); + + await vi.advanceTimersByTimeAsync(999); + expect(wsMock.created).toHaveLength(1); + + await vi.advanceTimersByTimeAsync(1); + expect(wsMock.created).toHaveLength(2); + + await ws.close(); + }); + + it("consumes a pending reconnect delay once before the next connect attempt", async () => { + wsMock.autoCloseCode = null; + const ws = client(); + (ws as unknown as { nextReconnectDelay: number | null }).nextReconnectDelay = QUOTA_RECONNECT_DELAY_MS; + ws.scheduleConnect(); + + expect(wsMock.created).toHaveLength(0); + expect(nextReconnectDelay(ws)).toBeNull(); + + await vi.advanceTimersByTimeAsync(QUOTA_RECONNECT_DELAY_MS - 1); + expect(wsMock.created).toHaveLength(0); + + await vi.advanceTimersByTimeAsync(1); + expect(wsMock.created).toHaveLength(1); + + await ws.close(); + }); +}); diff --git a/sdk/typescript/packages/core/tsconfig.build.json b/sdk/typescript/packages/core/tsconfig.build.json new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/sdk/typescript/packages/core/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/core/tsconfig.json b/sdk/typescript/packages/core/tsconfig.json new file mode 100644 index 0000000..6140ae3 --- /dev/null +++ b/sdk/typescript/packages/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/sdk/typescript/tsconfig.base.json b/sdk/typescript/tsconfig.base.json index e1cd4d1..1233a6c 100644 --- a/sdk/typescript/tsconfig.base.json +++ b/sdk/typescript/tsconfig.base.json @@ -1,14 +1,15 @@ { "compilerOptions": { - "target": "es2022", - "module": "nodenext", - "moduleResolution": "nodenext", - "lib": ["es2023"], + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", "strict": true, "declaration": true, + "declarationMap": true, + "sourceMap": true, "esModuleInterop": true, - "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "verbatimModuleSyntax": true + "skipLibCheck": true, + "types": ["node"] } }