Sabline 8.8.0

Changelog: 8.x: 8.8 to 8.7.0

Every release of this major version, newest first, as CHANGELOG.md records it.

8.8 - The three outside 9.0 #

decisions/0007 accepted three items as work that gates no rung of 9.0 and can be done at any time. This release is those three and nothing else: no rule of the language changed, no error code was added or removed, and no default moved. release_checks.py covered v8.7.1 reports that nothing STABILITY.md covers changed.

A claim two documents made, and no test held: the MCP server's confinement. docs/confinement.md says confinement is on by default "on the command line, in sabline.run(timeout=...), in sabline.Pool, on the HTTP door, on the MCP server and under sabline eval", and THREAT_MODEL.md's confinement row says the same of both doors, naming check_confine.py as the evidence. Six places were claimed. Five were asserted: check_confine.py covered the command line, run(timeout=), a Pool, sabline eval through _EvalPool, and the HTTP door - for which it starts a real sabline serve child with the fault-injection hook in its environment, posts a request that asks for no confinement in every field a request could carry it in, and holds the answer to E319 at the platform's own level. The MCP server had no equivalent, and the suite's own docstring listed only "run(timeout=), a Pool and the HTTP door".

It has one now, written the same way: a real sabline mcp child with the hook in its environment, one sabline_run call carrying every no-confinement field that request carries - confine: false, no_confine, no-confine, and --no-confine among the program's own arguments - and E319 at this platform's own level coming back, with the write the hook attempted never landing.

The claim was true. sabline_mcp.py sets CONFINE = True and builds both its pool registry and its checker with it, and the test holds that behaviour on each of the three platforms check_confine.py runs on - no release happens unless it passes on every one. Nothing was wrong, so no advisory follows; what was missing was the test. A second case makes the first one falsifiable: the same tool call against a server started with the operator's own --no-confine lets the write through at level none, so a green first case is green for the reason intended. decisions/0007 asked for exactly that, having warned that a test which cannot fail is worse than no test, because it is counted. The suite's docstring now lists all six places, and THREAT_MODEL.md's evidence cell names both doors rather than "a door's request".

A capability predicate now says how its evidence was arrived at. A Statement of https://sabline.dev/capability/v1 carries a sabline.audit/1 document, and a verifier reading one had no way to tell whether that evidence came from reading the source or from watching a run. The two bound different things: an audit says what a program can touch, a receipt (.../receipt/v1) says what one run did, and a run that did not reach a host is not a program that cannot. Raised on in-toto #594.

predicate.analysisMode answers it, with static the only value defined: the audit was read from the source - its declared types and effects - before the program ran. sabline attest writes it; the predicate's schema admits that value and no other; and because every predicate of this type carries an audit, a producer writes static or leaves the field out. No second value is defined, because no predicate of this type has earned one, and a value written for a predicate that is still an audit would make the field worse than its absence.

The field is additive within version 1, so no v2 is needed and a consumer that does not know it ignores it. A consumer must not read the field's absence as static: a Statement signed before this release has no field and says nothing here either way. The repository's own two consumers

  • policies/opa/capability.rego and the Kyverno policy - ask for it nowhere, which check_policies.py now asserts, so a Statement from a producer before this release is still admitted.

sabline-spec 0.15.0 defines it (section 8.5), and sabline attest's specification says that version. The same field is in the in-toto draft on the capability-predicate branch of gowrishankar-infra/attestation, where this predicate type is proposed as a generic one.

sabline new writes an AGENTS.md, and sabline card --agents prints it for a project that already exists. The primer says three things: read sabline card for the language, how a budget works, and write the program before reading the data. It is short on purpose and repeats nothing the card says, because a copy of it lives in someone else's repository where no check here can reach it; it carries the version it was written from, and templates/AGENTS.md is the one source both the command and the scaffold read, so the two cannot disagree. A build that ships no templates/ - the downloadable executables carry no LLM.md either - says on stderr that it wrote no primer rather than quietly writing two files where it printed three.

And a guide for the rule the primer states: write the program before you read the data. It is the fourth step of decisions/0007's item 34, and what it explains is which half of the problem the ordering is for. Not the door: a budget is matched against a value at the door, whichever door it came through, and over AgentDojo's four suites 238 programs that build an argument at run time out of what a read returned were run against three budgets each and decided identically to the literal program in all 714 runs. What the ordering is for is the budget itself - a program that takes its destination from what it read names no host, so the narrowest budget sabline audit can derive for it grants every host, and an operator who narrows it to the host that program wants is narrowing it to the attacker's. The guide says plainly what is not covered: Sabline has no per-value provenance, nothing refuses a program for having been written late, and the audit's any flags are the nearest signal there is.

check_differential.py against v8.7.1: none of the 97 examples' outputs differs, sabline-spec's 456 conformance cases give the same verdicts, and the quick benchmark's 20 programs the same verdicts. Nothing in the runtime's path through a program changed, which is what that measures.

MeasureResult
Cold start, sabline --version276 ms
Cold start, sabline check of a one-line file710 ms
Check, 1,013-line program1.344 s per 1,000 lines (0.081 s without proofs)
Check, 10,013-line program1.390 s per 1,000 lines (0.075 s without proofs)
Proof time per example with contracts (z3), p50 / p9524 ms / 598 ms over 56 files
JIT on examples/bench.vel: compile (+ llvmlite import)88 ms (+ 77 ms), burn compiled
Pure numeric, examples/bench.vel: native / interpreted5.87 s / 13.75 s, 2.34x (native code: burn)
Pure numeric, integer loop: native / interpreted4 ms / 3.31 s (native code: spin)
Pure numeric against v8.7.1, native5.88 s against 5.73 s, +2.6% (the gate allows +25%)
Pure numeric against v8.7.1, interpreted17.06 s against 16.49 s, +3.5%
--lite build sizethere is none
Pool worker RSS, 1 run -> 1,000 more26.7 MB -> 27.7 MB (+1.0 MB)
z3 / llvmlite imported by --version; by check with no contractsneither; neither
Importing z3 when a command needs it+216 ms at cold start
Importing llvmlite when a command needs it+223 ms at cold start

Measured on the machine MAINTENANCE.md names, Python 3.13, 16 CPUs, with 21% of them busy over the second before measuring: wall-clock figures, and noisy to that extent. The release's perf job runs the pure-numeric gate against v8.7.1 on its own runner, where the 25% limit is enforced.

api: tests/api/golden.json gains --agents in cli.commands.card and in cli.flags. One flag is added to one command - sabline card --agents prints the AGENTS.md primer instead of the language card - and nothing was removed, no signature changed, and no return type changed. sabline card with no flag prints what it always has. The command now refuses a flag it does not have, and a word after --agents, with exit 2 and a usage line, where before it ignored anything after card.

8.7.1 - The catalogue, checked #

Every incident summary is published, and so is the flagship. The catalogue has withheld all fifteen of its summaries since it was written: each entry said verified: false, so build_incidents.py counted it, named it, linked its sources and published the part a machine checks - the verdict it re-runs - while the part only a person can check, the account of what happened, stayed in the repository. On 2026-10-01 all fifteen were read against their sources by a person: eleven were approved as written and four were changed first. incidents/REVIEW.md is that decision, with a date and a list behind it; incidents/FACTCHECK.md now says where its flagged rows ended up rather than reading as outstanding work.

What this adds to the site: fifteen summaries on the catalogue, a page for each incident under the name people use for it, and docs/replayed.md, the flagship, which needed every entry checked because it summarises all of them - We examined 15 real incidents and replayed 8 in Sabline.

Four entries were corrected before their flag was set. Two cited a report only at second hand: tj-actions/changed-files now cites StepSecurity's report directly, which the Wiz post repeated, and Ultralytics cites William Woodruff's analysis, which the summary already named by way of PyPI's post deferring to it. Amazon Q said aws s3 rb in one place where the payload used aws s3 rm - rb removes a bucket, rm removes objects. And EchoLeak rested on a page that has answered HTTP 403 since August 2025: it now rests on Reddy and Gujral, arXiv:2509.10540 (AAAI Fall Symposium Series 2025), with Aim Labs kept as an archived copy and described as one, and its summary says only what that paper supports - which cost it three claims the paper does not make.

A link in an entry pointed outside the site once the entry was published. An entry links the rest of the repository the way its own directory sees it, and incidents/tj-actions-changed-files/incident.md named ../../docs/known-open.md. Copied onto a page under docs/, that path leaves the site, and build_docs.py refused it - a defect no release could have hit before, because no entry had ever been published as a page. build_incidents.py now rewrites such a link to the form the site uses for its own pages.

Nothing in the runtime, the compiler or the command line changed: release_checks.py covered v8.7.0 reports that nothing STABILITY.md covers moved, and tests/api/golden.json is unchanged.

8.7.0 - Evidence #

check_api.py counted a pre-release tag, and asked for the api: line in the wrong place. tests/api/golden.json is the Python package's surface, and a pre-release ships the sabline-rt crate and nothing else - the six version files do not move, and the golden does not either. This suite's own newest-tag lookup counted one all the same, and parse_version reads v9.0.0-alpha.1 as (9, 0, 0), so from that tag on every ordinary release read as older than the newest tag: the suite looked for the api: line above the CHANGELOG's first entry instead of in the entry being released. 8.7.0 is the first ordinary release since 9.0.0-alpha.1 and met it - release_checks.py gate said release: 8.7.0 with the line in its entry, where RELEASING.md puts it, while this suite asked for it somewhere else. It went unseen because test.yml checks out shallow, with no tags at all, so the lookup returned None and the case passed trivially on every leg. It now skips pre-release tags, which is the rule release_checks.prerelease_gate already had for the same reason, and says the golden differs from v8.6.0's.

A check stopped at its own ceiling was read as a crash. check_hostile.py's seventh case builds a valid 10,000-line program and holds check, audit, fmt --check and run over it to "no traceback, exit 0 or 1". A check and an audit have a ceiling of their own - 60 seconds, CHECK_TIMEOUT_DEFAULT - and on a slow enough machine a program that size reaches it: the answer is E613 and exit 2, which is the designed answer and not a crash. The case called it a failure. It fired for the first time on this release, on the macos-15-intel / Python 3.10 / full leg, where the whole suite took 244 s against 67 s on a developer's machine and the check took 60.4 s against a ceiling of 60; audit took 59.7 s on the same machine and passed, which is how narrow it was. The case now accepts E613 and E614 from the two commands that have a ceiling - and from neither of the two that do not - and still fails on a traceback, on any other exit, and on any other code. Cases beside it force the ceiling with --check-timeout 1 and pin that branch, so it is exercised on every machine and not only on one slow enough to find it by accident - and they force it with a map literal nested 22 deep, which stalls the type checker, not with the big program, which is only slow because of the prover. A minimal install has no prover: it checks those 1,168 functions, proves nothing, and finishes inside a second, so a pin written against that program passed every full leg and failed every minimal one.

A page the site had stopped writing stayed at the top of it. build_docs.py stages latest/ and the release's major.minor/ and swaps each into place, so each holds exactly what that build wrote. The top is written where it stands - it has to be, because every earlier release's directory lives under it - and nothing swept it, so a page the generator wrote once and stopped writing stayed there: served, in no sitemap.xml, linked from no page, and still titled with the version that wrote it. The changelog is split to the page budget and re-split on every release, and this entry packed the 8.x group into three pages where 8.6.0 needed four, which left changelog-8-4.html at the top saying "Sabline 8.6.0". The build now takes such a page away and says which, and check_site.py plants one at the top and one below it and fails if either survives - and plants a page without the generator's own <meta name="sabline-version">, and an earlier release's directory, and fails if either is touched. The one thing that still names the retired address is docs/8.6/changelog-8-4.html, an archive no build rewrites, in its canonical link and its og:url; those are hints in a <head> and not a link a reader can follow, so check_docs.py excuses them there and nowhere else - not a visible href in an archive, and nothing outside one.

api: tests/api/golden.json's http.log changes shape. It was a list of the shape of each line of sabline serve's invocation log, in the order the lines were in the file; it is now an object keyed by the request that produced the line, holding that line's endpoint and outcome as values and the shape of the whole line. Nothing the HTTP door offers changed, and no line of the log changed: this is what the suite records, not what the door does. The reason is that the door is a ThreadingHTTPServer and writes its line after it has answered, so the order of the file was a race the old golden depended on - it failed once on a Windows leg in 8.4 and again on #97, and never reproduced. check_api.py now waits for each request's line before making the next one, and terminates the door only once its last line is in. The new form also catches things the old one could not: which endpoint a line names, and which outcome, are now part of the golden.

An incident catalogue, in incidents/ and on /incidents.html. Fifteen publicly reported incidents from 2023 onward in this project's lane - AI agent failures and supply-chain attacks where code ran with more authority than it should have - each with the primary report it was written from, and a verdict on whether a program of the same shape is refused: STOPPED, PARTIAL, NOT COVERED, UNVERIFIED, or OUT OF SCOPE where nothing ran. A STOPPED or PARTIAL entry carries a real program, the command, the refusal the runtime printed and the receipt, recorded by incident_evidence.py and re-run by check_incidents.py on every push, so no release is made from a tree where one has stopped refusing. It is not a claim that adopting Sabline would have prevented any of the real events, and every entry says which part of its incident is outside Sabline entirely; the seven new rows in the known-open table are where those sentences went, each now naming the milestone that would close it or saying nothing will. A "How these were chosen" section states the window, the lane and the primary-source rule, and that this is a selection and not a survey. Every summary carries verified: false until a person has checked it against its sources, and build_incidents.py will not publish one that does. One candidate, an agent that deleted a production database (Replit, July 2025), was dropped for lack of any primary source - its record is participants' posts and the news that quoted them - and incidents/FACTCHECK.md keeps every source's evidence, row by row, behind the summaries.

tj-actions-changed-files gains a Linux-only evidence step: under full confinement (8.4) the runtime is refused a read of another process's memory and the run ends with E319. A step may now name platforms; it runs and is checked only where it applies, held to its assertions and recorded in no refusal.txt, so a refusal one operating system makes and another does not is evidence here without making the recorded bytes differ from machine to machine.

The known-open table has a page of its own, docs/known-open.md, with the crosswalk's section that lands each of its rows on a framework's controls. Those seven rows took threat-model.html and crosswalk.html past the 100,000-byte page budget, and the table grows every time a gap is recorded honestly, so it was moved rather than trimmed and the budget stays where it was. THREAT_MODEL.md keeps its Known open heading, which links to the page, and every word of the table and the section is where it was moved to.

Categories 16 to 20 are new in this release, so v8.6.0's benchmark has no program of that name and nothing to compare a verdict against.

differential: 16a - a program of benchmark category 16, new in 8.7, so v8.6.0 has no verdict for it differential: 17a - a program of benchmark category 17, new in 8.7, so v8.6.0 has no verdict for it differential: 18a - a program of benchmark category 18, new in 8.7, so v8.6.0 has no verdict for it differential: 19a - a program of benchmark category 19, new in 8.7, so v8.6.0 has no verdict for it differential: 20a - a program of benchmark category 20, new in 8.7, so v8.6.0 has no verdict for it

Competitor scoring (benchmark/competitors/, /competitors.html): the comparison benchmark run through Deno, CPython's WASI build under wasmtime, Starlark, smolagents' Python sandbox and CaMeL's reference implementation, each in its own pinned runtime, every cell's command and output recorded, and a CI leg that re-derives the table and fails if a cell moves. Its first version had Sabline ahead or level on every row, which said more about the benchmark than the tools, so it was corrected before it was published. Five categories were added (16 to 20, 102 programs now) where a competitor should win or Sabline should lose: a leak through a granted channel, one legitimate subprocess, correct programs Sabline's rules refuse (a loop until the input ends, Euclid's algorithm, 25! and a product modulo 2^61 - 1), danger below the language in a granted library or its native code, and rows that check the task's legitimate work still got done. The scoring changed for every column alike: the outcome first and the timing only on a tie; a task check, so a runtime that stops everything scores as a catch with the task broken; before-running credit only where a tool's design has a static step; Sabline's audit credited only on the marked dangerous line, as Deno's lint always was; CaMeL scored only on the rows its threat model claims; and a scenario a runtime cannot express recorded with the reason. Sabline now loses rows, and the page says which first. benchmark/results.json is re-recorded at this checkout. Corpus 13a's JavaScript no longer calls require, which Deno does not define, so Deno's catch there is now its permission check. The receipt sections of EMBEDDING.md moved to docs/receipts.md, to keep the embedding page inside the page budget.

The model round trip (roundtrip_eval.py, evals/roundtrip/, /roundtrip.html): for each model, ten small tasks asked for in Sabline, with the language card, and in Python, each answer compiled, run and checked by one check for both languages, with the toolchain's own messages sent back for up to six rounds and never the answer. A failed attempt is reported as one of four things - did not compile, refused by the budget, crashed, wrong output - per model with its exact version and date. Asking a model is --live, off by default and refused under CI; every reply is recorded, and a CI job re-derives every verdict from the recordings with no model and no key. One free model is recorded: qwen2.5:7b, locally, makes 7 of 10 tasks work in Sabline (6 on the first answer) and 9 of 10 in Python. The page publishes what asking every priced model would cost, from the vendors' published prices.

E101 says what a keyword is for. A keyword where a value belongs used to get the fix "expected a number, string, variable, or function call"; in the round trip's recording a model read that as an instruction, wrote fail(...) as a call, and sent the same program back until its six rounds ran out. Now fail after or is told that or fail belongs in a signature and how to handle or pass up a failure (check ... { ok ... fail ... }, or try inside an or fail function); invariant in a function body is told it is a loop clause, and that a promise about a result is ensures; any other keyword is told it cannot be a value, and that writing it as a call does not make it one. The message and the code are unchanged. Both parsers say the same words - sabline-rt's too, which the agreement gate compares - and tests/error_messages holds them: a golden entry may now hold its fixes, and four E101 cases do.

E101 says where requires and ensures go. In the round trip's E101 rerun, a model moved a function's { above its requires and was told four times only that a keyword cannot be a value. Now requires or ensures where a statement belongs is told it is a clause of a function's signature, written after its return type and before its {, with an example (and, for ensures, that result is what the function returns). The second fix gives the order: uses and or fail first, then requires and ensures, all before the {; and inside a body, a value is tested with if. The message and the code are unchanged, both parsers say the same words, and two more golden cases hold them. The known-open list now says that sabline replay does not print the operating-system-layer line, and why.

A run whose grants turn the operating system layer off says so on stderr. Plain ffi, ffi:os, ffi:subprocess and the other modules the confinement table widens to nothing enforced, and a module the table does not name - even one that does not exist - each turn the layer off for the whole run, which until now only the receipt and sabline audit said. A command-line run now writes one line as the program is about to run, on one condition, the receipt's: its OS policy is not enforced because of what the budget grants. The line says which grant: sabline: ffi:os is granted, so the operating system layer is off for this run: the budget is the only boundary, or plain ffi grants any module, or ffi:NAME is not in the confinement table. Nothing else changes: the run, its exit status and its receipt are what they were, sabline.run(), sabline replay, sabline serve and a Pool write nothing new, and --no-confine says its own line instead. The package-hallucination incident's recording shows it, for the invented huggingface_cli.

Four example programs print that line where v8.6.0 printed nothing. It is the whole of the difference: same exit status, same stdout, one line added to stderr.

differential: examples/ffi.vel - its budget grants ffi:builtins, so the OS layer is off for the run and 8.7 says so on stderr. Nothing else differs. differential: examples/sandbox.vel - the same, ffi:builtins. The program is a refusal exercise and every refusal in it is unchanged. differential: examples/database.vel - ffi:builtins is granted and ffi:_sqlite3 is not in the confinement table, and the line names both. differential: examples/stdlib_tools.vel - the same two, for the same reason.

The card changed where the round trip pointed, and did not move the number. Three fixes to LLM.md, and nothing else: the example to imitate no longer imports std.vel (six of ten first answers copied that line, and in one it was the whole failure), a to b stopping before b is said first among the rules, and or fail gets a wrong-then-right pair. Before the card was touched, five held-out tasks were committed (two ranges, a number read from standard input, two helpers a model might give a standard-library name) and parse got a second input, because the first recording's answer compared its input with "42" and printed a fixed 43. qwen2.5:7b, three runs of each card on the fifteen tasks: the original ten, Sabline 7, 6 and 6 on the first answer with the old card and 6, 6 and 6 with the new, 7 within six rounds every time; the held-out five, 0 on the first answer and 1 within with the old card, 1 and 1 with the new; Python 9 of 10 and 5 of 5 throughout. That is a one-task move, and it is read as noise. Under it, the card fixed parse, larger and the held-out total and broke write and home_set - the example's first line is now import "log.vel" as log, which the model copies instead - while the range rule fixed none of the three range tasks and over-corrected one. The same card, model build and options do not give the same answers twice: against the first recording, seven of nine unchanged Sabline first answers differed, which is why every run is kept (.run-2, .run-3) and reported. results.json is sabline.roundtrip/2, with the held-out set apart.

Findable by someone with the problem, in their own words (8.7; the research is in plan/findability-research/). A search baseline taken 2026-09-25 found Sabline in none of 33 searches for the problem it addresses, under either name, and no page of sabline.dev indexed at all. What changed, by legitimate means only - nothing hidden, nothing written for a crawler instead of a reader:

  • One description everywhere: "Run code an AI wrote without handing it everything you can reach." and the sentence after it open the homepage, README.md's first screen and every listing - PyPI, npm, crates.io, the VS Code Marketplace, the MCP bundle, and the MCP registry, whose 100-character limit takes the first sentence alone. check_listings.py holds them to it; --github checks the repository's description, homepage and topics. Keywords lead with the words people search with (ai-agents, mcp, secrets, sandbox, permissions, supply-chain-security, least-privilege).
  • Five guides, each titled from a phrasing people used about the problem, with the link, each saying near its top what it does not do: scripts that reach only the hosts you name; what a script will touch before it runs; between everything and nothing; the lethal trifecta, one run at a time; and secrets that should never leave your machine, which says in its first paragraph that it does not stop an agent's own shell reading .env. Every example is a file under examples/guides/; check_docs.py reads each guide as a test - a <!-- file: --> marker holds a block to its file, every guide must show a refusal, and each refusal line shown must be the one printed - so a guide whose example stops refusing fails the build.
  • Comparison pages, losses first: Deno, WASI, CaMeL and the Python sandbox, generated from the recorded benchmark by build_competitors_page.py, where a category the benchmark calls a judgement call says so beside the rows it would count as a win; and AILANG, written from its documentation read in full at 0.42.0.
  • Incident pages under the names people use ("Shai-Hulud", "tj-actions/changed-files", "the xz backdoor", "slopsquatting"; incidents/names.json), and a flagship page, "We examined 15 real incidents and replayed 8 in Sabline", on which each of the 7 with no replay says why - not covered, with nothing to run, or out of scope - so none reads as left out, and check_incidents.py holds the draft to that. build_incidents.py writes an incident's page only once its entry says verified: true, and the flagship only once every entry does - none do yet, so none is published; --drafts DIR writes them to read, and --check fails if docs/ holds one early.
  • The paper where Google Scholar can find it: build_paper.py builds the PDF with the title and author sizes Scholar's guidelines ask for, served at papers/sabline.pdf beside a landing page carrying citation_title, citation_author, citation_publication_date, citation_pdf_url and citation_doi, and a papers/ index; --check, in CI, fails when the paper changes and the PDF is not rebuilt. The preprint is on Zenodo: its concept DOI, 10.5281/zenodo.22952528, is the landing page's and CITATION.cff's, which also gains the author's ORCID iD, as does the paper's title footnote. Related work gains AILANG (read in full), ETAS and AgentBound, each saying where it is ahead of Sabline; the arXiv source is regenerated.
  • The site, for search: a title and a description of its own on every page (<!-- description: --> in a document), canonical links, a sitemap.xml dated from git, robots.txt, the IndexNow key and indexnow.yml, which sends the pages a push to main changed once Pages has published it; low-value extras labelled so in the code - OpenGraph cards and one small JSON-LD block with no rating on the landing page. check_site.py fails on a missing or repeated title or description, and, in site.yml, which now checks out the whole history, on a sitemap left stale by a commit that changed a page's sources and not it. The landing page leads with the same two sentences, says who Sabline is for and shows a refusal in one command; /velaris explains the rename to someone searching the old name.
  • The sitemap's <lastmod> days are computed in UTC, on the side that builds them and the side that checks them. They were git's %cs, the day in the committer's timezone, while check_site.py compares them against today in UTC. A commit made in the hours that are still yesterday in UTC - midnight to 05:30 from +0530 - dated every page it touched tomorrow: the sitemap then pointed at the future, which the SITEMAP rule refuses and a crawler reading <lastmod> would be right to distrust, and the staleness rule saw a page whose sources changed the day after the sitemap was written. Both sides now ask git for the day in UTC, so the dates, the history the staleness rule reads and the "not in the future" comparison are one clock.
  • A capture-the-flag, docs/ctf.md and the judge in ctf/. Ten claims (C1–C10), each a fixed budget and one thing no program under it may do; you write the program, the judge runs it five times with fresh canaries against the published package pinned by the hash of its wheel, on Linux and under the OS confinement that claim can get - full for the file, io and ffi claims, partial for the net and prover claims, none for the two scored through the tool door, stated per claim on the page and in ctf/status.json, and a run that does not reach its claim's level is not scored. It sees a break only through its own observers - hashes of every tree the run can reach but the one directory its budget lets it write, a listener, a nonce scan, a second run differing only in the nonce and compared on stdout, stderr and the files written (so an encoded leak counts, and one written with log counts as much as one printed), the tool door's transcript against both of its ceilings, the prover's answer for every file a submission brings against the run's receipt - never the submission's exit code or transcript, nor anything else the program can write. The receipt is written in a directory of the judge's own, outside the run's root entirely, and checked afterwards for having been changed; every run passes --no-native, and the judge refuses to score at all where native compilation is available anyway; --sabline must be an absolute path, never a name looked up on PATH. The judge's code is published here and pinned by commit from a private scoring repository, so it can be read but not edited by a submission; the safety rests on the per-run canaries, not on secrecy. Its self-test runs the C1 read submission against a build with the 8.1.1 double-dash hole and must report it broken, and against the pinned package it must hold; check_ctf.py runs the observers on every OS and, in one Linux job, the live self-test and a regression for each finding of the two adversarial reviews of the judge - a leak written with log, a write escaping into the run's own directory, an E601 raised by an imported helper's runtime-checked promise, and a program that floods the tool door's stderr. No money, no points: HALL_OF_FAME.md names the finders.

E101's new hints, measured: the task they were written for now works. qwen2.5:7b was run three more times, with the card, tasks and settings above and the compiler after the E101 change. The held-out months answer still puts or fail in main, and still gets E101. That E101 now offers check ... { ok ... fail ... }, and in all three runs the next answer is exactly that, and works. Before the change it never worked. Sabline, first answer / within six rounds: original 8/8, 6/7 and 6/7 (6/7 in every run before); held-out 2/3, 1/2 and 1/2 (1/1). Python was 9/9 and 5/5 throughout. That is a one-task move, read as noise; the mechanism is in the record. No compiler message can reach a first answer, since the prompt is the same byte for byte, yet one run's first answers moved by two tasks. That is the noise, measured with the prompt held still. The other two runs match the earlier ones on every Sabline task but months. A recording now names the compiler whose messages it was sent (sabline_sha256, also in its file name), and the page has a Compiler column.