attenu

Changelog

One line per change. The full notes for each version are on GitHub.

attenu-guard

0.19.1 · 2026-10-05

Changed

  • Allow.one_of and Deny.not_one_of are no longer frozensets.
  • A deny-list now matches by JSON type, so it admits some values it refused before.
  • A bundle that verified under an earlier release can now fail re-verification.
  • A constraint bound of the wrong type is malformed, so a token that carried one and verified before is now refused.
  • A constraint's key must be a string, so a token whose constraint had none, or a key of another type, is now refused.
  • A constraint must be a JSON object, and its type, when present, a string, so a token carrying another value is now refused.
  • An absent one_of or not_one_of is malformed.
  • A scoped call limit's applies_to must be a scope.
  • A verifier finding prints every string member of an allow-list or a deny-list in its escaped JSON form, quoted.
  • Two constraints under one key in one authority are malformed.

Fixed

  • An allow-list holding the number 1 admitted true.
  • Different ceiling types under one key raised AttributeError.
  • A custom ceiling could let a wider child of another type pass as narrower.
  • A custom ceiling's denial without a reason let the call through.
  • A null quantity passed a metered call.
  • Equal member sets re-emitted different bytes.
  • A list or an object in a request context raised TypeError out of permits().
  • A one_of or not_one_of that is not an array was read by projection.
  • A ceiling coerced a request value of the wrong JSON type, or raised on it.
  • A request context could choose which scoped call limit applied.
  • Unknown constraints were compared with Python's ==.

Known differences

  • A list or an object as a one_of or not_one_of member is refused here: as malformed in a token, and as unreadable_authority or unreadable_granted in a bundle. attenu-guard-ts accepts it and compares it by…
  • A ceiling bound or an unknown constraint value written as an integral number with a fraction or an exponent, such as 100.0 or 1e5, prints as a float here, in describe() and in a verifier finding…
  • An unknown constraint holding an integral float at or past 2^53, such as v: 1e16, subsumes an identical one here, since RFC 8785 writes it. attenu-guard-ts cannot write such a number, so there the constraint is…
  • An unknown constraint whose object has member names that look like integers, such as "10" and "2", prints with its members in wire order here, and with those members first, in ascending numeric order, in…
  • Whether a character prints as itself or as an escape in repr follows the runtime's Unicode tables, across Python versions too.

Full notes for 0.19.1 on GitHub

0.19.0 · 2026-10-05

Added

  • attenu-guard verify --entries prints, after the usual output, entries: and one line per ledger entry: seq, event, node, scope, the envelope state (witness-signed or process-asserted, bundles only),…
  • witness_keys rows take an optional not_after (RFC 3339 UTC, such as 2026-10-05T00:00:00Z).
  • report["envelopes"]["witnesses"] maps the index in bundle["entries"] of every witness-signed entry to the witness.kid of the envelope that verified for it.
  • report["failure_entries"], from verify_bundle() and verify_envelopes(), in step with failures and failure_details: the index in bundle["entries"] of the entry each failure is about, or None for a failure…

Changed

  • What a witness signature covers is stated plainly wherever the envelope states are described (tests/vectors/README.md, examples/verify/README.md, docs/THREAT-MODEL.md, the evidence docstrings): the entry's…
  • docs/THREAT-MODEL.md states that a witness in the subject's own trust domain protects nothing beyond that domain (#22)
  • witness_keys rows are read whole, one row per kid.
  • attenu-guard verify ends its summary line with ungated=N when the bundle has N allows that passed through un-gated, marked policy: "unlisted", which containment does not measure and actions_checked does not…

Fixed

  • A widened child verified OK when its spawn named no real parent.
  • Four more ways around monotonicity and containment are closed in the same pass.
  • A node or a context of the wrong type no longer raises out of verification.
  • The MCP server-verifier recipe did not run on mcp 2.x (#12). mcp 2.x removed the in-memory session helper the demo and its tests imported, renamed FastMCP to MCPServer and snake-cased the result fields…
  • A bundle value could add lines to the attenu-guard verify output.
  • A malformed --witness-keys file ended in a traceback.
  • A re-hashed chain could carry "seq": true and verify clean.
  • An entry whose seq was true took the envelope written for seq 1.
  • A v of true was read as version 1.
  • An integer past 2^53 from a bundle crashed attenu-guard verify.
  • Two leaf types crashed verification, in 0.17.0 and 0.18.0 too.

Known differences

  • A float past 2^53 in a ledger field, such as 1e300, verifies here and fails as a hash mismatch in attenu-guard-ts.

Full notes for 0.19.0 on GitHub

0.18.0 · 2026-09-28

Added

  • attenu-guard demo --audit-path FILE writes the demo's audit log to a file, so attenu-guard view FILE has something to render.

Fixed

  • An empty ledger verified as OK.
  • The README observe-mode snippet did not print the output shown under it.
  • The README quickstart advertised attenu-guard view log.jsonl without writing a log.
  • The existing-ledger error named a keyword Guard.issue does not take.
  • The demo's closing banner said "No rule was written for it" right after the demo declared the summariser's authority by hand.

Full notes for 0.18.0 on GitHub

0.17.0 · 2026-09-20

Changed

  • Authority.from_wire is now PARTIAL: it raises on a constraint carrying members this build does not read.

Fixed

  • export_bundle(strict=True) raised EvidenceLeakError on any run that refused a delegation.
  • verify_bundle reported success on ledger entries it only partly read.
  • The a2a adapter raised out of its own documented contract on an unreadable authority.
  • A Delegation Token carrying authorization data the verifier does not understand verified clean, with that data silently discarded.

Full notes for 0.17.0 on GitHub

0.16.2 · 2026-09-17

Fixed

  • The commerce-agents recipe README said every other integration installs its framework from PyPI.
  • Three doc links in the README were relative, so they were dead on the PyPI page, which renders the same README.
  • "individual submission, WIMSE" read as though the draft were adopted by the working group.
  • The demo's closing line claimed a comparison the run does not show ("Same attack, two outcomes") — the demo runs one guarded chain.

Full notes for 0.16.2 on GitHub

0.16.1 · 2026-09-17

Fixed

  • README and docs said more than the code does.
  • attenu-guard demo text: "SOC" is now "the security team", and a marketing phrase is gone from the closing line.
  • The CrewAI audit-continuity runner (docs/runs/scripts/crewai_continuity.py) recorded each model call's message list by reference, so every call printed the end-of-run list.

Full notes for 0.16.1 on GitHub

0.16.0 · 2026-09-07

Fixed

  • A policy value the format does not define bought an exemption from the containment check.
  • AstrBot: "did the body run?" was answered with per-TOOL state.
  • AstrBot: under strict_single_hook, a body refused by a nested layer was recorded as an ordinary return.
  • OpenHands: a delegation the chain refused widened the sub-agent to the parent's authority.
  • A revoked node that was asked again reported node_finalized instead of revoked.
  • A second user turn in one ADK session was refused with node_finalized, in both modes.
  • The Google ADK adapter did not record a failing context function it was missed in the sweep across the other sixteen.
  • A raising context function was still silent on a delegated child.
  • The body-refusal receipt was bound to the wrong Decision, so it only ever appeared on the un-gated path.
  • The body-refusal receipt was written on one path only.
  • The ADK 0.x callback fallback bound async callbacks, which fail OPEN there.
  • The Google ADK adapter could not attach to an ADK 0.x project at all.
  • An exempt_tools entry ran with no audit row of any kind in the Google ADK adapter — the one path in it with the B1 shape.
  • An allow whose body was refused by a framework layer nested inside the gate now says so.
  • guard_node on an async function and a schema_version=2 guard authorized nothing unless the caller awaited it.
  • A raising context function refused the call and wrote nothing, in every adapter.
  • Guard could not be copied.

Added

  • Two new adapters: OpenHands (openhands-sdk 1.44.1) and AstrBot.
  • smolagents: install(guard, agent, scopes, ...) — the installer the other adapters have, in this adapter's idiom.
  • smolagents: DelegatedAgent mirrors inputs and output_type as well as name/description. smolagents stamps all four in _setup_managed_agents, so swapping the proxy into an already-built managed_agents dict…
  • AstrBot: tools registered after install() are covered.
  • AstrBot: a ToolPolicy declared under the name an MCP server PUBLISHES now binds.
  • AstrBot: a declared policy that matches no registered tool is reported at install time (unbound_policies(), surfaced through the new on_unbound hook; default a logging warning, not a ledger entry — nothing has…
  • AstrBot: this adapter's gate is the OUTERMOST wrapper on both toolset branches.
  • OpenHands: GuardedDelegation.delegate_executor(inner) makes the SDK's second delegation mechanism a spawn seam.
  • OpenHands: tools registered AFTER install() are covered.
  • OpenHands: the SDK's sub-agent aliases are resolved before minting.
  • Both adapters: a failing gate is recorded.
  • Guard.record_passthrough(tool): an adapter running with allow_unlisted=True passed an undeclared tool straight through and left NOTHING on the audit trail (found on a real open-swe run — the tool ran, the ledger…
  • ReasonCode.DELEGATION_REFUSED and a ledger entry for it: a delegation refused by the adapter before it reached the chain (the named sub-agent has no declared Authority) went back to the model as a tool error with no…
  • Bundle vectors reject_unknown_policy_value and reject_policy_on_spawn, revision bundle_vectors_v1.4 (twenty cases): the two rules the accepting policy row does not imply, pinned so an implementer cannot make the…
  • Bundle vector valid_bundle_v2_ungated_allow, revision bundle_vectors_v1.3 (eighteen cases): a valid bundle carrying one policy="unlisted" allow, so the rule is pinned by a case before anyone implements it from…

Changed

  • AstrBot install() documents the late-registration gap it does not close (battery H12, on the plugin add_func and stdio-MCP routes) and names the workaround: call install() again after registering.
  • evidence.denials() folds spawn_denied alongside deny: a delegation the chain refuses structurally (revoked/expired parent, depth/fanout overflow) is recorded once, by Guard.delegate(), and was invisible to an…
  • README first sentence and the repository description say what the contract is: the caller declares the child's grant, the library bounds it by the parent's (the meet), so a child never holds more than its parent; "only…

Full notes for 0.16.0 on GitHub

0.15.0 · 2026-09-06

Added

  • Observer-envelope corpus revision envelope_vectors_v1.2, nineteen cases: reject_duplicate_subject_defective_second is reject_duplicate_subject with one hex nibble of the SECOND envelope's signature flipped.

Full notes for 0.15.0 on GitHub

0.14.1 · 2026-09-05

Changed

  • README: the first screen now opens with the definition, who it is for, the licence line (Apache-2.0, Python 3.9+, zero runtime dependencies, TypeScript port) and one snippet that prints on success; the live-enforcement…

Added

  • examples/integrations/commerce-agents/ — a recipe for anthropics/commerce-agents, whose delegate contract ("a delegate cannot write, present, or invoke other delegates") is stated in prose and held by each delegate…

Fixed

  • attenu-guard verify <missing path> printed a raw FileNotFoundError traceback; it now prints cannot read <path>: <reason> and exits 1, like every other usage error

Full notes for 0.14.1 on GitHub

0.14.0 · 2026-09-05

Added

  • adapters.pydantic_ai.GuardedToolsetCapability — the pydantic-ai integration's primary tool-invocation hook point, and now the one the shipped example runs on.
  • GuardedToolsetCapability.get_ordering() declares position="innermost", wrapped_by=[AbstractCapability].
  • Stated limit: beside a durable-execution engine the guarantee does not hold.
  • Two attenu-guard authorizers on one agent stay refused at construction, now by two different messengers.
  • The ledger's allow entries record WHICH hook point observed a call: a toolset-layer check writes adapter.hook_path of ...GuardedToolset.call_tool, the hook layer keeps ...DelegationGuard.wrap_tool_execute.
  • GuardedToolset.label — what the toolset calls itself in UnmappedToolError and MissingGuardError.

Fixed

  • adapters.pydantic_ai: a hand-built GuardedToolset nested inside a CombinedToolset in toolsets=[...] escaped BOTH agent-wide authorizers' dual-instrumentation check and every tool call was then authorized twice —…

Changed

  • adapters.pydantic_ai module and DelegationGuard docstrings state the hook layer's limit in the maintainer's terms, live-probed on 2.31.1: sorted last among capabilities, DelegationGuard still runs outside every…
  • adapters.pydantic_ai: DelegationGuard.for_agent() no longer refuses a capability ordered INSIDE it that also wraps tool execution.
  • examples/integrations/pydantic_ai/demo.py runs on GuardedToolsetCapability; build_scenario(..., guard=DelegationGuard) runs the same story through the hook layer, and a test asserts the export is denied before its…

Full notes for 0.14.0 on GitHub

0.13.0 · 2026-09-03

Added

  • Observer envelopes (envelope v1) — a witness's Ed25519 signature over the IDENTITY of one committed ledger entry (chain_id, node, seq, entry_hash, event, and call_id on an allow), carried beside the ledger…
  • verify_bundle() — witness_keys (the trust set: [{"kid", "alg", "public_key_hex"}] or a {kid: public_key} mapping) and envelope_bytes (the envelope bytes as received, which only envelope_non_canonical needs).
  • Seven named envelope failures, in evidence.ENVELOPE_FAILURES and in the same failures/failure_details list as every other bundle failure: envelope_unknown_version, envelope_unknown_member,…
  • Observer-envelope interop vectors (tests/vectors/envelopes/envelope_vectors_v1.json), packaged as attenu_guard.vectors.load_envelope_vectors() — eighteen scoring cases, revision envelope_vectors_v1.1, on the same…
  • attenu_guard._ed25519 — RFC 8032 Ed25519, stdlib-only, so the envelope corpus is scorable and regenerable by a pip install attenu-guard that pulled in no dependencies.
  • tests/vectors/README.md: a reason vocabulary — every one of the 33 reason strings a verifier may report (ledger-level, execution binding, envelope), each with its meaning and its {seq, node} position rule, above…
  • One entry, at most one envelope.
  • attenu-guard verify --witness-keys FILE — the trust set for a bundle's observer envelopes, as the vector file's [{"kid", "alg", "public_key_hex"}] array or a whole vector case.
  • adapters.a2a.verify_hop(..., witness_keys=...) — envelope trust for a hop check, plus checks["envelopes"].
  • witness.alg is checked against the contract , not against the trust-set row: v1 defines EdDSA and no other algorithm, so anything else is envelope_unknown_witness.
  • verify_bundle() reports on hostile bundle content and never raises.
  • Malformed caller inputs are refused rather than coerced: an envelope_bytes entry that is not hex or bytes reports envelope_non_canonical for that envelope instead of bytes(n) fabricating n zero bytes; a…

Changed

  • adapters.pydantic_ai header: re-verified against pydantic-ai-slim 2.38.0
  • tests/test_bundle_vectors.py reason-vocabulary anti-drift: the envelope reasons are now READ OUT of _score_envelope's own body (every literal passed to its report() helper) and asserted equal to…
  • evidence.export_bundle(redact_task=True, envelopes=[...]) raises ValueError: redaction rewrites every entry hash, so envelopes signed over the unredacted ledger shipped bound to entries that no longer existed and…

Full notes for 0.13.0 on GitHub

0.12.1 · 2026-09-03

Added

  • Bundle interop vectors, revision bundle_vectors_v1.2 — five appended cases: valid_bundle_v2_literal (the root holds {crm.read, mail.send}, so the child's scopes are a literal subset) and, derived from it,…

Full notes for 0.12.1 on GitHub

0.12.0 · 2026-09-03

Fixed

  • verify_bundle() monotonicity: a delegation that widened only ttl or only a ceiling verified CLEAN whenever the child's scopes were literally a subset of the parent's — the check was gated on a literal,…
  • adapters.pydantic_ai: DelegationGuard.get_ordering() adds wrapped_by=[AbstractCapability], so the sorter settles it LAST in every list order — position="innermost" alone is a tier whose only tiebreaker is list…
  • adapters.pydantic_ai: the per-call re-read of ctx.root_capability is no longer load-bearing — the ordering is the guarantee; it and the construction-time check are belt-and-braces, narrowed to "anything ordered…

Added

  • Bundle interop vectors, revision bundle_vectors_v1.1 — four appended cases: reject_widened_scope and reject_uncontained_allow (the delegation checks no rejecting case covered), reject_increased_ttl and…
  • tests/vectors/README.md — the permitted extras two independent runs reported on reject_duplicate_call_id (first-sighting vs last-sighting binding) and on reject_tampered_entry (stored-head vs recomputed-head),…

Full notes for 0.12.0 on GitHub

0.11.0 · 2026-09-02

Added

  • Bundle interop vectors (tests/vectors/bundles/bundle_vectors_v1.json), packaged as attenu_guard.vectors.load_bundle_vectors() — eight scoring cases
  • verify_bundle() — failure_details, a structured dict per failures string (reason, seq, node, call_id, detail); failures unchanged

Full notes for 0.11.0 on GitHub

0.10.0 · 2026-08-31

Fixed

  • Guard.check(): a PRE_HOOK_ONLY allow wedged complete() forever — only WRAPPER_SYNC/WRAPPER_ASYNC/FRAMEWORK_POST_HOOK calls now register pending
  • adapters.langgraph — _snapshot_params could alias live arguments; now routes through the shared adapters._snapshot.freeze() sanitizer
  • adapters.crewai: outcome correlation was thread-local — now keyed FIFO by id(ctx.tool_input); a third-party veto records ABANDONED
  • adapters.openai_agents: execution binding is opt-in via guarded_tool(..., registry=...), and guard.check() runs inside the tool's on_invoke_tool
  • Adapter snapshots could share live call arguments — one adapters._snapshot.freeze() now rebuilds containers, handles cycles, and marks the rest UNSUPPORTED
  • adapters.google_adk: a deferring tool was recorded RETURNED — after_tool_callback now checks is_long_running/_defers_response and records DEFERRED
  • adapters.haystack: a ToolPolicy with both scope and delegates_to left its call pending forever — the outcome is now carried into the child scope
  • adapters.pydantic_ai: capability ordering could leak or falsify outcomes — one wrap_tool_execute now does both, and for_agent() rejects conflicts
  • adapters.autogen: closing a call_tool_stream early recorded no outcome — GeneratorExit now records BodyState.ABANDONED before re-raising
  • tools/render_demo_gif.py uses shutil.which("ffmpeg") directly instead of a hardcoded fallback path; a machine-specific path in a demo comment removed

Added

  • Execution binding on schema_version=2 chains for adapters.crewai, openai_agents, google_adk, pydantic_ai, haystack and autogen; v1 unchanged
  • adapters.langchain: execution binding on schema_version=2, opt-in via GuardedDelegation(..., strict_single_hook=True); default records no outcome
  • adapters.llama_index: execution binding via guarded_tool()'s async wrapper (Capture.WRAPPER_ASYNC), snapshotting the model-supplied kwargs
  • adapters.smolagents: execution binding from GuardedTool.forward(); generators, coroutines and futures record DEFERRED, not RETURNED
  • adapters.strands: execution binding from AfterToolCallEvent, opt-in via DelegationGuard(..., strict_single_hook=True); lost-outcome paths documented
  • adapters.camel: execution binding from GuardedFunctionTool.__call__/async_call; the camel extra's stale mcp<3 pin corrected to mcp<2
  • adapters.agno: execution binding from guarded_tool_hook/aguarded_tool_hook, opt-in via strict_single_hook=True; default records no outcome
  • Execution binding wired into adapters.ag2 (the AutoGen fork): Capture.WRAPPER_ASYNC from _Gate.run, which awaits call_next(event, context) itself.
  • adapters.agent_framework: execution binding from DelegationGuard.process, opt-in via strict_single_hook=True; the default records no outcome
  • adapters.a2a: execution binding from guarded_tool()'s sync/async wrapper; its internal check calls guard.check() instead of guard.enforce()
  • Execution binding wired into adapters.claude_sdk (Claude Agent SDK), OPT-IN via DelegationGuardRegistry(..., strict_single_hook=True).
  • adapters.semantic_kernel: execution binding from _dg_tool_gate, opt-in via attach_guard(..., strict_single_hook=True); protobuf added to the extra

Full notes for 0.10.0 on GitHub

0.9.0 · 2026-08-31

Fixed

  • Integers outside the RFC 8785 safe range (±(2**53-1)) are now rejected at canonicalization, at RowLimit/SpendCap/CallLimit and by wire.load
  • evidence.verify_bundle and AuditLog.verify_anchor now check schema version and chain identity, so a bundle for the wrong chain no longer verifies

Added

  • AuditLog.append raises CommittedAuditError (carrying the committed entry) when persistence fails after the entry was committed to the chain
  • Execution binding, opt-in via Guard.issue(..., schema_version=2): Decision.call_id, Guard.record_outcome(), and verify_bundle's execution_binding

Changed

  • Behaviour change: an AuditLog/Guard.issue path naming a non-empty file raises FileExistsError instead of truncating; overwrite=True restores it

Full notes for 0.9.0 on GitHub

0.8.0 · 2026-08-29

Changed

  • Scope values now use one interoperable grammar: lowercase dot-separated segments, with * permitted only as the complete final segment after a dot.

Added

  • reject_bare_wildcard.json and reject_nonterminal_wildcard.json — interop suite at 19 vectors, pinning malformed wildcard forms to the malformed reason

Full notes for 0.8.0 on GitHub

0.7.1 · 2026-08-29

Changed

  • c14n is informational; producers still emit it, while verifiers enforce RFC 8785 JCS from canonical bytes and hashes regardless of the label.

Full notes for 0.7.1 on GitHub

0.7.0 · 2026-08-29

Changed — BREAKING

  • All signed and hash-linked artifacts use RFC 8785 JCS only: tokens declare "c14n":"JCS", no legacy or dual-format reader; the interop suite is 17 vectors

Added

  • Ninth interop vector reject_wildcard_boundary.json — crmx.read under a crm.* root must be rejected not_narrower; pins the wildcard's segment boundary

Full notes for 0.7.0 on GitHub

0.6.1 · 2026-08-29

Added

  • Eighth interop vector reject_wildcard_widening.json — a leaf claiming crm.* under a crm.read parent must be rejected not_narrower

Full notes for 0.6.1 on GitHub

0.6.0 · 2026-08-28

Added

  • Interop vectors ship in the package as attenu_guard.vectors (VECTOR_NAMES, load_vector, load_vectors, read_vector_bytes)
  • A2A adapter (attenu_guard.adapters.a2a, extra a2a) — carries the signed delegation chain across an Agent2Agent hop and verifies it offline server-side

Full notes for 0.6.0 on GitHub

0.5.0 · 2026-08-27

Added

  • Haystack adapter (attenu_guard.adapters.haystack, extra haystack) — guards Agents and pipelines through Tool.invoke/invoke_async
  • Microsoft Agent Framework and AG2 adapters (adapters.agent_framework, adapters.ag2; extras agent-framework and ag2) gate tool bodies and delegations
  • Supply chain: SLSA build provenance on every release, weekly OpenSSF Scorecard, and attenu-guard verify as a pre-commit hook via .pre-commit-hooks.yaml

Fixed

  • Adapter docstrings named the pre-rename paste-in modules — they now name attenu_guard.adapters.<name> and the matching extras

Full notes for 0.5.0 on GitHub

0.4.1 · 2026-08-26

Fixed

  • README: the install block still said "pre-publish… once published to PyPI"; the package has been on PyPI since 0.4.0.

Changed

  • Packaging: PyPI classifiers, Documentation / Issues / Changelog project URLs, and a summary aligned with the project description.

Full notes for 0.4.1 on GitHub

0.4.0 · 2026-08-24

Changed — BREAKING

  • Renamed delegation-guard to attenu-guard: module attenu_guard, CLI dg becomes attenu-guard; the API itself is unchanged

Fixed

  • Google ADK adapter: parallel AgentTool calls were chained, not fanned out — the delegating agent is now recorded at the tool call and used as the parent
  • Thread safety under parallel tool calls: the audit log, sequence clock and chain mutations are serialised per chain, so ts/seq advance atomically
  • strict_metering=True failed open on a partial context — a metered call must now declare every metered dimension the node holds (ceilings.ctx_field_of)
  • tests/test_langgraph_adapter.py asserted langgraph was not installed — it now asserts that importing the adapter does not import langgraph

Added

  • deny entries carry a disposition (held_pending_grant, withheld_tier2, unresolved, out_of_authority) via Guard.check()/Guard.record_denial()
  • evidence.denials(bundle) — deny events grouped by (node, tool, scope, disposition) with counts, seq range; delegation_graph gains denials_by_disposition
  • Disposition contract across all 12 adapters, passed to Guard.check; undeclared tools now land on the ledger as unresolved via Guard.record_denial
  • delegation_guard.identity — .attenu/product.json discovery (ATTENU_PRODUCT_DIR or walk-up), boot_id(), new_chain_id(), ledger_path/spool_path
  • AuditLog(sinks=...)/Guard.issue(audit_sinks=) + sinks.SpoolSink — append-only local-file sinks fed after the ledger write, never the network
  • wire.Ed25519Verifier — public-key-only verification for consoles and auditors; Ed25519Signer.private_bytes_raw()/from_private_bytes() for key files
  • Guard.is_descendant_of(other); the Google ADK adapter treats transfer_to_agent back to an ancestor as a return, not a delegation
  • evidence.delegation_graph names a disposition-less deny by its reason (revoked, ceiling_exceeded).

Changed

  • Version 0.3.0 (ledger schema gains an optional disposition field on deny; wire and hash chain unchanged).
  • ReasonCode.NO_AUTHORITY — the principal holds no Authority in this chain (adapter-level: undelegated agent, unmapped tool, unparseable args).
  • Guard.record_denial(reason, message, *, scope, tool, context) — put an adapter-level refusal on the audit trail as a schema-conformant deny event.
  • Guard.agent_id, Guard.is_revoked, Guard.is_expired (read-only).
  • Guard.revoke_agent(agent_id) — principal-scoped, chain-wide revocation with a grow-only ban (AuthorityError reason agent_banned on any later delegate())
  • Guard.would_delegate(agent_id, request) — pure dry-run of the delegation preconditions (Chain.delegation_error), no node, no fanout, no audit write.
  • AuditLog.__iter__ / __len__.
  • Framework adapters ship in the package as delegation_guard.adapters.<name>, with per-framework extras (pip install 'delegation-guard[crewai]')
  • Scoped call ceilings: CallLimit(max, applies_to=<scope|pattern>); Guard.check() auto-meters calls per node and pattern, would_allow() does not consume
  • Observe-mode hooks (default_policy=, default_delegation=) on the LangChain, Google ADK and CrewAI adapters record undeclared tools instead of denying them
  • Bundle redaction: export_bundle(strict=, context_allowlist=, redact_task=), evidence.redaction_report and EvidenceLeakError keep raw values out
  • Offline evidence bundle: export_bundle()/verify_bundle() in delegation_guard.evidence check integrity, monotonicity and containment from the bundle alone
  • Ledger anchoring (AuditLog.anchor/verify_anchor/head, ADR-14) — a signed commitment to the chain head that a consistent full rewrite cannot reproduce
  • StrikePolicy — Guard.issue(strikes=StrikePolicy(n=3, mode="same_scope")) cascade-revokes a node after N denials; one kill event, off by default
  • Guard.complete()/Guard.is_complete — node lifecycle end, an idempotent done audit event; authority is unchanged and revocation stays the hard stop
  • Ceiling.describe() on all built-ins, plus ceilings.describe() and Authority.describe(); ReasonCode constants for structural AuthorityError reasons
  • tools/render_demo_gif.py regenerates docs/assets/demo.gif from dg demo.
  • CI: actions bumped to v6; 6-hourly quickstart canary; per-framework pinned integrations job; weekly unpinned integrations-latest drift canary.

Full notes for 0.4.0 on GitHub

0.2.0 · 2026-08-17

Added

  • Wire format (delegation_guard.wire) — sign a delegation chain as JWS Delegation Tokens and verify child ⊆ parent offline, Ed25519 or a stdlib HS256 signer
  • Typed ceilings: RowLimit, SpendCap, CallLimit, EgressRank, Allow, Deny, Prefix, plus register_ceiling; unknown ceiling types fail closed
  • check() returns a bool-coercible Decision with reason codes and explain(); enforce() raises on denial; would_allow() is a side-effect-free dry run
  • Scenario harness — declarative JSON/YAML authorization tests (dg scenarios file.json)
  • LangGraph adapter under delegation_guard.adapters.langgraph
  • CLI: dg demo | view | verify | scenarios.

Changed

  • Public API is now Guard.issue / delegate / revoke and Authority.meet / is_narrower_than.
  • Package moved to a src/ layout.

Fixed (from the red-team pass)

  • TTL was never enforced.
  • is_narrower_than was unsound for custom ceilings.
  • Custom ceilings could be inert.
  • Wildcard scope pruning could false-deny.

Security

  • A property suite (4,000 random delegation trees per invariant) and a 17-attack red-team harness run in CI; findings pinned as regressions, docs/RED-TEAM.md

Full notes for 0.2.0 on GitHub

attenu-derive

0.2.2 · 2026-09-28

Added

  • Supply chain: SLSA build provenance (sigstore attestation) on every release; OpenSSF Scorecard weekly and on push.

Fixed

  • attenu init no longer requires --product: it keeps an existing product's name, else names the product after the directory.
  • attenu demo in a directory without a product exits 2 with one line saying to run attenu init, and writes nothing (it used to crash with a traceback and leave a partial ledger).
  • attenu demo ends with the three next commands and the real paths it wrote: attenu-guard view, attenu verify, attenu report.
  • attenu ui says the truth: the local console is not published yet, and attenu report produces the HTML evidence report today.
  • attenu link and attenu sync without the cloud client say the control plane is not published yet, instead of pointing at a package that is not on PyPI.

Full notes for 0.2.2 on GitHub

0.2.1 · 2026-08-26

Changed

  • Packaging: [project.urls] (homepage, docs, source, issues, changelog) so PyPI shows them; PyPI classifiers; the package summary and README opener now state what the engine does in one sentence.

Full notes for 0.2.1 on GitHub

0.2.0 · 2026-08-25

Changed — BREAKING

  • Open engine.
  • Enforcement needs no token.
  • Config-revision verification trusts the product's own anchor key plus attenu_derive.config.ISSUER_KEYS (empty by default; the cloud client contributes the Attenu issuer keys when installed).

Added

  • README.md for the public release; AGENTS.md for coding agents; this changelog.

Full notes for 0.2.0 on GitHub

attenu-guard-ts

0.13.1 · 2026-10-05

Changed

  • A constraint bound of the wrong type is malformed, so a token that carried one and verified before is now refused.
  • A constraint's key must be a string, so a token whose constraint had none, or a key of another type, is now refused.
  • A constraint must be a JSON object, and its type, when present, a string, so a token carrying another value is now refused.
  • An absent one_of or not_one_of is malformed.
  • A scoped call limit's applies_to must be a scope.
  • A verifier finding prints every string member of an allow-list or a deny-list in its escaped JSON form, quoted.
  • Two constraints under one key in one authority are malformed.
  • Allow.oneOf and Deny.notOneOf are read-only at runtime.
  • A token refused for a malformed constraint carries the error's text alone.
  • Ceiling.permits, called directly, reads only the context's own fields.
  • A bundle that verified under an earlier release can now fail re-verification.

Fixed

  • A deny-list let through a request value it could not compare.
  • A number read with this library's parseJson passed a deny-list.
  • Allow and Deny read a context field named like an Object.prototype member from the prototype.
  • Different ceiling types under one key threw a TypeError, or delegated by a guess.
  • A custom ceiling could let a wider child of another class pass as narrower.
  • A custom ceiling's denial without a reason let the call through.
  • A constraint member named like an Object.prototype member was ignored.
  • A null quantity passed a metered call.
  • Equal member sets re-emitted different bytes.
  • A one_of or not_one_of that is not an array was read anyway.
  • Unknown constraints were compared as JSON text with only the top-level keys sorted.
  • A ceiling coerced a request value of the wrong JSON type.
  • A request context could choose which scoped call limit applied.
  • An unknown constraint described itself and denied in other words than the Python implementation.
  • A number with a fraction printed in JavaScript's form where the Python implementation prints its repr.

Known differences

  • A ceiling bound or an unknown constraint value written as an integral number with a fraction or an exponent, such as 100.0 or 1e5, prints as an integer here, in describe() and in a verifier finding…
  • A list or an object as a one_of or not_one_of member is accepted here and compared by object identity, so no request value matches it, and a child that repeats it is not narrower than the parent holding it; a lone…
  • An unknown constraint holding an integral float at or past 2^53, such as v: 1e16, is identical to nothing here, since this build cannot write that number in RFC 8785, so a child that repeats its parent's constraint is…
  • An unknown constraint whose object has member names that look like integers, such as "10" and "2", prints with those members first, in ascending numeric order, in describe() and in a verifier finding, since a…
  • Whether a character prints as itself or as an escape in a Python-style repr follows each runtime's Unicode tables, and the Python implementation's varies across Python versions too.

Full notes for 0.13.1 on GitHub

0.13.0 · 2026-10-05

Added

  • attenu-guard verify <file> --entries prints, after the existing output, entries: and one line per ledger entry: seq, event, node, scope, the envelope state (with observed and witness when it is…
  • not_after on a trust-set row: an RFC 3339 date-time in UTC written with Z, such as 2026-10-05T00:00:00Z, compared to the microsecond.
  • report.envelopes.witnesses maps the index in bundle.entries of every witness-signed entry to the witness.kid of the envelope that verified for it.
  • test/fixtures/parity/: CLI parity cases recorded from the Python CLI by tools/gen_fixtures.py (a boolean, a string, a missing and a null seq under --entries; a seq or v written 1.0, 2.0, 3.0, 9.0, -0.0…
  • attenu-guard verify ends its summary line with ungated=N when the bundle has N allows that passed through un-gated, marked policy: "unlisted", which containment does not measure and actions_checked does not…
  • report.failure_entries, from verifyBundle() and verifyEnvelopes(), in step with failures and failure_details: the index in bundle.entries of the entry each failure is about, or null for a failure about no…

Fixed

  • A widened child verified OK when its spawn named no real parent.
  • Four more ways around monotonicity and containment are closed in the same pass.
  • A node of the wrong type is a finding.
  • A value from a bundle could add a line to attenu-guard verify output.
  • A malformed --witness-keys file stopped the CLI with a stack trace.
  • Failure strings that differed from the Python implementation's.
  • witnessKeys rows are read whole, one row per kid.
  • A seq or v follows the schema's integer type, by the same rule in both implementations.
  • An anchor whose seq or v is a number past 2^53 stopped attenu-guard verify --hs256-key with a stack trace.
  • An entry carrying a number past 2^53 stopped attenu-guard verify with a stack trace, with or without a key.
  • An allow whose scope is not a string stopped attenu-guard verify with a stack trace.
  • An allow whose context was not an object, such as [1], 5, "x" or true, verified as if it had none.
  • An envelope over an entry without a seq was checked against no hash.

Changed

  • The README and the WITNESS_SIGNED doc comment say what a witness signature attests: the entry's hash and chain position, plus what the witness-key holder observed.

Known differences

  • A float literal past 2^53 in a ledger field, such as 1e300, verifies in the Python implementation and fails here as a hash mismatch at that entry, because this canonicalizer refuses an integral number past 2^53 where…
  • An allow whose context carries a value of the wrong type for a ceiling, such as {"rows": "many"}, fails containment here, and the Python implementation raises on it
  • delegationGraph lists an edge only for a parent that is a non-empty string, which keeps its exported type, where the Python implementation's delegation_graph lists any parent it reads as true.

Full notes for 0.13.0 on GitHub

0.12.0 · 2026-09-28

Fixed

  • An empty ledger verified as OK.

Full notes for 0.12.0 on GitHub

0.11.0 · 2026-09-20

Fixed

  • export_bundle(strict=True) raised EvidenceLeakError on any run that refused a delegation.
  • verify_bundle reported success on ledger entries it only partly read.
  • A Delegation Token carrying authorization data the verifier does not understand verified clean, with that data silently discarded.

Full notes for 0.11.0 on GitHub

0.10.0 · 2026-09-08

Added

  • policy on an allow entry (Policy.UNLISTED, "unlisted"), matching Python's attenu_guard.reasons.Policy: an adapter running with allowUnlisted let a tool with no declared policy run WITHOUT a check(), and…
  • verifyBundle() reports ungated: allow entries carrying policy are NOT containment-checked and NOT counted in actions_checked.
  • ReasonCode.DELEGATION_REFUSED ("delegation_refused"): an adapter refused a delegation before it reached the chain, so nothing was minted and there is no spawn_denied; the refusal is recorded as a deny
  • Vendored bundle corpus moved to revision bundle_vectors_v1.3, eighteen cases, still copied byte for byte from the Python repository.

Fixed

  • A policy value the format does not define no longer excuses containment.

Changed

  • denials() folds spawn_denied alongside deny, matching Python.

Full notes for 0.10.0 on GitHub

0.9.0 · 2026-09-06

Added

  • Vendored observer-envelope corpus moved to revision envelope_vectors_v1.2, nineteen cases, still copied byte for byte from the Python repository (sha256…

Full notes for 0.9.0 on GitHub

0.8.0 · 2026-09-03

Added

  • Observer envelopes (envelope v1), matching Python 0.13.0 byte for byte — a witness's Ed25519 signature over the IDENTITY of one committed ledger entry (chain_id, node, seq, entry_hash, event, and call_id on…
  • verifyBundle() — witnessKeys (the trust set: [{kid, alg, public_key_hex}] or a {kid: publicKey} record) and envelopeBytes (the envelope bytes as received, which only envelope_non_canonical needs).
  • Seven named envelope failures, in ENVELOPE_FAILURES and in the same failures/failure_details list as every other bundle failure: envelope_unknown_version, envelope_unknown_member, envelope_subject_mismatch,…
  • Vendored observer-envelope interop vectors (test/fixtures/vectors/envelopes/envelope_vectors_v1.json), revision envelope_vectors_v1.1, eighteen cases, copied byte for byte from the Python repository where the…
  • One entry, at most one envelope.
  • attenu-guard verify --witness-keys FILE — the trust set for a bundle's observer envelopes, as the vector file's [{kid, alg, public_key_hex}] array or a whole vector case.
  • witness.alg is checked against the contract , not against the trust-set row: v1 defines EdDSA and no other algorithm, so anything else is envelope_unknown_witness.
  • verifyBundle() reports on hostile bundle content and never throws.
  • Malformed caller inputs are refused rather than coerced: an envelopeBytes entry that is not hex or bytes reports envelope_non_canonical for that envelope; a witnessKeys row whose key is not 64 hex characters or 32…

Changed

  • Two failure strings drop a language-specific type name so both implementations report the same bytes: subject is not a JSON object (was subject is str/subject is string) and <level> member set is not an object…
  • pyRepr renders booleans and containers the way Python's repr does, so a hostile value interpolated into a failure string matches across the two implementations
  • exportBundle({ redactTask: true, envelopes: [...] }) throws: redaction rewrites every entry hash, so envelopes signed over the unredacted ledger shipped bound to entries that no longer existed and failed…
  • test/evidence.test.ts compares checks against expected_reports.json as a superset rather than exactly: the fixture is written by the PINNED Python release, which predates checks.envelopes.

Full notes for 0.8.0 on GitHub

0.7.1 · 2026-09-03

Added

  • Vendored bundle interop vectors updated to revision bundle_vectors_v1.2 — five appended cases: valid_bundle_v2_literal (the root holds {crm.read, mail.send}, so the child's scopes are a literal subset) and,…

Full notes for 0.7.1 on GitHub

0.7.0 · 2026-09-03

Fixed

  • verifyBundle() monotonicity: a delegation that widened only ttl or only a ceiling verified CLEAN whenever the child's scopes were literally a subset of the parent's — the check was gated on a literal,…

Added

  • Vendored bundle interop vectors updated to revision bundle_vectors_v1.1 — four appended cases: reject_widened_scope, reject_uncontained_allow, reject_increased_ttl and reject_loosened_ceiling; version stays…

Full notes for 0.7.0 on GitHub

0.6.0 · 2026-09-02

Added

  • Added test/fixtures/vectors/bundles/bundle_vectors_v1.json, 8 bundle-verifier interop vectors matching the Python reference byte-for-byte.
  • verifyBundle() — reports failure_details alongside failures, one structured {reason, seq, node, call_id, detail} entry per failure.

Full notes for 0.6.0 on GitHub

0.5.0 · 2026-08-31

Fixed

  • freeze() — snapshot fallback could alias live call arguments and mishandle __proto__/sparse arrays/hostile getters; now the sole snapshot path.
  • Guard.check() — a bare/PRE_HOOK_ONLY allow no longer registers as pending, so complete() no longer wedges forever waiting for an outcome.
  • freeze() — ran attacker code before authorization on Proxy/boxed values; now checks Proxy first, returns FREEZE_UNSUPPORTED instead of a string.
  • freeze() — "<accessor>"/"<circular>" sentinel strings could collide with a matching real argument; both now use FREEZE_UNSUPPORTED.
  • interop-next CI check — a failed pip index versions query silently read as "not published yet"; now fails the job instead.

Added

  • Added execution binding (schemaVersion: 2): check/recordOutcome bind call outcomes via callId; verifyBundle reports execution_binding coverage.

Full notes for 0.5.0 on GitHub

0.3.1 · 2026-08-30

Fixed

  • Integers beyond the RFC 8785 safe range are now rejected (canonicalization, RowLimit/SpendCap/CallLimit, load) instead of silently colliding.
  • verifyBundle and AuditLog.verifyAnchor now check schema version and chain identity, so a bundle for the wrong version or chain fails to verify.

Full notes for 0.3.1 on GitHub

0.3.0 · 2026-08-29

Changed

  • Scope values now use the same lowercase, dot-separated grammar as Python.

Added

  • The two malformed-wildcard vectors from Python bring the shared interop suite to 19 vectors.

Full notes for 0.3.0 on GitHub

0.2.1 · 2026-08-29

Changed

  • c14n is informational; producers still emit it, while verifiers enforce RFC 8785 JCS from canonical bytes and hashes regardless of the label.

Full notes for 0.2.1 on GitHub

0.2.0 · 2026-08-29

Changed

  • RFC 8785 JCS is now the only canonicalization format across Delegation Tokens, ledger entries, anchors, and evidence bundles; no dual-format reader.
  • Canonicalizer now uses ECMAScript number serialization and UTF-16 ordering, rejecting non-finite numbers, duplicates, lone surrogates, and cycles.
  • Cross-language fixtures now target Python attenu-guard 0.7 and include all 17 published interop vectors.

Added

  • Added load(tokens, signer, {rootKeyIds, now}), running the offline verification algorithm over a Delegation Token chain and returning a VerifiedChain.
  • Added the 17 language-independent interop vectors from the Python repo to test/fixtures/vectors/; each adversarial chain is rejected for its declared reason.

Full notes for 0.2.0 on GitHub

0.1.1 · 2026-08-28

Changed

  • Releases are published from GitHub Actions with npm trusted publishing (OIDC) and provenance attestations; no tokens are stored anywhere.

Full notes for 0.1.1 on GitHub

0.1.0 · 2026-08-28

Added

  • Added verifyBundle (integrity, monotonicity, containment, HS256/Ed25519 anchor check), AuditLog.verify, and delegationGraph/denials.
  • Added core enforcement: Authority, Guard.issue/delegate/check/enforce, chain depth/fanout ceilings, revoke/revokeAgent, audit log.
  • Added ceilings: RowLimit, SpendCap, CallLimit (scoped, self-metering), EgressRank, Allow, Deny, Prefix, plus registerCeiling for custom types.
  • Added exportBundle with task redaction and a strict mode that refuses any field outside the published ledger allow-list.
  • Added the LangGraph.js adapter (attenu-guard/adapters/langgraph): guardTool, guardNode, addGuardedNode, delegateTo, toolArgs.
  • Added the CLI: attenu-guard verify <file> [--hs256-key HEX | --pubkey HEX] [--kid KID], matching the Python CLI's output and exit codes.
  • Added cross-language fixtures: tools/gen_fixtures.py builds test/fixtures/ from the Python package; the Python CLI verifies ledgers/bundles here.

Notes

  • Zero runtime dependencies.
  • Canonical JSON is byte-identical to Python's json.dumps(obj, sort_keys=True, separators=(",", ":")), including ASCII escaping and number formatting.

Full notes for 0.1.0 on GitHub