Skip to content

Releases: attenu-io/attenu-guard

0.10.0 — execution binding across all 17 adapters

Choose a tag to compare

@github-actions github-actions released this 31 Aug 17:58

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 by complete() 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. (Python 2256db5, TS b32b985.)
  • 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

Choose a tag to compare

@github-actions github-actions released this 31 Aug 06:04

Fixed

  • Integers beyond the RFC 8785 safe range (±(2**53-1)) are now rejected — at canonicalization, at
    RowLimit/SpendCap/CallLimit construction, and by wire.load (as malformed) — 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_bundle and AuditLog.verify_anchor now 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.append now raises CommittedAuditError (carrying the committed entry) 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 a call_id
    (fail-closed, with meters restored, if the CSPRNG fails) and return it on Decision.call_id;
    check() gains authorized_params/capture/adapter and refuses further calls once the node
    is complete()d (ReasonCode.NODE_FINALIZED). Guard.record_outcome(call_id, body_state, ...)
    binds what a body-owning wrapper observed afterwards — returned/raised/abandoned/deferred,
    with error_code required exactly when raised. On a schema_version=2 chain, complete()
    returns a bool-coercible CompletionResult and refuses while calls are pending; on
    schema_version=1 it still returns a plain bool, byte-and-type identical to every release
    before 0.9.0. revoke()/revoke_agent() snapshot still-pending call_ids onto the kill entry
    as pending_at_kill — atomically, under one hold of the chain lock, together with the
    revocation itself and (in check()) with complete()'s own check-pending-then-append sequence
    — without clearing them, so a late record_outcome() after a kill is still accepted. Every
    pre-commit check()/record_outcome() failure (not only CSPRNG exhaustion) rolls back its
    meters/bookkeeping. Arguments are committed via params_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_bundle gains
    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 applicable for a schema-version-1 bundle. verify_bundle also rejects a rootless bundle
    and accepts an optional independently retained expected_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/DelegatedToolNode call
    record_outcome on a schema_version=2 guard, 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 reported deferred and asyncio.CancelledError
    reported abandoned. Schema and verifier are event- and version-aware and strict: a v2 allow
    REQUIRES capture/adapter (Guard.check() supplies pre_hook_only plus a guard-attributed
    adapter when the caller passes neither — a bare check() IS itself pre_hook_only observation,
    never merely absent), deny FORBIDS every allow-only field, and a v1 entry FORBIDS every
    v2-only field (including call_id — v1 never allocates one); tests/test_execution_binding.py
    runs in CI. A language-neutral params_c14n_v1 parity vector file
    (tests/vectors/params_c14n/params_c14n_v1.json, consumed by tests/test_params_c14n_vectors.py)
    covers its accepted/rejected numeric boundaries and salt handling; the TypeScript consumer of
    this same file is being built on attenu-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 (or Guard.issue) with a path/audit_path that
    already names a non-empty file now raises FileExistsError instead of silently truncating it.
    Pass overwrite=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

Choose a tag to compare

@github-actions github-actions released this 30 Aug 21:18

See CHANGELOG.md for 0.8.1.

attenu-guard 0.8.0

Choose a tag to compare

@github-actions github-actions released this 29 Aug 19:24

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.json and reject_nonterminal_wildcard.json bring the
    interop suite to 19 vectors and pin malformed wildcard forms to the
    malformed wire reason.

attenu-guard 0.7.1

Choose a tag to compare

@github-actions github-actions released this 29 Aug 12:22

Changed

  • c14n is 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

Choose a tag to compare

@github-actions github-actions released this 29 Aug 09:50

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.read under a root holding the wildcard crm.* — a scope that shares the wildcard's
    letters but not its segment boundary. crm.* covers crm. followed by anything, so
    crmx.read is a different namespace and no ancestor grants it. It closes the half the eighth
    left open: reject_wildcard_widening.json pins the DIRECTION of the wildcard rule, and this
    pins its REACH. An independent verifier that implements the wildcard by stripping the .* and
    testing startswith("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

Choose a tag to compare

@rafaelasor rafaelasor released this 29 Aug 04:34

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 wildcard crm.* while its parent holds only the concrete crm.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.json shows a concrete
    crm.read sitting legitimately under a crm.* 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 itself crm.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

Choose a tag to compare

@rafaelasor rafaelasor released this 29 Aug 04:34

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-guard and no clone. tests/vectors/generate.py is the single writer for
    both copies — it serialises each vector once and writes those bytes to tests/vectors/ and
    src/attenu_guard/vectors/ — and tests/test_wire.py asserts 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, extra a2a, tested against a2a-sdk 1.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, a DelegationInterceptor (ClientCallInterceptor.before)
    mints the child with parent.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-Extensions header §4.6.1); server side, GuardedAgentExecutor wraps the deployment's AgentExecutor.execute,
    verifies the chain offline (wire.load: signatures, parent-hash linkage, depth, child ⊆ parent at every hop, expiry)
    and mints the served Guard from 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 and revocation_check= is the
    seam for a status list, both documented as limits. Example (offline demo plus a live_smoke.py verified over a real
    Starlette/uvicorn HTTP hop) and 35 offline tests; seventeenth entry in docs/INTEGRATIONS.md.

attenu-guard 0.5.0

Choose a tag to compare

@rafaelasor rafaelasor released this 27 Aug 19:46

Added

  • Haystack adapter (attenu_guard.adapters.haystack, extra haystack, tested against haystack-ai 3.1.0): guards deepset Haystack Agents and pipelines through Tool.invoke/invoke_async (a subclass of each tool's own class, so ComponentTool/AgentTool identity and the inputs_from_state/outputs_to_string machinery are untouched), mints the child Guard at the AgentTool call, and offers Haystack's own before_tool ConfirmationHook as an alternative denial path. Denials raise a ToolInvocationError subclass, so the Agent's existing raise_on_tool_invocation_failure decides between "tell the model" and "stop the run". Parent tracking is a ContextVar, so parallel delegations in one model turn are siblings, not a chain. Example + 26 offline tests; 13th framework in docs/INTEGRATIONS.md.
  • Two new framework adapters, both AutoGen successors. attenu_guard.adapters.agent_framework for 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 child Guard at Agent.as_tool() and handoff_to_<target> calls; denials come back as a function_result, or as MiddlewareFailure (on_deny="failure") for a fail-closed abort. attenu_guard.adapters.ag2 for AG2 1.0 (the AutoGen fork, a rewrite around the ag2 package) — DelegationGuard(BaseMiddleware).on_tool_execution gates the tool body and the task_<agent> delegation call, plus guarded_tools() / guard_tool_hook() for per-tool middleware, the only hook that reaches a child AG2 constructs itself from tasks=TaskConfig(...). Install with pip install 'attenu-guard[agent-framework]' / 'attenu-guard[ag2]'. Offline demos under examples/integrations/{agent_framework,ag2}/ and 36 tests under tests/integrations/; matrix rows in docs/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.yaml exposes attenu-guard verify as 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

Choose a tag to compare

@rafaelasor rafaelasor released this 26 Aug 16:58

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 / Changelog project URLs, and a summary aligned with the project description. No code changes.