# RALPLAN Revision 7 Addendum — Local Console and Controlled Telegram Gate

## Summary
Revision 6 remains intact and governing. Revision 7 is additive and supersedes Revision 6 only where Revision 6 left the dashboard service contract, secret handling, isolated E2E behavior, Telegram preflight, approval boundaries, or verification commands underspecified. The fixed result is a user-owned dashboard on IPv4 loopback `127.0.0.1:9120`, canonical `HERMES_HOME=/home/cube/.hermes`, fail-closed outbound controls, deterministic isolated Playwright coverage, and a one-use gateway capability for one allowlisted Telegram test delivery. No Revision 7 step enables global or real outbound.

## Intent Diff
- Preserve every Revision 6 requirement not explicitly tightened below; delete or relax none.
- Replace service-path ambiguity with the absolute-path unit/helper contract in this addendum.
- Replace bearer-token ambiguity with a 32-random-byte, unpadded base64url, no-newline file and fail-closed `openat` validation.
- Replace generic E2E guidance with one deterministic Python Playwright fixture and exact HTTP assertions.
- Replace direct Telegram sending with a gateway-owned, one-dispatch capability and three non-transitive Richard approvals.
- Reconcile intent: console access is ON locally; global outbound, ordinary test outbound, and real outbound remain OFF; only the approved scoped test capability may bypass the global OFF switch for its exact test target and marker.

## Decision Drivers
1. Least privilege and localhost-only exposure outrank remote convenience.
2. Revision 6's no-unapproved-outbound intent outranks test convenience.
3. Secrets and mutable state must be user-owned, non-symlinked, and mode-checked before use.
4. Tests must not touch `/home/cube/.hermes`, port 9120, the production process, or a real Telegram target.
5. A Telegram dispatch must be auditable, budgeted to one, non-retrying, and deactivated in a `finally` path.

## Options
**Selected and final:** a hardened systemd user service plus a separate gateway capability state machine. Root/system services, `0.0.0.0` or IPv6 wildcard listeners, direct application Telegram calls, reusable test capabilities, global test enablement, and production acceptance of test-only timeout variables are rejected.

## In scope / out of scope
**In scope:** local dashboard service/install contract; secure dashboard token; control/config/state ownership; readiness/authentication/CSRF behavior; isolated Python Playwright fixture; canonical registry preflight and redacted digest; scoped Telegram test capability; approval/audit/evidence contracts; pytest marker commands; ADR-007.

**Out of scope:** any Revision 6 feature not named above; remote dashboard access; modification or relocation of the customer registry; bulk or real customer sends; approval of the real pilot; turning any global/test/real outbound flag on; Telegram retries after dispatch begins.

## File-level changes
All runtime paths below are canonical; no other home or registry path is accepted.

- `/home/cube/.local/share/hermes-dashboard/current/hermes/dashboard/server.py`: bind/readiness behavior, test-env rejection, graceful shutdown.
- `/home/cube/.local/share/hermes-dashboard/current/hermes/dashboard/security.py`: component-wise no-follow token loading, bearer auth, CSRF.
- `/home/cube/.local/share/hermes-dashboard/current/hermes/gateway/control.py`: fail-closed control status and heartbeat/inflight reporting.
- `/home/cube/.local/share/hermes-dashboard/current/hermes/gateway/telegram.py`: registry preflight, redacted digest, immutable dispatch ledger, scoped capability transitions.
- `/home/cube/.local/share/hermes-dashboard/current/hermes/gateway/schemas.py`: exact config/state validation; reject unknown fields.
- `/home/cube/.local/share/hermes-dashboard/current/deploy/systemd/hermes-dashboard.service`: unit below.
- `/home/cube/.local/share/hermes-dashboard/current/deploy/systemd/hermes-dashboard`: helper below.
- `/home/cube/.local/share/hermes-dashboard/current/tests/e2e/conftest.py` and `tests/e2e/test_dashboard.py`: isolated fixture and exact HTTP assertions.
- `/home/cube/.local/share/hermes-dashboard/current/pyproject.toml`: register `integration` and `e2e` markers.
- `/home/cube/.local/share/hermes-dashboard/current/docs/adr/ADR-007-local-console-scoped-telegram.md`: ADR and Revision 6 intent reconciliation recorded below.

Runtime artifacts:
- unit: `/home/cube/.config/systemd/user/hermes-dashboard.service` (`cube:cube`, `0644`)
- helper: `/home/cube/.local/libexec/hermes-dashboard` (`cube:cube`, `0755`)
- canonical home: `/home/cube/.hermes` (`cube:cube`, `0700`)
- config/state/evidence directories: `/home/cube/.hermes/config`, `/home/cube/.hermes/state`, `/home/cube/.hermes/state/evidence` (`cube:cube`, `0700`)
- token: `/home/cube/.hermes/config/dashboard.token` (`cube:cube`, `0600`)
- control config: `/home/cube/.hermes/config/control.json` (`cube:cube`, `0600`)
- runtime state: `/home/cube/.hermes/state/runtime.json` (`cube:cube`, `0600`)
- capability state: `/home/cube/.hermes/state/telegram-test-capability.json` (`cube:cube`, `0600`)
- immutable gateway ledger: `/home/cube/.hermes/state/telegram-dispatch-ledger.jsonl` (`cube:cube`, `0600`)
- target env: `/home/cube/.config/hermes/telegram-test.env` (`cube:cube`, `0600`), whose entire content is `HERMES_TELEGRAM_TEST_TARGET=richard-test` followed by one newline.
- only registry: `/home/cube/.hermes/profiles/physique-coach/customers/registry.json` (`cube:cube`, `0600`); it must resolve alias `richard-test` to exactly one Telegram record explicitly allowlisted for environment `test`.

### Exact systemd and helper contract
`/home/cube/.config/systemd/user/hermes-dashboard.service` must be byte-for-byte:

```ini
[Unit]
Description=Hermes localhost dashboard
After=network.target

[Service]
Type=simple
Environment=HOME=/home/cube
Environment=HERMES_HOME=/home/cube/.hermes
Environment=PYTHONUNBUFFERED=1
WorkingDirectory=/home/cube/.local/share/hermes-dashboard/current
ExecStart=/home/cube/.local/libexec/hermes-dashboard --home /home/cube/.hermes --host 127.0.0.1 --port 9120
Restart=on-failure
RestartSec=2s
TimeoutStopSec=10s
KillSignal=SIGTERM
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/cube/.hermes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=default.target
```

`/home/cube/.local/libexec/hermes-dashboard` must be byte-for-byte:

```sh
#!/bin/sh
set -eu
[ "$#" -eq 6 ] || { echo 'invalid dashboard argv' >&2; exit 64; }
[ "$1" = '--home' ] && [ "$2" = '/home/cube/.hermes' ] || { echo 'invalid --home' >&2; exit 64; }
[ "$3" = '--host' ] && [ "$4" = '127.0.0.1' ] || { echo 'invalid --host' >&2; exit 64; }
[ "$5" = '--port' ] && [ "$6" = '9120' ] || { echo 'invalid --port' >&2; exit 64; }
exec /home/cube/.local/share/hermes-dashboard/venv/bin/python -m hermes.dashboard.server "$@"
```

Thus helper argv is exactly the six arguments shown in `ExecStart`, and the executed Python argv is exactly `/home/cube/.local/share/hermes-dashboard/venv/bin/python -m hermes.dashboard.server --home /home/cube/.hermes --host 127.0.0.1 --port 9120`. Here “localhost:9120” means IPv4 loopback `127.0.0.1:9120` only.

### Secret and state contracts
`hermes.admin init-dashboard-token --home /home/cube/.hermes` obtains 32 bytes from `secrets.token_bytes(32)`, encodes RFC 4648 base64url without `=` padding, writes exactly 43 ASCII bytes with no newline, fsyncs file and parent, and atomically installs mode `0600`. It never prints the token. Existing valid tokens are retained; invalid or symlinked paths fail without replacement.

The reader opens `/`, then each of `home`, `cube`, `.hermes`, and `config` with `openat(O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC)` and `fstat`; it opens `dashboard.token` relative to the verified config fd with `openat(O_RDONLY|O_NOFOLLOW|O_CLOEXEC)`. Before reading, `fstat` must prove regular file, `st_uid == pwd.getpwnam("cube").pw_uid`, `st_gid == pwd.getpwnam("cube").pw_gid`, mode exactly `0600`, `st_nlink == 1`, and size exactly 43. The bytes must match `[A-Za-z0-9_-]{43}`, contain no CR/LF, and decode with strict base64 validation plus one synthetic `=` to exactly 32 bytes. Any mismatch exits 78 before bind.

`control.json` has exactly:
```json
{"schema_version":1,"console":{"enabled":true},"outbound":{"global_enabled":false,"test_enabled":false,"real_enabled":false},"heartbeat":{"maximum_age_seconds":10}}
```
Unknown fields fail validation. `runtime.json` has exactly these fields and types: `schema_version` integer constant 1; `heartbeat_at` UTC RFC3339 string ending `Z`; `inflight` integer minimum 0; `console` enum `on|off`; `global_outbound`, `test_outbound`, and `real_outbound` booleans. Dashboard readiness is false unless console is `on`, all three outbound booleans are false, inflight is zero, and heartbeat age is strictly less than 10 seconds.

`telegram-test-capability.json` is validated with `additionalProperties:false` and exactly these required fields: `schema_version` (integer constant 1), `capability_id` (string constant `telegram-preflight-v1`), `channel` (string constant `telegram`), `profile` (string constant `physique-coach`), `environment` (string constant `test`), `target_alias_digest` (64 lowercase hex), `marker_digest` (64 lowercase hex), `status` (enum `inactive|requested|approved|armed|dispatching|delivered|failed|revoking`), `send_budget` (integer 0 or 1), `dispatch_count` (integer 0 or 1), `approved_by` (null or string constant `Richard`), `approved_at` (null or UTC RFC3339), `delivery_receipt_digest` (null or 64 lowercase hex), `failure_code` (null or string), and `revoked_at` (null or UTC RFC3339). The exact message is:

`[HERMES TEST][physique-coach][telegram-preflight-v1] CONTROLLED DELIVERY TEST — no coaching action.`

The gateway alone transitions `inactive -> requested -> approved -> armed -> dispatching -> delivered -> revoking -> inactive`; every error from `requested` onward transitions through `revoking -> inactive`. Approval is valid only for channel/profile/environment, the SHA-256 digest of alias `richard-test`, the exact marker digest, budget one, and a ten-minute expiry. Before the sole network call, the gateway atomically appends and fsyncs a dispatch-intent ledger row keyed by `telegram-preflight-v1`; the unique key forbids a second call. `dispatch_count` becomes one before I/O. There is no automatic retry, including timeout/unknown-result cases. The gateway may bypass global OFF only for this exact approved test tuple; global, ordinary test, and real outbound values remain false throughout. A `finally` block sets budget zero, revokes, deactivates, and waits for inflight zero.

The canonical registry is opened read-only with no-follow/fstat regular-file, `cube:cube`, mode `0600` checks. `registry-digest --redact` emits only `{"entries":<integer>,"path":"/home/cube/.hermes/profiles/physique-coach/customers/registry.json","sha256":"<64 lowercase hex>"}`; angle-bracket notation here defines JSON value constraints, not operator input. It must never emit names, aliases, chat IDs, usernames, phone numbers, or token material.

## Sequencing and dependencies
1. Implement schemas, secure file loading, fail-closed control status, APIs, and unit tests. Test-only routes and timeout handling must be guarded before any bind or state access.
2. Implement the isolated E2E fixture and pass default and explicit E2E suites.
3. Initialize owned directories/config/token as `cube`; preflight owner/mode checks.
4. Stage and verify helper/unit, atomically install them, reload the user manager, start, inspect, and prove loopback-only readiness and safe state.
5. Collect `/home/cube/.hermes/state/evidence/revision-7-code-readiness.json`; Richard records the separate code/readiness approval.
6. Read only the canonical registry, produce the redacted digest, request the exact scoped capability, then Richard separately approves the test send.
7. Execute the gateway test command once; unconditionally revoke/deactivate; prove one dispatch, one delivery receipt, flags false, and zero inflight.
8. Leave real-pilot approval inactive. A later real pilot requires its own Richard approval and is not authorized by either prior approval.
9. Record ADR-007 and evidence-to-acceptance mapping; reconcile that all Revision 6 safety intent remains satisfied.

### Deterministic Python Playwright isolated fixture
`tests/e2e/conftest.py` uses `tempfile.TemporaryDirectory(prefix="hermes-dashboard-e2e-")`, never pytest's shared user home. It creates `.hermes/config`, `.hermes/state`, and `.hermes/profiles/physique-coach/customers` under that temporary root; writes token `AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8` (the unpadded encoding of `bytes(range(32))`), the exact safe `control.json`, and seeded registry `{"schema_version":1,"customers":{"playwright-seed":{"channels":{}}}}`, all with deterministic JSON separators/sorted keys and modes `0700/0600`. It launches exactly:

```python
cmd = ["/home/cube/.local/share/hermes-dashboard/venv/bin/python", "-m", "hermes.dashboard.server", "--home", str(home), "--host", "127.0.0.1", "--port", "19120"]
env = {**os.environ, "HERMES_HOME": str(home), "HERMES_DASHBOARD_TEST_ISOLATED": "1", "HERMES_DASHBOARD_TEST_IDLE_TIMEOUT_SECONDS": "2", "PYTHONHASHSEED": "0", "TZ": "UTC"}
```

The fixture refuses to start if 19120 is occupied; it never kills an unrelated process. It polls `/readyz` for at most five seconds, yields the base URL/token to a Python Playwright browser context created with a fresh temporary profile, and in `finally` closes context/browser, sends SIGTERM, waits five seconds, sends SIGKILL only if needed, closes pipes, calls `TemporaryDirectory.cleanup()`, then asserts the PID is gone and the temporary root does not exist.

If `HERMES_DASHBOARD_TEST_IDLE_TIMEOUT_SECONDS` exists while `HERMES_DASHBOARD_TEST_ISOLATED != "1"`, startup exits 64 before opening files or binding, with stderr exactly `HERMES_DASHBOARD_TEST_IDLE_TIMEOUT_SECONDS is permitted only when HERMES_DASHBOARD_TEST_ISOLATED=1\n`. In isolated mode the value must be an integer 1–60; the fixture uses exactly 2.

E2E assertions are exact:
- `GET /readyz` -> 200, JSON content type, body `{"status":"ready"}`.
- unauthenticated `GET /api/v1/control-state` -> 401, body `{"error":"authentication_required"}`.
- invalid bearer -> 403, body `{"error":"authentication_invalid"}`.
- valid bearer -> 200; body proves `console="on"`, all three outbound booleans false, `inflight=0`, and `0 <= heartbeat_age_seconds < 10`.
- valid bearer `GET /api/v1/session` -> 200 and a 43-character base64url `csrf_token`.
- valid bearer `POST /api/v1/test/csrf-probe` without CSRF -> 403 `{"error":"csrf_required"}`; wrong CSRF -> 403 `{"error":"csrf_invalid"}`; returned CSRF -> 204 with an empty body.
- `/api/v1/test/csrf-probe` outside isolated mode -> 404 `{"error":"not_found"}`.

## Acceptance criteria
- **AC-1 Paths/service:** unit and helper bytes, owners/modes, `WorkingDirectory`, HERMES_HOME, full argv, and single loopback listener match this addendum; service is active.
- **AC-2 Secret:** token is 43 no-newline base64url bytes decoding to 32 bytes; no-follow/fstat owner/mode/link/size checks pass; negative symlink/mode/owner tests exit 78 before bind.
- **AC-3 Safe state:** console ON; global, ordinary test, and real outbound OFF; heartbeat age <10 seconds; inflight zero; owned config/state files reject unknown fields.
- **AC-4 Isolated E2E:** timeout override is rejected outside isolated mode; seeded fixture is deterministic and fully removed; all readiness/protected API/CSRF statuses/bodies above pass.
- **AC-5 Registry preflight:** only the canonical registry is opened; digest output is redacted; alias `richard-test` resolves exactly once and is explicitly test-allowlisted.
- **AC-6 Capability/send:** separate Richard test-send approval exists; gateway records one dispatch intent and performs exactly one network call to that one allowlisted target with the exact marker; one delivery receipt exists; no retry exists.
- **AC-7 Cleanup:** success, failure, interrupt, and timeout paths all leave capability inactive, budget zero, all outbound flags false, and inflight zero.
- **AC-8 Approval separation:** code/readiness, test-send, and real-pilot are three distinct, non-transitive records; real-pilot remains inactive after Revision 7.
- **AC-9 Tests/ADR:** default non-integration/non-E2E suite and explicit E2E suite pass; ADR-007 records the decision and Revision 6 reconciliation.

## Verification
Run as `cube`; commands are exact.

### Initialize and prove ownership
```sh
/usr/bin/install -d -o cube -g cube -m 0700 /home/cube/.hermes /home/cube/.hermes/config /home/cube/.hermes/state /home/cube/.hermes/state/evidence /home/cube/.config/hermes
/usr/bin/install -o cube -g cube -m 0600 /home/cube/.local/share/hermes-dashboard/current/deploy/config/control.json /home/cube/.hermes/config/control.json
/usr/bin/printf '%s\n' 'HERMES_TELEGRAM_TEST_TARGET=richard-test' | /usr/bin/install -o cube -g cube -m 0600 /dev/stdin /home/cube/.config/hermes/telegram-test.env
/home/cube/.local/share/hermes-dashboard/venv/bin/python -m hermes.admin init-dashboard-token --home /home/cube/.hermes
/usr/bin/chown cube:cube /home/cube/.hermes/profiles/physique-coach/customers/registry.json
/usr/bin/chmod 0600 /home/cube/.hermes/profiles/physique-coach/customers/registry.json
/usr/bin/stat -c '%U:%G %a %n' /home/cube/.hermes /home/cube/.hermes/config /home/cube/.hermes/state /home/cube/.hermes/config/control.json /home/cube/.hermes/config/dashboard.token /home/cube/.config/hermes/telegram-test.env /home/cube/.hermes/profiles/physique-coach/customers/registry.json
```
Expected token initializer JSON on first initialization: `{"bytes":32,"created":true,"encoding":"base64url","newline":false}`; a second invocation returns the same object with `created:false`. Expected `stat`: directories `cube:cube 700`; listed files `cube:cube 600`.

### Safe atomic install, reload, inspect, and port/readiness checks
```sh
/bin/sh -n /home/cube/.local/share/hermes-dashboard/current/deploy/systemd/hermes-dashboard
/usr/bin/systemd-analyze --user verify /home/cube/.local/share/hermes-dashboard/current/deploy/systemd/hermes-dashboard.service
/usr/bin/install -d -o cube -g cube -m 0755 /home/cube/.local/libexec /home/cube/.config/systemd/user
/usr/bin/install -o cube -g cube -m 0755 /home/cube/.local/share/hermes-dashboard/current/deploy/systemd/hermes-dashboard /home/cube/.local/libexec/.hermes-dashboard.new
/usr/bin/mv -fT /home/cube/.local/libexec/.hermes-dashboard.new /home/cube/.local/libexec/hermes-dashboard
/usr/bin/install -o cube -g cube -m 0644 /home/cube/.local/share/hermes-dashboard/current/deploy/systemd/hermes-dashboard.service /home/cube/.config/systemd/user/.hermes-dashboard.service.new
/usr/bin/mv -fT /home/cube/.config/systemd/user/.hermes-dashboard.service.new /home/cube/.config/systemd/user/hermes-dashboard.service
/usr/bin/systemctl --user daemon-reload
/usr/bin/systemctl --user enable --now hermes-dashboard.service
/usr/bin/systemctl --user is-active hermes-dashboard.service
/usr/bin/systemctl --user show hermes-dashboard.service --property=FragmentPath,WorkingDirectory,ExecStart,Environment --no-pager
/usr/bin/ss -H -ltn 'sport = :9120'
/usr/bin/curl --noproxy '*' -fsS http://127.0.0.1:9120/readyz
```
Expected: lint/verify/reload exit 0 with no diagnostics; `is-active` prints `active`; show reports the exact fragment, working directory, argv, HOME and HERMES_HOME above; `ss` has exactly one line bound to `127.0.0.1:9120` and none to `0.0.0.0`, `[::]`, or a non-loopback address; curl prints `{"status":"ready"}`.

### Prove controls, heartbeat, and inflight
```sh
DASHBOARD_TOKEN="$(/usr/bin/python3 -c 'import sys;sys.stdout.write(open("/home/cube/.hermes/config/dashboard.token","r",encoding="ascii").read())')"
/usr/bin/curl --noproxy '*' -fsS -H "Authorization: Bearer ${DASHBOARD_TOKEN}" http://127.0.0.1:9120/api/v1/control-state | /usr/bin/jq -e '.console == "on" and (.global_outbound|not) and (.test_outbound|not) and (.real_outbound|not) and .inflight == 0 and .heartbeat_age_seconds >= 0 and .heartbeat_age_seconds < 10'
unset DASHBOARD_TOKEN
```
Expected: `jq` prints `true` and exits 0. This maps directly to AC-3.

### Tests
```sh
(cd /home/cube/.local/share/hermes-dashboard/current && /home/cube/.local/share/hermes-dashboard/venv/bin/python -m pytest -m 'not integration and not e2e')
(cd /home/cube/.local/share/hermes-dashboard/current && /home/cube/.local/share/hermes-dashboard/venv/bin/python -m pytest -m e2e tests/e2e/test_dashboard.py)
```
Expected: both commands exit 0; the second exercises every AC-4 status/body and cleanup assertion. The first command is the mandatory default pytest selection; E2E is never implicit.

### Redacted Telegram preflight and approvals
```sh
. /home/cube/.config/hermes/telegram-test.env
/home/cube/.local/bin/hermes-gateway telegram registry-digest --redact --registry /home/cube/.hermes/profiles/physique-coach/customers/registry.json | /usr/bin/jq -e 'keys == ["entries","path","sha256"] and .entries >= 1 and .path == "/home/cube/.hermes/profiles/physique-coach/customers/registry.json" and (.sha256|test("^[0-9a-f]{64}$"))'
/home/cube/.local/bin/hermes-gateway telegram preflight --profile physique-coach --environment test --target-env HERMES_TELEGRAM_TEST_TARGET --registry /home/cube/.hermes/profiles/physique-coach/customers/registry.json | /usr/bin/jq -e '.allowlisted == true and .matches == 1 and (.global_outbound|not) and (.test_outbound|not) and (.real_outbound|not) and .inflight == 0 and .capability == "inactive"'
/home/cube/.local/bin/hermes-gateway evidence collect --revision 7 --output /home/cube/.hermes/state/evidence/revision-7-code-readiness.json
/home/cube/.local/bin/hermes-gateway approvals approve --phase code-readiness --actor Richard --evidence /home/cube/.hermes/state/evidence/revision-7-code-readiness.json
/home/cube/.local/bin/hermes-gateway capability request --id telegram-preflight-v1 --channel telegram --profile physique-coach --environment test --target-env HERMES_TELEGRAM_TEST_TARGET --marker '[HERMES TEST][physique-coach][telegram-preflight-v1] CONTROLLED DELIVERY TEST — no coaching action.' --budget 1 --expires-in-seconds 600
/home/cube/.local/bin/hermes-gateway capability approve --id telegram-preflight-v1 --phase test-send --actor Richard --budget 1
/home/cube/.local/bin/hermes-gateway approvals status --phase code-readiness --json | /usr/bin/jq -e '.approved == true and .actor == "Richard"'
/home/cube/.local/bin/hermes-gateway approvals status --phase test-send --json | /usr/bin/jq -e '.approved == true and .actor == "Richard"'
/home/cube/.local/bin/hermes-gateway approvals status --phase real-pilot --json | /usr/bin/jq -e '.approved == false and .status == "inactive"'
```
Richard personally runs the two `approve` commands in a separate authenticated console session after reviewing their evidence; neither command may be batch-run by the deployment actor. Expected preflight predicates are `true`. Approval status proves AC-5 and AC-8. The only future real-pilot approval command is `/home/cube/.local/bin/hermes-gateway approvals approve --phase real-pilot --actor Richard --scope telegram:real:physique-coach`; it is explicitly not run in Revision 7 and cannot change outbound flags by itself.

### Exactly-one test dispatch and mandatory finalizer
```sh
. /home/cube/.config/hermes/telegram-test.env
send_rc=0
/home/cube/.local/bin/hermes-gateway telegram test-send --capability telegram-preflight-v1 --target-env HERMES_TELEGRAM_TEST_TARGET --registry /home/cube/.hermes/profiles/physique-coach/customers/registry.json --marker '[HERMES TEST][physique-coach][telegram-preflight-v1] CONTROLLED DELIVERY TEST — no coaching action.' || send_rc=$?
/home/cube/.local/bin/hermes-gateway capability revoke --id telegram-preflight-v1 --reason mandatory-post-test-finalizer
/home/cube/.local/bin/hermes-gateway capability status --id telegram-preflight-v1 --json | /usr/bin/jq -e '.status == "inactive" and .send_budget == 0 and .dispatch_count == 1 and (.delivery_receipt_digest|test("^[0-9a-f]{64}$")) and .revoked_at != null'
/home/cube/.local/bin/hermes-gateway ledger count --id telegram-preflight-v1 --event dispatch-intent --json | /usr/bin/jq -e '.count == 1'
/home/cube/.local/bin/hermes-gateway control status --json | /usr/bin/jq -e '.console == "on" and (.global_outbound|not) and (.test_outbound|not) and (.real_outbound|not) and .inflight == 0'
test "${send_rc}" -eq 0
```
Expected: every `jq` prints `true`, final `test` exits 0, Telegram returns one delivery receipt, and the immutable ledger contains exactly one dispatch intent. Re-running `test-send` is rejected before network I/O with exit 73 and `{"error":"capability_already_consumed"}`; ledger count remains one. This maps to AC-6 and AC-7.

## Escalation/Risk Gate
This is fail-closed. Stop before code/readiness approval if AC-1–AC-4 fails. Stop before capability request if registry owner/mode, canonical path, alias cardinality, allowlist, or redaction fails. Stop before dispatch if either first approval is missing/expired, any outbound flag is true, heartbeat is stale, or inflight is nonzero. On dispatch timeout or unknown result, do not retry; revoke, preserve the ledger/receipt state, return nonzero, and escalate to Richard. Any proposal to bind beyond `127.0.0.1`, alter the canonical registry, enable a global/test/real flag, or combine approval phases requires a new ADR and a later revision; it is not an implementation choice under Revision 7.

## Verification Plan
| Acceptance | Evidence |
|---|---|
| AC-1 | systemd verify, active/show, `ss`, `/readyz` |
| AC-2 | admin initializer plus positive/negative security unit tests |
| AC-3 | authenticated control-state `jq` predicate |
| AC-4 | explicit `-m e2e` Playwright run and process/temp cleanup assertions |
| AC-5 | redacted digest and exact canonical preflight predicates |
| AC-6 | test-send receipt, unique dispatch-intent count, replay rejection |
| AC-7 | final capability/control predicates after success and injected failures |
| AC-8 | three phase-status records with real-pilot inactive |
| AC-9 | both pytest commands and ADR-007 review |

Evidence collector stores command, UTC timestamp, exit status, SHA-256 of non-secret output, and mapped AC IDs in `/home/cube/.hermes/state/evidence/revision-7-code-readiness.json`; it must redact bearer token, CSRF token, target alias value, chat ID, and Telegram credentials.

## ADR and intent reconciliation
ADR-007 status is **Accepted by Revision 7**. Decision: use a localhost-only systemd user unit; a file token loaded with no-follow/fstat checks; an isolated-only test timeout and route; and a gateway-owned one-shot test capability with three separate approvals. Consequences: no remote console, no direct sender, no retry after ambiguous dispatch, and explicit operator gates. Revision 6 intent reconciles as follows: local operability -> console ON at `127.0.0.1:9120`; outbound safety -> all ordinary outbound flags OFF; deterministic validation -> isolated temporary profile and port 19120; controlled preflight -> canonical registry plus redacted digest; human authority -> distinct code/readiness, test-send, and real-pilot approvals. No Revision 6 requirement is waived.

## Risks and mitigations
- **Symlink/permission substitution:** component-wise `openat`/`O_NOFOLLOW`, `fstat`, exact UID/GID/mode/link/size, atomic writes, fail before bind.
- **Accidental external listener:** fixed helper argv, loopback bind, helper validation, `ss` acceptance gate.
- **Test contaminates production:** temporary HERMES_HOME/profile, isolated flag, different fixed port, production rejection of timeout/test route, unconditional cleanup.
- **Duplicate/ambiguous Telegram send:** durable unique dispatch intent before I/O, budget one, no retry, replay exit 73, immutable ledger.
- **PII/secret leakage:** canonical read-only registry, digest-only output, redacted evidence, no token output.
- **Capability left active after crash/error:** startup recovery changes nonterminal test state through revoking to inactive without dispatch retry; external idempotent finalizer; zero-inflight gate.
- **Approval conflation:** distinct phase records, dependency checks, ten-minute test capability expiry, real-pilot remains inactive.
