Skip to content

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.

Praetor boundaries, agent interactions, git providers, and downstream infrastructure.
System Context Diagram
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.

Runtime modules within the Praetor binary, daemon services, and configuration inputs.
Container Diagram
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.

Six gate stages run in order; a failing stage rejects the change before an Exit-0 receipt is signed.
Gated pipeline
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.