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 initcargo install agent-guard-cli --lockedpip install agent-guard-pythonSee it before you install it
./demos/push-broker/demo.shThe 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-verifyscores 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-vectorsruns the published corpus;
verify_bundle_with_envelopesis 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_signaturealongsideenvelope_non_canonicalon the row whose
bytes were re-signed, and a secondenvelope_subject_mismatchon 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.seqfinds 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.algis 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. - The entry hash is recomputed, never read. Reading the stored
-
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-runfrom 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.shcarried a hand-written list of
crates/*/Cargo.toml, andagent-guard-broker— added at 0.2.2, after that
list was written — was silently left behind on this bump. Its=0.2.2pin
was unsatisfiable against a 0.2.3 workspace, and the only thing that noticed
was cargo failing to resolve:scripts/check-version-consistency.shreported
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
installedguard-hookand nothing else, while the gate it wired up printed
agent-guard push --remote origin --branch mainwhen it stopped a push. That
command ships inagent-guard-cli, which was never installed and never
mentioned — so the documented first run ended atcommand 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.--policyis now optional, resolving to$AGENT_GUARD_POLICYand then to
the policynpx agent-guard-plugin initinstalls. 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" — withissue_grant,
execute_push,execute_push_with_receiptandPushReceiptexported 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.