Skip to content

armenr/aegis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AEGIS

A governed, multi-tenant, agentic LLM platform — as a reference architecture you can actually learn from.

Most reference implementations for agentic systems show you a stack wired together: a model call behind an API, a queue, maybe a vector store, and a README that says "add your own auth." They demonstrate that the pieces connect. They rarely demonstrate the part that determines whether a system like this survives contact with real users, real tenants, and real auditors — the governance. AEGIS is built the other way around. It starts from the hard parts and treats the plumbing as the easy part, because in practice it is.

The shape is a governed edge (NestJS/Fastify: authentication, authorization, audit, and a human-in-the-loop safety gate) fronting durable outer orchestration (Hatchet), which drives an inner agent graph (LangGraph, in Python) over a typed gRPC contract, with model calls shaped by a structured prompting layer. It is multi-tenant by construction, backed by Postgres (with pgvector, and row-level security as the enforcement target — see Status) and ArangoDB. That paragraph could describe a dozen repos. The difference is in how the boundaries are enforced, not merely drawn.

Authorization belongs at the data layer, not at the route. A guard on an HTTP handler is a suggestion — one forgotten decorator, one new endpoint, one internal caller, and it's bypassed. AEGIS pushes the authorization decision down to where the rows actually are (policy compiled to a query predicate, backed by database-enforced row-level security), so "which records can this actor see" is answered by the store itself, not by remembering to ask nicely on the way in. (Today the predicate is applied in the repository; WU-05 lowers it into engine-enforced RLS — see Status.) The human-in-the-loop gate is structural for the same reason: a checkpoint the orchestration graph pauses on and cannot route around, not an if (needsApproval) branch someone can quietly delete. And the separation between sensitive data and the analytics/measurement path is a real firewall with a CI leak-gate — a test that fails the build if the two domains touch — rather than a naming convention and good intentions.

It is meant for engineers and architects who ship agentic systems into environments where "it worked in the demo" is not an acceptable standard, and who would rather study a small number of hard problems solved carefully than a large number of easy ones solved fast.

Honesty note. This is a work in progress, and the sections below label exactly what is wired today versus designed-next. The governance machinery is the deliverable; one neutral workload (a governed Case-approval flow) demonstrates it. An evaluator can grep the repo — nothing here claims more than the tree contains.


Architecture at a glance

The client's only entrypoint is the governed edge; enforcement converges on the data layer, so a bypassed route guard still returns zero unauthorized rows. Green = wired today · amber = present but off the governed read path · red-dashed = specified, not yet built (WU-05 keystone). Full walkthrough — six diagrams tying each layer to its ADR and its source files — in docs/ARCHITECTURE.md.

AEGIS system architecture — the client's only entrypoint is the governed edge; enforcement converges on the data layer, so a bypassed guard still returns zero unauthorized rows


Contracts are defined once and generated into every layer

Parity across a polyglot stack is usually somebody's job to remember. Here it's a property of the build — a contract change that outruns a consumer fails a build, not a production request.

  • One .proto → TypeScript and Python, in lockstep. proto/aegis/** is the single source of truth for the edge ↔ agent-tier boundary. buf generate emits the ts-proto surface the NestJS edge imports (@aegis/contracts) and the gRPC/protobuf stubs the Python agent tier imports (apps/agents/src/aegis/…) — from that one schema. Both are committed and drift-gated: a regenerate-and-diff test fails the build if either language's stubs fall out of sync with the .proto. [wired · ADR-0014 / ADR-0018]
  • One CASL ability model → the edge guard and the web UI. @aegis/authz is defined once; the NestJS PoliciesGuard enforces it at the data layer, and the React app renders from the same abilities — isomorphic authz, so the button a user can't see is exactly the action the server won't allow. Edge enforcement [wired], web render [designed-next · WU-11].
  • The edge's Zod DTOs → OpenAPI → a typed browser client, for free. Request/response shapes are Zod schemas (nestjs-zod); the edge emits an OpenAPI spec from them; the browser's REST client is generated from that spec, not hand-synced. [designed-next · WU-11 · ADR-0022]

The payoff: end-to-end type safety across TypeScript, Python, and the browser — from single sources — and cross-layer drift becomes impossible to ship silently, because the wiring keeps the layers in agreement, not a person remembering to update three places.

The stack, and why

Every non-trivial choice is recorded as an ADR that carries three things a "we chose X over Y" note drops: the deciding axis (the one dimension the call turned on), an honest steelman of the runner-up (the rejected option presented as genuinely good, because it usually was), and the flip-condition (the concrete change in constraints that reverses the call). The one-liners below are the deciding axis; the ADR carries the steelman and the flip-condition.

  • Governed edge + authorization at the data layer — a NestJS/Fastify boundary owning authn, CASL authorization, audit, and the safety gate; the CASL ability compiles into a store-level predicate (Postgres RLS / Arango FILTER) so a bypassed route guard still returns zero unauthorized rows. Deciding axis: enforcement has to survive a bypassed route guard — the store is the only plane the edge and the Python agent tier both share.ADR-0013, ADR-0017

  • protobuf + gRPC as the edge↔agent-tier contract — one .proto defines the polyglot, streaming boundary (RunAgent server-streams AgentEvent; an ActorContext carries governed identity across the process hop); buf codegen emits the Python and TypeScript surfaces from that single schema, drift-gated in CI. Deciding axis: one authoritative schema kept enforced across a polyglot, streaming boundary, where drift fails a build rather than a request.ADR-0014, ADR-0018

  • Hatchet — outer durable orchestration — durable-execution engine for the long-lived cross-tier process (fan-out, rate-limited LLM/embedding batches, durable waits on the human gate, crash-resume), riding the Postgres already in the stack with no new datastore. Deciding axis: the smallest operational/vendor surface that still gives true step-level durability, on infrastructure already present.ADR-0009

  • LangGraph — inner agent graph in a separate Python tier — the tool-calling loop, checkpoints, HITL pauses, and token streaming live in apps/agents (Python), reached over gRPC. One LangGraph run = one durable Hatchet task; neither layer checkpoints what the other already checkpoints. Deciding axis: two durability layers persisting the same state is a correctness liability, so split by responsibility (inner control-flow vs outer run) rather than stack them.ADR-0010

  • DSPy-shaped LLM calls — narrow LLM surfaces are written as typed Signature I/O contracts (adopted now, runnable un-optimized); the GEPA optimizer is deferred until a metric + labeled eval set exist, at which point the required safety/eval gate is the optimization signal. Deciding axis: structure and evaluability are cheap to buy now; optimization complexity is expensive and not yet earned.ADR-0011

  • Sensitive ↔ analytics firewall — an append-only, content-free events schema under distinct credentials computes the aggregate measurement asset without a grant that can read raw records; a CI leak-gate keeps the two domains from touching. Deciding axis: "computed without touching raw records" must be a property of the wiring (separate credentials), not of good intentions in a query.ADR-0015

  • Postgres + pgvector as default primary; ArangoDB scoped to earned traversal — relational tenant state, RLS isolation, and vector retrieval collapse onto Postgres; the graph store is retained only for the cross-entity traversal that measurably defeats recursive CTEs / Apache AGE. Deciding axis: can the engine backstop authorization at the data layer? Postgres RLS is engine-enforced and fail-closed; a graph store has no equivalent, so it must earn its place on evidence.ADR-0016, ADR-0019

The full deciding-axis · steelman · flip-condition writeups live in .agent-docs/decisions/.


Status — wired today vs designed-next

Wired (traced from a real entrypoint, gates green)

  • The governed edge boot chainmain.ts → NestFactory(AppModule) mounts a global JwtAuthGuard → PoliciesGuard → AuditInterceptor chain. PoliciesGuard fails open on an unannotated route by design (the fail-open baseline the architecture then closes), so every governed route carries an explicit @CheckPolicies.
  • The frozen CASL model (packages/authz) — six domain subjects, the four lifecycle actions (propose / approve / reject / commit), and separation-of-duties expressed as distinct actions on distinct roles (not a conditional update). Shared verbatim by the edge and the React frontend.
  • The proto/gRPC contract + buf codegenaegis.agent.v1 and aegis.governance.v1 in proto/; make proto (buf generate) emits the committed Python and TypeScript stubs, drift-gated by a regenerate-and-diff test.
  • The Postgres/Kysely governance data layer — a non-root PostgresService, a node-pg-migrate governance migration (five tables, indexed scoping columns, the effectively-once partial-unique index on committed decisions), Kysely DB types introspected from the migrated schema and drift-gated via testcontainers.
  • The tenant-scoped read planeGET /cases (and the sibling governance lists) run Controller → Service → Repository → Postgres, applying where tenant_id = actor.tenantId in the query, at the data layer — not at the route guard. This is the read half of the neutral governed-approval workload.

Designed-next (specified, not yet built) — WU-05, the keystone

  • The ability → {RLS | AQL FILTER} compiler: one seam that lowers a CASL ability into a Postgres RLS policy predicate and an ArangoDB AQL FILTER, so the row-level rules (workspaceId $in, assignedReviewerIds $in) are pushed to the store rather than applied in application code.
  • Dual-store enforcement: Postgres CREATE POLICY + ENABLE ROW LEVEL SECURITY under the engine backstop; ArangoDB routed through one mandatory filter-injecting chokepoint (no engine backstop there — the compiled predicate carries the full burden, so the chokepoint is construction-enforced).
  • Fail-open closure: a mechanical policy-omission lint (every tenant/scoped table must enable RLS and carry a policy) plus a cross-tenant-returns-zero falsifier run against both stores, so isolation is proven, not asserted.

Further out on the roadmap (tracked in .agent-docs/traceability/ledger.md): the immutable hash-chained audit (WU-10), the field-classification firewall manifest (WU-09), and the full agent-draft → HITL-approve → commit path through the agent tier. The read plane and the enforcement seams are in the tree today; the compiler that turns those seams into engine-enforced isolation is the next unit of work, not something already shipped.


Quickstart

Prerequisites — you need BOTH of these on the host, and nothing else:

  1. Jetify Devbox — the entire toolchain (node · pnpm · uv · python · buf · make) is pinned inside Devbox, so you don't install any of it by hand:

    curl -fsSL https://get.jetify.com/devbox | bash
  2. Docker — runs the infra (make up: Postgres · ArangoDB · Dragonfly · Hatchet) and the ephemeral testcontainers the apps/api suite spins up.

Devbox provides the languages and tools; Docker provides the services. Install both on the host — that's it.

# 1. Enter the toolchain shell (node/pnpm/uv/buf/make on PATH)
devbox shell

# 2. Bootstrap: install deps, generate proto stubs, build shared libs, install git hooks
make setup

# 3. Bring up infra (Docker must be running) — postgres, arango, dragonfly, hatchet.
#    On first run this also scaffolds .env from .env.example; edit it if you need to.
make up

# 4. Migrate and seed the governance schema (so GET /cases returns rows)
pnpm --dir apps/api pg:migrate:up
pnpm --dir apps/api pg:seed

# 5. Run the edge + web (api :3000, Swagger at /docs; web :5173)
make dev

The agent tier runs in its own terminals. It needs a Hatchet token — with infra up (step 3), mint one and paste it into .env as HATCHET_CLIENT_TOKEN, then start the two Python processes:

make hatchet-token   # prints a token — copy it into .env as HATCHET_CLIENT_TOKEN
make dev-agents      # Python gRPC agent server
make dev-worker      # Python Hatchet worker

Endpoints: API http://localhost:3000 (Swagger /docs) · Web http://localhost:5173 · Hatchet http://localhost:8888 · Arango http://localhost:8529.

What to look at first: authenticate at the edge, then call GET /cases. The response is tenant-scoped by a predicate in the query, not by a route check — remove the @CheckPolicies from the controller and the read is still bounded by the repository's where tenant_id, which is the whole point of authorization at the data layer. (Today that predicate is applied in the repository; WU-05 lowers it into engine-enforced RLS.)

Quality gates

All three must pass; the git hooks enforce a subset at commit/push time.

make lint        # eslint (strict) + ruff
make typecheck   # tsc + mypy --strict
make test        # vitest + pytest

Repository layout

apps/
  api/        NestJS/Fastify governed edge (TypeScript) — the boundary
  web/        React frontend (Vite) — shares the CASL model with the edge
  agents/     LangGraph inner graph + Hatchet worker (Python) — the agent tier
packages/
  authz/      CASL abilities + domain subjects (framework-free, shared)
  contracts/  generated gRPC/proto types (ts-proto) + shared zod schemas
proto/        the .proto contract (aegis.agent.v1, aegis.governance.v1)
.agent-docs/  the in-repo context system (see below)

The flagship vertical (a financial-crime-review workload) is meant to live on a branch as a governance pack; main carries the neutral governed-approval flow so the machinery is legible without a domain.


How this was built

AEGIS is also a demonstration of AI-native engineering done with discipline rather than vibes — and the working record is in the tree, not asserted after the fact.

  • An in-repo context system.agent-docs/ carries the fleeting state (now/), the decision record (decisions/), the production-reachability ledger (traceability/), review reports, and runbooks. It is the memory the build ran on.
  • ADRs with alternatives — every non-trivial decision records its deciding axis, an honest steelman of the runner-up, and a flip-condition. The rejected options are kept because the reasoning is portable even where the choice isn't — if you're building something adjacent, that's the part you actually need.
  • An adversarial, gated build — the reviewer is never the builder; designs and diffs are checked by a clean-context verifier before they proceed, and a unit of work is "done" only when a path traces from a real entrypoint to the new code (the IMPL→WIRED bar), not when the tests merely go green.

Orient yourself — clone, open Claude Code, /orient

The .agent-docs/ directory is not just documentation — it's a machine-readable context system built so a person or an AI coding agent can pick up the work with zero archaeology. It carries the fleeting state (now/ — a curated handoff, status, work-plan, open-questions), the decision record (decisions/), the production-reachability ledger (traceability/), typed review reports (reviews/), lessons, and runbooks — each routed through a per-directory index.md.

Clone the repo, open it in Claude Code, and run:

/orient

It reads the curated handoff, verifies it against the live git state, and surfaces a short brief — where the work is, what's in flight, the open questions that gate progress, and the immediate next action — so you start contributing in minutes instead of reverse-engineering the tree. /handoff regenerates that bridge at session end (or before context runs out); /flush checkpoints mid-session. The full working discipline is in CLAUDE.md and the cross-tool entrypoint AGENTS.md.

License

Apache License 2.0 — permissive, with an explicit patent grant, so you can fork this reference architecture into your own vertical. © 2026 Armen Rostamian.

About

A production-shaped reference architecture for governed, multi-tenant, agentic LLM apps — a NestJS/CASL boundary with data-layer authorization, Hatchet + LangGraph orchestration, human-in-the-loop safety gates, and a sensitive-data firewall. Built with a visible AI-native dev methodology.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors