Ting's Forgejo steward: rules, answers, and an audit trail.
  • Rust 75.7%
  • Nix 23.5%
  • Shell 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-20 05:48:31 +02:00
fixtures/lifecycle/v1 lifecycle: prove replay, restart, duplicate, and quarantine behaviour (#108) 2026-09-05 23:17:28 +02:00
nix fix: declare ratified Status/* label vocabulary (audit G23, comment 6802) 2026-09-20 05:48:31 +02:00
openspec openspec: join the shared contracts store 2026-09-04 20:45:31 +02:00
policy policy: no ticket delivered no merge, and the author cannot approve their own work 2026-08-11 22:18:43 +02:00
src lifecycle: three ways a report could misstate its own evidence 2026-09-06 08:37:29 +02:00
.gitignore doctor: declare nu and gum in the flake, and say how to bootstrap 2026-08-27 12:23:37 +02:00
.rumdl.toml style(markdown): format README for rumdl 2026-09-03 19:28:40 +02:00
AGENTS.md doctor: move into nix/ting, pin nushell, and retire the fastlane seam 2026-09-04 22:21:56 +02:00
Cargo.lock conformance and the record: labels declared, roster checked, the unheard audible, the audit durable (#35) 2026-08-11 20:26:42 +00:00
Cargo.toml conformance and the record: labels declared, roster checked, the unheard audible, the audit durable (#35) 2026-08-11 20:26:42 +00:00
CONTRACT.md doctor: move into nix/ting, pin nushell, and retire the fastlane seam 2026-09-04 22:21:56 +02:00
devenv.lock doctor: move into nix/ting, pin nushell, and retire the fastlane seam 2026-09-04 22:21:56 +02:00
devenv.nix ting: just in the base surface; opt-in rust.nix and ops.nix 2026-09-13 16:11:38 +02:00
devenv.yaml doctor: move into nix/ting, pin nushell, and retire the fastlane seam 2026-09-04 22:21:56 +02:00
flake.lock doctor: move into nix/ting, pin nushell, and retire the fastlane seam 2026-09-04 22:21:56 +02:00
flake.nix doctor: move into nix/ting, pin nushell, and retire the fastlane seam 2026-09-04 22:21:56 +02:00
README.md readme: the lifecycle audit surface, and what it will never do 2026-09-05 23:42:44 +02:00
resolve-new.rs policy: declare the label the rules read, and answer on one comment 2026-08-11 08:47:25 +02:00

Jostoph

Ting's Forgejo steward. Jostoph hears forge events, applies declared law, records the ruling, and performs only effects already named by that ruling.

It does not deliberate. Policy is Datalog, parameters are Nix, and every answer leaves evidence.

Built on jostoph-rs, consumed through its public midgard mirror. The substrate supplies service lifecycle, deduction, dispatch, and resolution. This repository supplies Ting's policy, Forgejo integration, and deployment.

The steward's circuit

Step Responsibility Home
Hear Verify a Forgejo delivery and extract facts src/forgejo.rs
Rule Apply Ting's Datalog policy src/policy.rs
Remember Append the verified delivery record src/record.rs
Answer Resolve conclusions and retain the audit trail src/audit.rs, jostoph-rs
Act Reconcile only a declared effect src/reconcile.rs, src/conformance.rs

The answering service never acts. It can declare an effect such as a comment or merge, return it, and audit it. One-shot executors carry separate credentials and perform only the effect they were given.

Today, jostoph conform --apply can reconcile the declared repository and organisation-label state. Webhook provisioning belongs to the Forgejo nixops4 provider. Merge, comment, label-transition, and base-push effects remain declared but unperformed; their executor is tracked in #26.

Perimeter

In scope

  • The Jostoph CLI and answering service.
  • Ting's rules and their Nix-crystallized parameters.
  • Forgejo webhook verification, delivery records, and audit trail.
  • Report-first conformance of repository settings and organization labels.
  • Nix packaging, module, and service wiring.

Out of scope

  • General service substrate: that belongs to lar.ad/jostoph-rs.
  • Agentic judgment or an LLM in the steward loop.
  • Repository creation, deletion, or undeclared Forgejo mutation.
  • Performing ruling effects until their executor lands.

Policy

Rules live in src/policy.rs. Their parameters live in nix/policy.nix, which renders the policy baked into the binary. policy/ting.json is the checked-in development value; nix flake check fails when it drifts from the rendered policy.

Current gates require:

  • Open issues and pull requests declare In scope, Out of scope, Boundary, and Acceptance.
  • Direct pushes to refs/heads/main are denied.
  • Unrecognized actors are gated.
  • A pull request carries one or more standalone Closes #n lines, or a Closes nothing: reason.
  • An author's own approval does not count.
  • An approval names the reviewed head_commit_id; drafts cannot merge.

Standing flows through groups rooted at steward_root; it is not a flat login list. nix/policy.nix declares group membership and nesting, while the Datalog relation closes the hierarchy transitively.

CLI

jostoph manifest
jostoph --policy candidate.json check event.json
jostoph check event.json
jostoph check --webhook --kind issues body.json
jostoph serve --listen 127.0.0.1:8420 \
  --webhook-secret-file /run/secrets/hook \
  --audit-log /var/lib/jostoph/audit.jsonl \
  --record-log /var/lib/jostoph/records.jsonl
jostoph roster --api https://jo.et0.pw/api/v1 \
  --token-file /run/secrets/ro-token --repo Ting/Jostoph
jostoph kinds --audit-log /var/lib/jostoph/audit.jsonl
jostoph replay --record-log /var/lib/jostoph/records.jsonl
jostoph conform --spec /etc/jostoph/forgejo-repos.json \
  --api https://jo.et0.pw/api/v1 --token-file /run/secrets/token
jostoph conform --apply --spec /etc/jostoph/forgejo-repos.json \
  --api https://jo.et0.pw/api/v1 --token-file /run/secrets/token

check exits 1 on denial. roster, kinds, and report-mode conform exit non-zero on drift. conform --apply performs exactly the reported edits and observes the forge again; the live forge, not the request log, is proof of convergence.

--policy FILE swaps the compiled-in parameters for a trial before a policy change is crystallized into the Nix derivation.

replay rebuilds the fact view from verified delivery records. Replaying the same records with the same binary and parameters produces the same view. Duplicate delivery_id values append nothing.

The lifecycle audit

Jostoph also hears the Forge identity lifecycle (#103): the declaring authority, Kanidm, OpenBao, Vedanta, the Forge credential adapter, and Forgejo each report their own outcome in one non-secret envelope (src/lifecycle/envelope.rs; the same contract as fixtures and a JSON Schema under fixtures/lifecycle/v1/). Jostoph keeps each layer's word verbatim, joins the layers by stable identifiers, and reports what is missing, contradictory, or drifted. It never mints, rotates, revokes, invalidates, activates a grant, or alters membership: no module under src/lifecycle/ holds a client with which to do so.

jostoph serve ... \
  --lifecycle-registry /etc/jostoph/lifecycle-emitters.json \
  --lifecycle-chain /var/lib/jostoph/lifecycle.jsonl \
  --lifecycle-quarantine /var/lib/jostoph/lifecycle-quarantine.jsonl
jostoph lifecycle ingest --registry R --chain C --quarantine Q events.jsonl
jostoph lifecycle view --chain C --quarantine Q
jostoph lifecycle drift --chain C
jostoph lifecycle acceptance --chain C --quarantine Q --attestations A.json

With the registry, serve also answers POST /v1/lifecycle/events. Every document — delivered or read from a file — passes the same gauntlet: secret-shaped paths are quarantined by path before the parse; the closed envelope is validated; the claimed component must be a declared emitter whose key verifies the document's own signature; the event type must be one that layer owns and the registry grants; then the append. A replay of a stable event id appends nothing and says so; the same id with different content is a conflict the original outlives.

view is grant -> lease -> operation -> token -> resource event, every layer under its own name, missing layers named, contradictions carried with the ids they came from. drift reports C01C08 and revocation_pending, each naming the affected layer and the owning compensation path. acceptance renders the canonical production acceptance facts and old-path exit conditions as pass, fail, pending, missing (naming the source), contradictory, or attested — never as pass without the event ids that earned it. All three exit non-zero while a finding stands, and none contains a credential value.

Deployment

{
  inputs.jostoph.url = "git+https://jo.et0.pw/Ting/Jostoph.git";

  imports = [ inputs.jostoph.nixosModules.default ];
  nixpkgs.overlays = [ inputs.jostoph.overlays.default ];

  services.jostoph = {
    enable = true;
    listen = "127.0.0.1:8420";
    webhookSecretFile = config.sops.secrets.jostoph-webhook.path;

    forgejo = {
      url = "http://127.0.0.1:8420/webhook/forgejo";
      repositories = [ "Ting/Jostoph" ];
    };
  };
}

This deployment currently belongs on the Forgejo host. The instance permits webhook delivery to loopback, not arbitrary private or external addresses. The listener therefore stays on 127.0.0.1; moving it requires an explicit Forgejo-side policy change.

Deliveries must carry a valid X-Forgejo-Signature. There is no unsigned mode. The webhook secret is minted and rotated by the deployment and reaches services through systemd credentials.

The same listener serves /healthz, /v1/manifest, /v1/evaluate, /v1/audit, and POST /webhook/forgejo. Keep it on loopback unless a reverse proxy deliberately exposes only the intended paths; otherwise the audit and evaluation surfaces would travel with the webhook.

services.jostoph.forgejo declares the expected webhook; it does not provision it. Forge-side resources with assigned identities remain the Forgejo nixops4 provider's responsibility. Repository and organization-label desired state is rendered by:

nix build .#forgejo-repos

Conformance is add-and-repair, never delete: an absent declared repository and an undeclared live resource are findings for a person.

Credentials

The build needs no Forgejo credential. jostoph-rs is pinned through the anonymous public mirror, and Cargo consumes the flake-populated vendor/jostoph path offline.

A credential is needed only to work directly against the private canonical substrate:

install -Dm600 /dev/null ~/.config/nix/netrc
printf 'machine jo.et0.pw login <user> password <forgejo-token>\n' >> ~/.config/nix/netrc

For a local checkout, --override-input (below) needs no credential at all.

The doctor

The estate's checkup reaches a repository as a devenv module, not as a file to fetch. There is no bootstrap step and nothing to keep in sync by hand: a repo declares Jostoph as an input, imports jostoph/nix/ting, and the whole ting:* task surface — plus openspec, nu and gum at the revisions Jostoph locks — arrives with it.

# devenv.yaml
inputs:
  jostoph:
    url: git+https://jo.et0.pw/Ting/Jostoph
    flake: false

# devenv.nix
imports:
  - jostoph/nix/ting

devenv update is the sync. Which doctor a repo ran is then a fact recorded in its devenv.lock, readable afterwards, rather than a race against whatever was on main that minute — and a local experiment is an ordinary input override instead of an edit the next run silently overwrites.

The checks run before the shell is entered, at most hourly and whenever either lockfile moves. Most report; a few heal. What the doctor will not do is edit a tracked file to impose a choice that is yours — governance in AGENTS.md, a default branch on the forge, a devenv input pointed at a public mirror are all named and left to you.

This repository imports ./nix/ting from its own working tree rather than a published copy of itself, so an edit to the doctor takes effect in the shell that is editing it.

Development

direnv allow         # mandatory once per checkout; loads the default flake shell
cargo test
cargo clippy --all-targets -- -D warnings
nix flake check      # build + tests + module eval + policy-drift check

vendor/jostoph is a path dependency populated from the jostoph-rs flake input rather than a Cargo git dependency, so cargo build --locked --offline works inside the Nix sandbox with no forge credentials.

To work against a local substrate checkout:

nix flake check --override-input jostoph-rs path:../jostoph-rs

The implementation campaigns remain on the forge: effect execution #81 and trusted delivery #83. Reusable failures and the rules they changed live in the incident postmortems.