Sabline 8.8.0

Changelog

Every release of Sabline, newest first, one page for each major version. The text is CHANGELOG.md's.

This project was called Velaris until 8.6.0; every entry below 8.6 uses the name it had at the time, which is what the record is for. docs/renamed.md says what moved where.

api: sabline conformance takes --runtime python|rust (9.0, M2): with rust, sabline-rt answers the case kinds it implements and every result says which runtime answered it; without the flag nothing changes.

api: a run takes --audit-stream FILE, and sabline.run, Pool.run and PoolRegistry.run take audit_stream= (9.0, decisions/0007 32b): the run's audit stream, sabline.audit-stream/1, one JSON object an event as it happens - written to FILE a line each, or handed to the function. Without either nothing changes.

api: SABLINE_REFERENCE_RUNTIME=rust sends every run through sabline-rt (9.0, M3; the variable MAINTENANCE.md reserved) - sabline run, sabline.run with and without timeout= and max_memory_mb=, Pool.run, PoolRegistry.run (and so the HTTP door's and the MCP server's runs), sabline replay and sabline test. Unset, or python, nothing changes; any other value is refused - exit 2 on the command line, a ValueError from the library and a pool - where until now nothing read it. A call to the network, Python or a tool is relayed back to the package, which makes it (decided on 2026-10-11); an eval run and sabline trace are the package's, and say so in one line on stderr. From 9.0's M4 sabline-rt asks the operating system to hold the budget in its own process, and a run asked for a level of confinement is its own. sabline/through_rt.py says what a run through sabline-rt is and is not.

api: an organisation ceiling (9.0, decisions/0007 32c): a file whoever administers a machine installs - /etc/sabline/ceiling.json, /Library/Application Support/Sabline/ceiling.json, or Sabline\ceiling.json in Windows' ProgramData - holding sabline.ceiling/1, whose allow caps what any budget on the machine may grant. A budget that goes past it is refused before the program is read, with the new code E325, naming the file, the first grant past it and what it allows; so is every run while the file cannot be read as one. SABLINE_CEILING=FILE holds a run to one more, never instead. A receipt's run_parameters gains ceilings, each {"path", "sha256", "allow"} (empty where none is installed); sabline ceiling [--json] shows what is installed; sabline audit names it. Budget.gap is the rule it holds a budget to, which Budget.covers (a door's --max-allow) now words. Where no ceiling is installed nothing else changes. docs/ceiling.md.

api: SABLINE_THROUGH_RT_LOG=FILE, with SABLINE_REFERENCE_RUNTIME=rust, has each run append one JSON line to FILE saying who answered it - sabline-rt, the checks, or the package and why (9.0, M3; what suites_through_rt.py counts). Without it nothing is written.

api: sabline run --confine strict, and confine="strict" for sabline.run, Pool and PoolRegistry (9.0, M4; decisions/0006 section e): a granted ffi module widens nothing the operating system holds, so the policy is the budget's fs: and net: grants whatever modules are granted, and a module's own native code, ctypes calls and processes are held as the runtime's are. The OS policy document gains "strict": true under it. Without it nothing changes. docs/confinement.md.

api: sabline run --require-confinement full|partial, and require_confinement= for sabline.run, Pool and PoolRegistry (9.0, M4; decisions/0007 item 32a): a run whose confinement, once applied, is below that level is refused before its first statement with the new code E326, and its receipt records the level it got. Beside --no-confine or confine=False either is a usage error. sabline.CONFINE_STRICT and sabline.REQUIRE_CONFINEMENT join the package's names, as CONFINE is. Without either nothing changes.

api: with SABLINE_REFERENCE_RUNTIME=rust, sabline-rt asks the operating system to hold the budget in its own process (9.0, M4), where until now a run through it asked for nothing and its receipt said so. It applies the same policy with the same layers - Landlock and seccomp-bpf on Linux, a sandbox profile on macOS, the job object and the token's privileges on Windows, with the low integrity level for a budget that grants no write - and records the level, the reason, the layers and the policy's digest in its own receipt. --confine strict and --require-confinement are carried in its request and answered there (E326), so neither keeps a run in the package any more. The command's own process is held to the same policy once sabline-rt has been started, because a call to the network, a granted Python module or a tool is made there. Three things follow for a run through sabline-rt: a sabline.run() with no limit can be confined, since the program runs in another process where the package's runs in the caller's; a run whose host is not confined - an unbounded sabline.run()'s - says so in its receipt's reason and is partial rather than full; and from M4's third checkpoint a Windows run is held by an AppContainer of its own, so a budget that restricts both directions and names no loopback host is full there. docs/confinement.md and docs/os-enforces.md.

api: sabline serve and sabline mcp take --confine strict and --require-confinement full|partial (9.0, M4), beside the --no-confine they already took: the operator's own, on the command line, carried to every pool the door makes. A request names a budget and never these. Either beside --no-confine asks for two opposite things and the door does not start. sabline eval refuses both by name, as it refuses --no-confine: confinement is part of its profile and has no switch; its run goes through sabline-rt from M4, and the stop file it needs is honoured there.

fix: sabline.run(source) through sabline-rt on Windows wrote the source to a temporary file in text mode, so the receipt's subject digest was of bytes with a carriage return before every line feed - not of the text that was passed in. The file is written with the text's own bytes now. Receipts made by the Python runtime, and by either runtime from a file on disk, were never affected.

compatibility: E326 is new, and given only to a run whose operator asked for a level of confinement; no run that does not ask is refused by it. On Windows the Python runtime holds neither reads nor the network, so full refuses every budget that restricts either - io does - where a run through sabline-rt is held by an AppContainer and is full; and on macOS every run is partial at best, so full refuses them all. docs/os-enforces.md has the table, for both runtimes.

compatibility: for 9.0's Python package, the native engine is never different from the interpreter, as it was always meant to be. A number past 64 bits, or a read outside a list or a text, stops the machine code where it happens and the interpreter runs the call again: E407 and E602 now carry the interpreter's words and line - the operator, the first read outside, the list's or the text's own fixes - where they named neither, at the function's line, and said "get" for a text. And and and or short-circuit in native code as they do interpreted, so a program whose right side the left one guarded no longer stops natively where it runs interpreted, and a loop that only E407 ends no longer runs forever natively. fuzz_native.py compares the two engines' stderr now, so it stays so.

compatibility: a function value is type checked once, in both runtimes, where until now one that captured nothing was checked a second time. A program's refusals are unchanged - sabline check reported each problem once already - but the check document check_agreement.py compares (sabline check --dump, each stage's problems before they are made one to a message) no longer repeats a problem of such a value. Nesting is why it matters: checking a value written inside a value checks the one inside that, so a chain of N nested values was checked N times over, and the parser fuzzer found a 23 KB program whose check document was 120 MB of the same few problems - a minute of work for both checkers, where it is now a fifth of a second. agreement_checks.py's fn-values-nested-deeply holds the two runtimes to it.

9.x: 9.0.0-alpha.13 to 9.0.0-alpha.8 #

9.x: 9.0.0-alpha.7 to 9.0.0-alpha.1 #

8.x: 8.8 to 8.7.0 #

8.x: 8.6 to 8.5 #

8.x: 8.4 to 8.3.1 #

8.x: 8.3 to 8.2.1 #

8.x: 8.2 to 8.0 #

7.x #

6.x #

5.x #

4.x: 4.4 to 4.2.1 #

4.x: 4.2 to 4.0 #

3.x #

2.x: 2.63.1 to 2.40.1 #

2.x: 2.40 to 2.0 #

1.x #

0.x #