Skip to content

feat(meeting-api,runtime): restore browser_session as a first-class capability — 201 intake + browser-session profile + zero-delay stop (#816) - #908

Open
DmitriyG228 wants to merge 1 commit into
rc/0.12.18from
feat/816-browser-session-restore
Open

feat(meeting-api,runtime): restore browser_session as a first-class capability — 201 intake + browser-session profile + zero-delay stop (#816)#908
DmitriyG228 wants to merge 1 commit into
rc/0.12.18from
feat/816-browser-session-restore

Conversation

@DmitriyG228

Copy link
Copy Markdown
Contributor

Closes #816

Restores the core-owned CREATE + STOP contract for browser_session — the capability api.v1 seals into the Platform enum but the 0.12 core had no runtime path for (it regressed at the 0.10→0.12 cutover; a paying customer's 2.6h session ran on 0.10). Replaces the interim #829 typed refusal with a real 201. Per the owner ruling (2026-07-19) the core defines the contract; no legacy {mode: browser_session} body is taught to core or gateway.

What this implements (restore-map items 1, 3, 7-stop)

  • Runtime profile browser-session (core/runtime/.../profiles.py) — image ${BROWSER_IMAGE}, no command (the image entrypoint switches on BOT_MODE=browser_session), idle_timeout 0 (externally managed). profile is an open string in runtime.v1 — no sealed contract touched.
  • Intake + spawn branch (bot_spawn/router.py + new bot_spawn/browser_session.py) — POST /bots {"platform":"browser_session"} mints bs-<hex> + an unguessable session_token, inserts the row ACTIVE immediately, and spawns a browser-session runtime.v1 WorkloadSpec (per-user S3 prefix users/{uid}/browser-userdata). Built around build_invocation — a session has no meetingUrl and is not in the narrower sealed invocation.v1 Platform enum, so it never enters that builder.
  • Stop (lifecycle/stop_router.py) — DELETE /bots/browser_session/{id} tears the workload down directly, zero stop delay and completes the row (no leave-command consumer to wait on). stop_router already accepted the platform; this gives it the right teardown semantics.

Observation bundle (issue acceptance table)

# Acceptance row Result
1 Dashboard 'Start browser session' → running session, noVNC reachable, human-witnessed DEFERRED — owner's live leg. Not covered here. Needs the bot-image entrypoint mode switch (item 2), gateway /b/{token} routes (item 5), address discovery (item 4), and S3 save-then-stop. I flag it; I do not claim it.
2 API-level: browser-session create → 201 with a stable session address; meetings without URL/id still 422 (negative control, #794 intact) Covered. test_browser_session_mints_active_row_201 (201, status:active, bs- id, session_token in data, bot_container_id written). Negative control test_real_meeting_without_url_or_id_still_422 (google_meet with no URL/id → 422, no row). test_browser_session_rejects_supplied_address_422 (a session addresses itself — supplying native_meeting_id/meeting_url → 422, no row). test_browser_session_spawns_browser_profile_workload (spec profile:"browser-session", BOT_MODE=browser_session, userdataS3Path=users/{uid}/browser-userdata, sealed-schema valid).
3 CI: compose stack-test leg creating a browser_session via the API PARTIAL. Hermetic unit/contract tests cover the create+stop contract end-to-end over the shipped build_router/request_browser_session/stop route (same class of assertion as the webhook leg). A compose leg is deferred with item 2 — until the bot image serves the session, a compose-spawned container has nothing to prove past the 201.

Additional evidence: profile registration test test_registry_resolves_browser_session_profile; direct-teardown test test_delete_browser_session_tears_down_directly_and_completes (workload deleted, no leave command published, row completed).

Live witness (my own run)

This slice's altitude is the API contract, and I witnessed it there: POST /bots {"platform":"browser_session"} → 201 with an ACTIVE bs-… row + session_token; DELETE → direct teardown + completed; a real meeting with no URL/id still 422. The noVNC altitude (acceptance row 1) is the owner's live leg — I did not witness it and do not claim it.

Gates

node scripts/gates.mjs allevery gate green, run piecewise (incl. the docker gates): python (12 pkgs pytest), stack ✅, compose ✅ ("REAL compose stack proven bot-ready"), contract-version/contract-conformance/schema (24 sealed contracts frozen — none moved), dataflow/isolation-py/graph-py/config-contract/parity/licenses/node/arch-report. compose-stress/compose-chaos skipped per maintainer guidance. Affected suites: meeting-api 817 passed, runtime 157 passed.

Deferred vs implemented / forks against the restore map

Implemented (this PR): restore-map items 1 (runtime profile), 3 (intake + spawn branch), and the stop half of 7 (direct teardown).

Deferred to the follow-on live-leg PR (with acceptance row 1): items 2 (bot-image entrypoint BOT_MODE=browser_session switch — the container serves noVNC/CDP), 4 (address discovery — kernel.py emits ports={}; the container's 6080/9223 must be discoverable), 5 (gateway /b/{token} routes), and the save half of 7.

Fork I hit (reported, not improvised): the restore map orders the gateway /b/{token} routes (item 5) into the 0.12 gateway, but the 0.12 gateway's RedisBus port is pubsub-only (no key-value get), so it cannot resolve the browser_session:{token} session records 0.10 read from Redis; and address discovery (item 4) is unimplemented, so a ported route has no container address to proxy to. Porting the routes now would be untestable proxy code gated on two unimplemented items — it belongs with the live leg (items 2+4) that culminates in the owner's noVNC witness. I therefore deferred item 5 rather than improvise it, and left KNOWN_GAPS.json's owned_elsewhere /b row unchanged (its reclassification lands with the actual route restoration).

Note for the live witness: on a real deployment the 201 now spawns a browser-session workload; until item 2 lands, that container boots the meeting-bot entrypoint and has no session to serve — so the row is ACTIVE but noVNC is not yet reachable. That is exactly the acceptance-row-1 live leg, and it is the owner's to walk once item 2 ships.

No sealed contract changed (api.v1 already seals browser_session; runtime.v1 profile is an open string). No deploy/helm/tests/test_template.sh or deployment-kubernetes.mdx touched. Ignore the spurious merge-card check.

…apability (#816)

api.v1 seals browser_session in the Platform enum with native_meeting_id and
meeting_url both optional, but the 0.12 core registered no runtime path for it —
the contract advertised a capability that regressed at the 0.10->0.12 cutover.
This restores the core-owned CREATE + STOP contract:

- runtime: register a 'browser-session' profile (image ${BROWSER_IMAGE}, no
  command so the image entrypoint switches on BOT_MODE, idle_timeout 0 / managed
  externally). profile is an open string in runtime.v1 -- no seal touched.
- intake: POST /bots {"platform":"browser_session"} mints bs-<hex> + an
  unguessable session_token, inserts the row ACTIVE immediately, and spawns a
  browser-session runtime.v1 WorkloadSpec (per-user S3 prefix
  users/{uid}/browser-userdata) -- built AROUND build_invocation (a session has
  no meetingUrl / is not in the narrower invocation.v1 Platform enum). Supplying
  native_meeting_id/meeting_url is a typed 422 (a session addresses itself); a
  real meeting with no URL/id still 422s (the #794 guarantee -- negative control).
- stop: DELETE /bots/browser_session/{id} tears the workload down directly with
  zero stop delay and completes the row (no leave-command consumer to wait on).

Replaces the interim #829 typed refusal. The gateway /b/{token} routes, the bot
image entrypoint mode switch, address discovery, and S3 save-then-stop are the
live leg (acceptance row 1, human noVNC witness) and stay open on the issue.
@DmitriyG228 DmitriyG228 added this to the v0.12.18 — next milestone Jul 21, 2026
@DmitriyG228

Copy link
Copy Markdown
Contributor Author

Release-assembly disposition (v0.12.18): HOLD — do not merge alone; owner decision needed

Reviewed in full. The core-owned CREATE+STOP contract slice is correct and well-tested — intake mints bs-<hex> + token, ACTIVE row, negative controls intact (native_meeting_id/meeting_url supplied → 422; a real meeting with no URL/id still → 422, #794 preserved), no sealed contract moved, gates green. The deferral fork (gateway /b/{token} routes gated on the pubsub-only RedisBus having no KV get + unimplemented address discovery) is reported honestly, not improvised. Good work.

Why it can't land alone: this PR replaces #829's honest refusal with a 201 for platform: browser_session callers, and then spawns a browser-session workload — but BOT_MODE is honored nowhere in the current bot/runtime tree (verified: entrypoint.sh unconditionally runs the meeting-bot worker; the only BOT_MODE reference in the repo is this PR's own browser_session.py). So on a real deploy the 201 spawns ${BROWSER_IMAGE} running the meeting-bot worker with no meeting URL → it fails, and the ACTIVE row is already persisted → orphan row + burned pod. That is strictly worse than #829's clean refusal for the one caller class that reaches this path.

(The deployed 0.10 dashboard is unaffected either way — it posts {mode: browser_session} with no platform field, so it still hits the required-fields 422 before this branch; only explicit API callers reach the new 201.)

Options for the owner:

  1. Land as a stack — hold this until item 2 (bot-image BOT_MODE=browser_session entrypoint) + item 4 (address discovery) are ready, then merge together so a 201 actually yields a reachable session. Preferred if browser_session ships in .18.
  2. Gate the spawn — merge the contract slice but keep fix(bot-spawn,join): browser_session refuses typed at intake; unknown platforms refuse in the join layer (#816) #829's typed refusal (or a 501 not-yet-serviceable) on the spawn, so no orphan ACTIVE rows until the image entrypoint lands.
  3. Move to v0.13 — treat browser_session restoration as the multi-PR feature it is and sequence it there.

Not merging into rc/0.12.18 pending that call. Acceptance row 1 (live noVNC witness) remains the owner's regardless.

@github-actions

Copy link
Copy Markdown

🃏 Merge card — #908

check what it needs
Value missing state: value-signed (the value sign-off)
Diff maintainer self-review — @DmitriyG228 holds the commit bit (no separate non-author review required)

Not mergeable yet — every row above must be accepted before merge (choke point 1). Fill in what's ❌ above, then this clears automatically.

How a PR reaches merge: the merge bar.

@DmitriyG228 DmitriyG228 added the goal: PDG3 programmable context Business goal: stable meeting artifacts, APIs, webhooks, and MCP for downstream systems label Jul 23, 2026
@DmitriyG228

Copy link
Copy Markdown
Contributor Author

Retriaged out of v0.12.18 to v0.13 — features. Reason: the partial browser-session contract must land only with the complete runnable profile and access path; a standalone 201 would create a false capability. This is an explicit defer, not silent backlog drift.

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

Labels

goal: PDG3 programmable context Business goal: stable meeting artifacts, APIs, webhooks, and MCP for downstream systems

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant