Skip to content

Architecture

Ospobox is a chassis: it schedules collection, stores evidence, runs scorers, evaluates rules and dispatches signed events, while knowing nothing about what any of those mean. Every domain-shaped decision is a plugin.

The claim is checkable. Delete src/ospobox/extensions/builtin/ and the application still starts, serves and dispatches, with nothing to collect and nothing to score.

The loop

graph LR
    S[Sources] -->|Evidence| E[(Evidence store)]
    S -->|Activity| D[(Relational store)]
    E --> SC[Scorers]
    D --> SC
    SC -->|Breakdown| R[Rule evaluators]
    R -->|Alerts| W[Signed webhook bus]
    W --> A[Subscribers]
    SC --> UI[Dashboard, reports, API]
    R --> UI

Each arrow is a contract, and each box on the left of the bus is replaceable without touching the ones on its right.

The five layers

The vocabulary below comes from the Software Health Control Plane design. It names the seams precisely, and it separates the engineering from the research.

L1. Ingestion

Today. Four forge adapters behind one normalized interface, driven by a scheduler that syncs every six hours or on demand. Beyond forges, an EvidenceSource plugin declares the evidence types it produces and the subject kinds it accepts, and yields evidence for a subject. Two reference sources ship: CycloneDX SBOMs and OSV advisories.

Research

Supply-chain connectors with real provenance: build attestations, signatures, registry metadata, identity assertions. The reference sources here prove the socket, and are meant to be replaced.

L2. Evidence

Today. One append-only, content-addressed table. A record is a subject, an evidence type, a source, a JSON payload, a sha256 of that payload, and a collection time. Re-collecting an unchanged fact is a no-op; a changed fact is a new row, and the history is the sequence of rows.

It is a table, and deliberately not a graph. Relationships live inside payloads: the SBOM ingester lists a repository's components in one record because there is nowhere else to put them.

Research

A signed, machine-verifiable evidence graph: relationships as first-class edges, signature chains over them, verification, and a post-quantum archival tier. The payload-shaped relationship above stops being a workaround there.

L3. Health model

Today. A Scorer takes a subject and its evidence and returns a breakdown. The one rule the chassis imposes: every component carries a one-line explanation, rendered verbatim. Two scorers ship, a five-factor activity composite and dependency exposure.

The composite is deliberately naive, and its weakness is instructive. It weights popularity (stars, forks) and responsiveness (PR and issue latency) at 35% of the total, and the SourceHut adapter can supply none of them. A maximally active sr.ht project is therefore capped at 49.5 out of 100, where the number measures the adapter's coverage.

Research

A seven-dimension explainable model that replaces the composite: components that declare what evidence they need and abstain when it is absent. The explanation requirement above is the chassis's half of that bargain, already in place.

L4. Policy

Today. RuleEvaluator plugins turn a repository's state and score into alerts. Six rules ship. Alerts deduplicate against what is still open, enforced by a partial unique index, so the same finding is filed once until someone acknowledges it.

Planned

Per-account thresholds. Today's are sensible defaults compiled in, and making them configurable is a settings surface over an existing mechanism.

Research

Policy-as-code verdicts (hold, quarantine, patch, roll back) with the reasoning attached. Alerts stay the currency the bus carries.

L5. Actuation

Today. HMAC-SHA256-signed webhooks with a delivery log, exponential-backoff retries, and lease-based claiming so two delivery passes can never send the same event twice. Five event types. Plus reports by email on a cadence, and a REST API.

A subscriber that verifies the signature is the whole integration contract: demo/webhook_receiver.py is thirty lines and forwards verified critical alerts to whatever acts on them.

Planned

Messaging destinations (Slack, Discord, Teams, Matrix, and the rest) as a dispatcher subscribing to the same bus.

Two plugin surfaces

Surface Entry-point group For
Forge adapters ospobox.platforms A code host: fetch organizations, repositories, commits, PRs, issues, releases
Chassis extensions ospobox.extensions Evidence sources, scorers, rule evaluators

Both load third-party packages through setuptools entry points, so adding a plugin changes nothing in this repository. Extension points has the interfaces and worked examples.

Subjects

Everything the system reasons about is a subject: a kind and an opaque identifier.

repo:https://codeberg.org/forgejo/forgejo
purl:pkg:pypi/requests@2.31.0

The chassis does not parse identifiers. A package URL means whatever the plugins that produce and consume it agree it means, which lets the evidence store hold facts about things the core has never heard of.

Data model

Two stores, on purpose.

Relational for things with shape and queries: accounts, organizations, repositories, commits, pull requests, issues, releases, reports, alerts, webhook endpoints and deliveries. Indexed, windowed, joined.

Evidence for facts that have no table: SBOM components, advisories, repository snapshots. Copying the activity tables into JSON would double every write and buy nothing.

Commits carry two timestamps: authored_at for attribution, committed_at for when work landed. Forge since filters apply to the second, and bucketing windows by the first put rebase- and squash-merged work into windows that had already closed.

Operational shape

  • Web and worker are separate processes. The worker runs SAQ against Redis, with each job getting its own database session; cron entries handle sync, scoring and alerts, webhook delivery, report generation and scheduled email.
  • Migrations are the only schema authority. The application does not create tables at boot, and a test fails if the models and the migrations drift.
  • Multi-tenancy runs through the account. Every query filters through the account that owns the organization; there is no cross-tenant read path.
  • Secrets are encrypted at rest: OAuth tokens and webhook signing secrets. A secret that cannot be decrypted fails its delivery loudly, because signing with a value no receiver can verify would be worse.

What this is not, yet

The limits:

  • No billing, and tier quotas are not enforced.
  • No team invitations: an account is effectively single-user.
  • No /api/v1/ prefix; the API is unversioned.
  • No audit log.
  • The evidence store has no relationships, signatures or verification.
  • Scores are not comparable across platforms where an adapter cannot supply an input (see L3).

The roadmap sorts those into what is next and what is research.