# Loopers OSS – Complete Technical Context for AI Agents This file contains everything an AI agent needs to understand, set up, and integrate Loopers OSS from scratch. It is sourced directly from the codebase and documentation. --- ## What Loopers Is Loopers is the open-source, bare-metal AI Firewall for the Agentic Era written in Go. It sits between your application and upstream LLM providers (OpenAI, Anthropic, Gemini, Groq, Bedrock, Azure OpenAI, Mistral, Cohere, DeepSeek, Together, Ollama, Fireworks, xAI, vLLM, OpenRouter) and enforces pre-call budget limits, runaway loop termination, MCP tool response inspection, persistent agent risk scoring, and real-time outbound semantic DLP redaction — all before forwarding requests upstream. **Core guarantee:** If Redis is unavailable, Loopers fails closed. No requests pass through. This prevents unbounded spending during infrastructure failures. --- ## Prerequisites - Go 1.26.6+ (toolchain go1.26.6) — required for native installation - Redis 7+ — required for all deployments (used for atomic budget reservation) - Docker + Docker Compose — recommended for running Redis locally --- ## Setup Method 1: Docker (Recommended) ### Step 1: Clone the repository ```bash git clone https://github.com/CURSED-ME/loopers-oss.git cd loopers-oss ``` ### Step 2: Start Redis and the Loopers firewall ```bash docker-compose up -d ``` The `docker-compose.yml` in the root starts two services: - `redis` — Redis 8 Alpine with password auth - `loopers` — the firewall image (`ghcr.io/cursed-me/loopers:latest`) on port 8080 ### Step 3: Create a proxy key ```bash docker-compose exec loopers /app/loopers keys create --name mykey --provider openai ``` This returns a raw key (`lp-xxx`) and a hash. The raw key is only shown once — save it immediately. ### Step 4: Set a budget ```bash docker-compose exec loopers /app/loopers budget set --daily 10.00 --hourly 2.00 ``` ### Step 5: Route your first request ```bash curl -X POST http://localhost:8080/openai/v1/chat/completions \ -H "Authorization: Bearer lp-xxx" \ -H "X-Loopers-Provider-Key: YOUR_REAL_OPENAI_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}]}' ``` --- ## Setup Method 2: Native Installation (Go 1.26.6+) ### Step 1: Install the CLI globally macOS / Linux / Windows: ```bash go install github.com/CURSED-ME/loopers-oss/cmd/loopers@latest ``` ### Step 2: Initialize configuration ```bash loopers init ``` This generates `loopers.yaml` (firewall config) and `docker-compose.yml` (Redis) in the current directory. ### Step 3: Start Redis only ```bash docker-compose up -d redis ``` > If you already have Redis running locally (e.g. via Homebrew, apt, or a remote instance), skip this step. Set `redis.addr` in `loopers.yaml` to point to your instance. ### Step 4: Start the firewall runtime server macOS / Linux: ```bash SERVER_INSECURE_DEV=true loopers serve ``` Windows (PowerShell): ```powershell $env:SERVER_INSECURE_DEV="true"; loopers serve ``` Windows (Command Prompt): ```cmd set SERVER_INSECURE_DEV=true && loopers serve ``` `SERVER_INSECURE_DEV=true` disables the TLS requirement for local development. Do NOT use in production. ### Step 5: Create a key and set a budget (new terminal) ```bash loopers keys create --name mykey --provider openai loopers budget set --daily 10.00 ``` --- ## Authentication Models ### Header Auth (standard) Pass the Loopers proxy key in the Authorization header. Pass your real provider API key in `X-Loopers-Provider-Key`. ```bash curl -X POST http://localhost:8080/openai/v1/chat/completions \ -H "Authorization: Bearer lp-xxx" \ -H "X-Loopers-Provider-Key: sk-proj-YOUR_REAL_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hi"}]}' ``` ### Path-Based Auth (zero-code for CLI agents) Embed the proxy key in the URL. Pass your real API key as the standard Bearer token. No code changes needed. ```bash curl -X POST http://localhost:8080/lp-xxx/openai/v1/chat/completions \ -H "Authorization: Bearer YOUR_REAL_OPENAI_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hi"}]}' ``` URL pattern: `http://localhost:8080///v1/` --- ## Complete CLI Reference ### Global flags ``` --config string config file path (default: ./loopers.yaml) -v, --verbose enable verbose debug logging -h, --help ``` ### loopers init Interactively generates `loopers.yaml` and `docker-compose.yml` in the current directory. ```bash loopers init ``` ### loopers serve Starts the Loopers AI firewall runtime server. All config comes from `loopers.yaml` or environment variables. ```bash loopers serve ``` ### loopers doctor Diagnoses Loopers firewall configuration, Redis connectivity, and security engines (Loop Detection, MCP Tool Response Inspector, Outbound Semantic DLP Gate, and Persistent Risk Engine). ```bash loopers doctor ``` ### loopers keys create ```bash loopers keys create --name --provider [flags] ``` Flags: - `--name string` (required) — name for the key - `--provider string` (required) — one of: `openai`, `anthropic`, `gemini`, `bedrock`, `azure`, `mistral`, `groq`, `cohere`, `deepseek`, `together`, `ollama`, `fireworks`, `xai`, `vllm`, `openrouter` - `--agent-name string` — agent identity for policy evaluation - `--owner string` — owner identity for policy evaluation - `--allowed-tools string` — comma-separated list of allowed MCP tools - `--allowed-providers string` — comma-separated list of allowed providers - `--tags string` — comma-separated `key=value` tags (e.g. `env=prod,team=alpha`) ### loopers keys list ```bash loopers keys list ``` ### loopers keys revoke ```bash loopers keys revoke ``` ### loopers budget set ```bash loopers budget set [--minute float] [--hourly float] [--daily float] [--weekly float] [--monthly float] ``` All flags are optional and in USD. The first limit reached blocks requests. ### loopers budget status ```bash loopers budget status ``` ### loopers exec Executes a CLI agent command with Loopers proxy environment injected automatically. ```bash loopers exec [--model-override string] [--model-map string] -- ``` Required environment variables: - `LOOPERS_PROXY_KEY` — your proxy key (`lp-xxx`) - `LOOPERS_PROVIDER` — upstream provider (e.g. `openai`, `anthropic`, `openrouter`). Auto-detected from executable name for `aider` (openai), `openhands` (openai), `pi` (openai), `claude` (anthropic), `gemini` (gemini), `codex` (openai), `opencode` (openai), `dsh` (deepseek), `deepseek-harness` (deepseek), `deepseek` (deepseek). Must be set explicitly for all other tools. Optional flags: - `--model-override ` — force all requests to use a specific model (e.g. `google/gemma-2-9b-it:free`) - `--model-map ` — remap model aliases (e.g. `gpt-4o=google/gemini-2.5-pro`) Examples: macOS / Linux — route opencode through OpenRouter: ```bash export OPENAI_API_KEY="sk-or-v1-YOUR_KEY" export LOOPERS_PROXY_KEY="lp-xxx" export LOOPERS_PROVIDER="openrouter" loopers exec --model-override "google/gemma-2-9b-it:free" -- opencode ``` Windows (PowerShell): ```powershell $env:OPENAI_API_KEY="sk-or-v1-YOUR_KEY" $env:LOOPERS_PROXY_KEY="lp-xxx" $env:LOOPERS_PROVIDER="openrouter" loopers exec --model-override "google/gemma-2-9b-it:free" -- opencode ``` ### loopers verify Audits and replays recorded session execution traces offline against declarative YAML Policy Cards and custom OPA Rego rules to check compliance. ```bash loopers verify --trace [flags] ``` Flags: - `--trace string` (required) — path to execution trace JSON file to verify - `--policy-file string` — path to declarative YAML Policy Card - `--policy-dir string` — directory containing custom Rego files - `--presets strings` — built-in templates to enable (`safety`, `pci`, `mcp_sandbox`) - `--format string` — output format: `pretty` (terminal table) or `json` - `--fail-on-violation` — exit with code 1 if violations are found (default: true) --- ## Configuration File (loopers.yaml) Full schema with all defaults: ```yaml server: port: 8080 admin_host: 127.0.0.1 # set to 0.0.0.0 in Docker/K8s for Prometheus scraping admin_port: 9090 log_level: info # debug | info | warn | error read_timeout: 30s write_timeout: 120s # increase for long streaming responses max_payload_bytes: 2097152 # 2MB shadow_mode: false # set to true to log policy violations without blocking traffic redis: addr: "localhost:6379" password: "" db: 0 max_retries: 3 dial_timeout: 5s read_timeout: 2s write_timeout: 2s proxy: upstream_timeout: 300s session: max_per_key: 0 # 0 = unlimited concurrent sessions per key loop_detection: enabled: true fingerprint: threshold: 3 # repeat count before flagging loop window_seconds: 60 similarity_threshold: 0.95 defeat_padding: false velocity: max_rps: 0 # 0 = disabled max_endpoint_repeats: 0 repeat_window_seconds: 60 stall: min_hamming_distance: 0 low_diversity_threshold: 5 action: "warn" # warn | block mcp: enabled: false max_request_size: 1048576 servers: - name: "my-mcp-server" url: "http://mcp-server:3001" circuit_breaker: enabled: true threshold: 5 window_seconds: 60 inspector: enabled: true quarantine_duration: "1h" custom_injection_patterns: [] risk_profile: enabled: true ttl: "0" # "0" = permanent / no-expiry auto_quarantine_threshold: 75 permanent_block_threshold: 90 alerting: webhook_url: "" thresholds: - percent: 80 message: "Budget 80% consumed" policy: enabled: false policy_file: "./policies.yaml" policy_dir: "./policies" presets: [] # safety | pci | mcp_sandbox | zero_trust default_action: "deny" signature: enabled: false type: "hmac" # hmac | ed25519 secret: "" rate_limit: enabled: false requests_per_minute: 60 otel: enabled: false endpoint: "localhost:4317" protocol: "grpc" # grpc | http | stdout sampling_rate: 1.0 zsp: enabled: false # enable ZSP & DPoP JWT validation jwt_only: false # require JWT auth for all proxy keys jwks_url: "https://auth.loopers.dev/.well-known/jwks.json" escalation_enabled: false # allow human approvals via Redis Pub/Sub escalation_secret: "" # HMAC-SHA256 signature secret for approvals providers: openai: base_url: https://api.openai.com anthropic: base_url: https://api.anthropic.com gemini: base_url: https://generativelanguage.googleapis.com ``` Environment variable overrides: ``` SERVER_PORT overrides server.port SERVER_INSECURE_DEV set to "true" to disable TLS requirement for local dev REDIS_ADDR overrides redis.addr REDIS_PASSWORD overrides redis.password LOG_LEVEL overrides server.log_level ``` --- ## HTTP Headers Reference ### Request Headers | Header | Required | Description | |---|---|---| | `Authorization: Bearer ` | Yes | Loopers proxy key (`lp-xxx`), or real provider key when using path-based auth | | `X-Loopers-Provider-Key` | Yes* | Real upstream API key (*not required for path-based auth) | | `X-Loopers-Signature` | No | Cryptographic signature of mutated request body | | `X-Loopers-Session-ID` | No | Session identifier for loop detection and session budgets | | `X-Loopers-Session-Budget` | No | Max USD spend for this session | | `X-Loopers-Session-Max-Steps` | No | Max number of AI calls for this session | | `Content-Type: application/json` | Yes | Required for all POST requests | ### Response Headers | Header | Description | |---|---| | `X-Loopers-Request-Cost` | Real cost of this request in USD | | `X-Loopers-Session-Spend` | Total session spend so far | | `X-Loopers-Session-Steps` | Number of AI calls made in this session | | `X-Loopers-Session-Remaining` | Remaining budget in the most constrained window | | `X-Loopers-Budget-Window` | Which budget window is closest to its limit (e.g. `daily`) | | `X-Loopers-Request-ID` | Unique request ID for debugging | | `X-Loopers-Policy-Block` | `"true"` when a request is blocked by OPA policy | | `X-Loopers-Block-Reason` | The deny reason text from the matching Rego rule | | `X-Loopers-Signature` | Cryptographic signature returning the action receipt back to downstream clients | ### Error Codes | HTTP Status | Type | Cause | |---|---|---| | 401 | `invalid_key` | Proxy key is invalid or revoked | | 403 | `policy_denied` | Request blocked by OPA policy (LLM calls) | | 200 (MCP JSON-RPC `-32001`) | `policy_denied` | MCP tool call denied (returned as 200 so LLM can self-correct) | | 429 | `budget_exceeded` | Budget window limit reached | | 429 | `loop_detected` | Agent loop detected in session | | 429 | `max_steps_exceeded` | Session max steps reached | | 503 | `redis_unavailable` | Redis offline — all requests blocked (fail-closed) | --- ## OPA/Rego & YAML Policy Engine All custom `.rego` files must use `package loopers.policy` and reside in the `policy_dir` directory. Loopers hot-reloads policies on file change. Alternatively, write simple declarative security rules in a single YAML Policy Card file (e.g. `policies.yaml` configured via `policy_file`), or enable out-of-the-box security templates/presets (`presets: ["safety", "pci", "mcp_sandbox"]` or `--presets` flag). Presets enforce SSN/PII masking, credential leak protection, prompt injections blocks, CVV, SQL Injection checks, relative path traversal prevention, and FSM sequence gating requiring `dry_run_command` prior to executing bash command tools. Loopers automatically transpiles these rules into OPA commands on the fly. YAML policies support regular expression matches, custom deny reasons, and Deterministic FSM Gating (Trajectory Risk Modeling using an `fsm` block and `session.state` condition rules). ### Input schema for LLM calls ```json { "agent": { "key_hash": "sha256:...", "name": "my-app", "agent_name": "research-agent", "owner": "alice", "provider": "openai", "tags": {"env": "prod", "team": "alpha"} }, "request": { "provider": "openai", "model": "gpt-4o", "method": "llm_call", "path": "/v1/chat/completions" } } ``` ### Input schema for MCP tool calls ```json { "agent": { "key_hash": "...", "name": "...", "agent_name": "...", "owner": "...", "provider": "...", "tags": {} }, "request": { "provider": "filesystem", "method": "mcp_tool_call", "tool_name": "read_file", "mcp_server": "filesystem", "path": "/mcp/filesystem/tools/call" }, "session": { "id": "sess-...", "state": "TRANSACTION_ACTIVE", "spend": 0.42, "steps": 5, "taint_flags": {"secret_accessed": true}, "tools_called": ["read_file", "read_secret"] }, "agent_risk": { "risk_score": 10, "total_policy_blocks": 1, "total_escalations": 0, "total_spend": 1.25, "persistent_taint_flags": ["secret_accessed"], "session_count": 3, "quarantine_active": false } } ``` ### Example policies Allow admin users: ```rego package loopers.policy default allow = false allow { input.agent.owner == "admin" } ``` Deny after taint (exfiltration prevention): ```rego package loopers.policy deny["outbound HTTP blocked after secret access"] { input.request.method == "mcp_tool_call" input.request.tool_name == "outbound_http" input.session.taint_flags["secret_accessed"] } ``` Restrict models by team: ```rego package loopers.policy deny["only ML team can use Claude"] { startswith(input.request.model, "claude") input.agent.tags["team"] != "ml" } ``` When an MCP tool call is denied, Loopers returns HTTP 200 with a JSON-RPC 2.0 error (`code: -32001`) so the LLM agent can read the denial reason and self-correct its plan. --- ## Tool Response Inspection (Capability 2) When `mcp.inspector.enabled: true` in `loopers.yaml`, Loopers intercepts and inspects outbound `tools/call` response payloads before delivering them downstream: 1. **Indirect Prompt Injection & Traversal:** Scans for jailbreak strings (e.g. `ignore previous instructions`) and path traversal markers (`../../`). Matches are redacted inline to `[Content removed: security policy]`, tagged with header `X-Loopers-Response-Redacted: true`, and emitted as security audit events. 2. **Secret Leakage Prevention:** Scans for AWS keys, OpenAI keys, JWT tokens, Slack tokens, and private keys. Matches mask the secret with `***`, write a quarantine lockout `loopers:quarantine:` in Redis (default `1h`), increment persistent risk score by `+30`, and return an agent-friendly error payload with header `X-Loopers-Policy-Block: true`. --- ## Persistent Agent Identity & Behavioral Risk (Capability 3) Loopers maintains a cross-session behavioral risk profile anchored to the agent's key hash in Redis (`loopers:risk_profile:{keyHash}`). ### Deterministic Risk Scoring (0–100) - **`+10`**: Policy block (`deny`) - **`+25`**: Quarantine action triggered (`quarantine`) - **`+15`**: Escalation action triggered (`escalate`) - **`+5`**: Sensitive taint flag added (e.g. `secret_accessed`) - **`+30`**: Secret exfiltration pattern detected in tool response - **`-5`**: Decay per 24 hours of clean inactivity (lazy evaluated on read, min clamp 0) ### Automated Gateway Thresholds - **Score > 75 (`auto_quarantine_threshold`)**: Agent is automatically placed under a 1-hour quarantine lockout in Redis, fast-failing at the edge (<2ms) with `403 Forbidden`. - **Score > 90 (`permanent_block_threshold`)**: Agent is permanently blocked at the gateway (`403 Forbidden` with reason `agent_risk_blocked`). ### Built-In Preset: `zero_trust` Enable with `presets: ["zero_trust"]` in `loopers.yaml` or `--presets zero_trust` via CLI. Restricts high-privilege actions for agents with `risk_score > 75` or active `secret_accessed` taint flags. --- ## Outbound Semantic DLP Gate (Capability 4) When `server.dlp.enabled: true` in `loopers.yaml`, Loopers intercepts outbound LLM completion text across both non-streaming JSON response envelopes and live Server-Sent Events (SSE) streaming token flows: ### Detection Engine (`internal/inspector`) - **PII Signatures:** - RFC 5322 Emails with domain allowlist filtering (`allowed_hosts`). - Credit Cards (Visa, MasterCard, Amex, Discover) with strict Luhn checksum validation. - US Social Security Numbers (`\b\d{3}-\d{2}-\d{4}\b`). - E.164 and NANP Phone Numbers (`(?:\+?1|\b1)?...`). - **Internal Infrastructure Indicators:** - RFC 1918 Private IPs (`10.x`, `172.16-31.x`, `192.168.x`) and loopback (`127.0.0.1`, `localhost`). - Internal host suffixes (`.internal`, `.local`, `.corp`). - **Secret Exfiltration:** - AWS Keys, OpenAI/OpenRouter Keys, GitHub PATs, Slack Bot Tokens, JWTs, and PEM Private Keys. ### Action Planes 1. **`mask` (Default for PII):** Redacts sensitive tokens inline with `***`. For non-streaming completions, mutates provider-specific JSON envelopes (OpenAI `choices.message.content` & `tool_calls.arguments`, Anthropic `content.text`, Gemini `candidates.content.parts.text`), recalculates `Content-Length`, and sets header `X-Loopers-DLP-Redacted: true`. For SSE streaming, rewrites individual chunk data payloads in-flight. 2. **`quarantine` (Default for Secrets):** Immediately severs the connection (HTTP 403 or error SSE frame `{"type":"dlp_quarantine"}`), attaches header `X-Loopers-DLP-Block: true`, sets a Redis lockout key (`loopers:quarantine:{keyHash}`, default 1h TTL), updates persistent agent risk score by `+30`, and rejects subsequent agent calls at the gateway auth layer. 3. **Streaming Sliding Window:** Maintains a 256-character rolling text window across incoming SSE packets to detect secret patterns fragmented across arbitrary token chunk boundaries. ### Configuration (`loopers.yaml`) ```yaml server: dlp: enabled: true action: "mask" # "mask" or "quarantine" scan_secrets: true scan_pii: true scan_network: true allowed_hosts: - "example.com" - "corp.internal" quarantine_duration: "1h" ``` --- ## Multi-Turn Conversation Drift Detection (Capability 5) When `session.drift_detection.enabled: true` in `loopers.yaml`, Loopers protects multi-turn agent sessions against gradual context divergence, goal hijacking, and stealth prompt injections: ### Algorithmic Engine (`internal/session/drift.go`) 1. **Session Anchoring:** Persists the initial user request ($T_1$) as an immutable reference anchor in Redis (`loopers:session:{keyHash}:{sessionID}:anchor`) via atomic `SetNX`. 2. **Trigram & Stopword Normalization:** Filters high-frequency English stopwords and extracts 3-character n-grams hashed to `uint16` with FNV-1a to capture technical vocabulary semantics. 3. **Containment Similarity:** Measures $\text{Containment}(A, B) = \frac{|A \cap B|}{|A|}$ to determine what fraction of the current prompt is grounded in session history without Jaccard denominator decay. 4. **Dual-Anchor Scoring:** Evaluates weighted continuity ($75\%$ anchor grounding + $25\%$ prior turn continuity) and normalizes drift score ($0.0$ to $1.0$). If drift exceeds `drift_score_threshold` after `min_turns`, requests are denied at the gateway. ### OPA & Policy Cards - **Rego Fields:** `input.session.drift.drift_detected`, `input.session.drift.drift_score`, `input.session.drift.anchor_similarity`, `input.session.drift.prior_similarity`, `input.session.drift.turn_count`. - **YAML Syntax:** `field: session.drift.drift_detected`, `op: equals`, `value: true`. - **Built-in Preset:** `safety_drift` (`loopers serve --presets safety_drift`). ### Configuration (`loopers.yaml`) ```yaml session: drift_detection: enabled: true min_turns: 3 anchor_similarity_threshold: 0.08 drift_score_threshold: 0.45 ``` --- ## Syntactic Normalization & Homoglyph Defense (Layer 3) The `internal/syntactic` engine runs high-performance 5-stage de-obfuscation on all inbound prompts and outbound tool responses to protect against adversarial bypass techniques: ### Normalization Pipeline 1. **Unicode Confusable Resolution:** Resolves lookalike homoglyphs from Cyrillic (`а, е, о, р, с, і`), Greek (`α, ο, ν, ρ, τ`), Mathematical Alphanumeric bold/italic (`𝐢𝐠𝐧𝐨𝐫𝐞`), Fullwidth (`ignore`), and Enclosed runes (`ⓘⓖⓝⓞⓡⓔ`) to ASCII equivalents using Unicode TR39 tables and NFKC normalization. 2. **Invisible Character Stripping:** Removes 28+ zero-width spaces, non-joiners, joiners, bi-directional overrides (`\u202E`, `\u200E`), soft hyphens, and combining grapheme joiners. 3. **Recursive Encoding Decoder:** Recursively unwraps double/triple URL percent-encoding (`%252e%252e` -> `..`), hex escapes (`\xHH`), unicode escapes (`\uHHHH`), and HTML entities (`i`). 4. **Multi-Layer Payload Extraction:** `ExtractAllTextLayers()` extracts printable UTF-8 strings inside Base64 blocks and unescaped buffers, enabling regex inspection without corrupting raw binary data. 5. **Token & Delimiter Collapsing:** Collapses intra-word single-delimiter splitting (`i.g.n.o.r.e`, `i_g_n_o_r_e`, `i-g-n-o-r-e`) and leetspeak substitutions (`1gn0r3` -> `ignore`). ### Policy Integration - **Dual Matching:** OPA Rego and YAML Policy Cards matching on `prompt_text` (`contains`, `matches_regex`) automatically evaluate against both raw prompt text and de-obfuscated `normalized_prompt`. - **Telemetry Fields:** - `input.action.normalized_prompt`: Canonical ASCII de-obfuscated string. - `input.action.obfuscation.obfuscation_detected`: Boolean flag indicating if any obfuscation technique was found. - `input.action.obfuscation.has_homoglyphs`: Boolean flag. - `input.action.obfuscation.has_invisible_chars`: Boolean flag. - `input.action.obfuscation.has_base64_payloads`: Boolean flag. - `input.action.obfuscation.has_encoding_attacks`: Boolean flag. - `input.action.obfuscation.has_delim_padding`: Boolean flag. --- ## Supported Providers | Provider | `--provider` value | |---|---| | OpenAI | `openai` | | Anthropic | `anthropic` | | Google Gemini | `gemini` | | AWS Bedrock | `bedrock` | | Azure OpenAI | `azure` | | Mistral | `mistral` | | Groq | `groq` | | Cohere | `cohere` | | DeepSeek | `deepseek` | | Together AI | `together` | | Ollama (local) | `ollama` | | Fireworks | `fireworks` | | xAI Grok | `xai` | | vLLM (local/cloud) | `vllm` | | OpenRouter | `openrouter` | --- ## Monitoring Loopers exposes Prometheus metrics at `http://:/metrics` (default: `http://127.0.0.1:9090/metrics`). In Docker/Kubernetes, set `SERVER_ADMIN_HOST=0.0.0.0` to allow external scraping. A pre-built Grafana dashboard is in `./grafana/`. --- ## Key Identity Metadata for Policies When creating keys, attach metadata to make OPA policies effective: ```bash loopers keys create \ --name my-app-key \ --provider openai \ --agent-name research-agent \ --owner alice \ --tags "env=prod,team=alpha" ``` This metadata populates `input.agent` on every request made with that key.