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_ofandDeny.not_one_ofare 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
keymust 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_ofornot_one_ofis malformed. - A scoped call limit's
applies_tomust 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
TypeErrorout ofpermits(). - A
one_ofornot_one_ofthat 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_ofornot_one_ofmember is refused here: asmalformedin a token, and asunreadable_authorityorunreadable_grantedin 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.0or1e5, prints as a float here, indescribe()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
reprfollows 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 --entriesprints, after the usual output,entries:and one line per ledger entry:seq,event,node,scope, the envelopestate(witness-signedorprocess-asserted, bundles only),…witness_keysrows take an optionalnot_after(RFC 3339 UTC, such as2026-10-05T00:00:00Z).report["envelopes"]["witnesses"]maps the index inbundle["entries"]of every witness-signed entry to thewitness.kidof the envelope that verified for it.report["failure_entries"], fromverify_bundle()andverify_envelopes(), in step withfailuresandfailure_details: the index inbundle["entries"]of the entry each failure is about, orNonefor 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, theevidencedocstrings): the entry's… docs/THREAT-MODEL.mdstates that a witness in the subject's own trust domain protects nothing beyond that domain (#22)witness_keysrows are read whole, one row per kid.attenu-guard verifyends its summary line withungated=Nwhen the bundle has N allows that passed through un-gated, markedpolicy: "unlisted", which containment does not measure andactions_checkeddoes 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
nodeor acontextof 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
FastMCPtoMCPServerand snake-cased the result fields… - A bundle value could add lines to the
attenu-guard verifyoutput. - A malformed
--witness-keysfile ended in a traceback. - A re-hashed chain could carry
"seq": trueand verify clean. - An entry whose seq was
truetook the envelope written for seq 1. - A
voftruewas 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 FILEwrites the demo's audit log to a file, soattenu-guard view FILEhas 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.jsonlwithout writing a log. - The existing-ledger error named a keyword
Guard.issuedoes 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_wireis now PARTIAL: it raises on a constraint carrying members this build does not read.
Fixed
export_bundle(strict=True)raisedEvidenceLeakErroron any run that refused a delegation.verify_bundlereported success on ledger entries it only partly read.- The
a2aadapter 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 demotext: "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
policyvalue 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_finalizedinstead ofrevoked. - 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_toolsentry ran with no audit row of any kind in the Google ADK adapter — the one path in it with the B1 shape. - An
allowwhose body was refused by a framework layer nested inside the gate now says so. guard_nodeon an async function and aschema_version=2guard authorized nothing unless the caller awaited it.- A raising context function refused the call and wrote nothing, in every adapter.
Guardcould not be copied.
Added
- Two new adapters: OpenHands (
openhands-sdk1.44.1) and AstrBot. - smolagents:
install(guard, agent, scopes, ...)— the installer the other adapters have, in this adapter's idiom. - smolagents:
DelegatedAgentmirrorsinputsandoutput_typeas well asname/description. smolagents stamps all four in_setup_managed_agents, so swapping the proxy into an already-builtmanaged_agentsdict… - AstrBot: tools registered after
install()are covered. - AstrBot: a
ToolPolicydeclared 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 newon_unboundhook; default aloggingwarning, 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 withallow_unlisted=Truepassed 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_REFUSEDand 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_valueandreject_policy_on_spawn, revisionbundle_vectors_v1.4(twenty cases): the two rules the acceptingpolicyrow does not imply, pinned so an implementer cannot make the… - Bundle vector
valid_bundle_v2_ungated_allow, revisionbundle_vectors_v1.3(eighteen cases): a valid bundle carrying onepolicy="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 pluginadd_funcand stdio-MCP routes) and names the workaround: callinstall()again after registering. evidence.denials()foldsspawn_deniedalongsidedeny: a delegation the chain refuses structurally (revoked/expired parent, depth/fanout overflow) is recorded once, byGuard.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_secondisreject_duplicate_subjectwith 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 foranthropics/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 rawFileNotFoundErrortraceback; it now printscannot 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()declaresposition="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
allowentries record WHICH hook point observed a call: a toolset-layer check writesadapter.hook_pathof...GuardedToolset.call_tool, the hook layer keeps...DelegationGuard.wrap_tool_execute. GuardedToolset.label— what the toolset calls itself inUnmappedToolErrorandMissingGuardError.
Fixed
adapters.pydantic_ai: a hand-builtGuardedToolsetnested inside aCombinedToolsetintoolsets=[...]escaped BOTH agent-wide authorizers' dual-instrumentation check and every tool call was then authorized twice —…
Changed
adapters.pydantic_aimodule andDelegationGuarddocstrings state the hook layer's limit in the maintainer's terms, live-probed on 2.31.1: sorted last among capabilities,DelegationGuardstill 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.pyruns onGuardedToolsetCapability;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, andcall_idon an allow), carried beside the ledger… verify_bundle()—witness_keys(the trust set:[{"kid", "alg", "public_key_hex"}]or a{kid: public_key}mapping) andenvelope_bytes(the envelope bytes as received, which onlyenvelope_non_canonicalneeds).- Seven named envelope failures, in
evidence.ENVELOPE_FAILURESand in the samefailures/failure_detailslist as every other bundle failure:envelope_unknown_version,envelope_unknown_member,… - Observer-envelope interop vectors (
tests/vectors/envelopes/envelope_vectors_v1.json), packaged asattenu_guard.vectors.load_envelope_vectors()— eighteen scoring cases, revisionenvelope_vectors_v1.1, on the same… attenu_guard._ed25519— RFC 8032 Ed25519, stdlib-only, so the envelope corpus is scorable and regenerable by apip install attenu-guardthat 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, pluschecks["envelopes"].witness.algis checked against the contract , not against the trust-set row: v1 definesEdDSAand no other algorithm, so anything else isenvelope_unknown_witness.verify_bundle()reports on hostile bundle content and never raises.- Malformed caller inputs are refused rather than coerced: an
envelope_bytesentry that is not hex or bytes reportsenvelope_non_canonicalfor that envelope instead ofbytes(n)fabricating n zero bytes; a…
Changed
adapters.pydantic_aiheader: re-verified against pydantic-ai-slim 2.38.0tests/test_bundle_vectors.pyreason-vocabulary anti-drift: the envelope reasons are now READ OUT of_score_envelope's own body (every literal passed to itsreport()helper) and asserted equal to…evidence.export_bundle(redact_task=True, envelopes=[...])raisesValueError: 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()addswrapped_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 ofctx.root_capabilityis 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_scopeandreject_uncontained_allow(the delegation checks no rejecting case covered),reject_increased_ttland… tests/vectors/README.md— the permitted extras two independent runs reported onreject_duplicate_call_id(first-sighting vs last-sighting binding) and onreject_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 asattenu_guard.vectors.load_bundle_vectors()— eight scoring cases verify_bundle()—failure_details, a structured dict perfailuresstring (reason,seq,node,call_id,detail);failuresunchanged
Full notes for 0.11.0 on GitHub
0.10.0 · 2026-08-31
Fixed
Guard.check(): aPRE_HOOK_ONLYallow wedgedcomplete()forever — onlyWRAPPER_SYNC/WRAPPER_ASYNC/FRAMEWORK_POST_HOOKcalls now register pendingadapters.langgraph—_snapshot_paramscould alias live arguments; now routes through the sharedadapters._snapshot.freeze()sanitizeradapters.crewai: outcome correlation was thread-local — now keyed FIFO byid(ctx.tool_input); a third-party veto recordsABANDONEDadapters.openai_agents: execution binding is opt-in viaguarded_tool(..., registry=...), andguard.check()runs inside the tool'son_invoke_tool- Adapter snapshots could share live call arguments — one
adapters._snapshot.freeze()now rebuilds containers, handles cycles, and marks the restUNSUPPORTED adapters.google_adk: a deferring tool was recordedRETURNED—after_tool_callbacknow checksis_long_running/_defers_responseand recordsDEFERREDadapters.haystack: aToolPolicywith bothscopeanddelegates_toleft its call pending forever — the outcome is now carried into the child scopeadapters.pydantic_ai: capability ordering could leak or falsify outcomes — onewrap_tool_executenow does both, andfor_agent()rejects conflictsadapters.autogen: closing acall_tool_streamearly recorded no outcome —GeneratorExitnow recordsBodyState.ABANDONEDbefore re-raisingtools/render_demo_gif.pyusesshutil.which("ffmpeg")directly instead of a hardcoded fallback path; a machine-specific path in a demo comment removed
Added
- Execution binding on
schema_version=2chains foradapters.crewai,openai_agents,google_adk,pydantic_ai,haystackandautogen; v1 unchanged adapters.langchain: execution binding onschema_version=2, opt-in viaGuardedDelegation(..., strict_single_hook=True); default records no outcomeadapters.llama_index: execution binding viaguarded_tool()'s async wrapper (Capture.WRAPPER_ASYNC), snapshotting the model-supplied kwargsadapters.smolagents: execution binding fromGuardedTool.forward(); generators, coroutines and futures recordDEFERRED, notRETURNEDadapters.strands: execution binding fromAfterToolCallEvent, opt-in viaDelegationGuard(..., strict_single_hook=True); lost-outcome paths documentedadapters.camel: execution binding fromGuardedFunctionTool.__call__/async_call; thecamelextra's stalemcp<3pin corrected tomcp<2adapters.agno: execution binding fromguarded_tool_hook/aguarded_tool_hook, opt-in viastrict_single_hook=True; default records no outcome- Execution binding wired into
adapters.ag2(the AutoGen fork):Capture.WRAPPER_ASYNCfrom_Gate.run, which awaitscall_next(event, context)itself. adapters.agent_framework: execution binding fromDelegationGuard.process, opt-in viastrict_single_hook=True; the default records no outcomeadapters.a2a: execution binding fromguarded_tool()'s sync/async wrapper; its internal check callsguard.check()instead ofguard.enforce()- Execution binding wired into
adapters.claude_sdk(Claude Agent SDK), OPT-IN viaDelegationGuardRegistry(..., strict_single_hook=True). adapters.semantic_kernel: execution binding from_dg_tool_gate, opt-in viaattach_guard(..., strict_single_hook=True);protobufadded 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/CallLimitand bywire.load evidence.verify_bundleandAuditLog.verify_anchornow check schema version and chain identity, so a bundle for the wrong chain no longer verifies
Added
AuditLog.appendraisesCommittedAuditError(carrying the committedentry) 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(), andverify_bundle'sexecution_binding
Changed
- Behaviour change: an
AuditLog/Guard.issuepath naming a non-empty file raisesFileExistsErrorinstead of truncating;overwrite=Truerestores 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.jsonandreject_nonterminal_wildcard.json— interop suite at 19 vectors, pinning malformed wildcard forms to themalformedreason
Full notes for 0.8.0 on GitHub
0.7.1 · 2026-08-29
Changed
c14nis 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.readunder acrm.*root must be rejectednot_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 claimingcrm.*under acrm.readparent must be rejectednot_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, extraa2a) — 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, extrahaystack) — guardsAgents and pipelines throughTool.invoke/invoke_async - Microsoft Agent Framework and AG2 adapters (
adapters.agent_framework,adapters.ag2; extrasagent-frameworkandag2) gate tool bodies and delegations - Supply chain: SLSA build provenance on every release, weekly OpenSSF Scorecard, and
attenu-guard verifyas 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/Changelogproject 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-guardtoattenu-guard: moduleattenu_guard, CLIdgbecomesattenu-guard; the API itself is unchanged
Fixed
- Google ADK adapter: parallel
AgentToolcalls 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/seqadvance atomically strict_metering=Truefailed 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.pyasserted langgraph was not installed — it now asserts that importing the adapter does not import langgraph
Added
denyentries carry adisposition(held_pending_grant,withheld_tier2,unresolved,out_of_authority) viaGuard.check()/Guard.record_denial()evidence.denials(bundle)— deny events grouped by (node, tool, scope, disposition) with counts, seq range;delegation_graphgainsdenials_by_disposition- Disposition contract across all 12 adapters, passed to
Guard.check; undeclared tools now land on the ledger asunresolvedviaGuard.record_denial delegation_guard.identity—.attenu/product.jsondiscovery (ATTENU_PRODUCT_DIRor walk-up),boot_id(),new_chain_id(),ledger_path/spool_pathAuditLog(sinks=...)/Guard.issue(audit_sinks=)+sinks.SpoolSink— append-only local-file sinks fed after the ledger write, never the networkwire.Ed25519Verifier— public-key-only verification for consoles and auditors;Ed25519Signer.private_bytes_raw()/from_private_bytes()for key filesGuard.is_descendant_of(other); the Google ADK adapter treatstransfer_to_agentback to an ancestor as a return, not a delegationevidence.delegation_graphnames a disposition-less deny by its reason (revoked,ceiling_exceeded).
Changed
- Version 0.3.0 (ledger schema gains an optional
dispositionfield 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-conformantdenyevent.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 (AuthorityErrorreasonagent_bannedon any laterdelegate())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_reportandEvidenceLeakErrorkeep raw values out - Offline evidence bundle:
export_bundle()/verify_bundle()indelegation_guard.evidencecheck 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; onekillevent, off by defaultGuard.complete()/Guard.is_complete— node lifecycle end, an idempotentdoneaudit event; authority is unchanged and revocation stays the hard stopCeiling.describe()on all built-ins, plusceilings.describe()andAuthority.describe();ReasonCodeconstants for structuralAuthorityErrorreasonstools/render_demo_gif.pyregeneratesdocs/assets/demo.giffromdg demo.- CI: actions bumped to v6; 6-hourly quickstart canary; per-framework pinned
integrationsjob; weekly unpinnedintegrations-latestdrift 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, plusregister_ceiling; unknown ceiling types fail closed check()returns a bool-coercibleDecisionwith reason codes andexplain();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 / revokeandAuthority.meet / is_narrower_than. - Package moved to a
src/layout.
Fixed (from the red-team pass)
- TTL was never enforced.
is_narrower_thanwas 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 initno longer requires--product: it keeps an existing product's name, else names the product after the directory.attenu demoin a directory without a product exits 2 with one line saying to runattenu init, and writes nothing (it used to crash with a traceback and leave a partial ledger).attenu demoends with the three next commands and the real paths it wrote:attenu-guard view,attenu verify,attenu report.attenu uisays the truth: the local console is not published yet, andattenu reportproduces the HTML evidence report today.attenu linkandattenu syncwithout 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.mdfor the public release;AGENTS.mdfor 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
keymust 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_ofornot_one_ofis malformed. - A scoped call limit's
applies_tomust 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.oneOfandDeny.notOneOfare 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
parseJsonpassed a deny-list. AllowandDenyread a context field named like anObject.prototypemember 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.prototypemember was ignored. - A null quantity passed a metered call.
- Equal member sets re-emitted different bytes.
- A
one_ofornot_one_ofthat 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.0or1e5, prints as an integer here, indescribe()and in a verifier finding… - A list or an object as a
one_ofornot_one_ofmember 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, indescribe()and in a verifier finding, since a… - Whether a character prints as itself or as an escape in a Python-style
reprfollows 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> --entriesprints, after the existing output,entries:and one line per ledger entry:seq,event,node,scope, the envelopestate(withobservedandwitnesswhen it is…not_afteron a trust-set row: an RFC 3339 date-time in UTC written withZ, such as2026-10-05T00:00:00Z, compared to the microsecond.report.envelopes.witnessesmaps the index inbundle.entriesof every witness-signed entry to thewitness.kidof the envelope that verified for it.test/fixtures/parity/: CLI parity cases recorded from the Python CLI bytools/gen_fixtures.py(a boolean, a string, a missing and a null seq under--entries; a seq orvwritten1.0,2.0,3.0,9.0,-0.0…attenu-guard verifyends its summary line withungated=Nwhen the bundle has N allows that passed through un-gated, markedpolicy: "unlisted", which containment does not measure andactions_checkeddoes not…report.failure_entries, fromverifyBundle()andverifyEnvelopes(), in step withfailuresandfailure_details: the index inbundle.entriesof the entry each failure is about, ornullfor 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
nodeof the wrong type is a finding. - A value from a bundle could add a line to
attenu-guard verifyoutput. - A malformed
--witness-keysfile stopped the CLI with a stack trace. - Failure strings that differed from the Python implementation's.
witnessKeysrows are read whole, one row per kid.- A
seqorvfollows the schema's integer type, by the same rule in both implementations. - An anchor whose
seqorvis a number past 2^53 stoppedattenu-guard verify --hs256-keywith a stack trace. - An entry carrying a number past 2^53 stopped
attenu-guard verifywith a stack trace, with or without a key. - An allow whose
scopeis not a string stoppedattenu-guard verifywith a stack trace. - An allow whose
contextwas not an object, such as[1],5,"x"ortrue, 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_SIGNEDdoc 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
contextcarries a value of the wrong type for a ceiling, such as{"rows": "many"}, fails containment here, and the Python implementation raises on it delegationGraphlists an edge only for a parent that is a non-empty string, which keeps its exported type, where the Python implementation'sdelegation_graphlists 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)raisedEvidenceLeakErroron any run that refused a delegation.verify_bundlereported 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
policyon anallowentry (Policy.UNLISTED,"unlisted"), matching Python'sattenu_guard.reasons.Policy: an adapter running withallowUnlistedlet a tool with no declared policy run WITHOUT acheck(), and…verifyBundle()reportsungated: allow entries carryingpolicyare NOT containment-checked and NOT counted inactions_checked.ReasonCode.DELEGATION_REFUSED("delegation_refused"): an adapter refused a delegation before it reached the chain, so nothing was minted and there is nospawn_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
policyvalue the format does not define no longer excuses containment.
Changed
denials()foldsspawn_deniedalongsidedeny, 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, andcall_idon… verifyBundle()—witnessKeys(the trust set:[{kid, alg, public_key_hex}]or a{kid: publicKey}record) andenvelopeBytes(the envelope bytes as received, which onlyenvelope_non_canonicalneeds).- Seven named envelope failures, in
ENVELOPE_FAILURESand in the samefailures/failure_detailslist 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), revisionenvelope_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.algis checked against the contract , not against the trust-set row: v1 definesEdDSAand no other algorithm, so anything else isenvelope_unknown_witness.verifyBundle()reports on hostile bundle content and never throws.- Malformed caller inputs are refused rather than coerced: an
envelopeBytesentry that is not hex or bytes reportsenvelope_non_canonicalfor that envelope; awitnessKeysrow 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(wassubject is str/subject is string) and<level> member set is not an object… pyReprrenders booleans and containers the way Python'sreprdoes, so a hostile value interpolated into a failure string matches across the two implementationsexportBundle({ 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.tscompareschecksagainstexpected_reports.jsonas a superset rather than exactly: the fixture is written by the PINNED Python release, which predateschecks.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_ttlandreject_loosened_ceiling;versionstays…
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()— reportsfailure_detailsalongsidefailures, 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_ONLYallow no longer registers as pending, socomplete()no longer wedges forever waiting for an outcome.freeze()— ran attacker code before authorization on Proxy/boxed values; now checksProxyfirst, returnsFREEZE_UNSUPPORTEDinstead of a string.freeze()—"<accessor>"/"<circular>"sentinel strings could collide with a matching real argument; both now useFREEZE_UNSUPPORTED.interop-nextCI check — a failedpip index versionsquery silently read as "not published yet"; now fails the job instead.
Added
- Added execution binding (
schemaVersion: 2):check/recordOutcomebind call outcomes viacallId;verifyBundlereportsexecution_bindingcoverage.
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. verifyBundleandAuditLog.verifyAnchornow 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
c14nis 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-guard0.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 aVerifiedChain. - 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, anddelegationGraph/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, plusregisterCeilingfor custom types. - Added
exportBundlewith 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.pybuildstest/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.