-
Notifications
You must be signed in to change notification settings - Fork 0
feat: A.I.R. API layer — VaultNode operational stack #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| .env | ||
| __pycache__/ | ||
| *.pyc | ||
| *.pyo | ||
| .venv/ | ||
| venv/ |
| 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: |
| 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: | ||
| 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}" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Incident IDs are derived only from 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]} | ||
| 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 |
| 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 | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 intoeventsas "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 👍 / 👎.