Skip to content

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 Email 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):

runners:
  routing:
    linux/gpu: null

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.

  • 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-26 and macos-26-intel. ADR-0006's frozen Tier 4 text still names macos-14, which carries a deprecated badge in actions/runner-images, and macos-13, which is no longer published (internal/config/hierarchy.go cites the evidence). The tiering itself is unchanged.