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.mdrestructured 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, plusCHANGELOG.mdand anllms.txt. The site renders a page per document at/docs/<slug>, a/changelogpage, 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}computedchain_validonly 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/billingcarries 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'sorders_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. Factsorder_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.mdcarries 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-tenantexisted, 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_actionis the one write, marked non-destructive. The annotations must agree with the tool's scope, andtests/test_mcp.pychecks that they do. Required for a listing in the Claude connectors directory, where a tool without them is flagged. product-catalog/mcp-tools.yamlnow carries each tool's title and annotation-derivedread_onlyanddestructiveflags.docs/mcp.md: the connector-directory listing (texts, use cases, authentication and data-handling answers, reviewer instructions).
Fixed
tests/test_metering.pypinned 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 byscripts/export_product_catalog.py(withopenapi.jsonandmcp-schema.jsonunder--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.pyand theknowledge-impactworkflow fail when the committed catalog differs from what the code exports, andscripts/check_docs_impact.pyfails 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 withdocs-impact: none - <reason>in the description). knowledge-dispatch: after a push to the production branch that touches product facts, acommitlayer_knowledge_updaterepository 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)andwebhook_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" withupgrade_toset 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 reportssucceeded_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)andclaims(tenant_id, created_at); the first-action check reads two rows instead of counting. GET /v1/admin/metricsscanned every run of the week, with a JSON extract per row, on each call. Migration 0016 addsruns(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 forstripe payand the Treasurysend_moneytool: the transfer must exist for the intended recipient, once, for the amount requested, reachposted, 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 laterpostedorreturnedlands. 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_atand theemail_verificationstable (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: Passwordfor 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_emailwith no session. Auditauth.verification_sent(orauth.verification_throttled). POST /v1/auth/verify-email { token }→ theSessionthe sign-in withheld (400 invalid_verificationwhen unknown, used or expired;403 account_heldfor a held workspace, whose address is still marked verified). Auditauth.email_verified, thenauth.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
/signupdo the same.
Changed
- The founder's welcome waits for the click on a password sign-up (
on_signup(welcome=False), thenon_email_verified), with a newemail_verifiedperson 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_emailtakes 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_secondsno longer counts a connection with no stored credential or one waiting on re-authentication: the daily sync cannot move its watermark until someone acts, andconnections_in_erroralready says so. Counting them turned a customer's expired token into a permanent platform alert.sync_lag_seconds.excludedreports how many were left out and why.
Added
stalest_connectionsonGET /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 firesuser_signed_upon the person (so the anonymous visitor the site identified becomes this user), queues the founder's welcome email (withWELCOME_REPLY_TO), and queues alifecycle_contactjob 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}andsecond_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 webhooksubscription_started {tier, from}andplan_upgraded {from, to}(a downgrade is not reported). send_emailtakes 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 underMASTER_KEY; idempotent; refuses to write anything when a key unwraps under neither value.ledger verify-master-key: every key unwraps underMASTER_KEYalone 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_withinprimitive 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-Afteris 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
stateis 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_mismatchaudited). 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/handofftrades it for the token server-side (auth_handoffs, hashed). The redirect also honours thenext=the sign-in started with, which was lost before. - M8, token-type confusion.
verify_sessionaccepts only asessiontoken with a subject; another token the same key signs answers401instead of a500. - M2, open routes unthrottled.
/v1/auth/*,/v1/public/*and/v1/oauth/*are limited per client address (ANON_RATE_PER_MINUTE, default 60;429withRetry-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
safeInternalPathguard for everynextand back-link://host,/\hostand 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 asinf/nanor 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
developmentandtest, the API refuses to start with aSESSION_SECRETorHMAC_PEPPERstill at the dev default or under 32 characters, or an emptyMASTER_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
roleorkind, so a free-form config no longer exempts a connection from the plan's count. - L1 and L7.
/openapi.jsonis off in production;/healthisstatus,version,engine_versionanddb(okorerror) only; the integrations map and the database error text moved toGET /v1/admin/metrics. - L2. The container runs as an unprivileged user: the entrypoint, still root, hands the data volume to
appand drops privileges withsetprivbefore starting the server. - M6.
python-multipart>=0.0.18andstarlette>=0.47.2as 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, atSIGNUP_REPUTATION_MIN_CONFIDENCE); and the client's country from a local GeoLite2 database againstSIGNUP_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 statusheld; sign-in and every product route answer403 account_held("waiting for a review") until an operator approves it.signup_refusalsrecords every refusal and hold.- Admin:
GET /v1/admin/signup-refusalswith 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_geoipjob: fetches the GeoLite2 country database withMAXMIND_LICENSE_KEYon 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_heldjoindisposable_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-assistwithenabled: trueanswers402 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 (statusmanual) and the synthetic demo are exempt.GETcarriesavailableandwaiting_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.paidwithamount_paid > 0recordstenants.first_paid_atonce, audited asbilling.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_errorin 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}/testdelivers 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 withinclude_archived=true; a retry un-archives it. Dead outbound deliveries stay on the seven-day hard delete, since each delivery is on record inwebhook_deliverieswith 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_jobskeeps a dead outbound delivery (webhook, integration, alert, email) for seven days and any other dead job for thirty; succeeded jobs are untouched. Migration0009removes 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 toAPI_KEY_TOUCH_SECONDSand 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/signupcreated 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 inX-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/configand their kind) answer 404 and ban the address;/robots.txtdisallows 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 lastsBAN_HOURS(24), lives in the database so every API process honours it, never applies to a non-public address (the proxy, health checks) or toBAN_EXEMPT_IPS, and is refused with403 blocked. Platform admins list and lift bans atGET/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 withDISPOSABLE_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 thatGET /v1/admin/metricsaccepts 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_ordersbut notread_customersmade Shopify answerACCESS_DENIEDon thecustomer { 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 withcustomer_hidnull (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
0007moves 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_exceededand 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:
requestsa minute on every authenticated call andwritesa minute on recording, verifying and pushing facts, as in-process token buckets;429 rate_limitedwithRetry-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 inapp/billing/pricing.pyand on the public pricing endpoint;RATE_LIMITS=falseonly 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 withmax_redemptionsas the cap. Every Builder checkout or switch carries it while seats remain; the public pricing endpoint reports the seats left from Stripe'stimes_redeemedand 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/pricingcarriessupport,rate_limitsandfair_useper tier,grace_actions, thefoundingoffer,included_everywhereand thecomparetable 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, Zendesktitle: 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 with400 INVALID_FIELDand stored nothing (the Case part had already run). The customer on an opportunity is its primaryOpportunityContactRole; the query now reads that role'sContact.Emailthrough 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-keyrecords the agent's actions,--evidence-key(bound to the sandbox's six sources) pushes the facts, and the full--api-keyonly creates the sandbox contracts and asks for verdicts. With a full key alone everything still runs and the result sayscredentials: 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 onPOST /v1/actions/verifyoutside a dry run, require it;actions:recordno longer implies it. Recording (POST /v1/actions, an inline action on verify) requiresactions:record. A key without the scope answers403 insufficient_scope. - Existing
recorderkeys 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 oldevidence),full.GET /v1/settings/api-keysreturnspreset_descriptions. - A key with
facts:writeis bound to the sources it may push for (fact_sourceson creation, required unless the key holds every scope). A fact or watermark for another source answers403 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 | independenton 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 iseffect_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/actionswith an existingaction_idis a no-op when it describes the same action and answers409 conflictwhen any of contract, subject, parameters, workflow, agent, type, channel, requested time orcompensatesdiffers. 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'srequested_atfurther ahead thanMAX_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
localhostor.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, withevidence_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 infacts_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/actionsandGET /v1/actions/summaryloaded 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 averdictorworkflow_idfilter 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 withinWRITE_SLOT_TIMEOUT_SECONDS(2) answers 503busywithRetry-After: 1without 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/actionsrecords 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, averify_actionjob picks it up. No action is recorded-but-never-verified.- Two in-flight retries of one
action_idcould 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_aton every call (that one line was most of the lock failures); it is refreshed at most everyAPI_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: 1and error codebusy, never a 500: the write lock waits at mostSQLITE_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 mostMAX_IN_FLIGHT_REQUESTS(64) at once. Requests slower thanSLOW_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_databasejob: 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 becomeorder_statusstate facts andorder_cancelled,refund_createdandfulfillment_createdevent 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/waitForSettlementin the Python and JS SDKs loop over it. Auditaction.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.mdsection 6. - Temporal and Inngest integrations for Python (
integrations/python, distributioncommitlayer-integrations): a Temporal activity interceptor that records a mapped activity as an action before it runs (idempotent ontemporal:{workflow_id}:{activity_id}, fail-open) and acommitlayer_verifyactivity that waits on the completion gate with heartbeats; an Inngestrecorded_stephelper that records inside the memoised step and await_for_verdictstep loop. The activity or step result is never sent. 61 tests against fakes; CI runs them. - The same for TypeScript (
integrations/js,@commitlayer/integrationswithtemporalandinngestsubpath exports): an activity interceptor factory, acommitlayerVerifyactivity,recordedStepandwaitForVerdict. 34 tests. Both integrations wait through the completion gate rather than polling plain reads. - SDKs:
contract_version/contractVersionon record, and a publicrequest(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 theactions:recordscope, 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 asmcp.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 withoutactions:recordacts 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,
/authorizewith PKCE handing off to the web app's consent page,/tokenwith 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 endpointsGET/POST /v1/oauth/consent; auditoauth.approved,oauth.denied. - The MCP recorder,
recorder/python(commitlayer-mcp-recorder) andrecorder/js(@commitlayer/mcp-recorder): a stdio JSON-RPC proxy that re-exports any downstream MCP server and records a mappedtools/callas 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
mcpSDK dependency (pinned to the 1.x line; 2.x renamed FastMCP). New tablesoauth_clients,oauth_codes,oauth_tokens;api_keys.scopes(migration0004_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 actionsreconciliation.*.
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_sourcerule 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: nulland 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 entrybilling.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_channelsfeature (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
starterid is kept for stored entitlements, Stripe metadata and trial links;STRIPE_PRICE_BUILDERis 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 carriesresolution(compensating, thencompensatedonce the correction settles PASS, orcompensation_failed), its headline says so,GET /v1/actions?verdict=COMPENSATEDlists it, the summary counts it andaction.compensatedis 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_integrationjob 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 acceptcompensates.
[0.2.5] — 2026-10-13
Changed
- Settlement waits for evidence, not for the clock. A verdict that is not yet a PASS stays
pendingafter its window while a source the contract reads has a watermark behind the window, bounded bySETTLEMENT_MAX_LAG(48h). The re-check comes forward when that source syncs or its watermark is pushed, and otherwise runs everySETTLEMENT_RECHECK_INTERVAL(15m). The view, the list and the summary carrysettlement.waiting_on; past the bound the verdict settles and names itsstale_sources.POST /v1/factsacceptswatermarksper 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 newaction.pendingevent announces the first provisional verdict once;action.verifiedis unchanged in name.
Added
- Connection trust:
independent,sharedorself_reportedon every connection (migration 0003), reported per source and per fact in each verification and counted insummary.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/validateandPOST /v1/contractscompare the version with the one it replaces and return every removed check, raised bound, extended window or shortened negative lookback; a saved weaker version emitscontract.weakenedand 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):Clientwithrecord_action,push_facts(watermarks),verify,get_action,draft_contract,validate_contract,save_contract, and an@client.actiondecorator 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, inlinePOST /v1/actions/verifyand the OpenTelemetry endpoint used to answer402once 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 andGET /v1/auth/mecarry anallowanceblock (within,overageon paid plans at the plan's per-action rate,gracethenexceededon Developer), and the first action past the allowance is audited once asallowance.exceeded.public_pricingexposesoverage_per_actionper 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/checkoutcarries 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_BUCKETnames an S3-compatible bucket (reached with theS3_*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./healthreportsintegrations.offsite_backups. - Second factor for privileged accounts: at every hosted sign-in the API mirrors the user's WorkOS authenticator
enrolment into
users.mfa_enabledand records the sign-in method on the session. Members enrol an authenticator app from Settings (POST /v1/auth/mfa/enrollandverify, through the WorkOS factor APIs; the secret is shown once and never stored). WithREQUIRE_ADMIN_MFA=platform(platform admins) oradmins(workspace owners/admins too), sessions without a second factor get403 mfa_requiredon 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(defaultSSO) lists methods whose identity provider enforces the factor.GET /v1/auth/mecarriesmfa: {required, satisfied, method}./healthreportsintegrations.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/logoutreturns the provider'slogout_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_exceedsin a newaction.aggregate_limitsblock: 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_differsinforbidden_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_observedin a newaction.reversed_byblock (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_actionjob, one per fact) and emitsaction.verifiedagain.
- Verification responses carry two new check groups,
aggregate_limitsandreversals; measured values (totals, expected values) are part of the evidence and therefore of the verdict hash. - Library contract
payout_workflowshowing 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) andappointment_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_idonPOST /v1/actionsor a non-dry-runPOST /v1/actions/verifycounts against the plan's monthly allowance and answers402once 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/orphansis gated to Growth and above (orphan_reportfeature), 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-statementreturns 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/runsrun a scripted scenario inside the workspace (namespaced records,channel: sandbox, never metered).sandbox.harness.run_scenariois the shared core used by the benchmark and the endpoint. GET /v1/overviewgainsverify_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). linksonPOST /v1/factsandPOST /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 runnerPOST /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 gainsunit, and per-tierforandfree. - 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): anaction: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 primitiveseffect_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 withtriggered_by=action; settlement window withpendingstatus and averify_actionjob that re-checks atsettles_at; webhook/Slack eventaction.verified. - API:
POST /v1/actions,POST /v1/facts,POST /v1/actions/verify(inline action and facts,dry_run),GET /v1/actions(filterscontract_id,workflow_id,verdict,period),GET /v1/actions/summary,GET /v1/actions/{action_ref}, publicGET /v1/public/sample-action. Public definitions exposekind,lens_labels,action_type,windowand per-section rule lists for action contracts. - Library contract
refund_workflow(internal refund policy template), synthetic demo actions including the showcase FAIL caseact_demo_refund_001, andtests/test_actions.py. - CLI:
ledger actions verify --tenant <slug|id> --file body.json [--dry-run]. - Migration
0002_contract_kind:contracts.kindcolumn andraw_refindexes 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_sourcesnow includes connection source names and the systems that pushed facts through the API, somissing_fact_sourcecan 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,
ledgerCLI, Docker image, Compose, Railway config, CI.