Skip to content

v0.2.3 — the command the hook names is one you have

Latest

Choose a tag to compare

@XuebinMa XuebinMa released this 07 Sep 05:39
· 2 commits to main since this release
68d10fa

The product is built on one path: your agent is stopped, you are told what to run, you run it. Until this release that path was broken at three different steps, and each break was found by walking it rather than by a test.

0.2.3 closes all three.

npx agent-guard-plugin init          # now installs both binaries
# your agent runs: git push origin main
# the hook says:   …run: agent-guard push --remote origin --branch main
agent-guard push --remote origin --branch main

That last line used to fail twice over: agent-guard was never installed, and even when present it died on a missing --policy. It now runs as printed, defaults to the policy the hook itself was wired to, and prints which policy judged the push.

Install

npx agent-guard-plugin init
cargo install agent-guard-cli --locked
pip install agent-guard-python

See it before you install it

./demos/push-broker/demo.sh

The whole broker path against a throwaway repository and a local remote — no credentials, nothing of yours touched. The recording in the README is generated from that same script, so it cannot drift from what the binaries actually print.

What this does not do

Keeping credentials away from the agent is a deployment decision this code cannot enforce. The broker uses whatever credential its own process holds, which is a boundary only if the agent has none of its own. Credential isolation says how to get that, and gives you a one-command check that tells you whether you actually have it rather than leaving you to assume.


Added

  • guard-verify scores the attenu-guard observer-envelope corpus: 18 of 18,
    first run.
    An observer envelope is an Ed25519 signature over the identity
    of one committed ledger entry — the question a ledger cannot answer about
    itself, which is whether anything outside the writing process ever saw the
    event. guard-verify attenu-envelope-vectors runs the published corpus;
    verify_bundle_with_envelopes is the API.

    The first number is 18 of 18, and that is not the verifier getting
    better.
    The bundle corpus scored 9 of 17 first, with every check right and
    every reason name wrong. That result was the argument for publishing the
    envelope vectors as text before anyone implemented them, and attenu-guard's
    README quotes it as the reason they did. So a clean first run is the
    process working, not the implementation being sharper — and it says only
    that two implementations agree on one frozen corpus.

    Both permitted extras land where the corpus says they may: an
    envelope_bad_signature alongside envelope_non_canonical on the row whose
    bytes were re-signed, and a second envelope_subject_mismatch on the other
    covered hop of a rehashed chain.

    Three decisions the corpus pins that are worth naming:

    • The entry hash is recomputed, never read. Reading the stored hash
      would let the edit that moved an entry also move what the envelope is
      compared against, which is exactly the attack the envelope exists to catch.
    • An entry is claimed the moment subject.seq finds it, before the
      envelope is judged on anything else, so a second envelope over one entry
      cannot escape the one-envelope rule by also being malformed. Without that,
      array order decides the state.
    • witness.alg is contract, not negotiation. Comparing it only against
      the trust-set row accepts "none" the moment both sides say so; ignoring
      it hands a non-Ed25519 envelope to an Ed25519 verifier and blames the
      signature, which was never the problem.

    Written from the published format description, without reading either
    reference implementation — the same discipline as the bundle verifier, since
    agreement is evidence about the format only when the code is not shared. The
    fixture's sha256 was checked against the published hash before scoring, and
    is pinned by a test.

  • Credential isolation has a page, and a way to check it. The README said
    twice that keeping credentials away from the agent is a deployment decision
    this code cannot enforce, and both times stopped there. A reader who wanted
    the property had a disclaimer and no instructions.

    Credential isolation states
    the requirement in one sentence — the push credential must live somewhere the
    agent's process cannot read — and is explicit that a default install
    satisfies none of it, because the agent and the broker are the same user with
    the same ~/.ssh. It gives the deployment that does hold (the agent in a
    container, the credential on the host), the weaker same-machine measure that
    is sometimes all you can do (a hardware key requiring a touch), and says
    exactly how much less the second one buys.

    The part worth having is the check: git push --dry-run from the agent's
    environment authenticates without updating a ref, so it separates "cannot
    push" from "can push" unambiguously and safely. Configuration nobody has
    tested is a belief, and a security boundary held as a belief is the failure
    this project keeps trying not to ship.

Fixed

  • The version bumper enumerates crate manifests instead of listing them.
    scripts/release/bump-version.sh carried a hand-written list of
    crates/*/Cargo.toml, and agent-guard-broker — added at 0.2.2, after that
    list was written — was silently left behind on this bump. Its =0.2.2 pin
    was unsatisfiable against a 0.2.3 workspace, and the only thing that noticed
    was cargo failing to resolve: scripts/check-version-consistency.sh reported
    the tree consistent, because it checks the version markers and not the
    inter-crate pins. The list is now a filesystem glob, so a crate added
    tomorrow is covered today.

  • The plugin now installs the command the hook names. npx agent-guard-plugin init
    installed guard-hook and nothing else, while the gate it wired up printed
    agent-guard push --remote origin --branch main when it stopped a push. That
    command ships in agent-guard-cli, which was never installed and never
    mentioned — so the documented first run ended at command not found.

    This is the same dead end as the fix below, one step further out: that one
    made the command parse, and a human still did not have the binary. init
    installs both crates, reports a partial install per binary rather than as one
    success, and says what each missing binary costs.

    A test reads the hint's own source, extracts every <name> push --remote
    command it prints, and asserts the plugin installs <name>. The invariant
    spans a Rust crate and a Node installer, which is why two rounds of testing
    each side passed while the path between them was broken.

  • The command the hook tells you to run now runs. A refused push printed
    agent-guard push --remote origin --branch main, and running exactly that
    died on a missing --policy. The hint added in 0.2.2 existed to remove a
    dead end, and it had moved the dead end one step later instead.

    --policy is now optional, resolving to $AGENT_GUARD_POLICY and then to
    the policy npx agent-guard-plugin init installs. That default is not an
    arbitrary guess: it is the file the hook itself is wired to, so the push is
    evaluated against the same rules that refused it rather than a different
    set. With no policy there at all, the error names the path it tried and how
    to get one, rather than printing a usage line.

    The policy in force is printed in the preview whether or not it was named on
    the command line. Approving a push means approving it under some set of
    rules, and a default that goes unstated is a rule set the person deciding
    never saw.

    A test asserts the CLI accepts the exact argument shape the hook prints.
    That invariant spans two crates, which is why nothing caught it breaking.

  • The broker crate's documentation described a crate that no longer exists.
    agent-guard-broker's crate-level docs were written when only the
    transaction resolver had landed, and still announced "No credential
    handling, no authorization, no execution, no receipt" — with issue_grant,
    execute_push, execute_push_with_receipt and PushReceipt exported ten
    lines below. Understating which properties are present is the safe direction
    to be wrong in, and it is still a false claim about a security boundary,
    sitting in the first thing a reader of the crate sees.

    The docs now describe the path the crate runs — resolve, grant, spend
    against a freshly resolved transaction, push with both ends pinned, receipt
    — and say which parts remain the caller's: policy is evaluated above this
    crate, and what the crate enforces is that the policy has not changed since
    the approval. The boundary that is still true is kept: credential isolation
    is a deployment property this code cannot verify.