# EZA-001 R001 · local-object-v1 — developer walkthrough

**Status: EXPERIMENTAL. Preview material, not published.**

This guide walks one developer through the reviewed reference-based adapter by
hand: an operator provisions one disposable synthetic domain and one single-use
grant, a requesting agent submits one exact request under its own operating
system identity, and a trusted observer reports what actually happened to the
protected object.

You will not run `acceptance.py` here. The harness proves the adapter to a
reviewer; this guide shows you the actual operator, requester and observer
steps. **A successful harness run and a usable walkthrough are different
results.**

## Governed scope

The only governed effect in this integration is **writing one exact value to one
synthetic local object inside a disposable domain**. Nothing else is governed.
No email, no cloud change, no arbitrary shell execution for an agent, no general
filesystem operations. Account creation, file permissions and shell commands
here are *setup mechanisms* of a throwaway machine, not governed actions.

Three separate things, none of which implies the next:

1. **Verifying downloaded files** — hashes and signature scope.
2. **Reproducing the reference tests** — the release's own reproduction path.
3. **Integrating enforcement into an agent's environment** — this guide.

## 1. Prepare a disposable Linux environment

**Do not use your everyday Mac, your work laptop or shared cloud
infrastructure.** Use a throwaway Linux VM or container you can delete.

Requirements:

| Requirement | Why |
| --- | --- |
| Linux with real UID separation (`setpriv` available, `util-linux`) | the boundary being demonstrated is an OS identity boundary |
| Administrator (`root`/`sudo`) throughout | identity switching, protected reads, request files, process control |
| Python **3.11+** | the frozen reference requires it |
| A **dedicated administrator-owned Python runtime** carrying the frozen reproduction pins `jsonschema==4.25.1` and `cryptography==46.0.0` — build it with section 1a | the reference evaluator imports them |
| `sha256sum`, `install`, `find`, `curl`, `unzip`/`python3 -m zipfile` | preflight and extraction |

### 1a. Build the dedicated Python runtime (do this first)

**`sudo python3 -m pip install jsonschema` does not work on a current Debian or
Ubuntu system** and you should not force it. On Ubuntu 24.04 (Python 3.12.3) that
command fails with `error: externally-managed-environment`, exit 1. Do **not**
pass `--break-system-packages`, do not remove `EXTERNALLY-MANAGED`, and do not
rely on the distribution's own `python3-jsonschema` (Ubuntu 24.04 ships 4.10.3,
which is not the pinned version this reference was reproduced with).

Instead create one throwaway-VM-wide virtual environment that **root owns** and
the requesting identity cannot write to, and use *its* interpreter for every
command in this guide:

```bash
# operator (ordinary shell, sudo available)
sudo apt-get update
sudo apt-get install -y python3-venv util-linux curl unzip

sudo python3 -m venv /opt/eza-runtime
sudo /opt/eza-runtime/bin/python -m pip install --no-cache-dir \
  jsonschema==4.25.1 cryptography==46.0.0

# root-owned, not writable by any other identity
sudo chown -R root:root /opt/eza-runtime
sudo chmod -R go-w /opt/eza-runtime
sudo find /opt/eza-runtime -type d -exec chmod 0755 {} +
```

Fix the runtime location once, and use exactly this variable everywhere below:

```bash
export EZA_BIN=/opt/eza-runtime/bin
```

**Activation is not enough.** `source .../activate` only changes your own
interactive shell; it does not follow `sudo`, `setpriv`, background service
children, or `python3 -I` (isolated mode ignores `PYTHONPATH` and user site
packages). Every command in this guide therefore names `"$EZA_BIN/python3"`
explicitly *and* puts `$EZA_BIN` first on `PATH` inside the elevated context, so
any child process the reference starts uses the same interpreter.

Confirm the runtime before continuing — as root, and under both enforcement
identities, which setup creates in section 3 (run the two `setpriv` checks again
after setup if you prefer):

```bash
sudo "$EZA_BIN/python3" -I -c 'import sys, importlib.metadata as m; \
print(sys.executable, sys.version.split()[0], m.version("jsonschema"), m.version("cryptography"))'

for u in 4100 4101; do
  sudo setpriv --reuid $u --regid $u --groups $u -- "$EZA_BIN/python3" -I -c \
    'import os, sys, jsonschema, cryptography, importlib.metadata as m; \
print("uid", os.getuid(), sys.executable, m.version("jsonschema"), m.version("cryptography"))'
done
```

Each line must print the `/opt/eza-runtime` interpreter with `4.25.1` and
`46.0.0`. The requesting identity must be able to *run* it and never to
*change* it:

```bash
sudo setpriv --reuid 4101 --regid 4101 --groups 4101 -- "$EZA_BIN/python3" -I -c \
  "import errno
try:
    open('/opt/eza-runtime/bin/python3', 'ab')
except OSError as e:
    print('denied', errno.errorcode[e.errno])
else:
    print('WRITABLE - stop and fix the permissions')"
```

Expect `denied EACCES`. If the requester can write the interpreter or its
site-packages, the boundary this walkthrough demonstrates is not intact.

### Operator privilege model (read this before pasting anything)

Every command below is run from **one ordinary operator shell that can use
`sudo`**. You do not need a root login shell, and you should not assume one.

* Setup, provisioning, starting and stopping the service, creating the request
  files, reading protected state and exporting evidence all require
  administrator privilege — **not setup alone.**
* Enforcement itself never runs as an administrator. The service runs as uid
  **4100** and refuses to start as root; the requester runs as uid **4101**.
* **Redirection must happen inside the elevated context.** Your operator shell
  opens `>` and `>>` files *before* `sudo` runs, and the evidence directory is
  mode `0750` owned by uid 4100 — an ordinary operator cannot open a file there.
  Every command that writes into the root therefore has the form
  `sudo sh -c '... > "$1/..."' sh "$ROOT"`, where the root shell performs the
  redirection. Do not "simplify" those commands by moving `>` outside `sudo`.

Identities created by setup (fixed numeric ids so peer-credential mapping is
explicit):

| Identity | uid/gid | Role |
| --- | --- | --- |
| `eza-svc` | 4100 | trusted enforcement service, observer, operator provisioning |
| `eza-agent` | 4101 | the requesting agent — **mapped** to actor `agent:B` |
| `eza-stranger` | 4102 | reaches the socket, mapped to **no** actor |
| `eza-req` | gid 4200 | may reach the request socket, nothing else |

## 2. Obtain and verify the two inputs

### 2a. The frozen release archive (signed release material)

```bash
curl -fSL -o /tmp/rc9.zip \
  https://eziah.dev/releases/eza-001/r001/EZA-001-R001-rc.9.zip
sha256sum /tmp/rc9.zip
```

Expected exactly:

```
3062213046b50db71ddbf13160a9711a25b2e8b1dd882a3bf35e007958b22e39  /tmp/rc9.zip
```

Size: **15,496,913 bytes**. Pinned release manifest SHA-256, used as
`--expected-manifest` in every command below:

```
fc44b015cbf152aef4e4cc25bc0bdc6d58f6763a0af871b81795de04e32f8f3e
```

If the hash differs, stop. Do not continue with different bytes.

### 2b. The reviewed adapter (separately identified, NOT signed release material)

The adapter is this directory, `integration/local-object-v1/`. It is delivered
as the attached review package while the Developers page and this guide remain in
preview; there is no public adapter download yet.

**Integrity scope, stated plainly:** the adapter's per-file hashes live in the
review package's `FILE-INVENTORY.json` and were measured at packaging time.
They are **outside the release manifest's signature scope**. The minisign
signature covers the release manifest and the files bound through its inventory
— it does not cover these adapter files, this guide, or any evidence record.

Adapter file inventory (executable files that participate in a run):

| File | Role |
| --- | --- |
| `setup_env.sh` | administrator setup of the disposable environment |
| `identity_preflight.py` | refuses identity conflicts before any mutation |
| `provision.py` | **operator**: issues one domain, one grant lineage, one object |
| `service.py` | **trusted service**: unix-socket front door over `Endpoint.call` |
| `agent_client.py` | **requester**: submits one framed envelope; reports OS answers |
| `observe.py` | **trusted observer**: read-only inspection of protected state |
| `acceptance.py` | the 54-check harness (not used in this guide) |
| `controls.py` | negative controls for the harness (not used in this guide) |
| `export_evidence.sh` | verified evidence export, required before cleanup |
| `cleanup.sh` | safeguarded removal of a marked disposable root |

## 3. Operator: set up, then provision one domain

Everything in this section is **operator** work. The requester never runs it and
never reads its outputs directly.

Fix one disposable root now and use exactly this value everywhere below. Every
example in this guide uses `/tmp/eza-wt-001`; if you choose another path, change
it in this one place only.

```bash
# operator (ordinary shell, sudo available), from the adapter directory
export ROOT=/tmp/eza-wt-001
export RELEASE=$ROOT/release/EZA-001-R001-rc.9
export MANIFEST=fc44b015cbf152aef4e4cc25bc0bdc6d58f6763a0af871b81795de04e32f8f3e
export EZA_BIN=/opt/eza-runtime/bin           # from section 1a

# setup_env.sh calls `python3` from PATH; give it the dedicated runtime.
sudo env "PATH=$EZA_BIN:/usr/bin:/bin:/usr/sbin:/sbin" \
  bash setup_env.sh "$ROOT" /tmp/rc9.zip
```

The `env "PATH=…"` prefix is required, not cosmetic: `sudo` resets `PATH` to its
own secure path, and the setup script's internal Python steps (archive
extraction, identity preflight, the installed-file record) must run on the same
pinned interpreter as everything else. The recorded `interpreter.executable` in
`$ROOT/service/installed.json` should read `/opt/eza-runtime/bin/python3`.

Expected tail:

```
run_id=run-<timestamp>-<random>
release_dir=/tmp/eza-wt-001/release/EZA-001-R001-rc.9
root=/tmp/eza-wt-001
```

The target **must not already exist**; setup refuses an existing path rather
than deleting it. Pick a fresh directory each time.

Record the run identity and a directory for your own manual records. Both are
written inside the elevated context:

```bash
sudo cat "$ROOT/.eza-disposable-root"                       # this run's run_id
sudo install -d -o 4100 -g 4100 -m 0750 "$ROOT/evidence/walkthrough"
```

```bash
# operator, as the service identity — provisioning is not a requester action
sudo sh -c '
  PATH="$4:$PATH"; export PATH
  setpriv --reuid 4100 --regid 4100 --groups 4100,4200 -- \
    "$4/python3" -I -B "$1/service/provision.py" \
      --release "$2" \
      --domain "$1/domain/protected" \
      --expected-manifest "$3" \
      --plan-out "$1/domain/plan.json" \
    > "$1/evidence/walkthrough/provision.out" 2>&1
' sh "$ROOT" "$RELEASE" "$MANIFEST" "$EZA_BIN"
sudo cat "$ROOT/evidence/walkthrough/provision.out"
```

Every elevated block below follows this same shape: the runtime's `bin` directory
arrives as the last positional argument, is prepended to `PATH` for any child
process, and the interpreter is named explicitly as `"$4/python3"`.

This prints, and writes to `plan.json`, the values **generated during
provisioning**:

| Field | Meaning | Enters the request as |
| --- | --- | --- |
| `resource` | the one synthetic object, e.g. `object:staging:<suffix>` | `resource` |
| `initial_value` | its value before any governed write | — (observed only) |
| `permitted_value` | the one exact value permitted, e.g. `approved-value-<suffix>` | `parameters.value` |
| `principal` | the owning principal | `principal` |
| `lineage` | `[root grant, child grant]` | `authority_lineage` |
| `delegate` | `agent:B` | `delegate` |

`plan.json` is mode `0640`, owned by the service. **Do not loosen it and do not
hand the file to the requester.** Copy only the five request fields above to the
requester. The approval records in the domain are fixture-generated operator
records, not independent validation.

The trusted map from OS identity to actor is `$ROOT/service/actors.json`
(mode `0400`, service-only). It contains exactly:

```json
{"4101": "agent:B"}
```

uid 4101 is the requesting identity. uid 4102 can reach the socket and is
deliberately absent from that map.

## 4. Operator: lifecycle commands, defined once

Paste these four helpers into your operator shell once. They are plain shell —
no adapter file changes — and they exist so that startup, readiness, shutdown,
requests and observations are ordered and recorded rather than eyeballed.

```bash
# operator: start the service and WAIT for readiness tied to the new pid.
wt_start() {
  sudo sh -c '
    PATH="$4:$PATH"; export PATH
    rm -f "$1/run/ready" "$1/run/service.pid"
    setpriv --reuid 4100 --regid 4100 --groups 4100,4200 -- \
      "$4/python3" -I -B "$1/service/service.py" \
        --release "$2" \
        --domain "$1/domain/protected" \
        --socket "$1/run/request.sock" \
        --actors "$1/service/actors.json" \
        --expected-manifest "$3" \
        --log "$1/evidence/service.log" \
        --ready "$1/run/ready" \
      >> "$1/evidence/service.out" 2>&1 &
    for _ in $(seq 1 60); do
      [ -f "$1/run/ready" ] && [ -f "$1/run/service.pid" ] && break
      sleep 0.5
    done
    [ -f "$1/run/ready" ] || { echo "service did not become ready in 30s" >&2
                               tail -20 "$1/evidence/service.out" >&2; exit 1; }
    p=$(cat "$1/run/service.pid")
    [ -d "/proc/$p" ] || { echo "recorded pid $p is not running" >&2; exit 1; }
    printf "ready pid=%s\n" "$p"
  ' sh "$ROOT" "$RELEASE" "$MANIFEST" "$EZA_BIN"
}

# operator: stop THIS run's service by its recorded pid, and wait for it to exit.
wt_stop() {
  sudo sh -c '
    [ -f "$1/run/service.pid" ] || { echo "no recorded pid; nothing to stop"; exit 0; }
    p=$(cat "$1/run/service.pid")
    case "$p" in ""|*[!0-9]*) echo "malformed pid file; not signalling" >&2; exit 2 ;; esac
    if [ ! -d "/proc/$p" ]; then echo "pid $p already gone"; rm -f "$1/run/ready"; exit 0; fi
    tr "\0" " " < "/proc/$p/cmdline" | grep -q -- "$1" || {
      echo "pid $p does not belong to this root; refusing to signal it" >&2; exit 3; }
    kill -TERM "$p"
    for _ in $(seq 1 60); do [ -d "/proc/$p" ] || break; sleep 0.5; done
    [ -d "/proc/$p" ] && { echo "service $p did not exit within 30s" >&2; exit 4; }
    rm -f "$1/run/ready"
    printf "stopped pid=%s\n" "$p"
  ' sh "$ROOT"
}

# operator: run ONE requester submission as uid 4101 and retain the raw reply.
# usage: wt_submit <case> <label>     e.g. wt_submit authorized 01
wt_submit() {
  sudo sh -c '
    PATH="$4:$PATH"; export PATH
    out="$1/evidence/walkthrough/response-$3-$2.json"
    setpriv --reuid 4101 --regid 4101 --groups 4101,4200 -- \
      "$4/python3" -I -B "$1/agent/agent_client.py" submit \
        --socket "$1/run/request.sock" \
        --envelope-file "$1/agent/$2.envelope" > "$out" 2>&1
    status=$?
    cat "$out"
    exit $status
  ' sh "$ROOT" "$1" "$2" "$EZA_BIN"
}

# operator: one trusted observation, retained. usage: wt_observe <label>
wt_observe() {
  sudo sh -c '
    PATH="$4:$PATH"; export PATH
    out="$1/evidence/walkthrough/observation-$2.json"
    setpriv --reuid 4100 --regid 4100 --groups 4100,4200 -- \
      "$4/python3" -I -B "$1/service/observe.py" --release "$3" \
        --domain "$1/domain/protected" > "$out" 2>&1
    status=$?
    cat "$out"
    exit $status
  ' sh "$ROOT" "$1" "$RELEASE" "$EZA_BIN"
}
```

These helpers read `$EZA_BIN` from your operator shell, so export it (section 1a)
in the same shell before defining them.

Start the service now — and only now. **Do not stop it yet;** the stop command
belongs at the restart in section 7 and at the final shutdown in section 8.

```bash
wt_start
```

Expected: `ready pid=<n>`. If it reports that it did not become ready, stop and
read `$ROOT/evidence/service.out`. The usual causes are a `ModuleNotFoundError`
for `jsonschema` — meaning the command reached the system interpreter instead of
`$EZA_BIN/python3`, so re-check section 1a — a wrong `--expected-manifest`, or an
attempt to run as root (refused by design). Do not continue to a later section
after a lifecycle failure.

Ownership and the first log line:

```bash
sudo cat "$ROOT/run/service.pid"
sudo ls -l "$ROOT/run/"                  # request.sock  srw-rw----  eza-svc  eza-req
sudo head -1 "$ROOT/evidence/service.log"
```

The log's first line is the `listening` event with the pid, uid and groups that
are actually enforcing.

### Operator: hand the request content to the requester

The requester composes its own bytes; it holds no authority state. Build the
three envelopes from the five fields above. The outer envelope is
`{"op":"submit","raw_hex":<hex of the canonical inner request>}`, canonical JSON
(sorted keys, ASCII, no spaces):

```bash
# operator: writes request FILES into the requester's own directory.
# This adds no authority decision; it only assembles the documented format.
sudo "$EZA_BIN/python3" -I - "$ROOT" <<'PY'
import json, os, sys
ROOT = sys.argv[1]
plan = json.load(open(f"{ROOT}/domain/plan.json"))
c = lambda v: json.dumps(v, sort_keys=True, ensure_ascii=True,
                         separators=(",", ":"), allow_nan=False).encode()

def inner(request_id, lineage, value):
    return c({"spec": "EZA-001", "version": "0.1.0-draft.7",
              "profile": "local-object-v1",
              "request_id": request_id,
              "principal": plan["principal"],
              "delegate": "agent:B",
              "audience": "service:objects",
              "action": "write",
              "resource": plan["resource"],
              "parameters": {"value": value},
              "authority_lineage": lineage})

cases = {
  "authorized":    inner("req-auth-001",    plan["lineage"], plan["permitted_value"]),
  "no-authority":  inner("req-noauth-001",  [],              plan["permitted_value"]),
  "changed-value": inner("req-badvalue-001", plan["lineage"], "attacker-value"),
}
for name, raw in cases.items():
    path = f"{ROOT}/agent/{name}.envelope"
    open(path, "wb").write(c({"op": "submit", "raw_hex": raw.hex()}))
    os.chown(path, 4101, 4101); os.chmod(path, 0o600)
    print(path)
PY

# retain the exact envelope bytes as evidence
sudo sh -c 'for n in authorized no-authority changed-value; do
              cp "$1/agent/$n.envelope" "$1/evidence/walkthrough/envelope-$n.json"
            done
            sha256sum "$1"/evidence/walkthrough/envelope-*.json \
              > "$1/evidence/walkthrough/envelope-hashes.txt"
            cat "$1/evidence/walkthrough/envelope-hashes.txt"' sh "$ROOT"
```

**In your own integration, this is the part your agent performs**: it produces
that inner request and submits the outer envelope over the socket. It receives
nothing else. It has no route to `Endpoint`, to the domain, or to the actor map.
The service authenticates the *local OS identity* of the peer with
`SO_PEERCRED` and looks the actor up itself — an actor in the request body is
never trusted, and the request bytes are never rewritten to make a request pass.

## 5. Observe first, then demonstrate two refusals

```bash
wt_observe 01-initial
```

Expected before any governed write — the object still holds its initial value,
nothing consumed, nothing journalled:

```json
{"accesses": [], "consumed_roots": [], "prepared": [],
 "resources": {"object:staging:<suffix>": "initial-value-<suffix>"}, "revision": 0}
```

Now, **while the authority is still valid and unspent**, submit the two refusal
cases and observe after each. These are complete commands; there is no
placeholder to repair:

```bash
wt_submit no-authority  02
wt_observe 02-after-no-authority

wt_submit changed-value 03
wt_observe 03-after-changed-value
```

Expected inside `response.record`:

| Case | `decision` | `reason_code` | `execution` | `protected_effect_count` |
| --- | --- | --- | --- | --- |
| `no-authority` | `REJECT` | `EZA-AUTHORITY-ABSENT` | `NOT_EXECUTED` | `0` |
| `changed-value` | `REJECT` | `EZA-SCOPE-EXCEEDED` | `NOT_EXECUTED` | `0` |

`transport` is `COMPLETE` in both: the service answered, and the answer was
"no". In both observations the object value, `accesses`, `consumed_roots` and
`prepared` must be unchanged. `revision` and `now` **do** advance — that is the
reference's legitimate revision and time history, not a protected effect.

The requester also has no direct route to protected state:

```bash
sudo sh -c '
  PATH="$2:$PATH"; export PATH
  out="$1/evidence/walkthrough/bypass-read-state.json"
  setpriv --reuid 4101 --regid 4101 --groups 4101,4200 -- \
    "$2/python3" -I -B "$1/agent/agent_client.py" bypass \
      --path "$1/domain/protected/authority/state.sqlite" --mode read > "$out" 2>&1
  cat "$out"' sh "$ROOT" "$EZA_BIN"
```

Expect `"outcome": "permission-denied"`, `"errno_name": "EACCES"`,
`"denied": true`. Only `EACCES`/`EPERM` counts as a permission boundary; a
missing path reports `missing-target` and proves nothing.

## 6. Submit the one authorized request

```bash
wt_submit authorized 04
```

Expected in `response.record`: `decision` `EXECUTE`, `reason_code`
`EZA-AUTHORIZED`, `execution` `SUCCEEDED`, `protected_effect_count` `1`, and a
`request_digest`. **A success-shaped reply is not the result.** Confirm it by
observation:

```bash
wt_observe 04-after-authorized
```

All four must hold:

1. `resources[<object>]` is now exactly `approved-value-<suffix>`;
2. `accesses` holds **one** entry, `{"action":"write", ...}` with that value;
3. `consumed_roots` holds the root grant id — the single-use authority is spent;
4. no second effect appears anywhere.

If the response is lost, times out, or is indeterminate: **do not resubmit and
do not issue a new grant.** Observe instead. A lost reply is not proof of no
effect. If observation shows the write landed, the authority is spent and the
run is complete.

## 7. Replay, before and after restart

Submit the **identical** `authorized.envelope` again — same bytes, same
`request_id`:

```bash
wt_submit authorized 05-replay-before-restart
wt_observe 05-after-replay-before-restart
```

Expected: `REJECT`, `EZA-REPLAY-DETECTED`, `NOT_EXECUTED`,
`protected_effect_count` `0`. Observation: still one access, same value.

Now restart the same service against the **same** domain. Do not re-provision
and do not reset consumption. `wt_stop` verifies the recorded pid belongs to
this root and waits for it to exit; `wt_start` then clears stale readiness,
launches, and waits for readiness tied to the new pid. Record both pids — they
must differ:

```bash
wt_stop      # -> stopped pid=<old>
wt_start     # -> ready   pid=<new>
```

If `wt_stop` reports a malformed pid, a pid outside this root, or a process that
did not exit, **stop here**: do not launch a second service against the same
domain and socket, and do not continue to the replay below.

```bash
wt_submit authorized 06-replay-after-restart
wt_observe 06-after-replay-after-restart
```

Expected again: `REJECT`, `EZA-REPLAY-DETECTED`, `NOT_EXECUTED`, zero effects,
and observation still showing exactly one write. Consumption lives in the
protected domain, not in service memory.

Want another authorized write? That requires an **explicitly new domain**: a new
fresh root and a new `provision.py` run. Never reuse this one.

## 8. Retain evidence, verify the export, then dispose

Everything you must keep is already being written as you go, under
`$ROOT/evidence/`: the three envelope files and their hashes, one raw response
per submission, one observation per step, the bypass result, the service log and
`service.out`. Add the plan, the service identities and the installed-code
metadata, then stop the service.

```bash
wt_stop

sudo sh -c '
  cp "$1/domain/plan.json"              "$1/evidence/walkthrough/plan.json"
  cp "$1/.eza-disposable-root"          "$1/evidence/walkthrough/run-id.txt"
  cp "$1/service/installed.json"        "$1/evidence/walkthrough/installed.json"
  grep -h "\"event\": \"listening\"" "$1/evidence/service.log" \
    > "$1/evidence/walkthrough/service-identities.jsonl"
  ( cd "$1" && sha256sum service/*.py agent/agent_client.py ) \
    > "$1/evidence/walkthrough/installed-code-hashes.txt"
  printf "manual walkthrough observation, not a 54-check acceptance PASS\n" \
    > "$1/evidence/walkthrough/RECORD-CLASS.txt"
  ls -l "$1/evidence/walkthrough/"' sh "$ROOT"
```

Label that record as exactly what it is: **a manual walkthrough observation.**
It is not a 54-check acceptance PASS, and it is not evidence of outside-developer
usability.

### Export out of the root, and verify the copy

The export destination must be **fresh** and **outside** the disposable root.
This block copies, then re-reads the destination bytes, compares them with the
source file set and sizes, records the run identity, and refuses on any
mismatch. Nothing is deleted here.

```bash
export EXPORT=/tmp/eza-wt-001-evidence      # must not already exist
sudo "$EZA_BIN/python3" -I - "$ROOT" "$EXPORT" <<'PY'
import hashlib, json, os, pathlib, shutil, sys, time
root, dest = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
run_id = (root / ".eza-disposable-root").read_text().strip()
src = root / "evidence"
real_root = os.path.realpath(root)
if os.path.realpath(dest.parent) == real_root or \
   os.path.realpath(dest.parent).startswith(real_root + os.sep):
    sys.exit("refusing: the export destination is inside the disposable root")
if dest.exists() or dest.is_symlink():
    sys.exit(f"refusing: export destination already exists: {dest}")
dest.mkdir(parents=True)

digest = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
required = ["service.log", "walkthrough/plan.json", "walkthrough/run-id.txt",
            "walkthrough/RECORD-CLASS.txt", "walkthrough/envelope-hashes.txt",
            "walkthrough/installed-code-hashes.txt", "walkthrough/installed.json",
            "walkthrough/service-identities.jsonl"]
files, problems = {}, []
for path in sorted(src.rglob("*")):
    if path.is_symlink() or not path.is_file():
        continue
    rel = path.relative_to(src).as_posix()
    target = dest / rel
    target.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(path, target)
    before, after = digest(path), digest(target)
    files[rel] = {"bytes": target.stat().st_size, "sha256": after}
    if before != after or path.stat().st_size != target.stat().st_size:
        problems.append(f"copy does not match source: {rel}")
for rel in required:
    if rel not in files:
        problems.append(f"required manual record missing: {rel}")
if not any(r.startswith("walkthrough/response-") for r in files):
    problems.append("no retained raw responses")
if not any(r.startswith("walkthrough/observation-") for r in files):
    problems.append("no retained trusted observations")
record = {"run_id": run_id, "record_class": "manual walkthrough observation",
          "exported_at": time.time(), "source": str(src), "destination": str(dest),
          "files": files, "verified": not problems, "failures": problems}
(dest / "WALKTHROUGH-EXPORT-INVENTORY.json").write_text(
    json.dumps(record, indent=2, sort_keys=True) + "\n")
print(json.dumps({"run_id": run_id, "files": len(files),
                  "verified": record["verified"], "failures": problems}, indent=2))
raise SystemExit(0 if record["verified"] else 1)
PY
```

`"verified": true` and exit 0 means every evidence file in the root now exists
outside it, byte-for-byte, with the run identity recorded. **A non-zero exit
leaves the environment intact** — fix the missing record and export again to a
new destination. Do not delete anything after a failed export.

### Reported interface gap (read before disposal)

`export_evidence.sh` and `cleanup.sh` both require an export containing
`acceptance-results.json` and `observations.json`, and `cleanup.sh` re-hashes
those exact files immediately before deleting anything. A manual walkthrough
produces neither file. **Do not fabricate an acceptance record to satisfy
cleanup** — that would put a false harness result into evidence. So the
safeguarded tool cannot dispose of a walkthrough root today.

*Smallest proposed adapter change, for a later step, not made here:* give
`export_evidence.sh` and `cleanup.sh` a `--records` option so an explicitly
labelled walkthrough record can be the verified required record, keeping the
canonical-path, marker, re-hash and stale-export checks exactly as they are. No
enforcement or authority-decision code would change.

### Guarded disposal of this identified environment

Until that option exists, dispose only through this block, which **enforces**
what it describes: the service is stopped, the target is an absolute canonical
non-symlink marked disposable root, the export is outside it, the export
inventory is verified and belongs to *this* run, and every evidence file in the
root is present in the export with matching bytes. Any failed condition refuses
and keeps the environment.

```bash
sudo "$EZA_BIN/python3" -I - "$ROOT" "$EXPORT" <<'PY'
import hashlib, json, os, pathlib, shutil, sys
root_arg, export = sys.argv[1], pathlib.Path(sys.argv[2])
if not root_arg.startswith("/"):
    sys.exit("refusing: the root must be an absolute path")
root = pathlib.Path(root_arg)
if root.is_symlink() or not root.is_dir():
    sys.exit(f"refusing: not a real directory: {root}")
canon = os.path.realpath(root)
if canon != root_arg.rstrip("/"):
    sys.exit(f"refusing a non-canonical target: {root_arg} (canonical: {canon})")
marker = root / ".eza-disposable-root"
if not marker.is_file():
    sys.exit(f"refusing: {canon} is not a marked disposable root")
run_id = marker.read_text().strip()
pidfile = root / "run/service.pid"
if pidfile.is_file():
    pid = pidfile.read_text().strip()
    if pid.isdigit() and pathlib.Path(f"/proc/{pid}").is_dir():
        sys.exit(f"refusing: this run's service (pid {pid}) is still running")
inv = export / "WALKTHROUGH-EXPORT-INVENTORY.json"
if not inv.is_file():
    sys.exit(f"refusing: no verified walkthrough export at {export}")
record = json.loads(inv.read_bytes())
problems = []
if record.get("run_id") != run_id:
    problems.append(f"export belongs to run {record.get('run_id')}, not {run_id}")
if not record.get("verified"):
    problems.append("export is not marked verified")
real_export = os.path.realpath(export)
if real_export == canon or real_export.startswith(canon + os.sep):
    problems.append("export lives inside the directory about to be deleted")
files = record.get("files") or {}
if not files:
    problems.append("export lists no files")
digest = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
for rel, stated in sorted(files.items()):
    if rel.startswith("/") or ".." in pathlib.PurePosixPath(rel).parts:
        problems.append(f"unsafe path in the export inventory: {rel}"); continue
    target = export / rel
    if target.is_symlink() or not target.is_file():
        problems.append(f"exported entry is not a regular file: {rel}"); continue
    if target.stat().st_size != stated.get("bytes") or digest(target) != stated.get("sha256"):
        problems.append(f"exported file changed since the export: {rel}")
src = root / "evidence"
if src.is_dir():
    for path in sorted(src.rglob("*")):
        if path.is_symlink() or not path.is_file():
            continue
        rel = path.relative_to(src).as_posix()
        if rel not in files:
            problems.append(f"evidence file not covered by the export: {rel}")
        elif digest(path) != files[rel]["sha256"]:
            problems.append(f"export does not match current evidence: {rel}")
if problems:
    sys.exit("refusing disposal:\n  " + "\n  ".join(problems))
shutil.rmtree(canon)
print(f"removed {canon} (run {run_id}); evidence retained at {real_export}")
PY
```

If that block refuses, **leave the environment in place.** The correct operator
action is then: read the stated reason, copy the missing or changed evidence out
to a new fresh export directory, re-run the export block, and only then dispose.
If you cannot satisfy it, keep the throwaway environment and dispose of it
deliberately once the verified evidence is outside it. Deleting a VM does not
preserve evidence that only exists inside that VM.

The safeguarded `cleanup.sh` remains the correct tool for harness runs.

## What this walkthrough does and does not establish

It shows the frozen EZA-001 R001 reference, behind the reviewed adapter, binding
one agent's action to valid authority in one disposable Linux environment, with
refusal of missing authority, an altered value and replay across a restart.

It is not: an independently authored enforcement implementation (see `TRIAL.md`
in the release for the separate evidence that claim requires — using this
reference is permitted); evidence of outside-developer usability; reboot or
power-loss durability; or production readiness. Python-level process separation
in the reference does not by itself establish OS isolation — the OS identities
and file modes in section 1 are what do that here.

## Author-run smoke checks

Three author-run checks exist. None is a fresh-reader result, and none is a
harness PASS.

* `evidence/run-005-walkthrough-smoke/` — the first author run of the original
  guide. Retained exactly as submitted: a final observation, the service log of
  that environment and a per-request **summary**. It did not retain each raw
  response or intermediate observation, and those raw outputs have not been
  reconstructed.
* `evidence/run-006-walkthrough-corrected/` — the author run of the corrected
  commands in this file, executed from an ordinary sudo-capable operator shell
  (not a root login shell), retaining the raw envelope bytes, every raw
  response, every intermediate observation, the bypass result, the service log,
  both service pids across restart, the export inventory and the disposal
  outcome.
* `evidence/run-007-dependency-runtime/` — author testing of the section 1a
  runtime only: the dedicated root-owned virtual environment, the frozen pins,
  isolated imports under uid 4100 and uid 4101, inheritance of the interpreter by
  child processes through `PATH`, and refusal of write access by the requesting
  identity. It was performed on Linux with CPython 3.13, **not** on Ubuntu 24.04
  with Python 3.12, and it did not run the governed action sequence again.

## Reader-trial history

* **Fresh-Reader Trial 002 (Ubuntu 24.04.5, Python 3.12.3) — blocked.** VM
  access, package integrity and real UID switching passed. `python3 -m pip
  install jsonschema` failed with `externally-managed-environment`, exit 1, and
  no integration setup or governed action ran. That trial stands as recorded
  history; section 1a is the correction made in response to it, and it does not
  turn the blocked trial into a completed one.

All three are author observations. Whether a fresh reader can follow this guide
unaided is still the next milestone and has not been established.
