Skip to content

feat: replace endpoint probing with apiKeys.list scope matching#73

Merged
Vortiago merged 7 commits intomainfrom
claude/document-apikeys-list-Sjfc2
Mar 3, 2026
Merged

feat: replace endpoint probing with apiKeys.list scope matching#73
Vortiago merged 7 commits intomainfrom
claude/document-apikeys-list-Sjfc2

Conversation

@Vortiago
Copy link
Owner

@Vortiago Vortiago commented Mar 3, 2026

Summary

  • Replace the dynamic tool list's endpoint-probing approach (~15 concurrent HTTP requests with fake UUIDs) with a single call to Outline's apiKeys.list endpoint, matching scopes locally using Outline's canAccess algorithm
  • Add scope_matching.py implementing the canAccess algorithm and rewrite get_blocked_tools() to use scope-based filtering
  • Document and test last4 collision handling: when multiple API keys share the same last 4 characters, their scopes are combined (union); if any has full access, full access wins

Test plan

  • Unit tests for scope matching algorithm (test_scope_matching.py)
  • Unit tests for get_blocked_tools() including collision scenarios
  • E2E tests updated to include apiKeys.list scope

claude added 7 commits March 3, 2026 14:40
Replace the dynamic tool list's endpoint-probing approach (which
made ~15 concurrent HTTP requests with fake UUIDs) with a single
call to Outline's apiKeys.list endpoint. The key's stored scopes
are matched locally using Outline's canAccess algorithm to
determine which tools to show.

Scoped API keys must include apiKeys.list in their scope array
for introspection to work. Without it the feature degrades
gracefully (shows all tools).

- Add OutlineClient.list_api_keys() method
- Add scope_matching.py implementing Outline's canAccess algorithm
- Rewrite get_blocked_tools() to use scope-based filtering
- 401 from apiKeys.list = invalid key (block all tools)
- 403/other errors = fail-open (show all tools)
- Match current key by last4 characters
- Update E2E tests to include apiKeys.list scope
- Add comprehensive unit tests for scope matching

https://claude.ai/code/session_01KB7wGDczzZM1KdsX8b34Bc
When multiple API keys share the same last 4 characters, all
their scopes are combined (union). If any matching key has null
scope (full access), the result is full access. This is
consistent with the fail-open design.

Add unit tests for collision union and null-wins behaviors.

https://claude.ai/code/session_01KB7wGDczzZM1KdsX8b34Bc
- Remove dead probe_endpoint method and its 9 tests
- Map tools to real API endpoints (attachments.redirect,
  collections.export, collections.export_all) instead of
  proxying through documents.info/collections.list
- Add status_code attribute to OutlineError for structured
  HTTP error detection (replaces string matching)
- Fix empty scopes [] semantic bug (was treated as full
  access, now correctly blocks all tools)
- Rename scope_matching.get_blocked_tools to
  blocked_tools_for_scopes to avoid name confusion
- Remove per-key _blocked_cache (one apiKeys.list call per
  request is cheap enough)
- Add try/except in filtered_list_tools for fail-open on
  unexpected errors
- Fix test_graceful_degradation_auth_failure to actually
  test error handling (was testing happy path)
- Add list_api_keys unit tests and empty-string API key test
- Use consistent PEP 585 type hints in scope_matching.py
- Update CLAUDE.md and feature docs

https://claude.ai/code/session_01KB7wGDczzZM1KdsX8b34Bc
- blocked_tools_for_scopes([]) now returns empty set (nothing blocked)
  instead of blocking all tools, consistent with fail-open design
- Add test for last4 collision across pagination pages
- Update test_empty_scopes to match new behavior

https://claude.ai/code/session_01KB7wGDczzZM1KdsX8b34Bc
Empty scopes `[]` now correctly blocks every tool instead of
fail-open. Only `None` (full-access key) returns an empty blocked
set. Also removes duplicate test, fixes E2E expected set for
`collections:read` (export_all defaults to write), and documents
non-obvious methodToScope defaults.

https://claude.ai/code/session_01KB7wGDczzZM1KdsX8b34Bc
Reduce CLAUDE.md from 141 to 33 lines — remove Outline source file
links, TypeScript snippets, and algorithm walkthrough that duplicates
scope_matching.py.  Trim __init__.py docstring (33 → 6 lines) and
get_blocked_tools docstring (17 → 5 lines).

https://claude.ai/code/session_01KB7wGDczzZM1KdsX8b34Bc
@Vortiago Vortiago merged commit 8f3de2b into main Mar 3, 2026
16 of 17 checks passed
@Vortiago Vortiago deleted the claude/document-apikeys-list-Sjfc2 branch March 3, 2026 19:29
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