Skip to content

feat: 1.0-readiness findings, shipped as 0.10.0 - #198

Merged
krakennetworks merged 12 commits into
mainfrom
feat/1.0-readiness-as-0.10
Aug 20, 2026
Merged

feat: 1.0-readiness findings, shipped as 0.10.0#198
krakennetworks merged 12 commits into
mainfrom
feat/1.0-readiness-as-0.10

Conversation

@se-jo-ma

Copy link
Copy Markdown
Member

Closes the 1.0-readiness findings that were deferred out of #195. Ships as 0.10.0, not 1.0.0 — 1.0 waits on the readiness audit.

Merge order

Merge #197 first and let release-please cut 0.9.0 from the batch already on main. This branch then produces 0.10.0. It carries the same release-please-config.json change as #197 (identical content, so the merge is clean).

What is in here

A rule pack that declared modules and fired nothing (f290e27)
A pack with modules/ but no focus_order compiled fine, focused nothing, drained no agenda, and returned the default decision — silently. load_modules() now falls back to declaration order, and the benchmark refuses to report evaluation timings for a pack whose rule_trace is empty. The perf gate had been measuring an empty agenda: its generated pack was exactly this shape.

The doc-source drift gate blocks the branch that caused the drift (3d6702e)
check_doc_sources.py ran only in the advisory docs-quality job, so #195 landed source changes without re-verifying eight pages and the failure surfaced on an unrelated PR. The required docs job now runs it scoped to the sources this branch touched (--changed-vs $BASE_SHA); the whole-repo sweep stays advisory, so a stale page still fails its own author rather than the next one.

A declared public surface, and a policy behind it (70a0e0f)
__all__ claimed 8 symbols while the docs taught imports from four other modules. It is now 26, resolving the attestation exports lazily (PEP 562) so import fathom still works without the attestation extra. New VERSIONING.md names the covered surfaces and what a 0.x minor and patch each mean; tests/test_public_surface.py (35 tests) holds the symbol block in that file to the package. New docs/reference/configuration.md documents all 13 FATHOM_* variables, the gRPC TLS requirement, and the token scopes.

SECURITY.md (58bdd24)
Was three lines promising support for 0.3.x. Now: support window, private GHSA reporting, response-time table, coordinated disclosure with a 90-day backstop, and explicit in/out-of-scope lists (test:, type: raw, the two dev escapes, and packages/fathom-editor are out).

One evaluate response across every transport (6d8f611)
/v1/evaluate dropped the firing rule's then.metadata, and never populated attestation_token — the REST app held an attestation service on app.state but built its engines without it, so the field was always null. gRPC carried neither field and could not distinguish "no rule decided" from a decision of "". Adds metadata to the REST model, passes the service into stateless and session engines, marks proto decision/reason optional, and adds metadata + attestation_token to the proto. The Go and TypeScript clients carry metadata; the Go gRPC client also reads back the token it used to discard.

Audit lines that carry their signature (36c8ef3)
docs/concepts/audit-attestation.md claimed an exported audit line could not be modified without breaking verify_token, while the same page said two sections earlier that the JWT was deliberately kept off AuditRecord. AuditRecord now carries attestation_token, and ChainedAttestationLog gained write() so it satisfies AuditSinkEngine(audit_sink=ChainedAttestationLog(path, service)) writes evaluations into a hash-chained log where a deleted entry breaks linkage. The threat-model section now separates what the token covers from what it does not.

Policy Studio, documented; README, corrected (67e0159)
The README listed one framework adapter where four ship, four rule packs where five do, fathom-editor among the SDKs, and python -m fathom.studio.app — an entry point that stopped existing when the Studio moved to its own package. New how-to: Running Policy Studio — how to run it from a checkout (it is not on PyPI), its token gate, its five views, and the four things it is not.

Verification

  • uv run pytest — 2186 passed, 4 skipped (the 2 skips are protoc-gated gRPC doc tests)
  • ruff check / ruff format --check / mypy src/ — clean
  • go vet ./... && go test ./..., pnpm lint && pnpm test — clean
  • mkdocs build --strict — clean; all generated reference regenerated (OpenAPI, Postman, gRPC, Go, TS, Python SDK, llms*.txt)
  • check_doc_sources.py --changed-vs origin/main — clean

Still needs your hands

  • pypi / npm environment protection rules (GitHub settings)
  • NPM_TOKEN (npm-publish has never succeeded)
  • adding CodeQL to main's required contexts

One Dependabot alert stays open and is not fixable here: chromadb (critical, transitive through the optional crewai extra, in uv.lock only). The advisory covers <= 1.5.9 and 1.5.9 is the newest release — there is no patched version to move to, and nothing in Fathom imports it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ro3WQQ1Dk76zrDmTEqygRA

The `focus_order` fix in #195 carried a `BREAKING CHANGE:` footer, and
release-please's default for a 0.x project is to treat that as the signal
to cut 1.0.0 -- so PR #196 opened as "chore(main): release 1.0.0". Nothing
about that branch was a decision to declare the API stable.

`bump-minor-pre-major` makes a breaking change bump the minor while the
project is on 0.x, which is what SemVer allows pre-1.0 and what
VERSIONING.md now documents. `bump-patch-for-minor-pre-major` stays off:
a feature should still move the minor, not hide in a patch.

1.0.0 becomes a deliberate act -- a commit that raises the version
explicitly, after the readiness audit -- rather than a side effect of a
commit footer.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
A pack that declared modules but no `focus_order:` fired nothing at all.
CLIPS drains only the agenda of the module holding the focus, so with an
empty focus list every rule scoped to a declared module sat unfired and
the caller got the default decision back — a wrong answer, not an error,
from a pack whose rules all matched.

`load_modules` now focuses the declared modules in declaration order when
nothing else has set a focus, and only then: a `focus_order:` in any file
or an earlier `set_focus` still wins outright.

This invalidated the benchmark added in #195, which generated exactly
such a pack: its two evaluation cases were timing an empty agenda and
reporting healthy microseconds for doing nothing. The generated pack now
declares its focus, and the script refuses to report timings for a pack
whose rule_trace comes back empty, so the same mistake cannot pass
silently again. The real numbers are inside their published targets
(single 71us, 100-rule 156us).

Docs: this behaviour is now stated in the module reference, the runtime
concept page, and the modules tutorial. The rest of the pages here are
the source-drift backlog from #195, which the docs job only warns about:
- writing-rules.md taught YAML that no longer compiles — bare
  `expression: active` (must be `equals(active)`) and `alias: req`
  (must be `$req`), plus an emission claim the compiler never made.
- template.md said no validator was applied to SlotDefinition.name,
  allowed_values, or default; all three are validated now.
- audit-attestation.md was missing the input-fact snapshot step and had
  a stale `sign()` signature.
- yaml-compilation.md said module `priority` drives focus ordering.
- function.md's validator table was missing the hierarchy validators and
  the unknown-key rejection; cli.md did not mention that `compile` now
  emits literals by declared slot type; hot-reload.md told readers to
  use a reload listener without naming `Engine.subscribe_reload`.
- fact.md and five-primitives.md carried line citations that had moved.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
The doc-source freshness gate was advisory in every job that ran it, and
the result is the previous commit: eight reference pages describing
validators, YAML forms, and a call signature that had not existed since
#195, one of them teaching rule YAML that no longer compiles. A warning
nobody reads is not a gate.

It was advisory for a real reason, though: editing a source ages every
page that cites it, so a whole-repo gate fails whoever opens the next
pull request rather than the author who changed the source. That is the
treadmill that got it switched off, and turning it back on unchanged
would just rebuild it.

`--changed-vs REF` narrows the check to the sources the branch itself
touched. Pages citing files this branch did not edit are someone else's
drift and stay in the advisory sweep; pages citing files it did edit must
be re-verified here. An unreadable ref falls back to checking everything
rather than passing, since a gate that no-ops on a bad ref is worse than
a noisy one.

The required `docs` job now runs the scoped check on pull requests, and
CONTRIBUTING documents what a contributor has to do when it fires.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
Fathom shipped eight releases without saying what it promised. `__all__`
existed on the top-level package and nowhere else, so every other module's
contents were public by accident of import: a caller doing `from
fathom.chained_log import _scan` had no way to learn that was internal, and
this project had no way to change it without maybe breaking someone.

What is covered is now stated in two places that cannot disagree.
VERSIONING.md names the surfaces — the Python symbols, the YAML authoring
keys, the REST/gRPC/MCP contracts, the CLI, the environment variables — and
tests/test_public_surface.py fails when its symbol list and `fathom.__all__`
drift apart, when a module VERSIONING names declares no `__all__`, or when a
top-level export is not public in the module that defines it.

`__all__` added to engine, models, errors, audit, fleet, attestation, and
chained_log. The top-level package re-exports the audit sinks, the fleet
types, the full exception hierarchy (`FathomError` was not exported at all,
so `except FathomError` needed a submodule import), and the attestation and
chained-log entry points. Those last four resolve through a module
`__getattr__` rather than at import: they need `jwt` and `cryptography` from
the optional `attestation` extra, and importing them eagerly would make
`import fathom` fail on every install that did not ask for signing. Accessing
one without the extra raises ImportError naming the extra.

The generated Python SDK reference follows `__all__`, so this adds fourteen
pages; the docs jobs now install the attestation extra so mkdocstrings can
resolve the lazy four.

VERSIONING.md also states what the version numbers mean, which nothing did:
breaking changes go in a `0.x` minor and never in a patch, deprecations run
two minors before removal, and the supported version is the latest minor.

The PyPI classifier moves from Alpha to Beta. Not to Production/Stable: that
is a claim about a 1.0 readiness audit that has not happened.

Also adds docs/reference/configuration.md, which VERSIONING has to be able to
point at for "documented FATHOM_* variables". All thirteen are listed with
their defaults, and the gRPC TLS story is written down for the first time —
the server refuses to start without a key pair unless
FATHOM_GRPC_ALLOW_INSECURE=1, which was true in the code and stated nowhere.
Both surfaces' token scopes are documented alongside it.

The hot-reload and release-signing how-tos were reachable only by direct
link; they are in the nav and the how-to index now.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
The old file named 0.7.x as the supported version (0.8.0 had shipped),
offered no disclosure policy, no embargo, no CVE process, and no statement
of what counts as a vulnerability in a project whose documented features
include two deliberate escape hatches for running arbitrary CLIPS.

Now: the support window points at VERSIONING.md instead of a version table
that goes stale on every release; the timeline covers acknowledgement,
assessment, and fix; disclosure is coordinated through GitHub's CNA with a
90-day backstop so a stalled report is not a silent one; and scope is
explicit in both directions.

The out-of-scope list matters most. `test:` conditional elements and
`type: raw` functions emit author-written CLIPS verbatim by design, and
`FATHOM_GRPC_ALLOW_INSECURE` and the unsigned-ruleset dev escape each require
an explicit opt-in and log what they turned off. Without saying so, every one
of those reads as a finding to anyone auditing the code.

Also corrects VERSIONING's description of the Studio: it mounts the REST app
at `/api`, not `/studio/api`.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
`/v1/evaluate` dropped the firing rule's `then.metadata` on the floor, and
never populated `attestation_token` at all: the REST app held an attestation
service on `app.state` but built its engines without it, so the field existed
in the schema and was always null. The gRPC response carried neither field,
and had no way to distinguish "no rule decided" from a decision of "" —
proto3 sends an unset string as "" to every client.

- `EvaluateResponse` (REST) gains `metadata`; `rest.evaluate` now passes the
  `app.state` attestation service into both the stateless engine and the
  session engines, so a configured service actually signs what it returns.
- `SessionStore.get_or_create` takes the service and applies it at creation.
- `fathom.proto` gains `metadata` and `attestation_token`, and marks
  `decision`/`reason` `optional` so unset is distinguishable from empty.
- The Go and TypeScript clients carry `metadata`; the Go gRPC client also
  reads back `attestation_token`, which it previously discarded.

Regenerates the Python and Go stubs, the OpenAPI spec, the Postman
collection, and the gRPC/Go/TS reference docs.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
`docs/concepts/audit-attestation.md` claimed an exported audit line could
not be modified without breaking `verify_token`, while the same page said
two sections earlier that the JWT was deliberately kept off `AuditRecord`.
The second statement was the true one, so the threat-model claim was empty:
a line lifted out of the log carried no signature at all.

- `AuditRecord` gains `attestation_token`, copied from the result the same
  evaluation produced, so a line is verifiable on its own.
- `ChainedAttestationLog` gains `write()`, which makes it satisfy
  `AuditSink`: `Engine(audit_sink=ChainedAttestationLog(path, service))`
  now writes evaluations into a hash-chained log, where a deleted or
  reordered entry breaks linkage. A per-line signature cannot detect that.
- The threat-model section now separates what the token covers (decision,
  rule_trace, session, iat, input_hash) from what it does not (reason,
  metadata, duration, the fact lists) and points at `verify-chain` for
  continuity.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
The README described integrations the repo had outgrown in both directions.
It listed one framework adapter where four are shipped, four rule packs where
five are, and pointed at `python -m fathom.studio.app`, an entry point that
stopped existing when the Studio moved into its own package. It also listed
`fathom-editor` — a stub with no tests and no backend wiring — beside the Go
and TypeScript clients as an SDK.

- New how-to: Running Policy Studio. It is the only shipped surface with no
  page of its own: how to run it from a checkout (it is not on PyPI), how its
  token gate works, what its five views drive, and the four things it is not
  — a sandbox, a stable API, a durable audit log, or safe to expose.
- README: adapters, packs, Studio, and SDK entries now match the tree; the
  test count is gone rather than left to rot.
- planned-integrations: the TypeScript suite is required in CI now, the Go
  SDK does implement the gRPC surface, and the Studio gets an entry saying
  what is missing from it (no release, no stability promise, in-memory audit).
- docs/index.md: links the pages added since it was last touched.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
@se-jo-ma
se-jo-ma force-pushed the feat/1.0-readiness-as-0.10 branch from 67e0159 to 17bdae4 Compare August 20, 2026 13:30

import pytest

import fathom
`verify-grpc` diffs the committed stubs against a fresh generation, and the
generators write the protoc version into each file's header — so stubs built
with a locally-installed protoc 5.27.3 fail the drift check against CI's
pinned 25.3 even though the descriptors are identical. Regenerated with 25.3,
and the Makefile now names the three pinned versions instead of leaving them
to be discovered from a red build.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
The DCO app checks every commit on a pull request, including the docs
regeneration commit release-please-docs.yml pushes onto the release
branch. That commit had no Signed-off-by, so every release PR came up
red and needed a hand-written third-party remediation commit. Committing
with -s as release-bot signs it off as its own author.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
The release PR's version-bump commit is authored by the RELEASE_PLEASE_TOKEN
owner and carried no Signed-off-by, so the DCO app flagged it on every
release PR. release-please's "signoff" config option adds the line; it
names the token owner so it matches the commit author the app checks
against.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
The bench gate ran at --slack 1.5 on the claim that a shared runner is
'roughly that much slower'. Five consecutive runs of the job on this
branch put the single-rule median at 105, 118, 130, 166 and 166us against
89us on the developer machine the published targets come from: 1.2x to
1.9x, and the two slowest walked through the 150us limit. That is the
gate failing on the hardware, not on the code.

Raises the factor to 2.0 and replaces the guess with the measurement in
all three places that state it. The gate still catches a real
regression: 2x on a developer machine lands at 4x of the published
target on a runner.

Signed-off-by: Sean Mauk <seanmauk@krakennetworks.com>
@krakennetworks
krakennetworks merged commit 79faa66 into main Aug 20, 2026
31 checks passed
@krakennetworks
krakennetworks deleted the feat/1.0-readiness-as-0.10 branch August 20, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants