Skip to content

fix(owletto-backend): re-register list_watchers/get_watcher/read_knowledge for REST#434

Merged
buremba merged 4 commits into
mainfrom
fix/rest-tool-registry
Apr 28, 2026
Merged

fix(owletto-backend): re-register list_watchers/get_watcher/read_knowledge for REST#434
buremba merged 4 commits into
mainfrom
fix/rest-tool-registry

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 28, 2026

Summary

Hotfix the Tool not found: list_watchers error that broke the watchers page in prod. Restores list_watchers, get_watcher, and read_knowledge to the REST/CLI tool surface (still hidden from external MCP tools/list), and adds a regression test that catches the same class of drift before it ships again.

Why it broke

PR #348 deleted these MCP-tool registrations as part of the move to execute + client.<ns>.<method>. The frontend's apiCall('list_watchers', …) in packages/owletto-web/src/hooks/use-watchers.ts was never migrated — and restToolProxy swallowed the resulting Tool not found error as a 400 JSON body, so Sentry never fired. The existing auth.test.ts assertion at expect(toolNames).not.toContain('list_watchers') codifies the MCP-surface removal, which is correct, but no test covers the REST proxy's view of the registry.

Changes

  • Re-register list_watchers / get_watcher / read_knowledge as internal: true in INTERNAL_REST_TOOLS (formerly LEGACY_ADMIN_TOOLS). They stay hidden from MCP tools/list (which calls getAllTools({ includeInternalTools: false })) but restToolProxy reaches them because it sets allowInternalTools=true.
  • Rename LEGACY_ADMIN_TOOLSINTERNAL_REST_TOOLS. Per the user's framing: the manage_/list_/get_* surface isn't legacy, it's the deliberate REST/CLI boundary alongside the smaller MCP boundary.
  • Consolidate the registry boilerplate. The 12 near-duplicate ToolDefinition literals collapse into a compact ENTRIES array; handlers are passed directly to defineTool instead of via async (args, env, ctx) => handler(args, env, ctx) pass-throughs. Same treatment applied to the public TOOLS array in registry.ts.
  • Surface registry drift to Sentry. New ToolNotRegisteredError thrown from checkToolAccess when getTool() returns undefined. restToolProxy captures it before returning the 400 — internal-tool hiding still throws a plain Error so we don't leak handler existence over MCP.
  • Regression test in tool-access.test.ts scans every apiCall(<…>)('name', …) call site under packages/owletto-web/src and asserts each name is registered. manage_queue is in KNOWN_DEAD_NAMES (the only caller is the unused useDeleteWindow hook); the test will fail if anyone wires it up without registering the backend handler first.

Verification

  • bun test packages/owletto-backend/src/auth/__tests__/tool-access.test.ts — 40 pass.
  • Sanity-check that the test catches the real bug: removing list_watchers from admin/index.ts locally produces Received: ["list_watchers"] and a red test.
  • bun run typecheck, bun run lint, bun run format:check — all green.
  • Sandbox suite (bun test packages/owletto-backend/src/__tests__/unit/sandbox) — 24 pass.

Test plan

  • Existing tool-access tests still pass.
  • New regression test fails when a name is removed from the registry.
  • Typecheck + lint + format clean.
  • After merge, watchers page at https://app.lobu.ai/{slug}/watchers loads instead of erroring.
  • No new ToolNotRegisteredError Sentry alerts in the hour after deploy.

…ledge for REST callers

The frontend's watchers page broke with `Tool not found: list_watchers`
because PR #348 deleted these MCP-tool registrations. The plan was to
migrate everything to `execute` + `client.<ns>.<method>`, but the
frontend (`apiCall('list_watchers'…)` in `use-watchers.ts`) and
owletto-cli still need the named handlers via the REST proxy.

The MCP boundary stays small. The REST/CLI boundary regains the names.

- Re-register `list_watchers`, `get_watcher`, `read_knowledge` as
  `internal: true` so they're hidden from MCP `tools/list` but reachable
  via `POST /api/{slug}/{toolName}` (the REST proxy sets
  `allowInternalTools=true`).
- Rename `LEGACY_ADMIN_TOOLS` → `INTERNAL_REST_TOOLS`. They're not
  legacy — they're the deliberate REST/CLI surface. Consolidate the 12
  near-duplicate `ToolDefinition` literals into a compact `ENTRIES`
  array, and pass handlers to `defineTool` directly instead of wrapping
  each in a pass-through `async (args, env, ctx) => handler(args, env,
  ctx)`. Same simplification applied to the public `TOOLS` array in
  `registry.ts`.
- Add `ToolNotRegisteredError` and throw it from `checkToolAccess` when
  `getTool()` returns undefined (registry/frontend drift). The REST
  proxy now captures it to Sentry so the next "Tool not found" outage
  surfaces as an alert instead of a silent 400. Internal-tool hiding
  still throws a plain Error to avoid leaking handler existence.
- Regression test in `tool-access.test.ts`: scan
  `packages/owletto-web/src` for every `apiCall(<…>)('toolName', …)`
  call site and assert each name is registered. `manage_queue` is in
  a `KNOWN_DEAD_NAMES` allowlist (the only frontend caller is the
  unused `useDeleteWindow` hook); the test fails the day someone wires
  it up without first registering a backend handler.

Verified: removing `list_watchers` from the registry locally now fails
the new test with `Received: ["list_watchers"]`.
@buremba buremba enabled auto-merge (squash) April 28, 2026 00:35
@github-actions github-actions Bot added the triage:needs-human Triage agent escalated for human review label Apr 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Triage decision: needs-human

Reasons:

  • PR size exceeds auto-merge threshold (425 lines, 6 files; limits 300 lines, 10 files)
  • Some CI checks still in progress (CodeQL javascript-typescript, build-test)
  • No approvals yet

Next: merge manually after final review — this is a hotfix for production watchers page errors

…ered internal tools

Direct assertion that list_watchers, get_watcher, and read_knowledge:
- look like 'Tool not found' to external MCP callers, and
- are reachable from the REST proxy when allowInternalTools=true.

Complements the apiCall-coverage scan with explicit access-control
coverage so the visibility split is hard to silently regress.
@github-actions github-actions Bot disabled auto-merge April 28, 2026 00:45
buremba added 2 commits April 28, 2026 01:46
…P surfaces)

Both the frontend and the CLI use the same dispatch
(`POST /api/:orgSlug/:toolName` → `restToolProxy` → `executeTool` →
`getTool(name)`), but the CLI's browser-auth flow goes through MCP RPC
and needs its tools to ALSO stay visible on `tools/list` (i.e. NOT
`internal: true`).

- Scan packages/owletto-cli/src for `callTool(ctx, 'X')` patterns and
  assert each name is registered (matches the frontend's REST surface).
- Scan browser-auth.ts for `name: 'X'` literals and assert each is
  registered AND non-internal — flipping `manage_connections` or
  `manage_auth_profiles` to internal would silently break the CLI's
  browser-auth flow.

Three first-party tool callers are now covered: owletto-web (frontend
REST), owletto-cli/seed (CLI REST), owletto-cli/browser-auth (CLI MCP).
- Frontend scanner missed hook-factory's `tool: 'X'` config form (api/entities.ts, api/connections.ts — 30+ sites). Combine apiCall(...) regex with a second tool: 'X' regex so removing any of those tools also fails the test.
- CLI MCP test was filtering candidates to registered names before checking missing — meaning an outright deletion silently passed. Replace the regex-derived candidate set with a hardcoded `CLI_PUBLIC_MCP_TOOLS` allowlist, plus a separate drift detector that asserts browser-auth.ts's tools/call literals match the pinned set exactly. Verified the new test fails when manage_connections is flipped to internal:true.
- Sentry tag cardinality: tool_name lives in `extra`, not `tags` — the URL segment is attacker-controlled and would blow up tag cardinality.
@buremba buremba enabled auto-merge (squash) April 28, 2026 00:52
@github-actions github-actions Bot disabled auto-merge April 28, 2026 00:54
@buremba buremba merged commit dac1603 into main Apr 28, 2026
14 checks passed
@buremba buremba deleted the fix/rest-tool-registry branch April 28, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:needs-human Triage agent escalated for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant