{"id":49888903,"url":"https://github.com/arpitcoder/aegrail","last_synced_at":"2026-05-15T20:01:27.088Z","repository":{"id":357078275,"uuid":"1235287277","full_name":"arpitcoder/aegrail","owner":"arpitcoder","description":"The runtime contract for AI agents in production. Scoped identity, hard budget kill-switches, forensic-grade audit log.","archived":false,"fork":false,"pushed_at":"2026-05-13T03:48:40.000Z","size":56,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-13T04:24:47.627Z","etag":null,"topics":["agent-security","ai-agents","ai-governance","infrastructure","llm","observability","python","runtime"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arpitcoder.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-11T07:20:40.000Z","updated_at":"2026-05-13T03:48:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/arpitcoder/aegrail","commit_stats":null,"previous_names":["arpitcoder/agentctl","arpitcoder/aegrail"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/arpitcoder/aegrail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arpitcoder%2Faegrail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arpitcoder%2Faegrail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arpitcoder%2Faegrail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arpitcoder%2Faegrail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arpitcoder","download_url":"https://codeload.github.com/arpitcoder/aegrail/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arpitcoder%2Faegrail/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32996054,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-13T13:14:54.681Z","status":"ssl_error","status_checked_at":"2026-05-13T13:14:51.610Z","response_time":115,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-security","ai-agents","ai-governance","infrastructure","llm","observability","python","runtime"],"created_at":"2026-05-15T20:01:00.141Z","updated_at":"2026-05-15T20:01:27.076Z","avatar_url":"https://github.com/arpitcoder.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# aegrail\n\n[![CI](https://github.com/arpitcoder/aegrail/actions/workflows/ci.yml/badge.svg)](https://github.com/arpitcoder/aegrail/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/aegrail.svg)](https://pypi.org/project/aegrail/)\n[![Python](https://img.shields.io/pypi/pyversions/aegrail.svg)](https://pypi.org/project/aegrail/)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)\n\n**The runtime contract for AI agents in production.**\n\nA container runtime assumes deterministic code. An agent isn't deterministic. Run your agents on something that knows that.\n\n---\n\n## Why this exists\n\nFor 15 years, \"container in production\" meant **microservice**. Every piece of cloud-native infrastructure — Kubernetes, Istio, Prometheus, OPA — was designed around assumptions a microservice satisfies. Those assumptions are load-bearing.\n\nAn agent in a container looks identical. Same Dockerfile, same pod spec, same `kubectl apply`. But it violates almost every one of those assumptions:\n\n| Property | Microservice | Agent |\n|---|---|---|\n| Output for the same input | Same | Different every time |\n| Execution path | Coded, finite | Decided by the LLM at runtime |\n| Cost per request | Sub-cent, predictable | $0.01 to $20+, unbounded |\n| Outbound calls | Static dependency graph | LLM decides at runtime |\n| Failure mode | Crash / 500 / timeout | \"Confidently wrong\" — returns 200 with garbage |\n| Identity | Service identity | Service identity + invoking user + agent role |\n| Trust boundary | Code trusted, input untrusted | Plus: the LLM's own decisions are untrusted |\n\nThe infrastructure stack hasn't caught up. That's why your agent looped for 63 hours and burned $4,200. That's why a malicious PR title made three production coding agents leak their own API keys. That's why your platform team can't tell you how many agents are in production right now.\n\n**`aegrail` is the missing runtime layer.** Deterministic enforcement of identity, budget, and audit on top of any agent stack you already use.\n\n---\n\n## What it does\n\nFour primitives. Nothing else.\n\n1. **Scoped identity** — every agent run gets a session-bound principal. No shared API keys. Audit logs are identity-linked from line one.\n2. **Hard budget kill-switches** — cost, tokens, wall-clock, recursion depth, tool calls. The runtime stops the agent. Not the system prompt. Not the LLM. The runtime.\n3. **Structured audit log** — identity-linked, append-only, replayable record of every prompt, tool call, denial, and outcome. Forensic-grade, not debug-grade.\n4. **Per-agent tool ACL _(v0.2)_** — each agent gets an explicit registry of tools it may invoke, with optional argument predicates. Calls outside the registry, or with denied args, raise `ToolNotPermitted` deterministically. Maps to **OWASP Top 10 for Agentic Applications**: **ASI02 (Tool Misuse)** and **ASI03 (Identity \u0026 Privilege Abuse)**.\n\nWhat it deliberately does **not** do (yet):\n- Egress allowlist proxy (v0.3)\n- Approval gates (v0.4)\n- Hosted dashboard (v1.0, paid)\n- Prompt management or eval (integrate Langfuse — we don't compete)\n\n---\n\n## Install\n\n```bash\npip install aegrail\n```\n\n\u003e **Note:** the first PyPI release will be `v0.2.0`. Until then, install from source:\n\u003e ```bash\n\u003e git clone https://github.com/arpitcoder/aegrail\n\u003e cd aegrail \u0026\u0026 pip install -e .\n\u003e ```\n\nPython 3.10+. Zero hard dependencies beyond `pydantic`. Works with any LLM provider (OpenAI, Anthropic, Bedrock, raw HTTP). Works alongside any agent framework (LangChain, LlamaIndex, MCP, custom).\n\n---\n\n## Hello world\n\n```python\nfrom aegrail import Agent, AuditSink, Budget, Tool\n\ndef refund(order_id: int) -\u003e str:\n    # Your real tool — could be an API call, DB write, anything.\n    return f\"refunded order {order_id}\"\n\nagent = Agent(\n    identity=\"support-bot/v1\",\n    budget=Budget(usd=5.0, tokens=100_000, wall_seconds=120, max_tool_calls=10),\n    audit=AuditSink.file(\"./audit.jsonl\"),\n    tools={\n        \"refund_api.refund\": Tool(\n            name=\"refund_api.refund\",\n            fn=refund,\n            description=\"Issue a refund for a customer order.\",\n            when=lambda args: isinstance(args.get(\"order_id\"), int),\n        ),\n    },\n)\n\nwith agent.session(user_id=\"alice\", task=\"refund order #4521\") as s:\n    # 1. Call your LLM however you like (OpenAI SDK, Anthropic SDK, raw HTTP).\n    #    Then tell the runtime what it cost. Provider-agnostic by design.\n    s.record_llm(\n        model=\"claude-sonnet-4-5\",\n        tokens_in=120,\n        tokens_out=300,\n        cost_usd=0.012,\n    )\n\n    # 2. Run a registered tool through the session — looked up by name,\n    #    arg predicate enforced, counted against the budget, audited.\n    result = s.call_tool(\"refund_api.refund\", order_id=4521)\n```\n\nThat's it. The session:\n\n- Generates a short-lived per-session principal (`support-bot/v1@sess_\u003cms\u003e_\u003crand\u003e`)\n- Tracks tokens and dollars against the budget; raises `BudgetExceeded` deterministically when hit\n- Emits a structured event for every LLM call, tool invocation, and policy denial — identity-linked, append-only\n- Refuses tools the agent is not registered for, or tool args that fail the `when` predicate — raising `ToolNotPermitted` deterministically (mapped to OWASP ASI02 / ASI03)\n- Stops the agent if wall-clock, recursion, or tool-call limits are hit, no matter what the LLM \"decides\"\n\nIf the budget is exceeded mid-loop, or a tool is denied, the session raises. The agent cannot talk its way out of it.\n\n---\n\n## Async — `AsyncSession` (v0.2.2)\n\nFor agents running on `asyncio` (FastAPI, MCP servers, anything using the OpenAI/Anthropic async clients), use `agent.async_session(...)`:\n\n```python\nimport asyncio\nfrom aegrail import Agent, AuditSink, Budget, Tool\n\nasync def real_refund(order_id: int) -\u003e str:\n    # any async work here — DB call, async HTTP, etc.\n    return f\"refunded {order_id}\"\n\nagent = Agent(\n    identity=\"support-bot/v1\",\n    budget=Budget(usd=5.0, wall_seconds=30, max_tool_calls=10),\n    audit=AuditSink.file(\"./audit.jsonl\"),\n    tools={\"refund\": Tool(name=\"refund\", fn=real_refund)},\n)\n\nasync def main() -\u003e None:\n    async with agent.async_session(user_id=\"alice\") as s:\n        await s.record_llm(model=\"gpt-4\", tokens_in=100, tokens_out=200, cost_usd=0.01)\n        result = await s.call_tool(\"refund\", order_id=4521)\n        print(result)\n\nasyncio.run(main())\n```\n\nThe async surface mirrors the sync one — same exceptions, same audit events, same tool ACL semantics — and adds one load-bearing property: **`wall_seconds` is enforced mid-tool-call** via `asyncio.wait_for`. If a tool call hangs past the remaining wall-clock budget, the runtime raises `BudgetExceeded('wall_seconds')` deterministically, rather than waiting for the call to return. Sync `Session` could only check at event boundaries.\n\nTool functions can be sync or async — the runtime detects via `inspect.iscoroutinefunction` and dispatches accordingly. Sync functions are wrapped in `asyncio.to_thread(...)` so the timeout still applies at the asyncio level.\n\nFull async demo (against local Ollama, no API key): [`examples/async_demo.py`](examples/async_demo.py).\n\n---\n\n## First 60 seconds\n\n```bash\ngit clone https://github.com/arpitcoder/aegrail\ncd aegrail\npip install -e .\n\n# Happy path — synthetic LLM call, real audit log.\npython examples/basic.py\n\n# The kill-switch — agent loops greedily, runtime stops it deterministically.\npython examples/budget_kill.py\n```\n\n`examples/budget_kill.py` prints:\n\n```\niteration 1: state={'tokens_used': 500, 'usd_used': 0.01, ...}\niteration 2: state={'tokens_used': 1000, 'usd_used': 0.02, ...}\niteration 3: state={'tokens_used': 1500, 'usd_used': 0.03, ...}\niteration 4: state={'tokens_used': 2000, 'usd_used': 0.04, ...}\niteration 5: state={'tokens_used': 2500, 'usd_used': 0.05, ...}\n\n[runtime] killed by reason=usd: usd budget exceeded: 0.0600 \u003e 0.0500\n```\n\nThat's the `$4,200-weekend` scenario, prevented in code.\n\n---\n\n## Real-provider examples\n\nWorking end-to-end demos with live LLM calls:\n\n- [`examples/openai_demo.py`](examples/openai_demo.py) — OpenAI Chat Completions\n- [`examples/anthropic_demo.py`](examples/anthropic_demo.py) — Anthropic Messages\n- [`examples/basic.py`](examples/basic.py) — provider-free walkthrough\n- [`examples/budget_kill.py`](examples/budget_kill.py) — the runtime stopping a runaway loop\n- [`examples/multi_agent_acl.py`](examples/multi_agent_acl.py) — _(v0.2)_ FinOps and Architect agents in one process, with cross-agent tool denial enforced deterministically\n\n```bash\npip install openai\nexport OPENAI_API_KEY=sk-...\npython examples/openai_demo.py\n```\n\n---\n\n## Tool ACL — v0.2\n\nEach `Agent` carries an explicit catalogue of tools it is permitted to invoke. Two agents in the same process with disjoint registries cannot cross-invoke each other's tools, no matter what the LLM is instructed to do.\n\n```python\nfrom aegrail import Agent, AuditSink, Budget, Tool, ToolNotPermitted\n\nfinops = Agent(\n    identity=\"finops/v1\",\n    budget=Budget(usd=1.0, max_tool_calls=10),\n    audit=AuditSink.stdout(),\n    tools={\n        \"cost_report\": Tool(\n            name=\"cost_report\",\n            fn=lambda period: f\"AWS spend {period}: $84,201.47\",\n            when=lambda args: args.get(\"period\") in {\"mtd\", \"qtd\", \"ytd\"},\n        ),\n    },\n)\n\narchitect = Agent(\n    identity=\"architect/v1\",\n    budget=Budget(usd=1.0, max_tool_calls=10),\n    audit=AuditSink.stdout(),\n    tools={\n        \"deploy_infra\": Tool(\n            name=\"deploy_infra\",\n            fn=lambda env: f\"deployed infra to {env}\",\n            when=lambda args: args.get(\"env\") in {\"staging\", \"prod\"},\n        ),\n    },\n)\n\nwith finops.session(user_id=\"alice\") as s:\n    try:\n        s.call_tool(\"deploy_infra\", env=\"prod\")  # not in finops's registry\n    except ToolNotPermitted as exc:\n        print(exc.reason)   # 'not_registered'\n        print(exc.tool_name)  # 'deploy_infra'\n```\n\nThree denial reasons surface on `ToolNotPermitted.reason`:\n\n- `'not_registered'` — the tool name isn't in this agent's registry (ASI03).\n- `'predicate_false'` — the tool's `when(args)` predicate returned `False` (ASI02).\n- `'predicate_error'` — the predicate raised; the original exception is on `__cause__`.\n\nEvery denial emits a `tool_denied` audit event with the agent's identity, principal, and a snapshot of the budget — so denied attempts are forensically queryable, not just thrown away.\n\nTools also accept an optional `redact(args) -\u003e dict` to control what shows up in the audit payload's `args` field. The default emits **keys only**, never values.\n\n---\n\n## Where this sits — defense-in-depth at the capability layer\n\naegrail's tool ACL is one of three complementary layers. Each protects against a different threat; none replaces the others.\n\n| Layer | Enforces | Threat it stops | aegrail role |\n|---|---|---|---|\n| **Network egress (L3/L4)** | Which hosts/ports the pod can reach | An agent dials an unapproved domain | _Out of scope today_ — use Kubernetes NetworkPolicy, Cilium, an egress proxy. v0.3 will add a proxy. |\n| **Tool ACL (L7 capability)** | Which named callables an identity may invoke, and with what args | A FinOps agent invokes a deploy tool because the LLM was prompt-injected to | **This is v0.2.** |\n| **Process isolation** | What the OS lets the agent's process do | A compromised agent reads another agent's memory or files | _Out of scope_ — use containers, gVisor, Firecracker, separate pods. |\n\nTwo agents in the same pod look identical to network policy: same source IP, same kube ServiceAccount, same outbound CIDR. The L3/L4 layer cannot tell them apart, which is why functional limits — *what tool a given identity may call* — must live at L7. That's what aegrail enforces, deterministically, in Python at the runtime boundary.\n\n**The discipline this requires.** aegrail only governs actions that flow through `session.call_tool(...)`. An agent that imports `requests` and POSTs to a banking API directly is invisible to the runtime: no audit event, no ACL check, no budget update. The contract is to register every sensitive action as a `Tool` and invoke it through the session. The library cannot prevent off-path bypasses without process-level isolation, which is intentionally out of scope.\n\nUse aegrail v0.2 *with* network policy and process isolation, not as a substitute. Defense-in-depth only works when the layers compose.\n\n---\n\n## Where it fits next to what you already use\n\n| Tool | What it does | Where aegrail fits |\n|---|---|---|\n| **Okta / Auth0 / WorkOS** | User identity, OAuth | Sits underneath — aegrail ties the user identity to per-session agent principals |\n| **Langfuse / Helicone / LangSmith** | LLM observability and prompt management | Complementary — Langfuse is debug-grade, aegrail is enforcement-grade. Run both. |\n| **Lakera / Prompt Security** | Input-layer prompt-injection filtering | Complementary — they guard inputs, aegrail guards actions |\n| **LangChain / LlamaIndex / MCP / OpenAI Agents SDK** | Agent frameworks | aegrail wraps your sessions; you keep your framework |\n| **OPA / Cedar** | General authorization policy | Complementary — aegrail v0.2 ships per-agent tool ACL in Python; a future release may compose with OPA/Cedar for org-wide policy |\n\naegrail is not a replacement for any of these. It is the **runtime layer** they all assume but none of them ship.\n\n---\n\n## What an audit event looks like\n\nEvery line of `audit.jsonl` is one event. Identity-linked, append-only, JSON.\n\n```json\n{\n  \"ts\": \"2026-05-11T09:14:22.481Z\",\n  \"session_id\": \"sess_1778480062481_4bf0a4f8cf1c\",\n  \"agent_identity\": \"support-bot/v1\",\n  \"invoking_user\": \"alice\",\n  \"principal\": \"support-bot/v1@sess_1778480062481_4bf0a4f8cf1c\",\n  \"event\": \"tool_call\",\n  \"payload\": {\n    \"tool\": \"refund_api.refund\",\n    \"description\": \"Issue a refund for a customer order.\",\n    \"args\": {\"kwarg_keys\": [\"order_id\"]},\n    \"ok\": true,\n    \"elapsed_ms\": 0.42\n  },\n  \"budget\": {\n    \"tokens_used\": 420,\n    \"usd_used\": 0.012,\n    \"tool_calls\": 1,\n    \"recursion_depth\": 0,\n    \"wall_elapsed\": 0.18\n  }\n}\n```\n\nTop-level fields are flat for log-ingestion friendliness (ship to S3, ClickHouse, Loki, Datadog, anything that takes JSONL). `payload` carries event-specific detail; `budget` carries a snapshot of consumption *at the moment of emission*, so you can reconstruct cost-over-time from the log alone.\n\nDesigned so you can answer the question every team eventually asks: *what did the agent do at 14:23, and why?*\n\n---\n\n## Alerts and fanout\n\nThe three core sinks (`file`, `stdout`, `memory`) cover persistence. Three more cover routing:\n\n```python\nfrom aegrail import Agent, AuditSink, Budget\n\n\ndef on_event(evt):\n    if evt.event == \"budget_exceeded\":\n        # Send to PagerDuty, Slack, your incident pipeline — anything.\n        ...\n\n\nagent = Agent(\n    identity=\"payments-bot/v1\",\n    budget=Budget(usd=5.0, wall_seconds=120),\n    audit=AuditSink.composite(\n        AuditSink.file(\"./audit.jsonl\"),                          # forensic record\n        AuditSink.webhook(\"https://alerts.example.com/aegrail\"), # real-time\n        AuditSink.callback(on_event),                             # in-process routing\n    ),\n)\n```\n\n- **`AuditSink.callback(fn)`** — invoke a Python function on every event. Synchronous; exceptions are caught.\n- **`AuditSink.webhook(url, *, headers=None, timeout=3.0)`** — POST events as JSON. Stdlib only, no `requests` dependency. Network errors, non-2xx responses, and timeouts are caught.\n- **`AuditSink.composite(*sinks)`** — fan out to multiple sinks. A failure in one child cannot affect the others — every child is isolated.\n\nSink failures **never** break the agent. Every sink wraps its write path; errors land on stderr.\n\n---\n\n## Design principles\n\n- **Wrapper, not framework.** `aegrail` works with your existing stack. We will never ask you to rewrite an agent to use us.\n- **Deterministic enforcement.** The system prompt is not a security boundary. The runtime is.\n- **Identity is first-class.** Every event ties to *agent identity + invoking user*. Authorization is the intersection.\n- **Audit is forensic, not debug.** Append-only, structured, replayable. Not log lines.\n- **Zero ambient credentials.** Sessions get short-lived scoped principals. Never share an API key.\n- **Provider and framework agnostic.** OpenAI, Anthropic, Bedrock. LangChain, LlamaIndex, MCP, custom. We don't pick sides.\n\n---\n\n## Status\n\n**v0.2 — narrow scope, growing surface.** Identity, budget, audit, and now the per-agent tool ACL. v0.3 adds the egress allowlist proxy; v0.4 adds approval gates.\n\n109 tests (75 sync + 16 async + 11 chain + 7 schema), ruff clean. CI green on Python 3.10, 3.11, 3.12.\n\nFor SOC 2 / ISO 27001 / NIST SP 800-53 control mappings and audit evidence extraction recipes, see [`COMPLIANCE.md`](COMPLIANCE.md).\n\nFor K8s deployment patterns (developer-effortless `AEGRAIL_INTERCEPT=1` env-var enforcement, plus a working kind cluster integration test), see [`docs/kubernetes.md`](docs/kubernetes.md).\n\n---\n\n## Roadmap\n\n- **v0.1** — scoped identity, budget kill-switches, audit log _(shipped)_\n- **v0.1.x** — alerting sinks (callback/webhook/composite) _(shipped)_\n- **v0.2** — per-agent tool ACL with arg predicates (OWASP ASI02 + ASI03) _(shipped)_\n- **v0.2.2** — `AsyncSession` with hard `wall_seconds` enforcement mid-tool-call _(shipped)_\n- **v0.2.3** — tamper-evident audit chain + `COMPLIANCE.md` (SOC 2 / ISO 27001 / NIST mappings) + Tool schema exports for OpenAI/Anthropic _(shipped)_\n- **v0.2.x** — provider helpers (OpenAI/Anthropic/litellm)\n- **v0.3** — egress allowlist proxy (network-level enforcement)\n- **v0.4** — approval gates for irreversible actions\n- **v1.0** — hosted control plane (paid)\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). Security reports: [SECURITY.md](SECURITY.md).\n\n---\n\n## License\n\nApache License 2.0. See [LICENSE](LICENSE) for full terms.\n\nCopyright © 2026 [Arpit Nigam](https://github.com/arpitcoder).\n\n`aegrail` is permissively licensed for commercial and non-commercial use. Contributions are welcome under the same license — see [CONTRIBUTING.md](CONTRIBUTING.md).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farpitcoder%2Faegrail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farpitcoder%2Faegrail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farpitcoder%2Faegrail/lists"}