AgentGit MVP¶
AgentGit is a thin provenance layer over normal git. The first MVP lets an agent evaluate its current worktree diff against a task-scoped policy and emit a signed JSON bundle for CI, reviewers, or another agent to inspect.
python -m swarm.agentgit attest \
--task issue-123 \
--agent codex \
--policy examples/agentgit_policy.yaml \
--check pytest=pass \
--output .agentgit/provenance.json
The bundle records:
- agent and task identity
- base commit and changed files
- additions/deletions by file
- path and size policy decisions
- required check results
- a
provenanceblock of what happened producing the diff (see below) - sealed admissibility receipt with the payload hash
Policy failures return a non-zero exit code by default. Use --warn-only when
you want to capture the bundle without blocking the current command.
Provenance Contents¶
Beyond the diff and policy verdict, schema agentgit.provenance.v1 records a
provenance block describing how the change was produced:
commands— commands executed (binary + args,return_code, OSisolationbackend,duration_seconds, and atimed_outflag). Build these from a worktreeCommandResultviaCommandRecord.from_command_result(result).environment— model / runtime / version of the producing agent.dependency_changes— manifest/lockfile edits (requirements.txt,pyproject.toml,package-lock.json,Cargo.lock,go.sum, …) detected automatically from the diff.sources— external sources consulted.reviews— reviewer decisions.overrides— human overrides.
from swarm.agentgit import CommandRecord, build_bundle
bundle = build_bundle(
...,
commands=[CommandRecord(command=["pytest", "-q"], return_code=0, isolation="bwrap")],
environment={"model": "claude-opus-4-7", "runtime": "python3.13"},
sources=["https://example.com/issue/123"],
reviews=[{"reviewer": "security", "decision": "approve"}],
)
The provenance block is folded into the signed receipt payload, so it is
tamper-evident: editing a recorded command or hiding a dependency change
makes verify_bundle fail the payload_hash check. Older v0 bundles (hashed
without provenance) still verify — verify_bundle reconstructs the payload per
schema version.
Conditional Policy & CI Gate¶
Beyond the fixed limits (allowed/denied paths, file/line caps, required checks),
a policy may carry conditional rules — when <condition> then <action>:
rules:
- id: deps-need-supply-chain-scan
when: {dependency_changed: true}
action: require_check
check: supply-chain-scan
- id: auth-needs-security-review
when: {paths_match: ["*auth*", "*security*"]}
action: require_review
- id: tests-must-pass
when: {check_failed: pytest}
action: deny
Conditions (ANDed): paths_match (fnmatch globs), dependency_changed,
added_lines_gt, changed_files_gt, check_failed, check_passed. Actions:
deny, require_check (needs check:), require_review. A firing blocking
rule passes if a human override names its id in provenance.overrides
({rule: <id>, by: ..., reason: ...}) — this is the "block unless override"
escape hatch. severity: warning rules surface but never block.
Rules evaluate at attest time (folded into the signed bundle decisions), so
verify already enforces them. They also run as a CI gate against an
already-attested bundle, using a policy CI/the org controls — judging what the
agent actually did against what's allowed, independent of the policy the agent
self-attested with:
python -m swarm.agentgit gate \
--bundle .agentgit/provenance.json \
--policy .github/agentgit.policy.yaml # exits non-zero on any blocking rule
gate first verifies the bundle's signature (failing closed on tampering or
malformed input), then evaluates the policy against the bundle's recorded facts
(changed files, totals, dependency changes) — so a stricter CI policy catches
violations even if the agent attested against a lax one. Dependency facts are
derived from the signed diff (git.changed_files), not the provenance
block, so dependency_changed rules fire even on older v0 bundles where
provenance is unsigned. The gate also requires an explicit signing key
(--signing-key or AGENTGIT_SIGNING_KEY) and fails closed if neither is set —
it never falls back to the public dev key, so a misconfigured CI job can't accept
a dev-key-signed bundle as authentic. Crucially, two agent-supplied fields are
not trusted at the gate:
provenance.overrides— an agent could otherwise pre-approve the rule meant to catch it. A CI override must be supplied explicitly with--override <rule-id>from a CI-controlled source.checks— the agent authored these, so a check-based rule (e.g.tests-must-pass:when: {check_failed: pytest} then deny) would be defeated by an agent self-attestingchecks={"pytest": true}. At gate time, supply the CI-authoritative result with--check <name=pass|fail>; unsupplied checks fail closed (the rule blocks until CI vouches for the result).
python -m swarm.agentgit gate \
--bundle .agentgit/provenance.json \
--policy .github/agentgit.policy.yaml \
--signing-key "$AGENTGIT_SIGNING_KEY" \
--check pytest=pass \
--override deps-need-supply-chain-scan
Worktree Loop¶
AgentGit also plugs into the worktree sandbox bridge:
python -m swarm.bridges.worktree create codex
python -m swarm.bridges.worktree exec codex -- python -m pytest tests/test_agentgit.py
python -m swarm.bridges.worktree attest codex \
--task issue-123 \
--policy examples/agentgit_policy.yaml \
--check pytest=pass \
--output .agentgit/provenance.json
python -m swarm.agentgit verify .agentgit/provenance.json
That gives the first complete local loop:
Cryptographic Identity & Delegation¶
The MVP signs bundles with a shared HMAC key, which proves a bundle was sealed
by someone holding the key but not which agent produced the change. The
swarm.agentgit.identity module adds verifiable identity with Ed25519
(asymmetric) signatures.
AgentKeypair— an Ed25519 keypair. Itsdidisdid:key:ed25519:<hex>, so the public key is embedded in the identifier and verifiers need no key registry.AgentIdentity— the agent's DID plus owner/org and model/runtime/version provenance and itsallowed_tools.DelegationChain— an ordered, individually-signedhuman -> org -> agentchain.verify()checks every link's signature, that the chain is connected (each link's subject issues the next), that permissions only narrow down the chain, and that no link has expired.
When identity + agent_keypair (and optionally delegation) are passed to
build_bundle, the agent's key signs the receipt payload_hash, binding a
verifiable identity to that exact diff:
from swarm.agentgit import AgentIdentity, AgentKeypair, build_bundle, sign_link, DelegationChain
org = AgentKeypair.generate()
agent = AgentKeypair.generate()
identity = AgentIdentity.for_keypair(agent, owner="alice", org="acme", allowed_tools=["read", "test"])
chain = DelegationChain(links=[sign_link(org, subject_did=agent.did, permissions=["read", "test", "open_pr"])])
bundle = build_bundle(..., identity=identity, agent_keypair=agent, delegation=chain)
verify_bundle then additionally checks the identity signature, the delegation
chain, that the chain's final subject is the signing identity, and that the
identity's allowed_tools stay within the delegated grant. Bundles built
without identity blocks still verify (backward compatible).
The CLI (
attest/verify) does not yet manage keypairs — that key-storage surface is tracked as a follow-up. Today identity is wired through the library API.
Capability Enforcement¶
Verifying a delegation chain is still advisory — it proves what an agent was
allowed to do without stopping it from doing more. swarm.agentgit.capabilities
turns a verified chain into the command allowlist the worktree sandbox
physically enforces, closing the loop identity → delegation → enforcement.
CAPABILITY_COMMANDS maps permission tokens to the command binaries they
authorize (read → ls/cat/grep/…, test → pytest/python, vcs → git,
etc.). enforced_allowlist_for_chain verifies the chain and returns the granted
commands — or, on any verification failure, an empty allowlist (deny by
default).
from swarm.bridges.worktree.config import WorktreeConfig
from swarm.bridges.worktree.policy import WorktreePolicy
policy = WorktreePolicy(WorktreeConfig())
ok, errors = policy.apply_delegation("codex", chain, expected_subject_did=agent.did)
# Now only the delegated capabilities execute:
policy.evaluate_command("codex", ["pytest", "tests/"]).allowed # True (test granted)
policy.evaluate_command("codex", ["git", "status"]).allowed # False (vcs not granted)
An invalid, expired, or over-scoped chain installs an empty allowlist, so the
agent can run nothing until a valid delegation is supplied. Unconditional
hard-blocks (ssh/scp, git push|fetch|pull|clone) still apply regardless of
what was delegated.
This slice enforces command capabilities (which binary may start).
OS-Level Isolation¶
Gating which binary starts is not enough: subprocess.run(cmd, cwd=sandbox)
runs an ordinary child process, so an allowlisted python can still write
anywhere and open sockets. swarm.bridges.worktree.sandbox_launch wraps the
executed command in a real OS confinement that limits filesystem writes to
the sandbox and blocks network egress:
- macOS →
sandbox-execwith an SBPL profile (denyfile-write*outside the sandbox + temp, denynetwork*). - Linux →
bwrap(read-only root, read-write bind on only the sandbox subtree, private empty network namespace via--unshare-net).
Opt-in via WorktreeConfig:
WorktreeConfig(os_isolation_enabled=True) # wrap when a backend exists
WorktreeConfig(os_isolation_enabled=True, require_os_isolation=True) # fail-closed
When enabled but no backend is available (e.g. CI/Linux without bwrap), the
command still runs and CommandResult.isolation is recorded as "none" — the
isolation status is never silent. Set require_os_isolation=True to instead
deny execution when no backend exists. Reads are not restricted in this
slice (interpreters need their stdlib); a stronger read-confining jail and
short-lived scoped git push tokens remain follow-ups.