feat: replace endpoint probing with apiKeys.list scope matching#73
Merged
feat: replace endpoint probing with apiKeys.list scope matching#73
Conversation
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
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.
Summary
apiKeys.listendpoint, matching scopes locally using Outline'scanAccessalgorithmscope_matching.pyimplementing thecanAccessalgorithm and rewriteget_blocked_tools()to use scope-based filteringlast4collision handling: when multiple API keys share the same last 4 characters, their scopes are combined (union); if any has full access, full access winsTest plan
test_scope_matching.py)get_blocked_tools()including collision scenariosapiKeys.listscope