Skip to content

Releases: qualixar/bounded-loops

bounded-loops 0.7.6

Choose a tag to compare

@varun369 varun369 released this 03 Sep 08:45

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

Choose a tag to compare

@varun369 varun369 released this 03 Sep 05:49

Hermes public-install compatibility repair. Users of 0.7.4 should upgrade before native Hermes installation.

bounded-loops 0.7.4

Choose a tag to compare

@varun369 varun369 released this 03 Sep 05:20

Native Hermes plugin support, an additive bridge v2 producer, and release-pinned runtime support.

bounded-loops 0.7.3

Choose a tag to compare

@varun369 varun369 released this 28 Aug 18:14

Fixes bl capabilities so loop and graph terminal states render separately.

0.7.2 — a forged evidence digest could reach the receipt

Choose a tag to compare

@varun369 varun369 released this 20 Aug 18:54

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 an evidence_digest whose whole job is to make the externalized verdict
    tamper-evident. validated_verdict_or_none already 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. A str
    subclass overriding startswith, __len__ and __getitem__ satisfied the format test while
    its real bytes were not 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
    plain str in 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_env and env_grant were validated and then re-derived. profile_from_mapping
    called _string_list once to validate these two fields and again in the constructor.
    _string_list calls str(item), and a provider plugin hands over constructed objects, so a
    str subclass with a stateful __str__ answered LEGIT_NAME for the validation loop and
    something else entirely for the constructor. They are now derived once and reused, which is
    what args always 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

Choose a tag to compare

@varun369 varun369 released this 20 Aug 18:01

One fix. The MCP server did not report its own version.

Fixed

  • serverInfo.version was the empty string in the MCP initialize handshake.
    MCPServer(...) defaults version to "" and it was never passed, so a client connecting
    to bounded-loops-mcp was told the name and nothing else. pyproject.toml,
    bounded_loops/__init__.py, the npm package, four plugin manifests and CITATION.cff all
    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-mcp as 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

Choose a tag to compare

@varun369 varun369 released this 20 Aug 14:29

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 --execute selected bubblewrap
    whenever bwrap was 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. --execute needs 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_ids parameter 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". NewType is 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 bare frozenset[str] at that parameter. Corrected in place.

Kept, deliberately

  • OutcomeLabel and label_node_outcome have 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_lineage lost its only caller to the removal above and stays for the same
    kind of reason: reconcile_audit still 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

Choose a tag to compare

@varun369 varun369 released this 15 Aug 15:51

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

Choose a tag to compare

@varun369 varun369 released this 15 Aug 14:14

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

Choose a tag to compare

@varun369 varun369 released this 15 Aug 09:26

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-loops now comes with the loops it advertises. The catalog lived
    only in the git repository, so a pip-only user ran bl 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, and bl 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 run writes its ledger BESIDE the
    loop. site-packages is not writable in a managed environment and is not a sensible place
    to accumulate one user's run receipts. bl loops install puts the loop in the project
    workspace, which is where a run's evidence belongs. --overwrite refuses 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 from pip install for 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

  • hatchling is pinned. Unpinned it emitted Metadata-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 new ServerSession for 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=false returns a confirm_token, and
    confirm=true requires 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=true without 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 status and bl graph metrics refused 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 irreversible and financial as effects that cannot be
    undone
    , so a graph whose publish node declares external_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 — while bl_capabilities promised 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-exec is 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.json and
    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.save wrote 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_status for
    graph_status, and wrong parameter names throughout. The contract test only read SKILL.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 digest did not exist although the composer agent instructed models to run it to
    obtain the one field they are forbidden to invent. It exists, and bl_catalog now carries the
    digest too.

Changed

  • MCP 2.0. mcp>=2,<3, protocol revision 2026-07-28, with 2025-era clients still served
    from the same process. The 1.x line went maintenance-only, and mcp.server.fastmcp — which the
    old pin depended on — no longer exists.
  • Jarvis is now the bounded-loops monitor. bl monitor is 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 init creates it; bl where prints the
    resolved workspace and, more usefully, why that one was chosen. It holds config.toml,
    graphs/, loops/, runs/, tickets/, and an index.json cache. 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 ...

Read more