Skip to content

feat(rc): viewer-host safety primitives, ported from #108 with refits (RC-02) - #147

Merged
AetherAI3 merged 1 commit into
mainfrom
feat/rc-viewer-current-main
Sep 6, 2026
Merged

feat(rc): viewer-host safety primitives, ported from #108 with refits (RC-02)#147
AetherAI3 merged 1 commit into
mainfrom
feat/rc-viewer-current-main

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

Lane

RC-02 — the Agent Remote Control viewer host. Built against main @ 28403f0e, not merged from PR #108.

Spec: docs/specs/2026-09-06-rc-02-viewer-host.md · Cloud counterpart: viewer_profile.py (RC-01, merged 39a16029).

Supersedes #108 — see §3 of the spec for the provenance and rejection map. PR #108 is OPEN, draft, c7ac06d8, DIRTY, 75 files.

What's here

Module Purpose
device_runtime/identity.ts loadEnrollmentMetadata() — identity fields, never the secrets
rc/receipts.ts When the durable cursor may move
rc/viewer_profile.ts The observe-only projection, mirroring Cloud
rc/redaction.ts The payload allowlist, ported from #108

The three refits worth reviewing

§5.1 — canonical enrollment. loadEnrollment() returns the full EnrollmentRecord including device_token and device_command_key. RC publishes observation events and must hold neither, so it gets a projection — field-by-field, deliberately not a rest spread, so a future secret added to EnrollmentRecord can't ride along silently. PR #108 instead defaulted to a synthetic dev_<uuid>: a self-asserted string that authenticates nothing.

§5.3 — receipts. PR #108's validator checked batch coverage, duplicate ids, the rejected flag, and positive safe integers. Necessary, not sufficient — it would have accepted:

  • sequences running backwards within a batch
  • sequences at or below the cursor the host already durably recorded
  • a receipt carrying a digest for different bytes

The first two let a broker replay stale sequence numbers to make the host drop events it never really stored. The third is the RC_EVENT_ID_CONFLICT binding. All three are checked now, and every ambiguity resolves to preserve the batch — re-sending is cheap and deduped broker-side, while advancing wrongly loses a viewer's session permanently. The validator returns a reason, not a boolean, so the host can say honestly why it didn't advance.

Digest reuse, not a fourth spelling. payloadDigest delegates to device_runtime/canonical_json.ts. AETHER-CLOUD already carries three functions named canonical_digest that disagree on non-ASCII input; adding a fourth on the boundary that decides whether a viewer's events get dropped would be the worst possible place for it.

What I kept from #108, and why

remote_redaction.ts was good code — a real allowlist with per-event-type keys, path relativization, and detector reuse from core/redaction.ts. It's ported nearly intact. Two changes: the event vocabulary is now imported from viewer_profile.ts instead of restated, so the allowlist and the projection can't drift; and transcript is gone with its entry.

The tokenizer starts with the Cloud fix already applied: RC-01 shipped one that treated emergency_stop as a single opaque word and its own tests caught it. No reason to repeat that here.

Test plan

npm run typecheck                              # clean
node --test dist/test/rc_viewer_host.test.js   # 60 pass, 0 fail

Includes a source-read guard asserting no RC module references loadEnrollment, device_token, or device_command_key — the technique child_env.test.ts already uses — plus the stale-cursor replay case, digest mismatch, control vocabulary in six naming styles, and a seeded token asserted absent from sanitizer output.

CI is authoritative for the full suite.

⚠ Scope: this is the primitives layer, not the whole lane

Not here: the host client (transport, outbox persistence, heartbeat, attach/reattach) and the aether rc command surface.

Consequently refits §5.2 and §5.4 are not implemented yet — neither is meaningful without the host:

Nothing is wired into a command or the session yet, so this PR changes no runtime behaviour. It lands the pieces the host will be built on, each independently tested, rather than a 559-line port reviewed in one gulp.

READY_FOR_MERGE=true · MERGED=false · ENABLED=false

… (RC-02)

The security-critical core of the RC viewer host, built against current main
rather than merged from PR #108 (OPEN, draft, c7ac06d, DIRTY, 75 files).
Provenance and rejection map are in docs/specs/2026-09-06-rc-02-viewer-host.md.

device_runtime/identity.ts gains loadEnrollmentMetadata(). loadEnrollment()
returns the full EnrollmentRecord including device_token and
device_command_key; RC is the process that publishes observation events and
must hold neither. The projection is field-by-field rather than a rest spread,
so a future secret added to EnrollmentRecord cannot ride along silently. This
is refit §5.1: PR #108 defaulted to a synthetic `dev_<uuid>`, which is a
self-asserted string that authenticates nothing.

rc/receipts.ts decides when the durable cursor may move. PR #108's validator
checked batch coverage, duplicate ids, the rejected flag, and positive safe
integers — necessary, not sufficient. It would have accepted sequences running
backwards within a batch, sequences at or below the cursor the host already
recorded, and a receipt carrying a digest for different bytes. The first two
let a broker replay stale sequence numbers to make the host drop events it
never really stored; the third is the RC_EVENT_ID_CONFLICT binding. All three
are now checked, and every ambiguity resolves to preserve-the-batch: re-sending
is cheap and deduped broker-side, while advancing wrongly loses a viewer's
session permanently. The validator returns a reason rather than a boolean so
the host can say honestly why it did not advance.

rc/viewer_profile.ts mirrors the Cloud projection merged as 39a16029. Exactly
["observe"], no `controller` presence role, and `transcript` excluded — v1 is a
structured-event view, and bounded summaries need a per-session exposure choice
that does not exist yet. The control-vocabulary tokenizer splits snake_case,
camelCase, PascalCase, dotted and kebab from the start: the Cloud side shipped
a tokenizer that treated `emergency_stop` as one opaque word and its own tests
caught it, so this mirror begins with the fix.

rc/redaction.ts is PR #108's allowlist sanitizer, which was good code and is
ported nearly intact. Two changes: the event vocabulary is imported from
viewer_profile rather than restated, so the allowlist and the projection cannot
drift; and `transcript` is gone with its entry. Detector reuse is unchanged —
SENSITIVE_KEY, redactEnvValues and redactInline still come from
core/redaction.ts, so a new detector there protects this sink too.

Tests: 60, including a source-read guard asserting no RC module references
loadEnrollment, device_token, or device_command_key — the same technique
child_env.test.ts uses — plus the stale-cursor replay case, the digest-mismatch
case, control vocabulary in six naming styles, and a seeded token asserted
absent from sanitizer output.

SCOPE: this is the primitives layer. The host client itself (transport,
outbox persistence, heartbeat, attach/reattach) and the `aether rc` command
surface are the next commit in this lane. Refits §5.2 (custody-or-do-not-
activate) and §5.4 (failed revoke_pending is a hard quarantine — PR #108 calls
persist() there without checking its return) land with the host, since neither
is meaningful without it.
@AetherAI3
AetherAI3 merged commit 0a48897 into main Sep 6, 2026
6 of 7 checks passed
@AetherAI3
AetherAI3 deleted the feat/rc-viewer-current-main branch September 6, 2026 20:23
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