Skip to content

feat(rc): /rc remote-control host (AETHER-AGENT-LIVE-01 R2, stacked on #107) - #108

Draft
AetherAI3 wants to merge 5 commits into
codex/product-spine-integrationfrom
feat/rc-host
Draft

feat(rc): /rc remote-control host (AETHER-AGENT-LIVE-01 R2, stacked on #107)#108
AetherAI3 wants to merge 5 commits into
codex/product-spine-integrationfrom
feat/rc-host

Conversation

@AetherAI3

@AetherAI3 AetherAI3 commented Aug 23, 2026

Copy link
Copy Markdown
Owner

AETHER-AGENT-LIVE-01 R2 — /rc Remote Control host

Adds the exclusive, outbound-TLS-only Remote Control host that lets a phone or browser observe the local Aether Agent session through the AETHER-CLOUD remote-session broker. Viewer-only (Wave 1); no controller, no inbound port.

Hard dependency — cannot merge before #107

This branch is stacked on #107 (codex/product-spine-integration, head 121d98dba19823766bc34d71aedc388b74836fd2) for the execution/preview foundations. This PR CANNOT merge before #107. Rebase onto main once #107 lands.

Frozen contract: AETHER-CLOUD ADR-0007 (remote-session broker) / AETHER-CLOUD PR #1321. Host endpoints and request/response shapes reconciled against the live R1 broker handlers in AETHER-CLOUD PR #1328.

What it does

  • aether rc / /rc — start (or resume) remote control on the current local session, optional session name.
  • /rc status — RC indicator (active / reconnecting / failed / off), session name, viewer URL, terminal QR, expiry.
  • /rc off — revoke all remote access without stopping the local session.

Wire contract (reconciled against R1 / #1328)

All POST:

  • register → POST /remote/sessions — body {protocol_version, device_id, session_name?, repo?, execution}; returns session_id + host_secret + viewer_url + grant token + expires_at.
  • host attach → POST /remote/sessions/{id}/host/attach — body carries device_id (+ host_secret, last_acked_seq).
  • heartbeat → POST /remote/sessions/{id}/host/heartbeat — body {device_id} (R1 reuses the RemoteHostAttach model, extra="forbid"; an empty body would 422).
  • events → POST /remote/sessions/{id}/host/events — body host_secret + events[] (each host_event_id + payload).
  • revoke → POST /remote/sessions/{id}/revoke — no body model ({}).
  • grant mint → POST /remote/sessions/{id}/grants · grant redeem → POST /remote/grants/redeem.

device_id: the host presents a stable enrolled device id on register / attach / heartbeat, persisted with the durable cursor so a resumed session presents the id the broker recorded.

G2 (whole-request idempotency): R1 returns the original seq for a re-sent (host_event_id, payload) pair but 409 EventConflictError for the same id with a different payload. The host never mutates a queued event's payload — each publish() mints a fresh host_event_id over an already-sanitized immutable payload — so replays after a flaky connection always re-send identical content. A top-level 409 on /host/events is handled distinctly from an attach/register 409 (takeover): the poisoned batch is dropped, the session survives, never misreported as a takeover or requeued forever.

G4 (per-item receipts): /host/events replies with a per-item receipt array. An item marked {rejected:true, reason:"redaction"} (the broker independently dropped that one payload as secret-shaped) is non-fatal — that single item is dropped and counted, the rest of the batch advances, and the session stays active. A redacted item is never misread as a batch failure.

Design invariants (from ADR-0007)

  • Local session is sacred. Every broker call is fail-soft — broker down, network loss, or revocation never stops, corrupts, or downgrades the local REPL.
  • Exclusive host. A second host attach/register is refused with 409 and surfaced honestly; never a takeover.
  • Durable resume. Last-acked-seq cursor + an on-disk outbox; reconnect resumes from the cursor and the broker dedupes by host-supplied host_event_id, so re-sending never duplicates.
  • Heartbeat 15s; reconnect backoff 1s→60s with jitter.
  • QR/URL carries a single-use redemption id, never a reusable bearer token, never a secret in the URL or logs (R1's register grant token is captured for forward-compat but never placed in a URL/log; the QR targets the redemption/viewer URL).
  • Broker base URL is configurable (config.baseUrl / AETHER_BASE_URL); no production URL is hardcoded.

Redaction — what the host does NOT upload (explicit allowlist, enforced before anything is queued)

  • environment variables
  • auth tokens
  • arbitrary file contents
  • unredacted shell history
  • absolute local paths (project-relative identifiers only)
  • MCP credentials
  • browser cookies
  • hidden prompts / private memory

Event payloads are the bounded, allowlisted shapes from the frozen event set (session/transcript/plan/subagent/tool_activity/diff_summary/tests/ci/pr_status/artifact/preview/presence/done/error) — identifiers and summaries, not raw content — capped at the 32 KiB frame bound. The broker's own G4 redaction is a defense-in-depth second gate on top of this.

Files

New: src/core/remote_host.ts, src/core/remote_redaction.ts, src/ui/qr.ts, src/commands/rc.ts, and four test files. Minimal wiring edits to slash.ts, slash_registry.ts, cli_registry.ts, command_manifest.ts. Docs: COMMANDS.md, docs/generated/commands.md (regenerated), RELEASE_NOTES.md, docs/releases/OPERATOR-PACKET-v0.3.0.md (pack inventory 612→624).

Test result (REAL, on this machine)

  • Full npm testexit 0, zero failures (whole suite, once the CI box freed up).
  • Lease suites — rc_host + rc_redaction + rc_command + qr = 30/30 pass (node --test on compiled dist/). Coverage includes R1-path reconciliation, device_id on register/attach/heartbeat, the G2 409 EventConflict poison-batch path, and the G4 per-item redaction drop. A real RS-generator ordering bug in the QR encoder was caught by qr.test.ts and fixed before it could ship an unscannable code.
  • Affected registry/docs/release suites — all green: command_docs_parity + release_coherence (13/13), command_manifest + cli_registry + slash_registry + generated_docs + public_docs_truth + slash (53/53).
  • tsc -p tsconfig.json --noEmit — clean.

Reviewer checklist

  • Full npm test run to completion — exit 0, zero failures.
  • R1 handler shapes reconciled (#1328): device_id on register/attach/heartbeat, host_secret+events[] on /host/events, empty-body /revoke, per-item G4 receipts, whole-request G2 409.
  • Rebase onto main after Product spine: local, headless, docs, and preview #107 merges; do not merge before Product spine: local, headless, docs, and preview #107.
  • Confirm the enrolled device_id source: this lane generates + persists a stable per-session device id (no device-enrollment plumbing exists in the CLI yet). If R1 requires a broker-issued or trusted-device id, wire that source in a follow-up.

🤖 Generated with Claude Code

Add the exclusive, outbound-only Remote Control host that lets a phone or
browser observe the local Aether Agent session via the AETHER-CLOUD
remote-session broker (ADR-0007). Viewer-only Wave 1.

- src/core/remote_host.ts: RemoteHostClient — register/attach (409 = one host),
  durable resume cursor + outbox, host_event_id dedupe on replay, 15s heartbeat,
  1s->60s jittered reconnect backoff, and fail-soft /rc off that revokes all
  grants without stopping the local session. Every broker call is fail-soft.
- src/core/remote_redaction.ts: per-event-type payload ALLOWLIST; drops env vars,
  tokens, file contents, shell history, absolute paths, MCP creds, cookies, and
  hidden prompts before anything is queued; relativizes paths; 32 KiB frame bound.
- src/ui/qr.ts: dependency-free byte-mode QR encoder for the single-use
  redemption URL (never a reusable bearer token).
- src/commands/rc.ts + registry wiring (slash + CLI + manifest): /rc, /rc status,
  /rc off, optional session name; QR + viewer URL + expiry + revoke controls.

Broker base URL is configurable (config baseUrl / AETHER_BASE_URL) — no prod URL
hardcoded. Tests use a mock broker. Docs + operator packet inventory updated.

Depends on aether-agent PR #107 (product-spine execution/preview foundations);
CANNOT merge before #107. Links AETHER-CLOUD ADR-0007 / PR #1321.
Update RC host endpoint constants to R1's actual paths: base is
/remote/sessions (register posts there), with /host/attach, /host/heartbeat,
/host/events, and /revoke sub-resources; add /remote/sessions/{id}/grants and
/remote/grants/redeem constants.

- /host/events body now carries host_secret + events[] to match R1's handler.
- A 409 on /host/events is R1's EventConflictError (payload-bound idempotency,
  G2), NOT a host takeover: drop the poisoned batch and keep the session,
  rather than misclassify it via the shared reconnect path or requeue forever.
  We never re-send changed content under an old host_event_id, so this path is
  defensive.
- Capture R1's register grant_token for forward-compat; the QR still targets
  the redemption/viewer URL, never the raw token.

Lease tests updated to R1 paths + new EventConflict coverage: 28/28 pass.
…ers)

Reconcile against R1's actual handler contracts (#1328):

- /host/heartbeat reuses the RemoteHostAttach model (extra="forbid"), so it
  requires {device_id}; the previous empty body would 422. The host now carries
  a stable enrolled device_id on register, attach, AND heartbeat, persisted with
  the cursor so a resumed session presents the id the broker recorded. /revoke
  takes no body model — unchanged.
- /host/events returns PER-ITEM receipts. A single {rejected:true,
  reason:"redaction"} item (G4: broker dropped that one payload as secret-shaped)
  is non-fatal: drop that item, count it, keep draining. Only a top-level 409
  EventConflictError (G2, whole request) aborts the batch. A redacted item is no
  longer misread as a batch failure.

Tests: device_id present on register/attach/heartbeat; per-item G4 drop keeps
the session active and delivers the surviving item. Lease suites 30/30 pass.
@AetherAI3

Copy link
Copy Markdown
Owner Author

Authoritative Cloud broker handoff — do not merge against the current mock

Per the Cloud/backend closure audit, #108 was not modified. Cloud #1328 is the only broker implementation, but it is not contract-authoritative or production-ready as written: its required Python check fails, its migration preview was skipped, owner UUID/project authorization and host capability binding are incomplete, and observer fanout/caps are process-local in a two-worker deployment.

The current #108 wire is incompatible with #1328: it omits canonical project_ref and dirty_file_count, sends a different registration/event body, never attaches after register, expects registration fields Cloud does not return, omits device binding on append, uses 5s heartbeat, and treats every 409 as an event-id conflict.

Cloud v1 contract to consume after #1321/#1328 are corrected and frozen

  • POST /remote/sessions: bearer owner; strict body {protocol_version:1, project_ref:<gateway-project-uuid>, device_id, session_name, repo:{repo,branch,base_commit,dirty_file_count}, execution:'local'}. Response returns protocol/session/state/device/project/expiry/latest-seq and a one-time host_secret.
  • POST /remote/sessions/{id}/host/attach: {protocol_version:1,device_id,host_secret,last_acked_seq}. Call after fresh registration and on resume. Same host is idempotent; a different host is a typed lease conflict.
  • POST /.../host/heartbeat: {protocol_version:1,device_id,host_secret} at 15s; reconnecting after 3 misses, offline after 12.
  • POST /.../host/events: {protocol_version:1,device_id,host_secret,events:[{host_event_id,event_type,payload}]}. Only typed event_id_conflict permits quarantining that event. Preserve the outbox on terminal/auth/lease failures.
  • GET /.../observe: authenticated owner or redeemed observe capability; Last-Event-ID is the durable sequence cursor.
  • POST /.../grants: {protocol_version:1,purpose:'observe'}; return a one-use token and fragment-based redemption URL. Viewer device should bind atomically at redemption because it is unknown at mint time.
  • POST /remote/grants/redeem: {protocol_version:1,token,device_id}; one transactional burn/device bind; replay, expiry, revoke, and foreign owner collapse to 404.
  • POST /.../revoke: {protocol_version:1,device_id,host_secret}; idempotent terminal transition visible across workers.

Required #108 adaptation

  • Persist the one-time host secret securely; register, then attach.
  • Require/canonicalize Gateway project_ref and the complete repo summary.
  • Mint the viewer grant separately; do not expect viewer URL/grant fields in registration.
  • Use a shared checked-in Cloud fixture for every event and error type; the current redaction fallback key note is not in Cloud’s allowlist.
  • Distinguish typed conflict/terminal/auth/rate errors; do not drop an entire batch for a generic 409.
  • Persist the outbox before send or explicitly document best-effort loss; losing an unsent outbox is not recoverable by server replay.
  • Rebase onto current Product spine: local, headless, docs, and preview #107 head after Product spine: local, headless, docs, and preview #107 is ready, not the stale base described in this PR.

Recommended stack: corrected #1321 → repaired #1328 → #1325 chaos/integration unskipped → #1326 viewer → #107 → rebase #108 onto #107 plus the published Cloud fixture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant