feat(auth): make ambient Claude and Cursor CLI lanes explicit opt-in - #969
Merged
code-yeongyu merged 3 commits intoAug 19, 2026
Merged
Conversation
A logged-in Claude Code CLI on the host made this provider report itself available with no senpi-side action, so senpi could spend the user's Claude subscription through a lane the user never configured. `claudeSdkOauthProvider.enabled` (default false, env override SENPI_CLAUDE_SDK_OAUTH_ENABLED) now gates the ambient lane only. It is checked after the environment-token short-circuit and never on the stored-account path, so an explicit senpi-side login stays its own opt-in: stored auth.json accounts and CLAUDE_CODE_OAUTH_TOKEN[_n] keep working with the flag unset. The flag rides the single predicate behind both `check` and `resolveAmbient`, so availability and resolution cannot disagree.
`cursorCliOauthProvider.enabled` defaulted to true, so an installed and logged-in cursor-agent alone made the lane ready and let senpi copy the host's native Cursor credential into a managed slot without the user opting in. The default is now false. Resolved settings gained `explicitlyDisabled`, true only when the last layer naming `enabled` set it to false verbatim, which keeps the documented `enabled: false` kill switch meaningful: without that distinction, flipping the default would have silently turned the existing opt-out into a no-op for anyone holding stored accounts. `isCursorCliOauthLaneEnabled` is the one rule shared by `check`, the turn path, and the bootstrap gate. Stored accounts keep the lane available without the flag, matching claude-sdk-oauth: they exist only after an explicit /login cursor-cli-oauth or /cursor-account import, and that login is the opt-in.
Record that claude-sdk-oauth's ambient lane and the cursor-cli-oauth lane are now explicit opt-ins, with their settings keys, env overrides, default values, and the stored-login exemption, plus why a vendor CLI being logged in on the machine is not consent to spend that subscription.
code-yeongyu
deleted the
code-yeongyu/feat/ambient-auth-explicit-opt-in
branch
August 19, 2026 05:19
code-yeongyu
added a commit
to leeseunguk/senpi
that referenced
this pull request
Aug 20, 2026
…th (issue code-yeongyu#723) The fullstack harness gains stallNextResponse() (headers flushed, first SSE event withheld) so a turn can stall at stream start against the loopback server. The new probe stalls a continuation turn, lets auto-retry fire, and asserts every post-stall continuity decision forks with deltaMessages == 1 — no flatten, no re-bill. Also repairs the harness for the post-code-yeongyu#969 ambient opt-in gate: seeds the sandbox CLAUDE_CONFIG_DIR credential store and sets the enabled opt-in.
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.
Problem
A vendor CLI being logged in on the machine is not consent to spend that subscription.
Both ambient-auth builtin providers reported themselves available purely from host state, with no senpi-side action:
claude-sdk-oauthhad no enable flag at all. Its availability probe spawns the SDK-bundled Claude binary withauth statusand treats exit 0 as available, so any machine with a logged-in Claude Code CLI silently routed turns through the user's Claude Pro/Max subscription — even with noclaude-sdk-oauthentry inauth.jsonand noCLAUDE_CODE_OAUTH_TOKEN.cursor-cli-oauthhadenabled, but it defaulted totrue, so an installed and logged-incursor-agentmade the lane ready and let senpi copy the host's native Cursor credential into a managed slot.This is amplified downstream: the retry-fallback expansion ranks
claude-sdk-oauthfirst inPROVIDER_PRECEDENCE, and the lane mirrors the entire Anthropic catalog, so it also opens builtin category gates (e.g.architect, which requiresclaude-fable-5) whose own fallback rungs do not list that provider.Change
One semantic, applied to both lanes: an
enabledflag that defaults to false and gates only the ambient, host-CLI-derived lane.An explicit senpi-side login is itself the opt-in. Stored OAuth accounts in
auth.jsonandCLAUDE_CODE_OAUTH_TOKEN[_n]env accounts keep working with the flag unset — nobody's working setup breaks.claudeSdkOauthProvider.enabled(envSENPI_CLAUDE_SDK_OAUTH_ENABLED). Checked after the env-token short-circuit and never on the stored-account path. It rides the single predicate behind bothcheckandresolveAmbient, so availability and resolution cannot disagree.cursorCliOauthProvider.enablednow defaults to false. Resolved settings gainedexplicitlyDisabled— true only when the last layer namingenabledset it tofalseverbatim — which keeps the documented kill switch meaningful. Without that distinction, flipping the default would have silently turned the existing opt-out into a no-op for anyone holding stored accounts.isCursorCliOauthLaneEnabledis the one rule shared bycheck, the turn path, and the bootstrap gate.Env-over-settings precedence is unchanged on both.
Verification
npm run checkexit 0 ·npm run buildexit 0 ·npm testexit 0 (8443 passed, 0 failed, 1021 files)Every behavior change was captured failing first: 3 RED assertions for the claude lane, 5 for cursor, then green. No test was deleted or skipped — pre-existing cases asserting "logged-in host CLI ⇒ available" were converted to declare the opt-in explicitly, since that behavior is precisely what this PR removes.
Real-surface probes drive the built
distthrough real registration, a real on-disksettings.json, an isolated agent dir, and a real subprocess probe stubbed to exit 0 (a faithful "host CLI is logged in"):claude-sdk-oauthcursor-cli-oauthenabled: truein settingsenabled: falseThe added integration case is mutation-proven: neutralizing the gate flips exactly that assertion at
ModelRuntime.hasConfiguredAuth— the function the fallback controller actually reads — and reverting restores green.Notes
no-changelog:CONTRIBUTING.mdreservesCHANGELOG.mdfor maintainers. The label bypasses only that policy, neverchanges.mdcoverage —scripts/audit-changes-md.mjspasses with zero uncovered paths, and both extensions carry full four-section entries.test/footer-data-provider.test.tswaits on an fs-watcher event against a fixed 10s wall-clock deadline, which loses under a loaded full-suite run. It passes in isolation on this branch and on clean base, and the final suite run was green.Summary by cubic
Make ambient auth for
claude-sdk-oauthandcursor-cli-oauthexplicit opt-in to prevent unconsented subscription usage. Previously, a logged-in vendor CLI on the host made these lanes available; now the ambient (host-CLI-derived) lane requiresenabled: true.claudeSdkOauthProvider.enabled(default false; envSENPI_CLAUDE_SDK_OAUTH_ENABLED) gates only the ambient lane. Stored OAuth accounts inauth.jsonandCLAUDE_CODE_OAUTH_TOKEN[_n]env accounts remain available without the flag. One predicate drives bothcheckandresolveAmbient.cursorCliOauthProvider.enablednow defaults to false; resolved settings addexplicitlyDisabledto preserve theenabled: falsekill switch. With the flag absent, stored accounts keep the lane available.isCursorCliOauthLaneEnabledis shared bycheck, the turn path, and the native-credential bootstrap gate; bootstrap now also requires the flag. Env override:SENPI_CURSOR_CLI_OAUTH_ENABLED.Migration
claudeSdkOauthProvider.enabled: trueor exportSENPI_CLAUDE_SDK_OAUTH_ENABLED=1.cursor-cli-oauth, setcursorCliOauthProvider.enabled: trueor exportSENPI_CURSOR_CLI_OAUTH_ENABLED=1to re-enable the lane and allow native credential bootstrap.CLAUDE_CODE_OAUTH_TOKEN[_n]forclaude-sdk-oauth; those continue to work with the flag unset.enabled: false(orSENPI_CURSOR_CLI_OAUTH_ENABLED=0) if you wantcursor-cli-oauthdisabled regardless of stored accounts; this remains a kill switch viaexplicitlyDisabled.Written for commit 34a3de0. Summary will update on new commits.