feat(meeting-api,runtime): restore browser_session as a first-class capability — 201 intake + browser-session profile + zero-delay stop (#816) - #908
Conversation
…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.
Release-assembly disposition (v0.12.18): HOLD — do not merge alone; owner decision neededReviewed in full. The core-owned CREATE+STOP contract slice is correct and well-tested — intake mints Why it can't land alone: this PR replaces #829's honest refusal with a 201 for (The deployed 0.10 dashboard is unaffected either way — it posts Options for the owner:
Not merging into |
🃏 Merge card — #908
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. |
|
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. |
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)
browser-session(core/runtime/.../profiles.py) — image${BROWSER_IMAGE}, no command (the image entrypoint switches onBOT_MODE=browser_session),idle_timeout 0(externally managed).profileis an open string in runtime.v1 — no sealed contract touched.bot_spawn/router.py+ newbot_spawn/browser_session.py) —POST /bots {"platform":"browser_session"}mintsbs-<hex>+ an unguessablesession_token, inserts the row ACTIVE immediately, and spawns abrowser-sessionruntime.v1WorkloadSpec(per-user S3 prefixusers/{uid}/browser-userdata). Built aroundbuild_invocation— a session has nomeetingUrland is not in the narrower sealed invocation.v1 Platform enum, so it never enters that builder.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_routeralready accepted the platform; this gives it the right teardown semantics.Observation bundle (issue acceptance table)
/b/{token}routes (item 5), address discovery (item 4), and S3 save-then-stop. I flag it; I do not claim it.test_browser_session_mints_active_row_201(201,status:active,bs-id,session_tokenindata,bot_container_idwritten). Negative controltest_real_meeting_without_url_or_id_still_422(google_meetwith no URL/id → 422, no row).test_browser_session_rejects_supplied_address_422(a session addresses itself — supplyingnative_meeting_id/meeting_url→ 422, no row).test_browser_session_spawns_browser_profile_workload(specprofile:"browser-session",BOT_MODE=browser_session,userdataS3Path=users/{uid}/browser-userdata, sealed-schema valid).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 testtest_delete_browser_session_tears_down_directly_and_completes(workload deleted, no leave command published, rowcompleted).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 ACTIVEbs-…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 all— every 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-chaosskipped per maintainer guidance. Affected suites:meeting-api817 passed,runtime157 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_sessionswitch — the container serves noVNC/CDP), 4 (address discovery —kernel.pyemitsports={}; 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'sRedisBusport is pubsub-only (no key-valueget), so it cannot resolve thebrowser_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 leftKNOWN_GAPS.json'sowned_elsewhere/brow unchanged (its reclassification lands with the actual route restoration).Note for the live witness: on a real deployment the 201 now spawns a
browser-sessionworkload; 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.v1profileis an open string). Nodeploy/helm/tests/test_template.shordeployment-kubernetes.mdxtouched. Ignore the spurious merge-card check.