Compare commits

..

6 Commits

Author SHA1 Message Date
07f6046c4c Moved audit forward somewhat 2026-07-04 00:39:03 +02:00
e8c300d265 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for audit-core
2026-06-24 15:30:14 +02:00
6e305f4f47 Complete State Hub bootstrap workplans (WP-0001)
- Review integration files; fill SCOPE where templated
- Document dev workflow in stack-and-commands.md
- Seed WP-0002 implementation workplan; mark bootstrap finished
- Hub sync via fix-consistency
2026-06-22 23:34:58 +02:00
0ef4450f39 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-22:
  - update .custodian-brief.md for audit-core
2026-06-22 23:32:49 +02:00
5b910d86d2 Normalize agent instructions and workplan frontmatter (STATE-WP-0067)
- Align agent files with on-disk workplan prefixes (infer from workplan ids)
- Set workplan domain to registered domain_slug; add topic_slug where applicable
- Repair frontmatter delimiter formatting; migrate legacy task status literals
- Regenerate AGENTS.md, CLAUDE.md, and .claude/rules from State Hub templates
2026-06-22 23:16:24 +02:00
6d7422d888 Human-review .repo-classification.yaml (CUST-WP-0050 follow-up) 2026-06-22 17:56:17 +02:00
21 changed files with 865 additions and 29 deletions

20
.claude/rules/agents.md Normal file
View File

@@ -0,0 +1,20 @@
## Kaizen Agents
Specialized agent personas available on demand via the state-hub MCP.
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
Common agents:
| Agent | Category | When to use |
|-------|----------|-------------|
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
| `test-maintenance` | testing | Diagnose and fix failing tests |
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
| `keepaTodofile` | process | Maintain TODO.md during work |
| `project-management` | process | Track status, determine next steps |
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
All 17 agents: call `list_kaizen_agents()` for the full list.

View File

@@ -0,0 +1,8 @@
## Architecture
<!-- TODO: Describe the key design decisions and component structure.
Key modules, data flows, external integrations, state machines, etc. -->
## Quick Reference
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference

View File

@@ -0,0 +1,50 @@
# Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=audit-core` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes**`warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`

View File

@@ -0,0 +1,38 @@
## First Session Protocol
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
- `~/the-custodian/canon/projects/infotech/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/infotech/roadmap_v0.1.md` — planned phases
- Scan repo root: README, directory structure, existing code or docs
**Step 2 — Survey in-progress work**
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
**Step 3 — Propose workstreams to Bernd**
Propose 13 workstreams — each a coherent strand, weeks to months, anchored to a
roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
```
workplans/AUDIT-WP-NNNN-<slug>.md ← write this first
```
Then register in the hub:
```
create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...")
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
```
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured infotech into N workstreams, M tasks",
event_type="milestone",
topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a",
detail={"workstreams": [...], "tasks_created": M}
)
```
<!-- Delete or archive this file once past first session -->

View File

@@ -0,0 +1,8 @@
## Repo boundary
This repo owns **audit-core** only. It does not own:
<!-- TODO: List what belongs in adjacent repos, e.g.:
- SSH key management → railiance-infra/
- State hub code → state-hub/
-->

View File

@@ -0,0 +1,5 @@
**Purpose:** Reliable multi-tenant auto setup audit capability.
**Domain:** infotech
**Repo slug:** audit-core
**Topic ID:** cee7bedf-2b48-46ef-8601-006474f2ad7a

View File

@@ -0,0 +1,85 @@
## Session Protocol
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
Read the offline-safe brief first — it works without a live hub connection:
```bash
cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("infotech")
```
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
```
If the hub is offline: `cd ~/state-hub && make api`
**Step 2 — Check inbox**
With MCP tools:
```
get_messages(to_agent="audit-core", unread_only=True)
```
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
requests before proceeding.
Without MCP tools:
```bash
curl -s "http://127.0.0.1:8000/messages/?to_agent=audit-core&unread_only=true" \
| python3 -m json.tool
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
**Step 3 — Scan workplans**
```bash
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:audit-core]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
- `alignment_warnings`: flag if active work is not aligned with current goal
4. **Suggested next action** — highest-priority open item
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
If no workstreams: follow First Session Protocol (`first-session.md`).
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
**Session close:**
With MCP tools:
```
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="<uuid>")
```
Without MCP tools:
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
```bash
git -C <repo_path> pull --ff-only
cd ~/state-hub && make fix-consistency REPO=audit-core
```
For repos where implementation runs on a remote machine (e.g. CoulombCore),
use the combined target which pulls before fixing:
```bash
cd ~/state-hub && make fix-consistency-remote REPO=audit-core
```
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
until you pull — intentional to prevent clobbering remote progress.

View File

@@ -0,0 +1,20 @@
## Stack
- **Language:** Python 3.11+
- **Key deps:** stdlib + pytest (see `pyproject.toml`)
## Dev Commands
```bash
# Install (editable)
pip install -e ".[dev]" # or: python3 -m pip install pytest
# Run tests
make test
python3 -m pytest -q
# Mock audit backend smoke / cleanup
make mock-audit-smoke
make mock-audit-cleanup
python3 -m audit_core emit --help
```

View File

@@ -0,0 +1,40 @@
## Workplan Convention (ADR-001)
File location: `workplans/AUDIT-WP-NNNN-<slug>.md`
ID prefix: `AUDIT-WP-`
Work items originate as files in this repo **before** being registered in the hub.
Canonical workplan/workstream frontmatter statuses are:
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
Use `proposed` for a newly drafted plan, `ready` after review against current
repo state, and `finished` when implementation is complete. `stalled` and
`needs_review` are derived health labels, not stored statuses.
Closed workplans may be moved to `workplans/archived/` with a completion-date
prefix: `YYMMDD-AUDIT-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:audit-core]` hub tasks —
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
Task blocks use this shape:
```task
id: AUDIT-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
```
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View File

@@ -2,18 +2,12 @@
# Custodian Brief — audit-core
**Domain:** infotech
**Last synced:** 2026-06-22 11:56 UTC
**Last synced:** 2026-06-24 13:30 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Bootstrap State Hub integration
Progress: 0/3 done | workstream_id: `85f4008a-d8a0-409e-886b-ffcc49f78bfb`
**Open tasks:**
- · Review Generated Integration Files `59896d6b`
- · Verify Local Developer Workflow `f74d7033`
- · Seed First Real Workplan `5df0cd46`
*(none — repo may need first-session setup)*
---
## MCP Orientation (when available)

View File

@@ -2,7 +2,7 @@ repo_classification:
standard: Repo Classification Standard
version: '1.0'
classified_at: '2026-06-22'
classified_by: agent
classified_by: human
category: tooling
domain: infotech
secondary_domains: []
@@ -20,4 +20,4 @@ repo_classification:
business_mechanics:
- control
- operation
notes: Multi-tenant audit emit capability for platform bootstrap wiring.
notes: Multi-tenant audit emit capability; human confirmed.

View File

@@ -101,7 +101,56 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
---
{CREDENTIAL_ROUTING}
## Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=audit-core` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.

12
CLAUDE.md Normal file
View File

@@ -0,0 +1,12 @@
# audit-core — Claude Code Instructions
@SCOPE.md
@.claude/rules/repo-identity.md
@.claude/rules/session-protocol.md
@.claude/rules/first-session.md
@.claude/rules/workplan-convention.md
@.claude/rules/stack-and-commands.md
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/agents.md

View File

@@ -1,5 +1,11 @@
Reliable multi-tenant auto setup audit capability
## Backend contract
The pluggable backend interface, event schema (`audit-core.event.v1alpha1`),
retention policy, and migration path from the mock file backend are documented
in [`docs/audit-backend-contract.md`](docs/audit-backend-contract.md).
## Development Mock Backend
The first implementation is intentionally tiny: a replaceable audit interface

View File

@@ -1,6 +1,24 @@
"""Audit Core public interface."""
"""Audit Core public interface.
from audit_core.interface import AuditBackend, AuditEvent
Contract reference: ``docs/audit-backend-contract.md``.
"""
from audit_core.interface import (
AuditBackend,
AuditEvent,
EventValidationError,
RetentionPolicy,
SCHEMA_VERSION_V1ALPHA1,
validate_event,
)
from audit_core.mock_file_backend import MockFileAuditBackend
__all__ = ["AuditBackend", "AuditEvent", "MockFileAuditBackend"]
__all__ = [
"AuditBackend",
"AuditEvent",
"EventValidationError",
"MockFileAuditBackend",
"RetentionPolicy",
"SCHEMA_VERSION_V1ALPHA1",
"validate_event",
]

View File

@@ -1,20 +1,56 @@
"""Small audit interface shared by backends and integrations."""
"""Pluggable audit backend contract.
See ``docs/audit-backend-contract.md`` for the full protocol, event schema,
retention policy, and migration path from the mock file backend.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from typing import Any, Literal, Protocol, runtime_checkable
from uuid import uuid4
SCHEMA_VERSION_V1ALPHA1 = "audit-core.event.v1alpha1"
CustodyClass = Literal["development", "archive", "hot_search"]
_REQUIRED_STRING_FIELDS = (
"schema_version",
"event_id",
"observed_at",
"tenant",
"scope",
"source",
"action",
"resource",
"outcome",
)
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
@dataclass(frozen=True)
class RetentionPolicy:
"""Declarative retention guarantees exposed by an audit backend."""
custody_class: CustodyClass
retention_days: int | None
immutable: bool
tamper_evidence: bool
durable: bool
@dataclass(frozen=True)
class AuditEvent:
"""Minimal event envelope for the first Audit Core implementation."""
"""Normalized audit event for the v1alpha1 module-level contract.
The flat record maps to JSON via :meth:`as_record`. The nested
``audit-core.event.v1`` envelope in the product requirements is the
long-term HTTP ingestion target; adapters will translate between them.
"""
source: str
action: str
@@ -27,7 +63,7 @@ class AuditEvent:
details: dict[str, Any] = field(default_factory=dict)
event_id: str = field(default_factory=lambda: str(uuid4()))
observed_at: str = field(default_factory=utc_now)
schema_version: str = "audit-core.event.v1alpha1"
schema_version: str = SCHEMA_VERSION_V1ALPHA1
def as_record(self) -> dict[str, Any]:
return {
@@ -46,8 +82,49 @@ class AuditEvent:
}
class EventValidationError(ValueError):
"""Raised when an event record fails contract validation."""
def validate_event(event: AuditEvent) -> None:
"""Validate an event against the v1alpha1 contract.
Raises :class:`EventValidationError` when required fields are missing,
empty, or use an unsupported schema version.
"""
record = event.as_record()
errors: list[str] = []
if record["schema_version"] != SCHEMA_VERSION_V1ALPHA1:
errors.append(
f"unsupported schema_version: {record['schema_version']!r} "
f"(expected {SCHEMA_VERSION_V1ALPHA1!r})"
)
for name in _REQUIRED_STRING_FIELDS:
value = record.get(name)
if not isinstance(value, str) or not value.strip():
errors.append(f"{name} must be a non-empty string")
if not isinstance(record.get("details"), dict):
errors.append("details must be a mapping")
if errors:
raise EventValidationError("; ".join(errors))
@runtime_checkable
class AuditBackend(Protocol):
"""Protocol implemented by audit sinks."""
"""Protocol implemented by replaceable audit sinks.
Production backends provide durable archive or hot-search custody.
Development backends (such as :class:`~audit_core.mock_file_backend.MockFileAuditBackend`)
are for wiring only and must not be treated as audit custody.
"""
@property
def retention_policy(self) -> RetentionPolicy:
"""Describe retention and custody guarantees for readiness checks."""
def emit(self, event: AuditEvent) -> str:
"""Persist an event and return a backend-specific reference."""
"""Persist an event and return a backend-specific reference."""

View File

@@ -1,8 +1,8 @@
"""Development-only file backend for Audit Core.
This backend intentionally writes local JSONL files under /tmp by default. It
is useful for wiring integrations before the durable Audit Core archive exists,
but it is not production audit custody.
Writes hourly JSONL files under ``/tmp/audit-core`` by default. See
``docs/audit-backend-contract.md`` for retention guarantees and the migration
path to durable archive backends.
"""
from __future__ import annotations
@@ -12,11 +12,15 @@ import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from audit_core.interface import AuditEvent
from audit_core.interface import AuditEvent, EventValidationError, RetentionPolicy, validate_event
class MockFileAuditBackend:
"""Append audit events to hourly JSONL files and clean up old files."""
"""Append audit events to hourly JSONL files and clean up old files.
Implements :class:`~audit_core.interface.AuditBackend` with
``custody_class=development``. Not suitable for production audit custody.
"""
def __init__(
self,
@@ -32,7 +36,23 @@ class MockFileAuditBackend:
)
self._now = now
@property
def retention_policy(self) -> RetentionPolicy:
days = self.retention_days if self.retention_days >= 0 else None
return RetentionPolicy(
custody_class="development",
retention_days=days,
immutable=False,
tamper_evidence=False,
durable=False,
)
def emit(self, event: AuditEvent) -> str:
try:
validate_event(event)
except EventValidationError as exc:
raise ValueError(str(exc)) from exc
self.base_dir.mkdir(parents=True, exist_ok=True)
self.cleanup_old_files()
path = self.current_path()

View File

@@ -0,0 +1,228 @@
# Audit Backend Contract
Audit Core separates **event producers** (integrations, CLI, future HTTP API) from
**audit backends** (sinks that persist normalized events). This document defines
the replaceable backend interface, the current event schema, retention guarantees,
and how to migrate from the development mock file backend to durable custody.
## AuditBackend protocol
Every backend implements `audit_core.interface.AuditBackend`:
```python
class AuditBackend(Protocol):
@property
def retention_policy(self) -> RetentionPolicy: ...
def emit(self, event: AuditEvent) -> str: ...
```
### `emit(event) -> str`
Persist one normalized `AuditEvent` and return a backend-specific reference.
Callers use the reference for debugging and correlation; it is not a stable
cross-backend identifier.
| Backend kind | Typical return value |
| --- | --- |
| Mock file | Absolute path to the hourly JSONL file |
| Archive (planned) | Batch URI or object key |
| Hot search (planned) | Stream offset or index document id |
Requirements:
- **Idempotent references:** Re-emitting the same logical event (same
`event_id`) must not corrupt prior records. Backends may append duplicates
unless deduplication is documented.
- **No secret dumping:** Backends must not log or persist plaintext secrets,
tokens, keys, or passwords from `details` or future payload fields.
- **Visible failure:** Raise on persistence failure; do not silently drop events.
- **Normalization only:** Backends receive `AuditEvent` instances. Source-specific
adapters run upstream.
### `retention_policy`
Each backend exposes a frozen `RetentionPolicy` describing what it guarantees.
Integrators and readiness checks use this to decide whether a sink satisfies a
scope's policy. See [Retention policy](#retention-policy).
## Event schema (`audit-core.event.v1alpha1`)
The first implementation uses a flat JSON record produced by `AuditEvent.as_record()`.
This is a deliberate simplification for local wiring; the long-term envelope in
`spec/ProductRequirementsDefinition.md` (`audit-core.event.v1`) nests source,
tenant, scope, actor, and result objects.
### Required fields
| Field | Type | Description |
| --- | --- | --- |
| `schema_version` | string | Must be `audit-core.event.v1alpha1` for this contract |
| `event_id` | string | UUID or source-stable identifier |
| `observed_at` | string | UTC ISO-8601 timestamp (no subsecond precision) |
| `tenant` | string | Tenant id; default `platform` for control-plane events |
| `scope` | string | Scope id; default `platform-control-plane` |
| `source` | string | Emitter id (e.g. `openbao`, `audit-core`) |
| `action` | string | Namespaced action (e.g. `openbao.audit.list`) |
| `resource` | string | Affected resource path or id |
| `outcome` | string | Result label (e.g. `success`, `failure`, `denied`) |
### Optional fields
| Field | Type | Description |
| --- | --- | --- |
| `actor` | string or null | Subject performing the action |
| `reason` | string or null | Human-readable result explanation |
| `details` | object | Source-specific extension map; must not contain secrets |
### Example record
```json
{
"action": "openbao.authenticated_readiness_proof",
"actor": null,
"details": {"backend": "mock-file", "file_audit_visible": true},
"event_id": "6f3e2b1a-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"observed_at": "2026-06-01T20:30:00+00:00",
"outcome": "success",
"reason": null,
"resource": "openbao/openbao-0",
"schema_version": "audit-core.event.v1alpha1",
"scope": "platform-control-plane",
"source": "openbao",
"tenant": "platform"
}
```
### Validation
Use `audit_core.interface.validate_event()` before emit. It checks required
fields, `schema_version`, and rejects empty strings on required identifiers.
### Evolution to `audit-core.event.v1`
Future backends will accept the nested v1 envelope at the HTTP ingestion layer.
Module-level backends may continue using `AuditEvent` with adapter translation.
Compatibility rules (not yet implemented):
- v1alpha1 records remain readable in archive export.
- New required v1 fields get sensible defaults during adapter migration.
- `schema_version` gates parser selection.
## Retention policy
`RetentionPolicy` is declarative metadata; enforcement is backend-specific.
| Field | Meaning |
| --- | --- |
| `custody_class` | `development`, `archive`, or `hot_search` |
| `retention_days` | Maximum age before eligible deletion; `None` means indefinite |
| `immutable` | Whether stored records are protected from in-place alteration |
| `tamper_evidence` | Whether manifests, hash chains, or signatures exist |
| `durable` | Whether survival is expected across process restarts and host reboots |
### Custody classes
| Class | Purpose | Guarantees |
| --- | --- | --- |
| `development` | Local integration and bootstrap wiring | Ephemeral local files; best-effort cleanup; **not audit custody** |
| `archive` | Long-term evidence (planned) | Durable object storage, batch manifests, explicit retention |
| `hot_search` | Operational investigation (planned) | Shorter retention; searchable; not the evidence record |
### Mock file backend policy
`MockFileAuditBackend.retention_policy`:
- `custody_class`: `development`
- `retention_days`: 7 (override via `AUDIT_CORE_MOCK_RETENTION_DAYS` or constructor)
- `immutable`: false
- `tamper_evidence`: false
- `durable`: false
Enforcement:
- Events append to hourly JSONL files under `AUDIT_CORE_MOCK_DIR` (default
`/tmp/audit-core`).
- `cleanup_old_files()` deletes `audit-*.jsonl` whose mtime is older than
`retention_days`. Cleanup runs on each `emit` and via `python3 -m audit_core cleanup`.
- Negative `retention_days` disables automatic deletion.
**Not guaranteed:** crash-safe writes, replication, encryption, tenant isolation,
integrity proofs, or survival of `/tmp` across reboots.
### Production archive policy (planned)
Target guarantees for the first durable backend:
- `custody_class`: `archive`
- `retention_days`: scope policy (often years, sometimes indefinite)
- `immutable`: true (WORM / object lock where available)
- `tamper_evidence`: true (batch manifests with content hashes)
- `durable`: true
## Migration path: mock file → durable backend
### Phase 0 — today (mock file)
Use `MockFileAuditBackend` or the CLI:
```bash
python3 -m audit_core emit \
--source my-service \
--action my_service.audit.smoke \
--resource my-service/instance-0 \
--outcome success
```
Integrations import the protocol, not the mock:
```python
from audit_core import AuditBackend, AuditEvent, MockFileAuditBackend
backend: AuditBackend = MockFileAuditBackend()
backend.emit(AuditEvent(source="...", action="...", resource="...", outcome="success"))
```
### Phase 1 — dual-write readiness (planned)
1. Register a durable archive backend implementing `AuditBackend`.
2. Configure routing: development scopes may keep mock; production scopes require
`custody_class=archive`.
3. Readiness checks compare `backend.retention_policy` against scope policy and
fail closed when custody is insufficient.
### Phase 2 — archive primary (planned)
1. Point `emit` calls (or HTTP ingestion) at the archive backend.
2. Retain mock only for local `make mock-audit-smoke` and unit tests.
3. Export historical mock JSONL into archive batches with manifest generation.
### Phase 3 — hot search adjunct (planned)
Add a second `AuditBackend` with `custody_class=hot_search` for investigation.
Archive remains the evidence record; hot search may use shorter `retention_days`.
### Code migration checklist
| Step | Action |
| --- | --- |
| 1 | Depend on `AuditBackend`, not `MockFileAuditBackend`, in integration code |
| 2 | Build `AuditEvent` with explicit `tenant`, `scope`, and `source` |
| 3 | Call `validate_event()` before emit |
| 4 | Inspect `retention_policy` in readiness gates |
| 5 | Replace mock construction with injected backend from configuration |
| 6 | Verify export/manifest workflow before decommissioning mock files |
## Reference implementations
| Backend | Module | Custody class |
| --- | --- | --- |
| Mock file JSONL | `audit_core.mock_file_backend.MockFileAuditBackend` | `development` |
| Archive (planned) | TBD | `archive` |
| Hot search (planned) | TBD | `hot_search` |
## Related documents
- `INTENT.md` — product purpose and principles
- `spec/ProductRequirementsDefinition.md` — full v1 envelope and API requirements
- `registry/capabilities/capability.audit.event-retain.md` — capability registry entry

118
tests/test_interface.py Normal file
View File

@@ -0,0 +1,118 @@
from __future__ import annotations
import pytest
from audit_core.interface import (
AuditBackend,
AuditEvent,
EventValidationError,
RetentionPolicy,
SCHEMA_VERSION_V1ALPHA1,
validate_event,
)
from audit_core.mock_file_backend import MockFileAuditBackend
def test_validate_event_accepts_minimal_event():
event = AuditEvent(
source="openbao",
action="audit.verify",
resource="openbao/openbao-0",
outcome="success",
)
validate_event(event)
@pytest.mark.parametrize(
"field_name,value",
[
("source", ""),
("action", " "),
("resource", ""),
("outcome", ""),
],
)
def test_validate_event_rejects_empty_required_fields(field_name, value):
kwargs = {
"source": "openbao",
"action": "audit.verify",
"resource": "openbao/openbao-0",
"outcome": "success",
field_name: value,
}
with pytest.raises(EventValidationError, match=field_name):
validate_event(AuditEvent(**kwargs))
def test_validate_event_rejects_unsupported_schema_version():
event = AuditEvent(
source="openbao",
action="audit.verify",
resource="openbao/openbao-0",
outcome="success",
schema_version="audit-core.event.v99",
)
with pytest.raises(EventValidationError, match="unsupported schema_version"):
validate_event(event)
def test_mock_backend_satisfies_audit_backend_protocol():
backend = MockFileAuditBackend()
assert isinstance(backend, AuditBackend)
def test_mock_backend_retention_policy_is_development():
backend = MockFileAuditBackend(retention_days=7)
policy = backend.retention_policy
assert policy == RetentionPolicy(
custody_class="development",
retention_days=7,
immutable=False,
tamper_evidence=False,
durable=False,
)
def test_mock_backend_retention_policy_none_when_cleanup_disabled():
backend = MockFileAuditBackend(retention_days=-1)
assert backend.retention_policy.retention_days is None
def test_audit_event_record_uses_v1alpha1_schema():
event = AuditEvent(
source="audit-core",
action="audit_core.contract.smoke",
resource="audit-core/tests",
outcome="success",
)
record = event.as_record()
assert record["schema_version"] == SCHEMA_VERSION_V1ALPHA1
assert set(record) == {
"schema_version",
"event_id",
"observed_at",
"tenant",
"scope",
"source",
"actor",
"action",
"resource",
"outcome",
"reason",
"details",
}
def test_mock_backend_emit_rejects_invalid_event(tmp_path):
backend = MockFileAuditBackend(base_dir=tmp_path)
event = AuditEvent(
source="",
action="audit.verify",
resource="openbao/openbao-0",
outcome="success",
)
with pytest.raises(ValueError, match="source must be a non-empty string"):
backend.emit(event)

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Bootstrap State Hub integration"
domain: infotech
repo: audit-core
status: ready
status: finished
owner: codex
topic_slug: custodian
created: "2026-06-22"
@@ -20,11 +20,14 @@ Reliable multi-tenant auto setup audit capability.
```task
id: AUDIT-WP-0001-T01
status: todo
status: done
priority: high
state_hub_task_id: "59896d6b-85ed-455d-9bd4-b546a050205e"
```
Result 2026-06-22: INTENT.md and SCOPE.md reviewed; AGENTS.md and brief confirmed.
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
Replace generated placeholders with repo-specific facts where needed.
@@ -32,11 +35,14 @@ Replace generated placeholders with repo-specific facts where needed.
```task
id: AUDIT-WP-0001-T02
status: todo
status: done
priority: high
state_hub_task_id: "f74d7033-a20b-45da-9d0a-23426e60403e"
```
Result 2026-06-22: Documented Python/pytest workflow in stack-and-commands.md.
Identify the repo's install, test, lint, build, and run commands. Add or refine
those commands in the agent instructions so future coding sessions can verify
changes confidently.
@@ -45,11 +51,14 @@ changes confidently.
```task
id: AUDIT-WP-0001-T03
status: todo
status: done
priority: medium
state_hub_task_id: "5df0cd46-9193-48a0-bd59-0e6acbc56270"
```
Result 2026-06-22: Created AUDIT-WP-0002.
Create the first implementation workplan for the repository's most important
next change. After workplan file updates, run from `~/state-hub`:

View File

@@ -0,0 +1,31 @@
---
id: AUDIT-WP-0002
type: workplan
title: "Pluggable audit backend contract"
domain: infotech
repo: audit-core
status: finished
owner: codex
topic_slug: custodian
created: "2026-06-22"
updated: "2026-06-24"
state_hub_workstream_id: "14725dbf-16ae-43e5-bf52-3f93238cf264"
---
# Pluggable audit backend contract
Define the replaceable audit backend interface beyond the mock JSONL writer and document retention guarantees.
## Author backend interface contract
```task
id: AUDIT-WP-0002-T01
status: done
priority: high
state_hub_task_id: "588ac6fa-eb41-49ce-8a1c-850d5791de0b"
```
Result 2026-06-24: Added `docs/audit-backend-contract.md`; expanded `AuditBackend`
with `RetentionPolicy`, `validate_event()`, and module docstrings; 13 tests pass.
Document `AuditBackend` protocol, event schema, retention policy, and migration path from the mock file backend in `docs/` or module docstrings.