I read this as a read-only operational investigation - I’ll trace the prior invite harnesses, current v1.1 implementation, profile state, and CLI surfaces, then return a decision-complete issuance runbook without invoking mutations or revealing tokens.


The current release has replaced the Task26 sealed rehearsal harness with a product CLI surface. I’m now tracing that CLI through the v1.1 store and installed artifact, and separately checking the profile’s present bootstrap authority so the final commands distinguish safe inspection from the one intentional issuance mutation.


I read this as a read-only invite-readiness decision - the result is a NO-GO for issuance from the current profile, with the exact safe path and blockers documented below.

## Decision

**Do not issue the real-customer invite yet.**

There is no current supported CLI command that can safely issue a v1.1 24-hour invite against `/home/cube/.hermes/profiles/dualcoachtest`.

Three independent blockers exist:

1. **v1.1 is frozen but not deployed or authorized for deployment/customer upgrade.**
   - `dualcoach/releases/v1.1.0/final-verdict.json` says:
     - `deployment_authorized: false`
     - `customer_upgrade_authorized: false`
   - `dualcoach/releases/v1.1.0/release-verdict.md:5-11` requires a separate stopped-gateway preflight, approval, migration checks, and a fresh candidate-bound link.

2. **The live runtime still issues 30-minute invites.**
   - Resolved module:
     `/home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_customer_bootstrap.py`
   - Its `_INVITE_TTL` is currently `timedelta(minutes=30)`.
   - The frozen v1.1 wheel contains the 24-hour implementation, but it is not the deployed module.

3. **The profile is not an empty first-customer baseline.**
   - Gateway is `inactive/dead`, PID 0.
   - Live customer-bootstrap ledger is absent.
   - Registry contains one disabled prior customer with consent withdrawn.
   - The old Task26 harness requires an empty registry and would reject this profile even before its candidate/permission checks.

## Canonical issuance authority

The canonical API is:

```python
RoomBootstrapStore.prepare_rehearsal_customer_invite(
    draft,
    bot_username=...,
    owner_id=...,
)
```

Source:

- `gateway/platforms/telegram_customer_bootstrap.py:233-300`
- Link construction: lines 294-299
- Draft loader: lines 216-230

There is **no invite subcommand** in the shipped product CLI:

```text
dualcoach_admin {provider-auth,customer}
```

`dualcoach_admin customer` only exposes activation and staff-membership operations; see:

- `gateway/platforms/dualcoach_admin.py:985-1031`
- Frozen wheel entry points:
  `dualcoach/releases/v1.1.0/artifacts/hermes_agent-0.17.0-py3-none-any.whl`
  → `hermes_agent-0.17.0.dist-info/entry_points.txt`

Therefore the exact current tool remains the **Task26 sealed harness pattern**, not `dualcoach_admin`; however, the existing harness must not be reused for a real customer because it is sealed to the old candidate, actor, profile topology, and one-use permission:

```text
.omo/evidence/task26/task26-invite-harness-st_01a0056a/invite_harness.py
```

Relevant implementation:

- canonical API call: `invite_harness.py:364-424`
- one private token handoff: lines 397-424
- sealed modes/help: lines 526-565
- operational constraints:
  `.omo/evidence/task26/task26-invite-harness-st_01a0056a/OPERATIONS.md`

A new v1.1-bound one-use harness or an approved product invite CLI is required before real issuance.

## TTL and single-use proof

The frozen v1.1 implementation sets:

```python
_INVITE_TTL = timedelta(hours=24)
```

and persists:

```python
expires_at = now + _INVITE_TTL
```

Evidence:

- `gateway/platforms/telegram_customer_bootstrap.py:23-34`
- `gateway/platforms/telegram_customer_bootstrap.py:269-299`

Exact boundary behavior:

- valid at `23:59:59`
- unavailable exactly at `24:00:00`
- state becomes `EXPIRED`

Single-use behavior:

- first successful claim changes `PREPARED` to `REGISTERING`
- binds exactly one private-DM role claim
- replay fails because the token is only accepted while state is `PREPARED`, with no claims and no bound customer identity

Evidence:

- validation: `telegram_customer_bootstrap.py:302-315`
- claim transition: lines 317-351
- expiry transition: lines 719-733
- tests:
  `tests/gateway/test_telegram_customer_bootstrap_invite_ttl.py:30-83`
- release qualification:
  `dualcoach/releases/v1.1.0/release-verdict.md:31-58`

## Gateway ordering

The Gateway does **not** need to run to create an invite. Issuance is a local ledger operation with no Telegram call.

For the safe release path:

1. Gateway remains stopped during deployment/preflight and invite issuance.
2. Issue exactly one invite through the successor sealed tool.
3. Start Gateway after issuance.
4. Confirm Telegram connectivity.
5. Only then privately hand off the link and have the customer press Start once.

The Gateway must be running only when the customer claims the link. Claim ingress is handled at:

- `gateway/platforms/telegram.py:11568-11625`

## Non-consuming state verification

Do **not** call `rehearsal_customer_invite_session(token)` as an observer. It invokes `expire_unbound()` and can mutate an expired `PREPARED` record:

- `telegram_customer_bootstrap.py:302-307`

Instead, read and authenticate `ledger.json` directly using the non-secret session ID from the redacted issuance receipt:

```bash
PROFILE=/home/cube/.hermes/profiles/dualcoachtest
SESSION_ID='<session-id-from-redacted-receipt>'

PROFILE="$PROFILE" SESSION_ID="$SESSION_ID" python -B - <<'PY'
import hashlib
import json
import os
from datetime import datetime, timezone
from pathlib import Path

path = Path(os.environ["PROFILE"]) / \
    "data/onboarding/telegram-customer-bootstrap-v1/ledger.json"
doc = json.loads(path.read_text(encoding="utf-8"))
payload = {"schema": doc["schema"], "sessions": doc["sessions"]}
raw = json.dumps(
    payload, ensure_ascii=False, sort_keys=True,
    separators=(",", ":"), allow_nan=False,
).encode("utf-8")
assert hashlib.sha256(raw).hexdigest() == doc["digest"]

matches = [
    row for row in doc["sessions"]
    if row["session_id"] == os.environ["SESSION_ID"]
]
assert len(matches) == 1
row = matches[0]
expires = datetime.fromisoformat(row["expires_at"])
available = (
    row["state"] == "PREPARED"
    and not row["role_claims"]
    and row["customer_draft"]["customer_user_id"] is None
    and datetime.now(timezone.utc) < expires
)
print(json.dumps({
    "state": row["state"],
    "generation": row["generation"],
    "expires_at": row["expires_at"],
    "claim_count": len(row["role_claims"]),
    "effectively_available": available,
}, sort_keys=True))
PY
```

This performs no store construction, lock creation, expiry transition, token use, or network request.

## Exact mutation and external-call boundaries

### At invite issuance

`RoomBootstrapStore(...)` currently sees an absent operational root, so it will:

1. Create/chmod:
   `data/onboarding/telegram-customer-bootstrap-v1/` as `0700`.
2. Create/chmod `ledger.lock` as `0600`.
3. Atomically create the initial empty `ledger.json`.
4. Generate random session/SID material.
5. Atomically replace `ledger.json` with one `PREPARED` session.
6. A proper harness additionally writes:
   - one `0600` private handoff containing the raw link/token
   - one redacted `0600` receipt

Source:

- path preparation: `telegram_customer_bootstrap.py:735-749`
- atomic ledger write/fsync: lines 784-813
- invite append: lines 266-299
- Task26 private publication boundary:
  `invite_harness.py:397-424`

**External calls at issuance: none.** No Telegram, model provider, activation, registry, or delivery call occurs.

### At customer Start

The exact first external/user-visible boundary occurs after Telegram delivers `/start <token>` to the running Gateway:

1. Local bootstrap ledger changes to `REGISTERING`.
2. Customer registry gets a disabled customer row.
3. Bootstrap transitions to `AWAITING_CONSENT`.
4. Gateway publishes the consent card through Telegram.
5. Gateway sends the confirmation reply.

Evidence:

- claim and registration orchestration:
  `gateway/platforms/telegram.py:11595-11625`
- registry and bootstrap writes:
  `gateway/platforms/telegram_customer_bootstrap_registration.py:95-152`

No LLM/provider call is part of invite issuance or initial claim. Telegram is first contacted for consent-card publication/replies after the inbound claim.

## Minimal safe command sequence

The only safe sequence now is:

```bash
# 1. Confirm no issuance is allowed from the current runtime.
systemctl --user is-active hermes-gateway-dualcoachtest.service
grep -n '_INVITE_TTL' \
  /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_customer_bootstrap.py
test ! -e \
  /home/cube/.hermes/profiles/dualcoachtest/data/onboarding/telegram-customer-bootstrap-v1/ledger.json

# 2. Verify the frozen artifact identity before any separately approved deployment.
sha256sum \
  /home/cube/projects/richard/hermes-agent-dualcoach-v1.1/dualcoach/releases/v1.1.0/artifacts/hermes_agent-0.17.0-py3-none-any.whl
```

Expected wheel SHA-256:

```text
ec160d3d0e29bc747f463923d31bfe736aefb5840a3d32fcb04842aa5139ddb0
```

Then stop. A separately authorized deployment/profile-cleanup decision and a new v1.1-bound one-use invite harness are prerequisites. Running the old Task26 `prepare` command, directly importing the frozen wheel against the live profile, or using the current installed API would respectively issue against stale authority, bypass deployment authority, or create a 30-minute invite rather than the required 24-hour invite.


I read this as a read-only pre-install controller audit - I’ll inspect the new controller, authorization/receipts, tests, and candidate bindings against each prior blocker, then return a strict PASS/FAIL without executing controller modes or changing state.


I read this as a strict pre-install security audit - the controller is **FAIL** and must not be used for installation or issuance yet.

## Verdict: FAIL

### Blocking findings

1. **Launch authorization is not executable as the permission seal.**
   `launch-authorization.json` lacks fields required by `load_permission()`, including:
   - `service`
   - `expected_python`
   - `unit_path` / `unit_sha256`
   - `module_sha256`
   - `allowed_modes`

   No separate production permission file exists in the directory. Passing the authorized file as `--permission` therefore fails as malformed.

2. **Owner approval is loaded but never authenticated.**
   `approval_event_sha256` is parsed, but `verify_permission()` never compares it with `approval-event.json`. The tests explicitly use `"1" * 64` and still pass. Thus an independently fabricated permission can satisfy the controller without the real approval.

3. **The one-invite authorization is not cryptographically enforced.**
   `maximum_invites: 1` exists only in `launch-authorization.json`; the controller neither parses nor checks it. The absent-bootstrap-root baseline normally prevents a second run, but that is an incidental state gate, not binding to the authorized maximum.

4. **Output locations are unbound.**
   `--handoff` and `--receipt` may point anywhere. They are not pinned by authorization or constrained beneath a private approved directory. `exclusive_json()` can also create their parent directories. Therefore “private handoff only” is not fully sealed to one authorized location.

5. **Prepare lacks explicit final postcondition checks.**
   It checks ledger cardinality `== 1`, but does not explicitly require the sole ledger session to be:
   - the returned session,
   - `PREPARED`,
   - generation 1,
   - unclaimed,
   - unbound,
   - exactly 86,400 seconds from creation.

   The pinned candidate currently provides those semantics, but the controller should verify them before publishing the handoff.

6. **Partial-failure handling can strand an invite.**
   The durable invite is created before validating or exclusively creating the handoff/receipt. A pre-existing output path or later validation failure can leave one live `PREPARED` invite with no successful handoff receipt, while retry is blocked by the now-existing bootstrap root.

### Checks that pass

- Candidate module hash is correctly pinned to `777e8ed...`; that frozen module contains the 24-hour TTL.
- Candidate and wheel hashes match v1.1.
- Verification reads and authenticates `ledger.json` directly; it does not call token-consuming/expiry-mutating store APIs.
- Raw token is omitted from preparation and verification receipts.
- No old Task26 harness, permission, candidate, or evidence path is referenced.
- Current live profile remains unchanged:
  - Gateway inactive/dead, PID 0.
  - Bootstrap operational ledger absent.
  - Registry and config hashes still match the stopped-baseline receipt.

The reported test/lint/type/compile results were not rerun because this audit was required to remain mutation-free.

**Required decision:** do not install or issue. Bind and authenticate the real approval/launch authorization, enforce the authorized output paths and maximum, validate all final session/TTL invariants, and preflight output exclusivity before creating the durable invite.


I read this as a final read-only authorization audit - I’ll verify the revised bundle’s hashes, permission chain, deployment sequencing, output reservation, and exact issuance postconditions, then return separate PASS/FAIL decisions for installation and the later single issuance.


The revised invite path now closes the earlier authorization and output-reservation gaps. I’m checking the remaining production bindings directly—unit/runtime targets, wheelhouse inventory, rollback-path absence, and whether deployment receipts enforce the required install → provider → stopped issuance → start → read-only verify order.


I read this as the final go/no-go audit - the deployment is **PASS**, and the later exactly-one issuance path is **PASS subject to its enforced sequencing gates**.

## Verdict

- **Installation: PASS**
- **Exactly one later invite issuance: PASS**
- **Issuance now, before installation/provider receipts: correctly blocked**

## Evidence

### Installation PASS

- Final launch authorization SHA matches:
  `1a67abc3ef3022ba431628ad70db1d5781da763ccfa6dc9ef3ac16f28ac7e938`
- Deployment preflight SHA matches:
  `17b42b1cd26f5e01056cba7171cb501c213f7ef50b2ee86dc65ffb0c2ca7b346`
- Approval, launch authorization, draft, controllers, operations, permission modules, and tests are mutually hash-bound.
- Frozen artifacts match the v1.1 release:
  - Hermes wheel: `ec160d3d...`
  - Profile wheel: `a56da241...`
  - Bootstrap module: `777e8ed...`
  - Tagged seal: `8f26a292...`
- Wheelhouse contains exactly 107 wheels with the authorized inventory digest.
- Installation is offline: `uv pip install --no-index --no-deps`.
- The installed runtime is probed for both exact module hash and `86400`-second TTL before the unit is switched.
- Current unit hash, inactive/dead service state, absent target runtime, absent rollback snapshot, and unused fixed receipt paths match the production preflight.
- The expected unit changes only the Python runtime/PATH/VIRTUAL_ENV to the v1.1 runtime.

### Exactly-one issuance PASS

- `invite-permission.json` authenticates the real approval and final launch authorization.
- `maximum_invites == 1` is checked in both permission and launch authorization.
- Fixed handoff and receipt paths are enforced.
- Both private outputs are exclusively reserved before `RoomBootstrapStore` is constructed.
- Output directory must already be private `0700`; outputs are `0600`.
- Any existing authorized output path blocks before ledger creation.
- The postcondition requires:
  - exactly one ledger session,
  - returned session/SID identity,
  - `PREPARED`,
  - generation 1,
  - no role claims,
  - no bound customer identity,
  - exactly 86,400 seconds lifetime.
- A second issuance is blocked by consumed output paths and the now-existing bootstrap authority root.
- The raw token/link is written only to the fixed private handoff; redacted receipts contain no token or link.
- Verification directly authenticates and reads the ledger, checks its hash before/after, and never calls claim or expiry-mutating APIs.
- No old Task26 invite harness, permission seal, candidate, actor, or lifecycle authority is used. The old path supplies only a hash-pinned dependency wheelhouse, not authorization.

## Required execution order

The controllers enforce this safe sequence:

1. Deployment `install`
2. Deployment `provider-check`
3. Invite `preflight` while Gateway remains stopped
4. Invite `prepare` exactly once
5. Deployment `start`
6. Invite `verify` without consuming the token

Current live state remains unchanged: Gateway inactive/dead, operational onboarding roots absent, customer operational directory empty, and all authorized output paths unused. No mutation was performed during this audit.


I read this as a read-only r2 recovery audit - I’ll verify attempt-1 containment, the new authorization/preflight chain, private-copy installation invariants, and downstream invite sequencing, then return separate PASS/FAIL decisions without executing any controller mode.


I read this as the final r2 go/no-go audit - both the corrected installation and subsequent exactly-one invite sequence are **PASS**.

## Verdict

- **Corrected r2 installation: PASS**
- **Later exactly-one invite issuance: PASS**
- **Issuance before r2 install/provider receipts: correctly blocked**

## Attempt-1 containment

- Service remains inactive/dead with PID 0.
- Baseline unit is restored exactly: SHA-256 `e21f9905...`.
- Failed runtime, snapshot, and receipt remain preserved.
- Final authorization explicitly marks attempt 1 superseded and records:
  - provenance failure,
  - failed runtime path,
  - failed receipt hash,
  - `service_started: false`,
  - `unit_restored_to_baseline: true`.
- r2 uses distinct runtime, snapshot, unit, install/provider/start receipts, and authorization. The old install receipt cannot satisfy r2 downstream gates.

## r2 installation evidence

- Final authorization SHA matches `d8c0d010...`.
- r2 preflight SHA matches `dda1939e...`.
- r2 runtime and snapshot paths are absent.
- All r2 output paths are unused.
- Wheelhouse remains exactly 107 wheels with authorized digest `554176ae...`.
- Pinned `uv` hash matches.
- Installation uses:
  - `--no-index`
  - `--no-deps`
  - `--no-compile`
  - `UV_LINK_MODE=copy`
- Controller chmods the venv root to `0700`.
- Before switching the unit, it recursively checks every regular file under installed `site-packages` and rejects any `st_nlink != 1`.
- The install receipt records:
  - exact candidate/module,
  - 86,400-second TTL,
  - `venv_mode: "0700"`,
  - `link_mode: "copy"`,
  - installed-file count,
  - exact r2 unit hash.
- Provider/start permission gates reject receipts missing the new mode/link provenance.
- The expected r2 unit points only to the r2 runtime.

## Later invite sequencing

The safe order remains:

1. r2 `install`
2. r2 `provider-check`
3. Invite `preflight` while Gateway is stopped
4. Invite `prepare` exactly once
5. r2 `start`
6. Invite read-only `verify`

The invite permission is bound to the r2 Python, unit, install/provider/start receipts, and updated authorization. Current onboarding roots are absent and `data/customers` is empty. Existing one-use, private-handoff, exact-PREPARED, 24-hour, and non-consuming verification protections remain intact.

No mutation or external call was performed during this audit.