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..6ea1215 --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,2676 @@ +{ + "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": "^6.5.0", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@secureagentics/adrian": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "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.7", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-require": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-3.1.2.tgz", + "integrity": "sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.13" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "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", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "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-darwin-arm64": { + "version": "1.32.0", + "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/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "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/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "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", + "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/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "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": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/protobufjs": { + "version": "8.4.2", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.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.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/rollup": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "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", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-6.5.0.tgz", + "integrity": "sha512-36u82r7rYqRHFkD15R20Cd4ercPkbYmuvRkz3Q1LCm5BsiFNUgpo36zbjVhCOgvjyxNBWNKHsaD5Rl8SykfzNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^3.1.2", + "cac": "^6.7.12", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.15.1", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^3.0.1", + "resolve-from": "^5.0.0", + "rollup": "^3.2.5", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.20.3", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": "^4.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/tsup/node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "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.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "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.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "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.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "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.20.1", + "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 + } + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "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": "^6.5.0", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + }, + "packages/openai": { + "extraneous": true + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index c9a6ace..1f958cf 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,7 +12,8 @@ }, "devDependencies": { "@types/node": "^25.9.1", - "tsup": "^8.5.1", + "@types/ws": "^8.18.1", + "tsup": "^6.5.0", "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..97f460b --- /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": "^6.5.0", + "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..6c22718 --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/patterns.ts @@ -0,0 +1,65 @@ +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; +} + +const PATTERNS: Array<[PiiType, RegExp]> = [ + [PiiType.EMAIL, /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g], + [PiiType.PHONE, /(? | null): Detection[] { + const detections: Detection[] = []; + for (const [piiType, pattern] of PATTERNS) { + if (types && !types.has(piiType)) continue; + pattern.lastIndex = 0; + for (const match of text.matchAll(pattern)) { + const matched = match[0]; + const start = match.index ?? 0; + if (piiType === PiiType.CREDIT_CARD && !luhnCheck(matched.replace(/\D/g, ""))) continue; + detections.push({ piiType, start, end: start + matched.length, text: matched }); + } + } + return detections.sort((a, b) => a.start - b.start || b.end - a.end); +} + +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 digits.length >= 13 && total % 10 === 0; +} 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..0e6b34e --- /dev/null +++ b/sdk/typescript/packages/core/src/pii/strategies.ts @@ -0,0 +1,38 @@ +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: { + const [local, domain] = text.split("@"); + const suffix = domain?.split(".").at(-1) ?? ""; + return `${local?.[0] ?? "*"}***@***.${suffix}`; + } + case PiiType.PHONE: + return `***-***-${text.replace(/\D/g, "").slice(-4)}`; + case PiiType.SSN: + return `***-**-${text.replace(/\D/g, "").slice(-4)}`; + case PiiType.CREDIT_CARD: + return `****-****-****-${text.replace(/\D/g, "").slice(-4)}`; + case PiiType.IP_ADDRESS: + return text.includes(":") ? `****:${text.split(":").at(-1) ?? ""}` : `***.***.***.${text.split(".").at(-1) ?? ""}`; + default: + return text.length <= 2 ? "*".repeat(text.length) : text[0] + "*".repeat(text.length - 2) + text.at(-1); + } +} 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..ac6e45f --- /dev/null +++ b/sdk/typescript/packages/core/tests/pii.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { redactText, RedactionStrategy } from "../src/pii/index.js"; + +it("redacts email addresses", () => { + const result = redactText("email me at test@example.com"); + expect(result.text).toContain("[EMAIL_REDACTED]"); +}); + +it("can mask phone numbers", () => { + const result = redactText("call 415-555-1234", { strategy: RedactionStrategy.MASK }); + expect(result.text).toContain("***-***-1234"); +}); 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"] } }