feat(daemon): live HTML KB view over OAuth listener, poll-based v1 (ADR-073, Phase E, #547) - #556
Merged
Merged
Conversation
…DR-073, Phase E, #547) Adds GET /kb/{kb_id}/view to the OAuth HTTPS listener (daemon/src/oauth.rs) serving a single self-contained HTML/CSS/JS page (new daemon/src/webview.rs, dependency-free of mae-canvas/mae-core/mae-gui per ADR-073 D2) — a node list + content pane whose client-side JS polls the existing kb/query.get/.graph RPCs (unchanged) on a 5s interval using the same bearer token the page was fetched with. Config: webview_enabled (default false, principle #12), a sibling capability on OAuthConfig following kb_query_enabled's established pattern exactly. Since a plain browser navigation can't set a custom Authorization header, this route uniquely accepts the bearer token via an ?access_token= query parameter (RFC 6750 §2.3) as well as the header — scoped ONLY to this route via a new extract_view_bearer_token; every other route, including the kb/query.* JSON-RPC endpoint this page's own JS calls, stays header-only. Access is gated through kb/query.capabilities (the same Read-access check every other kb/query.* method uses) before the page is ever returned, so a principal without access to the KB gets a real, immediate error rather than a page that only fails on its first background poll — a genuinely new consumer of the existing gated surface, not a new access path. Two real bugs caught by the adversarial test suite while writing this: - kb_id/token embedding via bare serde_json::to_string doesn't escape `/`, so a kb_id containing a literal `</script>` would have broken out of the page's own script block. Fixed with a js_string_literal helper that additionally escapes `</` to `<\/`. - The query-string bearer parser's original `splitn(2, '=').next()?` chain used `?` inside a for loop, which would abort scanning the ENTIRE query string (not just skip one malformed pair) on the first `=`-less segment. Fixed to `continue` past a malformed pair instead. Three test tiers, mirroring the existing ADR-052/053 convention exactly: unit (webview.rs: path parsing, script-breakout neutralization, HTML escaping, determinism), dispatch-level with a real DocStore (daemon/src/tests/webview_tests.rs: access gating, and a genuine two-KB raw-response-byte leak scan — gate G5), and real-binary e2e over actual TLS with real JWTs (daemon/tests/oauth_e2e.rs: disabled-by-default is inert, auth rejection is byte-for-byte identical to every other route on the listener, the query-token fallback is scoped to this route only). 380 mae-daemon tests passing (up from 366), clippy -D warnings and fmt clean. Phase G (wiring ADR-070/071's wedge primitives into the *native* chord diagram) and the SSE push upgrade (ADR-074) follow separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phase E (#547) of the KB graph-view UX epic (tracker #542): adds
GET /kb/{kb_id}/viewto the OAuth HTTPS listener (daemon/src/oauth.rs), serving a single self-contained HTML/CSS/JS page (newdaemon/src/webview.rs, dependency-free ofmae-canvas/mae-core/mae-guiper ADR-073 D2). The page is a node list + content pane whose client-side JS polls the existingkb/query.get/.graphRPCs (completely unchanged) on a 5s interval, using the same bearer token the page itself was fetched with. Config:webview_enabled(defaultfalse, principle #12), a sibling capability onOAuthConfigfollowingkb_query_enabled's established pattern exactly. This is a separate PR from the editor-workspace canvas-substrate PR (#555) since it's a distinct Cargo workspace/CI surface (daemon/).Design notes
Authorizationheader, so this route accepts the token via?access_token=(RFC 6750 §2.3) as a fallback — via a newextract_view_bearer_token, consulted only for this route. Every other route, including thekb/query.*JSON-RPC endpoint this page's own polling JS calls, stays header-only (verified bywebview_route_accepts_a_query_string_bearer_token_but_no_other_route_does).kb/query.capabilities(the same Read-access check every otherkb/query.*method uses) before returning the page, so a principal without access gets a real, immediate error rather than a page that only fails on its first background poll.kb_id/token embedding via bareserde_json::to_stringdoesn't escape/, so akb_idcontaining a literal</script>would have broken out of the page's own script block into the surrounding HTML. Fixed with ajs_string_literalhelper that additionally escapes</→<\/.splitn(2, '=').next()?chain used?inside aforloop, which would abort scanning the entire query string (not just skip one malformed pair) on the first=-less segment. Fixed tocontinuepast a malformed pair instead of aborting.Test plan
Three tiers, mirroring the existing ADR-052/053 convention:
webview.rs): path parsing (including embedded-/and empty-kb_idrejection), script-breakout neutralization, HTML escaping, determinism, "states plainly it polls" (gate G1)DocStore(daemon/src/tests/webview_tests.rs): member-with-access gets real HTML; non-member denied entirely; a genuine two-KB raw-response-byte leak scan (gate G5) — not a single-KB vacuous case; no-DocStore gets a clean 503daemon/tests/oauth_e2e.rs): disabled-by-default is inert (no HTML surface exists at all until opted in); auth rejection (missing/wrong-audience/expired/forged) is byte-for-byte identical to every other route on the same listener; the query-token fallback is scoped to this route onlycargo test -p mae-daemon— 380 passing (up from 366), 0 failedcargo clippy -p mae-daemon --lib --bins --tests -- -D warnings— cleancargo fmt --check— cleanNext
Phase G (wiring ADR-070/071's wedge/petal primitives from #555 into the native editor's chord diagram) and the SSE push upgrade (ADR-074, Phase J) follow separately.
🤖 Generated with Claude Code