diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5d2dfb..a85c488 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 two packages (`core`, `openai`). From that directory: + +```sh +cd sdk/typescript +npm install +npm run build +npm test +``` + +Provider packages include `@secureagentics/adrian` as a dependency and re-export `init`, `shutdown`, and other core APIs — users install one package (e.g. `@secureagentics/adrian-openai openai`) and import everything from it. 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/Makefile b/Makefile index af6d231..bf545e9 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ # Adrian OSS — top-level developer Makefile # # Local-dev convention: every Python entry point goes through the -# project-local ``.venv`` so the bundled ``sdk/python/`` install (``pip -# install -e ./sdk/python``) cannot collide with a system-wide -# ``adrian-sdk`` wheel from PyPI. ``make sdk-install`` creates the venv -# and the editable install in one shot. +# project-local ``.venv`` so the bundled Python SDK install (``pip +# install -e ./sdk/python``) cannot collide with a system-wide ``adrian-sdk`` +# wheel from PyPI. ``make sdk-install`` creates the venv and the +# editable install in one shot. .PHONY: sdk-install sdk-test sdk-clean help @@ -24,7 +24,7 @@ endif @echo "Or run anything via uv: 'uv run python ...'" sdk-test: ## Run the bundled SDK test suite - uv run --project ./sdk/python pytest sdk/python/tests + uv run --project ./sdk/python --extra dev pytest sdk/python/tests sdk-clean: ## Remove .venv and SDK build artefacts rm -rf .venv sdk/python/.venv sdk/python/dist sdk/python/build sdk/python/*.egg-info diff --git a/backend/Makefile b/backend/Makefile index 8a00e50..c4f3841 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -28,12 +28,12 @@ clean: # the Go bindings. Strips the buf.validate import and field annotations # (see proto/README.md). Requires protoc + protoc-gen-go on PATH. proto-sync: - @echo "Syncing proto/event.proto from ../proto/..." + @echo "Syncing proto/event.proto from sdk/python/adrian/proto/..." @sed \ -e '/^import "buf\/validate\/validate.proto";$$/d' \ -e 's| \[(buf\.validate\.field)[^]]*\]||g' \ -e '/^package adrian\.core_api\.v1;$$/a\option go_package = "github.com/secureagentics/Adrian/backend/internal/proto;proto";' \ - ../proto/event.proto > proto/event.proto + ../sdk/python/adrian/proto/event.proto > proto/event.proto @$(MAKE) proto-regen # Regenerate Go bindings from proto/event.proto. diff --git a/backend/data/adrian.db b/backend/data/adrian.db new file mode 100644 index 0000000..f17b681 Binary files /dev/null and b/backend/data/adrian.db differ diff --git a/backend/proto/README.md b/backend/proto/README.md index db15bf0..593737a 100644 --- a/backend/proto/README.md +++ b/backend/proto/README.md @@ -4,7 +4,7 @@ The wire schema for SDK <-> backend WebSocket frames. ## Layout -- `event.proto` is the Go-side copy of the wire schema. The single source of truth is `proto/event.proto` at the repo root. +- `event.proto` is the Go-side copy of the wire schema. The single source of truth is `sdk/python/adrian/proto/event.proto` at the repo root. - The Go-side copy is **stripped** of the `buf.validate` import and field annotations: - The `import "buf/validate/validate.proto";` line is removed. - Every `[(buf.validate.field).*]` annotation is removed. @@ -15,7 +15,7 @@ The wire schema for SDK <-> backend WebSocket frames. When the wire schema changes: -1. Edit `proto/event.proto` (the source of truth). +1. Edit `sdk/python/adrian/proto/event.proto` (the source of truth). 2. Regenerate the SDK's Python bindings: `cd sdk/python && uv run bash tools/regen-proto.sh`. 3. Sync the backend copy and regenerate the Go bindings: `cd backend && make proto-sync`. 4. Commit both. diff --git a/sdk/python/adrian/__init__.py b/sdk/python/adrian/__init__.py index 03b7fd4..212b4ff 100644 --- a/sdk/python/adrian/__init__.py +++ b/sdk/python/adrian/__init__.py @@ -23,10 +23,11 @@ import asyncio import atexit +import importlib import logging import os from pathlib import Path -from typing import Any +from typing import Any, cast from uuid import uuid4 from langchain_core.callbacks.manager import CallbackManager @@ -679,10 +680,15 @@ def _patch_langgraph() -> None: top-level call so all sub-agent events share the same ID. """ try: - from langgraph.pregel import Pregel + pregel_mod = importlib.import_module("langgraph.pregel") except ImportError: return + Pregel = cast("Any", getattr(pregel_mod, "Pregel", None)) + + if Pregel is None: + return + if getattr(Pregel, "_adrian_pregel_patched", False): return @@ -864,10 +870,15 @@ def _patch_tool_node() -> None: tools. On timeout it fails open. """ try: - from langgraph.prebuilt import ToolNode + prebuilt_mod = importlib.import_module("langgraph.prebuilt") except ImportError: return + ToolNode = cast("Any", getattr(prebuilt_mod, "ToolNode", None)) + + if ToolNode is None: + return + if getattr(ToolNode, "_adrian_tool_node_patched", False): return diff --git a/sdk/python/adrian/mcp.py b/sdk/python/adrian/mcp.py index b41eae9..7131a82 100644 --- a/sdk/python/adrian/mcp.py +++ b/sdk/python/adrian/mcp.py @@ -26,6 +26,7 @@ from __future__ import annotations import contextlib +import importlib import logging import sys from collections.abc import Mapping @@ -179,11 +180,11 @@ def _patch_mcp_adapter() -> None: # pyright: ignore[reportUnusedFunction] def _patch_langchain_mcp_adapters() -> None: """Patch ``MultiServerMCPClient.__init__`` to snapshot declarations.""" try: - from langchain_mcp_adapters import client as client_mod + client_mod = importlib.import_module("langchain_mcp_adapters.client") except ImportError: return - cls = getattr(client_mod, "MultiServerMCPClient", None) + cls = cast("Any", getattr(client_mod, "MultiServerMCPClient", None)) if cls is None or getattr(cls, "_adrian_mcp_patched", False): return diff --git a/sdk/python/adrian/proto/buf.gen.yaml b/sdk/python/adrian/proto/buf.gen.yaml new file mode 100644 index 0000000..5845eec --- /dev/null +++ b/sdk/python/adrian/proto/buf.gen.yaml @@ -0,0 +1,6 @@ +version: v2 +plugins: + - remote: buf.build/protocolbuffers/python + out: . + - remote: buf.build/protocolbuffers/pyi + out: . diff --git a/sdk/python/adrian/proto/buf.lock b/sdk/python/adrian/proto/buf.lock new file mode 100644 index 0000000..709ae02 --- /dev/null +++ b/sdk/python/adrian/proto/buf.lock @@ -0,0 +1,6 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 50325440f8f24053b047484a6bf60b76 + digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31 diff --git a/sdk/python/adrian/proto/buf.yaml b/sdk/python/adrian/proto/buf.yaml new file mode 100644 index 0000000..25426d9 --- /dev/null +++ b/sdk/python/adrian/proto/buf.yaml @@ -0,0 +1,5 @@ +version: v2 +modules: + - path: . +deps: + - buf.build/bufbuild/protovalidate diff --git a/proto/event.proto b/sdk/python/adrian/proto/event.proto similarity index 100% rename from proto/event.proto rename to sdk/python/adrian/proto/event.proto diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 2c18e6e..5d29efe 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.12" [[package]] name = "adrian-sdk-oss" -version = "1.0.2" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "langchain-core" }, @@ -18,6 +18,7 @@ dev = [ { name = "langchain-mcp-adapters" }, { name = "langgraph" }, { name = "langgraph-prebuilt" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -31,6 +32,7 @@ requires-dist = [ { name = "langchain-mcp-adapters", marker = "extra == 'dev'", specifier = ">=0.2.2" }, { name = "langgraph", marker = "extra == 'dev'", specifier = "==1.1.2" }, { name = "langgraph-prebuilt", marker = "extra == 'dev'", specifier = "==1.0.8" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.0" }, { name = "protobuf", specifier = ">=5.29.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, @@ -149,6 +151,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -380,6 +391,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -426,6 +455,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.13" @@ -626,6 +664,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "nodejs-wheel-binaries" version = "24.15.0" @@ -743,6 +790,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -752,6 +808,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "protobuf" version = "7.34.1" @@ -946,6 +1018,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "python-discovery" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1273,6 +1358,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, ] +[[package]] +name = "virtualenv" +version = "21.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, +] + [[package]] name = "websockets" version = "16.0" 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..4c02612 --- /dev/null +++ b/sdk/typescript/README.md @@ -0,0 +1,204 @@ +# Adrian TypeScript SDK + +Monorepo for the Adrian TypeScript SDK. Pick the package for your framework - the core SDK is installed automatically. + +The core package owns the event pipeline: event pairing, PII redaction, JSONL logging, WebSocket streaming, policy verdicts, and shared capture helpers. Provider packages, such as OpenAI, adapt framework-specific request and response shapes into that core pipeline. + +## Packages + +| Package | npm name | Install | Import | +|---|---|---|---| +| OpenAI | `@secureagentics/adrian-openai` | `npm install @secureagentics/adrian-openai openai` | `import { init, adrian, captureTool } from "@secureagentics/adrian-openai"` | +| Core only | `@secureagentics/adrian` | `npm install @secureagentics/adrian` | `import { init, shutdown } from "@secureagentics/adrian"` | + +Provider packages depend on `@secureagentics/adrian` and re-export `init`, `shutdown`, and other core APIs — one install, one import. + +## Two-step setup + +1. **`init()`** — starts the event pipeline (JSONL, WebSocket, PII redaction). +2. **`adrian()`** — connects your framework to Adrian. + +Both come from the same provider package: + +```ts +import { init, adrian, captureTool } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); +``` + +## Unified API + +The OpenAI provider package exports the familiar Adrian entrypoints: + +| Export | Purpose | +|---|---| +| `init` / `shutdown` | Re-exported from core | +| `adrian(...)` | Wrap an OpenAI client | +| `captureTool(...)` | Capture manual tool execution | + +Shared option types (same names in every provider package): + +| Type | Purpose | +|---|---| +| `AdrianOptions` | Optional metadata when wrapping a client or module | +| `ToolCallLike` | Shape of a tool call passed to `captureTool` | +| `ToolCaptureOptions` | Optional metadata when capturing tool execution | + +## Examples + +### OpenAI + +```bash +npm install @secureagentics/adrian-openai openai +``` + +```ts +import OpenAI from "openai"; +import { init, shutdown, adrian } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const openai = adrian(new OpenAI()); + +const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Hello" }], +}); + +await shutdown(); +``` + +### OpenAI tool execution + +OpenAI returns tool call requests; your app still executes the tools. Wrap that execution with `captureTool` so Adrian can apply BLOCK/HITL policy and capture the tool result: + +```ts +import OpenAI from "openai"; +import { init, shutdown, adrian, captureTool, AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const openai = adrian(new OpenAI()); +const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "user", content: "What is the weather in Paris?" }, +]; + +async function getWeather(city: string) { + return { city, forecast: "sunny" }; +} + +const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages, + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather for a city", + parameters: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }, + }, + ], +}); + +const assistantMessage = response.choices[0]?.message; +if (!assistantMessage) throw new Error("OpenAI response did not include an assistant message"); + +messages.push(assistantMessage); + +for (const toolCall of assistantMessage.tool_calls ?? []) { + let toolResult: unknown; + + try { + toolResult = await captureTool(toolCall, async () => { + const args = JSON.parse(toolCall.function.arguments || "{}") as { city?: string }; + return getWeather(args.city ?? ""); + }); + } catch (error) { + if (!(error instanceof AdrianPolicyBlockedError)) throw error; + toolResult = BLOCKED_TOOL_MESSAGE; + } + + messages.push({ + role: "tool", + tool_call_id: toolCall.id, + content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult), + }); +} + +await shutdown(); +``` + +### Responses API + +```ts +const response = await openai.responses.create({ + model: "gpt-4o-mini", + input: "Summarize the security considerations for this workflow.", +}); + +console.log(response.output_text); +``` + +### Streaming + +Streaming calls are passed through unchanged. Adrian emits one paired event when the stream finishes or the consumer exits early: + +```ts +const stream = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Write a short haiku." }], + stream: true, +}); + +for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); +} +``` + +### Local logging only + +Use `wsUrl: null` when you want JSONL logging without connecting to the Adrian backend: + +```ts +await init({ + wsUrl: null, + logFile: "events.jsonl", + onEvent: (eventType, data, runId, parentRunId, eventId) => { + console.log({ eventType, runId, parentRunId, eventId, data }); + }, +}); +``` + +## Environment variables + +| Variable | Description | +|---|---| +| `ADRIAN_API_KEY` | API key from the Adrian dashboard | +| `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/HITL verdict | +| `ADRIAN_REPLAY_BUFFER_FRAMES` | WebSocket replay buffer size | + +## Development + +```bash +npm install +npm run build +npm test +``` + +Build or test a single package: + +```bash +npm run build -w @secureagentics/adrian +npm test -w @secureagentics/adrian-openai +``` + +Per-package docs: [core](./packages/core) · [openai](./packages/openai) diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 0000000..d39fb7f --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,2736 @@ +{ + "name": "@secureagentics/adrian-workspace", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@secureagentics/adrian-workspace", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@secureagentics/adrian": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@secureagentics/adrian-openai": { + "resolved": "packages/openai", + "link": true + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "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", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "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", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "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", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "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", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/protobufjs": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.2.tgz", + "integrity": "sha512-64rfNzkWOZAIazXzpBFPWq6F9up6gMvTzjE2oWIzApx2N/dqVUEE7+bCn2+40780dFVtKOUab8QfxJ6KJDWbqA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "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": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "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", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "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", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "packages/core": { + "name": "@secureagentics/adrian", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + }, + "packages/openai": { + "name": "@secureagentics/adrian-openai", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@secureagentics/adrian": "^1.0.0" + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + }, + "peerDependencies": { + "openai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index c9a6ace..03f3bbb 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,7 +1,6 @@ { - "name": "adrian-typescript", + "name": "@secureagentics/adrian-workspace", "private": true, - "description": "Adrian TypeScript SDK monorepo (npm workspaces).", "workspaces": [ "packages/*" ], @@ -12,6 +11,7 @@ }, "devDependencies": { "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.7" diff --git a/sdk/typescript/packages/core/README.md b/sdk/typescript/packages/core/README.md new file mode 100644 index 0000000..eb9f1b1 --- /dev/null +++ b/sdk/typescript/packages/core/README.md @@ -0,0 +1,148 @@ +# @secureagentics/adrian + +Core TypeScript SDK for Adrian multi-agent security monitoring in Node.js. + +Handles the event pipeline: callback handler, event pairing, PII redaction, JSONL logging, WebSocket streaming, MCP inventory, and BLOCK/HITL tool gating. + +## Install + +You usually do **not** need to install this package directly. Provider packages install it automatically: + +```bash +npm install @secureagentics/adrian-openai openai # includes @secureagentics/adrian +``` + +Install core on its own only if you are wiring callbacks manually or building a custom integration: + +```bash +npm install @secureagentics/adrian +``` + +## Quick start (via a provider package) + +```ts +import OpenAI from "openai"; +import { init, shutdown, adrian } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); +const openai = adrian(new OpenAI()); +await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Hello" }], +}); +await shutdown(); +``` + +## Core exports + +| Export | Description | +|---|---| +| `init(options?)` | Initialise the SDK | +| `shutdown()` | Flush handlers and tear down | +| `getHandler()` | Access the callback handler for manual wiring | +| `getWebSocketClient()` | Access the WebSocket client | +| `AdrianCallbackHandler` | Event callback handler class | +| `JSONLHandler` | Local JSONL event sink | + +## Environment + +- `ADRIAN_API_KEY` — API key used for WebSocket authentication. +- `ADRIAN_LOG_FILE` — local JSONL log path. Defaults to `events.jsonl`. +- `ADRIAN_WS_URL` — Adrian WebSocket endpoint. Defaults to `ws://localhost:8080/ws`. +- `ADRIAN_SESSION_ID` — session identifier for grouping events. +- `ADRIAN_BLOCK_TIMEOUT` — seconds to wait for BLOCK/HITL verdicts. +- `ADRIAN_REPLAY_BUFFER_FRAMES` — number of WebSocket frames buffered while disconnected. + +Set `wsUrl: null` in `init()` for local JSONL logging without a WebSocket connection: + +```ts +import { init, shutdown } from "@secureagentics/adrian"; + +await init({ + wsUrl: null, + logFile: "events.jsonl", + onEvent: (eventType, data, runId, parentRunId, eventId) => { + console.log({ eventType, runId, parentRunId, eventId, data }); + }, +}); + +await shutdown(); +``` + +## Manual callback wiring + +```ts +import { init, getHandler } from "@secureagentics/adrian"; + +await init(); +const handler = 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 { init, shutdown, getHandler } from "@secureagentics/adrian"; + +await init({ wsUrl: null }); + +const handler = 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 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 { init, 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 init({ handlers: [handler] }); +``` + +## Subpath export + +`@secureagentics/adrian/capture` exposes shared LLM capture helpers used internally by provider packages. Most apps should use `adrian()` from a provider package instead. diff --git a/sdk/typescript/packages/core/package.json b/sdk/typescript/packages/core/package.json new file mode 100644 index 0000000..336e160 --- /dev/null +++ b/sdk/typescript/packages/core/package.json @@ -0,0 +1,53 @@ +{ + "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", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts src/capture/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "ai", + "agents", + "security", + "monitoring", + "observability", + "llm", + "multi-agent", + "prompt-injection" + ], + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/packages/core/src/capture/common.ts b/sdk/typescript/packages/core/src/capture/common.ts new file mode 100644 index 0000000..ebd18a2 --- /dev/null +++ b/sdk/typescript/packages/core/src/capture/common.ts @@ -0,0 +1,209 @@ +import { randomUUID } from "node:crypto"; +import { currentConfig } from "../config.js"; +import type { AdrianCallbackHandler } from "../handler.js"; +import { runWithInvocationId } from "../context.js"; +import { assertToolCallsAllowed } from "../policy.js"; +import { getWebSocketClient } from "../registry.js"; +import type { CallbackMetadata, ChatMessage, LlmEndData, TokenUsage, ToolArgs, ToolCallRecord } from "../types.js"; + +/** Gate tool calls after the paired LLM event has been emitted (maps tool-call ids on the WS client). */ +export async function gateLlmEndData(end: LlmEndData): Promise { + await assertToolCallsAllowed( + end.toolCalls.map((call) => call.id), + getWebSocketClient(), + currentConfig()?.blockTimeout ?? 30, + ); +} + +export interface LlmCaptureInput { + model: string; + messages: ChatMessage[]; + metadata?: CallbackMetadata | null; + parentRunId?: string; +} + +export async function captureLlmCall( + getHandler: () => AdrianCallbackHandler | null, + input: LlmCaptureInput, + execute: () => Promise, + extractOutput: (result: T) => LlmEndData | Promise, + afterPairedEmit?: (end: LlmEndData) => void | Promise, +): Promise { + const handler = getHandler(); + if (!handler) return execute(); + + const runId = randomUUID(); + return runWithInvocationId(randomUUID(), async () => { + await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata }); + try { + const result = await execute(); + const endData = await extractOutput(result); + await handler.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + return result; + } catch (error) { + await handler.handleLLMError(error, runId); + throw error; + } + }); +} + +export function captureLlmAsyncIterable( + getHandler: () => AdrianCallbackHandler | null, + input: LlmCaptureInput, + iterable: AsyncIterable, + aggregate: (chunk: T) => void, + extractOutput: () => LlmEndData | Promise, + afterPairedEmit?: (end: LlmEndData) => void | Promise, +): AsyncIterable { + const handler = getHandler(); + if (!handler) return iterable; + + const runId = randomUUID(); + const invocationId = randomUUID(); + + async function* wrapped(): AsyncGenerator { + await handler?.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata }); + yield* runWithInvocationId(invocationId, async function* () { + let emitted = false; + let failed = false; + try { + for await (const chunk of iterable) { + aggregate(chunk); + yield chunk; + } + emitted = true; + const endData = await extractOutput(); + await handler?.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + } catch (error) { + failed = true; + await handler?.handleLLMError(error, runId); + throw error; + } finally { + if (!emitted && !failed) { + const endData = await extractOutput(); + await handler?.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + } + } + }); + } + + return wrapped(); +} + +export function normalizeMessages(input: unknown): ChatMessage[] { + if (typeof input === "string") return [{ role: "user", content: input }]; + if (!Array.isArray(input)) return []; + return input.map((message) => { + const obj = message && typeof message === "object" ? message as Record : {}; + return { + role: String(obj.role), + content: stringifyContent(obj.content ?? obj.text), + }; + }); +} + +export function messagesFromPromptLike(args: Record): ChatMessage[] { + const system = args.instructions; + const messages = normalizeMessages(args.messages); + if (messages.length > 0) return prependSystem(system, messages); + if (typeof args.input === "string") return prependSystem(system, [{ role: "user", content: args.input }]); + const inputMessages = normalizeResponseInput(args.input); + if (inputMessages.length > 0) return prependSystem(system, inputMessages); + return prependSystem(system, []); +} + +/** Normalise OpenAI Responses API `input` arrays (roles, tool calls, tool outputs). */ +export function normalizeResponseInput(input: unknown): ChatMessage[] { + if (!Array.isArray(input)) return []; + const messages: ChatMessage[] = []; + for (const item of input) { + if (!item || typeof item !== "object") continue; + const obj = item as Record; + const type = String(obj.type); + + if (type === "function_call" || type === "tool_call") { + const name = String(obj.name); + const args = typeof obj.arguments === "string" ? obj.arguments : stringifyJson(obj.arguments); + messages.push({ role: "assistant", content: `[tool_call:${name}] ${args}` }); + continue; + } + if (type === "function_call_output") { + messages.push({ role: "tool", content: String(obj.output) }); + continue; + } + + const role = String(obj.role); + if (!role) continue; + messages.push({ + role: role === "developer" ? "system" : role, + content: stringifyContent(obj.content ?? obj.text), + }); + } + return messages; +} + +export function stringifyContent(value: unknown): string { + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value.map((part) => { + if (typeof part === "string") return part; + if (part && typeof part === "object") { + const obj = part as Record; + if (typeof obj.text === "string") return obj.text; + if (typeof obj.content === "string") return obj.content; + } + return stringifyJson(part); + }).join(""); + } + return stringifyJson(value); +} + +export function normalizeUsage(usage: unknown, promptKeys = ["promptTokens", "prompt_tokens", "input_tokens"], completionKeys = ["completionTokens", "completion_tokens", "output_tokens"]): TokenUsage | null { + if (!usage || typeof usage !== "object") return null; + const obj = usage as Record; + const promptTokens = numberFromKeys(obj, promptKeys); + const completionTokens = numberFromKeys(obj, completionKeys); + const totalTokens = numberFromKeys(obj, ["totalTokens", "total_tokens"]) ?? ((promptTokens ?? 0) + (completionTokens ?? 0)); + if (promptTokens === null && completionTokens === null && totalTokens === 0) return null; + return { promptTokens: promptTokens ?? 0, completionTokens: completionTokens ?? 0, totalTokens }; +} + +export function parseToolArgs(value: unknown): ToolArgs { + if (!value) return {}; + if (typeof value === "object" && !Array.isArray(value)) return value as ToolArgs; + if (typeof value !== "string") return {}; + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as ToolArgs : {}; + } catch { + return {}; + } +} + +export function emptyLlmEnd(output = "", toolCalls: ToolCallRecord[] = [], usage: TokenUsage | null = null): LlmEndData { + return { output, toolCalls, usage }; +} + +function prependSystem(system: unknown, messages: ChatMessage[]): ChatMessage[] { + return typeof system === "string" && system.length > 0 ? [{ role: "system", content: system }, ...messages] : messages; +} + +function numberFromKeys(obj: Record, keys: string[]): number | null { + for (const key of keys) { + const value = obj[key]; + if (typeof value === "number" && Number.isFinite(value)) return value; + } + return null; +} + +function stringifyJson(value: unknown): string { + if (value === null || value === undefined) return ""; + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} diff --git a/sdk/typescript/packages/core/src/capture/index.ts b/sdk/typescript/packages/core/src/capture/index.ts new file mode 100644 index 0000000..e9db7b2 --- /dev/null +++ b/sdk/typescript/packages/core/src/capture/index.ts @@ -0,0 +1 @@ +export * from "./common.js"; diff --git a/sdk/typescript/packages/core/src/config.ts b/sdk/typescript/packages/core/src/config.ts new file mode 100644 index 0000000..129f7e7 --- /dev/null +++ b/sdk/typescript/packages/core/src/config.ts @@ -0,0 +1,72 @@ +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; +} 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..a422643 --- /dev/null +++ b/sdk/typescript/packages/core/src/index.ts @@ -0,0 +1,107 @@ +import { 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 apiKey = options.apiKey ?? process.env.ADRIAN_API_KEY ?? null; + const logFile = process.env.ADRIAN_LOG_FILE ?? options.logFile ?? "events.jsonl"; + const wsUrl = process.env.ADRIAN_WS_URL ?? options.wsUrl ?? "ws://localhost:8080/ws"; + const sessionId = await envAwareResolveSessionId(options.sessionId ?? null); + const blockTimeout = Number(process.env.ADRIAN_BLOCK_TIMEOUT ?? options.blockTimeout ?? 30); + const replayBufferFrames = parseInt(process.env.ADRIAN_REPLAY_BUFFER_FRAMES ?? String(options.replayBufferFrames ?? 1000), 10); + + const config: AdrianConfig = { + apiKey, + logFile, + logLevel: options.logLevel ?? null, + sessionId, + wsUrl, + 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: Number.isFinite(replayBufferFrames) ? replayBufferFrames : 1000, + }; + setConfig(config); + + const handlerList: EventHandler[] = options.handlers ? [...options.handlers] : [new JSONLHandler(logFile)]; + let wsClient: WebSocketClient | null = null; + if (!options.handlers && wsUrl) { + if (!apiKey) console.warn("ADRIAN wsUrl is set but no apiKey was provided; the server will reject the connection."); + wsClient = new WebSocketClient({ + url: wsUrl, + sessionId, + apiKey: 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 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..837f929 --- /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 = "missing_tool_call_id" | "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" }; + + if (toolCallIds.some((id) => !id)) return { action: "block", reason: "missing_tool_call_id" }; + + const verdictTimeout = ws.blockTimeout(timeoutSeconds); + const verdicts = await Promise.all(toolCallIds.map((id) => ws.waitForToolCallVerdict(id, verdictTimeout))); + if (verdicts.some((verdict) => !verdict)) return { action: "block", reason: "verdict_timeout" }; + 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..ec5e05c --- /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 (process.env.ADRIAN_SESSION_ID) return process.env.ADRIAN_SESSION_ID; + if (explicit) return explicit; + 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..3decbeb --- /dev/null +++ b/sdk/typescript/packages/core/src/ws.ts @@ -0,0 +1,298 @@ +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 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) { + try { + await this.connectOnce(); + backoff = INITIAL_BACKOFF_MS; + await this.waitForClose(); + } catch { + // Connection errors are retried with backoff. + } + if (this.closing) return; + const delay = this.wsCloseCode() === QUOTA_EXHAUSTED_CLOSE_CODE ? QUOTA_RECONNECT_DELAY_MS : backoff; + await sleep(delay); + backoff = Math.min(backoff * 2, MAX_BACKOFF_MS); + } + } + + 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", () => { + this.loggedIn = false; + this.replaying = false; + this.policy = null; + this.mode = Mode.MODE_UNSPECIFIED; + this.resolveLoginAckWaiters(false); + if (!this.closing) void this.onDisconnect?.("recv_loop_exit"); + }); + }); + } + + 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 wsCloseCode(): number | null { + return null; + } + + 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); + if (prefix === "M0") return verdict.policy.policyM0; + if (prefix === "M2") return verdict.policy.policyM2; + if (prefix === "M3") return verdict.policy.policyM3; + if (prefix === "M4") return verdict.policy.policyM4; + 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/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..0895c9b --- /dev/null +++ b/sdk/typescript/packages/core/tests/policy.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { setConfig, Mode, type Verdict, type WebSocketClient } from "../src/index.js"; +import { + AdrianPolicyBlockedError, + 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("blocks on missing tool call id", 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: "block", reason: "missing_tool_call_id" }); + }); + + 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("assertToolCallsAllowed throws AdrianPolicyBlockedError", 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)).rejects.toBeInstanceOf(AdrianPolicyBlockedError); + }); +}); 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..fd87b63 --- /dev/null +++ b/sdk/typescript/packages/core/tests/ws.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import type { PairedEvent } from "../src/format/types.js"; +import { Mode, type Verdict } from "../src/proto/schema.js"; +import { WebSocketClient } from "../src/ws.js"; + +function client(): WebSocketClient { + return new WebSocketClient({ url: "ws://localhost:0", sessionId: "sess", apiKey: "key", replayBufferFrames: 10 }); +} + +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, + }; +} + +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); + }); +}); 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/packages/openai/README.md b/sdk/typescript/packages/openai/README.md new file mode 100644 index 0000000..bb7d1fb --- /dev/null +++ b/sdk/typescript/packages/openai/README.md @@ -0,0 +1,138 @@ +# @secureagentics/adrian-openai + +OpenAI SDK instrumentation for Adrian security monitoring. Includes `@secureagentics/adrian` as a dependency — no separate core install needed. + +## Install + +```bash +npm install @secureagentics/adrian-openai openai +``` + +```ts +import { init, adrian, captureTool } from "@secureagentics/adrian-openai"; +``` + +## Usage + +```ts +import OpenAI from "openai"; +import { init, shutdown, adrian, captureTool } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const openai = adrian(new OpenAI()); + +await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Hello" }], +}); + +await shutdown(); +``` + +`adrian()` wraps `chat.completions.create`, `responses.create`, and their streaming variants. Events are paired, PII-redacted, and streamed to Adrian automatically. + +When the dashboard policy is in **BLOCK** or **HITL** mode and `init()` is connected over WebSocket, the SDK waits for verdicts on LLM turns that propose tool calls and throws `AdrianPolicyBlockedError` before those tools can run. `captureTool` applies the same gate before executing your handler. + +Pass metadata when wrapping the client to attach app-specific context to each captured event: + +```ts +const openai = adrian(new OpenAI(), { + metadata: { environment: "production", service: "checkout-agent" }, +}); +``` + +## Responses API + +```ts +const response = await openai.responses.create({ + model: "gpt-4o-mini", + input: "Summarize the security considerations for this workflow.", +}); + +console.log(response.output_text); +``` + +## Streaming + +Streaming chunks are passed through unchanged. Adrian emits one paired LLM event after the stream completes, or when the consumer exits early: + +```ts +const stream = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Write a short haiku." }], + stream: true, +}); + +for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); +} +``` + +The Responses API streaming path is captured the same way: + +```ts +const stream = await openai.responses.create({ + model: "gpt-4o-mini", + input: "Write a one-sentence project update.", + stream: true, +}); + +for await (const event of stream) { + if (event.type === "response.output_text.delta") process.stdout.write(event.delta); +} +``` + +## Tool execution capture + +OpenAI returns tool call requests; your app executes the tools. Wrap that execution with `captureTool`: + +```ts +import { AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE } from "@secureagentics/adrian-openai"; + +async function runTool(name: string, argsJson: string) { + const args = JSON.parse(argsJson || "{}") as Record; + if (name === "get_weather") return { city: args.city, forecast: "sunny" }; + throw new Error(`Unknown tool: ${name}`); +} + +for (const toolCall of assistantMessage.tool_calls ?? []) { + let toolResult: unknown; + + try { + toolResult = await captureTool(toolCall, () => + runTool(toolCall.function.name, toolCall.function.arguments), + ); + } catch (error) { + if (!(error instanceof AdrianPolicyBlockedError)) throw error; + toolResult = BLOCKED_TOOL_MESSAGE; + } + + messages.push({ + role: "tool", + tool_call_id: toolCall.id, + content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult), + }); +} +``` + +## API + +| Export | Description | +|---|---| +| `init(options?)` | Initialise Adrian (re-exported from core) | +| `shutdown()` | Tear down Adrian (re-exported from core) | +| `adrian(client, options?)` | Wrap an OpenAI client | +| `captureTool(toolCall, execute, options?)` | Capture manual tool execution | +| `AdrianPolicyBlockedError` | Error thrown when policy blocks a tool call | +| `BLOCKED_TOOL_MESSAGE` | Standard blocked tool result string | + +### Types + +| Type | Fields | +|---|---| +| `AdrianOptions` | `metadata?` | +| `ToolCallLike` | `id`, `function.name`, `function.arguments`, … | +| `ToolCaptureOptions` | `metadata?`, `parentRunId?` | + +See the [workspace README](../../README.md) for environment variables and multi-provider setup. diff --git a/sdk/typescript/packages/openai/package.json b/sdk/typescript/packages/openai/package.json new file mode 100644 index 0000000..40cd21c --- /dev/null +++ b/sdk/typescript/packages/openai/package.json @@ -0,0 +1,52 @@ +{ + "name": "@secureagentics/adrian-openai", + "version": "1.0.0", + "description": "OpenAI SDK instrumentation for Adrian security monitoring.", + "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" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "openai", + "ai", + "agents", + "security", + "monitoring" + ], + "dependencies": { + "@secureagentics/adrian": "^1.0.0" + }, + "peerDependencies": { + "openai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/packages/openai/src/index.ts b/sdk/typescript/packages/openai/src/index.ts new file mode 100644 index 0000000..68d3672 --- /dev/null +++ b/sdk/typescript/packages/openai/src/index.ts @@ -0,0 +1,246 @@ +import { randomUUID } from "node:crypto"; +import { assertToolCallsAllowed, currentConfig, getHandler, getWebSocketClient, runWithInvocationId } from "@secureagentics/adrian"; +import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "@secureagentics/adrian"; +import { + captureLlmAsyncIterable, + captureLlmCall, + gateLlmEndData, + emptyLlmEnd, + messagesFromPromptLike, + normalizeMessages, + normalizeUsage, + parseToolArgs, + stringifyContent, +} from "@secureagentics/adrian/capture"; + +export interface AdrianOptions { + metadata?: CallbackMetadata | null; +} + +export interface ToolCallLike { + id: string; + type?: string; + function?: { + name?: string; + arguments?: string; + }; + name?: string; + arguments?: string; +} + +export interface ToolCaptureOptions { + metadata?: CallbackMetadata | null; + parentRunId?: string; +} + +/** Wrap an OpenAI client so Adrian captures LLM and tool events. */ +export function adrian(client: T, options: AdrianOptions = {}): T { + return new Proxy(client, { + get(target, prop, receiver) { + if (prop === "chat") return instrumentChat(Reflect.get(target, prop, receiver), options); + if (prop === "responses") return instrumentResponses(Reflect.get(target, prop, receiver), options); + return Reflect.get(target, prop, receiver); + }, + }); +} + +/** Wrap manual OpenAI tool execution so Adrian captures tool events. */ +export async function captureTool( + toolCall: ToolCallLike, + execute: () => T | Promise, + options: ToolCaptureOptions = {}, +): Promise { + const handler = getHandler(); + if (!handler) return execute(); + + const runId = randomUUID(); + const toolName = String(toolCall.function?.name ?? toolCall.name ?? "unknown"); + const toolCallId = String(toolCall.id ?? ""); + const input = String(toolCall.function?.arguments ?? toolCall.arguments ?? ""); + const metadata = integrationMetadata(options.metadata, "openai.tool_call"); + + await assertToolCallsAllowed(toolCallId ? [toolCallId] : [], getWebSocketClient(), currentConfig()?.blockTimeout ?? 30); + + return runWithInvocationId(randomUUID(), async () => { + await handler.handleToolStart({ name: toolName }, input, runId, options.parentRunId, { metadata, tool_call_id: toolCallId }); + try { + const result = await execute(); + await handler.handleToolEnd(result, runId); + return result; + } catch (error) { + await handler.handleToolError(error, runId); + throw error; + } + }); +} + +function instrumentChat(chat: unknown, options: AdrianOptions): unknown { + if (!chat || typeof chat !== "object") return chat; + return new Proxy(chat as Record, { + get(target, prop, receiver) { + if (prop === "completions") return instrumentChatCompletions(Reflect.get(target, prop, receiver), options); + return Reflect.get(target, prop, receiver); + }, + }); +} + +function instrumentChatCompletions(completions: unknown, options: AdrianOptions): unknown { + if (!completions || typeof completions !== "object") return completions; + return new Proxy(completions as Record, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop !== "create" || typeof value !== "function") return value; + return async function adrianOpenAIChatCreate(this: unknown, body: Record = {}, ...rest: unknown[]) { + const model = String(body.model ?? "openai"); + const metadata = integrationMetadata(options.metadata, "openai.chat.completions"); + const messages = normalizeMessages(body.messages); + if (body.stream === true) { + const result = await Promise.resolve(value.call(target, body, ...rest)); + if (isAsyncIterable(result)) return captureChatCompletionStream(model, messages, metadata, result); + return result; + } + return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractChatCompletion, gateLlmEndData); + }; + }, + }); +} + +function instrumentResponses(responses: unknown, options: AdrianOptions): unknown { + if (!responses || typeof responses !== "object") return responses; + return new Proxy(responses as Record, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop !== "create" || typeof value !== "function") return value; + return async function adrianOpenAIResponsesCreate(this: unknown, body: Record = {}, ...rest: unknown[]) { + const model = String(body.model ?? "openai"); + const metadata = integrationMetadata(options.metadata, "openai.responses"); + const messages = messagesFromPromptLike({ + input: body.input, + instructions: body.instructions, + }); + if (body.stream === true) { + const result = await Promise.resolve(value.call(target, body, ...rest)); + if (isAsyncIterable(result)) return captureResponseStream(model, messages, metadata, result); + return result; + } + return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractResponse, gateLlmEndData); + }; + }, + }); +} + +function captureChatCompletionStream(model: string, messages: ReturnType, metadata: CallbackMetadata | null, stream: AsyncIterable): AsyncIterable { + let output = ""; + let usage: LlmEndData["usage"] = null; + const toolCallParts = new Map(); + return captureLlmAsyncIterable(getHandler, { model, messages, metadata }, stream, (chunk) => { + const obj = chunk && typeof chunk === "object" ? chunk as Record : {}; + usage = normalizeUsage(obj.usage) ?? usage; + const choices = Array.isArray(obj.choices) ? obj.choices : []; + for (const choice of choices) { + const delta = (choice as Record).delta as Record | undefined; + output += stringifyContent(delta?.content); + const calls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : []; + for (const rawCall of calls) { + const call = rawCall as Record; + const index = typeof call.index === "number" ? call.index : toolCallParts.size; + const fn = call.function as Record | undefined; + const current = toolCallParts.get(index) ?? { id: "", name: "", args: "" }; + toolCallParts.set(index, { + id: typeof call.id === "string" ? call.id : current.id, + name: typeof fn?.name === "string" ? fn.name : current.name, + args: current.args + (typeof fn?.arguments === "string" ? fn.arguments : ""), + }); + } + } + }, () => emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage), gateLlmEndData); +} + +function captureResponseStream(model: string, messages: ReturnType, metadata: CallbackMetadata | null, stream: AsyncIterable): AsyncIterable { + let output = ""; + let usage: LlmEndData["usage"] = null; + const toolCallParts = new Map(); + return captureLlmAsyncIterable(getHandler, { model, messages, metadata }, stream, (chunk) => { + const obj = chunk && typeof chunk === "object" ? chunk as Record : {}; + usage = normalizeUsage(obj.usage) ?? usage; + if (obj.type === "response.output_text.delta" && typeof obj.delta === "string") output += obj.delta; + if (typeof obj.output_text === "string") output += obj.output_text; + collectResponseStreamToolCall(obj, toolCallParts); + }, () => emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage), gateLlmEndData); +} + +function extractChatCompletion(result: unknown): LlmEndData { + if (isAsyncIterable(result)) return emptyLlmEnd(); + const obj = result && typeof result === "object" ? result as Record : {}; + const choices = Array.isArray(obj.choices) ? obj.choices : []; + const first = choices[0] && typeof choices[0] === "object" ? choices[0] as Record : {}; + const message = first.message && typeof first.message === "object" ? first.message as Record : {}; + const toolCalls = normalizeOpenAIToolCalls(message.tool_calls); + return emptyLlmEnd(stringifyContent(message.content), toolCalls, normalizeUsage(obj.usage)); +} + +function extractResponse(result: unknown): LlmEndData { + if (isAsyncIterable(result)) return emptyLlmEnd(); + const obj = result && typeof result === "object" ? result as Record : {}; + return emptyLlmEnd(typeof obj.output_text === "string" ? obj.output_text : stringifyContent(obj.output), normalizeResponseToolCalls(obj.output), normalizeUsage(obj.usage)); +} + +function normalizeOpenAIToolCalls(raw: unknown): ToolCallRecord[] { + if (!Array.isArray(raw)) return []; + return raw.map((call) => { + const obj = call && typeof call === "object" ? call as Record : {}; + const fn = obj.function && typeof obj.function === "object" ? obj.function as Record : {}; + return { id: String(obj.id ?? ""), name: String(fn.name ?? obj.name ?? ""), args: parseToolArgs(fn.arguments ?? obj.arguments ?? obj.args) }; + }); +} + +function normalizeResponseToolCalls(raw: unknown): ToolCallRecord[] { + if (!Array.isArray(raw)) return []; + return raw.flatMap((item) => { + const obj = item && typeof item === "object" ? item as Record : {}; + if (obj.type === "message" && Array.isArray(obj.content)) { + return obj.content.flatMap((part) => normalizeResponseToolCalls([part])); + } + if (obj.type !== "function_call" && obj.type !== "tool_call") return []; + return [{ id: String(obj.call_id ?? obj.id ?? ""), name: String(obj.name ?? ""), args: parseToolArgs(obj.arguments) }]; + }); +} + +function collectResponseStreamToolCall(obj: Record, toolCallParts: Map): void { + const item = obj.item && typeof obj.item === "object" ? obj.item as Record : null; + if (item && (item.type === "function_call" || item.type === "tool_call")) { + const key = String(item.id ?? item.call_id ?? obj.output_index ?? toolCallParts.size); + const current = toolCallParts.get(key) ?? { id: "", name: "", args: "" }; + toolCallParts.set(key, { + id: String(item.call_id ?? item.id ?? current.id), + name: String(item.name ?? current.name), + args: typeof item.arguments === "string" ? item.arguments : current.args, + }); + } + + if (obj.type !== "response.function_call_arguments.delta" || typeof obj.delta !== "string") return; + const key = String(obj.item_id ?? obj.output_index ?? toolCallParts.size); + const current = toolCallParts.get(key) ?? { id: "", name: "", args: "" }; + toolCallParts.set(key, { ...current, args: current.args + obj.delta }); +} + +function integrationMetadata(metadata: CallbackMetadata | null | undefined, operation: string): CallbackMetadata { + return { ...(metadata ?? {}), adrianIntegration: "openai", operation }; +} + +function isAsyncIterable(value: unknown): value is AsyncIterable { + return Boolean(value && typeof value === "object" && Symbol.asyncIterator in value); +} + +export { + AdrianPolicyBlockedError, + BLOCKED_TOOL_MESSAGE, + init, + shutdown, + getHandler, + getWebSocketClient, + version, + __version__, +} from "@secureagentics/adrian"; + +export type { EventData, InitOptions } from "@secureagentics/adrian"; diff --git a/sdk/typescript/packages/openai/tests/openai.test.ts b/sdk/typescript/packages/openai/tests/openai.test.ts new file mode 100644 index 0000000..810893d --- /dev/null +++ b/sdk/typescript/packages/openai/tests/openai.test.ts @@ -0,0 +1,330 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as adrianCore from "@secureagentics/adrian"; +import { AdrianPolicyBlockedError, init, Mode, shutdown, type EventData, type Verdict, type WebSocketClient } from "@secureagentics/adrian"; +import { captureTool, adrian } from "../src/index.js"; + +function mockWs(halt: boolean): WebSocketClient { + return { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => ({ + eventId: `event-${toolCallId}`, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, + hitl: null, + } satisfies Verdict), + } as unknown as WebSocketClient; +} + +describe("OpenAI instrumentation", () => { + afterEach(async () => { + vi.restoreAllMocks(); + await shutdown(); + }); + + it("captures chat completion calls as paired LLM events", async () => { + const events: EventData[] = []; + const client = adrian({ + chat: { + completions: { + create: async (_body: Record) => ({ + choices: [{ + message: { + content: "hello", + tool_calls: [{ + id: "call-1", + function: { name: "search", arguments: "{\"query\":\"docs\"}" }, + }], + }, + }], + usage: { prompt_tokens: 3, completion_tokens: 4, total_tokens: 7 }, + }), + }, + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await client.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "system", content: "be brief" }, { role: "user", content: "hi" }], + }); + + expect(result.choices[0]?.message.content).toBe("hello"); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + output: "hello", + usage: { promptTokens: 3, completionTokens: 4, totalTokens: 7 }, + }); + expect("toolCalls" in events[0] && events[0].toolCalls[0]).toMatchObject({ id: "call-1", name: "search", args: { query: "docs" } }); + }); + + it("captures responses API calls", async () => { + const events: EventData[] = []; + const client = adrian({ + responses: { + create: async (_body: Record) => ({ + output_text: "done", + output: [{ type: "function_call", call_id: "call-2", name: "lookup", arguments: "{\"id\":42}" }], + usage: { input_tokens: 5, output_tokens: 6, total_tokens: 11 }, + }), + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + await client.responses.create({ model: "gpt-4.1", input: "run lookup" }); + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4.1", + output: "done", + toolCalls: [{ id: "call-2", name: "lookup", args: { id: 42 } }], + }); + }); + + it("captures responses API streaming tool calls and text", async () => { + const events: EventData[] = []; + async function* stream() { + yield { type: "response.output_text.delta", delta: "The answer " }; + yield { type: "response.output_item.added", item: { id: "item-1", type: "function_call", call_id: "call-4", name: "lookup" } }; + yield { type: "response.function_call_arguments.delta", item_id: "item-1", delta: "{\"id\"" }; + yield { type: "response.function_call_arguments.delta", item_id: "item-1", delta: ":7}" }; + yield { type: "response.output_text.delta", delta: "is ready." }; + } + const client = adrian({ + responses: { + create: async (_body: Record) => stream(), + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await client.responses.create({ model: "gpt-4.1", input: "lookup id 7", stream: true }); + for await (const _chunk of result) { + // consume the stream so Adrian can emit the paired event + } + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4.1", + output: "The answer is ready.", + toolCalls: [{ id: "call-4", name: "lookup", args: { id: 7 } }], + }); + }); + + it("captures Responses API instructions and array input for stream and non-stream", async () => { + const responseBody = { + instructions: "You are an autonomous assistant.", + input: [ + { role: "user", content: "Do the work." }, + { type: "function_call", call_id: "call-1", name: "get_weather", arguments: '{"city":"SF"}' }, + { type: "function_call_output", call_id: "call-1", output: '{"temp":58}' }, + ], + }; + + for (const stream of [false, true]) { + const events: EventData[] = []; + async function* responseStream() { + yield { type: "response.output_text.delta", delta: "Done." }; + } + const client = adrian({ + responses: { + create: async (_body: Record) => ( + stream ? responseStream() : { output_text: "Done.", usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 } } + ), + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + + const result = await client.responses.create({ + model: "gpt-4o-mini", + ...responseBody, + stream, + }); + + if (stream) { + for await (const _chunk of result as AsyncIterable) { + // consume stream + } + } + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + messages: [ + { role: "system", content: "You are an autonomous assistant." }, + { role: "user", content: "Do the work." }, + { role: "assistant", content: '[tool_call:get_weather] {"city":"SF"}' }, + { role: "tool", content: '{"temp":58}' }, + ], + }); + + await shutdown(); + } + }); + + it("emits partial stream data when the consumer stops early", async () => { + const events: EventData[] = []; + async function* stream() { + yield { choices: [{ delta: { content: "first " } }] }; + yield { choices: [{ delta: { content: "second" } }] }; + } + const client = adrian({ + chat: { + completions: { + create: async (_body: Record) => stream(), + }, + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "stream" }], stream: true }); + for await (const _chunk of result) { + break; + } + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + output: "first ", + }); + }); + + it("blocks captureTool when policy halts", async () => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, blockTimeout: 5 }); + vi.spyOn(adrianCore, "getWebSocketClient").mockReturnValue(mockWs(true)); + + let executed = false; + await expect(captureTool({ + id: "call-weather", + function: { name: "get_weather", arguments: "{}" }, + }, async () => { + executed = true; + return { ok: true }; + })).rejects.toBeInstanceOf(AdrianPolicyBlockedError); + + expect(executed).toBe(false); + }); + + it("captures local OpenAI tool execution as a tool event", async () => { + const events: Array<{ type: string; data: EventData }> = []; + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + const result = await captureTool({ + id: "call-weather", + type: "function", + function: { name: "get_weather", arguments: "{\"city\":\"San Francisco\"}" }, + }, async () => ({ temperatureF: 58, condition: "cloudy" })); + + expect(result).toEqual({ temperatureF: 58, condition: "cloudy" }); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "get_weather", + toolCallId: "call-weather", + input: "{\"city\":\"San Francisco\"}", + output: "{\"temperatureF\":58,\"condition\":\"cloudy\"}", + }, + }); + }); + + it("captures local OpenAI tool execution errors as tool events", async () => { + const events: Array<{ type: string; data: EventData }> = []; + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + await expect(captureTool({ + id: "call-weather", + type: "function", + function: { name: "get_weather", arguments: "{\"city\":\"San Francisco\"}" }, + }, async () => { + throw new Error("weather API unavailable"); + })).rejects.toThrow("weather API unavailable"); + + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "get_weather", + toolCallId: "call-weather", + output: "[ERROR] Error: weather API unavailable", + error: { name: "Error", message: "weather API unavailable" }, + }, + }); + }); + + it("captures OpenAI request errors as LLM events", async () => { + const events: Array<{ type: string; data: EventData }> = []; + const client = adrian({ + chat: { + completions: { + create: async (_body: Record) => { + throw new Error("rate limited"); + }, + }, + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + await expect(client.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hi" }], + })).rejects.toThrow("rate limited"); + + expect(events[0]).toMatchObject({ + type: "llm", + data: { + kind: "llm", + model: "gpt-4o-mini", + output: "[ERROR] Error: rate limited", + error: { name: "Error", message: "rate limited" }, + }, + }); + }); + + it("wraps OpenAI client via adrian()", async () => { + const events: EventData[] = []; + const client = adrian({ + chat: { + completions: { + create: async (_body: Record) => ({ + choices: [{ message: { content: "hello" } }], + }), + }, + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + + await client.chat.completions.create({ + model: "gpt-4o", + messages: [{ role: "user", content: "hi" }], + }); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ model: "gpt-4o", output: "hello" }); + }); +}); diff --git a/sdk/typescript/packages/openai/tsconfig.build.json b/sdk/typescript/packages/openai/tsconfig.build.json new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/sdk/typescript/packages/openai/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/openai/tsconfig.json b/sdk/typescript/packages/openai/tsconfig.json new file mode 100644 index 0000000..6140ae3 --- /dev/null +++ b/sdk/typescript/packages/openai/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"] } }