Skip to content

OAuth2: key token caches by the full credential configuration - #105

Merged
h3xxit merged 4 commits into
mainfrom
dev
Sep 6, 2026
Merged

OAuth2: key token caches by the full credential configuration#105
h3xxit merged 4 commits into
mainfrom
dev

Conversation

@h3xxit

@h3xxit h3xxit commented Sep 6, 2026

Copy link
Copy Markdown
Member

Follow-up to the 1.1.12 release. The http, sse, streamable_http, websocket and gql protocols cached OAuth2 tokens by client_id alone, so two templates sharing a client_id but differing in token URL, secret or scope received each other's tokens. The MCP plugin and the TypeScript SDK already keyed by the full configuration; this brings the rest of Python into line.

Design. One source of truth: OAuth2Auth.cache_key() on the core model is the identity of a credential configuration (token URL, client id, secret, scope; absent scope normalised to empty). Every plugin keys its cache by it and the MCP plugin's private helper delegates to it, so there are no per-package copies of the rule that must agree. The key embeds the secret and is only ever a dict key.

Packages. utcp 1.1.4 (new method), utcp-http 1.1.13, utcp-mcp 1.1.4, utcp-websocket 1.1.5, utcp-gql 1.1.5. The four plugins now require utcp>=1.1.4, so publish core first.

Tests. Core pins the key's identity semantics; each plugin has a network-free cache-isolation test (seed the cache for config A, confirm A is served from it, confirm a config sharing only the client_id has no entry). Suites: core 45, http 238, mcp 48, websocket 38, gql 23, all passing. Two pre-existing network-dependent tests (OpenAPI spec fetch from apis.guru, the public Countries GraphQL API) timed out once under parallel load and pass in isolation.

🤖 Generated with Claude Code


Summary by cubic

Stops the http, sse, streamable_http, websocket, and gql protocols from handing OAuth2 tokens to configs that share a client_id but differ in token URL, secret, or scope; caches are now keyed by the full credential configuration. CI now also installs and runs the file, websocket, and gql plugin suites, which previously shipped without coverage.

Migration

  • Publish utcp 1.1.4 first, then utcp-http 1.1.13, utcp-gql 1.1.5, utcp-mcp 1.1.4, and utcp-websocket 1.1.5, since the plugins now require utcp>=1.1.4.
  • The new OAuth2Auth.cache_key() embeds the secret, so use it only as a dict key and never log it.

Written for commit d452258. Summary will update on new commits.

Review in cubic

h3xxit and others added 2 commits September 6, 2026 16:55
The http, sse, streamable_http, websocket and gql protocols cached OAuth2 tokens by client_id alone, so two templates sharing a client_id but differing in token URL, secret or scope received each other's tokens. The MCP plugin and the TypeScript SDK already keyed by the full configuration.

Put the rule in one place: OAuth2Auth.cache_key() on the core model is the identity of a credential configuration (token URL, client id, secret, scope; absent scope normalised to empty). Every plugin now keys its cache by it, and the MCP plugin's private helper delegates to it, so there is a single source of truth rather than copies that must agree. The key embeds the secret and is used only as a dict key, never logged.

Tests: core pins the key's identity semantics (identical configs share a key, every token-affecting field changes it, same client_id at a different issuer does not share one); each plugin gets a network-free cache-isolation test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
utcp 1.1.3 -> 1.1.4 (adds OAuth2Auth.cache_key), utcp-http 1.1.12 -> 1.1.13, utcp-mcp 1.1.3 -> 1.1.4, utcp-websocket 1.1.4 -> 1.1.5, utcp-gql 1.1.4 -> 1.1.5. The four plugins raise their floor to utcp>=1.1.4 since they call the new method, so core must be published first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 16 files

Re-trigger cubic

test.yml installed and tested cli, http, mcp, text and socket only, so the websocket and gql packages (and file) shipped with no CI coverage; the OAuth cache-key change to websocket and gql was verified locally only. Install all three editable (file after http, which it depends on) and add their test directories to the pytest run. gql's integration tests use the public Countries API, matching the http suite's existing network-dependent OpenAPI test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/test.yml">

<violation number="1" location=".github/workflows/test.yml:41">
P2: This run line adds gql/tests/ to the 9-matrix CI jobs, and test_graphql_integration.py hits the external Countries API (countries.trevorblades.com) unconditionally with no offline/skip guard. If that public API is unreachable, down, or rate-limited, every matrix job fails and blocks all PRs. The PR description already notes these tests can time out under parallel load. Guard the network-dependent tests to skip when network is unavailable (e.g., a socket-gated skipif marker) or move them to a separate non-blocking job, so pipeline health does not depend on an external service.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

- name: Run tests with pytest
run: |
pytest core/tests/ plugins/communication_protocols/cli/tests/ plugins/communication_protocols/http/tests/ plugins/communication_protocols/mcp/tests/ plugins/communication_protocols/text/tests/ plugins/communication_protocols/socket/tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=core/src/utcp --cov-report=xml --cov-report=html
pytest core/tests/ plugins/communication_protocols/cli/tests/ plugins/communication_protocols/http/tests/ plugins/communication_protocols/file/tests/ plugins/communication_protocols/mcp/tests/ plugins/communication_protocols/text/tests/ plugins/communication_protocols/socket/tests/ plugins/communication_protocols/websocket/tests/ plugins/communication_protocols/gql/tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=core/src/utcp --cov-report=xml --cov-report=html

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This run line adds gql/tests/ to the 9-matrix CI jobs, and test_graphql_integration.py hits the external Countries API (countries.trevorblades.com) unconditionally with no offline/skip guard. If that public API is unreachable, down, or rate-limited, every matrix job fails and blocks all PRs. The PR description already notes these tests can time out under parallel load. Guard the network-dependent tests to skip when network is unavailable (e.g., a socket-gated skipif marker) or move them to a separate non-blocking job, so pipeline health does not depend on an external service.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/test.yml, line 41:

<comment>This run line adds gql/tests/ to the 9-matrix CI jobs, and test_graphql_integration.py hits the external Countries API (countries.trevorblades.com) unconditionally with no offline/skip guard. If that public API is unreachable, down, or rate-limited, every matrix job fails and blocks all PRs. The PR description already notes these tests can time out under parallel load. Guard the network-dependent tests to skip when network is unavailable (e.g., a socket-gated skipif marker) or move them to a separate non-blocking job, so pipeline health does not depend on an external service.</comment>

<file context>
@@ -29,13 +29,16 @@ jobs:
     - name: Run tests with pytest
       run: |
-        pytest core/tests/ plugins/communication_protocols/cli/tests/ plugins/communication_protocols/http/tests/ plugins/communication_protocols/mcp/tests/ plugins/communication_protocols/text/tests/ plugins/communication_protocols/socket/tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=core/src/utcp --cov-report=xml --cov-report=html
+        pytest core/tests/ plugins/communication_protocols/cli/tests/ plugins/communication_protocols/http/tests/ plugins/communication_protocols/file/tests/ plugins/communication_protocols/mcp/tests/ plugins/communication_protocols/text/tests/ plugins/communication_protocols/socket/tests/ plugins/communication_protocols/websocket/tests/ plugins/communication_protocols/gql/tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=core/src/utcp --cov-report=xml --cov-report=html
 
     - name: Upload coverage reports to Codecov
</file context>

The three new cache-isolation tests shared the basename test_oauth_cache_isolation.py. Once websocket and gql joined the single pytest run, pytest's default import mode imported them as one top-level module and failed collection with 'import file mismatch' on every CI job. The repo already avoids this with plugin-prefixed basenames (test_websocket_security.py, test_gql_security.py); follow that convention. Collection with the exact CI invocation now yields 489 tests and no errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@h3xxit
h3xxit merged commit 6b4f364 into main Sep 6, 2026
19 checks passed
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.

1 participant