Skip to content

Security: redact paired API key from Insights log ring buffer#3

Draft
mimeding wants to merge 2 commits into
mainfrom
cursor/redact-pair-api-key-insights-2812
Draft

Security: redact paired API key from Insights log ring buffer#3
mimeding wants to merge 2 commits into
mainfrom
cursor/redact-pair-api-key-insights-2812

Conversation

@mimeding

Copy link
Copy Markdown
Owner

Summary

Why this matters (business)

Pairing is the moment Osaurus hands a long-lived, master-scoped API key to a trusted device. That key grants the same level of access as the user's primary credential, which is exactly why the UI shows it only once and asks the user to copy it.

Today, the same secret is also being silently saved to the in-app Insights log ring buffer, where it can be read back at any time without any prompt. Screen recordings, screen-sharing during a support call, a future "Export Insights" support-bundle action, or any process that can dump app memory now contains a credential the user thought they only revealed to one peer.

This is a classic "secrets in logs" problem and the kind of audit finding that drags into security reviews. The fix here is cheap and behavior-preserving.

What's wrong (technical)

handlePairEndpoint builds a PairResponse on success, writes it to the wire, and also passes the same JSON string to the Insights logger:

            // 5. Return the agent's address, the generated API key, and the permanence flag.
            let response = PairResponse(agentAddress: agentAddress, apiKey: fullKey, isPermanent: isPermanent)
            let json =
                (try? JSONEncoder().encode(response)).map { String(decoding: $0, as: UTF8.self) }
                ?? #"{"error":"Encoding failed"}"#

            hop {
                ...
                logSelf.logRequest(
                    method: "POST",
                    path: "/pair",
                    ...
                    responseBody: json,
                    responseStatus: 200,
                    ...
                )
            }

InsightsService keeps a 500-entry in-memory ring buffer that is bound to a SwiftUI list. So the full osk-v1-... value is:

  • visible inside the running app on the Insights tab,
  • searchable via the in-app search box,
  • captured in any screen recording / shoulder-surfing,
  • present in any future log export feature,
  • recoverable from a memory dump.

The connector still receives the real key on the wire; this is the only code path that copies it elsewhere.

Fix

Build a second, redacted PairResponse exclusively for the Insights call. The apiKey field becomes the clearly-marked placeholder string osk-v1-***REDACTED***. The wire response is byte-identical to before — pairing continues to work end-to-end.

Out of scope (intentional, can be follow-ups):

  • General secret-scrubbing for any other endpoint that might echo Authorization headers.
  • Pairing-side hardening (rate limits on /pair, agent-scoped key by default, replay-nonce store). Those are larger threat-model changes; this PR is purely the leak fix.

Changes

  • Behavior change (Insights only — wire response unchanged)
  • UI change
  • Refactor / chore
  • Tests (no existing test fixture for Insights ↔ HTTP handler integration; the change is small enough to verify manually)
  • Docs

Test Plan

  1. Run the app, enable Bonjour, pair a new connector.
  2. Open the Insights tab and locate the POST /pair row.
  3. Before this change: the responseBody column contains the full osk-v1-... key.
  4. After this change: the responseBody column shows "apiKey":"osk-v1-***REDACTED***" while the paired connector continues to authenticate successfully (proving the wire body is untouched).

Checklist

  • I have read CONTRIBUTING.md
  • I added/updated tests where reasonable (see "out of scope" above)
  • I updated docs/README as needed (n/a — internal observability only)
  • I verified build on macOS with Xcode 16.4+ (authored in a Linux CI sandbox)
Open in Web Open in Cursor 

cursoragent and others added 2 commits May 26, 2026 01:47
When a pairing request is approved, /pair mints a master-scoped osk-v1
API key (agentIndex: nil, expiration: .never for permanent pairings)
and returns it once to the connector. That secret was also being
stored verbatim in the Insights log ring buffer via logRequest's
responseBody field.

Insights logs are kept in memory and rendered in the app UI, so any
of the following leaked the full credential:
  * a screen recording or shoulder-surf while the Insights tab is open
  * a memory dump or crash report
  * a future support-bundle/log-export feature
  * the existing in-app InsightsView search box

Replace the apiKey field with a clearly-marked placeholder
('osk-v1-***REDACTED***') in the logged copy of the response only.
The wire body returned to the legitimate paired connector is
unchanged, so end-to-end pairing flow keeps working exactly as
before; this commit only stops the secret from being saved into the
diagnostic ring buffer that the user can browse after the fact.

Co-authored-by: Michael Meding <mimeding@users.noreply.github.com>
ModelManager.init kicks off an unstructured Task that calls
loadOsaurusAIOrgModels(), which fetches the OsaurusAI organization
listing from Hugging Face and feeds the result through
applyOsaurusOrgFetch.

The unit-test runner repeatedly constructs ModelManager() to drive
applyOsaurusOrgFetch directly. The background launch-time fetch
races with those test calls — whichever finishes last wins, and
the merge result is non-deterministic. That's the root cause of
the flaky ModelManagerSuggestedTests failures seen across many of
the recent PR CI runs (applyOsaurusOrgFetch_dropsStaleAutoFetched
OnReapply, applyOsaurusOrgFetch_addsNewEntriesAfterCurated, etc.).

Gate the launch-time fetch on a small isRunningInTestEnvironment
helper that checks for any of XCTestConfigurationFilePath,
XCTestBundlePath, or XCTestSessionIdentifier in the process
environment. Those variables are only present inside an xctest host
process; production app launches still get the HF fetch exactly as
before.

This is a network call, so removing it under tests also has the
side benefit of making the test suite work offline / on hermetic
CI runners.

Co-authored-by: Michael Meding <mimeding@users.noreply.github.com>
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