Releases: attenu-io/attenu-guard
Releases · attenu-io/attenu-guard
Release list
0.10.0 — execution binding across all 17 adapters
Execution binding is now wired into every adapter the package ships: 17 frameworks in Python, LangGraph in TypeScript (attenu-guard-ts 0.5.0, released together). Each adapter records, per authorized call, what its wrapper actually observed — with the capture mode stating honestly what the framework's own hook surface can and cannot guarantee. Where a framework permits a sibling hook to skip or repeat the body, the adapter defaults to pre_hook_only and offers an opt-in strict mode with its residual gaps documented.
Also in this release:
- D14 fixed, both languages: a bare v2
check()followed bycomplete()wedged the lifecycle — the guard registered every v2 allow as pending regardless of capture mode. Registration is now gated on capture modes that promise terminal observation. (Python2256db5, TSb32b985.) - Snapshot hardening across every adapter in both languages: params snapshots are rebuilt without invoking copy protocols, accessors or iterators the value controls, and never alias live objects; values that cannot be rebuilt without running their code degrade the commitment to
params_hash_reason: "unsupported". - Wire vectors unchanged at 20. params_c14n_v1 unchanged at 11. Nothing anyone seeded from moves.
The full defect trail is in CHANGELOG.md; every finding came out of adversarial review against pinned framework sources before merge.
attenu-guard 0.9.0
Fixed
- Integers beyond the RFC 8785 safe range (±(2**53-1)) are now rejected — at canonicalization, at
RowLimit/SpendCap/CallLimitconstruction, and bywire.load(asmalformed) — instead of
silently colliding with a neighbouring integer once rendered through binary64. A tenth reject
vector,reject_unsafe_integer.json, brings the interop suite to 20. evidence.verify_bundleandAuditLog.verify_anchornow check the bundle/anchor schema version
and chain identity instead of ignoring them, so a bundle for the wrong version or the wrong chain
no longer verifies.
Added
AuditLog.appendnow raisesCommittedAuditError(carrying the committedentry) if persisting
an entry fails after it was already committed to the in-memory chain — the file write or a sink
raising no longer looks the same as nothing having been recorded. The entry stays committed;
callers must not retry the call that produced it on the strength of this error alone.- Execution binding, opt-in per chain via
Guard.issue(..., schema_version=2)(schema version 1
is unchanged and remains the default):check()/record_denial()now allocate acall_id
(fail-closed, with meters restored, if the CSPRNG fails) and return it onDecision.call_id;
check()gainsauthorized_params/capture/adapterand refuses further calls once the node
iscomplete()d (ReasonCode.NODE_FINALIZED).Guard.record_outcome(call_id, body_state, ...)
binds what a body-owning wrapper observed afterwards —returned/raised/abandoned/deferred,
witherror_coderequired exactly when raised. On aschema_version=2chain,complete()
returns a bool-coercibleCompletionResultand refuses while calls are pending; on
schema_version=1it still returns a plainbool, byte-and-type identical to every release
before 0.9.0.revoke()/revoke_agent()snapshot still-pending call_ids onto thekillentry
aspending_at_kill— atomically, under one hold of the chain lock, together with the
revocation itself and (incheck()) withcomplete()'s own check-pending-then-append sequence
— without clearing them, so a laterecord_outcome()after a kill is still accepted. Every
pre-commitcheck()/record_outcome()failure (not only CSPRNG exhaustion) rolls back its
meters/bookkeeping. Arguments are committed viaparams_c14n_v1(attenu_guard.params):
SHA-256(raw_salt || JCS(params)), never the raw value — closing, for this profile only, the
one gap the shared JCS canonicalizer leaves open for out-of-range integral floats, without
changing that canonicalizer's own behaviour elsewhere.evidence.verify_bundlegains
execution_binding: per-call observed/unobserved/unaccounted (an outcome counts as observed
only once it is bound correctly — right node, right order), per-node
finalized/in_progress/revoked_with_pending, an aggregate clean/incomplete/failed, and
params_coverage(computed over every call, not only those with an outcome) as its own axis —
not applicablefor a schema-version-1 bundle.verify_bundlealso rejects a rootless bundle
and accepts an optional independently retainedexpected_anchor/expected_head, so a rewritten
bundle whose own (self-consistent) anchor cannot be relied on is still caught. The LangGraph
adapter (adapters.langgraph) is the reference wiring:guard_node/DelegatedToolNodecall
record_outcomeon aschema_version=2guard, sync and async, from an immutable
pre-invocation argument snapshot (a callable that mutates its own inputs cannot cause a false
params mismatch), with generators/futures reporteddeferredandasyncio.CancelledError
reportedabandoned. Schema and verifier are event- and version-aware and strict: a v2 allow
REQUIREScapture/adapter(Guard.check()suppliespre_hook_onlyplus a guard-attributed
adapter when the caller passes neither — a barecheck()IS itself pre_hook_only observation,
never merely absent),denyFORBIDS every allow-only field, and a v1 entry FORBIDS every
v2-only field (includingcall_id— v1 never allocates one);tests/test_execution_binding.py
runs in CI. A language-neutralparams_c14n_v1parity vector file
(tests/vectors/params_c14n/params_c14n_v1.json, consumed bytests/test_params_c14n_vectors.py)
covers its accepted/rejected numeric boundaries and salt handling; the TypeScript consumer of
this same file is being built onattenu-guard-ts(feat/090-execution-binding) — parity
between the two is a release gate for 0.9.0, not deferred work.
Changed
- Behaviour change: constructing an
AuditLog(orGuard.issue) with apath/audit_paththat
already names a non-empty file now raisesFileExistsErrorinstead of silently truncating it.
Passoverwrite=True(Guard.issue(..., audit_overwrite=True)) to keep the old reset-on-open
behaviour where that is what you want.
attenu-guard 0.8.1
See CHANGELOG.md for 0.8.1.
attenu-guard 0.8.0
Changed
- Scope values now use one interoperable grammar: lowercase dot-separated
segments, with*permitted only as the complete final segment after a dot.
A terminal wildcard covers any depth below that segment boundary, but not the
bare prefix or an adjacent namespace. Constructors and wire verification
reject malformed scope syntax.
Added
reject_bare_wildcard.jsonandreject_nonterminal_wildcard.jsonbring the
interop suite to 19 vectors and pin malformed wildcard forms to the
malformedwire reason.
attenu-guard 0.7.1
Changed
c14nis informational; producers still emit it, while verifiers enforce RFC 8785 JCS from canonical bytes and hashes regardless of the label.
attenu-guard 0.7.0
Changed — BREAKING
- All signed and hash-linked artifacts now use RFC 8785 JCS exclusively. Delegation
tokens declare"c14n":"JCS"; their protected header and payload must already be
canonical JCS bytes. Audit entries, integrity seals, anchors, and evidence bundles use
the same canonicalizer and carry the same marker where the artifact has metadata.
Duplicate object members, non-finite numbers, lone surrogates, unmarked tokens, and
non-canonical encodings are rejected. The interop suite now contains 17 vectors,
including all six known Python/ECMAScript divergence classes. There is no legacy or
dual-format reader.
Added
- A ninth interop test vector,
reject_wildcard_boundary.json("expect_reject_reason": "not_narrower"), shipped in both copies and in the installed package. The leaf claims
crmx.readunder a root holding the wildcardcrm.*— a scope that shares the wildcard's
letters but not its segment boundary.crm.*coverscrm.followed by anything, so
crmx.readis a different namespace and no ancestor grants it. It closes the half the eighth
left open:reject_wildcard_widening.jsonpins the DIRECTION of the wildcard rule, and this
pins its REACH. An independent verifier that implements the wildcard by stripping the.*and
testingstartswith("crm")accepts the neighbouring namespace — the sloppy-prefix bug an
attacker uses to step sideways into the namespace next door — and so scored 8/8 while being
exploitable; it now fails a vector instead of shipping. The reference implementation already
rejected it (it strips only the*and keeps the dot); this is coverage, not a fix.
attenu-guard 0.6.1
Added
- An eighth interop test vector,
reject_wildcard_widening.json("expect_reject_reason": "not_narrower"), shipped in both copies and in the installed package. The leaf's scopes are
replaced with the wildcardcrm.*while its parent holds only the concretecrm.read, so the
leaf claims strictly more than any ancestor ever held. It pins down the direction of the
wildcard rule, which the existing seven left implicit:valid_chain.jsonshows a concrete
crm.readsitting legitimately under acrm.*parent, and this is that turned round. An
independent verifier that tests only whether a parent scope and a child scope are
wildcard-compatible, rather than which side is the broader one, accepts both directions and
lets a leaf hand itselfcrm.export— it now fails a vector instead of shipping. The reference
implementation already rejected it; this is coverage, not a fix.
attenu-guard 0.6.0
Added
- The interop test vectors ship inside the package as
attenu_guard.vectors
(VECTOR_NAMES,load_vector,load_vectors,read_vector_bytes, read through
importlib.resources). The Internet-Draft promises a chain that MUST verify and six that
MUST each be rejected for a named reason, so that an implementation written in ANY language
from the draft alone can score its own offline verifier; shipping them means doing that needs
pip install attenu-guardand no clone.tests/vectors/generate.pyis the single writer for
both copies — it serialises each vector once and writes those bytes totests/vectors/and
src/attenu_guard/vectors/— andtests/test_wire.pyasserts the two are byte-identical, so
they cannot diverge. A CI step verifies they survive an install, not just a checkout. - A2A adapter (
attenu_guard.adapters.a2a, extraa2a, tested againsta2a-sdk1.1.2): carries the attenuated
delegation chain across an Agent2Agent hop, so a remote agent in another process runs with permissions bounded by the
calling agent's. Two halves on public seams — client side, aDelegationInterceptor(ClientCallInterceptor.before)
mints the child withparent.delegate(...)and puts the signed Delegation Chain (attenu_guard.wire) on the outgoing
message as an A2A extension (Message.extensions+Message.metadata[<uri>], spec §4.6.2, with the
A2A-Extensionsheader §4.6.1); server side,GuardedAgentExecutorwraps the deployment'sAgentExecutor.execute,
verifies the chain offline (wire.load: signatures, parent-hash linkage, depth, child ⊆ parent at every hop, expiry)
and mints the servedGuardfrom the verified leaf, narrowed again by what the remote task needs. A missing, forged,
spliced, widened or expired chain — or any exception raised while deciding — refuses the request before the remote
agent's own logic starts, returning the denial contract in the extension's metadata slot.guarded_tool(fn, scope=…)
checks before each tool body;require_guard()refuses a tool reached outside the executor.verify_hop(tokens, signer, client_bundle=…, server_bundle=…)checks the caller's ledger, the remote ledger and the tokens that bind them
from those inputs alone, and reports an unsupplied bundle as "not checked" rather than as passing. This answers A2A
§7.6.4, which states that the protocol defines no scope, validity or revocation semantics for an in-task authorization
decision. Cross-process revocation propagation remains open: an expired chain is refused andrevocation_check=is the
seam for a status list, both documented as limits. Example (offline demo plus alive_smoke.pyverified over a real
Starlette/uvicorn HTTP hop) and 35 offline tests; seventeenth entry indocs/INTEGRATIONS.md.
attenu-guard 0.5.0
Added
- Haystack adapter (
attenu_guard.adapters.haystack, extrahaystack, tested againsthaystack-ai3.1.0): guards deepset HaystackAgents and pipelines throughTool.invoke/invoke_async(a subclass of each tool's own class, soComponentTool/AgentToolidentity and theinputs_from_state/outputs_to_stringmachinery are untouched), mints the childGuardat theAgentToolcall, and offers Haystack's ownbefore_toolConfirmationHookas an alternative denial path. Denials raise aToolInvocationErrorsubclass, so the Agent's existingraise_on_tool_invocation_failuredecides between "tell the model" and "stop the run". Parent tracking is aContextVar, so parallel delegations in one model turn are siblings, not a chain. Example + 26 offline tests; 13th framework indocs/INTEGRATIONS.md. - Two new framework adapters, both AutoGen successors.
attenu_guard.adapters.agent_frameworkfor Microsoft Agent Framework 1.15 (the AutoGen + Semantic Kernel successor) —DelegationGuard(FunctionMiddleware)gates every tool body through the one seam the framework's function-invocation loop can reach, and the same hook mints the childGuardatAgent.as_tool()andhandoff_to_<target>calls; denials come back as afunction_result, or asMiddlewareFailure(on_deny="failure") for a fail-closed abort.attenu_guard.adapters.ag2for AG2 1.0 (the AutoGen fork, a rewrite around theag2package) —DelegationGuard(BaseMiddleware).on_tool_executiongates the tool body and thetask_<agent>delegation call, plusguarded_tools()/guard_tool_hook()for per-tool middleware, the only hook that reaches a child AG2 constructs itself fromtasks=TaskConfig(...). Install withpip install 'attenu-guard[agent-framework]'/'attenu-guard[ag2]'. Offline demos underexamples/integrations/{agent_framework,ag2}/and 36 tests undertests/integrations/; matrix rows indocs/INTEGRATIONS.md. - Supply chain: every release now carries SLSA build provenance (sigstore attestation via
actions/attest-build-provenance); OpenSSF Scorecard runs weekly and on push; a.pre-commit-hooks.yamlexposesattenu-guard verifyas a pre-commit hook for committed evidence bundles.
Fixed
- Adapter docstrings still referred to the pre-rename paste-in module names (
dg_google_adk,dg_crewai,dg_smolagents,dg_llama_index) and said "paste/copy this file"; they now name the packaged modules (attenu_guard.adapters.<name>) and the matching extras.
attenu-guard 0.4.1
Fixed
- README: the install block still said "pre-publish… once published to PyPI"; the package has been on PyPI since 0.4.0. It now reads
pip install attenu-guard.
Changed
- Packaging: PyPI classifiers,
Documentation/Issues/Changelogproject URLs, and a summary aligned with the project description. No code changes.