Operational configuration
Praetor separates the engine from the operational configuration it acts on. This guide states which side owns what, and how an operator supplies the operational half.
Why the split exists
The engine is public. Organisation names, repository names, runner routing and cluster manifests are operator data: publishing them discloses private repositories and pins the engine to one operator's infrastructure. Before this split the engine shipped a fleet map naming private repositories, and it drifted from reality the moment the fleet changed, because nothing could update it.
The rule that keeps the two apart: the engine reads operational configuration, it never embeds it. When a
capability has no configuration, it reports that state — not configured — instead of falling back to a
built-in default that looks like fact.
What the engine owns
Everything needed to govern a repository without knowing whose repository it is:
- source code and its tests;
- archetypes and facets (
.config/archetypes/), linter and Semgrep policy, supply-chain policy; - the default label taxonomy, rulesets and GitHub App manifest;
- schemas, templates and anonymised fixtures.
GitHub App manifest
.config/github-app/manifest.json describes the praetor-governance-bot GitHub App, and
.config/github-app/permissions.md explains each permission it requests and how to create
the App from the manifest. Nothing provisions, installs or authenticates as that App today:
no workflow or command reads the manifest, and nothing requests that App, or any other bot,
as a reviewer. forge.AssignReviewers (internal/forge/pr.go) requests only the bots its
caller names and never a built-in account (TestAssignReviewers_Negative_NoBuiltInBot in
internal/forge/forge_test.go); the operator key for a review bot, forge.review_bot, is
empty by default and no command passes it yet (see
framework, forge and topology).
make github-app-test fails when the permission matrix and the manifest's
default_permissions disagree.
Commit identity of automated pushes
Automated commits default to the github-actions[bot] identity. Set a different one per repository or
organisation, without editing a workflow:
| Surface | Name | Unset | |
|---|---|---|---|
.github/workflows/adopt.yml |
Actions variable PRAETOR_BOT_NAME |
Actions variable PRAETOR_BOT_EMAIL |
github-actions[bot] and its noreply address |
scripts/sync_github_wiki.sh |
environment WIKI_GIT_NAME |
environment WIKI_GIT_EMAIL |
the same |
scripts/test_sync_github_wiki.py covers the default and the override.
What the operator owns
Supplied locally or from the operational fork, and ignored by this repository:
| Path | Holds |
|---|---|
.config/fleet-topology.yaml |
organisations governed and archetype membership, reported by harvest fleet |
.config/fleet.yaml |
fleet-wide runner defaults (tier 1 of the runner matrix) |
.config/orgs/<org>.yaml |
organisation runner overrides (tier 2) |
.config/operator/ |
operator settings the fork carries, such as fleet-wide and per-workstation settings files; explicitly selected by hook, client and workstation commands |
.config/operator/funding.yaml |
funding accounts rendered into .github/FUNDING.yml, the README badge and support blocks, the MkDocs announcement and social links, and the account blocks of docs/sponsoring.md and docs/monetization.md by praetorctl docs funding |
deploy/arc/ |
Actions Runner Controller scale sets for the operator's cluster |
deploy/k8s/ |
GitOps application and kustomization targeting the operator's cluster |
deploy/helm/ stays in the engine: the chart packages the product itself, not an operator's cluster.
Supplying it
Place the files at the paths above in your checkout, or have the operational fork carry them. They are listed
in .gitignore, so a local copy cannot be committed to the public engine by accident.
Carrying it in the operational fork
Because the engine ignores these paths, the fork tracks them with git add -f. Operational sync accepts
operator files only under the paths in the table above, only as regular non-executable files of at most
1 MiB, at most 256 of them, and only when the public source has no file at the same path; anything else
outside the four identity overlay files and the rendered funding surfaces still stops plan and prepare. The accepted files are reported as
owner_only_paths. The path list is engine schema, and an engine test proves that .gitignore ignores every
entry, so a prefix cannot be added that the public source could also track. See owner-only operator paths.
A new fork gets its identity overlay from praetorctl operational sync init, not from a hand edit; see
apply the first overlay. Review every operator file for
secrets before committing it: the fork is private, but credentials belong in a secret store, not in Git.
Fleet topology example
orgs:
- exampleOrg
- partnerOrg
archetypes:
framework:
- example-framework
library-client:
- example-client
- second-client
harvest fleet prints the configured topology. Without the file it reports not configured and names the
path it expected, rather than printing a fleet of its own.
Runner routing example
runners:
default: "example-runner-set-linux-amd64"
routing:
darwin/arm64:
type: "github-hosted"
runs_on:
- "macos-26"
ephemeral: true
Fleet defaults merge first, then the organisation file for the organisation being resolved.
Each tier starts from the routes before it: the built-in table (DefaultRunnerPolicy,
internal/config/hierarchy.go), then the fleet file, the organisation file and .standards.yaml. A
platform a tier leaves out keeps its inherited route. To drop a route, set the platform to null
(null, ~ or an empty value):
A later tier can add the platform back. Removing a platform that has no route changes nothing. A target
whose route was removed falls back to default, and praetorctl audit fails when that fallback breaks
the platform constraint, for example a removed darwin/* route. The merge is pinned by the
TestCascadingRunnerConfig_* cases in internal/config/hierarchy_integrity_test.go, the darwin fallback
refusal by internal/runner/constraint_test.go.
Funding example
Funding accounts are operator data. The engine ships no funding accounts: its own
.github/FUNDING.yml, README blocks, MkDocs blocks and the account blocks of the
sponsoring and monetization pages are the unconfigured rendering.
TestRepositoryFundingSurfacesMatchTheOperatorConfiguration in
internal/funding/funding_test.go fails if a configured rendering is committed to the
engine. In an operational fork, whose .standards.yaml records repository.source, the
same test requires the rendering of the fork's own document instead.
github: [exampleOrg] # GitHub Sponsors, at most four accounts
polar: exampleOrg # also renders the MkDocs bounty announcement
ko_fi: example
open_collective: example-collective
custom: ["https://example.org/donate"] # FUNDING.yml only, at most four https URLs
message: "Optional sentence that replaces the default README support prose."
praetorctl docs funding [path] renders the document. It rewrites .github/FUNDING.yml
and the lines between these markers (internal/funding/render.go):
| Marker | File |
|---|---|
praetor:funding-badges, praetor:funding-support |
README.md |
praetor:funding-announcement, praetor:funding-social |
mkdocs.yml |
praetor:funding-bounties |
docs/sponsoring.md |
praetor:funding-channels |
docs/monetization.md |
A file without the markers is skipped, not appended to. --check reports drift without
writing and exits non-zero when a surface differs; --config names another document.
Without the document, and for a document whose lists are empty, the command reports
not configured: the README and MkDocs blocks render empty and the two documentation
blocks say that no account is linked, so no link to an unconfigured account is published.
Unknown keys, malformed account names and non-https custom URLs are errors
(internal/funding/config.go). A file checked out with CRLF endings, as Windows checks out
README.md, is compared and rewritten with CRLF; a file with mixed endings is an error.
The fork carries the document at .config/operator/funding.yaml, tracked with
git add -f. Operational sync renders the funding surfaces from it on every prepare,
and plan accepts a fork that committed the rendering or has not rendered yet; see
funding surfaces.
Related decisions
- The boundary and its class list: decision Q-031 in the private questions ledger.
- The engine and operational fork topology, and the runner tiers this configuration feeds:
ADR-0012, decisions 2 and 5.
It supersedes ADR-0003 and ADR-0006. Decision 5 restates the Darwin defaults as
macos-26andmacos-26-intel. ADR-0006's frozen Tier 4 text still namesmacos-14, which carries a deprecated badge in actions/runner-images, andmacos-13, which is no longer published (internal/config/hierarchy.gocites the evidence). The tiering itself is unchanged.