Skip to content

feat: add env provision command and login mismatch safety#189

Closed
nicknisi wants to merge 1 commit into
nicknisi/messaging-sweepfrom
nicknisi/env-provision-login-safety
Closed

feat: add env provision command and login mismatch safety#189
nicknisi wants to merge 1 commit into
nicknisi/messaging-sweepfrom
nicknisi/env-provision-login-safety

Conversation

@nicknisi

@nicknisi nicknisi commented Jul 8, 2026

Copy link
Copy Markdown
Member

What

  • workos env provision: provisions an unclaimed staging environment and returns credentials only — apiKey/clientId/claimToken via --json, no auth required, no code-gen, and no writes to the project directory (it calls provisionUnclaimedEnvironment directly, not the installer path that rewrites .env.local and repoints the active env). UnclaimedEnvApiError failures (e.g. 429) surface as structured errors.
  • Login mismatch safety: auth login no longer silently repoints the active environment to a different account's staging. Mismatches are detected via ownerEmail/clientId comparison; human mode prompts, agent/JSON mode warns and preserves the existing env under a distinct staging-N key. Every login now ends with an explicit Now using: <env> (<email>) line.
  • Stretch (approved): owner-identity fields persisted on environments (stamped at claim/login) so future mismatches attribute by email, plus a setActiveEnvironment helper.

Why

From the friction log: agents ran install in throwaway directories just to extract credentials because provisioning was coupled to AI code-gen; and auth login silently switched the active environment to a different account's staging mid-session, which the user only discovered later.

Testing

  • New JSON-mode credential emission and 429 failure-path cases in env.spec.ts; clientId-mismatch / no-prior-env / same-account cases in login.spec.ts; config-store.spec.ts coverage for the new fields.
  • Failure-first verified; full gate green (2,297 tests at this point in the stack); runtime behavior verified on the built binary.

Notes

Commit is unsigned (headless 1Password limitation) — no signed-commit rule on the repo; squash merge re-signs.


Stack 5/7 (akshay-friction-log): merge after #188, then rebase onto main.

Add `workos env provision`, a credentials-only subcommand that calls
provisionUnclaimedEnvironment() directly (no auth, no code-gen). It emits
credentials on stdout (JSON is the agent credential channel), stores the
result as a local active unclaimed env so a follow-up `env claim` works, and
never writes to the project directory or any .env file. Failures (incl. 429)
surface as structured errors with no fallback to login.

Rework auth login staging provisioning to stop silent cross-account switches.
provisionStagingEnvironment now takes the authenticated account, detects a
mismatch against the prior active env (ownerEmail wins over clientId), and on
mismatch writes the new account's Staging under a distinct key instead of
clobbering the active slot. runLogin prompts (human) / warns (agent-CI) /
emits structured JSON, and always ends with an explicit "Now using" line.

Stretch: persist ownerEmail/ownerUserId on environment records and carry them
through markEnvironmentClaimed; add setActiveEnvironment().

Regression tests added first and confirmed failing on pre-fix code
(env provision, mismatch/in-place-clobber guard, "Now using", owner fields).
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds unclaimed environment provisioning and safer staging selection during login. The main changes are:

  • New workos env provision command that returns unclaimed environment credentials.
  • Local persistence of provisioned unclaimed environments without writing project .env files.
  • Owner identity fields on stored environments.
  • Login mismatch handling that preserves the active environment across different accounts.
  • JSON help metadata and tests for the new command and login paths.

Confidence Score: 4/5

Mostly safe, with one contained JSON-mode login issue to fix before merging.

Core provisioning and mismatch handling are covered, but auth login --json can emit non-JSON stdout before the final JSON payload.

src/commands/login.ts

T-Rex T-Rex Logs

What T-Rex did

  • Reproduced the JSON output corruption by running a focused Vitest harness against the real runLogin command with mocked device-code, token, browser-open, and staging APIs.
  • Observed that stdout printed a verification URL and user code before the final JSON payload, indicating human prompt text preceded the JSON.
  • Validated that parsing the captured stdout fails with an invalid JSON error due to the leading human prompt text.
  • Reran the test suite and confirmed Vitest completed with 4 test files passed, 138 tests passed, and exit code 0.
  • Reviewed provisioning-related logs and confirmed the CLI reports dist/bin.js exists, provides provision help, and exits successfully.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/bin.ts Registers the new env provision subcommand and wires insecure-storage handling consistently with other env subcommands.
src/commands/env.ts Adds runEnvProvision to call the unclaimed environment API, persist the unclaimed env locally, and emit credentials without project writes.
src/commands/login.ts Adds owner-aware staging provisioning and mismatch handling, but JSON login output is still preceded by human stdout prompts.
src/lib/config-store.ts Adds owner identity fields, preserves them when claiming unclaimed environments, and introduces a safe active-env setter.
src/utils/help-json.ts Adds the env provision command schema to generated JSON help metadata.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant CLI
participant UnclaimedAPI as Unclaimed Env API
participant StagingAPI as Staging API
participant Config as CLI Config Store

alt workos env provision
    User->>CLI: env provision
    CLI->>UnclaimedAPI: provisionUnclaimedEnvironment()
    UnclaimedAPI-->>CLI: apiKey, clientId, claimToken, authkitDomain
    CLI->>Config: save unclaimed env and set active
    CLI-->>User: credentials (JSON or human output)
else workos auth login
    User->>CLI: auth login
    CLI->>StagingAPI: fetchStagingCredentials(accessToken)
    StagingAPI-->>CLI: staging apiKey/clientId
    CLI->>Config: compare active ownerEmail/clientId
    alt same account or no prior active env
        CLI->>Config: write/update staging and set active if first env
    else different account
        CLI->>Config: write staging-N and preserve prior active env
        CLI-->>User: prompt or structured mismatch warning
    end
end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant CLI
participant UnclaimedAPI as Unclaimed Env API
participant StagingAPI as Staging API
participant Config as CLI Config Store

alt workos env provision
    User->>CLI: env provision
    CLI->>UnclaimedAPI: provisionUnclaimedEnvironment()
    UnclaimedAPI-->>CLI: apiKey, clientId, claimToken, authkitDomain
    CLI->>Config: save unclaimed env and set active
    CLI-->>User: credentials (JSON or human output)
else workos auth login
    User->>CLI: auth login
    CLI->>StagingAPI: fetchStagingCredentials(accessToken)
    StagingAPI-->>CLI: staging apiKey/clientId
    CLI->>Config: compare active ownerEmail/clientId
    alt same account or no prior active env
        CLI->>Config: write/update staging and set active if first env
    else different account
        CLI->>Config: write staging-N and preserve prior active env
        CLI-->>User: prompt or structured mismatch warning
    end
end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/commands/login.ts:258-264
**JSON output is corrupted**
`auth login --json` still writes human prompts to stdout before this `outputJson` call: lines 211-213 print the verification URL and user code with `console.log`. Consumers parsing stdout as JSON receive those prompt lines before the JSON object, so agent and JSON-mode login workflows can fail before they see the structured mismatch fields.

Reviews (1): Last reviewed commit: "feat: add env provision command and logi..." | Re-trigger Greptile

Comment thread src/commands/login.ts
Comment on lines +258 to +264
if (isJsonMode()) {
outputJson({
status: 'ok',
account: { email: account.email ?? null, userId: account.userId },
activeEnvironment: provision.activeEnvironment ?? null,
mismatch: provision.mismatch,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 JSON output is corrupted
auth login --json still writes human prompts to stdout before this outputJson call: lines 211-213 print the verification URL and user code with console.log. Consumers parsing stdout as JSON receive those prompt lines before the JSON object, so agent and JSON-mode login workflows can fail before they see the structured mismatch fields.

Artifacts

Repro: focused Vitest harness that runs auth login in JSON mode with mocked auth and staging APIs

  • Contains supporting evidence from the run (text/typescript; charset=utf-8).

Repro: verbose test output showing prompt text before the final JSON object on stdout

  • Keeps the command output available without making the summary code-heavy.

Repro: parse-attempt log showing captured stdout is not valid JSON

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/login.ts
Line: 258-264

Comment:
**JSON output is corrupted**
`auth login --json` still writes human prompts to stdout before this `outputJson` call: lines 211-213 print the verification URL and user code with `console.log`. Consumers parsing stdout as JSON receive those prompt lines before the JSON object, so agent and JSON-mode login workflows can fail before they see the structured mismatch fields.

How can I resolve this? If you propose a fix, please make it concise.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread src/commands/login.ts
Comment on lines +81 to +90
function isMismatch(
prior: EnvironmentConfig | undefined,
account: { email?: string },
staging: { clientId: string },
): boolean {
if (!prior) return false;
if (prior.ownerEmail && account.email) return prior.ownerEmail !== account.email;
if (prior.clientId) return prior.clientId !== staging.clientId;
return false;
}

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.

🔍 clientId-based mismatch detection may false-positive across environment types

The isMismatch function (src/commands/login.ts:81-90) falls back to comparing prior.clientId !== staging.clientId when neither side has ownerEmail. This works correctly when comparing two staging environments, but if the active environment is a production or manually-added sandbox env, its clientId will always differ from the staging credentials' clientId — even when both belong to the same WorkOS account. This would cause a false-positive mismatch, prompting the user to confirm an environment switch that isn't actually cross-account.

The new ownerEmail field added in this PR mitigates this for environments provisioned via auth login going forward, since email comparison takes priority. The clientId fallback only fires for legacy or manually-added environments without ownerEmail. The comment at src/commands/login.ts:78-80 acknowledges this limitation ('When neither is available we cannot tell — return false'), but the clientId path can actively misidentify same-account environments as mismatches rather than returning the safe false default.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/commands/env.ts
Comment on lines +152 to +162
// Persist as an unclaimed env (parity with install) — NEVER writes to the project dir.
const config = getOrCreateConfig();
config.environments['unclaimed'] = {
name: 'unclaimed',
type: 'unclaimed',
apiKey: result.apiKey,
clientId: result.clientId,
claimToken: result.claimToken,
};
config.activeEnvironment = 'unclaimed';
saveConfig(config);

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.

🔍 env provision unconditionally overwrites existing unclaimed environment and repoints active env

The runEnvProvision function at src/commands/env.ts:154-162 always writes to config.environments['unclaimed'] and always sets config.activeEnvironment = 'unclaimed', regardless of what was previously configured. If a user runs env provision twice without claiming the first environment, the first environment's claim token is permanently lost. This is consistent with the existing tryProvisionUnclaimedEnv in src/lib/unclaimed-env-provision.ts:58-66, so it's not a regression, but it contrasts with the careful mismatch/clobber-prevention logic added to provisionStagingEnvironment in this same PR. The unconditional active-env repoint also silently switches away from any prior active environment without warning.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@nicknisi

nicknisi commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Closing in favor of a single combined PR from nicknisi/akshay.

@nicknisi nicknisi closed this Jul 8, 2026
@nicknisi nicknisi deleted the nicknisi/env-provision-login-safety branch July 8, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant