Add tool launcher: handoff grant flow + per-tool attribution - #533
Merged
Conversation
Implements Phases 0 and 1 of docs/tool-launcher-plan.md. Phase 0 — attribute every metered request and study log line to (user, client_id): a registered tool's id via its grant token, an allowlisted X-Client-Id header otherwise (the add-in stamps its own), null for unlabelled first-party traffic. Adds llm_usage.client_id (db v2) and client_id on the JSONL log envelope. Phase 1 — handoff grant flow (toolGrants.ts, tool_grant table db v3): the signed-in taskpane mints a single-use, short-TTL grant carrying the user, tool, scopes, and an optional read-only document snapshot; the tool swaps it for an opaque wtk_ bearer token that resolveUser resolves locally, so the tool's proxy/log calls run under the user's account. Routes: POST /api/handoff, /api/handoff/exchange, GET /api/handoff/doc (gated by doc:read), POST /api/handoff/revoke. Adds a Tools page to the sidebar that launches a registered tool in the browser with a doc snapshot, plus a paste-a-URL field for device-flow tools. The tool token is our own credential rather than a Better Auth session (no server-side session-mint primitive; explicit scopes + per-token revoke). Scope enforcement beyond the doc re-fetch is deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012HPRT4mayaXBbyvXHmQy1x
kcarnold
force-pushed
the
feat/tool-launcher
branch
from
July 24, 2026 15:19
673c144 to
41221c6
Compare
kcarnold
marked this pull request as ready for review
July 24, 2026 15:19
Resolves the navbar/pageContext/pages-app conflicts by adopting main's page registry (src/pages/registry.tsx) instead of the three-file nav diff this branch had been carrying. The Tools page is now a single registry entry, tier 'lab', gated behind the 'tool-launcher' feature flag (src/pages/flags.ts) so it only shows in the Labs (···) menu when explicitly enabled.
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.
Implements Phases 0 and 1 of
docs/tool-launcher-plan.md— turning the sidebar into a launch point for external writing tools, with the "platform API" (auth + LLM proxy + logging + document access) the plan calls the real deliverable.Phase 0 — per-tool attribution
Every metered request and study-log line is now attributed to
(user, client_id):X-Client-Idheader otherwise (the add-in stamps its own; the header is validated against the device allowlist so attribution can't be spoofed),nullfor unlabelled first-party traffic.Adds
llm_usage.client_id(db migration v2) andclient_idon the JSONL log envelope. (The Bearer-required / fail-closed proxy the plan lists as the Phase 0 prereq was already in place.)Phase 1 — handoff grant flow + Tools page
backend/src/toolGrants.ts+tool_granttable (db migration v3): the signed-in taskpane mints a single-use, ~2-minwtg_grant carrying the user, toolclient_id, scopes, and an optional read-only document snapshot; the tool — on its own foreign origin — swaps the grant for an opaquewtk_bearer token (1-hr TTL).POST /api/handoff(authenticated),POST /api/handoff/exchange(unauth — the grant_id is the credential),GET /api/handoff/doc(gated bydoc:read),POST /api/handoff/revoke.resolveUserrecognizes thewtk_prefix and resolves tool tokens locally, so the tool's proxy/log calls run under the user's account.PageName.Tools, a navbar tab, andfrontend/src/pages/tools/— a hardcoded first-party list (mindmap) that launches in the browser at…#wt_grant=<id>with a read-only doc snapshot, plus a paste-a-URL field for device-flow tools. Launch usesOffice.context.ui.openBrowserWindow(falls back towindow.open), and the grant travels in the URL fragment so it never reaches the tool's server.Deviation from the plan
The tool's bearer token is our own opaque
wtk_credential (atool_grantrow), not a Better Auth session — Better Auth has no server-side session-mint primitive, and a parallel token keeps the tool's scopes and per-token revoke explicit. Revocation is per-token. Scope enforcement beyond the doc re-fetch is deferred to Phase 3 (a valid tool token is otherwise treated as a full session token, matching the plan's first-party v1 posture).Testing
toolGrants.test.ts+handoff.test.ts), including an end-to-end grant → exchange →wtk_token →/api/logcheck that confirmsclient_idattribution, plus single-use/expiry/revoke, scope gating, and header-spoof rejection.Config needed to run a real tool
client_id(e.g.mindmap) toBETTER_AUTH_DEVICE_CLIENT_IDS.frontend/src/pages/tools/index.tsxat the real host (currently a placeholder).🤖 Generated with Claude Code
https://claude.ai/code/session_012HPRT4mayaXBbyvXHmQy1x
Generated by Claude Code