Files
pad/web
xarmian b13e842740 fix(mcp): a refused tool call is audited as denied, not ok (BUG-3879) (#2222)
* fix(mcp): a refused tool call is audited as denied, not ok (BUG-3879)

A failed tool call answers the /mcp POST with 200 and an error result,
so the audit middleware, which classified only the transport status,
wrote result_status=ok for every refused write. MCPAuditLog now installs
an outcome sink on the request context; a tool-handler middleware on
every MCP server pad builds records a failed result's error code (and
details.reason) into it. permission_denied, auth_required and
rate_limited are denied; any other failure is an error; the code is the
error_kind.

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR

* test(mcp): unit rows for the tool-outcome sink, codes and middleware order (BUG-3879)

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR

* docs: the MCP audit status is the call's outcome, not the transport's (BUG-3879)

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR

* fix(mcp): a JSON-RPC error inside a 200 is audited too, via an OnError hook (BUG-3879 hunter r1 M1)

mcp-go refuses an unknown tool, and answers a missing resource or
prompt, before the tool-handler middleware runs, so those calls were
still audited as ok.

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR

* test(mcp): a failed tools/list audits as error; the prose names what OnError cannot see (BUG-3879 r2)

Lead ruling R2 pins the unscoped hook; hunter r2 L1 narrows the claim:
a message rejected before method dispatch never reaches OnError.

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR

* test(mcp): a refused write's audit kind carries the reason BUG-3864 added (BUG-3879)

After rebasing onto #2218 (BUG-3864), the 403s for a read-only token's
write and a viewer's write carry details.reason, token_scope and role.
recordToolOutcome writes error_kind as <code>:<reason> whenever a reason
is present, as designed, so the two transport rows now expect
permission_denied:token_scope and permission_denied:role. On the old
base those 403s carried no reason, which is what the rows had pinned.

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR

* fix(mcp): an unverified email's refused call is audited as denied (BUG-3879 r4)

Since BUG-3864, a write refused because the account's email is
unverified answers with its own MCP code, email_not_verified, rather
than permission_denied. classifyMCPToolOutcome only knew the older
three refusal codes, so after the rebase onto #2218 that refusal was
audited as error. It is denied now, as the classifier's own comment and
this bug's contract already said. The plan and member limits stay
errors (BUG-3894 owns that policy).

The unit rows that paired permission_denied with an email_not_verified
reason described a payload the server never sends; they now use
token_scope, a reason it does send, plus a row for the real code. A new
transport row drives an unverified owner's write on the Cloud fixture
and requires a denied row. docs/api.md and the metric's Help text name
the code.

Claude-Session: https://claude.ai/code/session_016Msqj8nRQUNbgdsXxhBBVR
2026-10-18 11:12:41 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens