Skip to content

Add tool launcher: handoff grant flow + per-tool attribution - #533

Merged
kcarnold merged 6 commits into
mainfrom
feat/tool-launcher
Jul 25, 2026
Merged

Add tool launcher: handoff grant flow + per-tool attribution#533
kcarnold merged 6 commits into
mainfrom
feat/tool-launcher

Conversation

@kcarnold

Copy link
Copy Markdown
Contributor

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):

  • a registered tool's id when the request carries a tool grant token,
  • an allowlisted X-Client-Id header otherwise (the add-in stamps its own; the header is validated against the device allowlist so attribution can't be spoofed),
  • null for unlabelled first-party traffic.

Adds llm_usage.client_id (db migration v2) and client_id on 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_grant table (db migration v3): the signed-in taskpane mints a single-use, ~2-min wtg_ grant carrying the user, tool client_id, scopes, and an optional read-only document snapshot; the tool — on its own foreign origin — swaps the grant for an opaque wtk_ bearer token (1-hr TTL).
  • Routes: POST /api/handoff (authenticated), POST /api/handoff/exchange (unauth — the grant_id is the credential), GET /api/handoff/doc (gated by doc:read), POST /api/handoff/revoke. resolveUser recognizes the wtk_ prefix and resolves tool tokens locally, so the tool's proxy/log calls run under the user's account.
  • Frontend: PageName.Tools, a navbar tab, and frontend/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 uses Office.context.ui.openBrowserWindow (falls back to window.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 (a tool_grant row), 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

  • Backend: 102 tests pass (23 new in toolGrants.test.ts + handoff.test.ts), including an end-to-end grant → exchange → wtk_ token → /api/log check that confirms client_id attribution, plus single-use/expiry/revoke, scope gating, and header-spoof rejection.
  • Frontend: 35 tests pass (7 new for the handoff helpers). Typechecks, lints, and builds clean.

Config needed to run a real tool

  • Add the tool's client_id (e.g. mindmap) to BETTER_AUTH_DEVICE_CLIENT_IDS.
  • Point the tool URL in frontend/src/pages/tools/index.tsx at the real host (currently a placeholder).

🤖 Generated with Claude Code

https://claude.ai/code/session_012HPRT4mayaXBbyvXHmQy1x


Generated by Claude Code

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
kcarnold force-pushed the feat/tool-launcher branch from 673c144 to 41221c6 Compare July 24, 2026 15:19
@kcarnold
kcarnold marked this pull request as ready for review July 24, 2026 15:19
kcarnold added 2 commits July 25, 2026 10:01
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.
@kcarnold
kcarnold merged commit 64accab into main Jul 25, 2026
10 checks passed
@kcarnold
kcarnold deleted the feat/tool-launcher branch July 25, 2026 16: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.

2 participants