generated from coulomb/repo-seed
Compare commits
6 Commits
cdb1a970e5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 07f6046c4c | |||
| e8c300d265 | |||
| 6e305f4f47 | |||
| 0ef4450f39 | |||
| 5b910d86d2 | |||
| 6d7422d888 |
20
.claude/rules/agents.md
Normal file
20
.claude/rules/agents.md
Normal 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.
|
||||||
8
.claude/rules/architecture.md
Normal file
8
.claude/rules/architecture.md
Normal 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
|
||||||
50
.claude/rules/credential-routing.md
Normal file
50
.claude/rules/credential-routing.md
Normal 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`
|
||||||
38
.claude/rules/first-session.md
Normal file
38
.claude/rules/first-session.md
Normal 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 1–3 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 -->
|
||||||
8
.claude/rules/repo-boundary.md
Normal file
8
.claude/rules/repo-boundary.md
Normal 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/
|
||||||
|
-->
|
||||||
5
.claude/rules/repo-identity.md
Normal file
5
.claude/rules/repo-identity.md
Normal 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
|
||||||
85
.claude/rules/session-protocol.md
Normal file
85
.claude/rules/session-protocol.md
Normal 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.
|
||||||
20
.claude/rules/stack-and-commands.md
Normal file
20
.claude/rules/stack-and-commands.md
Normal 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
|
||||||
|
```
|
||||||
40
.claude/rules/workplan-convention.md
Normal file
40
.claude/rules/workplan-convention.md
Normal 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 -->
|
||||||
@@ -2,18 +2,12 @@
|
|||||||
# Custodian Brief — audit-core
|
# Custodian Brief — audit-core
|
||||||
|
|
||||||
**Domain:** infotech
|
**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)*
|
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||||
|
|
||||||
## Active Workstreams
|
## Active Workstreams
|
||||||
|
|
||||||
### Bootstrap State Hub integration
|
*(none — repo may need first-session setup)*
|
||||||
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`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## MCP Orientation (when available)
|
## MCP Orientation (when available)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ repo_classification:
|
|||||||
standard: Repo Classification Standard
|
standard: Repo Classification Standard
|
||||||
version: '1.0'
|
version: '1.0'
|
||||||
classified_at: '2026-06-22'
|
classified_at: '2026-06-22'
|
||||||
classified_by: agent
|
classified_by: human
|
||||||
category: tooling
|
category: tooling
|
||||||
domain: infotech
|
domain: infotech
|
||||||
secondary_domains: []
|
secondary_domains: []
|
||||||
@@ -20,4 +20,4 @@ repo_classification:
|
|||||||
business_mechanics:
|
business_mechanics:
|
||||||
- control
|
- control
|
||||||
- operation
|
- operation
|
||||||
notes: Multi-tenant audit emit capability for platform bootstrap wiring.
|
notes: Multi-tenant audit emit capability; human confirmed.
|
||||||
|
|||||||
51
AGENTS.md
51
AGENTS.md
@@ -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 -->
|
<!-- REPO-AGENTS-EXTENSIONS -->
|
||||||
<!-- Append repo-specific agent instructions below this marker.
|
<!-- Append repo-specific agent instructions below this marker.
|
||||||
|
|||||||
12
CLAUDE.md
Normal file
12
CLAUDE.md
Normal 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
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
Reliable multi-tenant auto setup audit capability
|
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
|
## Development Mock Backend
|
||||||
|
|
||||||
The first implementation is intentionally tiny: a replaceable audit interface
|
The first implementation is intentionally tiny: a replaceable audit interface
|
||||||
|
|||||||
@@ -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
|
from audit_core.mock_file_backend import MockFileAuditBackend
|
||||||
|
|
||||||
__all__ = ["AuditBackend", "AuditEvent", "MockFileAuditBackend"]
|
__all__ = [
|
||||||
|
"AuditBackend",
|
||||||
|
"AuditEvent",
|
||||||
|
"EventValidationError",
|
||||||
|
"MockFileAuditBackend",
|
||||||
|
"RetentionPolicy",
|
||||||
|
"SCHEMA_VERSION_V1ALPHA1",
|
||||||
|
"validate_event",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Protocol
|
from typing import Any, Literal, Protocol, runtime_checkable
|
||||||
from uuid import uuid4
|
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:
|
def utc_now() -> str:
|
||||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
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)
|
@dataclass(frozen=True)
|
||||||
class AuditEvent:
|
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
|
source: str
|
||||||
action: str
|
action: str
|
||||||
@@ -27,7 +63,7 @@ class AuditEvent:
|
|||||||
details: dict[str, Any] = field(default_factory=dict)
|
details: dict[str, Any] = field(default_factory=dict)
|
||||||
event_id: str = field(default_factory=lambda: str(uuid4()))
|
event_id: str = field(default_factory=lambda: str(uuid4()))
|
||||||
observed_at: str = field(default_factory=utc_now)
|
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]:
|
def as_record(self) -> dict[str, Any]:
|
||||||
return {
|
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):
|
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:
|
def emit(self, event: AuditEvent) -> str:
|
||||||
"""Persist an event and return a backend-specific reference."""
|
"""Persist an event and return a backend-specific reference."""
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Development-only file backend for Audit Core.
|
"""Development-only file backend for Audit Core.
|
||||||
|
|
||||||
This backend intentionally writes local JSONL files under /tmp by default. It
|
Writes hourly JSONL files under ``/tmp/audit-core`` by default. See
|
||||||
is useful for wiring integrations before the durable Audit Core archive exists,
|
``docs/audit-backend-contract.md`` for retention guarantees and the migration
|
||||||
but it is not production audit custody.
|
path to durable archive backends.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -12,11 +12,15 @@ import os
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from audit_core.interface import AuditEvent
|
from audit_core.interface import AuditEvent, EventValidationError, RetentionPolicy, validate_event
|
||||||
|
|
||||||
|
|
||||||
class MockFileAuditBackend:
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -32,7 +36,23 @@ class MockFileAuditBackend:
|
|||||||
)
|
)
|
||||||
self._now = now
|
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:
|
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.base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.cleanup_old_files()
|
self.cleanup_old_files()
|
||||||
path = self.current_path()
|
path = self.current_path()
|
||||||
|
|||||||
228
docs/audit-backend-contract.md
Normal file
228
docs/audit-backend-contract.md
Normal 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
118
tests/test_interface.py
Normal 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)
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Bootstrap State Hub integration"
|
title: "Bootstrap State Hub integration"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: audit-core
|
repo: audit-core
|
||||||
status: ready
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: custodian
|
topic_slug: custodian
|
||||||
created: "2026-06-22"
|
created: "2026-06-22"
|
||||||
@@ -20,11 +20,14 @@ Reliable multi-tenant auto setup audit capability.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AUDIT-WP-0001-T01
|
id: AUDIT-WP-0001-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "59896d6b-85ed-455d-9bd4-b546a050205e"
|
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`.
|
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
|
||||||
Replace generated placeholders with repo-specific facts where needed.
|
Replace generated placeholders with repo-specific facts where needed.
|
||||||
|
|
||||||
@@ -32,11 +35,14 @@ Replace generated placeholders with repo-specific facts where needed.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AUDIT-WP-0001-T02
|
id: AUDIT-WP-0001-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "f74d7033-a20b-45da-9d0a-23426e60403e"
|
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
|
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
|
those commands in the agent instructions so future coding sessions can verify
|
||||||
changes confidently.
|
changes confidently.
|
||||||
@@ -45,11 +51,14 @@ changes confidently.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AUDIT-WP-0001-T03
|
id: AUDIT-WP-0001-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "5df0cd46-9193-48a0-bd59-0e6acbc56270"
|
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
|
Create the first implementation workplan for the repository's most important
|
||||||
next change. After workplan file updates, run from `~/state-hub`:
|
next change. After workplan file updates, run from `~/state-hub`:
|
||||||
|
|
||||||
|
|||||||
31
workplans/AUDIT-WP-0002-pluggable-audit-backend.md
Normal file
31
workplans/AUDIT-WP-0002-pluggable-audit-backend.md
Normal 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.
|
||||||
Reference in New Issue
Block a user