Skip to content

feat(auth): make ambient Claude and Cursor CLI lanes explicit opt-in - #969

Merged
code-yeongyu merged 3 commits into
mainfrom
code-yeongyu/feat/ambient-auth-explicit-opt-in
Aug 19, 2026
Merged

feat(auth): make ambient Claude and Cursor CLI lanes explicit opt-in#969
code-yeongyu merged 3 commits into
mainfrom
code-yeongyu/feat/ambient-auth-explicit-opt-in

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Aug 19, 2026

Copy link
Copy Markdown
Owner

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-oauth had no enable flag at all. Its availability probe spawns the SDK-bundled Claude binary with auth status and 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 no claude-sdk-oauth entry in auth.json and no CLAUDE_CODE_OAUTH_TOKEN.
  • cursor-cli-oauth had enabled, but it defaulted to true, so an installed and logged-in cursor-agent made 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-oauth first in PROVIDER_PRECEDENCE, and the lane mirrors the entire Anthropic catalog, so it also opens builtin category gates (e.g. architect, which requires claude-fable-5) whose own fallback rungs do not list that provider.

Change

One semantic, applied to both lanes: an enabled flag 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.json and CLAUDE_CODE_OAUTH_TOKEN[_n] env accounts keep working with the flag unset — nobody's working setup breaks.

  • claudeSdkOauthProvider.enabled (env SENPI_CLAUDE_SDK_OAUTH_ENABLED). Checked after the env-token short-circuit and never on the stored-account path. It rides the single predicate behind both check and resolveAmbient, so availability and resolution cannot disagree.
  • cursorCliOauthProvider.enabled now defaults to false. Resolved settings gained explicitlyDisabled — true only when the last layer naming enabled set it to false verbatim — 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. isCursorCliOauthLaneEnabled is the one rule shared by check, the turn path, and the bootstrap gate.

Env-over-settings precedence is unchanged on both.

Verification

npm run check exit 0 · npm run build exit 0 · npm test exit 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 dist through real registration, a real on-disk settings.json, an isolated agent dir, and a real subprocess probe stubbed to exit 0 (a faithful "host CLI is logged in"):

Case claude-sdk-oauth cursor-cli-oauth
Ambient only, no opt-in hidden hidden
enabled: true in settings available available
env var opt-in available available
Stored login, no flag available available
Explicit enabled: false hidden (kill switch)

The 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

  • Labeled no-changelog: CONTRIBUTING.md reserves CHANGELOG.md for maintainers. The label bypasses only that policy, never changes.md coverage — scripts/audit-changes-md.mjs passes with zero uncovered paths, and both extensions carry full four-section entries.
  • Unrelated pre-existing flake seen once mid-run and not touched here: test/footer-data-provider.test.ts waits 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-oauth and cursor-cli-oauth explicit 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 requires enabled: true.

  • claudeSdkOauthProvider.enabled (default false; env SENPI_CLAUDE_SDK_OAUTH_ENABLED) gates only the ambient lane. Stored OAuth accounts in auth.json and CLAUDE_CODE_OAUTH_TOKEN[_n] env accounts remain available without the flag. One predicate drives both check and resolveAmbient.
  • cursorCliOauthProvider.enabled now defaults to false; resolved settings add explicitlyDisabled to preserve the enabled: false kill switch. With the flag absent, stored accounts keep the lane available. isCursorCliOauthLaneEnabled is shared by check, the turn path, and the native-credential bootstrap gate; bootstrap now also requires the flag. Env override: SENPI_CURSOR_CLI_OAUTH_ENABLED.

Migration

  • If you relied on an ambient Claude Code CLI, set claudeSdkOauthProvider.enabled: true or export SENPI_CLAUDE_SDK_OAUTH_ENABLED=1.
  • If you relied on cursor-cli-oauth, set cursorCliOauthProvider.enabled: true or export SENPI_CURSOR_CLI_OAUTH_ENABLED=1 to re-enable the lane and allow native credential bootstrap.
  • No action required if you use stored OAuth accounts or CLAUDE_CODE_OAUTH_TOKEN[_n] for claude-sdk-oauth; those continue to work with the flag unset.
  • Keep enabled: false (or SENPI_CURSOR_CLI_OAUTH_ENABLED=0) if you want cursor-cli-oauth disabled regardless of stored accounts; this remains a kill switch via explicitlyDisabled.

Written for commit 34a3de0. Summary will update on new commits.

Review in cubic

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 code-yeongyu added the no-changelog Generated or internal-only change that does not require a release changelog entry label Aug 19, 2026
@code-yeongyu
code-yeongyu merged commit 2ea563a into main Aug 19, 2026
24 of 28 checks passed
@code-yeongyu
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog Generated or internal-only change that does not require a release changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant