Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# A.I.R. Environment — copy to .env and populate
# NEVER commit .env to version control

AIR_DB_USER=architect
AIR_DB_PASSWORD=your_sovereign_password_here
AIR_DB_NAME=air_node
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
__pycache__/
*.pyc
*.pyo
.venv/
venv/
23 changes: 23 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# A.I.R. — Agent Incident Recorder
# VaultNode // Blackglass Continuum LLC
# CAGE: 17TJ5 | UEI: SVZVXPTM9AF4

version: '3.8'

services:
air-db:
image: postgres:15-alpine
container_name: air_vault_db
environment:
POSTGRES_USER: ${AIR_DB_USER}
POSTGRES_PASSWORD: ${AIR_DB_PASSWORD}
POSTGRES_DB: ${AIR_DB_NAME}
ports:
- "5432:5432"
volumes:
- ./schema.sql:/docker-entrypoint-initdb.d/init.sql:ro
- air_db_data:/var/lib/postgresql/data
restart: unless-stopped

volumes:
air_db_data:
190 changes: 190 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# A.I.R. — Agent Incident Recorder
# VaultNode // Blackglass Continuum LLC
# CAGE: 17TJ5 | UEI: SVZVXPTM9AF4
# Mission: Truth Preservation in Agentic Workflows

from contextlib import asynccontextmanager
from typing import Any, Dict, List

import asyncpg
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from pydantic_settings import BaseSettings


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

class Settings(BaseSettings):
air_db_user: str
air_db_password: str
air_db_name: str
air_db_host: str = "localhost"
air_db_port: int = 5432

class Config:
env_file = ".env"

settings = Settings()


# ---------------------------------------------------------------------------
# Lifespan — DB pool init/teardown
# ---------------------------------------------------------------------------

@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.pool = await asyncpg.create_pool(
user=settings.air_db_user,
password=settings.air_db_password,
database=settings.air_db_name,
host=settings.air_db_host,
port=settings.air_db_port,
min_size=2,
max_size=10,
)
yield
await app.state.pool.close()


# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------

app = FastAPI(
title="A.I.R. VaultNode API",
version="0.1.0",
description="Truth Preservation in Agentic Workflows",
lifespan=lifespan,
)


# ---------------------------------------------------------------------------
# Sovereign State Definitions
# ---------------------------------------------------------------------------

class AgentEvent(BaseModel):
agent_id: str
session_id: str
action: str
state_before: str
state_after: str
metadata: Dict[str, Any] = {}


class WorkflowDef(BaseModel):
name: str
definition: Dict[str, List[str]]


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------

@app.post("/event")
async def log_event(event: AgentEvent):
"""
Core interdiction vector.
1. Fetch authorized workflow for this session.
2. If state_after NOT IN valid_transitions[state_before] → flag incident.
3. Else → commit event to ledger.
"""
async with app.state.pool.acquire() as conn:
# Fetch workflow bound to this session's agent
row = await conn.fetchrow(
"""
SELECT w.definition
FROM sessions s
JOIN workflows w ON w.name = s.agent_id
WHERE s.id = $1
""",
event.session_id,
)

if row:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject events when no workflow is found

The interdiction check only runs inside if row, so when a session exists but has no matching workflow, the request skips validation and still inserts into events as "authorized." This creates a straightforward bypass where unratified sessions can commit transitions without incident logging, which undermines the core enforcement path described by this endpoint.

Useful? React with 👍 / 👎.

definition = row["definition"]
valid_next = definition.get(event.state_before, [])
if event.state_after not in valid_next:
# Unauthorized transition — generate incident
incident_id = f"INC-{event.session_id}-{event.action}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make incident IDs unique per violation

Incident IDs are derived only from session_id and action, but inserts use ON CONFLICT (id) DO NOTHING, so multiple unauthorized transitions with the same action in one session are silently discarded after the first one. In practice this drops evidence of repeated drift and leaves the incidents ledger incomplete for exactly the cases this service is meant to audit.

Useful? React with 👍 / 👎.

await conn.execute(
"""
INSERT INTO incidents
(id, session_id, reason, observed_transition, expected_transition)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO NOTHING
""",
incident_id,
event.session_id,
"unauthorized_state_transition",
f"{event.state_before} -> {event.state_after}",
valid_next,
)
raise HTTPException(
status_code=409,
detail={
"status": "incident_flagged",
"incident_id": incident_id,
"observed": f"{event.state_before} -> {event.state_after}",
"authorized": valid_next,
},
)

# Authorized — commit to ledger
await conn.execute(
"""
INSERT INTO events
(id, session_id, action, state_before, state_after, raw_json)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5)
""",
event.session_id,
event.action,
event.state_before,
event.state_after,
event.metadata,
)

return {"status": "committed", "session_id": event.session_id}


@app.post("/workflow")
async def register_workflow(workflow: WorkflowDef):
"""Commits a JSON rule-engine to the workflows table."""
async with app.state.pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO workflows (name, definition)
VALUES ($1, $2)
ON CONFLICT (name) DO UPDATE SET definition = EXCLUDED.definition
""",
workflow.name,
workflow.definition,
)
return {"status": "workflow_locked", "workflow": workflow.name}


@app.get("/incidents")
async def list_incidents():
"""Returns the absolute proof of agent drift."""
async with app.state.pool.acquire() as conn:
rows = await conn.fetch(
"SELECT * FROM incidents ORDER BY created_at DESC"
)
return {"incidents": [dict(r) for r in rows]}


@app.get("/session/{session_id}")
async def replay_session(session_id: str):
"""Reconstructs the exact chronological timeline of an agent's actions."""
async with app.state.pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT id, timestamp, action, state_before, state_after
FROM events
WHERE session_id = $1
ORDER BY timestamp ASC
""",
session_id,
)
return {"session_id": session_id, "timeline": [dict(r) for r in rows]}
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fastapi==0.111.0
uvicorn[standard]==0.29.0
asyncpg==0.29.0
pydantic==2.7.1
pydantic-settings==2.2.1
39 changes: 39 additions & 0 deletions schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- A.I.R. — Agent Incident Recorder
-- VaultNode // Blackglass Continuum LLC
-- CAGE: 17TJ5 | UEI: SVZVXPTM9AF4
-- Schema Authority: Ratified Day One

CREATE TABLE agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL
);

CREATE TABLE sessions (
id TEXT PRIMARY KEY,
agent_id TEXT REFERENCES agents(id),
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE workflows (
name TEXT PRIMARY KEY,
definition JSONB NOT NULL
);

CREATE TABLE events (
id UUID PRIMARY KEY,
session_id TEXT REFERENCES sessions(id),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
action TEXT NOT NULL,
state_before TEXT NOT NULL,
state_after TEXT NOT NULL,
raw_json JSONB
);

CREATE TABLE incidents (
id TEXT PRIMARY KEY,
session_id TEXT REFERENCES sessions(id),
reason TEXT NOT NULL,
observed_transition TEXT NOT NULL,
expected_transition JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);