Releases: qualixar/bounded-loops
Release list
bounded-loops 0.7.6
Native Antigravity lifecycle-hook repair.
- Adds the root-level hooks.json required by current Antigravity.
- Preserves the additive skills, agents, commands, and MCP configuration.
- Updates every release surface to 0.7.6.
bounded-loops 0.7.5
Hermes public-install compatibility repair. Users of 0.7.4 should upgrade before native Hermes installation.
bounded-loops 0.7.4
Native Hermes plugin support, an additive bridge v2 producer, and release-pinned runtime support.
bounded-loops 0.7.3
Fixes bl capabilities so loop and graph terminal states render separately.
0.7.2 — a forged evidence digest could reach the receipt
Two fixes from an audit for one specific defect shape: a check whose criterion is applied to a
value the party under scrutiny controls. Both were confirmed by writing the exploit first.
Fixed
-
A forged evidence digest passed verdict validation and reached the receipt. A graph gate's
verdict carries anevidence_digestwhose whole job is to make the externalized verdict
tamper-evident.validated_verdict_or_nonealready read every field exactly once into a local
— the documented fix for validating one read and acting on another — and that was necessary
but not sufficient, because the object still owned every method the check called. Astr
subclass overridingstartswith,__len__and__getitem__satisfied the format test while
its real bytes werenot a digest at all, and that string was stored in the verdict and
written into the receipt.Fixed with
str.__str__as an unbound builtin, which a subclass cannot intercept: normalise
first, validate the normalised value, store that. The same field is now guaranteed to be a
plainstrin the receipt rather than whatever object a gate handed over. An empty reason
disguised by a lying__len__is refused for the same reason.This remedy already existed in this codebase, applied to the loop-gate boundary in 0.6.9. It
did not reach the graph-gate boundary. -
unset_envandenv_grantwere validated and then re-derived.profile_from_mapping
called_string_listonce to validate these two fields and again in the constructor.
_string_listcallsstr(item), and a provider plugin hands over constructed objects, so a
strsubclass with a stateful__str__answeredLEGIT_NAMEfor the validation loop and
something else entirely for the constructor. They are now derived once and reused, which is
whatargsalways did.Nothing escaped through the live path: env forwarding intersects the provider's declaration
with the operator's allow-list, and env keys come from the real environment, so a smuggled
non-name matches nothing and is logged as refused. The defect is that_is_env_name's stated
guarantee — this field forwards names, never values — was not the thing being enforced.
Both fixes carry a regression test that fails when the fix is reverted.
Audited and found correct
Recorded because "we looked and it was fine" is worth as much as a fix, and because the
reasoning is reusable. The provider-plugin set_env guard is belt-and-braces over a data
round-trip that drops the field entirely, which is the real defence. Price fields are read once
and returned unchanged, and correctly reject bool as an int subclass. Gate provenance
derives kind from the harness registry. And provenance's implementation field documents
itself as self-reported rather than pretending otherwise — when a value can only come from the
subject, saying so in the record is the honest handling.
Install
pip install bounded-loops==0.7.2
npx bounded-loops@0.7.2
Restart your MCP client after upgrading — editors spawn bounded-loops-mcp as a long-lived
child at startup.
Full changelog: https://github.com/qualixar/bounded-loops/blob/main/CHANGELOG.md
0.7.1 — the MCP handshake reports its version
One fix. The MCP server did not report its own version.
Fixed
-
serverInfo.versionwas the empty string in the MCP initialize handshake.
MCPServer(...)defaultsversionto""and it was never passed, so a client connecting
tobounded-loops-mcpwas told the name and nothing else.pyproject.toml,
bounded_loops/__init__.py, the npm package, four plugin manifests andCITATION.cffall
reported the version correctly; the one surface an agent client actually reads did not.No tool was affected. All 24 tools resolved and executed normally — this was a provenance
defect, not a functional one. It still matters: an engine whose subject is knowing which
version decided something should be able to say which version it is.Found by driving the installed server over stdio after the 0.7.0 release rather than by
reading the code. The release contract has a test asserting that every version-bearing
file is covered, and this is a runtime value, so nothing was watching it. There is now
a regression test asserting the handshake version equals__version__— compared against
the constant rather than a literal, so a future bump cannot leave the test green while the
handshake goes stale. Removing the fix makes that test fail.Restart your MCP client after upgrading. Editors spawn
bounded-loops-mcpas a
long-lived child process at startup, so an upgrade alone does not replace the running
server.
Install
pip install bounded-loops==0.7.1
npx bounded-loops@0.7.1
If you use the MCP server from an editor, restart the editor after upgrading — it spawns
bounded-loops-mcp as a long-lived child process at startup, so upgrading the package alone
does not replace the running server.
Full changelog: https://github.com/qualixar/bounded-loops/blob/main/CHANGELOG.md
0.7.0 — seven uncalled capabilities removed, and Linux sandboxed execution withdrawn
Removals. Seven of the nine capabilities the engine shipped, tested and never called, the six
serialisation helpers orphaned by taking one of them out, and one capability it advertised on
Linux and could not complete. Nothing was left behind a documentation note.
No total is given here on purpose. Three drafts of this line carried three different counts,
because a removal also takes out request types, a Protocol, an enum member and a capability
field, and "public symbol" is a judgement call. git diff v0.6.10..v0.7.0 is the answer that
cannot go stale.
Two of the nine are kept on purpose and named at the bottom of this entry.
This is a minor bump rather than a patch because importable public symbols are gone. Every
one of them is recoverable from tag v0.6.10.
Removed
-
Linux sandboxed execution via bubblewrap.
bl graph run --executeselected bubblewrap
wheneverbwrapwas on your PATH, and the node's declared output then never reached the
promoted workspace — so the run could not finish. Linux now refuses at preflight and says
why, instead of choosing a mechanism it cannot honour.--executeneeds macOS Seatbelt on
this release; every other command is unchanged and platform-independent.The previous release documented this as a known limitation. Documenting a capability that
does not work is not a substitute for either fixing or withdrawing it, so the claim is
withdrawn. A test now pins the removal, because the failure mode being restored is a run
that reports success having produced no output. -
The connection-admission lifecycle —
register_connection,advance_connection,
authorize_route,compiler_connection_snapshot. Four functions implementing
discover → admit → route → compile, with no caller anywhere in the engine. A
credential-negotiating admission lifecycle also contradicts this engine's posture of
no-secret connectors. The execution-grant path in the same module has real callers and is
untouched: a grant is still bound to one run, node, attempt and effect, and carries no
credential. -
The local audit-plan store —
AuditPlanService,LocalAuditStore, and the six audit
serialisation helpers that existed only for its read and write paths. The service deferred
its own wiring to "a parallel effort" that was never built. The one symbol anything imported
from the store,plan_from_mapping, lives in the domain layer and is imported from there now. -
resolve_by_repair. The repair flow was never wired end to end. The
repaired_finding_idsparameter it fed remains, and its documentation now states plainly
that validating a repair's lineage is the caller's obligation.
Fixed
- A comment claimed compiler enforcement that Python does not provide.
ValidatedRepairIds
was described as making forgery impossible — "the trust boundary is compiler-enforced, not
just documented".NewTypeis erased at runtime, so a single call constructs one, and a test
in this repository had always done exactly that. What the brand actually buys is that mypy
rejects a barefrozenset[str]at that parameter. Corrected in place.
Kept, deliberately
-
OutcomeLabelandlabel_node_outcomehave no caller either, and stay. They record whether
a node's output was actually correct, independent of the gate verdict — ground truth an
evaluation needs and currently gets by hand. Wiring them needs a labeling API surface and is
a feature, not a removal. -
validate_repair_lineagelost its only caller to the removal above and stays for the same
kind of reason:reconcile_auditstill accepts repaired finding ids, and deleting the one
function able to validate them would make that obligation impossible to meet.
Install
pip install bounded-loops==0.7.0
npx bounded-loops@0.7.0
Verified against the published artifacts — the sha256 digests PyPI serves are identical to
those computed locally before upload.
Full changelog: https://github.com/qualixar/bounded-loops/blob/main/CHANGELOG.md
0.6.3 — bl_graph_evidence takes run_ref
Fixed
bl_graph_evidence advertised the wrong argument name.
Its published MCP schema said run_id, while bl_graph_terminal_runs returns the address as run_ref, the documentation says to pass run_ref, and the resolver wants the directory name. A consumer reading the tool schema would pass the run's identity — which does not resolve, because a run usually lives in a directory named something else.
The tool did the right thing under the wrong name. The argument is now run_ref.
Nothing else moved: no change to resolution behaviour, evidence shape, contract id, trust semantics, or terminal-run handling. bounded-loops.dev/slm-bridge/v1 is unchanged, and a consumer branching on the contract id rather than the version is unaffected either way.
Two tests were added — one asserts the real registered signature rather than the docstring, the other performs the round trip a consumer actually makes: whatever the listing returns as run_ref must work as the fetch argument. That round trip is what would have caught this before publication.
Found by the SuperLocalMemory 4.0.4 bridge audit.
Verification
3338 passed, 15 skipped. ruff and mypy clean.
0.6.2 — gate defects, the evidence contract, catalog-wide tests
Fixed
Fourteen shipped gates accepted broken work or rejected correct work. Each was confirmed by running the gate, and each now has a regression test in both directions.
Passed genuinely broken work: a fabricated legal citation in a reporter the trusted file had never heard of; USER root after a non-root USER; documents denying the very term they were required to declare; bare headings with nothing written under them; HTML images and anchors only markdown syntax could see; secrets in mapping literals; commented-out annotations; async def tests; objectives with no key results.
Blocked correct work: feat!:, and exactly-pinned dependencies using extras, spacing, local versions or environment markers.
Three of those fixes were themselves evadable and were fixed again — the negation defect took three rounds, because requiring a heading moved it into headings rather than removing it.
Added
A stable evidence contract, bounded-loops.dev/slm-bridge/v1. Another product can observe a finished graph run over MCP without importing this package, parsing its receipt files, or pinning its version. Two read-only tools — bl_graph_terminal_runs and bl_graph_evidence — and bl_capabilities advertises the contract so a consumer can discover it.
Compatibility is the contract id, not the version number. SuperLocalMemory is the first consumer and an entirely optional one: neither product depends on the other, and each is complete installed alone.
Gate prose, artifact contents, paths, commands and secrets never appear in the document or in its refusals. workspace_id is a digest. eligible_for_learning is false in the payload, and demonstration marks a cassette replay — this evidence supports observation, not learning.
See docs/evidence-contract.md.
Every loop now has an end-to-end test that runs by default — it must reach DONE, and its untouched seed must fail its own gate. Previously ten loops had such a test and all ten were excluded from the default run.
Verification
3334 passed, 15 skipped. ruff and mypy clean. All 68 loops converge.
0.6.1 — pip install now comes with the loop catalog
Changelog
All notable changes to bounded-loops are documented here. This project follows
Semantic Versioning.
[Unreleased]
[0.6.1] — 2026-08-15
Fixed
-
pip install bounded-loopsnow comes with the loops it advertises. The catalog lived
only in the git repository, so a pip-only user ranbl loops list, saw "No loops found",
and was told to "run from a bounded-loops source checkout" — by a package whose README
opens by advertising 68 of them. The wheel now carries the catalog, andbl loops install <name>copies one into your project.Bundled rather than downloaded on demand, deliberately. A catalog that needed github.com
would be unavailable in the air-gapped, corporate-proxied and egress-restricted
environments this engine is aimed at, and the whole posture is that it runs offline with no
credential. 2.5 MB in the wheel is the cheaper honesty.Installing is a separate step from bundling because
bl runwrites its ledger BESIDE the
loop.site-packagesis not writable in a managed environment and is not a sensible place
to accumulate one user's run receipts.bl loops installputs the loop in the project
workspace, which is where a run's evidence belongs.--overwriterefuses any target that is
not itself a loop package, and the loop name is validated as one path segment before it can
reach a delete. -
README images rendered broken on PyPI.
readme = "README.md"makes that file the PyPI
project page, and PyPI does not resolve relative image paths the way GitHub does. The
architecture diagram had been relative since it was added, so it was broken on the page most
people reach frompip installfor every prior release — while looking correct in the editor
and on GitHub the whole time. Two tests now cover it: one for relative paths, one for
absolute URLs pointing at files that were never committed. -
Switching runs in the monitor left a node from the other run's graph selected. The
Configure panel stayed headed with a node the displayed graph did not contain. The saved-graph
path already cleared this, with a comment explaining why; the run path never did.
Changed
hatchlingis pinned. Unpinned it emittedMetadata-Version: 2.5, which twine rejects and
whose PyPI acceptance could not be confirmed.
[0.6.0] — 2026-08-15
Two independent auditors went through this release end to end, five focused passes each. Eight
HIGH findings survived verification; all eight are fixed below, along with everything they found
at MEDIUM and LOW. Every fix carries a test that was checked by re-introducing the bug.
Fixed — the MCP server
-
bl_run(confirm=true)could never execute. The preview→confirm handshake keyed its state
on the MCP session object, and MCP 2.0 builds a newServerSessionfor every request — so the
preview landed under one key and the confirm looked under another. Every confirm came back
"no matching preview", from every host, on both protocol eras. The tool's core function was
unreachable on the transport it ships over.The handshake is now a signed token:
confirm=falsereturns aconfirm_token, and
confirm=truerequires it. It is an HMAC over the run's full executable identity — gate
command, runner, agent_cmd, cassette, iteration cap, run_id, resume, and a content hash of the
loop's files — signed with a per-process secret and valid for 15 minutes. It works identically
on stdio, HTTP, stateless or pooled, and it closes the old fallback path in which one client
could confirm another client's preview.This changes the tool contract. A caller that passes
confirm=truewithout a token is
refused, and told what to do. Nothing that worked before stops working, because nothing
worked before.
Fixed — surfaces that claimed more than the receipts support
bl graph statusandbl graph metricsrefused runs they had just watched succeed, with
"package digest is not admitted". Four reload sites passed no admitted loop packages, and that
parameter defaults to the empty set, so forgetting it produced a confident wrong answer rather
than an error. An AST check now fails the build if any caller omits it.- STATE.md ended a run with "all nodes succeeded" while the node table printed directly above
it showed a SKIPPED branch. It now says how many branches were not taken, and names them. - The Arena drew "Gate passed — an independent check confirmed the result" on any SUCCEEDED
node. An approval node succeeds because a human held it, with no gate verdict in the log at
all. The badge now reads the verdict it was already being handed. - The confirm screen listed only
irreversibleandfinancialas effects that cannot be
undone, so a graph whose publish node declaresexternal_write— every shipped publish graph
— showed an empty list under a sentence about work that cannot be undone. Both this and the
configuration interview now take the set from the domain. - An approval preview said only "approved approval node 'gate'". Approving a gate releases
everything downstream of it, which is what the recorded grant has always contained. The preview
now names those effects and flags the irreversible ones, and the monitor renders them. - A loop declaring an isolation tier this host cannot deliver was started anyway, then failed
at the node — whilebl_capabilitiespromised refusal before the run starts. The pre-run gate
had exempted loop nodes using the connector-transport predicate; a loop needs no transport
while very much running in a sandbox. - The Seatbelt probe checked whether
sandbox-execis executable, which is vacuously true
inside a nested sandbox where applying a profile fails. It now applies one and reads the exit
status.
Fixed — durability and safety
- A run killed mid-flight left receipts nothing could open.
run-meta.json,plan.jsonand
the manifest were written after the work finished, so a hard kill — including Ctrl-C on
bl monitor, whose execute route runs on a daemon thread — produced a hash-valid log that
every surface refused. Those files describe the plan, not the outcome, so they are now written
before the first node runs. graph.savewrote through a symlink. It resolved the path and then asked whether it was a
symlink, a question that can only answer False once the link has been followed. An alias inside
the workspace silently overwrote the file it pointed at while reporting the alias was saved.- The string
"false"counted as confirmation and started runs. Confirmation now requires
the boolean. - Two confirms in the same second could mint the same run directory, and
exist_ok=True
swallowed it: both callers were told the run started, and both then watched the first run's
receipts. - Symlinked directories were advertised as runs, and the refusal to open one escaped as a
closed socket plus a traceback on the operator's terminal. - Monitor pages now carry a Content-Security-Policy and
X-Frame-Options: DENY.
Fixed — the host pack
- Every shipped command named MCP tools that were never registered —
bl_graph_statusfor
graph_status, and wrong parameter names throughout. The contract test only readSKILL.md,
so the primary product path was broken and nothing said so. All corrected, and the test now
checks every command and agent against the live registry. bl graph digestdid not exist although the composer agent instructed models to run it to
obtain the one field they are forbidden to invent. It exists, andbl_catalognow carries the
digest too.
Changed
- MCP 2.0.
mcp>=2,<3, protocol revision2026-07-28, with 2025-era clients still served
from the same process. The 1.x line went maintenance-only, andmcp.server.fastmcp— which the
old pin depended on — no longer exists. - Jarvis is now the bounded-loops monitor.
bl monitoris unchanged; the package moved.
Added
bl graph digest <loop-dir>— the content digest a loop node must reference./bl-configure— walks a saved graph's interview and applies the answers.scripts/sync_host_pack.py— mirrors the canonical host pack to all three hosts. The contract
test told developers to run this for some time before it existed.- Approval receipts now record who decided, not only which tenant, with the source of that
name and an explicit note that it is not authenticated on a local run.
Added — the project home and the monitor (built for this release)
-
.bounded-loops/is now a project home.bl initcreates it;bl whereprints the
resolved workspace and, more usefully, why that one was chosen. It holdsconfig.toml,
graphs/,loops/,runs/,tickets/, and anindex.jsoncache. One resolver answers
"where does this project keep its runs" for the CLI, MCP, and the UI — the same question
answered twice is the defect class the 0.5 audits kept finding.Discovery walks up from the current directory for an existing
.bounded-loops/, bounded by
the git repository root so a checkout can never silently borrow a workspace sitting above it,
then falls back to the repository root, then to the current directory. A symlinked workspace
root is refused: it would silently relocate every receipt in the project. -
A capability contract, over MCP and on the command line.
bl_capabilities(MCP) and
bl capabilities(CLI) serve the same document from one function: node kinds with their
kind-specific fields, gate kinds and what each mechanically checks plus whether it is
available on this host, isolation tiers with the controls actually enforced here, which failure
policies are honoured versus merely declared, the repair contract and its bound,
the effect vocabulary, every budget field and where it is enforced, the terminal statuses and
which ...