Changelog: 8.x: 8.7.1 to 8.6
Every release of this major version, newest first, as CHANGELOG.md records it.
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.pyholds them to it;--githubchecks 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 underexamples/guides/;check_docs.pyreads 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, andcheck_incidents.pyholds the draft to that.build_incidents.pywrites an incident's page only once its entry saysverified: true, and the flagship only once every entry does - none do yet, so none is published;--drafts DIRwrites them to read, and--checkfails if docs/ holds one early. - The paper where Google Scholar can find it:
build_paper.pybuilds the PDF with the title and author sizes Scholar's guidelines ask for, served atpapers/sabline.pdfbeside a landing page carryingcitation_title,citation_author,citation_publication_date,citation_pdf_urlandcitation_doi, and apapers/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 andCITATION.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, asitemap.xmldated from git,robots.txt, the IndexNow key andindexnow.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.pyfails 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;/velarisexplains 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, whilecheck_site.pycompares 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 -fullfor the file, io and ffi claims,partialfor the net and prover claims,nonefor the two scored through the tool door, stated per claim on the page and inctf/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 withlogcounts 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;--sablinemust be an absolute path, never a name looked up onPATH. 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.pyruns 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 withlog, 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.
8.6 - The name #
The project is renamed. Velaris is Sabline from this release. Nothing else changed: not the language, not the budget grammar, not an error code, not a guarantee, not a line of the threat model. 8.6.0 renames, and does nothing else.
Why. The name Velaris belongs to an unrelated company in the same market - velaris.io, which sells an agent product with an MCP server. Two things with one name in one market is a problem for whoever meets the second one, and they were there first. The name was given up rather than contested. There is no dispute, and nothing was asked of anyone.
What a user of 8.5 has to change: nothing, until 9.0. A rename that
stopped a command, an import, an environment variable or a committed file
from working would be a break, and a break ships only in a major version
(STABILITY.md rule 1). So every name that worked in 8.5 works in 8.6, each
saying once on stderr that it has changed, and each is removed no sooner
than 9.0 (rule 2). The nine are listed under Deprecations in force in
STABILITY.md, and docs/renamed.md is the whole table:
velaris the command, import velaris, velaris.VelarisError,
velaris_mcp, velaris_mcp_install, velaris_magic, VELARIS_*,
velaris.capabilities / velaris.toml / velaris.lock, and every
velaris.* document schema. Each is an alias and not a copy - velaris
is the sabline module object, VelarisError is SablineError - so
there is no second implementation that can drift.
compatibility: the velaris command is kept as a second entry point on the same main(), on PyPI and on npm, so every command line, script, CI step and Dockerfile that runs velaris runs, with one line on stderr saying the name has changed. No flag, command name or exit code changed; the command line STABILITY.md covers is sabline's, and it is velaris's.
compatibility: import velaris gives back the sabline module itself, so the six library names STABILITY.md covers - velaris.check, velaris.audit, velaris.run, velaris.Pool, velaris.card, velaris.attest - are the same functions, returning the same CheckResult, AuditResult, RunResult and Problem objects. velaris.VelarisError is SablineError, the same class rather than a subclass, so except and isinstance answer as they did. A DeprecationWarning is raised as well as the line on stderr, so -W error finds it.
compatibility: Sabline writes sabline.audit/1, sabline.receipt/1, sabline.capabilities/1 and every other sabline.* schema where 8.5 wrote velaris.*, and reads both spellings of each as the same format at the same version. One document is the exception, and is still written under the old name: sabline-spec's conformance corpus, tests/index.json, whose format is velaris.conformance-corpus/1. The corpus is published for an implementation in any language to run, and every implementation that exists is a released version of this one - each reads that field strictly and none can be changed - so a corpus under the new name would stop velaris conformance for everyone who has not upgraded. The three schema files beside it keep their names for the same reason: v8.5.0 opens schemas/velaris.audit.1.schema.json and velaris.capabilities.1.schema.json by name, so renaming them was the same break by another route. 8.6 reads the corpus format under either name and looks for the sabline.* schema filename before falling back, so it is ready for the move before the move happens; the name moves no sooner than sabline-spec 0.15.0. check_differential.py found both halves of this, by failing to run the corpus under v8.5.0. That is an addition to what a reader accepts, which the velaris.audit/1 clause allows within version 1; nothing a consumer could rely on disappeared. A tool outside Sabline that matched the schema string exactly must accept both - the Kyverno policy in policies/ shows how, with AnyIn.
compatibility: a committed velaris.capabilities still holds the capability ratchet, read when there is no sabline.capabilities beside it, so a repository that upgrades needs no commit to keep its baseline working; the same rule covers velaris.toml and velaris.lock. Sabline writes only the sabline.* name.
compatibility: a VELARIS_* environment variable is read where the SABLINE_* name of that variable is unset. Both set to different text is refused rather than guessed, because a guess at SABLINE_TOKEN decides who may reach a door; both set to the same text is one name written twice, and is fine. A message about such a variable names the spelling the operator wrote: VELARIS_PROOF_TIMEOUT=x is complained about as VELARIS_PROOF_TIMEOUT, not as the new name, which would send somebody to look at a variable they never set.
compatibility: sabline attest writes the predicate type https://sabline.dev/capability/v1, and a receipt names https://sabline.dev/receipt/v1, where 8.3 to 8.5 named both at velaris-lang.dev and 4.2 to 8.2.1 at the project's GitHub Pages address. All three spellings are read as the same type by sabline verify, sabline receipts diff, sabline replay and the OPA policy, so every attestation and receipt ever signed still verifies; nothing is removed from that list, a name only ever joins it. A verifier outside Sabline that pins one type name - cosign's --type, the Kyverno policy - takes the name the Statement carries.
compatibility: the documentation site is sabline.dev, and velaris-lang.dev redirects to it path for path, so the reference: line every error and refusal printed by 8.3 through 8.5 still leads to the card. Error codes, messages and the errors page's anchors are unchanged. One address could NOT be kept: a GitHub Pages site is served at <owner>.github.io/<repo>, so renaming the repository moved it, and https://gowrishankar-infra.github.io/velaris-lang/... - the reference: line of 8.0 through 8.2.1 - answers 404. Serving it again would need a repository called velaris-lang, and creating one would end GitHub's redirect from every old repository URL, uses: gowrishankar-infra/velaris-lang@<commit> among them; those are worth more than one address. It does not touch the predicate type named there, which is a name and not a page: every Statement signed by 4.2 to 8.2.1 still verifies. docs/renamed.md says all of this.
compatibility: no error code was added, removed or changed in meaning; no flag was removed; no default changed. The release gate reads the compiler's source at v8.5.0 under the name it had then (release_checks.py, RENAMED_IN_8_6), so the package moving from velaris/ to sabline/ is not read as every code, flag and default being added at once - which is what it looked like before that was taught to it.
compatibility: the VS Code extension is republished under a new id, gowrishankar-infra.sabline, because a Marketplace id cannot be renamed. The old id gets a final version whose README says where it went. This one is a real break for anyone who had the extension installed: they must install the new one. It could not be avoided, and it is the only thing in this release that a user must do something about.
compatibility: GET /health on the HTTP door names the version under both sabline and velaris. The endpoint is documented (EMBEDDING.md) and is not provisional, and what reads it is a monitoring script doing jq .velaris, which would have begun reading null rather than failing. velaris goes in 9.0. sabline deps-diff finds a dependency's lockfile under either sabline.lock or velaris.lock, since a dependency locked by an earlier release has the old name.
compatibility: two fields of documents STABILITY.md marks PROVISIONAL are renamed rather than added to, which is what provisional is for, and each is named here as that clause requires: sabline.deps-diff/1's velaris_files is sabline_files, and the ready line of sabline.tools-door/1 names the version under sabline where 8.5 wrote velaris. Both are one release old, both are read by a host written against 8.5, and both are declared rather than doubled, because a provisional format that is never allowed to change is not provisional.
api: velaris_version is added beside sabline_version in every sabline.audit/1 and sabline.capabilities/1 document, in what the MCP server's audit returns, and as a property of AuditResult; velaris is added beside sabline in what GET /health, GET /health without a token and GET / answer - the same value under both names, because a required field of a version-1 document may not disappear and STABILITY.md covers the fields of what a call returns; velaris.VelarisError is added to the library's names, as an alias of SablineError; CAPABILITY_PREDICATE_TYPE and RECEIPT_PREDICATE_TYPE name sabline.dev, and each of CAPABILITY_PREDICATE_TYPES and RECEIPT_PREDICATE_TYPES gains a third spelling. No name was removed, no signature changed, and no return type changed.
differential: examples/avg_bad.vel, examples/builtin_unhandled.vel, examples/callsite_bad.vel, examples/caught.vel, examples/conj_bad.vel, examples/contract_broken.vel, examples/contract_impure.vel, examples/discount_bad.vel, examples/div_bad.vel, examples/fail_proof_bad.vel, examples/failing_bad.vel, examples/ffi.vel, examples/floats_bad.vel, examples/fp_proof_bad.vel, examples/funcs_bad.vel, examples/generics_bad.vel, examples/grid_bad.vel, examples/import_bad.vel, examples/lambda_contract_bad.vel, examples/list_mixed.vel, examples/list_oob.vel, examples/list_proof_bad.vel, examples/loop_bad.vel, examples/loop_proof_bad.vel, examples/many_errors.vel, examples/map_bad.vel, examples/maps_bad.vel, examples/ns_bad.vel, examples/offbyone_bad.vel, examples/proof_catch.vel, examples/qlist_bad.vel, examples/rec_proof_bad.vel, examples/records_bad.vel, examples/secret_bad.vel, examples/sneaky.vel, examples/std_bad.vel, examples/types_bad.vel - each prints the reference: line every error and refusal has carried since 8.0, and it now names sabline.dev where it named velaris-lang.dev. Nothing else in any of their outputs differs: same exit code, same error codes, same message text, same fixes. The old address redirects, so the line printed by 8.0 to 8.5 still reaches the card.
differential: examples/edges.vel - its own output shouts the project's name and encodes it: first, shouted: VELARIS is now SABLINE, and base64: dmVsYXJpcw== is the base64 of sabline. The program is a text-and-encoder exercise; the name is its input.
differential: examples/text_tools.vel - the same: the program's heading reads Sabline edge and property tests, and a shouted name in its output is SABLINE.
differential: examples/tools.vel - its usage line says try: sabline examples/tools.vel alpha beta where it said velaris.
differential: examples/trace_demo.vel - the prover's timeout note names SABLINE_PROOF_TIMEOUT where it named VELARIS_PROOF_TIMEOUT; both are read (STABILITY.md), and the note gives the name to write now.
differential: examples/native_build.vel - it reports [sabline] where it reported [velaris].
differential: examples/sandbox.vel - it tries to read the compiler's own launcher by name and is refused: cannot read file 'sabline.py' where the file was velaris.py. The refusal is the point of the program and is unchanged.
What is renamed #
- The packages. PyPI and npm
velaris-langbecomesabline-lang; the MCP registry serverio.github.gowrishankar-infra/velarisbecomes.../sabline; the VS Code extensiongowrishankar-infra.velarisbecomesgowrishankar-infra.sabline. - The repositories, renamed in place with
gh repo rename, so GitHub redirects every old URL - the web page,git clone, and a workflow that saysuses: gowrishankar-infra/velaris-lang@<commit>:velaris-lang,velaris-spec,velaris-kitandvelaris-canaryaresabline-lang,sabline-spec,sabline-kitandsabline-canary. - The domain. sabline.dev.
security@sabline.devis the security contact;security@velaris-lang.devstill reaches the maintainer. - The source, by
scripts/rename.py, which is committed: 7,393 occurrences in 293 files, as one case-preserving substitution with two lists beside it - the strings in which the old name is the fact being recorded, and the files that record rather than describe. The script run with no arguments is the drift test, andcheck_rename.pyruns it on every leg of CI beside a case for each of the nine aliases.
What is not renamed #
.vel, the file extension. It is name-neutral, and changing it would break every program that exists.- The eight published advisories, their text and their GHSA ids. An
advisory records what was wrong with a release that was called Velaris,
and its affected package really is
velaris-langon PyPI. - This changelog before this entry, and STABILITY.md's record of earlier releases. They say what happened.
- The documentation of 8.3, 8.4 and 8.5, at
/8.3/,/8.4/and/8.5/, left as those releases published it, at the address it was published at. Every link in it redirects. - Anything already published. Nothing on PyPI, npm, the MCP registry,
the Marketplace or the GitHub releases was deleted, yanked or moved. An
install pinned to
velaris-lang==8.5.0resolves to the same file, with the same digest and the same signature, for the reason 3.4 was not retagged and 6.0.0 was yanked rather than deleted. The old names get one last release that depends on the new one and prints the notice, and the npm one is marked deprecated. - Git history. Every file was moved with
git mv.
The new things this release does have #
docs/renamed.md, at sabline.dev/renamed.html: every published address, where it now points, and what was deliberately not renamed.sabline/naming.py, where every old name that is still accepted is decided - the environment, the document schemas, the file names, and the one-line notice each alias prints. One place to read, and one place to delete in 9.0.- A conformance case compares
safe_commandby its grants, not as a whole string (sabline-spec 8.3, 0.14.0). What that section defines is the grant list; what comes before--allowis the producer's own command name. Comparing the string had required every implementation to be called what the reference was called, so 27 L1 cases read as a changed verdict across the rename while nothing about the grants had moved. A corpus published for an implementation in any language must not ask it to be named a particular thing. check_rename.py, which runs every promise above: the command, the import, the submodule import, the warning,VelarisError, both spellings of six schemas, avelaris.capabilitiesofvelaris.capabilities/1checked by the ratchet, and a Statement of each earlier predicate type verified - and refuses one of a type Sabline does not define.