Skip to content

feat(daemon): live HTML KB view over OAuth listener, poll-based v1 (ADR-073, Phase E, #547) - #556

Merged
cuttlefisch merged 3 commits into
mainfrom
feat/547-daemon-html-kb-view-phase-e
Jul 30, 2026
Merged

feat(daemon): live HTML KB view over OAuth listener, poll-based v1 (ADR-073, Phase E, #547)#556
cuttlefisch merged 3 commits into
mainfrom
feat/547-daemon-html-kb-view-phase-e

Conversation

@cuttlefisch

Copy link
Copy Markdown
Owner

Summary

Implements Phase E (#547) of the KB graph-view UX epic (tracker #542): 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). The page is a node list + content pane whose client-side JS polls the existing kb/query.get/.graph RPCs (completely unchanged) on a 5s interval, using the same bearer token the page itself was fetched with. Config: webview_enabled (default false, principle #12), a sibling capability on OAuthConfig following kb_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

  • Query-string bearer token, scoped narrowly: a plain browser navigation can't set a custom Authorization header, so this route accepts the token via ?access_token= (RFC 6750 §2.3) as a fallback — via a new extract_view_bearer_token, consulted only for this route. Every other route, including the kb/query.* JSON-RPC endpoint this page's own polling JS calls, stays header-only (verified by webview_route_accepts_a_query_string_bearer_token_but_no_other_route_does).
  • Access-gated before rendering: the route calls kb/query.capabilities (the same Read-access check every other kb/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.
  • Two real bugs the adversarial tests caught while writing this (principle feat: editor polish, version bump & new features (v0.3.0) #14 paying off, not just confirmation):
    1. 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 into the surrounding HTML. Fixed with a js_string_literal helper that additionally escapes </<\/.
    2. 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 of aborting.

Test plan

Three tiers, mirroring the existing ADR-052/053 convention:

  • Unit (webview.rs): path parsing (including embedded-/ and empty-kb_id rejection), script-breakout neutralization, HTML escaping, determinism, "states plainly it polls" (gate G1)
  • Dispatch-level, real 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 503
  • Real-binary e2e over actual TLS + real JWTs (daemon/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 only
  • cargo test -p mae-daemon — 380 passing (up from 366), 0 failed
  • cargo clippy -p mae-daemon --lib --bins --tests -- -D warnings — clean
  • cargo fmt --check — clean
  • CI (in progress)

Next

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

cuttlefisch and others added 3 commits July 30, 2026 14:04
…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>
@cuttlefisch
cuttlefisch merged commit 7b9ce30 into main Jul 30, 2026
19 checks passed
@cuttlefisch
cuttlefisch deleted the feat/547-daemon-html-kb-view-phase-e branch July 30, 2026 13: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