From bfcc12b1b11184f7d50b7c4d422ee4b3b163ef71 Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Wed, 20 May 2026 12:54:00 +0500 Subject: [PATCH 1/9] docs: add GET_STARTED_AI_GUIDE.md for AI coding assistants A self-contained briefing intended to be dropped into a coding agent's context (Claude, Cursor, Copilot Chat, etc.) so the agent can install, configure, and troubleshoot Adrian end-to-end without further reading. Covers: managed cloud vs self-hosted bring-up, the minimal asyncio snippet, every adrian.init() argument worth knowing, the three execution modes (Alert / Human Review / Block) mapped to the MAD taxonomy (M0-M4), webhook + MCP integrations, dashboard navigation, manual instrumentation, fork-safety and event-loop pitfalls, full env var table, the eleven most common failure modes with fixes, the API key lifecycle, a copy-pasteable smoke test, and a compact system-prompt cheat sheet. --- GET_STARTED_AI_GUIDE.md | 390 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 GET_STARTED_AI_GUIDE.md diff --git a/GET_STARTED_AI_GUIDE.md b/GET_STARTED_AI_GUIDE.md new file mode 100644 index 0000000..be01782 --- /dev/null +++ b/GET_STARTED_AI_GUIDE.md @@ -0,0 +1,390 @@ +# Adrian — AI Agent Onboarding Guide + +> **Audience: AI coding assistants (Claude, Cursor, GitHub Copilot Chat, Windsurf, Aider, ChatGPT, etc.) helping a human developer integrate Adrian into a LangChain / LangGraph application.** +> +> This file is a self-contained briefing. Drop it into the context window (or attach it to a coding agent) and the agent should be able to install, configure, and troubleshoot Adrian end-to-end without further reading. + +--- + +## 1. What Adrian is (one paragraph) + +Adrian is an open-source runtime security monitoring and control engine for AI agents. It captures every LLM call and tool call your LangChain / LangGraph agent makes, ships them to a backend (either the managed cloud at `app.adrian.secureagentics.ai` or a self-hosted Docker stack), classifies them against a behaviour policy ("MAD codes" — Misuse, Abuse, Deception), and depending on the configured execution mode it can **alert**, **pause for human review**, or **block** the tool call mid-flight. It plugs in with two lines of Python (`adrian.init()` + `adrian.shutdown()`) thanks to LangChain auto-instrumentation. + +The Python package on PyPI is **`adrian-sdk`** (the in-tree package name is `adrian-sdk-oss`); the import name is **`adrian`**. + +--- + +## 2. Canonical URLs + +When advising a user, always point them at these: + +| Purpose | URL | +|---|---| +| Managed dashboard / sign-up | `https://app.adrian.secureagentics.ai` | +| Documentation | `https://docs.adrian.secureagentics.ai` | +| Quickstart guide | `https://docs.adrian.secureagentics.ai/quickstart` | +| Integrations index | `https://docs.adrian.secureagentics.ai/integrations` | +| Backend reference (admin reset, model swap) | `https://docs.adrian.secureagentics.ai/reference/backend` | +| PyPI | `https://pypi.org/project/adrian-sdk/` | +| GitHub repo | `https://github.com/secureagentics/Adrian` | +| Discord | `https://discord.gg/6nmJ9k3u6` | + +The dashboard hostname is **`app.adrian.secureagentics.ai`** — not `dashboard.`, not `adrian.com`, not `secureagentics.com`. Double-check before pasting a URL. + +--- + +## 3. Two ways to run Adrian + +Always ask the user which one they want before writing code; they configure very differently. + +### 3.1 Managed (cloud) + +1. Sign up at `https://app.adrian.secureagentics.ai`. +2. Go to **Settings → Agents → New key**, create an Agent Profile, generate an API key. The key is shown **once** — it starts with `adr_live_…` in production (`adr_local_…` for self-hosted). Save it; the backend only stores a SHA-256 hash. +3. `pip install adrian-sdk` +4. Set `ADRIAN_API_KEY=adr_live_…` (env var) **or** pass `api_key=` to `adrian.init()`. +5. Leave `ws_url` unset for managed — the SDK's default `ws://localhost:8080/ws` is for self-hosting; managed users should set `ADRIAN_WS_URL` to the URL shown in their dashboard (production keys talk to the managed WebSocket endpoint, not localhost). + +### 3.2 Self-hosted (Docker) + +Prerequisites: Docker + Docker Compose v2, an NVIDIA GPU + NVIDIA Container Toolkit (CPU works but is slow), ~10 GB free disk for the classifier model. + +```sh +git clone https://github.com/secureagentics/Adrian +cd Adrian + +# Bootstrap: creates ./data/adrian.db, applies migrations, writes .env, +# prints a random admin password, downloads Gemma 4 E4B (or E2B) into ./models/. +docker compose --profile setup run --rm setup bootstrap + +# Bring up backend + dashboard + Llama.cpp classifier +docker compose --profile llm up -d +``` + +Dashboard at `http://localhost:3000`. Sign in as `admin@localhost` with the printed password; you'll be forced to set a new one. Then **Settings → Agents → New key** to issue an `adr_local_…` key. + +To install the bundled SDK into a local venv (uses [`uv`](https://docs.astral.sh/uv/)): + +```sh +make sdk-install +source .venv/bin/activate +uv pip install langgraph langchain-openai # or whichever provider +``` + +The SDK's default `ws_url=ws://localhost:8080/ws` already points at the bootstrapped backend — for a self-hosted setup the user only needs to pass `api_key`. + +--- + +## 4. The minimal working snippet + +This is the canonical "hello world". **It must be run inside an asyncio event loop** — Adrian's WS client, pairing buffer, and LangGraph patches all assume an async context. + +```python +import asyncio +import adrian +from langchain_openai import ChatOpenAI + +async def main(): + adrian.init(api_key="adr_live_...") # or set ADRIAN_API_KEY + llm = ChatOpenAI(model="gpt-4o") + response = await llm.ainvoke("Summarise the latest IPO filings") + print(response.content) + adrian.shutdown() + +asyncio.run(main()) +``` + +Things the agent must NOT do when generating code: + +- **Do not** call `llm.invoke()` (sync) and expect block-mode gating to work. Block / Human Review gating only fires through the async path (`ainvoke`, `astream`) because the patched `ToolNode.ainvoke` is what awaits the verdict. The sync path will still capture events for logging but cannot halt a tool call. +- **Do not** call `adrian.init()` at module import time outside an event loop and then immediately spawn workers — see §10 on fork safety. +- **Do not** wrap `adrian.init()` in `asyncio.run()` separately from the agent code; both must share the same loop, otherwise the WS client schedules its connect against a now-dead loop. +- **Do not** forget `adrian.shutdown()` at clean exit. `atexit.register(shutdown)` runs it automatically, but in long-lived servers (FastAPI, Celery) wire it into the framework's shutdown hook. + +--- + +## 5. `adrian.init()` — every argument worth knowing + +```python +adrian.init( + api_key: str | None = None, # ADRIAN_API_KEY env fallback + log_file: str | Path = "events.jsonl", # ADRIAN_LOG_FILE + handlers: list[EventHandler] | None = None, + auto_instrument: bool = True, + log_level: str | None = None, # "DEBUG" turns on SDK verbose logs + ws_url: str | None = None, # ADRIAN_WS_URL, default ws://localhost:8080/ws + session_id: str | None = None, # ADRIAN_SESSION_ID, else per-cwd persistent UUID + block_timeout: float = 30.0, # ADRIAN_BLOCK_TIMEOUT + on_event=None, on_verdict=None, + on_block=None, on_audit=None, + on_disconnect=None, on_reconnect=None, + on_mcp_server=None, + replay_buffer_frames: int = 1000, # ADRIAN_REPLAY_BUFFER_FRAMES +) +``` + +Key facts: + +- `api_key` accepts `adr_live_…` (managed cloud) or `adr_local_…` (self-hosted). Test keys generated by the open-source backend always carry the `adr_local_` prefix. +- `ws_url` must be a **WebSocket** URL (`ws://` or `wss://`), not HTTPS. For managed, the dashboard tells you the exact URL. +- `session_id` is persistent **per current working directory** by default (see `session_persistence.py`). The same agent script run from the same folder twice will reuse the same session ID, which is usually what you want. +- `block_timeout` is the fail-open ceiling in `MODE_BLOCK` only. In `MODE_HITL` the SDK waits **indefinitely** for a human reviewer; bump `block_timeout` anyway for symmetry but it isn't consulted. +- `auto_instrument=True` monkey-patches `Runnable`, `CallbackManager`, `BaseChatModel`, `langgraph.pregel.Pregel`, and `langgraph.prebuilt.ToolNode` at init time. To opt out, set it `False` and attach `adrian.get_handler()` to each chain via `config={"callbacks": [handler]}`. +- PII redaction is **always on** — every handler is wrapped in `RedactingHandler`. There is no opt-out flag. + +--- + +## 6. Execution modes and the MAD taxonomy + +Three execution modes are configurable in the dashboard at **Settings → Policy** (organisation-wide) and **Settings → Agents → ** (per agent profile): + +| Mode (wire enum) | Dashboard label | What the SDK does | +|---|---|---| +| `MODE_ALERT` (1) | **Alert** | Fire-and-forget. Events captured, verdicts logged, tools run. | +| `MODE_HITL` (2) | **Human Review** | The patched `ToolNode.ainvoke` pauses on tool calls and awaits a `/reviews` resolution. Verdict's `hitl.continue_execution` decides halt vs. proceed. **Waits indefinitely.** | +| `MODE_BLOCK` (3) | **Block** | The patched `ToolNode.ainvoke` halts tools whose verdict tier is in the policy's MAD scope. Fails open on `block_timeout`. | + +A halted tool returns `ToolMessage(content="[BLOCKED by security policy]", ...)` to the graph in place of the real tool result — the tool function itself never runs. + +### MAD codes + +The classifier emits a code shaped `M{0..4}.{a..e}`: + +- **M0** — Benign. No action. +- **M2** — Likely Misuse. Default: NOTIFY. +- **M3** — High-Risk Misuse. Default: BLOCK. +- **M4** — Malicious. Default: ESCALATE. + +Each tier's per-code definitions live in `backend/internal/alerts/alerts.json`. Examples: `M3.c` is data exfiltration intent, `M3.d` is privilege escalation, `M4.d` is destructive action (e.g. `DROP TABLE`), `M4.c` is alignment circumvention. Reference these codes when surfacing verdicts to the user. + +The per-MAD bools in the policy (`policy_m0` / `policy_m2` / `policy_m3` / `policy_m4`) decide which tiers the active execution mode actually halts on. So you can run in `MODE_BLOCK` with only `policy_m4=true` and the SDK will only block M4 tool calls; M3 events still surface as alerts but tools run. + +--- + +## 7. Integrations + +### Frameworks at launch +- **LangChain / LangGraph** — first-class, auto-instrumented. + +### Frameworks on roadmap (no SDK support yet — do not write code claiming these work) +- OpenAI Agents SDK +- Anthropic Agents SDK +- CrewAI +- OpenClaw + +If the user asks for one of the roadmap frameworks, advise that today they need to bridge to LangChain (e.g. wrap the model in `ChatOpenAI` or `ChatAnthropic`) or use the manual instrumentation path (§9) and attach the handler themselves. Point at the Discord for roadmap timing. + +### Notifications (alerting integrations) + +The README lists Discord and Slack as "at launch" integrations and shows both logos. In the **in-tree code** as of this guide's verification date, the notifications package (`backend/internal/notifications/`) is Discord-first: `ValidateDiscordWebhookURL` only accepts `https://discord.com/api/webhooks/` or `https://discordapp.com/api/webhooks/` prefixes. Slack webhook delivery is on the immediate roadmap and may be available in the managed cloud ahead of the OSS repo — check the dashboard's Webhooks page (or the live `/api/webhooks` schema) for the current allow-list before promising the user a specific channel. + +On roadmap (not yet wired up at all): WhatsApp, Microsoft Teams, PagerDuty. + +#### Setting up a Discord webhook (works today) + +1. In Discord: **Server Settings → Integrations → Webhooks → New Webhook**, copy the URL. It must start with `https://discord.com/api/webhooks/` or `https://discordapp.com/api/webhooks/` — the backend validator rejects anything else. +2. In the Adrian dashboard, go to the Webhooks settings page and `POST /api/webhooks` (or use the UI) with: + +```json +{ + "webhook_url": "https://discord.com/api/webhooks/…", + "alert_type": "M3" +} +``` + +Valid `alert_type` values are exactly `"M3"`, `"M4"`, or `"all"`. M0 / M2 verdicts never fan out to webhooks regardless of filter — they're either benign or notify-tier and the dispatcher drops them before send. The dispatcher posts a Discord embed including the MAD code, classification, session ID, agent ID, and a deep link back to the dashboard event page. + +### MCP server inventory + +If the agent uses [`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters), Adrian auto-detects every registered MCP server and reports its name / transport / endpoint to the backend (visible in the dashboard at **MCP**). No extra config — it patches `MultiServerMCPClient.__init__` and the underlying `mcp.client.*_client` transports. + +--- + +## 8. Reading events in the dashboard + +Once events arrive (the WS push usually shows in under a second), the user can navigate: + +- **Events** (`/events`) — the raw paired-event feed, every `chat_model_start+llm_end` and `tool_start+tool_end` pair, with verdicts attached. +- **Sessions** (`/sessions/`) — timeline for a single session ID. +- **Agents** (`/agents` and `/agents/`) — per-agent rollups. +- **Reviews** (`/reviews`) — Human Review queue. When `MODE_HITL` is active and a tool gets flagged, the request lands here. Approve to release the tool call; reject to substitute `[BLOCKED by security policy]`. +- **MCP** (`/mcp`) — discovered MCP servers across all sessions. +- **Settings → Policy** — execution mode + per-MAD policy. +- **Settings → Agents** — agent profiles (name, remit, M0 accepted behaviours, M3 known-risks), API key issue / revoke. +- **Settings → Webhooks** — Discord / Slack alert routing. +- **Audit log** — admin activity (key rotations, policy edits). + +A locally-running events file is also written to `./events.jsonl` (override with `log_file=` or `ADRIAN_LOG_FILE`). That JSONL is one record per paired event and is what the JSONL handler writes whether or not the WS handler is also active. + +--- + +## 9. Manual instrumentation (when auto-patching is unwanted) + +Some users (security-sensitive shops, frameworks that already manage callbacks) prefer not to patch LangChain at import. Pattern: + +```python +import adrian +from langchain_openai import ChatOpenAI + +async def main(): + adrian.init(api_key="adr_live_...", auto_instrument=False) + handler = adrian.get_handler() # set during init() + if handler is None: + raise RuntimeError("Adrian handler missing — check adrian.init()") + + llm = ChatOpenAI(model="gpt-4o") + await llm.ainvoke("prompt", config={"callbacks": [handler]}) + + adrian.shutdown() +``` + +The handler still has to be attached to **every** chain / runnable / graph that should be observed. Forgetting one is silent — the chain runs unmonitored. See `examples/manual_instrumentation.py`. + +--- + +## 10. Fork safety, threading, and event loops + +This is the area where users most often break the SDK. Encode the following as constraints: + +- **Single event loop.** `adrian.init()` must be called from the same loop that drives the agent. The WS client schedules its connect against `asyncio.get_running_loop()`; if no loop is running at init time the connect is deferred until the first send. +- **Pre-fork servers** (`gunicorn --preload`, `multiprocessing.Pool`, Celery prefork): each child must call `adrian.init()` in its worker startup hook. The SDK registers an `os.register_at_fork` handler that nulls out the parent's WS / handler / hook globals in the child, so reusing the parent's connection from two processes can't corrupt frames on the wire. The child will silently have no instrumentation until it re-inits. +- **Sync code paths.** `llm.invoke()` (sync) still emits events via the patched `Runnable.invoke`, but block / HITL gating only engages on `ToolNode.ainvoke`. For a strict "block before tool runs" guarantee, the user must be on the async path. +- **Shutdown.** Long-running services (FastAPI, Streamlit) should call `adrian.shutdown()` on app shutdown. `atexit.register` handles ad-hoc scripts. + +--- + +## 11. Environment variables — full list + +These are read inside `adrian.init()`. Setting them is interchangeable with passing kwargs; kwargs win when both are present (except for `ADRIAN_API_KEY` and `ADRIAN_WS_URL` where env vars are preferred). + +| Env var | Default | Purpose | +|---|---|---| +| `ADRIAN_API_KEY` | — | `adr_live_…` / `adr_local_…` | +| `ADRIAN_WS_URL` | `ws://localhost:8080/ws` | WebSocket endpoint | +| `ADRIAN_LOG_FILE` | `events.jsonl` | JSONL output path | +| `ADRIAN_SESSION_ID` | (per-cwd UUID) | Override session identity | +| `ADRIAN_BLOCK_TIMEOUT` | `30.0` | Fail-open ceiling in `MODE_BLOCK` | +| `ADRIAN_REPLAY_BUFFER_FRAMES` | `1000` | In-memory ring buffer for WS replay | + +For self-hosted deployments (read by Docker Compose, not the SDK) the bootstrap also writes `ADRIAN_LLM_URL`, `ADRIAN_LLM_MODEL_PATH`, `ADRIAN_LLM_API_KEY`, `ADRIAN_LLM_MODEL`, `ADRIAN_LLM_CTX_SIZE`, `ADRIAN_SLIDING_WINDOW_SIZE`, `ADRIAN_SLIDING_WINDOW_TTL_SECONDS`, `ADRIAN_BACKEND_PORT`, `ADRIAN_DASHBOARD_PORT`, `ADRIAN_SESSION_SECRET` into `.env`. Touch these only if changing models or ports. + +--- + +## 12. Common failure modes and fixes + +When the user reports a problem, walk this list first. + +### "Adrian SDK has not been initialised. Call adrian.init() first." +`get_config()` was called before `init()`. Confirm `adrian.init()` actually ran (not just imported) and that it ran in the same process (not a forked child — see §10). + +### "ws_url is set but no api_key provided" warning + WS rejected +No API key. Set `ADRIAN_API_KEY` or pass `api_key=`. The server hangs up immediately if the bearer token doesn't match a row in the `api_keys` table (or matches a revoked row — see §13). + +### "ToolNode: LoginAck not received within 5s; halting" +The backend never confirmed login within 5 s of the first tool call. SDK refuses to let a tool run without a verified policy, so it returns `[BLOCKED by security policy]`. Causes: backend down, wrong `ws_url`, invalid / revoked key, network firewall. Check `curl /healthz` (HTTP, not WS). + +### `"verdict timeout for tool_call_id=… fail-open"` +The verdict didn't arrive within `block_timeout`. The tool runs anyway (fail-open is deliberate — Adrian never wedges your agent). Bump `block_timeout` or check classifier health (`docker compose logs llm` for self-hosted; managed users should check the dashboard's status page). + +### "Events visible in `events.jsonl` but not in the dashboard" +The local JSONL handler always writes; the WS handler is a separate emitter. Check that `ws_url` resolved correctly (it logs the resolved value in the `Adrian v… initialised` line) and that the API key is for the same backend. + +### "`[BLOCKED by security policy]` appearing for benign tools" +The agent profile / policy is more aggressive than intended. Check **Settings → Agents → ** for the mode (Alert / Human Review / Block), and **Settings → Policy** for which MAD tiers are armed (`policy_m2` / `policy_m3` / `policy_m4`). M3 with `policy_m3=true` in Block mode will halt high-risk verdicts — review them on the Events page. + +### "RuntimeError: There is no current event loop in thread …" +You're calling `adrian.init()` from sync code. Wrap in `asyncio.run(main())` or use the existing loop (`asyncio.get_event_loop().run_until_complete(...)`). + +### Multiple processes / Celery workers and one worker logs nothing +Each forked worker has to call `adrian.init()` in its own startup hook. The fork handler nulled out the inherited state. + +### "Adrian not capturing my LangGraph subgraph" +Confirm the subgraph is invoked via `ainvoke` / `astream` and that `auto_instrument=True` (default). If using a custom Runnable that bypasses `Runnable.invoke` (unusual), attach the handler explicitly. + +### Self-hosted: bootstrap fails or model download stalls +Run `docker compose --profile setup run --rm setup bootstrap --gguf my-model.gguf` after manually placing a Gemma 4 GGUF under `./models/`. The `--gguf` flag skips the interactive download. + +### Self-hosted: "lost admin password" +See `https://docs.adrian.secureagentics.ai/reference/backend#reset-the-admin-password`. There's a documented CLI reset that rewrites the bcrypt hash in `data/adrian.db`. + +--- + +## 13. API key lifecycle + +Keys are issued per **Agent Profile**, not per user. Each profile carries the remit / M0 / M3 entries that the classifier compares actions against. Creating a new key for a profile **revokes the previous key for that profile** server-side (the response includes a `revoked_previous` count) and the SDK is kicked off the WS if it was using one of the rotated keys. Rotate by creating a new key; revoke explicitly via the dashboard's Keys table or `DELETE /api/keys/{id}`. + +The plaintext key is returned exactly once at creation (`api_key` field in the create response). After that the backend only has the SHA-256 hash. Lost keys cannot be recovered — issue a new one and rotate clients. + +--- + +## 14. Verifying an install (smoke test) + +When the user says "it doesn't work", before debugging anything else have them run this: + +```python +import asyncio, os, adrian +from langchain_openai import ChatOpenAI + +async def smoke(): + assert os.environ.get("ADRIAN_API_KEY"), "ADRIAN_API_KEY missing" + assert os.environ.get("OPENAI_API_KEY"), "OPENAI_API_KEY missing" + adrian.init(log_level="DEBUG") + out = await ChatOpenAI(model="gpt-4o-mini").ainvoke("say ok") + print("LLM:", out.content) + adrian.shutdown() + +asyncio.run(smoke()) +``` + +Expected: +1. Log line `Adrian v1.0.0 initialised (handlers=2, ws=ws://…)`. +2. A `LoginAck` debug line showing the resolved mode + policy snapshot. +3. One event in `./events.jsonl` and one event row in the dashboard within ~2 s. + +If any of those is missing, go back to §12. + +--- + +## 15. What this guide deliberately does NOT cover + +- **Custom classifiers / training data** — Adrian uses Gemma 4 (E2B/E4B) by default for self-host; the managed cloud runs the same lineage. Swapping classifiers is a self-host backend change (`ADRIAN_LLM_*` vars + restart), not an SDK concern. +- **Source-level changes to the engine, dashboard, or backend** — see `CONTRIBUTING.md` and the per-package `Makefile`s for that. PRs use British English and no em-dashes. +- **Non-LangChain frameworks** — see §7. Today's answer is "bridge through LangChain" or "use manual instrumentation and attach the handler". +- **HTTP transport** — there isn't one. The SDK speaks the binary `ClientFrame` / `ServerFrame` protocol over WebSocket only. Any future HTTP transport will arrive as a new `EventHandler` implementation; for now WS is the only live channel. + +--- + +## 16. Quick reference card (paste into your agent's system prompt) + +``` +You are advising on Adrian (https://github.com/secureagentics/Adrian), +an OSS runtime security control plane for LangChain / LangGraph agents. + +Facts: + - Dashboard: https://app.adrian.secureagentics.ai + - Docs: https://docs.adrian.secureagentics.ai + - Package: pip install adrian-sdk (import name: adrian) + - Two lines: adrian.init(api_key="adr_live_..."); adrian.shutdown() + - Must run inside asyncio (asyncio.run(main())); block / HITL gating + only fires on ainvoke / astream, not invoke. + - Modes: Alert (no gating), Human Review (waits on /reviews), Block + (halts in-flight, fails open at block_timeout, default 30s). + - MAD codes: M0 benign, M2 misuse (notify), M3 high-risk (block), + M4 malicious (escalate). + - Webhooks: Discord + Slack at launch; alert_type ∈ {"M3","M4","all"}. + - Keys are per Agent Profile; creating a new key revokes the previous. + Plaintext returned ONCE. + - PII redaction is always on; no opt-out. + - Self-host: docker compose --profile setup run --rm setup bootstrap + then docker compose --profile llm up -d. Dashboard at :3000. + - Default ws_url = ws://localhost:8080/ws (self-host); managed users + set ADRIAN_WS_URL from their dashboard. + +Refuse to fabricate framework support that is not in §7 of the guide. +When in doubt, read backend/internal/alerts/alerts.json for the exact +MAD definitions or point the user at the Discord. +``` + +--- + +*Last verified against the in-tree code on 2026-05-20. If the SDK version (`adrian.__version__`) has moved past 1.0.0, re-check §5 and §12 against the new release notes before quoting line-numbered behaviour.* From 85192acbc9375c8f0771b4317f2c52fdc0c14084 Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Thu, 21 May 2026 13:26:30 +0500 Subject: [PATCH 2/9] Updated guide behavior to help setup Adrian in 60sec --- GET_STARTED_AI_GUIDE.md | 349 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 346 insertions(+), 3 deletions(-) diff --git a/GET_STARTED_AI_GUIDE.md b/GET_STARTED_AI_GUIDE.md index be01782..1077fe5 100644 --- a/GET_STARTED_AI_GUIDE.md +++ b/GET_STARTED_AI_GUIDE.md @@ -1,11 +1,354 @@ -# Adrian — AI Agent Onboarding Guide +# Adrian — AI Agent Setup Guide -> **Audience: AI coding assistants (Claude, Cursor, GitHub Copilot Chat, Windsurf, Aider, ChatGPT, etc.) helping a human developer integrate Adrian into a LangChain / LangGraph application.** +> **For human developers:** drop this file into your AI assistant's context (Claude, Codex, Cursor, Aider, Windsurf, Copilot Chat, ChatGPT, etc.) and ask it to set up Adrian. The agent walks you through a 60-second flow: API key → install → first event in the dashboard. Everything below **Part 1** is reference material the agent reads when something goes wrong. > -> This file is a self-contained briefing. Drop it into the context window (or attach it to a coding agent) and the agent should be able to install, configure, and troubleshoot Adrian end-to-end without further reading. +> **For AI agents:** if a user has just pointed you at this file (or at `github.com/secureagentics/Adrian`) and asked for help setting up, execute **Part 1** below in order. Don't skim. Don't suggest a different flow. The single success criterion is "first event visible in the dashboard within 60 seconds". Treat Part 2 as a lookup index for failure modes. --- +# PART 1 — 60-Second Setup Flow (executable) + +> **AI agent: this section is a program, not documentation. Run it top-to-bottom. Branch only at the marked decision points. If a step fails, jump to the matching subsection in Part 2 and resume.** + +## Step 0 — Confirm preconditions (5 seconds) + +Run these checks silently (don't ask the user to type them). Either you can shell out yourself, or you ask the user to confirm. + +| Requirement | How to check | +|---|---| +| Python ≥ 3.12 | `python3 --version` | +| pip available | `python3 -m pip --version` | +| A working directory | `pwd` (any folder; we'll write one or two files here) | +| Network access to PyPI + `app.adrian.secureagentics.ai` | implicit; flag if obviously blocked | + +If Python < 3.12, stop and ask the user to install 3.12 (or use `pyenv install 3.12`). Don't proceed — the SDK declares `requires-python = ">=3.12"` and will fail to install. + +## Step 1 — Get the user's Adrian API key (15 seconds) + +This step has three substeps. Don't skip the verification at the end — Steps 3 and 4 assume you've already confirmed a valid key is in place. + +### 1.1 Walk the user through generating the key + +Tell the user this, click-by-click. Don't paraphrase — the user is on a 60-second clock and a sentence-long "go get a key" leaves them hunting menus. + +> 1. Open **https://app.adrian.secureagentics.ai** in your browser. +> 2. Click **Sign up** (top right) and choose Google, Microsoft, or GitHub SSO. About 30 seconds. +> 3. Once you're in the dashboard, go to **Settings → Agents** (left sidebar). +> 4. Click **New key**. Give it any label (e.g. "60-second setup") and click **Generate**. +> 5. Copy the key. It starts with `adr_live_…`. The dashboard shows it **once** — if you close the modal without copying, you'll need to issue a new one. +> 6. Tell me when you have it. + +Wait for the user to indicate they have the key. Don't proceed without that confirmation. + +### 1.2 Recommend the secure path (.env file), and offer to accept a paste as a fallback + +Default flow — what you should ask for first: + +> Recommended: create a file called `.env` in your project directory with this one line, then save it: +> +> ``` +> ADRIAN_API_KEY=adr_live_… +> ``` +> +> (Replace `adr_live_…` with the key you just copied.) When you've saved the file, type **done** and I'll verify it. + +If the user follows this path → go to 1.3 ("verification of .env"). + +If the user pastes the key directly into chat instead (this happens often) → accept it gracefully. Don't refuse. But switch behaviour: + +- **Do not try to silently write it to `.env`.** Many agents can't write arbitrary files (Codex web, ChatGPT, Cursor in restricted mode), and even agents that can may not survive a context reload to remember the file path. The user's shell may also have already loaded a stale `.env`. Trying to "help" here usually creates a confusing mismatch between what's in the file and what the running shell sees. +- **Instead, plan to hardcode the key into the script you write in Step 3.** When you reach Step 3A, change `adrian.init()` to `adrian.init(api_key="adr_live_…")` with the actual key. For Step 3B, do the same patch in their file. +- **Warn the user explicitly, in the message where you confirm receipt of the key:** + > Got it. I'm going to put this key directly into the code I write so it runs straight away. Before you commit that file to git or share it with anyone, replace the hardcoded key with `os.environ["ADRIAN_API_KEY"]` and move the actual value into a `.env` file that's gitignored. I'll mark the line with a `# TODO` comment so you can find it. + +Go to 1.3 ("verification of paste"). + +**One rule that applies to both paths:** never echo the full key back in any later message. Refer to it as `adr_live_…` from here on. If you need to mention it in a log line, redact past the prefix. + +### 1.3 Verify before moving on + +This is a hard gate. Step 2 starts only after one of the following is true. + +**If the user said "done" (the .env path):** + +1. Confirm `.env` exists in the current working directory. If your client can read files, read it and check. If not, ask the user: + > Quick check — can you run `cat .env` (or `type .env` on Windows) and paste the output? I want to make sure the key landed correctly. +2. Confirm the line `ADRIAN_API_KEY=…` is present and the value: + - Starts with `adr_live_` (managed cloud) or `adr_local_` (self-hosted). + - Has no surrounding quotes — `ADRIAN_API_KEY="adr_live_xxx"` works in most shells but tripped some users; if you see quotes, ask the user to remove them. + - Has no surrounding whitespace — a trailing space is a common paste artifact and will fail the auth check silently later. +3. If the format is wrong, point at the exact issue and ask the user to fix it. Most common: the key was truncated during copy (Discord and Slack sometimes trim long strings on send-from-mobile). +4. If everything looks good, tell the user: + > Key verified in `.env`. I'll read it from the environment in the code I'm about to write. + +**If the user pasted the key (the chat-paste path):** + +1. Confirm the pasted value matches `^adr_(live|local)_[0-9a-f]+$`. If not, ask the user to re-copy from the dashboard — they probably grabbed the prefix or a nearby string by mistake. +2. Store the key in your working memory for this session only. **Do not write it to disk yet** — you'll embed it in the script in Step 3. +3. Tell the user (do not include the key value): + > Key received. Format looks valid. Moving on — I'll embed it into the script with a `# TODO: move to .env` marker. + +Once either verification passes, set an internal flag the rest of this flow checks: + +- `key_source = "env"` → in Steps 3A and 3B, leave `adrian.init()` argument-less and rely on `ADRIAN_API_KEY` from the environment. +- `key_source = "paste"` → in Steps 3A and 3B, generate `adrian.init(api_key="adr_live_…")` with the literal value and add a `# TODO: move to a .env file before committing` comment above the line. + +## Step 2 — Decision point: test agent or own agent? (5 seconds) + +Ask the user this question. Use whichever tool you have: + +- **If you have a structured question tool** (e.g. Claude's `AskUserQuestion`): use it with the two options below. +- **If you don't** (Codex, Aider, plain chat): ask in chat and wait for "a" or "b" or the option name. + +> Two ways to see Adrian in action: +> +> **(A) Test agent** — I write a tiny LangChain script, run it, and you watch the event appear in the dashboard. Fastest. Needs an `OPENAI_API_KEY` (or I can use a fake LLM if you don't have one). +> +> **(B) Your agent** — Tell me the path to your existing LangChain / LangGraph file and I add two lines so Adrian instruments it. Then run your agent as you normally would. + +Branch on the answer. + +## Step 3A — Test agent path (35 seconds) + +**Substep 3A.1 — OpenAI key check.** Ask: "Do you have an `OPENAI_API_KEY`?" Three branches: + +- **Yes, in env** → continue to 3A.2. +- **Yes, has the value** → write it into the same `.env` (`OPENAI_API_KEY=sk-…`) and continue. +- **No** → use the FakeChatModel script at the end of this section instead of the OpenAI one. Continue to 3A.2. + +**Substep 3A.2 — Create venv and install.** Run, in the user's working directory: + +```sh +python3 -m venv .venv +source .venv/bin/activate # on Windows: .venv\Scripts\activate +pip install adrian-sdk langchain-openai +``` + +If `pip install` exits non-zero, jump to Part 2 §12. + +**Substep 3A.3 — Write the smoke-test script.** Create `adrian_quickstart.py` in the working directory. **The exact code depends on the `key_source` you set in Step 1.3** — pick the matching variant. + +#### Variant A — `key_source = "env"` (user saved the key to `.env`) + +```python +"""Adrian 60-second smoke test.""" +import asyncio +import os +import sys +import adrian +from langchain_openai import ChatOpenAI + + +async def main() -> int: + if not os.environ.get("ADRIAN_API_KEY"): + sys.exit("ADRIAN_API_KEY missing. Did you source your .env? " + "Try: set -a; . ./.env; set +a") + if not os.environ.get("OPENAI_API_KEY"): + sys.exit("OPENAI_API_KEY missing. Set it in your shell or .env.") + + adrian.init() # reads ADRIAN_API_KEY from env automatically + llm = ChatOpenAI(model="gpt-4o-mini") + response = await llm.ainvoke("In one sentence, why is the sky blue?") + print("LLM said:", response.content) + adrian.shutdown() + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) +``` + +#### Variant B — `key_source = "paste"` (user pasted the key into chat) + +Substitute the literal key the user pasted in place of `adr_live_REPLACE_ME` on the marked line. **Keep the TODO comment** — the user explicitly opted into the convenience trade-off and the comment is the audit trail. + +```python +"""Adrian 60-second smoke test.""" +import asyncio +import os +import sys +import adrian +from langchain_openai import ChatOpenAI + + +async def main() -> int: + # TODO: move this key to a .env file before committing this script. + # Replace the literal with os.environ["ADRIAN_API_KEY"] and put + # ADRIAN_API_KEY=adr_live_... in .env (which should be gitignored). + ADRIAN_API_KEY = "adr_live_REPLACE_ME" + + if not os.environ.get("OPENAI_API_KEY"): + sys.exit("OPENAI_API_KEY missing. Set it in your shell or .env.") + + adrian.init(api_key=ADRIAN_API_KEY) + llm = ChatOpenAI(model="gpt-4o-mini") + response = await llm.ainvoke("In one sentence, why is the sky blue?") + print("LLM said:", response.content) + adrian.shutdown() + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) +``` + +#### FakeChatModel fallback (no OpenAI key) + +Use this *instead* of the variant above if the user has no `OPENAI_API_KEY`. Pick the env / paste flavour to match `key_source` (only the `adrian.init` line differs): + +```python +"""Adrian 60-second smoke test (no LLM provider needed).""" +import asyncio +import os +import sys +import adrian +from langchain_core.language_models.fake_chat_models import FakeListChatModel + + +async def main() -> int: + # env variant: + adrian.init() + # paste variant (replace with the literal key): + # adrian.init(api_key="adr_live_REPLACE_ME") # TODO: move to .env + + llm = FakeListChatModel(responses=["The sky is blue because of Rayleigh scattering."]) + out = await llm.ainvoke("In one sentence, why is the sky blue?") + print("Fake LLM said:", out.content) + adrian.shutdown() + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) +``` + +**Substep 3A.4 — Run it.** Run from the same shell that has the venv active. The command depends on `key_source`: + +- **`key_source = "env"`** — source `.env` first so `ADRIAN_API_KEY` (and `OPENAI_API_KEY`, if applicable) are in the process environment: + ```sh + set -a; . ./.env; set +a + python adrian_quickstart.py + ``` +- **`key_source = "paste"`** — the Adrian key is already inlined in the script. You still need `OPENAI_API_KEY` in the shell (unless you used the FakeChatModel variant): + ```sh + export OPENAI_API_KEY=sk-... # skip if using FakeChatModel + python adrian_quickstart.py + ``` + +Expected output: a log line `Adrian v1.0.0 initialised (handlers=2, ws=ws://localhost:8080/ws)` (or the managed cloud's WS URL once configured), followed by the LLM's response. If you see `Adrian v…` you've already won — the event is on its way. + +If the WS URL still says `ws://localhost:8080/ws` and the user is on managed (not self-hosted), they need `ADRIAN_WS_URL` set to the URL the dashboard tells them to use. Add it to `.env` and re-run. Don't make this a blocker for the smoke test — the local JSONL handler is still writing `events.jsonl` next to the script so the user has something tangible immediately. + +**Substep 3A.5 — Direct to dashboard.** Tell the user: + +> Open **https://app.adrian.secureagentics.ai/events** — your event should be there within 2-3 seconds, classified as benign (M0). + +Then go to Step 4. + +## Step 3B — Own agent path (35 seconds) + +**Substep 3B.1 — Get the file.** Ask: + +> What's the path to the LangChain or LangGraph agent file you want to instrument? (Absolute path, or relative to the current directory.) + +Read the file. If it's a directory, ask which file inside. If it's not Python, stop and explain Adrian's SDK is Python-only (LangChain/LangGraph). + +**Substep 3B.2 — Validate the file.** Check three things: + +1. Does it import from `langchain_*` or `langgraph`? If neither, this file isn't a LangChain agent — stop and tell the user. +2. Does it have an `async def` somewhere (the agent entry)? Look for the function that calls `await ...ainvoke(...)` or `await ...astream(...)`. +3. Does it use **sync** `.invoke()` instead of `.ainvoke()`? If yes, warn: + > Heads-up: your agent uses the sync `.invoke()` path. Adrian will still capture events for logging, but Block / Human Review gating only fires on the async path (`ainvoke` / `astream`). If you want in-flight tool blocking later, you'll need to convert to async. + Continue anyway — capture works either way. + +**Substep 3B.3 — Identify the patch site.** The patch is two insertions, and the second one differs based on the `key_source` flag set in Step 1.3. + +1. **Imports block at the top of the file:** add `import adrian`. Add `import os` too if `os` isn't already imported (only needed for the env variant below). + +2. **Entry function — first statement inside the `async def`.** Find the function the user runs as the agent's entry point. This is usually the `async def main():` (or similar) that's called from `asyncio.run(main())`. Insert one of the two variants below as the first statement inside that function. + + #### Variant A — `key_source = "env"` (user has `.env` set up) + + ```python + adrian.init(api_key=os.environ["ADRIAN_API_KEY"]) + ``` + + The user is responsible for sourcing `.env` before running their agent. If they already use `python-dotenv` or `direnv`, this just works. If they don't, mention it: "Source your `.env` before running with `set -a; . ./.env; set +a`." + + #### Variant B — `key_source = "paste"` (user pasted the key directly) + + ```python + # TODO: move this key to a .env file before committing this script. + # Replace the literal below with os.environ["ADRIAN_API_KEY"] and put + # ADRIAN_API_KEY=adr_live_... in .env (gitignored). + adrian.init(api_key="adr_live_REPLACE_ME") + ``` + + Substitute `adr_live_REPLACE_ME` with the actual key the user pasted. **Keep the comment block** — the user opted into the convenience trade-off and the comment makes the eventual cleanup trivial to find with `grep -rn 'TODO.*\.env'`. + +You do **not** need to add `adrian.shutdown()` — the SDK registers it via `atexit` automatically. (You can still add it before a clean `return` if the agent runs forever and you want explicit teardown.) + +**Sync-only agents:** if there's no `async def` and the agent is fully sync, put the same `adrian.init(...)` line once at module import time (after the imports block, before any LangChain object is constructed). Pick the env or paste variant the same way. Sync mode still captures events; it just doesn't gate tool calls. + +**Substep 3B.4 — Show the diff before applying.** Print the patched file as a unified diff and ask the user to confirm before writing. (Skip this step if your client doesn't support arbitrary file writes — paste the patched code and tell the user to save it.) + +**Substep 3B.5 — Install the SDK in the user's existing environment.** Detect what they're using: + +- Plain venv → `pip install adrian-sdk` +- Poetry → `poetry add adrian-sdk` +- uv → `uv add adrian-sdk` (or `uv pip install adrian-sdk` in legacy projects) +- Conda → `pip install adrian-sdk` inside the activated env +- Requirements file → append `adrian-sdk` to `requirements.txt`, run `pip install -r requirements.txt` + +If you can't tell, ask the user once: "What package manager does this project use?" + +**Substep 3B.6 — Run their agent.** Tell the user: + +> Now run your agent as you normally would. Adrian's instrumentation captures every LLM call and tool call automatically — you don't need to change how you invoke the agent. + +Then go to Step 4. + +## Step 4 — Verify in the dashboard (5 seconds) + +Tell the user: + +> Open **https://app.adrian.secureagentics.ai/events**. Within a couple of seconds you should see one or more events listed — each is one LLM call or tool call your agent made, with a classification badge (M0 benign, M2 misuse, M3 high-risk, M4 malicious). Most first runs are all M0. + +If nothing shows up in 10 seconds, jump to Part 2 §12 ("Common failure modes") and walk the user through the relevant entry. + +## Step 5 — Offer the next obvious step (10 seconds) + +After the user confirms they see events, offer one of these as a natural next step (don't force it; just one sentence each): + +- **"Want Adrian to actually block dangerous tool calls?"** → flip the agent profile to Block mode (Settings → Agents). Requires the async path. +- **"Want a Discord alert when a tool call gets flagged?"** → Settings → Webhooks → New, paste a Discord webhook URL, choose alert type `M3` / `M4` / `all`. +- **"Want to tell Adrian what your agent is supposed to do (its remit), so misuse classification is more accurate?"** → Settings → Agents → edit the profile, fill in remit + expected behaviours + known risks. + +Then stop. Setup is done. The rest of this file is reference for when something goes wrong. + +--- + +## Cross-client compatibility notes + +| Agent | Multi-choice tool | File write | Shell execution | Special quirks | +|---|---|---|---|---| +| **Claude Code** (CLI) | none — ask in chat | Edit/Write tools | Bash tool | Sync via Edit; preserves diffs cleanly | +| **Claude Desktop** (Cowork) | `AskUserQuestion` | Edit/Write tools | sandboxed bash | Prefer `.env` for the API key — chat persists | +| **Codex CLI** | none — ask in chat | direct edit | shell exec | Codex defaults to streaming edits; show diff first | +| **Codex web / ChatGPT** | none — ask in chat | offer code blocks | none | Tell the user to save / run; you can't execute | +| **Cursor** chat | none — ask in chat | Composer can write | terminal panel | Composer multi-file edits work for own-agent path | +| **Aider** | none — ask in chat | yes (its model) | shell exec | Map files into the session before patching | +| **Windsurf / Copilot Chat** | none — ask in chat | varies | varies | Treat as Codex-equivalent | + +If your client supports it, prefer: +- A structured multi-choice question over a free-text "type a or b". Reduces parse errors. +- Showing a diff before writing patched code (own-agent path) over a silent write. Users want a chance to bail. +- Reading `.env` once and not echoing the key back over re-reading it in every step. Reduces leak surface. + +--- + +# PART 2 — Reference (lookup index) + ## 1. What Adrian is (one paragraph) Adrian is an open-source runtime security monitoring and control engine for AI agents. It captures every LLM call and tool call your LangChain / LangGraph agent makes, ships them to a backend (either the managed cloud at `app.adrian.secureagentics.ai` or a self-hosted Docker stack), classifies them against a behaviour policy ("MAD codes" — Misuse, Abuse, Deception), and depending on the configured execution mode it can **alert**, **pause for human review**, or **block** the tool call mid-flight. It plugs in with two lines of Python (`adrian.init()` + `adrian.shutdown()`) thanks to LangChain auto-instrumentation. From 093d4c0c9f4fab048bd3abf2e1a48adc0a2abec2 Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Thu, 21 May 2026 23:38:25 +0500 Subject: [PATCH 3/9] Improving AI Guide feature --- GET_STARTED_AI_GUIDE.md | 453 ++++++++++++++++++++++++++++++---------- 1 file changed, 338 insertions(+), 115 deletions(-) diff --git a/GET_STARTED_AI_GUIDE.md b/GET_STARTED_AI_GUIDE.md index 1077fe5..f41860b 100644 --- a/GET_STARTED_AI_GUIDE.md +++ b/GET_STARTED_AI_GUIDE.md @@ -10,19 +10,36 @@ > **AI agent: this section is a program, not documentation. Run it top-to-bottom. Branch only at the marked decision points. If a step fails, jump to the matching subsection in Part 2 and resume.** -## Step 0 — Confirm preconditions (5 seconds) +### Ground rules for the AI agent (read once, apply throughout) -Run these checks silently (don't ask the user to type them). Either you can shell out yourself, or you ask the user to confirm. +These rules override your normal defaults for this flow. Breaking any of them is the difference between a 60-second success and a frustrated user who has to redo the setup. + +1. **Cross-confirm every decision with the user. Never silently pick a default.** Every time the flow branches — LLM provider, where to store an API key, what to do after the smoke test, whether to instrument an existing project — surface the choices to the user and wait for their answer. Do not assume OpenAI, do not assume `.env`, do not assume "yes, keep going". Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` on Claude, otherwise list the options in chat and wait). +2. **Do not offer or use a fake / mock / stub LLM under any circumstance.** A fake LLM does not produce real tool calls or model events, which means Adrian has nothing meaningful to capture, which means the smoke test "succeeds" while teaching the user nothing about how Adrian works. If the user has no LLM provider credentials at all, stop and help them get one (link to OpenAI / Anthropic / Google / Bedrock / Ollama install) before proceeding. Do not invent a path around this. +3. **Be LLM-agnostic.** Adrian instruments anything that runs through LangChain / LangGraph. Ask the user which provider they want to use rather than assuming. Supported provider integrations include (non-exhaustive): OpenAI, Anthropic, Google (Gemini / Vertex), AWS Bedrock, Azure OpenAI, Ollama (local models), Groq, Mistral, Together, Fireworks, Cohere, HuggingFace endpoints. If the user names something not on this list, ask for the LangChain package name (`langchain-`) — most providers have one. +4. **Apply the same storage preference to every secret in the flow.** The decision the user made about the Adrian API key in Step 1.2 (`key_source ∈ {"env", "inline_user", "inline_agent"}`) governs how you treat their LLM provider key in Step 3 too. Do not mix paths — that is what produces hardcoded keys sitting next to a half-populated `.env`. +5. **Explain code before you ask the user to run it.** Every script or patched file you produce must be preceded by a one-paragraph description in plain English of what it will do (which model it will call, what prompt it will send, what side effects it has, what it writes to disk). Users should never paste-and-run code they don't understand the shape of. +6. **At every named "Step N" boundary, summarise what just happened and ask if it's OK to proceed.** Especially after the smoke test runs — do not auto-jump to "configure webhooks now". Present the structured options in Step 5 and let the user pick. + +## Step 0 — Confirm preconditions and establish the working directory (5 seconds) + +Run these checks. Either shell out yourself or ask the user to run the commands and paste output. | Requirement | How to check | |---|---| | Python ≥ 3.12 | `python3 --version` | | pip available | `python3 -m pip --version` | -| A working directory | `pwd` (any folder; we'll write one or two files here) | +| Absolute working directory path | `pwd` (macOS / Linux / Git Bash / PowerShell), `cd` (Windows cmd) | | Network access to PyPI + `app.adrian.secureagentics.ai` | implicit; flag if obviously blocked | If Python < 3.12, stop and ask the user to install 3.12 (or use `pyenv install 3.12`). Don't proceed — the SDK declares `requires-python = ">=3.12"` and will fail to install. +**Then — before moving to Step 1 — announce the absolute working directory back to the user, and store it for the rest of the flow.** This is the single most common source of confusion later (the `.env` file ends up in a different folder than the user expected). Get it straight before any files exist. + +> Your working directory is `/Users/you/myproject`. Every file we create — `.env`, `adrian_quickstart.py`, the virtual env — will live here. Sound right? If you wanted to be in a different folder, `cd` there now and let me know the new path before we continue. + +Wait for confirmation, or for the user to give you a different path. Then store this value as an internal variable `working_dir = ""` and reference it whenever you generate a file-creation command in later steps. Never tell the user to "create a file in your project directory" without naming the actual absolute path. + ## Step 1 — Get the user's Adrian API key (15 seconds) This step has three substeps. Don't skip the verification at the end — Steps 3 and 4 assume you've already confirmed a valid key is in place. @@ -36,62 +53,116 @@ Tell the user this, click-by-click. Don't paraphrase — the user is on a 60-sec > 3. Once you're in the dashboard, go to **Settings → Agents** (left sidebar). > 4. Click **New key**. Give it any label (e.g. "60-second setup") and click **Generate**. > 5. Copy the key. It starts with `adr_live_…`. The dashboard shows it **once** — if you close the modal without copying, you'll need to issue a new one. -> 6. Tell me when you have it. +> 6. Keep the key on your clipboard — I'll ask you next where you want to store it. -Wait for the user to indicate they have the key. Don't proceed without that confirmation. +Don't pause for a separate "do you have it?" confirmation. Go straight to 1.2; the user's answer to the storage question in 1.2 is also the signal that they have the key. -### 1.2 Recommend the secure path (.env file), and offer to accept a paste as a fallback +### 1.2 Ask the user how they want to store the key (upfront 3-option choice) -Default flow — what you should ask for first: +Don't pick a storage path on the user's behalf — present all three options at once and let them pick. Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` on Claude); otherwise list them in chat numbered 1 / 2 / 3 and wait for the user's pick. -> Recommended: create a file called `.env` in your project directory with this one line, then save it: +> You've got your Adrian API key. Where would you like to store it? Pick one — same answer will apply to any other API keys we need later (e.g. for an LLM provider) so we stay consistent. > -> ``` -> ADRIAN_API_KEY=adr_live_… -> ``` +> **1. Save it to a `.env` file in your project directory (recommended).** Most flexible and most secure: the script reads the key from the environment, the file never goes to git, and you can rotate the key by editing one line. The key never passes through this chat. Best default for almost everyone. > -> (Replace `adr_live_…` with the key you just copied.) When you've saved the file, type **done** and I'll verify it. - -If the user follows this path → go to 1.3 ("verification of .env"). +> **2. Add it to the code yourself.** I'll write the script with a `PASTE_YOUR_KEY_HERE` placeholder; you replace it with your real key in your editor before running. The key never passes through this chat. Pick this if you don't want your key transiting the chat context (e.g. on Codex web / ChatGPT) or if you just prefer to handle secrets manually. +> +> **3. Paste it to me in chat and I'll embed it in the script.** Fastest path; everything runs in one shot. The key will sit in this chat history. I'll add a `# TODO: move to .env` comment so future-you can clean it up later. Pick this only if you trust this chat with your key. -If the user pastes the key directly into chat instead (this happens often) → accept it gracefully. Don't refuse. But switch behaviour: +Wait for the user's answer. Set the internal flag `key_source` and proceed to 1.3: -- **Do not try to silently write it to `.env`.** Many agents can't write arbitrary files (Codex web, ChatGPT, Cursor in restricted mode), and even agents that can may not survive a context reload to remember the file path. The user's shell may also have already loaded a stale `.env`. Trying to "help" here usually creates a confusing mismatch between what's in the file and what the running shell sees. -- **Instead, plan to hardcode the key into the script you write in Step 3.** When you reach Step 3A, change `adrian.init()` to `adrian.init(api_key="adr_live_…")` with the actual key. For Step 3B, do the same patch in their file. -- **Warn the user explicitly, in the message where you confirm receipt of the key:** - > Got it. I'm going to put this key directly into the code I write so it runs straight away. Before you commit that file to git or share it with anyone, replace the hardcoded key with `os.environ["ADRIAN_API_KEY"]` and move the actual value into a `.env` file that's gitignored. I'll mark the line with a `# TODO` comment so you can find it. +- Option **1** → `key_source = "env"` +- Option **2** → `key_source = "inline_user"` (you'll leave a placeholder in the script; the user replaces it in their editor) +- Option **3** → `key_source = "inline_agent"` (the user pastes the key to you in chat; you embed it literally in the script) -Go to 1.3 ("verification of paste"). +**A few rules that apply to every option:** -**One rule that applies to both paths:** never echo the full key back in any later message. Refer to it as `adr_live_…` from here on. If you need to mention it in a log line, redact past the prefix. +- Never echo the full key back in any later message. Refer to it as `adr_live_…` from here on. If you need to mention it in a log line, redact past the prefix. +- The same option will govern any other secrets in this flow (e.g. the LLM provider key in Step 3A). Don't mix paths — if Adrian's key went into `.env`, the provider key goes into the same `.env`; if Adrian's key is inline in the script, the provider key is inline the same way. +- If the user picks option 2, do **not** also ask them to paste the key — the whole point of option 2 is that the key never enters this chat. +- If the user picks option 3, you don't need a separate "warn before inlining" step — by picking option 3 they've already consented. Just confirm receipt of the key (without echoing it) and move on. ### 1.3 Verify before moving on -This is a hard gate. Step 2 starts only after one of the following is true. +This is a hard gate. Step 2 starts only after the verification path matching the user's option in 1.2 passes. + +**If `key_source = "env"` (option 1, `.env` file):** + +Give the user a single copy-pasteable command that creates the `.env` file at the **absolute path** you stored in Step 0 as `working_dir`, then opens it for editing. Don't tell them to "create a file in your project directory" — that's the wording that produced the file-in-the-wrong-place failure in early demos. Use the absolute path and pick the command for their OS. + +Example wording (substitute `working_dir` with the actual absolute path, e.g. `/Users/you/myproject`): + +> Run **one** of these in your terminal — whichever matches your OS. The path is the absolute path to your working directory, so the file ends up exactly where the script will look for it later. +> +> **macOS:** +> ```sh +> touch "/Users/you/myproject/.env" && open -t "/Users/you/myproject/.env" +> ``` +> +> **Linux (any editor):** +> ```sh +> touch "/Users/you/myproject/.env" && "${EDITOR:-nano}" "/Users/you/myproject/.env" +> ``` +> +> **Windows PowerShell:** +> ```powershell +> New-Item -ItemType File -Force "C:\Users\you\myproject\.env"; notepad "C:\Users\you\myproject\.env" +> ``` +> +> **Windows cmd:** +> ```cmd +> type nul > "C:\Users\you\myproject\.env" && notepad "C:\Users\you\myproject\.env" +> ``` +> +> When the editor opens, paste this single line (with your actual key substituted for `adr_live_…`), save the file, and close the editor: +> +> ``` +> ADRIAN_API_KEY=adr_live_… +> ``` +> +> Type **done** when the file is saved. -**If the user said "done" (the .env path):** +When they confirm, verify: -1. Confirm `.env` exists in the current working directory. If your client can read files, read it and check. If not, ask the user: - > Quick check — can you run `cat .env` (or `type .env` on Windows) and paste the output? I want to make sure the key landed correctly. +1. Confirm `.env` exists at `working_dir`. If your client can read files, read `/.env` and check. If not, ask the user: + > Quick check — can you run `cat "/.env"` (or `type "\.env"` on Windows) and paste the output? I want to make sure the key landed at the right path. 2. Confirm the line `ADRIAN_API_KEY=…` is present and the value: - Starts with `adr_live_` (managed cloud) or `adr_local_` (self-hosted). - Has no surrounding quotes — `ADRIAN_API_KEY="adr_live_xxx"` works in most shells but tripped some users; if you see quotes, ask the user to remove them. - Has no surrounding whitespace — a trailing space is a common paste artifact and will fail the auth check silently later. 3. If the format is wrong, point at the exact issue and ask the user to fix it. Most common: the key was truncated during copy (Discord and Slack sometimes trim long strings on send-from-mobile). -4. If everything looks good, tell the user: - > Key verified in `.env`. I'll read it from the environment in the code I'm about to write. +4. If `.env` is not at `working_dir` but exists somewhere else on the user's machine (e.g. the user's home directory or the Adrian repo), do **not** silently accept it — that's the bug we're guarding against. Tell the user the path mismatch explicitly and re-run the create command at `working_dir`. +5. If everything looks good, tell the user: + > Key verified in `/.env`. I'll read it from the environment in the code I'm about to write. + +**If `key_source = "inline_user"` (option 2, user will edit the script):** + +There's no value for you to verify yet — the user will paste the key into the script file in Step 3A.3 (or into their own agent file in Step 3B.3), where you'll leave a `adr_live_PASTE_YOUR_KEY_HERE` placeholder for them. + +Just confirm the plan back to the user: -**If the user pasted the key (the chat-paste path):** +> Got it. I'll write the script with a `PASTE_YOUR_KEY_HERE` placeholder. Before running it, you'll replace that placeholder with your actual key in your editor. I'll remind you again at the right moment. + +Then proceed to Step 2. Set a reminder for yourself to: +- Use `adr_live_PASTE_YOUR_KEY_HERE` as the literal in any generated code. +- Pause before Step 3A.4 / 3B.6 (the "run it" step) to remind the user to fill in the placeholder. + +**If `key_source = "inline_agent"` (option 3, user pastes key to you):** + +Ask the user to paste the key now. Then: 1. Confirm the pasted value matches `^adr_(live|local)_[0-9a-f]+$`. If not, ask the user to re-copy from the dashboard — they probably grabbed the prefix or a nearby string by mistake. 2. Store the key in your working memory for this session only. **Do not write it to disk yet** — you'll embed it in the script in Step 3. 3. Tell the user (do not include the key value): > Key received. Format looks valid. Moving on — I'll embed it into the script with a `# TODO: move to .env` marker. -Once either verification passes, set an internal flag the rest of this flow checks: +**Summary of the flag values the rest of this flow reads:** - `key_source = "env"` → in Steps 3A and 3B, leave `adrian.init()` argument-less and rely on `ADRIAN_API_KEY` from the environment. -- `key_source = "paste"` → in Steps 3A and 3B, generate `adrian.init(api_key="adr_live_…")` with the literal value and add a `# TODO: move to a .env file before committing` comment above the line. +- `key_source = "inline_user"` → in Steps 3A and 3B, generate `adrian.init(api_key="adr_live_PASTE_YOUR_KEY_HERE")` with a placeholder and a `# TODO: replace placeholder, then move to a .env file before committing` comment above the line. Remind the user to fill the placeholder before running. +- `key_source = "inline_agent"` → in Steps 3A and 3B, generate `adrian.init(api_key="adr_live_…")` with the literal value the user pasted, plus the same `# TODO: move to a .env file before committing` comment. + +The two `inline_*` paths produce structurally identical scripts — the only difference is whether you fill in the literal key (`inline_agent`) or leave a placeholder for the user to fill in (`inline_user`). ## Step 2 — Decision point: test agent or own agent? (5 seconds) @@ -102,118 +173,180 @@ Ask the user this question. Use whichever tool you have: > Two ways to see Adrian in action: > -> **(A) Test agent** — I write a tiny LangChain script, run it, and you watch the event appear in the dashboard. Fastest. Needs an `OPENAI_API_KEY` (or I can use a fake LLM if you don't have one). +> **(A) Test agent** — I write a tiny LangChain script that calls a real LLM once and prints the response. You watch the event show up in the dashboard. Fastest path. **Requires real credentials for some LLM provider** — OpenAI, Anthropic, Google (Gemini / Vertex), AWS Bedrock, Azure OpenAI, Ollama running locally, Groq, Mistral, or any other provider with a `langchain-…` integration. I'll ask which one you'd like to use. > > **(B) Your agent** — Tell me the path to your existing LangChain / LangGraph file and I add two lines so Adrian instruments it. Then run your agent as you normally would. +If the user picks (A) but does not currently have credentials for any LLM provider, **stop and help them get one** before continuing — do not substitute a fake / mock / stub LLM. A fake LLM produces no real model events for Adrian to capture, so the smoke test would silently teach the user the wrong mental model. Point them at the cheapest viable option for their situation: Ollama for "I don't want to pay anything", OpenAI / Anthropic free tiers for "I'll sign up now", AWS Bedrock for "I already use AWS", etc. Then resume Step 3A once they have a key. + Branch on the answer. ## Step 3A — Test agent path (35 seconds) -**Substep 3A.1 — OpenAI key check.** Ask: "Do you have an `OPENAI_API_KEY`?" Three branches: +**Substep 3A.1 — Ask the user which LLM provider to use.** Do not pick one for them. Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` for Claude); otherwise list these options in chat and wait for the user's answer. + +> Which LLM provider should the smoke-test script call? Pick whichever you already have credentials for: +> +> **a. OpenAI** — needs `OPENAI_API_KEY` (starts with `sk-…`). LangChain package: `langchain-openai`. +> **b. Anthropic** — needs `ANTHROPIC_API_KEY` (starts with `sk-ant-…`). LangChain package: `langchain-anthropic`. +> **c. Google (Gemini)** — needs `GOOGLE_API_KEY`. LangChain package: `langchain-google-genai`. +> **d. AWS Bedrock** — uses your existing AWS credentials chain (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION`, or `~/.aws/credentials`). LangChain package: `langchain-aws`. +> **e. Azure OpenAI** — needs `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, and a deployment name. LangChain package: `langchain-openai` (the `AzureChatOpenAI` class). +> **f. Ollama (local models)** — no API key needed; needs `ollama serve` running on `localhost:11434` and a model pulled (e.g. `ollama pull llama3.2`). LangChain package: `langchain-ollama`. +> **g. Something else** — tell me which one and I'll look up the right `langchain-` package. + +Wait for the user's choice. Record it as `llm_provider` (one of `openai`, `anthropic`, `google`, `bedrock`, `azure`, `ollama`, `custom`). All subsequent substeps depend on this value. + +**Substep 3A.1b — Confirm credentials and apply the same storage choice as the Adrian key.** For every provider except Ollama (which has no API key), ask the user the credential question explicitly. Two parts: + +1. **Do they have a credential ready?** If they don't, stop and help them get one — link to the provider's signup page. Do not invent a workaround. +2. **Use the same storage path they chose for the Adrian key in Step 1.2.** Branch on the `key_source` flag: + + - **`key_source = "env"`** → tell the user to add the provider key to the same `.env` file, e.g.: + ``` + OPENAI_API_KEY=sk-… # for option (a) + ANTHROPIC_API_KEY=sk-ant-… # for option (b) + GOOGLE_API_KEY=… # for option (c) + AWS_REGION=us-east-1 # for option (d) — plus the access/secret keys, or rely on ~/.aws/credentials + AZURE_OPENAI_API_KEY=… # for option (e) — plus AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_DEPLOYMENT + # (Ollama needs no key) + ``` + Ask them to confirm the file is saved before moving on. Verify the same way you verified the Adrian key (`cat .env` if you can't read files directly). + + - **`key_source = "inline_user"`** → do not ask the user to paste the provider key into chat. Instead tell them: "I'll write the script with a placeholder for your provider key too (e.g. `PASTE_YOUR_OPENAI_KEY_HERE`). You'll replace it in the script before running, the same way you'll replace the Adrian one." Set yourself a reminder to leave placeholders for both keys in Step 3A.3 and to prompt the user to fill them in before Step 3A.4. + + - **`key_source = "inline_agent"`** → ask them to paste the provider key into chat. You'll embed it inline in the script in Step 3A.3 with the same `# TODO: move to .env` marker used for the Adrian key. Confirm receipt (without echoing the key value) and proceed. -- **Yes, in env** → continue to 3A.2. -- **Yes, has the value** → write it into the same `.env` (`OPENAI_API_KEY=sk-…`) and continue. -- **No** → use the FakeChatModel script at the end of this section instead of the OpenAI one. Continue to 3A.2. +For **Ollama (option f)**, no key prompt is needed under any `key_source` — but do confirm: "Is `ollama serve` running and do you have at least one model pulled? If not, run `ollama pull llama3.2` (or any chat model) before we continue." Wait for confirmation. -**Substep 3A.2 — Create venv and install.** Run, in the user's working directory: +For **AWS Bedrock (option d)**, also ask which model ID they want (e.g. `anthropic.claude-3-5-sonnet-20241022-v2:0`) — Bedrock has no default and the script needs to name one. For **Azure OpenAI (option e)**, ask for the deployment name. + +Once credentials are confirmed and stored according to `key_source`, proceed to 3A.2. + +**Substep 3A.2 — Create venv and install.** The LangChain provider package depends on `llm_provider`. Run, in the user's working directory: ```sh python3 -m venv .venv source .venv/bin/activate # on Windows: .venv\Scripts\activate -pip install adrian-sdk langchain-openai +pip install adrian-sdk ``` +Substitute `` with: + +| `llm_provider` | Package | +|---|---| +| `openai` | `langchain-openai` | +| `anthropic` | `langchain-anthropic` | +| `google` | `langchain-google-genai` | +| `bedrock` | `langchain-aws` | +| `azure` | `langchain-openai` (same package; uses `AzureChatOpenAI`) | +| `ollama` | `langchain-ollama` | +| `custom` | whatever the user names (`langchain-groq`, `langchain-mistralai`, `langchain-cohere`, etc.) | + If `pip install` exits non-zero, jump to Part 2 §12. -**Substep 3A.3 — Write the smoke-test script.** Create `adrian_quickstart.py` in the working directory. **The exact code depends on the `key_source` you set in Step 1.3** — pick the matching variant. +**Substep 3A.3 — Explain the script, then write it.** Before creating any file, tell the user in plain English what the script will do — they should never be in the position of pasting and running code without knowing what it does. -#### Variant A — `key_source = "env"` (user saved the key to `.env`) +Send this explanation first (adapted to the chosen `llm_provider`): -```python -"""Adrian 60-second smoke test.""" -import asyncio -import os -import sys -import adrian -from langchain_openai import ChatOpenAI +> Here's what I'm about to create. The script `adrian_quickstart.py` is a tiny LangChain agent that does three things: +> +> 1. Calls `adrian.init()` to start Adrian's instrumentation. Adrian monkey-patches LangChain so every model call and tool call is captured automatically. +> 2. Builds a single chat-model client using **{provider name}** ({model id we picked}) and asks it one short question: *"In one sentence, why is the sky blue?"*. That's it — no tools, no agent loop, just one model call so we have something visible in the dashboard quickly. +> 3. Prints the model's reply and calls `adrian.shutdown()` to flush events. +> +> Side effects: it writes captured events to `./events.jsonl` next to the script, and pushes them over WebSocket to the Adrian backend. It does not write or modify anything else. +> +> Ready for me to create it? (yes/no) +Wait for the user to confirm before writing the file. Now create `adrian_quickstart.py` in the working directory. **The exact code depends on both `key_source` (from Step 1.2) and `llm_provider` (from Step 3A.1)** — assemble the script from the two pieces below. -async def main() -> int: +##### Piece 1 — Adrian initialisation block + +Pick exactly one of the three, matching `key_source`: + +**A. `key_source = "env"`** (user saved the Adrian key to `.env`): +```python if not os.environ.get("ADRIAN_API_KEY"): sys.exit("ADRIAN_API_KEY missing. Did you source your .env? " "Try: set -a; . ./.env; set +a") - if not os.environ.get("OPENAI_API_KEY"): - sys.exit("OPENAI_API_KEY missing. Set it in your shell or .env.") - adrian.init() # reads ADRIAN_API_KEY from env automatically - llm = ChatOpenAI(model="gpt-4o-mini") - response = await llm.ainvoke("In one sentence, why is the sky blue?") - print("LLM said:", response.content) - adrian.shutdown() - return 0 - - -if __name__ == "__main__": - sys.exit(asyncio.run(main())) ``` -#### Variant B — `key_source = "paste"` (user pasted the key into chat) - -Substitute the literal key the user pasted in place of `adr_live_REPLACE_ME` on the marked line. **Keep the TODO comment** — the user explicitly opted into the convenience trade-off and the comment is the audit trail. - +**B. `key_source = "inline_user"`** (user will edit the script before running). Leave the placeholder literally as-is — the user fills it in. **Keep the TODO comment** so the placeholder is easy to find: ```python -"""Adrian 60-second smoke test.""" -import asyncio -import os -import sys -import adrian -from langchain_openai import ChatOpenAI + # TODO: replace the PASTE_YOUR_KEY_HERE placeholder below with your actual + # Adrian API key, then move it to a .env file before committing this script. + # See https://docs.adrian.secureagentics.ai/quickstart for the .env pattern. + adrian.init(api_key="adr_live_PASTE_YOUR_KEY_HERE") +``` +After writing the file, remind the user: "Before you run this, open `adrian_quickstart.py` in your editor and replace `adr_live_PASTE_YOUR_KEY_HERE` with the real key you copied from the dashboard. Do the same for any other placeholder I left." -async def main() -> int: +**C. `key_source = "inline_agent"`** (user pasted the Adrian key into chat). Substitute the literal key in place of `adr_live_REPLACE_ME`. **Keep the TODO comment** — it's the audit trail for the convenience trade-off the user opted into: +```python # TODO: move this key to a .env file before committing this script. # Replace the literal with os.environ["ADRIAN_API_KEY"] and put # ADRIAN_API_KEY=adr_live_... in .env (which should be gitignored). - ADRIAN_API_KEY = "adr_live_REPLACE_ME" + adrian.init(api_key="adr_live_REPLACE_ME") +``` - if not os.environ.get("OPENAI_API_KEY"): - sys.exit("OPENAI_API_KEY missing. Set it in your shell or .env.") +##### Piece 2 — LLM provider block - adrian.init(api_key=ADRIAN_API_KEY) - llm = ChatOpenAI(model="gpt-4o-mini") - response = await llm.ainvoke("In one sentence, why is the sky blue?") - print("LLM said:", response.content) - adrian.shutdown() - return 0 +Pick the block that matches `llm_provider`. Storage handling follows `key_source`: +- `env` → no key in code; the script asserts the relevant env var is set. +- `inline_user` → leave a `PASTE_YOUR__KEY_HERE` placeholder with the same TODO comment pattern. +- `inline_agent` → inline the literal key the user pasted with the same TODO comment pattern. +**`openai`** — import `from langchain_openai import ChatOpenAI`; build with `ChatOpenAI(model="gpt-4o-mini")`. Key handling: `key_source = "env"` → add an upfront check for `OPENAI_API_KEY`; `key_source = "inline_user"` → at the top of `main()` set `os.environ["OPENAI_API_KEY"] = "PASTE_YOUR_OPENAI_KEY_HERE"` (with TODO comment); `key_source = "inline_agent"` → same line but substitute the actual `sk-…` value the user pasted. -if __name__ == "__main__": - sys.exit(asyncio.run(main())) -``` +**`anthropic`** — import `from langchain_anthropic import ChatAnthropic`; build with `ChatAnthropic(model="claude-3-5-haiku-latest")`. Env / inline pattern uses `ANTHROPIC_API_KEY`. Placeholder for `inline_user`: `PASTE_YOUR_ANTHROPIC_KEY_HERE`. + +**`google`** — import `from langchain_google_genai import ChatGoogleGenerativeAI`; build with `ChatGoogleGenerativeAI(model="gemini-1.5-flash")`. Env / inline pattern uses `GOOGLE_API_KEY`. Placeholder for `inline_user`: `PASTE_YOUR_GOOGLE_KEY_HERE`. + +**`bedrock`** — import `from langchain_aws import ChatBedrockConverse`; build with `ChatBedrockConverse(model="", region_name=os.environ.get("AWS_REGION", "us-east-1"))`. For `env`, rely on the AWS credentials chain; for `inline_user`, leave placeholders for `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION`; for `inline_agent`, inline the values the user pasted. All paths get the same TODO marker. + +**`azure`** — import `from langchain_openai import AzureChatOpenAI`; build with `AzureChatOpenAI(azure_deployment="", api_version="2024-10-21")`. Env / inline pattern uses `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT`. Placeholder for `inline_user`: `PASTE_YOUR_AZURE_KEY_HERE`. -#### FakeChatModel fallback (no OpenAI key) +**`ollama`** — import `from langchain_ollama import ChatOllama`; build with `ChatOllama(model="")`. No API key handling under any `key_source`; just confirm `ollama serve` is reachable at `http://localhost:11434` (the default). -Use this *instead* of the variant above if the user has no `OPENAI_API_KEY`. Pick the env / paste flavour to match `key_source` (only the `adrian.init` line differs): +**`custom`** — import what the user names and build with sensible defaults; ask the user for the class name, model id, and env-var name if you don't know them. Apply the same `key_source` rules to the env-var name they give you. + +##### Full template + +Assemble the chosen blocks into this skeleton (this example shows `key_source = "env"` + `llm_provider = "openai"`; substitute the blocks above for your case): ```python -"""Adrian 60-second smoke test (no LLM provider needed).""" +"""Adrian 60-second smoke test. + +This script makes one LLM call ("Why is the sky blue?") through LangChain +so that Adrian's auto-instrumentation captures a real model event. The +event will appear in the dashboard at https://app.adrian.secureagentics.ai/events +within a couple of seconds and is also written to ./events.jsonl locally. +""" import asyncio import os import sys import adrian -from langchain_core.language_models.fake_chat_models import FakeListChatModel +from langchain_openai import ChatOpenAI # <-- swap to your provider's import async def main() -> int: - # env variant: + # === Adrian init (Piece 1) === + if not os.environ.get("ADRIAN_API_KEY"): + sys.exit("ADRIAN_API_KEY missing. Did you source your .env? " + "Try: set -a; . ./.env; set +a") adrian.init() - # paste variant (replace with the literal key): - # adrian.init(api_key="adr_live_REPLACE_ME") # TODO: move to .env - llm = FakeListChatModel(responses=["The sky is blue because of Rayleigh scattering."]) - out = await llm.ainvoke("In one sentence, why is the sky blue?") - print("Fake LLM said:", out.content) + # === Provider key check (Piece 2) === + if not os.environ.get("OPENAI_API_KEY"): + sys.exit("OPENAI_API_KEY missing. Set it in your shell or .env.") + + # === Model call === + llm = ChatOpenAI(model="gpt-4o-mini") + response = await llm.ainvoke("In one sentence, why is the sky blue?") + print("LLM said:", response.content) + adrian.shutdown() return 0 @@ -222,18 +355,29 @@ if __name__ == "__main__": sys.exit(asyncio.run(main())) ``` -**Substep 3A.4 — Run it.** Run from the same shell that has the venv active. The command depends on `key_source`: +After writing the file, show the user the final assembled code (or a diff if your client renders one) and ask: "Saved. Want me to run it now?" Wait for confirmation before executing in Step 3A.4. + +**Substep 3A.4 — Run it.** Run from the same shell that has the venv active. The exact command depends on `key_source` *and* `llm_provider`. Tell the user which command to run — don't make them guess. -- **`key_source = "env"`** — source `.env` first so `ADRIAN_API_KEY` (and `OPENAI_API_KEY`, if applicable) are in the process environment: +- **`key_source = "env"`** (recommended) — source `.env` once so every key in it (Adrian + whichever provider variables apply) is in the process environment, then run: ```sh set -a; . ./.env; set +a python adrian_quickstart.py ``` -- **`key_source = "paste"`** — the Adrian key is already inlined in the script. You still need `OPENAI_API_KEY` in the shell (unless you used the FakeChatModel variant): + On Windows PowerShell, use `Get-Content .env | ForEach-Object { $k,$v = $_ -split '=',2; [Environment]::SetEnvironmentVariable($k,$v) }` or have the user install `python-dotenv` and add `from dotenv import load_dotenv; load_dotenv()` near the top of the script. + +- **`key_source = "inline_user"`** — before running, remind the user to open `adrian_quickstart.py` and replace every `PASTE_YOUR_..._HERE` placeholder with the actual values. Once they confirm the placeholders are filled in, run: + ```sh + python adrian_quickstart.py + ``` + If you can read files, double-check there are no remaining `PASTE_YOUR_` substrings in the file before running. If you can't read files, ask the user to confirm explicitly: "Have you replaced every `PASTE_YOUR_…` placeholder?" Wait for a yes. + +- **`key_source = "inline_agent"`** — both the Adrian key and the provider key are already inlined in the script (with TODO markers). Just run it: ```sh - export OPENAI_API_KEY=sk-... # skip if using FakeChatModel python adrian_quickstart.py ``` + Exception: if `llm_provider = "bedrock"` and the user relies on `~/.aws/credentials` rather than inlined access keys, the AWS SDK will pick those up automatically — nothing extra to export. + Exception: if `llm_provider = "ollama"`, confirm `ollama serve` is reachable: `curl http://localhost:11434/api/tags` should return JSON. Expected output: a log line `Adrian v1.0.0 initialised (handlers=2, ws=ws://localhost:8080/ws)` (or the managed cloud's WS URL once configured), followed by the LLM's response. If you see `Adrian v…` you've already won — the event is on its way. @@ -261,11 +405,11 @@ Read the file. If it's a directory, ask which file inside. If it's not Python, s > Heads-up: your agent uses the sync `.invoke()` path. Adrian will still capture events for logging, but Block / Human Review gating only fires on the async path (`ainvoke` / `astream`). If you want in-flight tool blocking later, you'll need to convert to async. Continue anyway — capture works either way. -**Substep 3B.3 — Identify the patch site.** The patch is two insertions, and the second one differs based on the `key_source` flag set in Step 1.3. +**Substep 3B.3 — Identify the patch site.** The patch is two insertions, and the second one differs based on the `key_source` flag set in Step 1.2. 1. **Imports block at the top of the file:** add `import adrian`. Add `import os` too if `os` isn't already imported (only needed for the env variant below). -2. **Entry function — first statement inside the `async def`.** Find the function the user runs as the agent's entry point. This is usually the `async def main():` (or similar) that's called from `asyncio.run(main())`. Insert one of the two variants below as the first statement inside that function. +2. **Entry function — first statement inside the `async def`.** Find the function the user runs as the agent's entry point. This is usually the `async def main():` (or similar) that's called from `asyncio.run(main())`. Insert one of the three variants below as the first statement inside that function. #### Variant A — `key_source = "env"` (user has `.env` set up) @@ -275,7 +419,17 @@ Read the file. If it's a directory, ask which file inside. If it's not Python, s The user is responsible for sourcing `.env` before running their agent. If they already use `python-dotenv` or `direnv`, this just works. If they don't, mention it: "Source your `.env` before running with `set -a; . ./.env; set +a`." - #### Variant B — `key_source = "paste"` (user pasted the key directly) + #### Variant B — `key_source = "inline_user"` (user will edit the patched file themselves) + + ```python + # TODO: replace the PASTE_YOUR_KEY_HERE placeholder below with your actual + # Adrian API key, then move it to a .env file before committing this script. + adrian.init(api_key="adr_live_PASTE_YOUR_KEY_HERE") + ``` + + **Keep the placeholder literally as-is** — the user fills it in. After applying the patch in 3B.4, remind the user explicitly: "Open the patched file in your editor and replace `adr_live_PASTE_YOUR_KEY_HERE` with the real key before running." + + #### Variant C — `key_source = "inline_agent"` (user pasted the key directly) ```python # TODO: move this key to a .env file before committing this script. @@ -316,15 +470,44 @@ Tell the user: If nothing shows up in 10 seconds, jump to Part 2 §12 ("Common failure modes") and walk the user through the relevant entry. -## Step 5 — Offer the next obvious step (10 seconds) +## Step 5 — Ask the user what they want to do next (default is stop) + +**Do not auto-chain to anything.** The next-step prompt depends on which path the user came through — Step 3A (smoke test) or Step 3B (their own agent). The two cases are structurally similar but the phrasing matters: don't offer 3B users a "instrument an existing project" option when they just did exactly that. + +Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` on Claude); otherwise list in chat and wait for "a" or "b". The default is always (a) stop — never push the user toward the secondary option. + +### 5.A — If the user came through Step 3A (test agent / smoke test) -After the user confirms they see events, offer one of these as a natural next step (don't force it; just one sentence each): +> Adrian is capturing events from the test script — that's everything the setup flow needed to show you. What would you like to do next? +> +> **(a) Stop here. [default]** You've seen the loop work end-to-end. You can come back to this guide any time to instrument a real agent. +> +> **(b) Instrument an existing LangChain / LangGraph project of yours.** Tell me the file path and I'll add the same two lines (`import adrian` + `adrian.init(...)` in the async entry function) so Adrian captures events from your real code too. Same provider, same key, no other changes. + +If the user picks **(b)**, jump to **Step 3B**, starting at 3B.1. + +### 5.B — If the user came through Step 3B (already instrumented their own agent) + +> Adrian is now instrumented in your agent at `` and capturing events. What would you like to do next? +> +> **(a) Stop here. [default]** Your agent will keep running with Adrian observing every LLM call and tool call — no further setup needed. Run the agent as you normally would. +> +> **(b) Instrument another agent file.** If you have additional LangChain / LangGraph entry points (multiple agents in one project, or a separate project), point me at the next file and I'll do the same patch. + +If the user picks **(b)**, jump back to **Step 3B.1** with the new file path. Reuse the existing venv if the file is in the same directory; otherwise repeat the install per 3B.5. Do not silently change the LLM provider — keep using whatever the user's project already uses. + +### Shared rules for both 5.A and 5.B -- **"Want Adrian to actually block dangerous tool calls?"** → flip the agent profile to Block mode (Settings → Agents). Requires the async path. -- **"Want a Discord alert when a tool call gets flagged?"** → Settings → Webhooks → New, paste a Discord webhook URL, choose alert type `M3` / `M4` / `all`. -- **"Want to tell Adrian what your agent is supposed to do (its remit), so misuse classification is more accurate?"** → Settings → Agents → edit the profile, fill in remit + expected behaviours + known risks. +If the user picks **(a)** — stop cleanly. Summarise what was set up in one or two sentences (key created, smoke test ran or agent instrumented, first events visible) and say goodbye. Do not keep offering things. Do not volunteer Block mode, webhooks, agent remit, or anything else — they asked to stop. Stop. -Then stop. Setup is done. The rest of this file is reference for when something goes wrong. +**Only if the user proactively asks about further configuration**, point them at the relevant Part 2 sections — don't list these unprompted: + +- Switching the agent profile to Block mode (halts risky tool calls mid-flight) → see §6 (Execution modes and the MAD taxonomy). +- Setting up a Discord webhook for M3 / M4 alerts → see §7 (Integrations → Notifications). +- Filling in the agent's remit / known risks so the classifier is more accurate → see §8 (Reading events → Settings → Agents) or edit the profile directly in the dashboard. +- Anything else → §12 (failure modes) or the Discord linked in §2. + +The rest of this file is reference for when something goes wrong. --- @@ -332,18 +515,30 @@ Then stop. Setup is done. The rest of this file is reference for when something | Agent | Multi-choice tool | File write | Shell execution | Special quirks | |---|---|---|---|---| -| **Claude Code** (CLI) | none — ask in chat | Edit/Write tools | Bash tool | Sync via Edit; preserves diffs cleanly | -| **Claude Desktop** (Cowork) | `AskUserQuestion` | Edit/Write tools | sandboxed bash | Prefer `.env` for the API key — chat persists | -| **Codex CLI** | none — ask in chat | direct edit | shell exec | Codex defaults to streaming edits; show diff first | -| **Codex web / ChatGPT** | none — ask in chat | offer code blocks | none | Tell the user to save / run; you can't execute | -| **Cursor** chat | none — ask in chat | Composer can write | terminal panel | Composer multi-file edits work for own-agent path | -| **Aider** | none — ask in chat | yes (its model) | shell exec | Map files into the session before patching | -| **Windsurf / Copilot Chat** | none — ask in chat | varies | varies | Treat as Codex-equivalent | +| **Claude Code** (CLI) | none — ask in chat | Edit/Write tools | Bash tool | Sync via Edit; preserves diffs cleanly. Bash runs in the same shell cwd as the user expects. | +| **Claude Desktop** (Cowork) | `AskUserQuestion` | Edit/Write tools | sandboxed bash | Sandboxed bash has its **own** cwd, separate from the user's terminal — *never* use bare `touch .env`; always use the absolute `working_dir` path from Step 0. Chat persists, so prefer `.env` over `inline_agent`. | +| **Codex CLI** | none — ask in chat | direct edit | shell exec | Streaming edits — show diff first. Shell exec runs in Codex's session cwd, which may differ from the user's terminal cwd; confirm it matches `working_dir`. | +| **Codex web / ChatGPT** | none — ask in chat | offer code blocks | none | Can't execute or write files — the user runs every command. Hand them the absolute-path shell command from Step 1.3 verbatim. | +| **Cursor** chat | none — ask in chat | Composer can write | terminal panel | Composer multi-file edits work for the own-agent path. Restricted mode can't write `.env` — fall back to giving the user the shell command. Cursor's terminal panel may open at the project root, which may differ from `working_dir`; confirm. | +| **Aider** | none — ask in chat | yes (its model) | shell exec | Map files into the session before patching. Aider's cwd is wherever the user invoked it from — should match `working_dir` if the user followed Step 0. | +| **Windsurf / Copilot Chat** | none — ask in chat | varies | varies | Treat as Codex-equivalent. Same cwd-mismatch risk; always use absolute paths from `working_dir`. | + +### Critical: the `.env`-vs-cwd mismatch (the single most common failure mode across clients) + +The agent you're running may have a working directory that **does not match the user's terminal cwd**. Examples: Claude Desktop's sandboxed bash has a session-mount cwd; Codex CLI starts a session in whichever folder the user invoked it from, which may not be where they intend to run the script; Cursor's terminal panel may open at the project root rather than a subfolder. If you create `.env` via your own shell tool and the user runs the script from their terminal, the two cwds may point at different folders — the script won't see the key and you'll spend time debugging a phantom "key missing" error. + +The fix is universal and already baked into Step 0 and Step 1.3, but worth restating: + +1. **Always establish `working_dir` as an absolute path in Step 0** before any file-creation step. Announce it back to the user and get explicit confirmation. +2. **Every file-creation command must use the absolute path** — never `touch .env`, always `touch "/.env"`. +3. **Whenever you verify a file exists**, check it at `/` explicitly, not at whatever your shell's current cwd happens to be. +4. **If your shell tool's cwd differs from `working_dir`**, either `cd` to `working_dir` first or pass the absolute path to every command. Don't assume the user's terminal will be in the same place as yours. If your client supports it, prefer: - A structured multi-choice question over a free-text "type a or b". Reduces parse errors. - Showing a diff before writing patched code (own-agent path) over a silent write. Users want a chance to bail. - Reading `.env` once and not echoing the key back over re-reading it in every step. Reduces leak surface. +- Absolute paths in every file-touching command, even when you "know" the cwd is right. The cost of the prefix is two seconds; the cost of debugging an `.env` in the wrong folder is ten minutes. --- @@ -662,15 +857,24 @@ The plaintext key is returned exactly once at creation (`api_key` field in the c ## 14. Verifying an install (smoke test) -When the user says "it doesn't work", before debugging anything else have them run this: +When the user says "it doesn't work", before debugging anything else have them run a minimal smoke test using **whatever LLM provider they actually have credentials for** — not necessarily OpenAI. Pick the import and constructor for their provider; everything else stays the same. ```python import asyncio, os, adrian -from langchain_openai import ChatOpenAI +# Swap this import line for the user's actual provider: +# from langchain_openai import ChatOpenAI # OpenAI +# from langchain_anthropic import ChatAnthropic # Anthropic +# from langchain_google_genai import ChatGoogleGenerativeAI # Google +# from langchain_aws import ChatBedrockConverse # AWS Bedrock +# from langchain_openai import AzureChatOpenAI # Azure OpenAI +# from langchain_ollama import ChatOllama # Ollama (local) +from langchain_openai import ChatOpenAI # example async def smoke(): assert os.environ.get("ADRIAN_API_KEY"), "ADRIAN_API_KEY missing" - assert os.environ.get("OPENAI_API_KEY"), "OPENAI_API_KEY missing" + # Assert the env var for whichever provider was chosen (OPENAI_API_KEY, + # ANTHROPIC_API_KEY, GOOGLE_API_KEY, AZURE_OPENAI_API_KEY, AWS_REGION+creds, ...). + # Ollama needs no key — skip the assert. adrian.init(log_level="DEBUG") out = await ChatOpenAI(model="gpt-4o-mini").ainvoke("say ok") print("LLM:", out.content) @@ -679,6 +883,8 @@ async def smoke(): asyncio.run(smoke()) ``` +**Do not substitute a fake / mock LLM** here either, even for diagnostic purposes — a fake LLM does not generate the LangChain callbacks Adrian listens for, so a passing fake-LLM smoke test tells you nothing about whether the real instrumentation works. + Expected: 1. Log line `Adrian v1.0.0 initialised (handlers=2, ws=ws://…)`. 2. A `LoginAck` debug line showing the resolved mode + policy snapshot. @@ -723,6 +929,23 @@ Facts: - Default ws_url = ws://localhost:8080/ws (self-host); managed users set ADRIAN_WS_URL from their dashboard. +Behavioural rules (these override default LLM tendencies): + - LLM-AGNOSTIC: ask the user which provider they want (OpenAI, + Anthropic, Google, AWS Bedrock, Azure, Ollama, Groq, Mistral, …). + Never assume OpenAI. Whatever has a langchain- package + works. + - NEVER suggest or use a fake / mock / stub LLM (no FakeListChatModel, + FakeChatModel, etc.). It generates no real callbacks, so the + instrumentation is invisible. If the user has no provider key at + all, help them get one — do not invent a workaround. + - CROSS-CONFIRM every branching decision (provider, key storage path, + next steps). Do not silently pick defaults. + - SAME STORAGE for every secret in the flow: if the Adrian key went + into .env, the provider key goes into the same .env; if the Adrian + key was inlined with a TODO marker, the provider key is inlined the + same way. + - EXPLAIN code in plain English before asking the user to run it. + Refuse to fabricate framework support that is not in §7 of the guide. When in doubt, read backend/internal/alerts/alerts.json for the exact MAD definitions or point the user at the Discord. From a8e6d5702eb30be3d76dcbede5d860932261a76f Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Fri, 22 May 2026 13:12:03 +0500 Subject: [PATCH 4/9] AI Get Started Guide V2 version --- GET_STARTED_AI_GUIDE_V2.md | 449 +++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 GET_STARTED_AI_GUIDE_V2.md diff --git a/GET_STARTED_AI_GUIDE_V2.md b/GET_STARTED_AI_GUIDE_V2.md new file mode 100644 index 0000000..fbe6ab7 --- /dev/null +++ b/GET_STARTED_AI_GUIDE_V2.md @@ -0,0 +1,449 @@ +# Adrian - Quick Start AI Guide V2 + +> For humans: this is a short setup guide you can give to an AI assistant +> such as Claude, Codex, Cursor, Windsurf, or ChatGPT. It is intentionally +> narrow: managed Adrian app only, local `.env` secrets only, and the fewest +> prompts needed to get your first event into the dashboard. +> +> For AI assistants: follow this guide in order. Keep the language simple. +> Move quickly, but do not hide what you are doing. + +--- + +## What This Guide Is Allowed To Do + +This guide is designed to be easy to review. It only asks the assistant to: + +- check Python is available +- help the user create an Adrian API key in the managed dashboard +- create or update a local `.env` file for secrets +- add `.env` to `.gitignore` if the project uses git and it is not already ignored +- create a tiny `adrian_quickstart.py` example file, or edit one existing agent file the user points to +- create a Python virtual environment and install `adrian-sdk` plus the selected LangChain provider package +- run the example or the user's agent + +This guide does not ask the assistant to delete files, upload code, change git +history, edit backend services, or paste API keys into chat. + +--- + +## Simple Rules For The Assistant + +1. Use the managed Adrian app: `https://app.adrian.secureagentics.ai`. +2. Store secrets in `.env` only. Do not ask the user to paste API keys into chat. +3. Do not write API keys directly into Python files. +4. Use a real LLM provider. Do not use fake, mock, or stub LLMs. +5. Pause only when user input is actually needed. +6. Explain file changes in one short paragraph before making them. +7. If a key has already been pasted into chat or hardcoded in a file, recommend + revoking it and creating a fresh key stored in `.env`. + +Planned pause points: + +1. Ask the user to confirm they have copied their Adrian API key from the + dashboard. +2. Ask whether to use the Simple Example Agent or integrate an existing agent. +3. If using the Simple Example Agent, ask which LLM provider to use. +4. Ask the user to confirm `.env` is filled in before running. + +Do not add extra confirmation prompts unless something is unclear or risky. + +--- + +## Step 0 - Check The Folder And Python + +Run these in the current folder: + +```sh +pwd +python3 --version +python3 -m pip --version +``` + +If Python is older than 3.12, stop and ask the user to install Python 3.12 or +newer. Adrian's SDK requires Python `>=3.12`. + +Tell the user the absolute folder path. Example: + +> I will set Adrian up in `/absolute/path/to/project`. The `.env`, +> virtual environment, and quickstart file will live here. + +--- + +## Step 1 - Get The Adrian API Key + +Ask the user to open the managed dashboard: + +`https://app.adrian.secureagentics.ai` + +If this is their first time signing in: + +1. Sign up with Google, Microsoft, or GitHub. +2. Follow the first-time onboarding until Adrian shows an API key. +3. Copy the API key. It starts with `adr_live_`. +4. Skip detailed agent configuration for now. The quickstart only needs the API + key. The SDK handles the live Adrian connection automatically. + +If they already have an account: + +1. Go to **Configurations**. +2. Open the agent/API key area. +3. Create or copy an agent API key. + +Tell the user: + +> Keep the key copied somewhere local for the next step. Do not paste it into +> this chat. We will put it into `.env` on your machine. + +Pause here and ask: + +> Do you have your Adrian API key ready? + +--- + +## Step 2 - Create `.env` + +Create a local `.env` file in the working folder with placeholders. Use the +absolute path when creating or opening the file. + +If the project has a `.gitignore`, make sure it contains: + +```gitignore +.env +``` + +Template: + +```env +ADRIAN_API_KEY=adr_live_replace_this +LLM_PROVIDER=openai +``` + +`LLM_PROVIDER` is only used by the Simple Example Agent. The user can leave it +as `openai` or change it in Step 4A. + +Ask the user to fill in the real values locally in their editor. Do not ask them +to paste secrets into chat. + +Before running anything, verify without printing full secrets: + +- `ADRIAN_API_KEY` exists and starts with `adr_live_` +- there are no quote marks around the values +- there are no placeholder values left + +If the assistant can read files, it may check `.env` directly but must not echo +the key back to the chat. + +--- + +## Step 3 - Choose The Agent To Run + +Ask: + +> Adrian needs an agent to monitor. Do you want to: +> +> **A. Simple Example Agent** - use a tiny example agent that asks an LLM: +> "In one sentence, why is the sky blue?" Fastest route to your first event. +> +> **B. Integrate Adrian with one of my existing agents** - point me at your +> LangChain or LangGraph agent and I will integrate Adrian with it. + +If the user chooses A, continue to Step 4A. + +If the user chooses B, continue to Step 4B. + +--- + +## Step 4A - Simple Example Agent + +Ask: + +> In order to set up the example agent, you need to provide an LLM. Pick your +> preference: +> +> **a. OpenAI** - needs `OPENAI_API_KEY`; package `langchain-openai` +> **b. Anthropic** - needs `ANTHROPIC_API_KEY`; package `langchain-anthropic` +> **c. Google Gemini** - needs `GOOGLE_API_KEY`; package `langchain-google-genai` +> **d. Azure OpenAI** - needs `AZURE_OPENAI_API_KEY`, +> `AZURE_OPENAI_ENDPOINT`, and `AZURE_OPENAI_DEPLOYMENT`; package +> `langchain-openai` +> **e. Ollama** - no API key; usually runs locally at +> `http://localhost:11434`; package `langchain-ollama` + +For the chosen provider, add only that provider's values to `.env`. + +Examples: + +```env +# OpenAI +LLM_PROVIDER=openai +OPENAI_API_KEY=sk_replace_this + +# Anthropic +LLM_PROVIDER=anthropic +ANTHROPIC_API_KEY=sk-ant-replace-this + +# Google Gemini +LLM_PROVIDER=google +GOOGLE_API_KEY=replace_this + +# Azure OpenAI +LLM_PROVIDER=azure +AZURE_OPENAI_API_KEY=replace_this +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com +AZURE_OPENAI_DEPLOYMENT=your-deployment-name + +# Ollama +LLM_PROVIDER=ollama +OLLAMA_MODEL=llama3.2 +``` + +For Ollama, check whether it is running: + +```sh +curl http://localhost:11434/api/tags +``` + +If Ollama is not running, ask the user to run: + +```sh +ollama serve +ollama pull llama3.2 +``` + +### Install + +Create a virtual environment and install the SDK plus the provider package: + +```sh +python3 -m venv .venv +source .venv/bin/activate +pip install adrian-sdk langchain-openai +``` + +Replace `langchain-openai` with the package for the chosen provider. + +### Create `adrian_quickstart.py` + +Before writing the file, say: + +> I am going to create a tiny example agent. It initializes Adrian, asks an LLM +> "In one sentence, why is the sky blue?", prints the answer, and sends the +> event to your Adrian dashboard. + +Use this file: + +```python +import asyncio +import os +import sys + +import adrian + + +def require_env(name: str) -> str: + value = os.environ.get(name) + if not value: + sys.exit(f"{name} is missing. Add it to .env and run again.") + return value + + +def build_llm(): + provider = os.environ.get("LLM_PROVIDER", "openai").strip().lower() + + if provider == "openai": + require_env("OPENAI_API_KEY") + from langchain_openai import ChatOpenAI + + return ChatOpenAI(model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini")) + + if provider == "anthropic": + require_env("ANTHROPIC_API_KEY") + from langchain_anthropic import ChatAnthropic + + return ChatAnthropic( + model=os.environ.get("ANTHROPIC_MODEL", "claude-3-5-haiku-latest") + ) + + if provider == "google": + require_env("GOOGLE_API_KEY") + from langchain_google_genai import ChatGoogleGenerativeAI + + return ChatGoogleGenerativeAI( + model=os.environ.get("GOOGLE_MODEL", "gemini-1.5-flash") + ) + + if provider == "azure": + require_env("AZURE_OPENAI_API_KEY") + require_env("AZURE_OPENAI_ENDPOINT") + deployment = require_env("AZURE_OPENAI_DEPLOYMENT") + from langchain_openai import AzureChatOpenAI + + return AzureChatOpenAI( + azure_deployment=deployment, + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"), + ) + + if provider == "ollama": + from langchain_ollama import ChatOllama + + return ChatOllama( + model=os.environ.get("OLLAMA_MODEL", "llama3.2"), + base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"), + ) + + sys.exit( + "Unsupported LLM_PROVIDER. Use openai, anthropic, google, azure, or ollama." + ) + + +async def main() -> int: + require_env("ADRIAN_API_KEY") + + adrian.init() + llm = build_llm() + response = await llm.ainvoke("In one sentence, why is the sky blue?") + print(response.content) + adrian.shutdown() + return 0 + + +if __name__ == "__main__": + raise SystemExit(asyncio.run(main())) +``` + +### Run + +Ask the user to confirm `.env` is filled in. Then run: + +```sh +set -a +. ./.env +set +a +python adrian_quickstart.py +``` + +If it prints an answer, say: + +> Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you +> should see the event within a few seconds. + +If the event does not appear, go to "If Anything Goes Wrong" below. + +--- + +## Step 4B - Integrate Adrian With An Existing Agent + +Ask: + +> Please give me the absolute path to your LangChain or LangGraph agent file. +> If your assistant cannot read outside this folder, copy the agent file into +> this project and point me at that copy. + +Read the file and check: + +- it is Python +- it imports or uses LangChain or LangGraph +- it has an entry point such as `main()`, `async def main()`, or code that calls + `.invoke()`, `.ainvoke()`, or `.astream()` + +Before editing, say: + +> I will integrate Adrian with this agent by importing `adrian`, initializing it +> near the start of the agent run, and keeping secrets in `.env`. + +Patch the file: + +1. Add `import adrian` near the imports. +2. Add this before the model, chain, or graph is created or called: + +```python +adrian.init() +``` + +If the agent has a clear shutdown path, add: + +```python +adrian.shutdown() +``` + +If it is a long-running app, mention that `adrian.shutdown()` should be called +from the app's normal shutdown hook. + +Install the SDK in the existing environment: + +```sh +pip install adrian-sdk +``` + +Run the agent the way the user normally runs it, after loading `.env`: + +```sh +set -a +. ./.env +set +a +# then run the user's normal agent command +``` + +If the agent runs, say: + +> Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you +> should see events within a few seconds. + +--- + +## If Anything Goes Wrong + +Keep troubleshooting short. Check these first: + +### Python is too old + +Adrian requires Python `>=3.12`. + +### A secret is missing + +Make sure `.env` contains: + +```env +ADRIAN_API_KEY=... +``` + +For the Simple Example Agent, it also needs the selected provider key, unless +the provider is Ollama. + +### The dashboard has no event + +Check: + +- the key starts with `adr_live_` +- the script was run after loading `.env` +- `events.jsonl` exists locally, which means Adrian captured the event + +### The LLM provider fails + +Check the provider key and package: + +- OpenAI: `OPENAI_API_KEY`, `langchain-openai` +- Anthropic: `ANTHROPIC_API_KEY`, `langchain-anthropic` +- Google Gemini: `GOOGLE_API_KEY`, `langchain-google-genai` +- Azure OpenAI: Azure key, endpoint, deployment, `langchain-openai` +- Ollama: `ollama serve`, local model pulled, `langchain-ollama` + +### A key was pasted into chat or hardcoded + +Recommend this cleanup: + +1. Revoke that key in the Adrian dashboard or provider dashboard. +2. Create a fresh key. +3. Store the fresh key only in `.env`. +4. Remove hardcoded keys from Python files. + +--- + +## Final Success Message + +When setup works, keep the final message simple: + +> Everything worked. Adrian is now receiving events from your agent. Your local +> secrets are in `.env`, your local event copy is in `events.jsonl`, and the +> dashboard event feed is at `https://app.adrian.secureagentics.ai/events`. From 03f7b54f521a4ff56af22f65bfb0f96042db2c8a Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Fri, 22 May 2026 15:08:51 +0500 Subject: [PATCH 5/9] Updated v2.1 --- GET_STARTED_AI_GUIDE_V2.md | 86 +++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/GET_STARTED_AI_GUIDE_V2.md b/GET_STARTED_AI_GUIDE_V2.md index fbe6ab7..c41fad8 100644 --- a/GET_STARTED_AI_GUIDE_V2.md +++ b/GET_STARTED_AI_GUIDE_V2.md @@ -37,14 +37,18 @@ history, edit backend services, or paste API keys into chat. 6. Explain file changes in one short paragraph before making them. 7. If a key has already been pasted into chat or hardcoded in a file, recommend revoking it and creating a fresh key stored in `.env`. +8. When editing `.env`, stop immediately after opening the file. Wait for the + user to confirm they saved it before installing packages, creating scripts, + or running anything. Planned pause points: 1. Ask the user to confirm they have copied their Adrian API key from the dashboard. -2. Ask whether to use the Simple Example Agent or integrate an existing agent. -3. If using the Simple Example Agent, ask which LLM provider to use. -4. Ask the user to confirm `.env` is filled in before running. +2. Ask the user to save the Adrian API key in `.env`. +3. Ask whether to use the Simple Example Agent or integrate an existing agent. +4. If using the Simple Example Agent, ask which LLM provider to use. +5. Ask the user to save the selected provider values in `.env`. Do not add extra confirmation prompts unless something is unclear or risky. @@ -103,8 +107,8 @@ Pause here and ask: ## Step 2 - Create `.env` -Create a local `.env` file in the working folder with placeholders. Use the -absolute path when creating or opening the file. +Create and open a local `.env` file in the working folder. Use the absolute path +when creating or opening the file so the user knows exactly where it lives. If the project has a `.gitignore`, make sure it contains: @@ -112,20 +116,41 @@ If the project has a `.gitignore`, make sure it contains: .env ``` -Template: +Use the command for the user's operating system. Replace +`/absolute/path/to/project` or `C:\absolute\path\to\project` with the working +folder from Step 0. + +```sh +# macOS +touch "/absolute/path/to/project/.env" && open -a TextEdit "/absolute/path/to/project/.env" +``` + +```sh +# Linux +touch "/absolute/path/to/project/.env" && ${EDITOR:-nano} "/absolute/path/to/project/.env" +``` + +```powershell +# Windows PowerShell +New-Item -ItemType File -Force "C:\absolute\path\to\project\.env"; notepad.exe "C:\absolute\path\to\project\.env" +``` + +Ask the user to paste this into the file, replacing the placeholder with their +real Adrian API key: ```env ADRIAN_API_KEY=adr_live_replace_this -LLM_PROVIDER=openai ``` -`LLM_PROVIDER` is only used by the Simple Example Agent. The user can leave it -as `openai` or change it in Step 4A. +Then say: -Ask the user to fill in the real values locally in their editor. Do not ask them -to paste secrets into chat. +> Save `.env`, close the editor, and come back here when you are done. Do not +> paste the key into chat. -Before running anything, verify without printing full secrets: +Stop here. Do not continue to agent selection until the user confirms the file +is saved. + +After they confirm, verify without printing full secrets: - `ADRIAN_API_KEY` exists and starts with `adr_live_` - there are no quote marks around the values @@ -170,9 +195,11 @@ Ask: > **e. Ollama** - no API key; usually runs locally at > `http://localhost:11434`; package `langchain-ollama` -For the chosen provider, add only that provider's values to `.env`. +For the chosen provider, open `.env` again using the same command style from +Step 2. Ask the user to add only that provider's values below the existing +Adrian key. -Examples: +Use the matching block: ```env # OpenAI @@ -198,6 +225,20 @@ LLM_PROVIDER=ollama OLLAMA_MODEL=llama3.2 ``` +Then say: + +> Save `.env`, close the editor, and come back here when you are done. Do not +> paste the provider key into chat. + +Stop here. Do not install packages or create `adrian_quickstart.py` until the +user confirms `.env` is saved. + +After they confirm, verify without printing full secrets: + +- `LLM_PROVIDER` is one of `openai`, `anthropic`, `google`, `azure`, or `ollama` +- the selected provider's required values exist +- there are no placeholder values left for the selected provider + For Ollama, check whether it is running: ```sh @@ -314,7 +355,7 @@ if __name__ == "__main__": ### Run -Ask the user to confirm `.env` is filled in. Then run: +Load `.env` and run the example: ```sh set -a @@ -326,7 +367,8 @@ python adrian_quickstart.py If it prints an answer, say: > Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you -> should see the event within a few seconds. +> should see the event within a few seconds. Then use the final success message +> below. If the event does not appear, go to "If Anything Goes Wrong" below. @@ -388,7 +430,8 @@ set +a If the agent runs, say: > Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you -> should see events within a few seconds. +> should see events within a few seconds. Then use the final success message +> below. --- @@ -444,6 +487,9 @@ Recommend this cleanup: When setup works, keep the final message simple: -> Everything worked. Adrian is now receiving events from your agent. Your local -> secrets are in `.env`, your local event copy is in `events.jsonl`, and the -> dashboard event feed is at `https://app.adrian.secureagentics.ai/events`. +> Everything worked! You have just integrated a security monitoring system +> around an agent. Every action that agent takes can now appear in your Adrian +> dashboard, where Adrian assesses whether the action looks safe or dangerous. +> You can use **Configurations** to give Adrian more context about the agent it +> is monitoring, choose whether to block dangerous actions or only receive +> alerts, and set up alerting through Discord or Slack. From 33cf6919fe364ec3c2fa4966ba5254dbd3e1a036 Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Fri, 22 May 2026 15:30:30 +0500 Subject: [PATCH 6/9] Clean latest version of AI guide v2.2 --- GET_STARTED_AI_GUIDE_V2.md | 52 ++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/GET_STARTED_AI_GUIDE_V2.md b/GET_STARTED_AI_GUIDE_V2.md index c41fad8..e1a8f39 100644 --- a/GET_STARTED_AI_GUIDE_V2.md +++ b/GET_STARTED_AI_GUIDE_V2.md @@ -107,8 +107,9 @@ Pause here and ask: ## Step 2 - Create `.env` -Create and open a local `.env` file in the working folder. Use the absolute path -when creating or opening the file so the user knows exactly where it lives. +Create a local `.env` file in the working folder, write the Adrian placeholder +line into it, then open it for the user. Use the absolute path when creating or +opening the file so the user knows exactly where it lives. If the project has a `.gitignore`, make sure it contains: @@ -116,27 +117,40 @@ If the project has a `.gitignore`, make sure it contains: .env ``` -Use the command for the user's operating system. Replace +Use the command for the user's operating system. These commands put the +placeholder in the file before the editor opens, so the user does not need to +copy anything from chat and overwrite the API key on their clipboard. Replace `/absolute/path/to/project` or `C:\absolute\path\to\project` with the working folder from Step 0. +If the assistant can run shell commands, it should run this command itself so +the user's clipboard can keep the Adrian API key. + ```sh # macOS -touch "/absolute/path/to/project/.env" && open -a TextEdit "/absolute/path/to/project/.env" +ENV_FILE="/absolute/path/to/project/.env" +grep -q '^ADRIAN_API_KEY=' "$ENV_FILE" 2>/dev/null || printf 'ADRIAN_API_KEY=adr_live_replace_this\n' >> "$ENV_FILE" +open -a TextEdit "$ENV_FILE" ``` ```sh # Linux -touch "/absolute/path/to/project/.env" && ${EDITOR:-nano} "/absolute/path/to/project/.env" +ENV_FILE="/absolute/path/to/project/.env" +grep -q '^ADRIAN_API_KEY=' "$ENV_FILE" 2>/dev/null || printf 'ADRIAN_API_KEY=adr_live_replace_this\n' >> "$ENV_FILE" +${EDITOR:-nano} "$ENV_FILE" ``` ```powershell # Windows PowerShell -New-Item -ItemType File -Force "C:\absolute\path\to\project\.env"; notepad.exe "C:\absolute\path\to\project\.env" +$envFile = "C:\absolute\path\to\project\.env" +if (!(Test-Path $envFile) -or -not (Select-String -Path $envFile -Pattern '^ADRIAN_API_KEY=' -Quiet)) { + Add-Content -Path $envFile -Value 'ADRIAN_API_KEY=adr_live_replace_this' +} +notepad.exe $envFile ``` -Ask the user to paste this into the file, replacing the placeholder with their -real Adrian API key: +Ask the user to replace the placeholder in the file with their real Adrian API +key: ```env ADRIAN_API_KEY=adr_live_replace_this @@ -195,11 +209,14 @@ Ask: > **e. Ollama** - no API key; usually runs locally at > `http://localhost:11434`; package `langchain-ollama` -For the chosen provider, open `.env` again using the same command style from -Step 2. Ask the user to add only that provider's values below the existing -Adrian key. +For the chosen provider, write exactly one matching provider block into `.env` +before opening the editor. Do not ask the user to copy the block from chat. If a +provider block already exists, update it instead of adding duplicates. + +Then open `.env` again using the same editor style from Step 2, and ask the +user to replace the provider placeholder with their real provider key. -Use the matching block: +Use only the matching block: ```env # OpenAI @@ -367,8 +384,15 @@ python adrian_quickstart.py If it prints an answer, say: > Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you -> should see the event within a few seconds. Then use the final success message -> below. +> should see the event within a few seconds. + +Then ask: + +> Want me to integrate Adrian with one of your real agents now? If yes, send me +> the path to your LangChain or LangGraph agent file. If not, you can stop here. + +If the user says yes or sends a file path, continue to Step 4B. If the user +stops, use the final success message below. If the event does not appear, go to "If Anything Goes Wrong" below. From 5a45cfc5b03f352d36890e32cf46411595bacc31 Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Mon, 25 May 2026 13:15:46 +0500 Subject: [PATCH 7/9] AI Guide Fully working with new agent example and existing agents integrations --- GET_STARTED_AI_GUIDE.md | 1145 ++++++++++++------------------------ GET_STARTED_AI_GUIDE_V2.md | 519 ---------------- README.md | 4 +- 3 files changed, 367 insertions(+), 1301 deletions(-) delete mode 100644 GET_STARTED_AI_GUIDE_V2.md diff --git a/GET_STARTED_AI_GUIDE.md b/GET_STARTED_AI_GUIDE.md index f41860b..4aa2a75 100644 --- a/GET_STARTED_AI_GUIDE.md +++ b/GET_STARTED_AI_GUIDE.md @@ -1,956 +1,539 @@ -# Adrian — AI Agent Setup Guide +# Adrian - Quick Start AI Guide V2 -> **For human developers:** drop this file into your AI assistant's context (Claude, Codex, Cursor, Aider, Windsurf, Copilot Chat, ChatGPT, etc.) and ask it to set up Adrian. The agent walks you through a 60-second flow: API key → install → first event in the dashboard. Everything below **Part 1** is reference material the agent reads when something goes wrong. +> For humans: this is a short setup guide you can give to an AI assistant +> such as Claude, Codex, Cursor, Windsurf, or ChatGPT. It is intentionally +> narrow: managed Adrian app only, local `.env` secrets only, and the fewest +> prompts needed to get your first event into the dashboard. > -> **For AI agents:** if a user has just pointed you at this file (or at `github.com/secureagentics/Adrian`) and asked for help setting up, execute **Part 1** below in order. Don't skim. Don't suggest a different flow. The single success criterion is "first event visible in the dashboard within 60 seconds". Treat Part 2 as a lookup index for failure modes. +> For AI assistants: follow this guide in order. Keep the language simple. +> Move quickly, but do not hide what you are doing. --- -# PART 1 — 60-Second Setup Flow (executable) +## What You Are Setting Up -> **AI agent: this section is a program, not documentation. Run it top-to-bottom. Branch only at the marked decision points. If a step fails, jump to the matching subsection in Part 2 and resume.** +Start by telling the user: -### Ground rules for the AI agent (read once, apply throughout) +> You are setting up Adrian, a security monitoring system for AI agents. To do +> this, we will get an API key from the Adrian dashboard and save it to a local +> `.env` file on your machine. Then we will run a simple example agent, or +> integrate Adrian with one of your real agents. -These rules override your normal defaults for this flow. Breaking any of them is the difference between a 60-second success and a frustrated user who has to redo the setup. - -1. **Cross-confirm every decision with the user. Never silently pick a default.** Every time the flow branches — LLM provider, where to store an API key, what to do after the smoke test, whether to instrument an existing project — surface the choices to the user and wait for their answer. Do not assume OpenAI, do not assume `.env`, do not assume "yes, keep going". Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` on Claude, otherwise list the options in chat and wait). -2. **Do not offer or use a fake / mock / stub LLM under any circumstance.** A fake LLM does not produce real tool calls or model events, which means Adrian has nothing meaningful to capture, which means the smoke test "succeeds" while teaching the user nothing about how Adrian works. If the user has no LLM provider credentials at all, stop and help them get one (link to OpenAI / Anthropic / Google / Bedrock / Ollama install) before proceeding. Do not invent a path around this. -3. **Be LLM-agnostic.** Adrian instruments anything that runs through LangChain / LangGraph. Ask the user which provider they want to use rather than assuming. Supported provider integrations include (non-exhaustive): OpenAI, Anthropic, Google (Gemini / Vertex), AWS Bedrock, Azure OpenAI, Ollama (local models), Groq, Mistral, Together, Fireworks, Cohere, HuggingFace endpoints. If the user names something not on this list, ask for the LangChain package name (`langchain-`) — most providers have one. -4. **Apply the same storage preference to every secret in the flow.** The decision the user made about the Adrian API key in Step 1.2 (`key_source ∈ {"env", "inline_user", "inline_agent"}`) governs how you treat their LLM provider key in Step 3 too. Do not mix paths — that is what produces hardcoded keys sitting next to a half-populated `.env`. -5. **Explain code before you ask the user to run it.** Every script or patched file you produce must be preceded by a one-paragraph description in plain English of what it will do (which model it will call, what prompt it will send, what side effects it has, what it writes to disk). Users should never paste-and-run code they don't understand the shape of. -6. **At every named "Step N" boundary, summarise what just happened and ask if it's OK to proceed.** Especially after the smoke test runs — do not auto-jump to "configure webhooks now". Present the structured options in Step 5 and let the user pick. - -## Step 0 — Confirm preconditions and establish the working directory (5 seconds) - -Run these checks. Either shell out yourself or ask the user to run the commands and paste output. - -| Requirement | How to check | -|---|---| -| Python ≥ 3.12 | `python3 --version` | -| pip available | `python3 -m pip --version` | -| Absolute working directory path | `pwd` (macOS / Linux / Git Bash / PowerShell), `cd` (Windows cmd) | -| Network access to PyPI + `app.adrian.secureagentics.ai` | implicit; flag if obviously blocked | - -If Python < 3.12, stop and ask the user to install 3.12 (or use `pyenv install 3.12`). Don't proceed — the SDK declares `requires-python = ">=3.12"` and will fail to install. - -**Then — before moving to Step 1 — announce the absolute working directory back to the user, and store it for the rest of the flow.** This is the single most common source of confusion later (the `.env` file ends up in a different folder than the user expected). Get it straight before any files exist. - -> Your working directory is `/Users/you/myproject`. Every file we create — `.env`, `adrian_quickstart.py`, the virtual env — will live here. Sound right? If you wanted to be in a different folder, `cd` there now and let me know the new path before we continue. - -Wait for confirmation, or for the user to give you a different path. Then store this value as an internal variable `working_dir = ""` and reference it whenever you generate a file-creation command in later steps. Never tell the user to "create a file in your project directory" without naming the actual absolute path. - -## Step 1 — Get the user's Adrian API key (15 seconds) - -This step has three substeps. Don't skip the verification at the end — Steps 3 and 4 assume you've already confirmed a valid key is in place. - -### 1.1 Walk the user through generating the key - -Tell the user this, click-by-click. Don't paraphrase — the user is on a 60-second clock and a sentence-long "go get a key" leaves them hunting menus. - -> 1. Open **https://app.adrian.secureagentics.ai** in your browser. -> 2. Click **Sign up** (top right) and choose Google, Microsoft, or GitHub SSO. About 30 seconds. -> 3. Once you're in the dashboard, go to **Settings → Agents** (left sidebar). -> 4. Click **New key**. Give it any label (e.g. "60-second setup") and click **Generate**. -> 5. Copy the key. It starts with `adr_live_…`. The dashboard shows it **once** — if you close the modal without copying, you'll need to issue a new one. -> 6. Keep the key on your clipboard — I'll ask you next where you want to store it. - -Don't pause for a separate "do you have it?" confirmation. Go straight to 1.2; the user's answer to the storage question in 1.2 is also the signal that they have the key. - -### 1.2 Ask the user how they want to store the key (upfront 3-option choice) - -Don't pick a storage path on the user's behalf — present all three options at once and let them pick. Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` on Claude); otherwise list them in chat numbered 1 / 2 / 3 and wait for the user's pick. - -> You've got your Adrian API key. Where would you like to store it? Pick one — same answer will apply to any other API keys we need later (e.g. for an LLM provider) so we stay consistent. -> -> **1. Save it to a `.env` file in your project directory (recommended).** Most flexible and most secure: the script reads the key from the environment, the file never goes to git, and you can rotate the key by editing one line. The key never passes through this chat. Best default for almost everyone. -> -> **2. Add it to the code yourself.** I'll write the script with a `PASTE_YOUR_KEY_HERE` placeholder; you replace it with your real key in your editor before running. The key never passes through this chat. Pick this if you don't want your key transiting the chat context (e.g. on Codex web / ChatGPT) or if you just prefer to handle secrets manually. -> -> **3. Paste it to me in chat and I'll embed it in the script.** Fastest path; everything runs in one shot. The key will sit in this chat history. I'll add a `# TODO: move to .env` comment so future-you can clean it up later. Pick this only if you trust this chat with your key. - -Wait for the user's answer. Set the internal flag `key_source` and proceed to 1.3: - -- Option **1** → `key_source = "env"` -- Option **2** → `key_source = "inline_user"` (you'll leave a placeholder in the script; the user replaces it in their editor) -- Option **3** → `key_source = "inline_agent"` (the user pastes the key to you in chat; you embed it literally in the script) - -**A few rules that apply to every option:** - -- Never echo the full key back in any later message. Refer to it as `adr_live_…` from here on. If you need to mention it in a log line, redact past the prefix. -- The same option will govern any other secrets in this flow (e.g. the LLM provider key in Step 3A). Don't mix paths — if Adrian's key went into `.env`, the provider key goes into the same `.env`; if Adrian's key is inline in the script, the provider key is inline the same way. -- If the user picks option 2, do **not** also ask them to paste the key — the whole point of option 2 is that the key never enters this chat. -- If the user picks option 3, you don't need a separate "warn before inlining" step — by picking option 3 they've already consented. Just confirm receipt of the key (without echoing it) and move on. - -### 1.3 Verify before moving on - -This is a hard gate. Step 2 starts only after the verification path matching the user's option in 1.2 passes. - -**If `key_source = "env"` (option 1, `.env` file):** - -Give the user a single copy-pasteable command that creates the `.env` file at the **absolute path** you stored in Step 0 as `working_dir`, then opens it for editing. Don't tell them to "create a file in your project directory" — that's the wording that produced the file-in-the-wrong-place failure in early demos. Use the absolute path and pick the command for their OS. - -Example wording (substitute `working_dir` with the actual absolute path, e.g. `/Users/you/myproject`): +--- -> Run **one** of these in your terminal — whichever matches your OS. The path is the absolute path to your working directory, so the file ends up exactly where the script will look for it later. -> -> **macOS:** -> ```sh -> touch "/Users/you/myproject/.env" && open -t "/Users/you/myproject/.env" -> ``` -> -> **Linux (any editor):** -> ```sh -> touch "/Users/you/myproject/.env" && "${EDITOR:-nano}" "/Users/you/myproject/.env" -> ``` -> -> **Windows PowerShell:** -> ```powershell -> New-Item -ItemType File -Force "C:\Users\you\myproject\.env"; notepad "C:\Users\you\myproject\.env" -> ``` -> -> **Windows cmd:** -> ```cmd -> type nul > "C:\Users\you\myproject\.env" && notepad "C:\Users\you\myproject\.env" -> ``` -> -> When the editor opens, paste this single line (with your actual key substituted for `adr_live_…`), save the file, and close the editor: -> -> ``` -> ADRIAN_API_KEY=adr_live_… -> ``` -> -> Type **done** when the file is saved. +## What This Guide Is Allowed To Do -When they confirm, verify: +This guide is designed to be easy to review. It only asks the assistant to: -1. Confirm `.env` exists at `working_dir`. If your client can read files, read `/.env` and check. If not, ask the user: - > Quick check — can you run `cat "/.env"` (or `type "\.env"` on Windows) and paste the output? I want to make sure the key landed at the right path. -2. Confirm the line `ADRIAN_API_KEY=…` is present and the value: - - Starts with `adr_live_` (managed cloud) or `adr_local_` (self-hosted). - - Has no surrounding quotes — `ADRIAN_API_KEY="adr_live_xxx"` works in most shells but tripped some users; if you see quotes, ask the user to remove them. - - Has no surrounding whitespace — a trailing space is a common paste artifact and will fail the auth check silently later. -3. If the format is wrong, point at the exact issue and ask the user to fix it. Most common: the key was truncated during copy (Discord and Slack sometimes trim long strings on send-from-mobile). -4. If `.env` is not at `working_dir` but exists somewhere else on the user's machine (e.g. the user's home directory or the Adrian repo), do **not** silently accept it — that's the bug we're guarding against. Tell the user the path mismatch explicitly and re-run the create command at `working_dir`. -5. If everything looks good, tell the user: - > Key verified in `/.env`. I'll read it from the environment in the code I'm about to write. +- check Python is available +- help the user create an Adrian API key in the managed dashboard +- create or update a local `.env` file for secrets +- add `.env` to `.gitignore` if the project uses git and it is not already ignored +- create a tiny `adrian_quickstart.py` example file, or edit one existing agent file the user points to +- create a Python virtual environment and install `adrian-sdk` plus the selected LangChain provider package +- run the example or the user's agent -**If `key_source = "inline_user"` (option 2, user will edit the script):** +This guide does not ask the assistant to delete files, upload code, change git +history, edit backend services, or paste API keys into chat. -There's no value for you to verify yet — the user will paste the key into the script file in Step 3A.3 (or into their own agent file in Step 3B.3), where you'll leave a `adr_live_PASTE_YOUR_KEY_HERE` placeholder for them. +--- -Just confirm the plan back to the user: +## Simple Rules For The Assistant -> Got it. I'll write the script with a `PASTE_YOUR_KEY_HERE` placeholder. Before running it, you'll replace that placeholder with your actual key in your editor. I'll remind you again at the right moment. +1. Use the managed Adrian app: `https://app.adrian.secureagentics.ai`. +2. Store secrets in `.env` only. Do not ask the user to paste API keys into chat. +3. Do not write API keys directly into Python files. +4. Use a real LLM provider. Do not use fake, mock, or stub LLMs. +5. Pause only when user input is actually needed. +6. Explain file changes in one short paragraph before making them. +7. If a key has already been pasted into chat or hardcoded in a file, recommend + revoking it and creating a fresh key stored in `.env`. +8. When editing `.env`, stop immediately after opening the file. Wait for the + user to confirm they saved it before installing packages, creating scripts, + or running anything. -Then proceed to Step 2. Set a reminder for yourself to: -- Use `adr_live_PASTE_YOUR_KEY_HERE` as the literal in any generated code. -- Pause before Step 3A.4 / 3B.6 (the "run it" step) to remind the user to fill in the placeholder. +Planned pause points: -**If `key_source = "inline_agent"` (option 3, user pastes key to you):** +1. Ask the user to confirm they have copied their Adrian API key from the + dashboard. +2. Ask the user to save the Adrian API key in `.env`. +3. Ask whether to use the Simple Example Agent or integrate an existing agent. +4. If using the Simple Example Agent, ask which LLM provider to use. +5. Ask the user to save the selected provider values in `.env`. -Ask the user to paste the key now. Then: +Do not add extra confirmation prompts unless something is unclear or risky. -1. Confirm the pasted value matches `^adr_(live|local)_[0-9a-f]+$`. If not, ask the user to re-copy from the dashboard — they probably grabbed the prefix or a nearby string by mistake. -2. Store the key in your working memory for this session only. **Do not write it to disk yet** — you'll embed it in the script in Step 3. -3. Tell the user (do not include the key value): - > Key received. Format looks valid. Moving on — I'll embed it into the script with a `# TODO: move to .env` marker. +--- -**Summary of the flag values the rest of this flow reads:** +## Step 0 - Check The Folder And Python -- `key_source = "env"` → in Steps 3A and 3B, leave `adrian.init()` argument-less and rely on `ADRIAN_API_KEY` from the environment. -- `key_source = "inline_user"` → in Steps 3A and 3B, generate `adrian.init(api_key="adr_live_PASTE_YOUR_KEY_HERE")` with a placeholder and a `# TODO: replace placeholder, then move to a .env file before committing` comment above the line. Remind the user to fill the placeholder before running. -- `key_source = "inline_agent"` → in Steps 3A and 3B, generate `adrian.init(api_key="adr_live_…")` with the literal value the user pasted, plus the same `# TODO: move to a .env file before committing` comment. +Run these in the current folder: -The two `inline_*` paths produce structurally identical scripts — the only difference is whether you fill in the literal key (`inline_agent`) or leave a placeholder for the user to fill in (`inline_user`). +```sh +pwd +python3 --version +python3 -m pip --version +``` -## Step 2 — Decision point: test agent or own agent? (5 seconds) +If Python is older than 3.12, stop and ask the user to install Python 3.12 or +newer. Adrian's SDK requires Python `>=3.12`. -Ask the user this question. Use whichever tool you have: +Tell the user the absolute folder path. Example: -- **If you have a structured question tool** (e.g. Claude's `AskUserQuestion`): use it with the two options below. -- **If you don't** (Codex, Aider, plain chat): ask in chat and wait for "a" or "b" or the option name. +> I will set Adrian up in `/absolute/path/to/project`. The `.env`, +> virtual environment, and quickstart file will live here. -> Two ways to see Adrian in action: -> -> **(A) Test agent** — I write a tiny LangChain script that calls a real LLM once and prints the response. You watch the event show up in the dashboard. Fastest path. **Requires real credentials for some LLM provider** — OpenAI, Anthropic, Google (Gemini / Vertex), AWS Bedrock, Azure OpenAI, Ollama running locally, Groq, Mistral, or any other provider with a `langchain-…` integration. I'll ask which one you'd like to use. -> -> **(B) Your agent** — Tell me the path to your existing LangChain / LangGraph file and I add two lines so Adrian instruments it. Then run your agent as you normally would. +--- -If the user picks (A) but does not currently have credentials for any LLM provider, **stop and help them get one** before continuing — do not substitute a fake / mock / stub LLM. A fake LLM produces no real model events for Adrian to capture, so the smoke test would silently teach the user the wrong mental model. Point them at the cheapest viable option for their situation: Ollama for "I don't want to pay anything", OpenAI / Anthropic free tiers for "I'll sign up now", AWS Bedrock for "I already use AWS", etc. Then resume Step 3A once they have a key. +## Step 1 - Get The Adrian API Key -Branch on the answer. +Tell the user: -## Step 3A — Test agent path (35 seconds) +> **Please open the Adrian dashboard now:** +> `https://app.adrian.secureagentics.ai` -**Substep 3A.1 — Ask the user which LLM provider to use.** Do not pick one for them. Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` for Claude); otherwise list these options in chat and wait for the user's answer. +If this is their first time signing in: -> Which LLM provider should the smoke-test script call? Pick whichever you already have credentials for: -> -> **a. OpenAI** — needs `OPENAI_API_KEY` (starts with `sk-…`). LangChain package: `langchain-openai`. -> **b. Anthropic** — needs `ANTHROPIC_API_KEY` (starts with `sk-ant-…`). LangChain package: `langchain-anthropic`. -> **c. Google (Gemini)** — needs `GOOGLE_API_KEY`. LangChain package: `langchain-google-genai`. -> **d. AWS Bedrock** — uses your existing AWS credentials chain (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION`, or `~/.aws/credentials`). LangChain package: `langchain-aws`. -> **e. Azure OpenAI** — needs `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, and a deployment name. LangChain package: `langchain-openai` (the `AzureChatOpenAI` class). -> **f. Ollama (local models)** — no API key needed; needs `ollama serve` running on `localhost:11434` and a model pulled (e.g. `ollama pull llama3.2`). LangChain package: `langchain-ollama`. -> **g. Something else** — tell me which one and I'll look up the right `langchain-` package. +1. Sign up with Google, Microsoft, or GitHub. +2. Follow the first-time onboarding until Adrian shows an API key. +3. Copy the API key. It starts with `adr_live_`. +4. Skip detailed agent configuration for now. The quickstart only needs the API + key. The SDK handles the live Adrian connection automatically. -Wait for the user's choice. Record it as `llm_provider` (one of `openai`, `anthropic`, `google`, `bedrock`, `azure`, `ollama`, `custom`). All subsequent substeps depend on this value. +If they already have an account: -**Substep 3A.1b — Confirm credentials and apply the same storage choice as the Adrian key.** For every provider except Ollama (which has no API key), ask the user the credential question explicitly. Two parts: +1. Go to **Configurations**. +2. Open the agent/API key area. +3. Create or copy an agent API key. -1. **Do they have a credential ready?** If they don't, stop and help them get one — link to the provider's signup page. Do not invent a workaround. -2. **Use the same storage path they chose for the Adrian key in Step 1.2.** Branch on the `key_source` flag: +Tell the user: - - **`key_source = "env"`** → tell the user to add the provider key to the same `.env` file, e.g.: - ``` - OPENAI_API_KEY=sk-… # for option (a) - ANTHROPIC_API_KEY=sk-ant-… # for option (b) - GOOGLE_API_KEY=… # for option (c) - AWS_REGION=us-east-1 # for option (d) — plus the access/secret keys, or rely on ~/.aws/credentials - AZURE_OPENAI_API_KEY=… # for option (e) — plus AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_DEPLOYMENT - # (Ollama needs no key) - ``` - Ask them to confirm the file is saved before moving on. Verify the same way you verified the Adrian key (`cat .env` if you can't read files directly). +> Keep the key copied somewhere local for the next step. Do not paste it into +> this chat. We will put it into `.env` on your machine. - - **`key_source = "inline_user"`** → do not ask the user to paste the provider key into chat. Instead tell them: "I'll write the script with a placeholder for your provider key too (e.g. `PASTE_YOUR_OPENAI_KEY_HERE`). You'll replace it in the script before running, the same way you'll replace the Adrian one." Set yourself a reminder to leave placeholders for both keys in Step 3A.3 and to prompt the user to fill them in before Step 3A.4. +Pause here and ask: - - **`key_source = "inline_agent"`** → ask them to paste the provider key into chat. You'll embed it inline in the script in Step 3A.3 with the same `# TODO: move to .env` marker used for the Adrian key. Confirm receipt (without echoing the key value) and proceed. +> Do you have your Adrian API key ready? -For **Ollama (option f)**, no key prompt is needed under any `key_source` — but do confirm: "Is `ollama serve` running and do you have at least one model pulled? If not, run `ollama pull llama3.2` (or any chat model) before we continue." Wait for confirmation. +--- -For **AWS Bedrock (option d)**, also ask which model ID they want (e.g. `anthropic.claude-3-5-sonnet-20241022-v2:0`) — Bedrock has no default and the script needs to name one. For **Azure OpenAI (option e)**, ask for the deployment name. +## Step 2 - Create `.env` -Once credentials are confirmed and stored according to `key_source`, proceed to 3A.2. +Create a local `.env` file in the working folder, write the Adrian placeholder +line into it, then open it for the user. Use the absolute path when creating or +opening the file so the user knows exactly where it lives. -**Substep 3A.2 — Create venv and install.** The LangChain provider package depends on `llm_provider`. Run, in the user's working directory: +If the project has a `.gitignore`, make sure it contains: -```sh -python3 -m venv .venv -source .venv/bin/activate # on Windows: .venv\Scripts\activate -pip install adrian-sdk +```gitignore +.env ``` -Substitute `` with: - -| `llm_provider` | Package | -|---|---| -| `openai` | `langchain-openai` | -| `anthropic` | `langchain-anthropic` | -| `google` | `langchain-google-genai` | -| `bedrock` | `langchain-aws` | -| `azure` | `langchain-openai` (same package; uses `AzureChatOpenAI`) | -| `ollama` | `langchain-ollama` | -| `custom` | whatever the user names (`langchain-groq`, `langchain-mistralai`, `langchain-cohere`, etc.) | - -If `pip install` exits non-zero, jump to Part 2 §12. - -**Substep 3A.3 — Explain the script, then write it.** Before creating any file, tell the user in plain English what the script will do — they should never be in the position of pasting and running code without knowing what it does. - -Send this explanation first (adapted to the chosen `llm_provider`): - -> Here's what I'm about to create. The script `adrian_quickstart.py` is a tiny LangChain agent that does three things: -> -> 1. Calls `adrian.init()` to start Adrian's instrumentation. Adrian monkey-patches LangChain so every model call and tool call is captured automatically. -> 2. Builds a single chat-model client using **{provider name}** ({model id we picked}) and asks it one short question: *"In one sentence, why is the sky blue?"*. That's it — no tools, no agent loop, just one model call so we have something visible in the dashboard quickly. -> 3. Prints the model's reply and calls `adrian.shutdown()` to flush events. -> -> Side effects: it writes captured events to `./events.jsonl` next to the script, and pushes them over WebSocket to the Adrian backend. It does not write or modify anything else. -> -> Ready for me to create it? (yes/no) +Use the command for the user's operating system. These commands put the +placeholder in the file before the editor opens, so the user does not need to +copy anything from chat and overwrite the API key on their clipboard. Replace +`/absolute/path/to/project` or `C:\absolute\path\to\project` with the working +folder from Step 0. -Wait for the user to confirm before writing the file. Now create `adrian_quickstart.py` in the working directory. **The exact code depends on both `key_source` (from Step 1.2) and `llm_provider` (from Step 3A.1)** — assemble the script from the two pieces below. +If the assistant can run shell commands, it should run this command itself so +the user's clipboard can keep the Adrian API key. -##### Piece 1 — Adrian initialisation block - -Pick exactly one of the three, matching `key_source`: - -**A. `key_source = "env"`** (user saved the Adrian key to `.env`): -```python - if not os.environ.get("ADRIAN_API_KEY"): - sys.exit("ADRIAN_API_KEY missing. Did you source your .env? " - "Try: set -a; . ./.env; set +a") - adrian.init() # reads ADRIAN_API_KEY from env automatically +```sh +# macOS +ENV_FILE="/absolute/path/to/project/.env" +grep -q '^ADRIAN_API_KEY=' "$ENV_FILE" 2>/dev/null || printf 'ADRIAN_API_KEY=adr_live_replace_this\n' >> "$ENV_FILE" +open -a TextEdit "$ENV_FILE" ``` -**B. `key_source = "inline_user"`** (user will edit the script before running). Leave the placeholder literally as-is — the user fills it in. **Keep the TODO comment** so the placeholder is easy to find: -```python - # TODO: replace the PASTE_YOUR_KEY_HERE placeholder below with your actual - # Adrian API key, then move it to a .env file before committing this script. - # See https://docs.adrian.secureagentics.ai/quickstart for the .env pattern. - adrian.init(api_key="adr_live_PASTE_YOUR_KEY_HERE") +```sh +# Linux +ENV_FILE="/absolute/path/to/project/.env" +grep -q '^ADRIAN_API_KEY=' "$ENV_FILE" 2>/dev/null || printf 'ADRIAN_API_KEY=adr_live_replace_this\n' >> "$ENV_FILE" +${EDITOR:-nano} "$ENV_FILE" ``` -After writing the file, remind the user: "Before you run this, open `adrian_quickstart.py` in your editor and replace `adr_live_PASTE_YOUR_KEY_HERE` with the real key you copied from the dashboard. Do the same for any other placeholder I left." - -**C. `key_source = "inline_agent"`** (user pasted the Adrian key into chat). Substitute the literal key in place of `adr_live_REPLACE_ME`. **Keep the TODO comment** — it's the audit trail for the convenience trade-off the user opted into: -```python - # TODO: move this key to a .env file before committing this script. - # Replace the literal with os.environ["ADRIAN_API_KEY"] and put - # ADRIAN_API_KEY=adr_live_... in .env (which should be gitignored). - adrian.init(api_key="adr_live_REPLACE_ME") +```powershell +# Windows PowerShell +$envFile = "C:\absolute\path\to\project\.env" +if (!(Test-Path $envFile) -or -not (Select-String -Path $envFile -Pattern '^ADRIAN_API_KEY=' -Quiet)) { + Add-Content -Path $envFile -Value 'ADRIAN_API_KEY=adr_live_replace_this' +} +notepad.exe $envFile ``` -##### Piece 2 — LLM provider block - -Pick the block that matches `llm_provider`. Storage handling follows `key_source`: -- `env` → no key in code; the script asserts the relevant env var is set. -- `inline_user` → leave a `PASTE_YOUR__KEY_HERE` placeholder with the same TODO comment pattern. -- `inline_agent` → inline the literal key the user pasted with the same TODO comment pattern. - -**`openai`** — import `from langchain_openai import ChatOpenAI`; build with `ChatOpenAI(model="gpt-4o-mini")`. Key handling: `key_source = "env"` → add an upfront check for `OPENAI_API_KEY`; `key_source = "inline_user"` → at the top of `main()` set `os.environ["OPENAI_API_KEY"] = "PASTE_YOUR_OPENAI_KEY_HERE"` (with TODO comment); `key_source = "inline_agent"` → same line but substitute the actual `sk-…` value the user pasted. - -**`anthropic`** — import `from langchain_anthropic import ChatAnthropic`; build with `ChatAnthropic(model="claude-3-5-haiku-latest")`. Env / inline pattern uses `ANTHROPIC_API_KEY`. Placeholder for `inline_user`: `PASTE_YOUR_ANTHROPIC_KEY_HERE`. - -**`google`** — import `from langchain_google_genai import ChatGoogleGenerativeAI`; build with `ChatGoogleGenerativeAI(model="gemini-1.5-flash")`. Env / inline pattern uses `GOOGLE_API_KEY`. Placeholder for `inline_user`: `PASTE_YOUR_GOOGLE_KEY_HERE`. - -**`bedrock`** — import `from langchain_aws import ChatBedrockConverse`; build with `ChatBedrockConverse(model="", region_name=os.environ.get("AWS_REGION", "us-east-1"))`. For `env`, rely on the AWS credentials chain; for `inline_user`, leave placeholders for `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION`; for `inline_agent`, inline the values the user pasted. All paths get the same TODO marker. - -**`azure`** — import `from langchain_openai import AzureChatOpenAI`; build with `AzureChatOpenAI(azure_deployment="", api_version="2024-10-21")`. Env / inline pattern uses `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT`. Placeholder for `inline_user`: `PASTE_YOUR_AZURE_KEY_HERE`. +Ask the user to replace the placeholder in the file with their real Adrian API +key: -**`ollama`** — import `from langchain_ollama import ChatOllama`; build with `ChatOllama(model="")`. No API key handling under any `key_source`; just confirm `ollama serve` is reachable at `http://localhost:11434` (the default). - -**`custom`** — import what the user names and build with sensible defaults; ask the user for the class name, model id, and env-var name if you don't know them. Apply the same `key_source` rules to the env-var name they give you. - -##### Full template - -Assemble the chosen blocks into this skeleton (this example shows `key_source = "env"` + `llm_provider = "openai"`; substitute the blocks above for your case): - -```python -"""Adrian 60-second smoke test. - -This script makes one LLM call ("Why is the sky blue?") through LangChain -so that Adrian's auto-instrumentation captures a real model event. The -event will appear in the dashboard at https://app.adrian.secureagentics.ai/events -within a couple of seconds and is also written to ./events.jsonl locally. -""" -import asyncio -import os -import sys -import adrian -from langchain_openai import ChatOpenAI # <-- swap to your provider's import - - -async def main() -> int: - # === Adrian init (Piece 1) === - if not os.environ.get("ADRIAN_API_KEY"): - sys.exit("ADRIAN_API_KEY missing. Did you source your .env? " - "Try: set -a; . ./.env; set +a") - adrian.init() - - # === Provider key check (Piece 2) === - if not os.environ.get("OPENAI_API_KEY"): - sys.exit("OPENAI_API_KEY missing. Set it in your shell or .env.") - - # === Model call === - llm = ChatOpenAI(model="gpt-4o-mini") - response = await llm.ainvoke("In one sentence, why is the sky blue?") - print("LLM said:", response.content) - - adrian.shutdown() - return 0 - - -if __name__ == "__main__": - sys.exit(asyncio.run(main())) +```env +ADRIAN_API_KEY=adr_live_replace_this ``` -After writing the file, show the user the final assembled code (or a diff if your client renders one) and ask: "Saved. Want me to run it now?" Wait for confirmation before executing in Step 3A.4. - -**Substep 3A.4 — Run it.** Run from the same shell that has the venv active. The exact command depends on `key_source` *and* `llm_provider`. Tell the user which command to run — don't make them guess. - -- **`key_source = "env"`** (recommended) — source `.env` once so every key in it (Adrian + whichever provider variables apply) is in the process environment, then run: - ```sh - set -a; . ./.env; set +a - python adrian_quickstart.py - ``` - On Windows PowerShell, use `Get-Content .env | ForEach-Object { $k,$v = $_ -split '=',2; [Environment]::SetEnvironmentVariable($k,$v) }` or have the user install `python-dotenv` and add `from dotenv import load_dotenv; load_dotenv()` near the top of the script. - -- **`key_source = "inline_user"`** — before running, remind the user to open `adrian_quickstart.py` and replace every `PASTE_YOUR_..._HERE` placeholder with the actual values. Once they confirm the placeholders are filled in, run: - ```sh - python adrian_quickstart.py - ``` - If you can read files, double-check there are no remaining `PASTE_YOUR_` substrings in the file before running. If you can't read files, ask the user to confirm explicitly: "Have you replaced every `PASTE_YOUR_…` placeholder?" Wait for a yes. - -- **`key_source = "inline_agent"`** — both the Adrian key and the provider key are already inlined in the script (with TODO markers). Just run it: - ```sh - python adrian_quickstart.py - ``` - Exception: if `llm_provider = "bedrock"` and the user relies on `~/.aws/credentials` rather than inlined access keys, the AWS SDK will pick those up automatically — nothing extra to export. - Exception: if `llm_provider = "ollama"`, confirm `ollama serve` is reachable: `curl http://localhost:11434/api/tags` should return JSON. - -Expected output: a log line `Adrian v1.0.0 initialised (handlers=2, ws=ws://localhost:8080/ws)` (or the managed cloud's WS URL once configured), followed by the LLM's response. If you see `Adrian v…` you've already won — the event is on its way. - -If the WS URL still says `ws://localhost:8080/ws` and the user is on managed (not self-hosted), they need `ADRIAN_WS_URL` set to the URL the dashboard tells them to use. Add it to `.env` and re-run. Don't make this a blocker for the smoke test — the local JSONL handler is still writing `events.jsonl` next to the script so the user has something tangible immediately. - -**Substep 3A.5 — Direct to dashboard.** Tell the user: - -> Open **https://app.adrian.secureagentics.ai/events** — your event should be there within 2-3 seconds, classified as benign (M0). - -Then go to Step 4. - -## Step 3B — Own agent path (35 seconds) - -**Substep 3B.1 — Get the file.** Ask: - -> What's the path to the LangChain or LangGraph agent file you want to instrument? (Absolute path, or relative to the current directory.) - -Read the file. If it's a directory, ask which file inside. If it's not Python, stop and explain Adrian's SDK is Python-only (LangChain/LangGraph). - -**Substep 3B.2 — Validate the file.** Check three things: - -1. Does it import from `langchain_*` or `langgraph`? If neither, this file isn't a LangChain agent — stop and tell the user. -2. Does it have an `async def` somewhere (the agent entry)? Look for the function that calls `await ...ainvoke(...)` or `await ...astream(...)`. -3. Does it use **sync** `.invoke()` instead of `.ainvoke()`? If yes, warn: - > Heads-up: your agent uses the sync `.invoke()` path. Adrian will still capture events for logging, but Block / Human Review gating only fires on the async path (`ainvoke` / `astream`). If you want in-flight tool blocking later, you'll need to convert to async. - Continue anyway — capture works either way. - -**Substep 3B.3 — Identify the patch site.** The patch is two insertions, and the second one differs based on the `key_source` flag set in Step 1.2. - -1. **Imports block at the top of the file:** add `import adrian`. Add `import os` too if `os` isn't already imported (only needed for the env variant below). - -2. **Entry function — first statement inside the `async def`.** Find the function the user runs as the agent's entry point. This is usually the `async def main():` (or similar) that's called from `asyncio.run(main())`. Insert one of the three variants below as the first statement inside that function. - - #### Variant A — `key_source = "env"` (user has `.env` set up) +Then say: - ```python - adrian.init(api_key=os.environ["ADRIAN_API_KEY"]) - ``` +> Save `.env`, close the editor, and come back here when you are done. Do not +> paste the key into chat. - The user is responsible for sourcing `.env` before running their agent. If they already use `python-dotenv` or `direnv`, this just works. If they don't, mention it: "Source your `.env` before running with `set -a; . ./.env; set +a`." +Stop here. Do not continue to agent selection until the user confirms the file +is saved. - #### Variant B — `key_source = "inline_user"` (user will edit the patched file themselves) +After they confirm, verify without printing full secrets: - ```python - # TODO: replace the PASTE_YOUR_KEY_HERE placeholder below with your actual - # Adrian API key, then move it to a .env file before committing this script. - adrian.init(api_key="adr_live_PASTE_YOUR_KEY_HERE") - ``` +- `ADRIAN_API_KEY` exists and starts with `adr_live_` +- there are no quote marks around the values +- there are no placeholder values left - **Keep the placeholder literally as-is** — the user fills it in. After applying the patch in 3B.4, remind the user explicitly: "Open the patched file in your editor and replace `adr_live_PASTE_YOUR_KEY_HERE` with the real key before running." +If the assistant can read files, it may check `.env` directly but must not echo +the key back to the chat. - #### Variant C — `key_source = "inline_agent"` (user pasted the key directly) - - ```python - # TODO: move this key to a .env file before committing this script. - # Replace the literal below with os.environ["ADRIAN_API_KEY"] and put - # ADRIAN_API_KEY=adr_live_... in .env (gitignored). - adrian.init(api_key="adr_live_REPLACE_ME") - ``` - - Substitute `adr_live_REPLACE_ME` with the actual key the user pasted. **Keep the comment block** — the user opted into the convenience trade-off and the comment makes the eventual cleanup trivial to find with `grep -rn 'TODO.*\.env'`. - -You do **not** need to add `adrian.shutdown()` — the SDK registers it via `atexit` automatically. (You can still add it before a clean `return` if the agent runs forever and you want explicit teardown.) - -**Sync-only agents:** if there's no `async def` and the agent is fully sync, put the same `adrian.init(...)` line once at module import time (after the imports block, before any LangChain object is constructed). Pick the env or paste variant the same way. Sync mode still captures events; it just doesn't gate tool calls. - -**Substep 3B.4 — Show the diff before applying.** Print the patched file as a unified diff and ask the user to confirm before writing. (Skip this step if your client doesn't support arbitrary file writes — paste the patched code and tell the user to save it.) - -**Substep 3B.5 — Install the SDK in the user's existing environment.** Detect what they're using: - -- Plain venv → `pip install adrian-sdk` -- Poetry → `poetry add adrian-sdk` -- uv → `uv add adrian-sdk` (or `uv pip install adrian-sdk` in legacy projects) -- Conda → `pip install adrian-sdk` inside the activated env -- Requirements file → append `adrian-sdk` to `requirements.txt`, run `pip install -r requirements.txt` - -If you can't tell, ask the user once: "What package manager does this project use?" - -**Substep 3B.6 — Run their agent.** Tell the user: - -> Now run your agent as you normally would. Adrian's instrumentation captures every LLM call and tool call automatically — you don't need to change how you invoke the agent. - -Then go to Step 4. - -## Step 4 — Verify in the dashboard (5 seconds) - -Tell the user: - -> Open **https://app.adrian.secureagentics.ai/events**. Within a couple of seconds you should see one or more events listed — each is one LLM call or tool call your agent made, with a classification badge (M0 benign, M2 misuse, M3 high-risk, M4 malicious). Most first runs are all M0. - -If nothing shows up in 10 seconds, jump to Part 2 §12 ("Common failure modes") and walk the user through the relevant entry. - -## Step 5 — Ask the user what they want to do next (default is stop) - -**Do not auto-chain to anything.** The next-step prompt depends on which path the user came through — Step 3A (smoke test) or Step 3B (their own agent). The two cases are structurally similar but the phrasing matters: don't offer 3B users a "instrument an existing project" option when they just did exactly that. - -Use a structured multi-choice prompt if your client supports one (`AskUserQuestion` on Claude); otherwise list in chat and wait for "a" or "b". The default is always (a) stop — never push the user toward the secondary option. - -### 5.A — If the user came through Step 3A (test agent / smoke test) - -> Adrian is capturing events from the test script — that's everything the setup flow needed to show you. What would you like to do next? -> -> **(a) Stop here. [default]** You've seen the loop work end-to-end. You can come back to this guide any time to instrument a real agent. -> -> **(b) Instrument an existing LangChain / LangGraph project of yours.** Tell me the file path and I'll add the same two lines (`import adrian` + `adrian.init(...)` in the async entry function) so Adrian captures events from your real code too. Same provider, same key, no other changes. +--- -If the user picks **(b)**, jump to **Step 3B**, starting at 3B.1. +## Step 3 - Choose The Agent To Run -### 5.B — If the user came through Step 3B (already instrumented their own agent) +Ask: -> Adrian is now instrumented in your agent at `` and capturing events. What would you like to do next? +> Adrian needs an agent to monitor. Do you want to: > -> **(a) Stop here. [default]** Your agent will keep running with Adrian observing every LLM call and tool call — no further setup needed. Run the agent as you normally would. +> **A. Simple Example Agent** - use a tiny example agent that asks an LLM: +> "In one sentence, why is the sky blue?" Fastest route to your first event. > -> **(b) Instrument another agent file.** If you have additional LangChain / LangGraph entry points (multiple agents in one project, or a separate project), point me at the next file and I'll do the same patch. - -If the user picks **(b)**, jump back to **Step 3B.1** with the new file path. Reuse the existing venv if the file is in the same directory; otherwise repeat the install per 3B.5. Do not silently change the LLM provider — keep using whatever the user's project already uses. - -### Shared rules for both 5.A and 5.B - -If the user picks **(a)** — stop cleanly. Summarise what was set up in one or two sentences (key created, smoke test ran or agent instrumented, first events visible) and say goodbye. Do not keep offering things. Do not volunteer Block mode, webhooks, agent remit, or anything else — they asked to stop. Stop. - -**Only if the user proactively asks about further configuration**, point them at the relevant Part 2 sections — don't list these unprompted: - -- Switching the agent profile to Block mode (halts risky tool calls mid-flight) → see §6 (Execution modes and the MAD taxonomy). -- Setting up a Discord webhook for M3 / M4 alerts → see §7 (Integrations → Notifications). -- Filling in the agent's remit / known risks so the classifier is more accurate → see §8 (Reading events → Settings → Agents) or edit the profile directly in the dashboard. -- Anything else → §12 (failure modes) or the Discord linked in §2. +> **B. Integrate Adrian with one of my existing agents** - point me at your +> LangChain or LangGraph agent and I will integrate Adrian with it. -The rest of this file is reference for when something goes wrong. +If the user chooses A, continue to Step 4A. ---- - -## Cross-client compatibility notes - -| Agent | Multi-choice tool | File write | Shell execution | Special quirks | -|---|---|---|---|---| -| **Claude Code** (CLI) | none — ask in chat | Edit/Write tools | Bash tool | Sync via Edit; preserves diffs cleanly. Bash runs in the same shell cwd as the user expects. | -| **Claude Desktop** (Cowork) | `AskUserQuestion` | Edit/Write tools | sandboxed bash | Sandboxed bash has its **own** cwd, separate from the user's terminal — *never* use bare `touch .env`; always use the absolute `working_dir` path from Step 0. Chat persists, so prefer `.env` over `inline_agent`. | -| **Codex CLI** | none — ask in chat | direct edit | shell exec | Streaming edits — show diff first. Shell exec runs in Codex's session cwd, which may differ from the user's terminal cwd; confirm it matches `working_dir`. | -| **Codex web / ChatGPT** | none — ask in chat | offer code blocks | none | Can't execute or write files — the user runs every command. Hand them the absolute-path shell command from Step 1.3 verbatim. | -| **Cursor** chat | none — ask in chat | Composer can write | terminal panel | Composer multi-file edits work for the own-agent path. Restricted mode can't write `.env` — fall back to giving the user the shell command. Cursor's terminal panel may open at the project root, which may differ from `working_dir`; confirm. | -| **Aider** | none — ask in chat | yes (its model) | shell exec | Map files into the session before patching. Aider's cwd is wherever the user invoked it from — should match `working_dir` if the user followed Step 0. | -| **Windsurf / Copilot Chat** | none — ask in chat | varies | varies | Treat as Codex-equivalent. Same cwd-mismatch risk; always use absolute paths from `working_dir`. | - -### Critical: the `.env`-vs-cwd mismatch (the single most common failure mode across clients) - -The agent you're running may have a working directory that **does not match the user's terminal cwd**. Examples: Claude Desktop's sandboxed bash has a session-mount cwd; Codex CLI starts a session in whichever folder the user invoked it from, which may not be where they intend to run the script; Cursor's terminal panel may open at the project root rather than a subfolder. If you create `.env` via your own shell tool and the user runs the script from their terminal, the two cwds may point at different folders — the script won't see the key and you'll spend time debugging a phantom "key missing" error. - -The fix is universal and already baked into Step 0 and Step 1.3, but worth restating: - -1. **Always establish `working_dir` as an absolute path in Step 0** before any file-creation step. Announce it back to the user and get explicit confirmation. -2. **Every file-creation command must use the absolute path** — never `touch .env`, always `touch "/.env"`. -3. **Whenever you verify a file exists**, check it at `/` explicitly, not at whatever your shell's current cwd happens to be. -4. **If your shell tool's cwd differs from `working_dir`**, either `cd` to `working_dir` first or pass the absolute path to every command. Don't assume the user's terminal will be in the same place as yours. - -If your client supports it, prefer: -- A structured multi-choice question over a free-text "type a or b". Reduces parse errors. -- Showing a diff before writing patched code (own-agent path) over a silent write. Users want a chance to bail. -- Reading `.env` once and not echoing the key back over re-reading it in every step. Reduces leak surface. -- Absolute paths in every file-touching command, even when you "know" the cwd is right. The cost of the prefix is two seconds; the cost of debugging an `.env` in the wrong folder is ten minutes. +If the user chooses B, continue to Step 4B. --- -# PART 2 — Reference (lookup index) - -## 1. What Adrian is (one paragraph) - -Adrian is an open-source runtime security monitoring and control engine for AI agents. It captures every LLM call and tool call your LangChain / LangGraph agent makes, ships them to a backend (either the managed cloud at `app.adrian.secureagentics.ai` or a self-hosted Docker stack), classifies them against a behaviour policy ("MAD codes" — Misuse, Abuse, Deception), and depending on the configured execution mode it can **alert**, **pause for human review**, or **block** the tool call mid-flight. It plugs in with two lines of Python (`adrian.init()` + `adrian.shutdown()`) thanks to LangChain auto-instrumentation. - -The Python package on PyPI is **`adrian-sdk`** (the in-tree package name is `adrian-sdk-oss`); the import name is **`adrian`**. - ---- - -## 2. Canonical URLs - -When advising a user, always point them at these: +## Step 4A - Simple Example Agent -| Purpose | URL | -|---|---| -| Managed dashboard / sign-up | `https://app.adrian.secureagentics.ai` | -| Documentation | `https://docs.adrian.secureagentics.ai` | -| Quickstart guide | `https://docs.adrian.secureagentics.ai/quickstart` | -| Integrations index | `https://docs.adrian.secureagentics.ai/integrations` | -| Backend reference (admin reset, model swap) | `https://docs.adrian.secureagentics.ai/reference/backend` | -| PyPI | `https://pypi.org/project/adrian-sdk/` | -| GitHub repo | `https://github.com/secureagentics/Adrian` | -| Discord | `https://discord.gg/6nmJ9k3u6` | +Ask: -The dashboard hostname is **`app.adrian.secureagentics.ai`** — not `dashboard.`, not `adrian.com`, not `secureagentics.com`. Double-check before pasting a URL. - ---- +> In order to set up the example agent, you need to provide an LLM. Pick your +> preference: +> +> **a. OpenAI** - needs `OPENAI_API_KEY`; package `langchain-openai` +> **b. Anthropic** - needs `ANTHROPIC_API_KEY`; package `langchain-anthropic` +> **c. Google Gemini** - needs `GOOGLE_API_KEY`; package `langchain-google-genai` +> **d. Azure OpenAI** - needs `AZURE_OPENAI_API_KEY`, +> `AZURE_OPENAI_ENDPOINT`, and `AZURE_OPENAI_DEPLOYMENT`; package +> `langchain-openai` +> **e. Ollama** - no API key; usually runs locally at +> `http://localhost:11434`; package `langchain-ollama` + +For the chosen provider, write exactly one matching provider block into `.env` +before opening the editor. Do not ask the user to copy the block from chat. If a +provider block already exists, update it instead of adding duplicates. + +Then open `.env` again using the same editor style from Step 2, and ask the +user to replace the provider placeholder with their real provider key. + +Use only the matching block: + +```env +# OpenAI +LLM_PROVIDER=openai +OPENAI_API_KEY=sk_replace_this + +# Anthropic +LLM_PROVIDER=anthropic +ANTHROPIC_API_KEY=sk-ant-replace-this + +# Google Gemini +LLM_PROVIDER=google +GOOGLE_API_KEY=replace_this + +# Azure OpenAI +LLM_PROVIDER=azure +AZURE_OPENAI_API_KEY=replace_this +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com +AZURE_OPENAI_DEPLOYMENT=your-deployment-name + +# Ollama +LLM_PROVIDER=ollama +OLLAMA_MODEL=llama3.2 +``` -## 3. Two ways to run Adrian +Then say: -Always ask the user which one they want before writing code; they configure very differently. +> Save `.env`, close the editor, and come back here when you are done. Do not +> paste the provider key into chat. -### 3.1 Managed (cloud) +Stop here. Do not install packages or create `adrian_quickstart.py` until the +user confirms `.env` is saved. -1. Sign up at `https://app.adrian.secureagentics.ai`. -2. Go to **Settings → Agents → New key**, create an Agent Profile, generate an API key. The key is shown **once** — it starts with `adr_live_…` in production (`adr_local_…` for self-hosted). Save it; the backend only stores a SHA-256 hash. -3. `pip install adrian-sdk` -4. Set `ADRIAN_API_KEY=adr_live_…` (env var) **or** pass `api_key=` to `adrian.init()`. -5. Leave `ws_url` unset for managed — the SDK's default `ws://localhost:8080/ws` is for self-hosting; managed users should set `ADRIAN_WS_URL` to the URL shown in their dashboard (production keys talk to the managed WebSocket endpoint, not localhost). +After they confirm, verify without printing full secrets: -### 3.2 Self-hosted (Docker) +- `LLM_PROVIDER` is one of `openai`, `anthropic`, `google`, `azure`, or `ollama` +- the selected provider's required values exist +- there are no placeholder values left for the selected provider -Prerequisites: Docker + Docker Compose v2, an NVIDIA GPU + NVIDIA Container Toolkit (CPU works but is slow), ~10 GB free disk for the classifier model. +For Ollama, check whether it is running: ```sh -git clone https://github.com/secureagentics/Adrian -cd Adrian +curl http://localhost:11434/api/tags +``` -# Bootstrap: creates ./data/adrian.db, applies migrations, writes .env, -# prints a random admin password, downloads Gemma 4 E4B (or E2B) into ./models/. -docker compose --profile setup run --rm setup bootstrap +If Ollama is not running, ask the user to run: -# Bring up backend + dashboard + Llama.cpp classifier -docker compose --profile llm up -d +```sh +ollama serve +ollama pull llama3.2 ``` -Dashboard at `http://localhost:3000`. Sign in as `admin@localhost` with the printed password; you'll be forced to set a new one. Then **Settings → Agents → New key** to issue an `adr_local_…` key. +### Install -To install the bundled SDK into a local venv (uses [`uv`](https://docs.astral.sh/uv/)): +Create a virtual environment and install the SDK plus the provider package: ```sh -make sdk-install +python3 -m venv .venv source .venv/bin/activate -uv pip install langgraph langchain-openai # or whichever provider +pip install adrian-sdk langchain-openai ``` -The SDK's default `ws_url=ws://localhost:8080/ws` already points at the bootstrapped backend — for a self-hosted setup the user only needs to pass `api_key`. +Replace `langchain-openai` with the package for the chosen provider. ---- +### Create `adrian_quickstart.py` + +Before writing the file, say: -## 4. The minimal working snippet +> I am going to create a tiny example agent. It initializes Adrian, asks an LLM +> "In one sentence, why is the sky blue?", prints the answer, and sends the +> event to your Adrian dashboard. -This is the canonical "hello world". **It must be run inside an asyncio event loop** — Adrian's WS client, pairing buffer, and LangGraph patches all assume an async context. +Use this file: ```python import asyncio -import adrian -from langchain_openai import ChatOpenAI - -async def main(): - adrian.init(api_key="adr_live_...") # or set ADRIAN_API_KEY - llm = ChatOpenAI(model="gpt-4o") - response = await llm.ainvoke("Summarise the latest IPO filings") - print(response.content) - adrian.shutdown() - -asyncio.run(main()) -``` +import os +import sys -Things the agent must NOT do when generating code: +import adrian -- **Do not** call `llm.invoke()` (sync) and expect block-mode gating to work. Block / Human Review gating only fires through the async path (`ainvoke`, `astream`) because the patched `ToolNode.ainvoke` is what awaits the verdict. The sync path will still capture events for logging but cannot halt a tool call. -- **Do not** call `adrian.init()` at module import time outside an event loop and then immediately spawn workers — see §10 on fork safety. -- **Do not** wrap `adrian.init()` in `asyncio.run()` separately from the agent code; both must share the same loop, otherwise the WS client schedules its connect against a now-dead loop. -- **Do not** forget `adrian.shutdown()` at clean exit. `atexit.register(shutdown)` runs it automatically, but in long-lived servers (FastAPI, Celery) wire it into the framework's shutdown hook. ---- +def require_env(name: str) -> str: + value = os.environ.get(name) + if not value: + sys.exit(f"{name} is missing. Add it to .env and run again.") + return value -## 5. `adrian.init()` — every argument worth knowing -```python -adrian.init( - api_key: str | None = None, # ADRIAN_API_KEY env fallback - log_file: str | Path = "events.jsonl", # ADRIAN_LOG_FILE - handlers: list[EventHandler] | None = None, - auto_instrument: bool = True, - log_level: str | None = None, # "DEBUG" turns on SDK verbose logs - ws_url: str | None = None, # ADRIAN_WS_URL, default ws://localhost:8080/ws - session_id: str | None = None, # ADRIAN_SESSION_ID, else per-cwd persistent UUID - block_timeout: float = 30.0, # ADRIAN_BLOCK_TIMEOUT - on_event=None, on_verdict=None, - on_block=None, on_audit=None, - on_disconnect=None, on_reconnect=None, - on_mcp_server=None, - replay_buffer_frames: int = 1000, # ADRIAN_REPLAY_BUFFER_FRAMES -) -``` +def build_llm(): + provider = os.environ.get("LLM_PROVIDER", "openai").strip().lower() -Key facts: + if provider == "openai": + require_env("OPENAI_API_KEY") + from langchain_openai import ChatOpenAI -- `api_key` accepts `adr_live_…` (managed cloud) or `adr_local_…` (self-hosted). Test keys generated by the open-source backend always carry the `adr_local_` prefix. -- `ws_url` must be a **WebSocket** URL (`ws://` or `wss://`), not HTTPS. For managed, the dashboard tells you the exact URL. -- `session_id` is persistent **per current working directory** by default (see `session_persistence.py`). The same agent script run from the same folder twice will reuse the same session ID, which is usually what you want. -- `block_timeout` is the fail-open ceiling in `MODE_BLOCK` only. In `MODE_HITL` the SDK waits **indefinitely** for a human reviewer; bump `block_timeout` anyway for symmetry but it isn't consulted. -- `auto_instrument=True` monkey-patches `Runnable`, `CallbackManager`, `BaseChatModel`, `langgraph.pregel.Pregel`, and `langgraph.prebuilt.ToolNode` at init time. To opt out, set it `False` and attach `adrian.get_handler()` to each chain via `config={"callbacks": [handler]}`. -- PII redaction is **always on** — every handler is wrapped in `RedactingHandler`. There is no opt-out flag. + return ChatOpenAI(model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini")) ---- + if provider == "anthropic": + require_env("ANTHROPIC_API_KEY") + from langchain_anthropic import ChatAnthropic -## 6. Execution modes and the MAD taxonomy + return ChatAnthropic( + model=os.environ.get("ANTHROPIC_MODEL", "claude-3-5-haiku-latest") + ) -Three execution modes are configurable in the dashboard at **Settings → Policy** (organisation-wide) and **Settings → Agents → ** (per agent profile): + if provider == "google": + require_env("GOOGLE_API_KEY") + from langchain_google_genai import ChatGoogleGenerativeAI -| Mode (wire enum) | Dashboard label | What the SDK does | -|---|---|---| -| `MODE_ALERT` (1) | **Alert** | Fire-and-forget. Events captured, verdicts logged, tools run. | -| `MODE_HITL` (2) | **Human Review** | The patched `ToolNode.ainvoke` pauses on tool calls and awaits a `/reviews` resolution. Verdict's `hitl.continue_execution` decides halt vs. proceed. **Waits indefinitely.** | -| `MODE_BLOCK` (3) | **Block** | The patched `ToolNode.ainvoke` halts tools whose verdict tier is in the policy's MAD scope. Fails open on `block_timeout`. | + return ChatGoogleGenerativeAI( + model=os.environ.get("GOOGLE_MODEL", "gemini-1.5-flash") + ) -A halted tool returns `ToolMessage(content="[BLOCKED by security policy]", ...)` to the graph in place of the real tool result — the tool function itself never runs. + if provider == "azure": + require_env("AZURE_OPENAI_API_KEY") + require_env("AZURE_OPENAI_ENDPOINT") + deployment = require_env("AZURE_OPENAI_DEPLOYMENT") + from langchain_openai import AzureChatOpenAI -### MAD codes + return AzureChatOpenAI( + azure_deployment=deployment, + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"), + ) -The classifier emits a code shaped `M{0..4}.{a..e}`: + if provider == "ollama": + from langchain_ollama import ChatOllama -- **M0** — Benign. No action. -- **M2** — Likely Misuse. Default: NOTIFY. -- **M3** — High-Risk Misuse. Default: BLOCK. -- **M4** — Malicious. Default: ESCALATE. + return ChatOllama( + model=os.environ.get("OLLAMA_MODEL", "llama3.2"), + base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"), + ) -Each tier's per-code definitions live in `backend/internal/alerts/alerts.json`. Examples: `M3.c` is data exfiltration intent, `M3.d` is privilege escalation, `M4.d` is destructive action (e.g. `DROP TABLE`), `M4.c` is alignment circumvention. Reference these codes when surfacing verdicts to the user. + sys.exit( + "Unsupported LLM_PROVIDER. Use openai, anthropic, google, azure, or ollama." + ) -The per-MAD bools in the policy (`policy_m0` / `policy_m2` / `policy_m3` / `policy_m4`) decide which tiers the active execution mode actually halts on. So you can run in `MODE_BLOCK` with only `policy_m4=true` and the SDK will only block M4 tool calls; M3 events still surface as alerts but tools run. ---- - -## 7. Integrations +async def main() -> int: + require_env("ADRIAN_API_KEY") -### Frameworks at launch -- **LangChain / LangGraph** — first-class, auto-instrumented. + adrian.init() + llm = build_llm() + response = await llm.ainvoke("In one sentence, why is the sky blue?") + print(response.content) + adrian.shutdown() + return 0 -### Frameworks on roadmap (no SDK support yet — do not write code claiming these work) -- OpenAI Agents SDK -- Anthropic Agents SDK -- CrewAI -- OpenClaw -If the user asks for one of the roadmap frameworks, advise that today they need to bridge to LangChain (e.g. wrap the model in `ChatOpenAI` or `ChatAnthropic`) or use the manual instrumentation path (§9) and attach the handler themselves. Point at the Discord for roadmap timing. +if __name__ == "__main__": + raise SystemExit(asyncio.run(main())) +``` -### Notifications (alerting integrations) +### Run -The README lists Discord and Slack as "at launch" integrations and shows both logos. In the **in-tree code** as of this guide's verification date, the notifications package (`backend/internal/notifications/`) is Discord-first: `ValidateDiscordWebhookURL` only accepts `https://discord.com/api/webhooks/` or `https://discordapp.com/api/webhooks/` prefixes. Slack webhook delivery is on the immediate roadmap and may be available in the managed cloud ahead of the OSS repo — check the dashboard's Webhooks page (or the live `/api/webhooks` schema) for the current allow-list before promising the user a specific channel. +Load `.env` and run the example: -On roadmap (not yet wired up at all): WhatsApp, Microsoft Teams, PagerDuty. +```sh +set -a +. ./.env +set +a +python adrian_quickstart.py +``` -#### Setting up a Discord webhook (works today) +If it prints an answer, say: -1. In Discord: **Server Settings → Integrations → Webhooks → New Webhook**, copy the URL. It must start with `https://discord.com/api/webhooks/` or `https://discordapp.com/api/webhooks/` — the backend validator rejects anything else. -2. In the Adrian dashboard, go to the Webhooks settings page and `POST /api/webhooks` (or use the UI) with: +> Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you +> should see the event within a few seconds. -```json -{ - "webhook_url": "https://discord.com/api/webhooks/…", - "alert_type": "M3" -} -``` +Then ask: -Valid `alert_type` values are exactly `"M3"`, `"M4"`, or `"all"`. M0 / M2 verdicts never fan out to webhooks regardless of filter — they're either benign or notify-tier and the dispatcher drops them before send. The dispatcher posts a Discord embed including the MAD code, classification, session ID, agent ID, and a deep link back to the dashboard event page. +> Want me to integrate Adrian with one of your real agents now? If yes, send me +> the path to your LangChain or LangGraph agent file. If not, you can stop here. -### MCP server inventory +If the user says yes or sends a file path, continue to Step 4B. If the user +stops, use the final success message below. -If the agent uses [`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters), Adrian auto-detects every registered MCP server and reports its name / transport / endpoint to the backend (visible in the dashboard at **MCP**). No extra config — it patches `MultiServerMCPClient.__init__` and the underlying `mcp.client.*_client` transports. +If the event does not appear, go to "If Anything Goes Wrong" below. --- -## 8. Reading events in the dashboard +## Step 4B - Integrate Adrian With An Existing Agent -Once events arrive (the WS push usually shows in under a second), the user can navigate: +Ask: -- **Events** (`/events`) — the raw paired-event feed, every `chat_model_start+llm_end` and `tool_start+tool_end` pair, with verdicts attached. -- **Sessions** (`/sessions/`) — timeline for a single session ID. -- **Agents** (`/agents` and `/agents/`) — per-agent rollups. -- **Reviews** (`/reviews`) — Human Review queue. When `MODE_HITL` is active and a tool gets flagged, the request lands here. Approve to release the tool call; reject to substitute `[BLOCKED by security policy]`. -- **MCP** (`/mcp`) — discovered MCP servers across all sessions. -- **Settings → Policy** — execution mode + per-MAD policy. -- **Settings → Agents** — agent profiles (name, remit, M0 accepted behaviours, M3 known-risks), API key issue / revoke. -- **Settings → Webhooks** — Discord / Slack alert routing. -- **Audit log** — admin activity (key rotations, policy edits). +> Please give me the absolute path to your LangChain or LangGraph agent file. +> If your assistant cannot read outside this folder, copy the agent file into +> this project and point me at that copy. -A locally-running events file is also written to `./events.jsonl` (override with `log_file=` or `ADRIAN_LOG_FILE`). That JSONL is one record per paired event and is what the JSONL handler writes whether or not the WS handler is also active. +Read the file and check: ---- - -## 9. Manual instrumentation (when auto-patching is unwanted) +- it is Python +- it imports or uses LangChain or LangGraph +- it has an entry point such as `main()`, `async def main()`, or code that calls + `.invoke()`, `.ainvoke()`, or `.astream()` -Some users (security-sensitive shops, frameworks that already manage callbacks) prefer not to patch LangChain at import. Pattern: +Before editing, say: -```python -import adrian -from langchain_openai import ChatOpenAI +> I will integrate Adrian with this agent by importing `adrian`, initializing it +> near the start of the agent run, and keeping secrets in `.env`. -async def main(): - adrian.init(api_key="adr_live_...", auto_instrument=False) - handler = adrian.get_handler() # set during init() - if handler is None: - raise RuntimeError("Adrian handler missing — check adrian.init()") +Patch the file: - llm = ChatOpenAI(model="gpt-4o") - await llm.ainvoke("prompt", config={"callbacks": [handler]}) +1. Add `import adrian` near the imports. +2. Add this before the model, chain, or graph is created or called: - adrian.shutdown() +```python +adrian.init() ``` -The handler still has to be attached to **every** chain / runnable / graph that should be observed. Forgetting one is silent — the chain runs unmonitored. See `examples/manual_instrumentation.py`. +If the agent has a clear shutdown path, add: ---- - -## 10. Fork safety, threading, and event loops +```python +adrian.shutdown() +``` -This is the area where users most often break the SDK. Encode the following as constraints: +If it is a long-running app, mention that `adrian.shutdown()` should be called +from the app's normal shutdown hook. -- **Single event loop.** `adrian.init()` must be called from the same loop that drives the agent. The WS client schedules its connect against `asyncio.get_running_loop()`; if no loop is running at init time the connect is deferred until the first send. -- **Pre-fork servers** (`gunicorn --preload`, `multiprocessing.Pool`, Celery prefork): each child must call `adrian.init()` in its worker startup hook. The SDK registers an `os.register_at_fork` handler that nulls out the parent's WS / handler / hook globals in the child, so reusing the parent's connection from two processes can't corrupt frames on the wire. The child will silently have no instrumentation until it re-inits. -- **Sync code paths.** `llm.invoke()` (sync) still emits events via the patched `Runnable.invoke`, but block / HITL gating only engages on `ToolNode.ainvoke`. For a strict "block before tool runs" guarantee, the user must be on the async path. -- **Shutdown.** Long-running services (FastAPI, Streamlit) should call `adrian.shutdown()` on app shutdown. `atexit.register` handles ad-hoc scripts. +Install the SDK in the existing environment: ---- +```sh +pip install adrian-sdk +``` -## 11. Environment variables — full list +Run the agent the way the user normally runs it, after loading `.env`: -These are read inside `adrian.init()`. Setting them is interchangeable with passing kwargs; kwargs win when both are present (except for `ADRIAN_API_KEY` and `ADRIAN_WS_URL` where env vars are preferred). +```sh +set -a +. ./.env +set +a +# then run the user's normal agent command +``` -| Env var | Default | Purpose | -|---|---|---| -| `ADRIAN_API_KEY` | — | `adr_live_…` / `adr_local_…` | -| `ADRIAN_WS_URL` | `ws://localhost:8080/ws` | WebSocket endpoint | -| `ADRIAN_LOG_FILE` | `events.jsonl` | JSONL output path | -| `ADRIAN_SESSION_ID` | (per-cwd UUID) | Override session identity | -| `ADRIAN_BLOCK_TIMEOUT` | `30.0` | Fail-open ceiling in `MODE_BLOCK` | -| `ADRIAN_REPLAY_BUFFER_FRAMES` | `1000` | In-memory ring buffer for WS replay | +If the agent runs, say: -For self-hosted deployments (read by Docker Compose, not the SDK) the bootstrap also writes `ADRIAN_LLM_URL`, `ADRIAN_LLM_MODEL_PATH`, `ADRIAN_LLM_API_KEY`, `ADRIAN_LLM_MODEL`, `ADRIAN_LLM_CTX_SIZE`, `ADRIAN_SLIDING_WINDOW_SIZE`, `ADRIAN_SLIDING_WINDOW_TTL_SECONDS`, `ADRIAN_BACKEND_PORT`, `ADRIAN_DASHBOARD_PORT`, `ADRIAN_SESSION_SECRET` into `.env`. Touch these only if changing models or ports. +> Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you +> should see events within a few seconds. Then use the final success message +> below. --- -## 12. Common failure modes and fixes - -When the user reports a problem, walk this list first. - -### "Adrian SDK has not been initialised. Call adrian.init() first." -`get_config()` was called before `init()`. Confirm `adrian.init()` actually ran (not just imported) and that it ran in the same process (not a forked child — see §10). - -### "ws_url is set but no api_key provided" warning + WS rejected -No API key. Set `ADRIAN_API_KEY` or pass `api_key=`. The server hangs up immediately if the bearer token doesn't match a row in the `api_keys` table (or matches a revoked row — see §13). - -### "ToolNode: LoginAck not received within 5s; halting" -The backend never confirmed login within 5 s of the first tool call. SDK refuses to let a tool run without a verified policy, so it returns `[BLOCKED by security policy]`. Causes: backend down, wrong `ws_url`, invalid / revoked key, network firewall. Check `curl /healthz` (HTTP, not WS). - -### `"verdict timeout for tool_call_id=… fail-open"` -The verdict didn't arrive within `block_timeout`. The tool runs anyway (fail-open is deliberate — Adrian never wedges your agent). Bump `block_timeout` or check classifier health (`docker compose logs llm` for self-hosted; managed users should check the dashboard's status page). - -### "Events visible in `events.jsonl` but not in the dashboard" -The local JSONL handler always writes; the WS handler is a separate emitter. Check that `ws_url` resolved correctly (it logs the resolved value in the `Adrian v… initialised` line) and that the API key is for the same backend. +## If Anything Goes Wrong -### "`[BLOCKED by security policy]` appearing for benign tools" -The agent profile / policy is more aggressive than intended. Check **Settings → Agents → ** for the mode (Alert / Human Review / Block), and **Settings → Policy** for which MAD tiers are armed (`policy_m2` / `policy_m3` / `policy_m4`). M3 with `policy_m3=true` in Block mode will halt high-risk verdicts — review them on the Events page. +Keep troubleshooting short. Check these first: -### "RuntimeError: There is no current event loop in thread …" -You're calling `adrian.init()` from sync code. Wrap in `asyncio.run(main())` or use the existing loop (`asyncio.get_event_loop().run_until_complete(...)`). +### Python is too old -### Multiple processes / Celery workers and one worker logs nothing -Each forked worker has to call `adrian.init()` in its own startup hook. The fork handler nulled out the inherited state. +Adrian requires Python `>=3.12`. -### "Adrian not capturing my LangGraph subgraph" -Confirm the subgraph is invoked via `ainvoke` / `astream` and that `auto_instrument=True` (default). If using a custom Runnable that bypasses `Runnable.invoke` (unusual), attach the handler explicitly. +### A secret is missing -### Self-hosted: bootstrap fails or model download stalls -Run `docker compose --profile setup run --rm setup bootstrap --gguf my-model.gguf` after manually placing a Gemma 4 GGUF under `./models/`. The `--gguf` flag skips the interactive download. +Make sure `.env` contains: -### Self-hosted: "lost admin password" -See `https://docs.adrian.secureagentics.ai/reference/backend#reset-the-admin-password`. There's a documented CLI reset that rewrites the bcrypt hash in `data/adrian.db`. - ---- +```env +ADRIAN_API_KEY=... +``` -## 13. API key lifecycle +For the Simple Example Agent, it also needs the selected provider key, unless +the provider is Ollama. -Keys are issued per **Agent Profile**, not per user. Each profile carries the remit / M0 / M3 entries that the classifier compares actions against. Creating a new key for a profile **revokes the previous key for that profile** server-side (the response includes a `revoked_previous` count) and the SDK is kicked off the WS if it was using one of the rotated keys. Rotate by creating a new key; revoke explicitly via the dashboard's Keys table or `DELETE /api/keys/{id}`. +### The dashboard has no event -The plaintext key is returned exactly once at creation (`api_key` field in the create response). After that the backend only has the SHA-256 hash. Lost keys cannot be recovered — issue a new one and rotate clients. +Check: ---- +- the key starts with `adr_live_` +- the script was run after loading `.env` +- `events.jsonl` exists locally, which means Adrian captured the event -## 14. Verifying an install (smoke test) +### The LLM provider fails -When the user says "it doesn't work", before debugging anything else have them run a minimal smoke test using **whatever LLM provider they actually have credentials for** — not necessarily OpenAI. Pick the import and constructor for their provider; everything else stays the same. - -```python -import asyncio, os, adrian -# Swap this import line for the user's actual provider: -# from langchain_openai import ChatOpenAI # OpenAI -# from langchain_anthropic import ChatAnthropic # Anthropic -# from langchain_google_genai import ChatGoogleGenerativeAI # Google -# from langchain_aws import ChatBedrockConverse # AWS Bedrock -# from langchain_openai import AzureChatOpenAI # Azure OpenAI -# from langchain_ollama import ChatOllama # Ollama (local) -from langchain_openai import ChatOpenAI # example - -async def smoke(): - assert os.environ.get("ADRIAN_API_KEY"), "ADRIAN_API_KEY missing" - # Assert the env var for whichever provider was chosen (OPENAI_API_KEY, - # ANTHROPIC_API_KEY, GOOGLE_API_KEY, AZURE_OPENAI_API_KEY, AWS_REGION+creds, ...). - # Ollama needs no key — skip the assert. - adrian.init(log_level="DEBUG") - out = await ChatOpenAI(model="gpt-4o-mini").ainvoke("say ok") - print("LLM:", out.content) - adrian.shutdown() +Check the provider key and package: -asyncio.run(smoke()) -``` +- OpenAI: `OPENAI_API_KEY`, `langchain-openai` +- Anthropic: `ANTHROPIC_API_KEY`, `langchain-anthropic` +- Google Gemini: `GOOGLE_API_KEY`, `langchain-google-genai` +- Azure OpenAI: Azure key, endpoint, deployment, `langchain-openai` +- Ollama: `ollama serve`, local model pulled, `langchain-ollama` -**Do not substitute a fake / mock LLM** here either, even for diagnostic purposes — a fake LLM does not generate the LangChain callbacks Adrian listens for, so a passing fake-LLM smoke test tells you nothing about whether the real instrumentation works. +### A key was pasted into chat or hardcoded -Expected: -1. Log line `Adrian v1.0.0 initialised (handlers=2, ws=ws://…)`. -2. A `LoginAck` debug line showing the resolved mode + policy snapshot. -3. One event in `./events.jsonl` and one event row in the dashboard within ~2 s. +Recommend this cleanup: -If any of those is missing, go back to §12. +1. Revoke that key in the Adrian dashboard or provider dashboard. +2. Create a fresh key. +3. Store the fresh key only in `.env`. +4. Remove hardcoded keys from Python files. --- -## 15. What this guide deliberately does NOT cover - -- **Custom classifiers / training data** — Adrian uses Gemma 4 (E2B/E4B) by default for self-host; the managed cloud runs the same lineage. Swapping classifiers is a self-host backend change (`ADRIAN_LLM_*` vars + restart), not an SDK concern. -- **Source-level changes to the engine, dashboard, or backend** — see `CONTRIBUTING.md` and the per-package `Makefile`s for that. PRs use British English and no em-dashes. -- **Non-LangChain frameworks** — see §7. Today's answer is "bridge through LangChain" or "use manual instrumentation and attach the handler". -- **HTTP transport** — there isn't one. The SDK speaks the binary `ClientFrame` / `ServerFrame` protocol over WebSocket only. Any future HTTP transport will arrive as a new `EventHandler` implementation; for now WS is the only live channel. +## Final Success Message ---- +When setup works, keep the final message simple. If the user only ran the +Simple Example Agent, say: -## 16. Quick reference card (paste into your agent's system prompt) +> Everything worked! You have just connected Adrian to a simple example agent. +> This proves Adrian can receive events from this machine and show them in your +> dashboard. Your real agents are not monitored yet unless we integrated one in +> the next step. -``` -You are advising on Adrian (https://github.com/secureagentics/Adrian), -an OSS runtime security control plane for LangChain / LangGraph agents. - -Facts: - - Dashboard: https://app.adrian.secureagentics.ai - - Docs: https://docs.adrian.secureagentics.ai - - Package: pip install adrian-sdk (import name: adrian) - - Two lines: adrian.init(api_key="adr_live_..."); adrian.shutdown() - - Must run inside asyncio (asyncio.run(main())); block / HITL gating - only fires on ainvoke / astream, not invoke. - - Modes: Alert (no gating), Human Review (waits on /reviews), Block - (halts in-flight, fails open at block_timeout, default 30s). - - MAD codes: M0 benign, M2 misuse (notify), M3 high-risk (block), - M4 malicious (escalate). - - Webhooks: Discord + Slack at launch; alert_type ∈ {"M3","M4","all"}. - - Keys are per Agent Profile; creating a new key revokes the previous. - Plaintext returned ONCE. - - PII redaction is always on; no opt-out. - - Self-host: docker compose --profile setup run --rm setup bootstrap - then docker compose --profile llm up -d. Dashboard at :3000. - - Default ws_url = ws://localhost:8080/ws (self-host); managed users - set ADRIAN_WS_URL from their dashboard. - -Behavioural rules (these override default LLM tendencies): - - LLM-AGNOSTIC: ask the user which provider they want (OpenAI, - Anthropic, Google, AWS Bedrock, Azure, Ollama, Groq, Mistral, …). - Never assume OpenAI. Whatever has a langchain- package - works. - - NEVER suggest or use a fake / mock / stub LLM (no FakeListChatModel, - FakeChatModel, etc.). It generates no real callbacks, so the - instrumentation is invisible. If the user has no provider key at - all, help them get one — do not invent a workaround. - - CROSS-CONFIRM every branching decision (provider, key storage path, - next steps). Do not silently pick defaults. - - SAME STORAGE for every secret in the flow: if the Adrian key went - into .env, the provider key goes into the same .env; if the Adrian - key was inlined with a TODO marker, the provider key is inlined the - same way. - - EXPLAIN code in plain English before asking the user to run it. - -Refuse to fabricate framework support that is not in §7 of the guide. -When in doubt, read backend/internal/alerts/alerts.json for the exact -MAD definitions or point the user at the Discord. -``` - ---- +If the user integrated one of their real agents, say: -*Last verified against the in-tree code on 2026-05-20. If the SDK version (`adrian.__version__`) has moved past 1.0.0, re-check §5 and §12 against the new release notes before quoting line-numbered behaviour.* +> Everything worked! You have just integrated a security monitoring system +> around an agent. Every action that agent takes can now appear in your Adrian +> dashboard, where Adrian assesses whether the action looks safe or dangerous. +> You can use **Configurations** to give Adrian more context about the agent it +> is monitoring, choose whether to block dangerous actions or only receive +> alerts, and set up alerting through Discord or Slack. diff --git a/GET_STARTED_AI_GUIDE_V2.md b/GET_STARTED_AI_GUIDE_V2.md deleted file mode 100644 index e1a8f39..0000000 --- a/GET_STARTED_AI_GUIDE_V2.md +++ /dev/null @@ -1,519 +0,0 @@ -# Adrian - Quick Start AI Guide V2 - -> For humans: this is a short setup guide you can give to an AI assistant -> such as Claude, Codex, Cursor, Windsurf, or ChatGPT. It is intentionally -> narrow: managed Adrian app only, local `.env` secrets only, and the fewest -> prompts needed to get your first event into the dashboard. -> -> For AI assistants: follow this guide in order. Keep the language simple. -> Move quickly, but do not hide what you are doing. - ---- - -## What This Guide Is Allowed To Do - -This guide is designed to be easy to review. It only asks the assistant to: - -- check Python is available -- help the user create an Adrian API key in the managed dashboard -- create or update a local `.env` file for secrets -- add `.env` to `.gitignore` if the project uses git and it is not already ignored -- create a tiny `adrian_quickstart.py` example file, or edit one existing agent file the user points to -- create a Python virtual environment and install `adrian-sdk` plus the selected LangChain provider package -- run the example or the user's agent - -This guide does not ask the assistant to delete files, upload code, change git -history, edit backend services, or paste API keys into chat. - ---- - -## Simple Rules For The Assistant - -1. Use the managed Adrian app: `https://app.adrian.secureagentics.ai`. -2. Store secrets in `.env` only. Do not ask the user to paste API keys into chat. -3. Do not write API keys directly into Python files. -4. Use a real LLM provider. Do not use fake, mock, or stub LLMs. -5. Pause only when user input is actually needed. -6. Explain file changes in one short paragraph before making them. -7. If a key has already been pasted into chat or hardcoded in a file, recommend - revoking it and creating a fresh key stored in `.env`. -8. When editing `.env`, stop immediately after opening the file. Wait for the - user to confirm they saved it before installing packages, creating scripts, - or running anything. - -Planned pause points: - -1. Ask the user to confirm they have copied their Adrian API key from the - dashboard. -2. Ask the user to save the Adrian API key in `.env`. -3. Ask whether to use the Simple Example Agent or integrate an existing agent. -4. If using the Simple Example Agent, ask which LLM provider to use. -5. Ask the user to save the selected provider values in `.env`. - -Do not add extra confirmation prompts unless something is unclear or risky. - ---- - -## Step 0 - Check The Folder And Python - -Run these in the current folder: - -```sh -pwd -python3 --version -python3 -m pip --version -``` - -If Python is older than 3.12, stop and ask the user to install Python 3.12 or -newer. Adrian's SDK requires Python `>=3.12`. - -Tell the user the absolute folder path. Example: - -> I will set Adrian up in `/absolute/path/to/project`. The `.env`, -> virtual environment, and quickstart file will live here. - ---- - -## Step 1 - Get The Adrian API Key - -Ask the user to open the managed dashboard: - -`https://app.adrian.secureagentics.ai` - -If this is their first time signing in: - -1. Sign up with Google, Microsoft, or GitHub. -2. Follow the first-time onboarding until Adrian shows an API key. -3. Copy the API key. It starts with `adr_live_`. -4. Skip detailed agent configuration for now. The quickstart only needs the API - key. The SDK handles the live Adrian connection automatically. - -If they already have an account: - -1. Go to **Configurations**. -2. Open the agent/API key area. -3. Create or copy an agent API key. - -Tell the user: - -> Keep the key copied somewhere local for the next step. Do not paste it into -> this chat. We will put it into `.env` on your machine. - -Pause here and ask: - -> Do you have your Adrian API key ready? - ---- - -## Step 2 - Create `.env` - -Create a local `.env` file in the working folder, write the Adrian placeholder -line into it, then open it for the user. Use the absolute path when creating or -opening the file so the user knows exactly where it lives. - -If the project has a `.gitignore`, make sure it contains: - -```gitignore -.env -``` - -Use the command for the user's operating system. These commands put the -placeholder in the file before the editor opens, so the user does not need to -copy anything from chat and overwrite the API key on their clipboard. Replace -`/absolute/path/to/project` or `C:\absolute\path\to\project` with the working -folder from Step 0. - -If the assistant can run shell commands, it should run this command itself so -the user's clipboard can keep the Adrian API key. - -```sh -# macOS -ENV_FILE="/absolute/path/to/project/.env" -grep -q '^ADRIAN_API_KEY=' "$ENV_FILE" 2>/dev/null || printf 'ADRIAN_API_KEY=adr_live_replace_this\n' >> "$ENV_FILE" -open -a TextEdit "$ENV_FILE" -``` - -```sh -# Linux -ENV_FILE="/absolute/path/to/project/.env" -grep -q '^ADRIAN_API_KEY=' "$ENV_FILE" 2>/dev/null || printf 'ADRIAN_API_KEY=adr_live_replace_this\n' >> "$ENV_FILE" -${EDITOR:-nano} "$ENV_FILE" -``` - -```powershell -# Windows PowerShell -$envFile = "C:\absolute\path\to\project\.env" -if (!(Test-Path $envFile) -or -not (Select-String -Path $envFile -Pattern '^ADRIAN_API_KEY=' -Quiet)) { - Add-Content -Path $envFile -Value 'ADRIAN_API_KEY=adr_live_replace_this' -} -notepad.exe $envFile -``` - -Ask the user to replace the placeholder in the file with their real Adrian API -key: - -```env -ADRIAN_API_KEY=adr_live_replace_this -``` - -Then say: - -> Save `.env`, close the editor, and come back here when you are done. Do not -> paste the key into chat. - -Stop here. Do not continue to agent selection until the user confirms the file -is saved. - -After they confirm, verify without printing full secrets: - -- `ADRIAN_API_KEY` exists and starts with `adr_live_` -- there are no quote marks around the values -- there are no placeholder values left - -If the assistant can read files, it may check `.env` directly but must not echo -the key back to the chat. - ---- - -## Step 3 - Choose The Agent To Run - -Ask: - -> Adrian needs an agent to monitor. Do you want to: -> -> **A. Simple Example Agent** - use a tiny example agent that asks an LLM: -> "In one sentence, why is the sky blue?" Fastest route to your first event. -> -> **B. Integrate Adrian with one of my existing agents** - point me at your -> LangChain or LangGraph agent and I will integrate Adrian with it. - -If the user chooses A, continue to Step 4A. - -If the user chooses B, continue to Step 4B. - ---- - -## Step 4A - Simple Example Agent - -Ask: - -> In order to set up the example agent, you need to provide an LLM. Pick your -> preference: -> -> **a. OpenAI** - needs `OPENAI_API_KEY`; package `langchain-openai` -> **b. Anthropic** - needs `ANTHROPIC_API_KEY`; package `langchain-anthropic` -> **c. Google Gemini** - needs `GOOGLE_API_KEY`; package `langchain-google-genai` -> **d. Azure OpenAI** - needs `AZURE_OPENAI_API_KEY`, -> `AZURE_OPENAI_ENDPOINT`, and `AZURE_OPENAI_DEPLOYMENT`; package -> `langchain-openai` -> **e. Ollama** - no API key; usually runs locally at -> `http://localhost:11434`; package `langchain-ollama` - -For the chosen provider, write exactly one matching provider block into `.env` -before opening the editor. Do not ask the user to copy the block from chat. If a -provider block already exists, update it instead of adding duplicates. - -Then open `.env` again using the same editor style from Step 2, and ask the -user to replace the provider placeholder with their real provider key. - -Use only the matching block: - -```env -# OpenAI -LLM_PROVIDER=openai -OPENAI_API_KEY=sk_replace_this - -# Anthropic -LLM_PROVIDER=anthropic -ANTHROPIC_API_KEY=sk-ant-replace-this - -# Google Gemini -LLM_PROVIDER=google -GOOGLE_API_KEY=replace_this - -# Azure OpenAI -LLM_PROVIDER=azure -AZURE_OPENAI_API_KEY=replace_this -AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com -AZURE_OPENAI_DEPLOYMENT=your-deployment-name - -# Ollama -LLM_PROVIDER=ollama -OLLAMA_MODEL=llama3.2 -``` - -Then say: - -> Save `.env`, close the editor, and come back here when you are done. Do not -> paste the provider key into chat. - -Stop here. Do not install packages or create `adrian_quickstart.py` until the -user confirms `.env` is saved. - -After they confirm, verify without printing full secrets: - -- `LLM_PROVIDER` is one of `openai`, `anthropic`, `google`, `azure`, or `ollama` -- the selected provider's required values exist -- there are no placeholder values left for the selected provider - -For Ollama, check whether it is running: - -```sh -curl http://localhost:11434/api/tags -``` - -If Ollama is not running, ask the user to run: - -```sh -ollama serve -ollama pull llama3.2 -``` - -### Install - -Create a virtual environment and install the SDK plus the provider package: - -```sh -python3 -m venv .venv -source .venv/bin/activate -pip install adrian-sdk langchain-openai -``` - -Replace `langchain-openai` with the package for the chosen provider. - -### Create `adrian_quickstart.py` - -Before writing the file, say: - -> I am going to create a tiny example agent. It initializes Adrian, asks an LLM -> "In one sentence, why is the sky blue?", prints the answer, and sends the -> event to your Adrian dashboard. - -Use this file: - -```python -import asyncio -import os -import sys - -import adrian - - -def require_env(name: str) -> str: - value = os.environ.get(name) - if not value: - sys.exit(f"{name} is missing. Add it to .env and run again.") - return value - - -def build_llm(): - provider = os.environ.get("LLM_PROVIDER", "openai").strip().lower() - - if provider == "openai": - require_env("OPENAI_API_KEY") - from langchain_openai import ChatOpenAI - - return ChatOpenAI(model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini")) - - if provider == "anthropic": - require_env("ANTHROPIC_API_KEY") - from langchain_anthropic import ChatAnthropic - - return ChatAnthropic( - model=os.environ.get("ANTHROPIC_MODEL", "claude-3-5-haiku-latest") - ) - - if provider == "google": - require_env("GOOGLE_API_KEY") - from langchain_google_genai import ChatGoogleGenerativeAI - - return ChatGoogleGenerativeAI( - model=os.environ.get("GOOGLE_MODEL", "gemini-1.5-flash") - ) - - if provider == "azure": - require_env("AZURE_OPENAI_API_KEY") - require_env("AZURE_OPENAI_ENDPOINT") - deployment = require_env("AZURE_OPENAI_DEPLOYMENT") - from langchain_openai import AzureChatOpenAI - - return AzureChatOpenAI( - azure_deployment=deployment, - api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"), - ) - - if provider == "ollama": - from langchain_ollama import ChatOllama - - return ChatOllama( - model=os.environ.get("OLLAMA_MODEL", "llama3.2"), - base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"), - ) - - sys.exit( - "Unsupported LLM_PROVIDER. Use openai, anthropic, google, azure, or ollama." - ) - - -async def main() -> int: - require_env("ADRIAN_API_KEY") - - adrian.init() - llm = build_llm() - response = await llm.ainvoke("In one sentence, why is the sky blue?") - print(response.content) - adrian.shutdown() - return 0 - - -if __name__ == "__main__": - raise SystemExit(asyncio.run(main())) -``` - -### Run - -Load `.env` and run the example: - -```sh -set -a -. ./.env -set +a -python adrian_quickstart.py -``` - -If it prints an answer, say: - -> Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you -> should see the event within a few seconds. - -Then ask: - -> Want me to integrate Adrian with one of your real agents now? If yes, send me -> the path to your LangChain or LangGraph agent file. If not, you can stop here. - -If the user says yes or sends a file path, continue to Step 4B. If the user -stops, use the final success message below. - -If the event does not appear, go to "If Anything Goes Wrong" below. - ---- - -## Step 4B - Integrate Adrian With An Existing Agent - -Ask: - -> Please give me the absolute path to your LangChain or LangGraph agent file. -> If your assistant cannot read outside this folder, copy the agent file into -> this project and point me at that copy. - -Read the file and check: - -- it is Python -- it imports or uses LangChain or LangGraph -- it has an entry point such as `main()`, `async def main()`, or code that calls - `.invoke()`, `.ainvoke()`, or `.astream()` - -Before editing, say: - -> I will integrate Adrian with this agent by importing `adrian`, initializing it -> near the start of the agent run, and keeping secrets in `.env`. - -Patch the file: - -1. Add `import adrian` near the imports. -2. Add this before the model, chain, or graph is created or called: - -```python -adrian.init() -``` - -If the agent has a clear shutdown path, add: - -```python -adrian.shutdown() -``` - -If it is a long-running app, mention that `adrian.shutdown()` should be called -from the app's normal shutdown hook. - -Install the SDK in the existing environment: - -```sh -pip install adrian-sdk -``` - -Run the agent the way the user normally runs it, after loading `.env`: - -```sh -set -a -. ./.env -set +a -# then run the user's normal agent command -``` - -If the agent runs, say: - -> Everything worked! Open `https://app.adrian.secureagentics.ai/events` and you -> should see events within a few seconds. Then use the final success message -> below. - ---- - -## If Anything Goes Wrong - -Keep troubleshooting short. Check these first: - -### Python is too old - -Adrian requires Python `>=3.12`. - -### A secret is missing - -Make sure `.env` contains: - -```env -ADRIAN_API_KEY=... -``` - -For the Simple Example Agent, it also needs the selected provider key, unless -the provider is Ollama. - -### The dashboard has no event - -Check: - -- the key starts with `adr_live_` -- the script was run after loading `.env` -- `events.jsonl` exists locally, which means Adrian captured the event - -### The LLM provider fails - -Check the provider key and package: - -- OpenAI: `OPENAI_API_KEY`, `langchain-openai` -- Anthropic: `ANTHROPIC_API_KEY`, `langchain-anthropic` -- Google Gemini: `GOOGLE_API_KEY`, `langchain-google-genai` -- Azure OpenAI: Azure key, endpoint, deployment, `langchain-openai` -- Ollama: `ollama serve`, local model pulled, `langchain-ollama` - -### A key was pasted into chat or hardcoded - -Recommend this cleanup: - -1. Revoke that key in the Adrian dashboard or provider dashboard. -2. Create a fresh key. -3. Store the fresh key only in `.env`. -4. Remove hardcoded keys from Python files. - ---- - -## Final Success Message - -When setup works, keep the final message simple: - -> Everything worked! You have just integrated a security monitoring system -> around an agent. Every action that agent takes can now appear in your Adrian -> dashboard, where Adrian assesses whether the action looks safe or dangerous. -> You can use **Configurations** to give Adrian more context about the agent it -> is monitoring, choose whether to block dangerous actions or only receive -> alerts, and set up alerting through Discord or Slack. diff --git a/README.md b/README.md index 9e69b64..d4d50a1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ https://github.com/user-attachments/assets/96974b9d-4862-41ac-a499-ef5cfe76e16a ## Quickstart -The fastest way to try Adrian is the managed dashboard at [app.adrian.secureagentics.ai](https://app.adrian.secureagentics.ai). Sign-up takes a minute and there is nothing to install beyond the SDK. To run Adrian on your own infrastructure instead, jump to [Self-hosting](#self-hosting) below. +> **Want the stupidly simple, 60-second hands-off install?** Feed your coding agent (Claude, Codex, Cursor, etc.) this file: [GET_STARTED_AI_GUIDE.md](https://github.com/secureagentics/Adrian/blob/main/GET_STARTED_AI_GUIDE.md). It will walk you through the installation process. + +The next fastest way to try Adrian is the managed dashboard at [app.adrian.secureagentics.ai](https://app.adrian.secureagentics.ai). Sign-up takes a minute and there is nothing to install beyond the SDK. To run Adrian on your own infrastructure instead, jump to [Self-hosting](#self-hosting) below. 1. Sign up at [app.adrian.secureagentics.ai](https://app.adrian.secureagentics.ai) and generate an API key. From 2dfa2056dde0e3633288ce7cc8f22d8c3aaf76e3 Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Mon, 25 May 2026 15:43:59 +0500 Subject: [PATCH 8/9] Added the warning for always reviews instructions manually --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4d50a1..9be6664 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ https://github.com/user-attachments/assets/96974b9d-4862-41ac-a499-ef5cfe76e16a ## Quickstart -> **Want the stupidly simple, 60-second hands-off install?** Feed your coding agent (Claude, Codex, Cursor, etc.) this file: [GET_STARTED_AI_GUIDE.md](https://github.com/secureagentics/Adrian/blob/main/GET_STARTED_AI_GUIDE.md). It will walk you through the installation process. +> **Want the stupidly simple, 60-second hands-off install?** Feed your coding agent (Claude, Codex, Cursor, etc.) this file: [GET_STARTED_AI_GUIDE.md](https://github.com/secureagentics/Adrian/blob/main/GET_STARTED_AI_GUIDE.md). It will walk you through the installation process. Always review instructions manually The next fastest way to try Adrian is the managed dashboard at [app.adrian.secureagentics.ai](https://app.adrian.secureagentics.ai). Sign-up takes a minute and there is nothing to install beyond the SDK. To run Adrian on your own infrastructure instead, jump to [Self-hosting](#self-hosting) below. From f4c1373780d888126644c4b56555a3ada9f626cd Mon Sep 17 00:00:00 2001 From: FaiziKhan2020 Date: Wed, 27 May 2026 00:43:39 +0500 Subject: [PATCH 9/9] fix(ws): bind session subscribers to owners What changed: the WebSocket hub now records a server-derived route owner for each session_id and rejects attempts by a different authenticated owner to register the same session. The route owner is the agent_profile_id when present, with api_key_id as the fallback for unprofiled keys. Conflicting reuse closes the second connection with a policy-violation close code while preserving the original subscriber. Why changed: the previous routing key was only the client-supplied session_id, so any valid SDK key could claim another client session_id and receive verdict/HITL responses meant for that original SDK connection. Why not bind to raw API key only: agent-profile keys are rotated during normal dashboard flows. Binding only to api_key_id would fix takeover but could break same-agent reconnect/rotation continuity. Using agent_profile_id first preserves that path while still preventing cross-agent takeover. Tests: added hub and live WebSocket regression coverage for different owners sharing a session_id, updated HITL hub subscriber tests, and ran go test ./... in the backend via golang:1.25-alpine. --- backend/internal/api/handlers_test.go | 5 +- backend/internal/ws/frames.go | 1 + backend/internal/ws/handler.go | 20 ++++- backend/internal/ws/handler_test.go | 102 ++++++++++++++++++++++++++ backend/internal/ws/hub.go | 50 ++++++++----- backend/internal/ws/hub_test.go | 48 +++++++++++- backend/internal/ws/session.go | 13 ++++ 7 files changed, 215 insertions(+), 24 deletions(-) diff --git a/backend/internal/api/handlers_test.go b/backend/internal/api/handlers_test.go index 1a4437d..a361c21 100644 --- a/backend/internal/api/handlers_test.go +++ b/backend/internal/api/handlers_test.go @@ -505,7 +505,10 @@ func TestApproveReviewPublishesToSubscriber(t *testing.T) { } // Fake SDK subscriber. - ch, dereg := hub.Register(sessID) + ch, dereg, err := hub.Register(sessID, "test-owner") + if err != nil { + t.Fatalf("Register: %v", err) + } defer dereg() resp := postJSON(t, srv, cookie, "/api/reviews/"+queueID+"/approve", map[string]any{}) diff --git a/backend/internal/ws/frames.go b/backend/internal/ws/frames.go index f7ae604..23c1e91 100644 --- a/backend/internal/ws/frames.go +++ b/backend/internal/ws/frames.go @@ -21,6 +21,7 @@ const schemaVersion = 2 // plus our application-specific 4xxx codes. const ( closeProtocolError = 1002 + closePolicyViolation = 1008 closeInternalServerErr = 1011 closeQuotaExhausted = 4003 ) diff --git a/backend/internal/ws/handler.go b/backend/internal/ws/handler.go index 29a16a5..6db577d 100644 --- a/backend/internal/ws/handler.go +++ b/backend/internal/ws/handler.go @@ -99,7 +99,25 @@ func serve(ctx context.Context, conn *websocket.Conn, sess *session, st *store.S // verdicts and HITL resolutions land here, drained by the writer // goroutine. The LoginAck is written directly inside handleLogin // (single goroutine, pre-register, no concurrency to serialise). - hubCh, deregister := hub.Register(sess.sessionID) + hubCh, deregister, err := hub.Register(sess.sessionID, sess.routeOwner()) + if err != nil { + if errors.Is(err, ErrSessionOwnerConflict) { + slog.WarnContext(ctx, "ws.session_owner_conflict", + "session_id", sess.sessionID, + "api_key_id", sess.apiKey.ID, + "route_owner", sess.routeOwner(), + ) + closeWith(conn, closePolicyViolation, "session_id already active for another owner") + return + } + slog.ErrorContext(ctx, "ws.session_register_failed", + "error", err, + "session_id", sess.sessionID, + "api_key_id", sess.apiKey.ID, + ) + closeWith(conn, closeInternalServerErr, "internal error") + return + } writerDone := make(chan struct{}) go func() { defer close(writerDone) diff --git a/backend/internal/ws/handler_test.go b/backend/internal/ws/handler_test.go index a4f9142..4cdf544 100644 --- a/backend/internal/ws/handler_test.go +++ b/backend/internal/ws/handler_test.go @@ -235,6 +235,97 @@ func TestAlertModeNoFanOut(t *testing.T) { } } +func TestSessionIDReuseDifferentOwnerDoesNotStealVerdicts(t *testing.T) { + db := openInMemoryDB(t) + t.Cleanup(func() { _ = db.Close() }) + + st := store.New(db) + plaintextKeyA := "adr_local_test_key_owner_a" + plaintextKeyB := "adr_local_test_key_owner_b" + insertAPIKeyWithProfile(t, db, sha256Hex(plaintextKeyA), "agent-profile-a") + insertAPIKeyWithProfile(t, db, sha256Hex(plaintextKeyB), "agent-profile-b") + + if _, err := db.Exec(`UPDATE policies SET mode = 'block' WHERE id = 1`); err != nil { + t.Fatalf("set mode=block: %v", err) + } + + mux := http.NewServeMux() + mux.Handle("/ws", ws.AuthMiddleware(st)(ws.NewHandler(st, &fakeClassifier{}, ws.NewHub(), nil, nil))) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws" + connA, _, err := websocket.DefaultDialer.Dial(wsURL, http.Header{ + "Authorization": {"Bearer " + plaintextKeyA}, + }) + if err != nil { + t.Fatalf("dial client A: %v", err) + } + t.Cleanup(func() { _ = connA.Close() }) + + const sessionID = "shared-session-takeover-test" + if err := writeProto(connA, &bpb.ClientFrame{ + Frame: &bpb.ClientFrame_Login{Login: &bpb.SessionLogin{ + SessionId: sessionID, SchemaVersion: 2, + }}, + }); err != nil { + t.Fatalf("send login client A: %v", err) + } + if _, err := readServerFrame(connA); err != nil { + t.Fatalf("read login_ack client A: %v", err) + } + + connB, _, err := websocket.DefaultDialer.Dial(wsURL, http.Header{ + "Authorization": {"Bearer " + plaintextKeyB}, + }) + if err != nil { + t.Fatalf("dial client B: %v", err) + } + t.Cleanup(func() { _ = connB.Close() }) + if err := writeProto(connB, &bpb.ClientFrame{ + Frame: &bpb.ClientFrame_Login{Login: &bpb.SessionLogin{ + SessionId: sessionID, SchemaVersion: 2, + }}, + }); err != nil { + t.Fatalf("send login client B: %v", err) + } + if _, err := readServerFrame(connB); err != nil { + t.Fatalf("read login_ack client B: %v", err) + } + if err := connB.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { + t.Fatalf("set client B deadline: %v", err) + } + if _, _, err := connB.ReadMessage(); err == nil { + t.Fatal("expected conflicting client B to be closed") + } else if closeErr, ok := err.(*websocket.CloseError); !ok || closeErr.Code != websocket.ClosePolicyViolation { + t.Fatalf("client B close err = %v, want close code %d", err, websocket.ClosePolicyViolation) + } + + eventID := uuid.NewString() + if err := writeProto(connA, &bpb.ClientFrame{ + Frame: &bpb.ClientFrame_PairedBatch{PairedBatch: &bpb.PairedEventBatch{ + Events: []*bpb.PairedEvent{{ + EventId: eventID, SessionId: sessionID, + PairType: bpb.PairType_PAIR_TYPE_TOOL, + Agent: &bpb.AgentContext{AgentId: "owner-a-agent"}, + Data: &bpb.PairedEvent_Tool{Tool: &bpb.ToolPairData{ + ToolName: "noop", ToolCallId: "tc-owner-a", Input: "{}", Output: "ok", + }}, + }}, + }}, + }); err != nil { + t.Fatalf("send paired_batch client A: %v", err) + } + + verdict, err := readServerFrame(connA) + if err != nil { + t.Fatalf("read verdict client A: %v", err) + } + if got := verdict.GetVerdict(); got == nil || got.EventId != eventID { + t.Fatalf("client A verdict = %+v, want event_id %q", got, eventID) + } +} + // TestRevokeKicksLiveWS asserts the security guarantee: an open WS // authenticated with key X gets terminated within seconds when X is // revoked, not at next-disconnect-whenever. Drives the path the REST @@ -403,6 +494,17 @@ func insertAPIKey(t *testing.T, db *sql.DB, hashHex string) { } } +func insertAPIKeyWithProfile(t *testing.T, db *sql.DB, hashHex, agentProfileID string) { + t.Helper() + _, err := db.Exec( + `INSERT INTO api_keys (id, key_hash, prefix, label, agent_profile_id) VALUES (?, ?, ?, ?, ?)`, + uuid.NewString(), hashHex, "adr_local_te", "test", agentProfileID, + ) + if err != nil { + t.Fatalf("insert api_keys: %v", err) + } +} + func writeProto(conn *websocket.Conn, msg proto.Message) error { buf, err := proto.Marshal(msg) if err != nil { diff --git a/backend/internal/ws/hub.go b/backend/internal/ws/hub.go index fe2c129..7bf5d86 100644 --- a/backend/internal/ws/hub.go +++ b/backend/internal/ws/hub.go @@ -4,6 +4,7 @@ package ws import ( + "errors" "sync" "google.golang.org/protobuf/proto" @@ -11,40 +12,53 @@ import ( pb "github.com/secureagentics/Adrian/backend/internal/proto" ) +// ErrSessionOwnerConflict is returned when another logical client owner +// already holds the subscriber slot for a session_id. +var ErrSessionOwnerConflict = errors.New("session_id already registered by another owner") + +type subscriber struct { + owner string + ch chan []byte +} + // Hub is a process-local pub/sub keyed by session_id. The WS handler // for each connected SDK registers a write channel; the REST review // approve/reject path publishes a HITL-resolution Verdict frame to it. // -// Single subscriber per session_id, re-Register replaces any prior -// channel (covers SDK reconnect during a hold). On disconnect the WS -// handler must call deregister to free the slot. +// Single subscriber per session_id. Re-register by the same server-derived +// owner replaces the prior channel (SDK reconnect / key rotation). A +// different owner claiming the same session_id is rejected so it cannot steal +// verdict or HITL routing. type Hub struct { mu sync.Mutex - subs map[string]chan []byte + subs map[string]subscriber } // NewHub returns a fresh hub. func NewHub() *Hub { - return &Hub{subs: make(map[string]chan []byte)} + return &Hub{subs: make(map[string]subscriber)} } -// Register adds a subscriber for sessionID and returns its write +// Register adds a subscriber for sessionID + owner and returns its write // channel plus a deregister callback. The caller spawns a writer // goroutine that drains the channel and calls conn.WriteMessage. // -// If a prior subscriber exists for the same session_id, its channel -// is closed so its writer goroutine exits cleanly. (Concurrent -// connections under one session_id are not a normal case; an SDK -// reconnect during a hold is the realistic path.) -func (h *Hub) Register(sessionID string) (<-chan []byte, func()) { +// If a prior subscriber exists for the same session_id and owner, its channel +// is closed so its writer goroutine exits cleanly. If the existing subscriber +// belongs to another owner, registration fails and the old channel remains +// active. +func (h *Hub) Register(sessionID, owner string) (<-chan []byte, func(), error) { h.mu.Lock() defer h.mu.Unlock() if old, ok := h.subs[sessionID]; ok { - close(old) + if old.owner != owner { + return nil, nil, ErrSessionOwnerConflict + } + close(old.ch) } ch := make(chan []byte, 8) - h.subs[sessionID] = ch + h.subs[sessionID] = subscriber{owner: owner, ch: ch} deregister := func() { h.mu.Lock() @@ -52,12 +66,12 @@ func (h *Hub) Register(sessionID string) (<-chan []byte, func()) { // Only delete + close when the entry is still ours; a later // Register may have replaced it and already closed the prior // channel. - if cur, ok := h.subs[sessionID]; ok && cur == ch { + if cur, ok := h.subs[sessionID]; ok && cur.ch == ch { delete(h.subs, sessionID) close(ch) } } - return ch, deregister + return ch, deregister, nil } // Publish marshals and pushes a frame to the subscriber for sessionID. @@ -70,13 +84,13 @@ func (h *Hub) Publish(sessionID string, frame *pb.ServerFrame) bool { return false } h.mu.Lock() - ch, ok := h.subs[sessionID] - h.mu.Unlock() + defer h.mu.Unlock() + sub, ok := h.subs[sessionID] if !ok { return false } select { - case ch <- buf: + case sub.ch <- buf: return true default: return false diff --git a/backend/internal/ws/hub_test.go b/backend/internal/ws/hub_test.go index 3c33a39..17a5c18 100644 --- a/backend/internal/ws/hub_test.go +++ b/backend/internal/ws/hub_test.go @@ -4,6 +4,7 @@ package ws import ( + "errors" "testing" "google.golang.org/protobuf/proto" @@ -13,7 +14,10 @@ import ( func TestHubPublishDeliversToSubscriber(t *testing.T) { h := NewHub() - ch, dereg := h.Register("sess-1") + ch, dereg, err := h.Register("sess-1", "owner-1") + if err != nil { + t.Fatalf("Register: %v", err) + } defer dereg() frame := &pb.ServerFrame{Frame: &pb.ServerFrame_Verdict{Verdict: &pb.Verdict{ @@ -45,11 +49,17 @@ func TestHubPublishNoSubscriberReturnsFalse(t *testing.T) { func TestHubReRegisterClosesPriorChannel(t *testing.T) { h := NewHub() - first, _ := h.Register("sess-x") + first, _, err := h.Register("sess-x", "owner-1") + if err != nil { + t.Fatalf("first Register: %v", err) + } // New register replaces the slot; the old channel must close so a // writer goroutine reading from it exits cleanly. - second, dereg := h.Register("sess-x") + second, dereg, err := h.Register("sess-x", "owner-1") + if err != nil { + t.Fatalf("second Register: %v", err) + } defer dereg() if _, ok := <-first; ok { @@ -67,9 +77,39 @@ func TestHubReRegisterClosesPriorChannel(t *testing.T) { } } +func TestHubRejectsReRegisterFromDifferentOwner(t *testing.T) { + h := NewHub() + first, dereg, err := h.Register("sess-x", "owner-1") + if err != nil { + t.Fatalf("first Register: %v", err) + } + defer dereg() + + second, secondDereg, err := h.Register("sess-x", "owner-2") + if !errors.Is(err, ErrSessionOwnerConflict) { + t.Fatalf("Register err = %v, want ErrSessionOwnerConflict", err) + } + if second != nil || secondDereg != nil { + t.Fatal("conflicting Register returned a subscriber") + } + + if !h.Publish("sess-x", &pb.ServerFrame{ + Frame: &pb.ServerFrame_Verdict{Verdict: &pb.Verdict{EventId: "ev-y"}}, + }) { + t.Fatal("Publish to original subscriber should still succeed") + } + got := <-first + if len(got) == 0 { + t.Fatal("expected non-empty frame delivered to original subscriber") + } +} + func TestHubDeregisterRemovesEntry(t *testing.T) { h := NewHub() - _, dereg := h.Register("sess-d") + _, dereg, err := h.Register("sess-d", "owner-1") + if err != nil { + t.Fatalf("Register: %v", err) + } dereg() if h.Publish("sess-d", &pb.ServerFrame{ Frame: &pb.ServerFrame_Verdict{Verdict: &pb.Verdict{}}, diff --git a/backend/internal/ws/session.go b/backend/internal/ws/session.go index f10ed69..cb251a3 100644 --- a/backend/internal/ws/session.go +++ b/backend/internal/ws/session.go @@ -25,3 +25,16 @@ func (s *session) agentProfileID() *string { } return s.apiKey.AgentProfileID } + +// routeOwner returns the server-authenticated logical owner for hub routing. +// Agent-profile keys may rotate, so profile ownership is preferred over raw +// key ID to preserve reconnect continuity. Unprofiled keys fall back to key ID. +func (s *session) routeOwner() string { + if s.apiKey == nil { + return "" + } + if s.apiKey.AgentProfileID != nil && *s.apiKey.AgentProfileID != "" { + return "agent_profile:" + *s.apiKey.AgentProfileID + } + return "api_key:" + s.apiKey.ID +}