Skip to content

feat(watch,serve): live watch from a phone + tunnel-edge exposure auth (0.18.0) - #293

Merged
danielgwilson merged 4 commits into
mainfrom
codex/watch-live
Aug 2, 2026
Merged

feat(watch,serve): live watch from a phone + tunnel-edge exposure auth (0.18.0)#293
danielgwilson merged 4 commits into
mainfrom
codex/watch-live

Conversation

@danielgwilson

Copy link
Copy Markdown
Owner

What

Makes "watch a live persona run from your phone" real, and moves Observer exposure to tunnel-edge auth — no hand-rolled auth in humanish. Three coherent changes shipped as 0.18.0.

1. Tunnel-edge exposure auth (replaces the in-process capability link)

serve and watch expose the Observer through an authenticated tunnel, not an in-process secret:

humanish watch <cua-lab> --expose \
  --tunnel ngrok --oauth google --allow-email you@example.com
  • New --oauth <provider> (google), repeatable --allow-email / --allow-domain, passed through to ngrok's edge OAuth. --public-url stays for a bring-your-own authed edge (Cloudflare Access / Tailscale).
  • --safe remains an orthogonal share_ready-only content filter on serve.
  • Fail-closed: a tunnel with no edge auth and no --safe is refused; --expose always requires a reachable origin.
  • BREAKING (pre-1.0): the in-process capability-link (--auth link|none, --ttl, the cookie/token machinery in observer-auth.ts) is removed. Auth belongs at the edge. Better Auth is documented as the right choice for a future hosted humanish dashboard, not this ephemeral CLI server.

2. Live watch from a phone

Single-lane / app-url computer-use watch now streams the live E2B desktop during the run (wires onObserverReady, mirroring the concurrent handler) and keeps serving even when the run fails or times out — previously it only served a finished, successful run, so you could never watch it play.

3. budget_reached completion

A computer-use session that plays productively until its wall-clock cap is now recorded as completed (budget_reached), not timed_out=failure. A session that made zero progress then timed out is still a failure. execution.timeoutMs is a generous safety cap; the persona stops on goal.

Safety / how the blocker was caught

The adversarial review found — and this PR fixes — a real ship blocker: an exposed watch was serving the operator's entire local run library (all prior runs' raw, unverified screenshots) to the edge, and --safe was silently ignored on that surface. Fixed by scoping an exposed watch to only the single run being watched (history filtered to that run; every other run id 404s byte-identically), rejecting --safe on watch as inapplicable with a clear message, and adding an explicit "raw evidence is visible to edge-authed viewers" warning. Verified by a dedicated test (exposed = only the attached run reachable; loopback = full library unchanged).

Core invariants preserved: runtime E2B stream URLs stay WeakMap-gated and never persisted; loopback watch/observe/serve are byte-identical without --expose; cleanup by exact id unchanged.

Verification

  • pnpm release:check green: 1140 tests pass (public-surface scan 471 files / 2 binaries, pack dry-run good).
  • Adversarial review (correctness / security / public-safety) → 1 blocker + 2 minors, all fixed; the blocker fix was independently re-verified against the source and by a new single-run-scoping test.
  • No new dependencies. Placeholder domain uses RFC-reserved example.com.

🤖 Generated with Claude Code

danielgwilson and others added 4 commits August 2, 2026 07:36
The computer-use loop now distinguishes two ways to hit execution.timeoutMs:
- budget_reached: the wall-clock cap was reached AFTER at least one material
  (non-idle) action. Maps to ActorStatus "passed" — a non-failure completion for
  open-ended "watch it play" sessions, distinct from goal_satisfied.
- timed_out: the cap was reached with ZERO material progress (hung provider,
  idle-only stall). Stays a failure.

Adds a materialActions counter (exposed as counts.materialActions), branches both
deadline sites, and handles the new reason in the exhaustive statusForCompletion
switch (no default → compile-time forcing). ActorStatus is unchanged, so no
provider mapper ripples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decision A (tunnel-edge auth): serve and watch gain first-class authenticated
exposure. Exposure is TUNNEL-EDGE only — ngrok --oauth google (with repeatable
--allow-email/--allow-domain allow rules) or an operator --public-url they
secure. One shared validateExposure() enforces a fail-closed matrix: exposure
requires EITHER edge auth OR --safe (serve); watch --expose always requires edge
auth (a live run is never share_ready). Removes the in-process capability-link
entirely — deletes src/observer-auth.ts and the --auth/--ttl machinery (pre-1.0
breaking change). Keeps the run-library server, host allowlist, security headers,
share-safety admission, and the /_humanish/api 501 seam.

Decision B (live watch): runCuaBackend now wires onObserverReady so a single-lane
(and app-url) computer-use watch serves the LIVE Observer with the E2B desktop
stream DURING the run, mirroring the concurrent handler. The attached server comes
up regardless of eventual pass/fail and survives a timed_out/failed run (serving
is not gated on result.ok). serveObserver gains an `exposed` mode that enforces the
same Host allowlist + security headers (shared via src/serve-http.ts), closing the
DNS-rebinding gap on the live server. Composes with --expose so it is watchable
from a phone behind the ngrok edge.

Better Auth is intentionally NOT used here (wrong fit for an ephemeral no-DB CLI
server); documented in serve.md as belonging to a future hosted control-plane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump package version and the release-test pin, plus the source-version pins in
docs/ramp/README.md, docs/goals/current.md, and (in the prior commit)
docs/contracts/schemas.md. Leaves the README drawDB hero unpkg literal untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… under --safe; reserved doc domain

BLOCKER (Finding 1): serveObserver in exposed mode served the whole run
library. Now an exposed watch serves ONLY the attached run: /_humanish/history.json
is filtered to result.run and any other run id 404s byte-identically to a
nonexistent run (no cross-run access, no existence oracle). Loopback behavior is
byte-identical. watch --expose --safe is now REFUSED with
HUMANISH_WATCH_SAFE_NOT_APPLICABLE instead of silently ignoring the dead flag,
and the exposed watch path emits the missing raw-evidence exposure warning.

Finding 2: serve --expose with no --tunnel and no --public-url now fails with
HUMANISH_SERVE_EXPOSE_REQUIRES_ORIGIN even under --safe (an origin-less exposed
server was an unreachable loopback no-op).

Finding 3: replaced the non-reserved placeholder domain observer.example.dev with
the RFC-6761 reserved observer.example.com across src/, tests/, and docs/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danielgwilson
danielgwilson merged commit 7b59a18 into main Aug 2, 2026
3 checks passed
@danielgwilson
danielgwilson deleted the codex/watch-live branch August 2, 2026 08:12
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