Praetor Architecture: C4 Models & System Lattice
This document specifies the architectural topology of the Praetor Governance Engine using C4 modeling and interactive figures.
1. Level 1: System Context Diagram
The System Context diagram details Praetor's boundaries, autonomous agent interactions, git providers, and downstream cluster infrastructure.
Text description
- Praetor Boundary: Praetor Engine (praetorctl).
- Boxes: Engineer / Operator, AI Coding Agent (fed by compile-context), Operational Fork (operator data), Git Provider (GitHub enforces), ARC Runner Scale Sets (fork deploy/arc/), GitOps Controller (fork deploy/k8s/).
- Engineer / Operator → Praetor Engine (runs praetorctl).
- AI Coding Agent → Praetor Engine (reads AGENTS.md).
- Operational Fork → Praetor Engine (demand) → Operational Fork (sync).
- Praetor Engine → Git Provider (branch protection).
- Praetor Engine → ARC Runner Scale Sets (runner labels).
- GitOps Controller → Praetor Engine (deploys chart).
- demand is the .needs.yaml report praetorctl needs aggregates; sync is praetorctl operational, which prepares the fork from reviewed engine commits.
- The runner labels and the deployed chart meet owner-only fork paths; Praetor dispatches no jobs.
The runner labels and deploys chart edges cross into infrastructure this repository does not
ship. The ARC scale sets and the ArgoCD Application are operator data: deploy/arc/ and
deploy/k8s/ are owner-only paths (ownerOnlyPrefixes in internal/operationalsync/overlay.go)
that exist only in the operational fork, and the engine's .gitignore ignores them
(TestOwnerOnlyPrefixesAreIgnoredByTheEngine in internal/operationalsync/owner_only_test.go).
Praetor does not dispatch jobs: the runner policy only names the scale-set labels, praetorctl audit
checks that it resolves (auditRunnerMatrix in cmd/standardsctl/audit.go), and this repository's
workflows run on GitHub-hosted runners. The chart the GitOps controller deploys is the engine's own
deploy/helm/praetor.
The fork and the engine talk both ways (ADR-0012, decision 2). praetorctl operational prepares the
fork from reviewed engine commits (runOperationalSync in cmd/standardsctl/operational.go), and
the fork reports its demand back through .needs.yaml, which praetorctl needs scans and
aggregates. The engine reconciles branch protection through its forge driver
(ReconcileProtection in internal/forge/forge.go); only the GitHub driver enforces, the GitLab
and Gitea drivers fail every enforcement method with ErrNotImplemented.
2. Level 2: Container Diagram
The Container diagram illustrates the runtime modules within the Praetor binary, daemon services, and configuration inputs.
Text description
- Repository Space: .config/fleet.yaml (fleet layer, optional), .config/orgs/*.yaml (org layer, optional), .standards.yaml (manifest, repo layer), AGENTS.md (instructions), .agents/ (personas, skills), routing.yaml (.config/models/).
- Praetor Core Engine (Go Binary / Container): Runner Matrix Router (runner.ResolveRunner), Gating Engine (praetorctl gate run), HISS Invariant Scanner (hiss.Scan), CLI & Servers (cmd/standardsctl, mcp, lsp), HTTP Health Probes (praetorctl serve), Compiler & Transpiler (praetorctl compile-context), Bump Canary (bump.RunCanary), SARIF Distiller (lockdown.DistillSARIF), Model Router (praetorctl models).
- Generated Projections: Distroless Image (ghcr.io/cordanallm/praetor), Vendor Agent Files (CLAUDE.md, etc.), Diagnostic Summary (SARIF output).
- routing.yaml → Model Router.
- .config/fleet.yaml → Runner Matrix Router (fleet layer).
- .config/orgs/*.yaml → Runner Matrix Router (org layer).
- .standards.yaml → Runner Matrix Router (repo layer, wins).
- .standards.yaml → Gating Engine (lockfile policy).
- AGENTS.md → Compiler & Transpiler.
- .agents/ → Compiler & Transpiler.
- CLI & Servers → Gating Engine.
- CLI & Servers → Compiler & Transpiler.
- Gating Engine → HISS Invariant Scanner.
- Compiler & Transpiler → Vendor Agent Files.
- Bump Canary → SARIF Distiller (SARIF) → Diagnostic Summary.
- HTTP Health Probes → Distroless Image (packaged in).
- Scenario 1, compile-context: Compiles AGENTS.md and .agents/ to vendor-specific instructions. Reads AGENTS.md. Reads .agents/ personas and skills (agents.go persona projection). Renders 6 vendor files (CLAUDE.md, etc.).
- Scenario 2, gating: Anti-Direct-Merge Gating Pipeline. Runs praetorctl gate run. Reads policy and verifies lockfiles. 6 stages including HISS.
- Scenario 3, runner routing: Resolves runners using hierarchical policy; CI itself runs on GitHub-hosted runners and does not consume this result (ADR-0012). LoadCascadingRunnerConfigContext starts from DefaultRunnerPolicy and merges .config/fleet.yaml onto it. Then .config/orgs/<org>.yaml overrides fleet. Then the runners: key of .standards.yaml overrides fleet and org; the last layer merged wins.
- Scenario 4, SARIF distillation: Condenses SARIF logs into bounded summaries. Canary test wraps failure in SARIF. Distills summary and writes full report.
- The CLI routes commands to internal engines.
- SARIF Distillation condenses failure outputs for the agent.
The HISS scanner returns a ScanReport to the gate and to praetorctl audit; it emits no SARIF.
SARIF enters through lockdown.DistillSARIF, which condenses a SARIF log (today the bump canary's
wrapped test failure, internal/bump/canary.go) into a bounded summary and writes the full report
under .workingdir/evidence/. The gate reads .standards.yaml twice: the prefetch stage requires it
and .standards.lock to exist, and the HISS stage scans with the function-length limit its policy
resolves to. .github/workflows/release-binaries.yml builds the image from the released
praetorctl binaries, pushes it and the Helm chart to GHCR and signs both; a published chart pulls
the image tagged with its appVersion (ADR-0013).
The runner router starts from DefaultRunnerPolicy and merges .config/fleet.yaml,
.config/orgs/<org>.yaml and the runners: key of .standards.yaml in that order, so the
repository layer wins (LoadCascadingRunnerConfigContext in internal/config/hierarchy.go). The
fleet and org files are optional owner-only paths, so in the engine only the defaults and
.standards.yaml apply. The model router reads .config/models/routing.yaml, the file
praetorctl models loads by default.
3. Level 3: Component Diagram (Gating Engine)
The Component diagram details the internal workflow of the Anti-Direct-Merge Gating Pipeline (internal/gating/pipeline.go).
Every stage that can fail has its own edge to the rejected disposition, and its tabs play four
runs: a clean run that signs the receipt, a HISS violation rejected at stage 2, a dry run, and a
missing signing key that fails stage 6 closed.
Text description
- Gate stages: 1. Prefetch & Lockfiles (lockfiles, go mod verify), 2. HISS Invariant Scan (debt-baseline ratchet), 3. Security & SCA Scan (govulncheck, gosec), 4. Flavor Conformance (flavor audit), 5. Race-Detector Tests (go test -race, worktree), 6. Exit-0 Receipt (Ed25519-signed output).
- Boxes: Signing key (env var or key file), Candidate commit (praetorctl gate run), .standards-receipt.json (Exit-0 receipt), Rejected (exit 1, stage named).
- Candidate commit → 1. Prefetch & Lockfiles (clean tree) → 2. HISS Invariant Scan → 3. Security & SCA Scan → 4. Flavor Conformance → 5. Race-Detector Tests → 6. Exit-0 Receipt.
- Signing key → 6. Exit-0 Receipt (LoadSigningKey) → .standards-receipt.json (signed).
- 1. Prefetch & Lockfiles → Rejected (lockfiles).
- 2. HISS Invariant Scan → Rejected (new debt).
- 3. Security & SCA Scan → Rejected (findings).
- 4. Flavor Conformance → Rejected (below bar).
- 5. Race-Detector Tests → Rejected (tests fail).
- 6. Exit-0 Receipt → Rejected (no key).
- Scenario 1, clean run: Every stage passes and the receipt is signed. The tree matches HEAD. Lockfiles found; modules verified. No HISS violation beyond the baseline. govulncheck and gosec pass. Flavor conformance holds. Race-detector tests pass. LoadSigningKey returns the Ed25519 key. The stage output is signed for this commit.
- Scenario 2, HISS violation: A new violation fails stage 2; no later stage runs. The tree matches HEAD. Stage 1 passes. The ratchet finds a violation beyond the baseline (an incomplete scan fails too); the gate exits 1.
- Scenario 3, dry run: gate run --dry-run runs the read-only checks only. The tree may differ from HEAD. Lockfiles checked; module download skipped. The HISS scan runs. Security scan skipped. Flavor conformance runs. Race-detector tests skipped. No receipt is minted.
- Scenario 4, no signing key: Stage 6 fails closed without a key. Stages 1 to 5 pass. No PRAETOR_RECEIPT_KEY and no key file: the stage fails and nothing is signed.
- Only a failed stage stops the pipeline; a stage that ran nothing is skipped or not_applicable.
- Stages 1, 3 and 5 run Go where a go.mod is present and Cargo where a Cargo.lock is.
- (Shortened to 2500 characters; docs/figures/gating-pipeline.ts holds the full content.)
Stage order and names come from executeStages in internal/gating/pipeline.go. The rejection
edges name what fails each stage: a missing or empty .standards.yaml or .standards.lock
(VerifyLockfiles), or a failed go mod verify or go mod download (prefetchDependencies, both
in internal/gating/prefetch.go); new debt beyond .standards-baseline.json or an incomplete scan;
a govulncheck or gosec finding, or a missing scanner or .gosec.json; a flavor audit below
passingScore or with a missing template (internal/flavor/audit.go); a failing go test -race;
and no signing key. Each stage records one StageStatus: passed, failed, skipped or
not_applicable. Only failed stops the pipeline and leads to the rejected disposition; a stage
that ran nothing is never recorded as passed, in the CLI report, in --json, or in the signed
praetor-gate-output/v2 stage output.
A dry run (gate run --dry-run) changes nothing and reaches no network. It verifies the lockfiles
and runs stages 2 and 4. In a Go repository it records the module prefetch in stage 1, stage 3,
stage 5 and the receipt as skipped; without a go.mod, stages 1 and 3 are not_applicable
instead. It mints no receipt (TestExecuteStages_DryRunInvokesNoCommand). gate verify,
forge validate-pr and paperclip verify refuse a receipt whose gate output is not
praetor-gate-output/v2 (lockdown.VerifyPinnedReceiptFile, lockdown.VerifyUnpinnedReceiptFile). The adoption verification guide
covers each verdict. ADR-0012 (decision 3) records the pipeline; ADR-0004's four-stage description
is superseded.