Skip to content
Browse the documentation
Changelog

What changed, release by release

Versions follow SemVer; the format follows Keep a Changelog. Each entry says what was added, changed or fixed and, where a reader found it, who.

Last changed Markdown versionllms.txt

All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow SemVer.

[0.9.17] — 2026-11-09

Changed

  • docs/API.md restructured as a reference: an introduction (authentication, conventions, an error table, metering), then one heading per endpoint with its role, request body and response shape as code, and the notes as prose. The same content as before, readable on the site at /docs/api.

Added

  • scripts/export_site_docs.py: the documentation in this repository (API reference, action contracts, MCP, the OpenTelemetry convention, the sandbox, the nine connector guides, the four SDK readmes, the two recorder readmes and the workflow-runtime readmes) exported to the site as one markdown file per document, with links rewritten to site routes and each document's last commit date in a manifest, plus CHANGELOG.md and an llms.txt. The site renders a page per document at /docs/<slug>, a /changelog page, and serves the raw markdown beside each. The export refuses anything that looks like a credential. tests/test_site_docs_export.py.

[0.9.16] — 2026-11-05

Fixed

  • The public verification page said "chain not intact" for every verdict hash. GET /v1/public/verify/{hash} computed chain_valid only for statements; a verdict match carried no chain field and the page rendered the absence as a broken chain, under a footer saying the chain was intact. The endpoint now verifies the verdict's own chain (every verdict of its run up to that one re-hashes and links, from the run's genesis) and the page shows intact, not intact, or not checked, never a default. Found by an outside reader on a fresh sandbox PASS.
  • The Agent sandbox answered FAIL, not UNKNOWN, for "a required system is down at verification time" in any workspace that had run the sandbox before: facts pushed by earlier runs made the down system count as a connected source, so its missing effects were judged. The harness now passes the systems the extractor could not reach to the verifier, which treats them as not connected for that verification, as a connector in error is. The benchmark and the test suite never saw this because they use a fresh workspace per scenario; the endpoint test now runs a baseline and then the unreachable scenario in one workspace.
  • Trials are card-free, as the pricing page and the terms have always said: Checkout no longer collects a card when nothing is due today, and a trial that ends without a card cancels the subscription instead of charging. GET /v1/settings/billing carries the Founding Builder offer so the in-app upgrade cards show the price Checkout charges (they showed the list price while Checkout applied the offer).

[0.9.15] — 2026-11-03

Added

  • Library contract coinbase_agent_trade: an agent places an order on a Coinbase account (Coinbase for Agents, the Coinbase MCP server's orders_create, or the Advanced Trade API). The subject is the product the agent was told to trade; the contract requires one order on it, created within the window and filled, for the notional asked, within the per-order limit and the agent's 24-hour total; it forbids a second order, a cancelled, expired or rejected one, and treats a fill Coinbase busts or corrects within 7 days as a reversal. Statuses and fields are transcribed from Coinbase's reference (cited); the limits are template values.
  • Coinbase connector (coinbase, facts, read-only): orders and fills from the Advanced Trade API with a CDP API key holding the View permission only, a fresh two-minute ES256 token per request, an optional portfolio filter so an agent's portfolio is read on its own, seven days of re-read on every sync. Facts order_created, order_filled, order_failed, order_reversed, keyed on the product id. An Ed25519 key is refused with the reason. Available on every paid plan. docs/connectors/coinbase.md.
  • Recorder mapping examples for the Coinbase MCP server (recorder/*/examples/coinbase.json) and the README notes on the remote-server bridge and --warn-unmapped.
  • Sandbox: a seventh system of record, a broker with tradable products, orders and fills, and an eighth workflow, "Place a trade" (sbx_place_order), with the generic failure matrix plus seven specific cases: over limit, halted product, daily total exceeded, amount mismatch, wrong product (ETH-USDC for ETH-USD), order accepted but never filled, fill busted after settlement. 18/18 scenarios agree with the oracle; the Failure Lab fixtures are regenerated with the trade workflow (48 fixtures). docs/benchmark.md carries the scripted-agent rows.

[0.9.14] — 2026-11-02

Added

  • The API serves the brand mark at /favicon.ico (16, 32 and 48 px) and /favicon.png (128 px). The Claude connectors directory and Claude's own UI read a server's icon from its URL, and the API host answered 404.

[0.9.13] — 2026-11-02

Fixed

  • Invitations can be accepted. The invitation email always said "sign in to accept", but a member could only act inside a workspace they were already active in, so an invited membership stayed pending for ever and the invitee's first sign-in created a workspace of their own. Signing in with the invited address (hosted sign-in, the verification link, the development login) now activates every pending membership of that address, audited as member.accepted, and the sign-in lands in the workspace that invited them when they have no other.
  • The web app has a workspace switcher (the account menu lists the member's other workspaces), so a person who belongs to more than one can move between them; POST /v1/auth/switch-tenant existed, nothing called it.

[0.9.12] — 2026-11-02

Added

  • Every MCP tool carries a title and the standard annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). Fourteen tools are reads and say so, which lets Claude run them without a confirmation per call; record_action is the one write, marked non-destructive. The annotations must agree with the tool's scope, and tests/test_mcp.py checks that they do. Required for a listing in the Claude connectors directory, where a tool without them is flagged.
  • product-catalog/mcp-tools.yaml now carries each tool's title and annotation-derived read_only and destructive flags.
  • docs/mcp.md: the connector-directory listing (texts, use cases, authentication and data-handling answers, reviewer instructions).

Fixed

  • tests/test_metering.py pinned its actions to October and failed from the first of November; the fixture now records in the current month.

[0.9.11] — 2026-10-29

Added

  • product-catalog/: the machine-readable product facts the CommitLayer knowledge MCP server (kobikwelu/cl-repository-svcs) answers from. Plans, features, connectors, verdicts, reason codes, MCP tools and scopes, API capabilities and release notes are exported from the code by scripts/export_product_catalog.py (with openapi.json and mcp-schema.json under --artifacts); products, security claims and limitations are written by hand and reviewed in the pull request that changes them.
  • CI keeps the two in step: tests/test_product_catalog.py and the knowledge-impact workflow fail when the committed catalog differs from what the code exports, and scripts/check_docs_impact.py fails a pull request that changes routes, MCP tools, entitlements, connectors, reason codes, verdicts, pricing or security code without updating the matching manifest or doc (waivable with docs-impact: none - <reason> in the description).
  • knowledge-dispatch: after a push to the production branch that touches product facts, a commitlayer_knowledge_update repository dispatch with the exact commit asks the knowledge repository to open a sync pull request. Needs a GitHub App installed on the knowledge repository (Contents: read and write).

Fixed

  • The approved security claims do not say records are deleted after 13 months: the retention setting is stored but no job enforces it yet. Uploaded files and transcript excerpts are purged on schedule and say so. Listed as a limitation until a purge job exists.

[0.9.10] — 2026-10-29

Fixed

  • A webhook delivery held SQLite's write lock for as long as the receiver took to answer, up to the 15-second timeout: the delivery row was added before the network call and the next read flushed it. Every API write in that window waited, and a recorded action's verdict INSERT took two to three seconds whenever the previous event was being delivered to a slow receiver. Nothing is written now until the receiver has answered; a test takes the write lock from a second connection during the call to prove it.

[0.9.9] — 2026-10-28

Fixed

  • The last slow statement on the admin metrics: the count of dead, unarchived jobs of the week walked every dead row through the status index, archived ones included, at one to two seconds. Migration 0019 adds jobs(status, archived_at, created_at), which answers it from the index alone.

[0.9.8] — 2026-10-28

Fixed

  • The admin metrics' last unindexed counts: jobs created in the last seven days (two seconds over the whole jobs table, once a minute) and failed webhook deliveries of the last day. Migration 0018 adds jobs(created_at) and webhook_deliveries(status, created_at).

[0.9.7] — 2026-10-28

Changed

  • The trial-ended error (402 trial_expired) names the plan the trial was on and says to add a payment method to keep it or choose another plan. It read "Choose a plan to continue" with upgrade_to set to the workspace's own plan, which the site rendered as "This needs the Scale plan" to a workspace already on Scale.

Fixed

  • After 0.9.6 the admin metrics were still one to four seconds: the slowest statement had become the queue depth, jobs by status with archived ones left out, over a table that kept every succeeded job forever. Migration 0017 adds jobs(archived_at, status), which covers the query, and the nightly purge archives succeeded jobs after thirty days (they stay, as the record of what ran, and the admin list shows them on request), so the unarchived set is a month of jobs rather than all of them. The purge reports succeeded_jobs_archived.

[0.9.6] — 2026-10-28

Recording an action took about a second in production (62 ms on a small database) and the admin metrics took one to four seconds every minute.

Fixed

  • Three counts over the workspace's claims ran on every recorded action without an index for their filter (the monthly allowance by billing period, the daily cap by creation time, and "is this the first action", which counted every claim the workspace ever recorded). Migration 0016 adds claims(tenant_id, billing_period, channel) and claims(tenant_id, created_at); the first-action check reads two rows instead of counting.
  • GET /v1/admin/metrics scanned every run of the week, with a JSON extract per row, on each call. Migration 0016 adds runs(created_at).
  • The nightly maintenance job checkpoints the write-ahead log with TRUNCATE instead of PASSIVE: a passive checkpoint stopped at the oldest open reader and never reset the file, so a WAL that grew under the metrics poll stayed large for every later commit to work through. The job's result says whether a reader held it back.

Added

  • Commit time in the slow-request log line (N commits, M ms in commit): the lock and checkpoint wait that the per-statement timing never saw. database: { bytes, wal_bytes } on the admin metrics, with an alert above 256 MB of WAL.

[0.9.5] — 2026-10-28

A transfer an agent sends from a Stripe financial account is verified against Stripe's record of it.

Added

  • stripe_b2b_transfer, a library action contract for stripe pay and the Treasury send_money tool: the transfer must exist for the intended recipient, once, for the amount requested, reach posted, and not be returned within 30 days; a failed or canceled one, a second one in the window, one over the per-transfer limit, in a currency not allowed, or over the agent's 24-hour total, fails. Status meanings are quoted from Stripe's OutboundPayment reference (retrieved 2026-10-28); limits are template values.
  • The Stripe connector reads outbound payments from Treasury (v1, per financial account) and Money Management (v2, the API behind Global Payouts and Stripe Pay) into facts on the recipient: outbound_payment_created, outbound_payment_posted, outbound_payment_returned, outbound_payment_failed. The last 35 days are re-read on every sync so a later posted or returned lands. A key without the permissions still syncs refunds; the connection's health message names what to grant.

[0.9.4] — 2026-10-27

A password sign-up proves its address before it gets a session.

Added

  • users.email_verified_at and the email_verifications table (migration 0015; accounts that exist already are marked verified, they were admitted before the gate). app/core/emailverify.py: a random token, stored hashed, good once and for 24 hours; a new link retires the previous one; at most one link a minute per address.
  • The hosted sign-in callback, when WorkOS reports authentication_method: Password for an account that has never verified: the workspace is created as before, a link to ${WEB_URL}/verify-email?token= is emailed, and the browser goes to /login?notice=verify_email with no session. Audit auth.verification_sent (or auth.verification_throttled).
  • POST /v1/auth/verify-email { token } → the Session the sign-in withheld (400 invalid_verification when unknown, used or expired; 403 account_held for a held workspace, whose address is still marked verified). Audit auth.email_verified, then auth.login. POST /v1/auth/verify-email/resend { email } → { ok: true } whatever the address.
  • Google, GitHub, Microsoft, Apple, SSO, magic-link and passkey sign-ins mark the address verified on arrival and never see the step; the development login and the dev-only /signup do the same.

Changed

  • The founder's welcome waits for the click on a password sign-up (on_signup(welcome=False), then on_email_verified), with a new email_verified person event; every other sign-up gets it at once as before.

[0.9.3] — 2026-10-27

Changed

  • The founder's welcome email comes from WELCOME_FROM (default "Kenneth at CommitLayer hello@commitlayer.ai") rather than the no-reply address; send_email takes a per-message sender.

Operations

  • 2026-10-27 · MASTER_KEY rotated (RUNBOOKS §7, the no-downtime path): nine tenant data keys re-wrapped, verified under the new key alone, fallback removed. Reason: the hosting provider's variable exposure.

[0.9.2] — 2026-10-26

Fixed

  • The first-and-second-verdict milestones (0.9.0) counted every stored verification on each settled verdict: 1.7 s per verification on a busy workspace. The query now stops after three rows.

[0.9.1] — 2026-10-26

Sync lag that names its connection (Sentinel incident, 2026-10-26).

Changed

  • sync_lag_seconds no longer counts a connection with no stored credential or one waiting on re-authentication: the daily sync cannot move its watermark until someone acts, and connections_in_error already says so. Counting them turned a customer's expired token into a permanent platform alert. sync_lag_seconds.excluded reports how many were left out and why.

Added

  • stalest_connections on GET /v1/admin/metrics: the five oldest counted connections with tenant, source, name and age. GET /v1/admin/connections: every live connection of a real workspace, stalest first, with the exclusion reason, credential presence and health message. Connections tab on the admin page.

[0.9.0] — 2026-10-26

Sign-up, activation and billing told to the systems that act on them (Growth OS attribution and lifecycle email).

Added

  • app/ops/lifecycle.py. A new workspace fires user_signed_up on the person (so the anonymous visitor the site identified becomes this user), queues the founder's welcome email (with WELCOME_REPLY_TO), and queues a lifecycle_contact job that posts the person id, workspace id, email, plan and consent source to Growth OS at /v1/lifecycle/contacts (GROWTH_OS_URL, GROWTH_OS_TOKEN; a 4xx other than 429 is final). A held sign-up fires the event but no welcome until it is approved.
  • Milestones on the workspace owner's person: first_action_recorded, first_verdict_received {verdict} and second_verification_completed (settled verdicts of the workspace's own actions, sandbox excluded), first_connector_connected {source}, checkout_started {tier, mode, founding}, and from the Stripe webhook subscription_started {tier, from} and plan_upgraded {from, to} (a downgrade is not reported).
  • send_email takes a Reply-To.

[0.8.10] — 2026-10-25

Fixed

  • Webhook deliveries failed on every attempt since 0.8.7: the pinned-address request passed the TLS name as an option the module-level httpx helper does not take. The request now goes through a client with the option on the request, and the test drives a real transport instead of a patched helper. Deliveries that died in the meantime are retried by the queue as usual; the dead ones are the webhook's failure count.

[0.8.9] — 2026-10-25

Fixed

  • The Stripe webhook handler reads a subscription's period end from its items when the endpoint's API version (2025-03 and later) no longer puts it on the subscription; the live endpoint is on such a version, the sandbox one was not. Without it the billing page had no renewal date.

[0.8.8] — 2026-10-23

Master-key rotation as a command, with no downtime (Kenneth, after the hosting provider's key exposure).

Added

  • MASTER_KEY_PREVIOUS: during a rotation, a tenant data key still wrapped under the retired master key unwraps through this fallback, so the new key can be deployed before anything is re-wrapped.
  • ledger rotate-master-key [--no-backup]: backup, then re-wrap every tenant data key under MASTER_KEY; idempotent; refuses to write anything when a key unwraps under neither value. ledger verify-master-key: every key unwraps under MASTER_KEY alone and one connection's credentials per tenant decrypt (exit 1 while any key still needs the fallback). RUNBOOKS §7 rewritten around them; the hand-run script is gone.

[0.8.7] — 2026-10-23

Security review follow-through (static audit of the deployed branch, rev. 2). Every finding verified against the code before it was fixed; two were partly wrong and are noted below.

Fixed

  • C1, cross-tenant SQL injection. The opportunity_reached_stage_within primitive rendered the stage name into compiled SQL unescaped, and compiled contract SQL runs outside the ORM's tenant hook. The value now goes through the dialect escaper (Dialect.str_) and must be a plain name to compile at all.
  • H2, admin to owner. Nobody assigns a role above their own (403 role_above_own) or changes or removes their own membership (403 own_membership); only an owner mints or demotes an owner. Accepting one's own invitation at the invited role still works.
  • H1, connector SSRF. A connector's base URL from stored credentials passes the egress check (https, public name, public addresses) before any request; a hostile Retry-After is capped at sixty seconds.
  • NV-1, DNS rebinding on webhook delivery. The delivery connects to the address the check resolved, with the original host for TLS and routing (egress.pinned_post).
  • M1 and M3, hosted sign-in. The state is a signed ten-minute token, also set as a cookie on the API host; the callback requires the two to match, so a sign-in link made elsewhere goes nowhere (auth.state_mismatch audited). The in-process state dict, which grew without bound and broke across worker processes, is gone.
  • M4, token in the URL. The callback redirect carries a one-time sixty-second code; POST /v1/auth/handoff trades it for the token server-side (auth_handoffs, hashed). The redirect also honours the next= the sign-in started with, which was lost before.
  • M8, token-type confusion. verify_session accepts only a session token with a subject; another token the same key signs answers 401 instead of a 500.
  • M2, open routes unthrottled. /v1/auth/*, /v1/public/* and /v1/oauth/* are limited per client address (ANON_RATE_PER_MINUTE, default 60; 429 with Retry-After).
  • L4, sessions could not be revoked. Sign-out stamps users.sessions_revoked_at; tokens issued before it are refused on REST and MCP.
  • L12. Only an active membership grants access (REST and MCP resolution).
  • M5 and NV-2b (site). One safeInternalPath guard for every next and back-link: //host, /\host and control characters fall back to the app. The site's callback route exchanges the code; the development login and impersonation hand their token to that route by POST instead of the URL.
  • Migration 0014 (users.sessions_revoked_at, auth_handoffs).
  • H3, upload expansion. Uploads are parsed off the event loop (a worker thread), with a row cap (200,000) and, for XLSX, a check of the declared uncompressed size (256 MiB) before the archive is opened.
  • H4, path traversal in object keys. The stored name is the sanitised basename of the upload, and the local object store refuses any key with a .. segment or an absolute path.
  • M10 and L9, parser by extension; 500s on bad input. The format is checked by magic bytes; an unreadable file answers 400 unreadable_file; an empty file is an empty table; a score, amount or unit price that is not a finite number is skipped rather than stored as inf/nan or crashing the ingest. The review said the crash came after the file was stored; it came before, so nothing was left behind, but the orphan on a later failure is real and the parse now precedes every write.
  • NV-2a. The Parquet temp file is deleted and the DuckDB connection closed after every parse.
  • M7, dev-default secrets. Outside development and test, the API refuses to start with a SESSION_SECRET or HMAC_PEPPER still at the dev default or under 32 characters, or an empty MASTER_KEY.
  • M9, dry-run without a bound. A dry run is interrupted after DRY_RUN_SECONDS (default 10; 408 dry_run_timeout) and reads at most 50,000 rows.
  • L10. The reference-number regex in transcript redaction no longer backtracks cubically on whitespace (3,000 spaces redact in milliseconds).
  • L11. A client cannot set a connection's role or kind, so a free-form config no longer exempts a connection from the plan's count.
  • L1 and L7. /openapi.json is off in production; /health is status, version, engine_version and db (ok or error) only; the integrations map and the database error text moved to GET /v1/admin/metrics.
  • L2. The container runs as an unprivileged user: the entrypoint, still root, hands the data volume to app and drops privileges with setpriv before starting the server.
  • M6. python-multipart>=0.0.18 and starlette>=0.47.2 as floors (the image already carried newer).
  • L3 (site). The published image is built with NEXT_PUBLIC_AUTH_MODE=workos.
  • Not done, on purpose: L8 (the benchmark SDKs stay in the image because the admin benchmark runs in production), L5 (a nonce-based script-src for the site needs middleware work and is a follow-up), and a lockfile for Python dependencies (follow-up).

[0.8.6] — 2026-10-23

The sign-up screen: a mail server, a reputation, a country (Kenneth: stop automated sign-ups, hold Pakistan for review, show every refusal on the admin page).

Added

  • app/core/signupguard.py: four checks on an address about to create a NEW account, cheapest first. Throwaway domain (as before); a domain with no mail server (no_mail_server); an address or network reported for sign-up spam at StopForumSpam, asked with the MD5 of the address and the public client address so the address never leaves the process (reputation, at SIGNUP_REPUTATION_MIN_CONFIDENCE); and the client's country from a local GeoLite2 database against SIGNUP_COUNTRY_POLICY (IN=block,PK=hold). A check that cannot complete lets the person through and logs it. An existing account is never screened.
  • hold: the workspace is created with status held; sign-in and every product route answer 403 account_held ("waiting for a review") until an operator approves it. signup_refusals records every refusal and hold.
  • Admin: GET /v1/admin/signup-refusals with the screen's state (policy, allow list, country database presence and age), POST /{id}/approve (active, owner emailed, signup.approved), POST /{id}/reject (suspended, signup.rejected). Sign-ups tab on the admin page.
  • refresh_geoip job: fetches the GeoLite2 country database with MAXMIND_LICENSE_KEY on boot when missing and weekly after; unpacked to a temporary name and renamed over, so a reader never sees a half file.
  • Migration 0013 (signup_refusals). Refused rows are purged after 90 days, decided ones after a year.

Changed

  • Sign-in page error codes: no_mail_server, reputation, region_closed, account_held join disposable_email.

[0.8.5] — 2026-10-21

Transcript assist waits for the first invoice (infrastructure cost review: a Scale trial could spend its whole model cap before any card was charged).

Changed

  • Transcript assist, the one model call in the product, cannot be switched on until the workspace's first invoice with money on it has cleared (PATCH /v1/settings/transcript-assist with enabled: true answers 402 payment_required, details {waiting_for: "first_invoice"}); the pipeline refuses to label before that even if the switch was on. A workspace provisioned by hand (status manual) and the synthetic demo are exempt. GET carries available and waiting_for.
  • The default monthly cost cap drops from $25 to $5. Migration 0012 moves a cap still at the old default; a cap someone set by hand is left alone.
  • invoice.paid with amount_paid > 0 records tenants.first_paid_at once, audited as billing.first_invoice_paid. The trial's $0 invoice does not count.

[0.8.4] — 2026-10-20

Webhooks that say what is wrong and mend themselves (Kenneth: "fix this once and for all").

Added

  • A webhook now carries its own health: last_error in words that name the fix ("the receiver rejected the signature; rotate the secret here and set the new value there", "nothing is listening at this URL"), last_response_code, last_attempt_at, last_delivered_at, disabled_at. Shown on the settings page.
  • POST /v1/settings/webhooks/{id}/test delivers now and reports the receiver's answer in the same call, so a wrong secret shows in one click; a success re-enables a disabled webhook.
  • POST /v1/settings/webhooks/{id}/rotate-secret: a new signing secret shown once, the old one retired, the failure count reset. No more deleting and recreating a webhook (and losing its id) to get a secret.
  • POST /v1/settings/webhooks/{id}/enable: re-enable by hand.

Changed

  • Disabling is no longer a dead end. Twenty consecutive failures still disable a webhook (audited, webhook.disabled, with the reason on the webhook), but one delivery an hour is still tried, and any success re-enables it (webhook.recovered). A receiver fixed next week comes back on its own.

[0.8.3] — 2026-10-20

Changed

  • Dead engine jobs are archived, not deleted (Kenneth, on 0.8.2: keep them for the record). After thirty days a dead verification, run or statement job gets archived_at: it leaves the queue's counts, the status page and the admin metrics, and the admin job list shows it only with include_archived=true; a retry un-archives it. Dead outbound deliveries stay on the seven-day hard delete, since each delivery is on record in webhook_deliveries with its response and error. A deleted workspace takes its jobs, archived included; before this its jobs were the one table tenant deletion did not clear.

[0.8.2] — 2026-10-20

Changed

  • Dead jobs no longer accumulate forever. A nightly purge_dead_jobs keeps a dead outbound delivery (webhook, integration, alert, email) for seven days and any other dead job for thirty; succeeded jobs are untouched. Migration 0009 removes the dead webhook-delivery jobs the Sentinel storm left, once, on deploy. Applied migrations are now logged with the rows they changed.

[0.8.1] — 2026-10-20

What the Sentinel webhook storm exposed, fixed. (The settlement delay itself was on the Sentinel side: its synthetic journey dated facts before the action it then created; see the GTM log.)

Fixed

  • MCP: every call wrote the key's last_used_at, so every model-facing read was a writer on the one SQLite lock, and a lock it lost answered the model a 500. Touches are now throttled to API_KEY_TOUCH_SECONDS and a busy database never fails the call, as on REST since 0.5.2. Same for OAuth tokens.
  • Webhooks: a receiver answering 401 or 403 has refused the signature, which retrying cannot change. That delivery is now dead at once with a message naming the fix (the secret on the receiving side), instead of five attempts; the next event tries again and twenty refusals still disable the webhook. Delivery failures are one log line, not a traceback per attempt (a receiver that was down pushed the log past the platform's rate limit).
  • Job queue: per-workspace concurrency is now per lane. Outbound deliveries (webhooks, integrations, alerts, email) wait on someone else's server for up to fifteen seconds each, and a flood of them held a workspace's settlement re-checks behind them; the engine lane has its own cap and is claimed first.

[0.8.0] — 2026-10-19

Bot and input hardening, asked for by Kenneth: honeypots that ban, throwaway emails refused, markup kept out of names, ceilings on input. Surveying for it found two holes worth more than the features.

Security

  • POST /v1/auth/signup created a user and a workspace for any address in production, with no proof the caller held it (only the session token was withheld), so a script could mint workspaces in bulk. It is now development-only, like the development login; production accounts come from the hosted sign-in.
  • The client address was the leftmost X-Forwarded-For, which the caller writes. An IP allowlist could be passed by naming an allowed address, the audit log recorded a chosen address, and a ban keyed on it could have been aimed at an innocent customer. The address is now the one Railway's edge sets in X-Real-IP (TRUSTED_IP_HEADER); allowlist entries are validated and normalised.
  • Validation errors no longer echo the offending input (a 4 MB body came back as a 4 MB error; a markup payload was reflected verbatim) and no longer crash into a 500 when a custom rule rejects a value.

Added

  • Bot traps that ban: scanner paths (/.env, /wp-login.php, /.git/config and their kind) answer 404 and ban the address; /robots.txt disallows a path nothing links to, and a crawler that goes there is banned; the lead and walkthrough forms carry a hidden honeypot field, and a submission that fills it is answered as a success, stored nowhere, and banned. A ban lasts BAN_HOURS (24), lives in the database so every API process honours it, never applies to a non-public address (the proxy, health checks) or to BAN_EXEMPT_IPS, and is refused with 403 blocked. Platform admins list and lift bans at GET / DELETE /v1/admin/bans.
  • GET /v1/public/whoami: the address the API sees, the one an allowlist is checked against.
  • Throwaway email domains refused (400 disposable_email) where a new account, an invite or a lead would be created: the vendored CC0 list of 9,913 domains, subdomains included, adjustable with DISPOSABLE_EMAIL_ALLOW / DISPOSABLE_EMAIL_EXTRA. An existing account keeps signing in whatever its address. The hosted sign-in sends a refused address back to the sign-in page with the reason.
  • Request body ceilings, enforced as the bytes arrive: 64 KiB on unauthenticated routes, 50 MiB for uploads (which used to read a whole file before checking it), 4 MiB elsewhere; 413 payload_too_large.
  • Every request string capped (20,000 characters unless the field names its own limit) through one shared base model, so a model written tomorrow is capped too. Names of workspaces, members, keys, connections and companies refuse <, > and control characters; free text drops control characters, including bidirectional overrides.
  • Security headers: Content-Security-Policy: default-src 'none' on JSON, a no-script policy on the HTML exports, X-Frame-Options: DENY, and HSTS in production (security assessment item 7). On the site: HSTS, Permissions-Policy and the parts of a content security policy that cannot break it.

Fixed

  • The sign-in page printed any ?error= text verbatim, so a link could put someone else's sentence on it. Only known error codes are shown now.

[0.7.4] — 2026-10-19

Added

  • METRICS_READ_TOKEN: a bearer token that GET /v1/admin/metrics accepts in addition to a platform-admin session, for an external monitor (Sentinel) that needs the aggregate platform metrics around the clock. Read-only, one route, constant-time compare; a token sent anywhere else fails as an invalid session. Unset, nothing changes. Authored on the Sentinel branch (b1472cb), reviewed and merged here.

[0.7.3] — 2026-10-19

Fixed

  • Public status page, again: a webhook receiver answering 404 produced a dead delivery job every minute and the engine read degraded within the hour of 0.7.2. Outbound deliveries (webhooks, integrations, alerts, email) die on someone else's endpoint and no longer count toward the engine's health; connector syncs already did not. What counts: verification, runs, statements, reconciliation, exports, housekeeping.
  • Webhook URLs are normalised on save: repeated slashes in the path are collapsed, so a base pasted with a trailing slash plus a path with a leading one no longer calls host//v1/... on every delivery.

[0.7.2] — 2026-10-19

Fixed

  • Public status page: "Verification engine" read degraded since 2026-10-16 because the check counted dead-lettered jobs for all time, and twelve nights of one customer's rejected Zendesk token had produced twelve of them. Degraded now means engine job types (not connector syncs) dead within the last 24 hours. Found by the Sentinel monitor's first live observation.

[0.7.1] — 2026-10-19

Fixed

  • Shopify connector: a token with read_orders but not read_customers made Shopify answer ACCESS_DENIED on the customer { email } field, which failed the whole sync and read as a credential problem. The connector now sends the orders query again without the field, finishes the sync with customer_hid null (what the connector doc always promised), and notes the missing scope in the connection's health. Sync health messages carry a connector's notes in general. Found while preparing the live Shopify test.

[0.7.0] — 2026-10-19

Pricing follow-through from an external review Kenneth brought (docs/GTM.md, 2026-10-19). Headline prices stay; allowances rise; free usage gets a boundary; a founding offer; support only from Team up.

Changed

  • Allowances: Builder 5,000 (was 2,500), Team 25,000 (was 10,000), Growth 100,000 (was 50,000); Scale stays at 250,000 until the database move. Overage 2.5¢ / 1.5¢ / 1¢ / 1¢ (was 4 / 3 / 2 / 1): a rate that never rises when you upgrade. Migration 0007 moves a stored entitlement still on the old figure to the new one and leaves a hand-set figure alone.
  • Developer's grace past the allowance is bounded: 72 hours or 250 actions, whichever first. After it a new action answers 402 allowance_exceeded and is not recorded (API, inline verify and OTLP alike); re-recording an existing action, verifying by id, dry runs and the settlement re-checks of recorded actions continue. Paid plans are unchanged: never refused, overage billed.
  • Support is a line on every plan: none below Team ("Docs and self-serve; no support included"), email support on Team, priority on Growth and Scale, dedicated on Enterprise.

Added

  • Per-workspace rate limits by plan: requests a minute on every authenticated call and writes a minute on recording, verifying and pushing facts, as in-process token buckets; 429 rate_limited with Retry-After, nothing applied. Developer fair-use caps a day: 250 recorded actions (counted from the database) and 5,000 pushed facts; 429 fair_use_exceeded. Figures in app/billing/pricing.py and on the public pricing endpoint; RATE_LIMITS=false only in tests.
  • Founding Builder: Builder at $59 a month for 12 months for the first 50 workspaces, a Stripe coupon (founding-builder) created on first use with max_redemptions as the cap. Every Builder checkout or switch carries it while seats remain; the public pricing endpoint reports the seats left from Stripe's times_redeemed and shows nothing when Stripe is not configured or cannot be read. A coupon Stripe refuses between the read and the checkout falls back to the list price in the same call.
  • GET /v1/public/pricing carries support, rate_limits and fair_use per tier, grace_actions, the founding offer, included_everywhere and the compare table the pricing page renders, so the page and the limits have one source.

[0.6.3] — 2026-10-16

Connections that fail now say what happened and stop retrying what cannot change. From the live Zendesk and Salesforce tests.

Changed

  • Provider errors are one line a person can act on: the host, the status, the endpoint without its query string, and the provider's own message (Salesforce errorCode: message, Zendesk title: message). The full URL and the link to a status-code encyclopedia are gone from connection health and sync history.
  • A credential the provider rejects (401/403) is a terminal failure: the sync job is dead after one attempt with its error, instead of failing five times over half an hour; the connection reads "needs re-authentication"; the nightly sweep skips it until someone re-authenticates.
  • Re-authenticating an API-key connection queues a sync immediately, so the fix shows within a minute.
  • Connection page (web): the health banner distinguishes a rejected credential from a failed sync and carries the fix; Re-authorize opens a form with the connector's own credential fields instead of a dead button; the sync history groups consecutive identical outcomes ("failed × 5, 03:00 to 03:08"), clamps errors to one line with a details toggle, and shows ten groups with "show all".

[0.6.2] — 2026-10-16

Fixed

  • Salesforce connector: the Opportunity query asked for Contact.Email, a field Opportunity does not have, so the first live sync against a real org failed with 400 INVALID_FIELD and stored nothing (the Case part had already run). The customer on an opportunity is its primary OpportunityContactRole; the query now reads that role's Contact.Email through a subquery and hashes it as before. Found on the first connection to a Developer Edition org; a recorded-shape sync test now covers both queries, reopen detection and the watermark.

[0.6.1] — 2026-10-16

Changed

  • The benchmark harness runs with one credential per side of the line: --recorder-key records the agent's actions, --evidence-key (bound to the sandbox's six sources) pushes the facts, and the full --api-key only creates the sandbox contracts and asks for verdicts. With a full key alone everything still runs and the result says credentials: single. The SDK READMEs (Python, JavaScript, Go, Java), the action-contract walkthrough and the site's code samples show a recorder client and an evidence client.

[0.6.0] — 2026-10-16

Evidence provenance (item 1 of docs/security-assessment.md): the credential that records an action never also supplies the facts that verify it.

Changed

  • New scope facts:write. POST /v1/facts, and inline facts on POST /v1/actions/verify outside a dry run, require it; actions:record no longer implies it. Recording (POST /v1/actions, an inline action on verify) requires actions:record. A key without the scope answers 403 insufficient_scope.
  • Existing recorder keys can no longer push facts. Keys that hold every scope (full, and keys created before scopes existed) are unchanged.
  • Presets renamed to say which side of the line a key is on: recorder (record and read), evidence (push facts and watermarks, read verdicts and watermarks), reader (every read scope plus validate and draft; the old evidence), full. GET /v1/settings/api-keys returns preset_descriptions.
  • A key with facts:write is bound to the sources it may push for (fact_sources on creation, required unless the key holds every scope). A fact or watermark for another source answers 403 source_not_allowed.
  • A source a live connector serves (its own name and everything it provides) cannot be pushed through any key: 409 source_served_by_connector. A pushed fact can never stand in for what the connector reports.
  • Model-facing principals (OAuth tokens and bearers on the MCP server) never hold facts:write.
  • Contracts: min_trust: self_reported | shared | independent on an expected effect. A fact from a source below that level does not satisfy the effect; the check reports it as observed but not ok with the trust seen, the reason code is effect_below_trust:<source>.<type>, and the headline says why. Trust is read live (the connection's level, or the operator's per-source override on the facts-API connection), so a reclassification applies to the next verification. Default: no gate; existing contracts are unchanged.

Hardening from the security assessment in docs/security-assessment.md (items 2 to 6 of its priority list).

Changed

  • A recorded action is immutable. POST /v1/actions with an existing action_id is a no-op when it describes the same action and answers 409 conflict when any of contract, subject, parameters, workflow, agent, type, channel, requested time or compensates differs. Until now the second call silently rewrote the stored action. A change of mind is a new action, or a compensating one.
  • Future-dated inputs are refused: a fact's occurred_at, a declared watermark or an action's requested_at further ahead than MAX_CLOCK_SKEW_SECONDS (900) answers 400. Server ingestion time was already stored apart (created_at) and is unchanged by the caller.
  • Every CSV export (statements, dispute packages, delta reports, the audit log) neutralises cells that a spreadsheet would read as a formula (=, +, -, @, tab, carriage return) by prefixing an apostrophe.
  • Webhook URLs are checked when saved and again at every delivery: https only, no credentials in the URL, no localhost or .internal-style names, and the host must resolve only to publicly routable addresses (EGRESS_GUARD, on by default). A delivery whose URL now points inside a network fails without a request.
  • Contract YAML is refused above MAX_CONTRACT_BYTES (128 KiB), with more than 16 anchors or 32 aliases, deeper than 32 levels or above 50,000 values, before anything expands. The loader was already the safe one.

Changed

  • The evidence freeze. When an action settles, the check table, headline, sources, facts, settlement and reversal are stored with the verdict (verification_snapshots, one row per settled run) and a read of the action returns them as they were, with evidence_frozen_at. Until now a read re-ran both lenses against the facts currently stored, so a FAIL that settled because an effect never arrived in time showed that effect as seen once it arrived late, next to a verdict that said the opposite. Facts recorded after settlement are listed apart in facts_after_settlement. A source's trust label and the action's resolution stay live. Pending actions and verdicts stored before this release are still evaluated fresh on read. Reading a settled action no longer runs the engine (it was a third of read CPU in the load profile).
  • The boot-time demo seed refreshes the sample's watermarks before it re-verifies, so a settled sample never freezes a picture of its sources weeks behind.

[0.5.2] — 2026-10-15

Phase C of the load plan: the re-test after 0.5.1 showed the list and summary reads as the slow path and fifty writers polling the busy timeout against each other.

Fixed

  • GET /v1/actions and GET /v1/actions/summary loaded up to 2000 actions and ran three queries per action (1.7 s at ten readers, 18 s with writers present). The list now cuts the page in SQL and evaluates only that page; with a verdict or workflow_id filter the newest 2000 are evaluated in bulk. Latest verdicts load in one query per page or summary instead of one per action.
  • A recorded action whose verification failed and whose recovery job could not be queued (both lost the write lock) stayed unverified: the recovery enqueue retries briefly, and the worker sweeps for recorded actions with no verdict older than UNVERIFIED_ACTION_SWEEP_SECONDS (60) once a minute and queues their verification.
  • The worker no longer logs a traceback when the queue claim or a job loses the write lock under load; a lock loss is a warning and the job's own retry schedule handles it.

Changed

  • Write gate: at most MAX_CONCURRENT_WRITES (8) requests are on the write path (POST /v1/actions, POST /v1/facts) at once; a writer that finds no slot within WRITE_SLOT_TIMEOUT_SECONDS (2) answers 503 busy with Retry-After: 1 without touching the database. The verdict commit happens while the slot is held.
  • UVICORN_WORKERS (2): the API runs as two processes. The load test showed reads CPU-bound in one Python process (12 ms of SQL in a 1.1 s list at ten readers); two processes served 85 requests per second where one served 19, and 33 writes per second where one served 18. The job worker still runs once, in the parent.
  • Load test after this release, same script, local 4 vCPU box, two processes: ten readers p50 list 113 ms, summary 107 ms, single action 163 ms; ten writers 33 req/s, p50 197 ms, p95 0.9 s, no errors; fifty writers 28 req/s, 870 of 887 recorded, 17 fast 503s, zero lock errors, zero tracebacks, every recorded action verified. Remaining CPU: the summary is linear in the month's action count (about 0.1 ms per action, 16 s at fifty concurrent dashboard refreshes over 2400 actions) and GET /v1/actions/{ref} re-runs the engine to rebuild the check table (a third of read CPU). Both are listed as the next items.

[0.5.1] — 2026-10-15

Overload behaviour, after a load test and a fact-check of its review (docs/GTM.md log).

Fixed

  • A write that lost the SQLite lock could answer 201 and store nothing: the request session committed after the response was sent (FastAPI's request-scoped dependency). Every route now commits before the response goes out.
  • POST /v1/actions records the claim in one short transaction, verifies outside any write transaction, and commits the verdicts in a second; if verification fails after the claim is in, a verify_action job picks it up. No action is recorded-but-never-verified.
  • Two in-flight retries of one action_id could create two claims: (tenant_id, raw_ref) is now unique and the second insert takes the update path.
  • API-key requests no longer write last_used_at on every call (that one line was most of the lock failures); it is refreshed at most every API_KEY_TOUCH_SECONDS (300), in its own short transaction. API-key reads of an action no longer write an audit row; a person viewing evidence is still audited.
  • Slack alert jobs are queued only when a Slack channel is configured for the event. A job whose final status update lost the lock stayed "running" and the per-tenant cap held the tenant's queue; the status update is now its own retried transaction after the handler's work commits.

Changed

  • Overload is a 503 with Retry-After: 1 and error code busy, never a 500: the write lock waits at most SQLITE_BUSY_TIMEOUT_MS (3000, was 10000), the connection pool matches the threadpool (40 + 24, 5 s timeout, was 5 + 10 with 30 s), and uvicorn admits at most MAX_IN_FLIGHT_REQUESTS (64) at once. Requests slower than SLOW_REQUEST_MS (1000) log one line with statement count, SQL time and the slowest statement.
  • Parsed contracts are cached by YAML digest (C loader) and compiled contracts by digest, dialect and sources; a request resolves its contract once instead of three times.
  • Nightly optimize_database job: ANALYZE, PRAGMA optimize and a passive WAL checkpoint.

[0.5.0] — 2026-10-15

Added

  • Shopify connector (shopify, facts only): orders changed since the watermark through the Admin GraphQL API become order_status state facts and order_cancelled, refund_created and fulfillment_created event facts, keyed by the numeric order id; customer email hashed; throttling honoured. The flagship refund contract names Shopify as the order system and until now it could only be fed through the facts API. docs/connectors/shopify.md.
  • The completion gate: GET /v1/actions/{ref}/wait?timeout= holds the request until the verdict settles or the timeout passes (60 seconds per call), evaluating the evidence that arrived and storing the settled verification once; wait_for_settlement / waitForSettlement in the Python and JS SDKs loop over it. Audit action.awaited. Documented as the pattern for a workflow that must not report success before the outcome is proven.
  • Competitive landscape verified against each product's own site or docs, in docs/GTM.md section 6.
  • Temporal and Inngest integrations for Python (integrations/python, distribution commitlayer-integrations): a Temporal activity interceptor that records a mapped activity as an action before it runs (idempotent on temporal:{workflow_id}:{activity_id}, fail-open) and a commitlayer_verify activity that waits on the completion gate with heartbeats; an Inngest recorded_step helper that records inside the memoised step and a wait_for_verdict step loop. The activity or step result is never sent. 61 tests against fakes; CI runs them.
  • The same for TypeScript (integrations/js, @commitlayer/integrations with temporal and inngest subpath exports): an activity interceptor factory, a commitlayerVerify activity, recordedStep and waitForVerdict. 34 tests. Both integrations wait through the completion gate rather than polling plain reads.
  • SDKs: contract_version / contractVersion on record, and a public request(method, path, body) for any endpoint.

Fixed

  • The synthetic demo's connections never sync, so their fixed watermarks aged and every public sample reported its sources as weeks behind at settlement. Each boot now moves the demo watermarks to the current time; the showcase verdicts are unchanged, only the lag they reported.

[0.4.0] — 2026-10-15

Added

  • CommitLayer over MCP (docs/mcp.md). The API serves an MCP server at /mcp (streamable HTTP, stateless, JSON responses) with fifteen tools that read actions, verdicts, reasons and evidence pointers, contracts, coverage, reconciliations and usage, validate, dry-run and draft contracts, and, with the actions:record scope, record an action; two resources (commitlayer://reason-codes, commitlayer://actions/{ref}) and two prompts. Nothing on it writes facts, activates contracts, overrides verdicts or deletes. Every call is audited as mcp.tool_called; plan gates answer with the plan that unlocks the tool.
  • Scopes on API keys (actions:read, actions:record, contracts:read, contracts:validate, contracts:draft, reconcile:read, connectors:read, usage:read) with three presets: full, evidence (read-only) and runtime recorder. Over REST a key without actions:record acts as a viewer. Keys created earlier keep every scope.
  • OAuth 2.1 authorization server in the API for remote MCP clients (ChatGPT, Claude): metadata documents, dynamic client registration, /authorize with PKCE handing off to the web app's consent page, /token with code exchange and refresh rotation, /revoke. Tokens are hashed, bound to the member and the workspace they approved, capped to the member's current role, and end when the member leaves. Consent endpoints GET/POST /v1/oauth/consent; audit oauth.approved, oauth.denied.
  • The MCP recorder, recorder/python (commitlayer-mcp-recorder) and recorder/js (@commitlayer/mcp-recorder): a stdio JSON-RPC proxy that re-exports any downstream MCP server and records a mapped tools/call as an action (POST /v1/actions, channel: mcp) before forwarding it. Mapping file per tool (contract, subject path, params, action_id template, compensates); unmapped tools pass through; fail-open by default; the tool's own result is never sent. Standard library only, tested against a fake MCP server and a fake API.

Changed

  • mcp SDK dependency (pinned to the 1.x line; 2.x renamed FastMCP). New tables oauth_clients, oauth_codes, oauth_tokens; api_keys.scopes (migration 0004_api_key_scopes).

[0.3.0] — 2026-10-14

Added

  • Reconciliations as a first-class object (/v1/reconciliations): a vendor's claims for one period, keyed by contract and period like runs and statements. Creating one from a contract fixes the vendor, outcome type and subject kind; the view names which active connection covers each fact source the contract needs and the signals no source covers; the claims upload pins rows to the period and remembers the column mapping per vendor, so the next month needs no mapping; a one-off facts file can stand in for a system without a connector; the run, statement, exports, invoice and disputes are reached from the same view. New audit actions reconciliation.*.

Changed

  • Connections are live, read-only fact sources. File uploads happen inside a reconciliation; the connections upload endpoint stays for API clients but is no longer a connector in the app.
  • The Zendesk connector is facts-only. Its claims path read an "automated resolutions" endpoint Zendesk does not provide; the automated-resolutions report is a download and is uploaded as the reconciliation's claims. The connector is now displayed as "Zendesk" with kind facts.
  • Contract summaries carry subject_kind.

Fixed

  • A required fact source is satisfied by a broader family that contains it, in the wizard's coverage and in the engine's missing_fact_source rule alike: a HubSpot connection (tickets, crm) covers a contract that needs conversations. Before, the wizard said "covers none of the required sources" and the engine would have marked every claim unverifiable.
  • Intercom: conversations a teammate starts from the Inbox carry source: null and no contacts; the connector assumed a dict and every sync of such a workspace failed.
  • A failed sync now leaves a failed sync row and the connection in error. Before, the job handler raised and the worker's rollback erased that state, so the connection sat on "syncing" with rows stuck "running". A new sync marks any row still running as superseded.
  • A connection's watermark now advances to the sync time (less a five-minute safety margin, SYNC_SAFETY_LAG_SECONDS) on every successful sync. Before, it stayed at the newest record seen, so a quiet source such as a Stripe account with no refunds this week looked days behind: the admin lag alert fired and pending action verdicts would have waited on it for the full settlement bound.
  • The admin sync-lag metric ignores synthetic (demo) workspaces.
  • The source that holds a vendor's uploaded claims no longer counts against the plan's connection limit; imported facts files still do. A Builder workspace with Zendesk, Stripe and a vendor export was refused a third live connection.
  • Re-uploading a vendor's export moves its rows to the contract, period and connection of the new upload. Before, a row first uploaded under another contract kept that contract on every later upload, so a reconciliation could report "2 claims ingested" and count zero. Claim ids from CSV uploads are now scoped to the vendor as well as the vendor's row id.

[0.2.8] — 2026-10-13

Fixed

  • Overage is now actually billed. A monthly job (bill_overages, the 2nd of each month) invoices every paid workspace for the verified actions it recorded past its allowance in the previous month, at the plan's per-action rate, as a Stripe invoice item on the next subscription invoice; once per period, with an audit entry billing.overage_billed. Before this the allowance reported overage but nothing charged it.
  • Draft contracts are never counted: the contract limit applies at selection, so a Developer workspace can hold any number of drafts and two active contracts, as the pricing page says.
  • Viewers are never seats: the seat limit counts owners, admins and analysts, and a viewer invite is never refused for it.
  • PagerDuty, Jira and GitHub Checks are gated on their own incident_channels feature (Growth and above) rather than on Slack alerts, which start at Builder.

[0.2.7] — 2026-10-13

Changed

  • Plans: a Builder plan at $99 (2,500 verified actions, four connections, five contracts, Slack and the orphan report) sits between Developer and Team; Starter is renamed Team and rebalanced to 10,000 actions, eight connections and ten contracts (its starter id is kept for stored entitlements, Stripe metadata and trial links; STRIPE_PRICE_BUILDER is the new price setting). Developer gets two connections. Growth and Scale keep their prices with 15 and 30 connections and unlimited contracts. Overage is a straight ladder: 4, 3, 2 and 1 cents per action. Every paid plan may connect any core system; the count is the only limit. Custom action and outcome types start at Growth, the audit log export at Team.

[0.2.6] — 2026-10-13

Changed

  • Plans: OpenTelemetry ingestion is on every plan (the actions it records count toward the allowance like any other); Starter's two connections may be any core connector (Stripe, Salesforce, HubSpot, Zendesk, Intercom); the facts API and OpenTelemetry push endpoints no longer count as connections. A connector not in the plan answers 402 with the lowest plan that includes it. The docs now say plainly that recording and verifying actions never answer 402; only feature, connection, seat and contract limits do.

Added

  • COMPENSATED: an action recorded with compensates: "<action_id>" corrects an earlier one. The earlier action's verdict and hash never change; it carries resolution (compensating, then compensated once the correction settles PASS, or compensation_failed), its headline says so, GET /v1/actions?verdict=COMPENSATED lists it, the summary counts it and action.compensated is emitted. A compensation is itself verified.
  • Incident and review channels under Settings → Integrations (Growth+): PagerDuty (incident on FAIL, warning on UNKNOWN, resolved on PASS or compensation), Jira Cloud (one issue per failing action, comments on later verdicts) and GitHub Checks (a check run on the commit an action names, in progress while pending, updated on re-verification). Credentials are envelope-encrypted; deliveries run in the deliver_integration job with retries, and twenty consecutive failures disable the channel until it is saved again.
  • SDKs in TypeScript/JavaScript (sdk/js), Go (sdk/go) and Java (sdk/java) with the same surface as the Python SDK and its action wrapper, standard library only; all SDKs accept compensates.

[0.2.5] — 2026-10-13

Changed

  • Settlement waits for evidence, not for the clock. A verdict that is not yet a PASS stays pending after its window while a source the contract reads has a watermark behind the window, bounded by SETTLEMENT_MAX_LAG (48h). The re-check comes forward when that source syncs or its watermark is pushed, and otherwise runs every SETTLEMENT_RECHECK_INTERVAL (15m). The view, the list and the summary carry settlement.waiting_on; past the bound the verdict settles and names its stale_sources. POST /v1/facts accepts watermarks per source.
  • Every outbound notification about a verdict says whether it is pending or settled, why (reasons), what it waits on, the record that reversed it (reversal) and what it was before (previous, changed). A new action.pending event announces the first provisional verdict once; action.verified is unchanged in name.

Added

  • Connection trust: independent, shared or self_reported on every connection (migration 0003), reported per source and per fact in each verification and counted in summary.evidence_trust; PATCH /v1/connections/{id} sets it, per pushed source for the facts API. SECURITY.md §16 states the credential rule.
  • Weakening warnings: POST /v1/contracts/validate and POST /v1/contracts compare the version with the one it replaces and return every removed check, raised bound, extended window or shortened negative lookback; a saved weaker version emits contract.weakened and is counted in the audit entry.
  • POST /v1/contracts/draft: an action contract drafted from recorded actions and the facts observed around them, with the sample's shares and arrival times in the YAML comments. Nothing is saved.
  • Python SDK (sdk/python, standard library only): Client with record_action, push_facts (watermarks), verify, get_action, draft_contract, validate_contract, save_contract, and an @client.action decorator that records the call as an action before it runs, fail-open by default.

[0.2.4] — 2026-10-11

Changed

  • The monthly allowance no longer stops anything. POST /v1/actions, inline POST /v1/actions/verify and the OpenTelemetry endpoint used to answer 402 once the month's verified actions were used up, which made the verification layer itself the reason an action went unverified. Now every action is recorded and verified; the response and GET /v1/auth/me carry an allowance block (within, overage on paid plans at the plan's per-action rate, grace then exceeded on Developer), and the first action past the allowance is audited once as allowance.exceeded. public_pricing exposes overage_per_action per tier and the policy text.
  • Changing plan is one flow with a preview: GET /v1/settings/billing/preview?tier= says whether the choice starts a trial (and when the first charge falls), is charged today because the trial is used, or switches an existing subscription in place; POST /v1/settings/billing/checkout carries it out, with the trial days Stripe is given derived from the workspace's own trial rather than a flat 14. STRIPE_PRICE_<TIER> accepts a product id as well as a price id. Stripe's refusals and configuration gaps come back as messages (400/502), not 500s.
  • Enterprise no longer lists an optional recovery share on credited disputes: a fee tied to what the verifier finds is at odds with the independence policy. Fees are predictable and unrelated to any verdict.

Added

  • Off-site database backups: BACKUP_BUCKET names an S3-compatible bucket (reached with the S3_* credentials) that receives every nightly dump in addition to the copy in the object store; the job fails when the off-site copy cannot be written, both copies are kept 35 days, and the worker queues one backup at boot (once a day) so a newly configured bucket holds a copy at once. /health reports integrations.offsite_backups.
  • Second factor for privileged accounts: at every hosted sign-in the API mirrors the user's WorkOS authenticator enrolment into users.mfa_enabled and records the sign-in method on the session. Members enrol an authenticator app from Settings (POST /v1/auth/mfa/enroll and verify, through the WorkOS factor APIs; the secret is shown once and never stored). With REQUIRE_ADMIN_MFA=platform (platform admins) or admins (workspace owners/admins too), sessions without a second factor get 403 mfa_required on every product and admin route, while the session and enrolment endpoints still answer so the gate page can fix it in place; MFA_DELEGATED_METHODS (default SSO) lists methods whose identity provider enforces the factor. GET /v1/auth/me carries mfa: {required, satisfied, method}. /health reports integrations.admin_mfa.
  • Sign-out ends the hosted session as well: the WorkOS session id travels in the session token (through workspace switches and impersonation) and POST /v1/auth/logout returns the provider's logout_url, which brings the browser back to the site. Before, sign-out cleared the cookie and landed on the sign-in page, which with a live provider session signed the person straight back in.

[0.2.3] — 2026-10-09

Added

  • Three action-contract primitives that look beyond one action, each domain-free (source + type + attribute + window + number):
    • aggregate_exceeds in a new action.aggregate_limits block: the sum of an effect attribute, or the count of effects, across the actions in scope (per agent, per subject, or across the contract) over a lookback.
    • effect_attr_differs in forbidden_effects: an observed value must equal the action's parameter, or the latest state before the action plus or minus a parameter, within a tolerance. No reference, no violation.
    • reversal_observed in a new action.reversed_by block (default lookahead 30d): a later record that undoes the action fails both lenses, and pushing such a fact after settlement re-verifies the action (verify_action job, one per fact) and emits action.verified again.
  • Verification responses carry two new check groups, aggregate_limits and reversals; measured values (totals, expected values) are part of the evidence and therefore of the verdict hash.
  • Library contract payout_workflow showing all three, and three library contracts outside money that use the same checks: deploy_workflow (CI, monitoring, change records; a rollback is the reversal), access_grant_workflow (identity provider, HR, tickets; a removed membership or a departure is the reversal) and appointment_reschedule_workflow (scheduling, record system, messaging; a cancellation or no-show is the reversal).
  • Agent sandbox: a sixth workflow, send a payout, against a fifth fake system (ledger). Besides the eleven injected failures it has five cases of its own: over limit, frozen account, per-agent daily total exceeded (three earlier payouts run first), paid amount differing from the request, and a payout returned after the verdict settled (re-verified to FAIL at the re-check). Sandbox agent ids are per run.
  • Agent sandbox: a seventh workflow, deploy a service, against a sixth fake system (platform: services, deploys, health checks), with five cases of its own: degraded service, environment not allowed, more than three deploys an hour, unhealthy after the deploy, and a rollback after the verdict settled.

Fixed

  • Verified actions are metered on the Verify path: a new action_id on POST /v1/actions or a non-dry-run POST /v1/actions/verify counts against the plan's monthly allowance and answers 402 once it is used up; an OTel batch is refused once the month is used up. Re-recording an action, dry runs, verifying by id and sandbox runs are never metered.
  • GET /v1/actions/orphans is gated to Growth and above (orphan_report feature), as the pricing page says. Flags added to pricing after a tenant was seeded now resolve from the plan's defaults.
  • GET /v1/public/sample-statement returns the latest demo statement that has billed claims; the monthly schedule also writes an empty statement for a period with no claims, which has no verdict chain to show.

[0.2.2] — 2026-09-29

Added

  • Agent sandbox for every workspace: GET /v1/sandbox/scenarios, POST /v1/sandbox/runs run a scripted scenario inside the workspace (namespaced records, channel: sandbox, never metered). sandbox.harness.run_scenario is the shared core used by the benchmark and the endpoint.
  • GET /v1/overview gains verify_onboarding: progress through the guided first-verdict flow (sample action, API key, first own action, action contract chosen).
  • Hosted sign-in is locked to production (DEV_LOGIN_KEY), GET /v1/auth/providers, and sign-up creates the workspace on the plan picked on the pricing page (Developer by default).
  • links on POST /v1/facts and POST /v1/actions/verify: two record ids that are the same case (a support ticket and its order). Effects on a linked subject count for the action's subject; linked subjects are never orphans.
  • docs/benchmark.md: three real agents (Claude, GPT, Gemini) against the sandbox failure matrix, with a trace-only judge as the baseline; founder-only runner POST /v1/admin/benchmark.

Changed

  • Pricing (app/billing/pricing.py): plans are sized by verified actions per month (a billed outcome in Reconcile counts as one). New free Developer tier (1,000 actions, 2 contracts, facts API, webhook, no trial, no card) for engineers adopting action contracts; Starter gains a second connection and three contracts; Growth ten contracts and the orphan-effect report; Enterprise adds committed-use pricing and a shadow-mode enforcement preview. Tier ids and limit keys are unchanged, so stored entitlements keep working. The public pricing payload gains unit, and per-tier for and free.
  • Benchmark replay (--replay, replay_of) re-verifies a recorded run on the current engine without model calls.

Fixed

  • A notification or CRM note keyed to the ticket instead of the order was reported as a missing effect and as an orphan (found by the gpt-5-mini benchmark run).

[0.2.1] — 2026-09-28

Added

  • sandbox/: an artificial enterprise (payments, orders, CRM, tickets), failure injection, a scripted agent, five action contracts and a 62-scenario failure matrix scored against a database oracle (docs/sandbox.md).
  • GET /v1/actions/orphans?contract_id=&period=: effects observed on subjects with no recorded action.

Changed

  • Action contracts no longer fall back to no_structured_signal (UNKNOWN); an asserted action with no observed effects is a FAIL naming every missing effect.

[0.2.0] — 2026-09-28

Added

  • Action contracts (kind: action): an action: block (window, preconditions, limits, expected_effects, forbidden_effects, unverifiable_if) compiled into the two engine lenses, presented as outcome and policy with PASS / FAIL / UNKNOWN labels. New primitives effect_observed, effect_count_exceeds, effect_attr_exceeds, state_before_action / precondition_unmet, param_exceeds (param_max), param_not_in (param_in); reason codes that name the effect or parameter (expected_effect_missing:<source>.<type>, duplicate_effect:…, forbidden_effect_observed:…, precondition_unmet:…, limit_exceeded:<param>, param_not_allowed:<param>). Reference: docs/action-contracts.md.
  • Synchronous action verification (app/engine/actions.py): actions stored as claims, single-claim SQL scope in the compiler, both lenses evaluated with evidence and hash chain under a Run with triggered_by=action; settlement window with pending status and a verify_action job that re-checks at settles_at; webhook/Slack event action.verified.
  • API: POST /v1/actions, POST /v1/facts, POST /v1/actions/verify (inline action and facts, dry_run), GET /v1/actions (filters contract_id, workflow_id, verdict, period), GET /v1/actions/summary, GET /v1/actions/{action_ref}, public GET /v1/public/sample-action. Public definitions expose kind, lens_labels, action_type, window and per-section rule lists for action contracts.
  • Library contract refund_workflow (internal refund policy template), synthetic demo actions including the showcase FAIL case act_demo_refund_001, and tests/test_actions.py.
  • CLI: ledger actions verify --tenant <slug|id> --file body.json [--dry-run].
  • Migration 0002_contract_kind: contracts.kind column and raw_ref indexes on claims and facts.

Changed

  • Product renamed CommitLayer (commitlayer.ai), positioned as the verification layer for enterprise AI actions; outcome-billing verification (statements, reconciliation, disputes) is presented as the CommitLayer Reconcile module. Placeholders replaced across code, contracts and docs; {{LEGAL_ENTITY}} remains.
  • available_sources now includes connection source names and the systems that pushed facts through the API, so missing_fact_source can name systems directly (stripe, shopify).
  • Docs: docs/API.md (Actions section), METHODOLOGY.md (section 13, action contracts), README.md (positioning, quickstart, CLI), DECISIONS.md (D18, D19), docs/TRUST_CENTER.md.

[0.1.0] — 2026-09-11

Added

  • Outcome Contract DSL (YAML → AST → SQL) with 20 primitives, SQLite and DuckDB dialects, golden fixtures and property tests.
  • Verification engine: two lenses, fixed evaluation order, evidence pointers, per-statement hash chain, deterministic statements, coverage disclosure.
  • Starter contract library with cited vendor definitions: Intercom Fin (resolution, procedure handoff), Zendesk AI agents, Sierra, Decagon, Salesforce Agentforce, HubSpot Breeze customer agent, generic CSV, internal OTel.
  • Statements (HTML/PDF/CSV/JSON), public hash verification, definition delta reports.
  • Invoice import (CSV/XLSX/PDF with mandatory review), reconciliation, dispute packages with status tracking.
  • Transcript assist: redaction, env-configured provider, cost caps, calibration sampling, 30-day excerpt purge, statement disclosure.
  • Connectors: Intercom, Zendesk, Salesforce, HubSpot, Stripe, generic uploads, OTLP/HTTP ingestion; envelope-encrypted credentials; HMAC-hashed identifiers.
  • Multi-tenant API with roles, entitlements driven by a single pricing config, API keys, webhooks, Slack, audit log, counterparty surface, admin surface.
  • SQLite job queue + worker + schedules (daily syncs, monthly statements, purges, nightly backups).
  • Synthetic demo tenant, ledger CLI, Docker image, Compose, Railway config, CI.