Releases: Perseus-Computing-LLC/ledger
Release list
v1.1.1
Canonicalize the public Ledger container image namespace as ghcr.io/perseus-computing-llc/ledger. The plutus-agent package and plutus CLI remain supported compatibility surfaces.
What's Changed
- fix: map savings fields in remote-mode track() result (#143) by @tcconnally in #144
- fix: close metering and legacy monitor issue set by @tcconnally in #146
- feat: finish Team billing, price catalog, and metering backtest by @tcconnally in #147
- test: add end-to-end metering acceptance harness by @tcconnally in #152
- feat: add coverage-aware operational savings windows by @tcconnally in #153
- [P1] Productize API-key rotation, ingest diagnostics, and tenant-safe operations (#150) by @tcconnally in #154
- [#151] Make savings-share billing commercially defensible by @tcconnally in #155
- fix(#151): deterministic correction hash, CLI config thresholds, report display by @tcconnally in #156
- feat: define cloud pricing and audit contract by @tcconnally in #157
- feat: expose universal audit receipt and free donation recommendation by @tcconnally in #158
- test: add configurable public launch smoke gate by @tcconnally in #161
- fix: center Free dashboard on savings and optional support by @tcconnally in #162
- fix(#159): surface the 5% verified-savings donation on public pricing by @tcconnally in #165
- docs(#164): define evidence-gated Team (>10 seats) beta plan by @tcconnally in #166
- fix: show visible Stripe checkout handoff by @tcconnally in #167
- fix: derive Stripe return URLs from public origin by @tcconnally in #168
- fix: prevent donations from creating credit by @tcconnally in #169
- fix(#170): flag tier-capped workspace folds in /v1/usage responses by @tcconnally in #172
- fix(#171): default PLUTUS_STATE_DB to $HERMES_HOME/state.db by @tcconnally in #173
- feat(ui): redesign dashboard as an operations monitor by @tcconnally in #174
- feat(#175): gate Pro/Team subscription checkout behind subscriptions_enabled by @tcconnally in #176
- feat(#177): add plutus reconcile-webhooks — detect + replay dropped Stripe events by @tcconnally in #178
- feat: position Plutus as Perseus Ledger by @tcconnally in #179
- feat: add hash-chained evidence receipts by @tcconnally in #180
- feat: hash-chain decision evidence context by @tcconnally in #181
- feat: attach Hermes evidence to Ledger sync by @tcconnally in #182
- feat: record authorized action receipt provenance by @tcconnally in #184
- fix(config): prevent runtime secrets mutating defaults by @tcconnally in #185
- test: configure deterministic pytest discovery by @tcconnally in #186
- feat(receipts): bind context render evidence hashes by @tcconnally in #188
- feat(receipts): org-scoped Bearer API-key access to /api/audit by @tcconnally in #189
- docs: establish runtime-neutral Perseus Ledger brand architecture by @tcconnally in #190
- docs: make Ledger public contracts canonical by @tcconnally in #191
- docs: rebrand launch and Claude Code integration by @tcconnally in #192
- docs: make cloud and settlement guidance Ledger-first by @tcconnally in #193
- feat: project resource-bound receipt evidence by @tcconnally in #194
- test: fix time-sensitive savings fixtures by @tcconnally in #195
- feat: publish Ledger container under canonical name by @tcconnally in #196
Full Changelog: v1.1.0...v1.1.1
Plutus v1.0.1
[1.0.1] — 2026-07-05
Security-hardening release. A four-part follow-up to the internal pre-1.0
review, from a fresh 2026-07-05 audit (money/ledger, auth/OIDC, HTTP surface,
deploy posture). No API/contract changes; the /v1 surface is unchanged. Every
finding was traced to source and covered by tests. Highlights: closed an OIDC
login-CSRF, made all money side effects atomic with the markers guarding them
(no webhook credit-loss / idempotency double-debit under crash+retry), added
security headers + CSV-injection defense, and made the server fail closed rather
than expose an unauthenticated dashboard on a public interface.
Security
- Fail closed instead of exposing an unauthenticated dashboard. The server now
refuses to bind a non-loopback host (e.g.--host 0.0.0.0) when authentication
is disabled — previously the shipped Docker/compose default (serve --demo --host 0.0.0.0) and the off-by-default / fail-off-on-misconfig auth could
publish an open billing console on the network. Override for a trusted network
with--allow-insecure/PLUTUS_ALLOW_INSECURE=1; the disposable demo image
sets it explicitly. Newdocs/deploy-hardening.mddocuments the safe config. - Config backups no longer escape the ignore rules.
.gitignore/.dockerignore
matched only*.plutus-bak-*but the real backup name isconfig.yaml.bak-<ts>,
which could carry file-sourced secrets into a commit or image; nowconfig.yaml*
and*.bak-*are ignored. - SMTP
alerts.require_tls(opt-in) refuses to send alerts over an
unencrypted connection (protects alert bodies, not just credentials, from a
STARTTLS downgrade). PyYAML is now a hard requirement at server start
(fail fast) rather than silently degrading config parsing.
Changed
- Dependency floors gained upper bounds (
stripe<14,reportlab<5,PyYAML<7,
pytest<10) for reproducible builds; the Docker image now runs as a non-root
user. - Ledger atomicity: every money side effect now commits in the same transaction
as the marker guarding it. Two crash-window bugs are closed:
(1) Webhook silent credit loss / double-reverse —mark_stripe_eventcommitted
the event claim in a separate transaction from the creditadd_ledger, so a
crash between them plus Stripe's retry left the event marked "duplicate" with the
credit never applied (customer paid, no credit); concurrent refund/dispute events
for one charge could also double-reverse.handle_webhook_eventnow wraps the
claim and all side effects in onedb.immediate(BEGIN IMMEDIATE) transaction
withcommit=Falsethreaded throughmark_stripe_event/add_ledger/
set_org_tier; any error rolls the whole thing back for a clean retry.
(2)/v1/usageidempotency double-debit — the batch committed, but the
idempotency-key status was flipped in a later separate commit, so a crash in
between left committed events behind a NULL-status claim that the 120s reclaim
deleted, letting a retry re-record the batch. The response is now stored inside
the samedb.immediateblock as the debits. (2026-07-05 security review) - Prepaid hard-stop decided in integer micro-dollars. The
block_over_balance
check compared float USD (balance - cost_usd < 0); it now uses
get_balance_micros/usd_to_microsso sub-micro float error can't let a debit
slip past zero or wrongly block one. (2026-07-05 security review) - OIDC login-CSRF: bind the OAuth flow to the browser that started it. The
authorizationstatewas held only in a process-global pool, so an attacker
could complete their own Google sign-in, capture theircode+state, and feed
a victim's browser a/auth/callback?code=…&state=…link — planting an
attacker-owned session in the victim's browser (the victim would then operate
inside, and leak API keys to, the attacker's tenant)./auth/loginnow sets a
short-livedHttpOnly; SameSite=Laxplutus_oauth_statecookie and
handle_callbackrequires the callbackstateto match it (constant-time),
in addition to the existing_pendingnonce check. (2026-07-05 security review) - Security headers on every response.
_sendnow emitsX-Frame-Options: DENY
(+ CSPframe-ancestors 'none') so the dashboard can't be framed/clickjacked,
X-Content-Type-Options: nosniff,Referrer-Policy: same-origin, and a CSP that
locksdefault-src/object-src/base-uri/form-actionto self. Script/style
keep'unsafe-inline'(the dashboard is inline-only); a nonce-basedscript-src
is a follow-up. (2026-07-05 security review) - CSV export formula-injection neutralized. Tenant-controlled
provider/model/workspace/task_typecells beginning with= + - @(or a
leading tab/CR) are now quote-prefixed inexport.csv, so a crafted value like
=HYPERLINK(...)can't execute when a teammate opens the export. (2026-07-05) - Webhook error/log hardening.
/webhook/stripeno longer echoes the raw
exception text to the caller on a bad signature (returns a generic message, logs
detail server-side), and the success path logs only the event id/type instead of
the applied result (org_id/amount/balance). (2026-07-05 security review)
Changed
views.simple_page's body parameter renamed tobody_htmlwith a docstring
making explicit that it is inserted as raw HTML and callers must pre-escape
untrusted data (removes a latent XSS foot-gun; no live path today). (2026-07-05)
Plutus v1.0.0
[1.0.0] — 2026-06-27
Plutus 1.0 — the billing loop is closed and the contract is frozen. The
ledger is now an auditable mirror of Stripe (refunds, disputes, and failed
payments reverse it idempotently); every money- and quota-bearing input is
guarded; ingest and auth are hardened; self-serve export and a token-scoped admin
API are in; and the /v1 OpenAPI spec plus the DB forward-compatibility policy
are published as the frozen contract Perseus and Perseus Vault build against. An internal
security review (documented in docs/security-review-2026-06-27.md) cleared the
money/auth/tenant surfaces; an external review remains the gate before any public
launch.
Fixed
- Orphaned in-flight Idempotency-Key no longer 409s forever (review F3, #80).
If a request crashed between claiming anIdempotency-Keyand storing its
response, the row stayedstatus=NULLand every retry got409permanently.
A claimed-but-unanswered row older than a 2-minute grace window is now
reclaimable (re-processed), while a completed claim is never reclaimed
(replay preserved). Addeddb.purge_idempotency()to bound the table.
Added
- OpenAPI 3.1 spec for
/v1/*+ the forward-compatibility contract (#67).
openapi.yamldocuments the frozen/v1surface (usage ingest,
spend export, admin) that Perseus/Perseus Vault build against.docs/schema.md
states the database forward-compat policy (additive-only within 1.x; breaking
changes need a new major), anddocs/postgres.mdrecords the
ADR keeping the single-file SQLite backend for 1.0 while documenting the
Postgres migration shape.db.SCHEMA_VERSIONbumped to 5 (the
ingest_idempotencytable);init_schemanow refuses to open a database
written by a newer Plutus, anddb.get_schema_version()reads the stamped
version.
Security
- Negative token counts can no longer rewind the free-tier meter (#80). The
/v1/usageboundary validated only that token fields coerce to int, so a
negativeinput_tokens(with a non-negativecost_usd, dodging the #61 guard)
rewoundtracked_tokens_mtd— bypassing the Free-tier quota — and corrupted
SUM(tokens)aggregates. Negatives are now rejected with a400at the
boundary and aValueErrorinrecord_usage. - CSRF synchronizer token as defense-in-depth (#58). State-changing
dashboard POSTs now accept a per-session CSRF token in addition to the existing
fail-closed Origin/Referer check: a request passes if it is same-origin or
carries a valid token. This lets through legitimate requests whose
Origin/Referer a privacy proxy stripped (which the origin check rejects), while
a forgery — which can't know the token — is still blocked. The token is
HMAC-SHA256(session_token, "plutus-csrf-v1"), derivable only by the cookie
holder and never leaking the cookie; it's embedded as a hidden_csrffield in
every dashboard/pricing form. The origin check remains the first gate. - Per-IP self-serve signup throttle (#59). The existing global hourly limiter
and DB-backed daily org cap (#33) are both global, so one abuser could drain
the whole daily budget and lock out legitimate signups. A new per-IP cap
(auth.max_signups_per_ip_per_day, default 3; in-memory 24h ring) is checked
before the global limiter. The client IP is the socket peer by default, or the
firstX-Forwarded-Forhop whenauth.trust_forwarded_foris set (for running
behind a trusted reverse proxy). Existing members signing in are never
throttled — only new-org self-serve signups.
Fixed
- Dashboard "Sign out" chip used an undefined
--mutedCSS var (#56). The
signed-in user chip and its Sign-out button now usevar(--dim), so the text
renders in the intended dim gray instead of falling back to the inherited color.
Changed
- Package version is single-sourced (#57).
pyproject.tomlnow declares
dynamic = ["version"]reading fromplutus_agent.__version__, so the wheel
metadata andplutus versioncan no longer drift apart.
Tests
- High-risk auth/tenant coverage (#66, part 3 — closes #66). Added tests for
the previously-untested money/auth paths: the hand-rolled OIDC RS256 verifier
itself (a real pure-Python RSA-signed token verifies; a tampered payload and a
non-RS256algare rejected — every other auth test had set
allow_unsigned_tokens, so the signature math was never exercised); the
_authz_orgcross-tenantPermissionErrorpath; and the
allow_negative_balanceexemption end-to-end over HTTP. (The #60/#61/#62
coverage landed with those fixes.)
Added
- Token-scoped admin API (#66, part 2). A new
/v1/admin/*surface lets an
operator script tenant management instead of using the CLI/dashboard only:
GET/POST /v1/admin/orgs(list / create),POST /v1/admin/credits
(grant/adjustledger entries), andGET/POST /v1/admin/keys(list /
mint — the secret is returned once). Gated by a singleadmin.token
(envPLUTUS_ADMIN_TOKEN, masked from saved config, constant-time compared);
with no token configured the API is disabled and returns404. - Self-serve spend export + cursor pagination (#66, part 1). New
GET /v1/usage/export.csvandexport.json(Bearer-authenticated, org-scoped,
optional?since/?untilepoch bounds) let a customer pull their own usage
for their books. List endpoints now paginate with a?limit&before=<_rowid>
cursor: newGET /api/ledgerandGET /api/eventsreturn{items, next_before, limit}, andGET /api/orgsaccepts?limit&offset. The
underlyingdb.ledger_history/metering.recent_eventsgained abefore
cursor.
Security
/v1/usageingest hardening (#65).- Idempotency-Key. A retried or duplicated POST used to double-count usage
and double-debit credit (the inverse of the webhook idempotency from #26).
The endpoint now accepts anIdempotency-Keyheader, claims it atomically
with the recording (per-orgingest_idempotencytable), and replays the
stored response on a duplicate instead of re-recording. - Per-key rate limit. A leaked/abusive key could fire unbounded batches; a
per-key token-bucket limiter (configingest.rate_per_min/burst) now
returns429when exceeded. - Monitor-bridge lock-down. The bridge subprocess now requires the command
to be an absolute path present inmonitor.allowed_binaries(fail-closed,
structured argv,shell=False), and when auth is on it only shells out for
an authenticated request — an unauthenticated dashboard hit no longer
triggers it.
- Idempotency-Key. A retried or duplicated POST used to double-count usage
Added
- Estimated costs are flagged
unpricedwhen no exact model price exists
(#64). Whenever a usage event is metered without an exactcost_usdand the
(provider, model) isn't in the price table, the cost falls back to a
provider/global default — previously with no signal, so a coarse estimate
looked authoritative.MeterResult.unpricednow carries that signal and it is
surfaced per-event in the/v1/usageresponse. The price table is expanded to
current 2026 models (addsclaude-fable-5, the GPT-5 family, more Gemini, and
xAI / Mistral / Cohere / Meta providers), carries a datedPRICE_TABLE_AS_OF
stamp shown on the pricing page, andModelPricecan now price reasoning
tokens separately (defaults to the output rate, so existing estimates are
unchanged). Deferred: persistingunpricedonto historical dashboard rows
(needs ausage_eventscolumn) and cache-write token pricing (needs a new
event token field) — both noted for a follow-up.
Fixed
- Money-correctness cluster (#63) — four independent fixes:
- USD-only is enforced. The credit ledger stores plain USD micro-dollars
with no currency dimension, so a non-USD top-up was recorded as the wrong
number of dollars. A configuredbilling.currencyother thanusdnow
raises a clearBillingErrorinstead of silently mis-billing. past_dueno longer counts as active Pro. A subscription in dunning
used to retain full Pro for the whole retry window; Pro is now kept only
throughactive/trialing(Stripe restores Pro on the nextactive).- Credit checkout amounts are bounded to a finite $1–$10,000 at the form
boundary —inf/nan/a 9-figure typo previously passed straight to Stripe. - Month boundaries are computed in UTC, matching the UTC-epoch event store,
so the free-tier quota reset and MTD reports no longer shift by the server's
UTC offset on a non-UTC host.
- USD-only is enforced. The credit ledger stores plain USD micro-dollars
- Batch
/v1/usageno longer hides prepaid-hard-stop rejections (#62). The
multi-event summary reported only the free-tierblockedcount;
over_balancerejections were absent, so a prepaid org past zero credit could
get a200with events silently dropped. The summary now carries
over_balance_blocked,free_limit_blocked, and ablockedtotal covering
both reasons, and the endpoint returns402whenever nothing landed —
including a batch split across both rejection reasons (previously it only 402'd
when a single reason accounted for the whole batch).
Added
- Stripe refunds, disputes, and failed payments now reverse the ledger
(#60). The webhook handler previously ignored every reversal event, so a
refunded or charged-back prepaid top-up left the credit on Plutus's
append-only ledger forever. New handlers:charge.refundedposts a negative
refundentry (converging to the charge's cumulativeamount_refunded, so
partial/repeat refunds reverse exactly once);charge.dispute.created/
charge.dispute.funds_withdrawnpost a negativeadjustfor the disputed
amount (both events for one dispute converge to a single reversal); and
invoice.payment_failedis recorded as a dunning alert. Top-ups are now keyed
on the PaymentIntent so a dispute (...
v0.3.0 — Google OIDC sign-in
App-native Google OIDC sign-in for the dashboard and billing endpoints — Plutus no longer needs an external proxy to be private. Off by default; enable with auth.enabled + a Google OAuth client. Revocable server-side sessions, allow-listed access, per-user org scoping. See docs/auth.md.
Schema v2 (adds sessions, auto-applied). Existing behavior unchanged until configured.
🤖 Generated with Claude Code