Skip to content

Commit 7110a26

Browse files
committed
feat: add BoundaryAttest-gated export handoff
1 parent aa2fa3a commit 7110a26

9 files changed

Lines changed: 607 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,27 @@ jobs:
7373
go-version: '1.26'
7474
- name: sync + export/import round trips
7575
run: bash scripts/verify-portability.sh
76+
77+
integration-boundaryattest:
78+
runs-on: ubuntu-latest
79+
steps:
80+
- uses: actions/checkout@v7
81+
- uses: actions/checkout@v7
82+
with:
83+
repository: cullenmeyers/BoundaryAttest
84+
ref: 89c6c4706b368651c2549d7e43de08c24f3079e5
85+
path: boundaryattest
86+
- uses: actions/setup-go@v7
87+
with:
88+
go-version: '1.26'
89+
- uses: actions/setup-python@v6
90+
with:
91+
python-version: '3.13'
92+
- name: install BoundaryAttest Python interop dependency
93+
run: python -m pip install -r boundaryattest/examples/python-interop-v0.1/requirements.txt
94+
- name: signed export/import handoff
95+
run: |
96+
go build -ldflags "-X main.version=boundaryattest-ci" -o /tmp/agentenv .
97+
bash examples/boundaryattest-export/demo.sh \
98+
"$GITHUB_WORKSPACE/boundaryattest/examples/python-interop-v0.1" \
99+
/tmp/agentenv

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ All notable changes to this project are documented here. The format is based on
66

77
## [Unreleased]
88

9+
### Added
10+
- Optional `examples/boundaryattest-export/` integration: signs an exported
11+
bundle with BoundaryAttest's strict Python interop envelope, verifies the
12+
expected key and agentenv-specific claim semantics, recomputes the received
13+
artifact hash, and imports only the same private staged bytes that passed the
14+
gate. Its fixture covers a full DAG with two branches and a `winner` tag plus
15+
signature, key, envelope, semantic-role, and bundle-tampering failures.
16+
917
## [0.4.0] - 2026-07-31
1018

1119
### Added

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# make vet gofmt check + go vet
1010
# make verify-rootless end-to-end rootless E2E (Docker on macOS)
1111
# make verify-portability export/import + DAG round trips (Docker on macOS)
12+
# make verify-boundaryattest signed export/import handoff (Docker, external checkout)
1213
# make verify-btrfs end-to-end privileged btrfs E2E (Docker, --privileged)
1314
# make dev-shell drop into a persistent dev container (fast inner loop)
1415
# make clean remove built artifacts
@@ -17,7 +18,7 @@ GOOS_LINUX ?= linux
1718
GOARCH ?= $(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/;s/arm64/arm64/')
1819
GOPROXY ?= $(shell if command -v go >/dev/null 2>&1; then go env GOPROXY; else printf '%s' 'https://proxy.golang.org,direct'; fi)
1920

20-
.PHONY: help demo build test vet verify-rootless verify-portability verify-btrfs verify-supervise verify-mcp verify-rollback openapi-snapshot dev-shell dev-shell-stop clean
21+
.PHONY: help demo build test vet verify-rootless verify-portability verify-boundaryattest verify-btrfs verify-supervise verify-mcp verify-rollback openapi-snapshot dev-shell dev-shell-stop clean
2122

2223
help:
2324
@awk 'BEGIN{FS=":.*##"; printf "Targets:\n"} /^[a-zA-Z_-]+:.*##/ { printf " %-22s %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
@@ -55,6 +56,17 @@ verify-portability: ## sync + export/import + full-DAG round trips (Docker)
5556
-e CGO_ENABLED=0 -e GOCACHE=/tmp/gocache -e GOMODCACHE=/tmp/gomodcache \
5657
-w /src golang:1.26 bash /src/scripts/verify-portability.sh
5758

59+
verify-boundaryattest: ## signed bundle verification gates import (set BOUNDARYATTEST_DIR)
60+
@test -n "$(BOUNDARYATTEST_DIR)" || { \
61+
echo "set BOUNDARYATTEST_DIR to a BoundaryAttest checkout" >&2; exit 2; }
62+
@test -f "$(BOUNDARYATTEST_DIR)/examples/python-interop-v0.1/verify_receipt.py" || { \
63+
echo "BoundaryAttest python-interop-v0.1 example not found" >&2; exit 2; }
64+
docker build -f examples/boundaryattest-export/Dockerfile \
65+
-t agentenv-boundaryattest-example .
66+
docker run --rm \
67+
-v "$(abspath $(BOUNDARYATTEST_DIR)):/boundaryattest:ro" \
68+
agentenv-boundaryattest-example
69+
5870
verify-btrfs: ## end-to-end privileged btrfs E2E (Docker, --privileged)
5971
docker run --rm --privileged \
6072
-v "$$PWD":/src -v "$$HOME/go/pkg/mod":/go/pkg/mod \

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ agentenv import snap.agentenv # or add a snapshot un
253253
snapshot bundle adds its node; a whole-repo bundle restores the full DAG into an
254254
empty `AGENTENV_ROOT`.
255255

256+
For cross-trust-domain handoffs, the optional
257+
[`boundaryattest-export`](./examples/boundaryattest-export/) example signs the
258+
exact bundle digest and refuses to import until the BoundaryAttest signature,
259+
expected key, agentenv claim semantics, and received bytes all verify. Python
260+
and cryptography stay outside the agentenv binary.
261+
256262
## Agent integration (socket API)
257263

258264
`agentenv daemon` serves a newline-delimited JSON protocol over a unix socket, so

examples/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ agentenv is **language-agnostic** on both sides:
55
- **Workloads**: capture/rollback happens at the *filesystem* level, so the agent
66
can build/run anything — `go build`, `mvn package`, `pip install`, `cargo build`,
77
`npm i` — and it's all snapshotted regardless of language.
8-
- **Drivers**: three ways to drive agentenv, each language-neutral:
8+
- **Drivers**: four ways to drive agentenv, each language-neutral:
99
1. **CLI**`agentenv checkout <id>`, `agentenv log`, `agentenv delete <id>`,
1010
`agentenv tournament …`. When a `daemon`/`supervise` holds the lock,
1111
mutating commands auto-route through its socket — same command works
@@ -79,3 +79,10 @@ one `docker run -it ghcr.io/css521/rewindable-claude` lands you in a sandbox
7979
where Claude Code can edit, install, and **roll back its OWN environment** via
8080
the `agentenv__checkout` / `agentenv__delete` MCP tools — without exiting the
8181
session.
82+
83+
## Signed export handoff
84+
85+
[`boundaryattest-export/`](./boundaryattest-export/) is an optional host-side
86+
adapter and full-DAG fixture. It uses BoundaryAttest's Python reference signer
87+
and verifier, binds the receipt to the exact exported bytes, and only then calls
88+
`agentenv import`. Neither Python nor signing-key custody enters agentenv core.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM golang:1.26 AS build
2+
3+
WORKDIR /src
4+
COPY go.mod go.sum ./
5+
RUN go mod download
6+
COPY . .
7+
RUN CGO_ENABLED=0 go build \
8+
-ldflags "-X main.version=boundaryattest-example" \
9+
-o /out/agentenv .
10+
11+
FROM python:3.13-slim
12+
13+
# Same range as BoundaryAttest's python-interop-v0.1 requirements.txt.
14+
RUN python3 -m pip install --no-cache-dir "cryptography>=41,<47"
15+
COPY --from=build /out/agentenv /usr/local/bin/agentenv
16+
COPY examples/boundaryattest-export /opt/agentenv-boundaryattest
17+
18+
ENV HOME=/tmp
19+
USER 1001:1001
20+
ENTRYPOINT ["bash", "/opt/agentenv-boundaryattest/demo.sh"]
21+
CMD ["/boundaryattest/examples/python-interop-v0.1", "/usr/local/bin/agentenv"]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# BoundaryAttest-gated export/import
2+
3+
This optional example signs an exact `agentenv export` bundle with
4+
[BoundaryAttest's Python Interop Profile v0.1 example][interop], then refuses to
5+
run `agentenv import` until all of these checks pass:
6+
7+
1. BoundaryAttest accepts the strict three-field envelope, expected Ed25519
8+
public key, canonical claim, and signature.
9+
2. The claim says `server_attested`, `agentenv.export_bundle`, `success`, and a
10+
supported `export_kind` (`full_dag` or `snapshot`).
11+
3. SHA-256 of the received bundle bytes matches `claim.artifact_hash`.
12+
4. `agentenv import` reads the same private staged copy that was hashed.
13+
14+
The final step avoids a check/use race: changing the received path after it was
15+
hashed cannot switch the bytes that agentenv imports.
16+
17+
## Run the fixture
18+
19+
Keep BoundaryAttest as a separate checkout; no Python or crypto dependency is
20+
added to the agentenv binary. This example is tested against BoundaryAttest
21+
commit `89c6c4706b368651c2549d7e43de08c24f3079e5`.
22+
23+
```bash
24+
git clone https://github.com/cullenmeyers/BoundaryAttest.git /tmp/BoundaryAttest
25+
git -C /tmp/BoundaryAttest checkout 89c6c4706b368651c2549d7e43de08c24f3079e5
26+
27+
make verify-boundaryattest BOUNDARYATTEST_DIR=/tmp/BoundaryAttest
28+
```
29+
30+
The Docker target creates a parent with two branch tips, tags candidate B as
31+
`winner`, exports the complete DAG, signs its digest, verifies and imports it
32+
into an empty root, and checks that HEAD, both branches, the tag, and their files
33+
survived. It also proves that import is not reached for:
34+
35+
- a one-byte bundle change (`artifact_hash_mismatch`);
36+
- an edited signed claim (`invalid_signature`);
37+
- a different expected key (`public_key_id_mismatch`);
38+
- an extra envelope field (strict-envelope rejection); and
39+
- a validly signed `client_observed` claim (agentenv semantic-policy rejection).
40+
41+
On Linux, the same fixture can run without Docker:
42+
43+
```bash
44+
python3 -m venv /tmp/agentenv-boundaryattest-venv
45+
/tmp/agentenv-boundaryattest-venv/bin/pip install \
46+
-r /tmp/BoundaryAttest/examples/python-interop-v0.1/requirements.txt
47+
go build -o /tmp/agentenv .
48+
49+
PATH=/tmp/agentenv-boundaryattest-venv/bin:$PATH \
50+
bash examples/boundaryattest-export/demo.sh \
51+
/tmp/BoundaryAttest/examples/python-interop-v0.1 /tmp/agentenv
52+
```
53+
54+
## Adapter usage
55+
56+
The receiver supplies the bundle, receipt, an independently trusted public key,
57+
an empty destination root, and a local BoundaryAttest reference checkout:
58+
59+
```bash
60+
python3 examples/boundaryattest-export/verify_and_import.py \
61+
environment.agentenv.gz \
62+
environment.agentenv.gz.receipt.json \
63+
host-public-key.pem \
64+
--interop-dir /path/to/BoundaryAttest/examples/python-interop-v0.1 \
65+
--agentenv /usr/local/bin/agentenv \
66+
--import-root /fresh/agentenv-root
67+
```
68+
69+
The signer and receipt store belong outside the rewindable rootfs. The checked-in
70+
BoundaryAttest demo key is only a deterministic test fixture; use a separately
71+
managed host/orchestrator key in a real handoff.
72+
73+
Passing this gate proves that the expected key signed the claim and that the
74+
imported bytes match its artifact digest. It does not prove the claim was true,
75+
the export was authorized, the internal snapshots are tamper-evident, or either
76+
runtime was uncompromised.
77+
78+
[interop]: https://github.com/cullenmeyers/BoundaryAttest/tree/main/examples/python-interop-v0.1

0 commit comments

Comments
 (0)