diff --git a/README.md b/README.md index 3df20538..572d0394 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,22 @@ Commands: skills Manage WorkOS skills for coding agents (install, uninstall, list) mcp Manage the WorkOS MCP server in coding agents setup Set up WorkOS skills and the MCP server +``` + +**Nothing is installed into your coding agents without explicit opt-in.** The CLI never silently writes skills or MCP configuration into `~/.claude`, `~/.cursor`, etc. After `workos login` or `workos install`, an interactive session may offer to set up your agents — the prompt defaults to **No**, and declining (or running non-interactively) installs nothing. To opt in at any time: + +```bash +workos setup # interactive setup (skills + MCP server) +workos setup --yes # non-interactive opt-in +workos skills install # skills only +workos mcp install # MCP server only +``` -`workos setup` installs WorkOS skills and configures the MCP server only after consent. Use `workos skills list` to check skill status, `workos mcp status` to check whether the server definition is configured, or `workos doctor --fix` to refresh stale skills. +Use `workos skills list` to check skill status, `workos mcp status` to check whether the server definition is configured, or `workos doctor --fix` to refresh stale skills you previously installed. MCP configuration and OAuth authentication are separate states. The WorkOS CLI never inspects a coding agent's credentials, so "configured" means the server definition is in place — it cannot prove that OAuth is usable in any agent. Each agent owns its own OAuth; with Codex, for example, complete or refresh it with `codex mcp login workos` in your normal host shell. See the [WorkOS MCP setup and recovery guide](https://workos.com/docs/mcp) for user-global and trusted-project-only configuration. +```text Resource Management: organization (org) Manage organizations user Manage users diff --git a/src/commands/setup.spec.ts b/src/commands/setup.spec.ts index 28593d6b..f92324bc 100644 --- a/src/commands/setup.spec.ts +++ b/src/commands/setup.spec.ts @@ -203,6 +203,15 @@ describe('runSetup — automatic triggers (login/install)', () => { expect(prefs.recordSetupCompleted).not.toHaveBeenCalled(); }); + it('defaults the consent prompt to No (AUTH-6734: install is explicit opt-in)', async () => { + detectSome(); + vi.mocked(ui.confirm).mockResolvedValue(false); + + await runSetup({ trigger: 'login' }); + + expect(ui.confirm).toHaveBeenCalledWith(expect.objectContaining({ initialValue: false })); + }); + it('records an absolute decline and installs nothing on "no"', async () => { detectSome(); vi.mocked(ui.confirm).mockResolvedValue(false); @@ -214,6 +223,19 @@ describe('runSetup — automatic triggers (login/install)', () => { expect(prefs.recordSetupCompleted).not.toHaveBeenCalled(); }); + it('prints manual-install instructions when the user declines', async () => { + detectSome(); + vi.mocked(ui.confirm).mockResolvedValue(false); + + await runSetup({ trigger: 'login' }); + + const hints = vi.mocked(ui.log.hint).mock.calls.map(([msg]) => String(msg)); + expect(hints.some((m) => m.includes('Nothing was installed'))).toBe(true); + expect(hints.some((m) => m.includes('workos setup'))).toBe(true); + expect(hints.some((m) => m.includes('workos skills install'))).toBe(true); + expect(hints.some((m) => m.includes('workos mcp install'))).toBe(true); + }); + it('treats cancel (ctrl-c) as skip — no decline recorded, but emits a cancelled event', async () => { detectSome(); vi.mocked(ui.confirm).mockResolvedValue(CANCEL); @@ -307,6 +329,17 @@ describe('runSetup — command trigger', () => { expect(prefs.recordSetupDeclined).not.toHaveBeenCalled(); }); + it('scopes the decline instructions to what was offered', async () => { + vi.mocked(detectAgents).mockReturnValue([claudeAgent as any]); + vi.mocked(ui.confirm).mockResolvedValue(false); + + await runSetup({ trigger: 'command', skillsOnly: true }); + + const hints = vi.mocked(ui.log.hint).mock.calls.map(([msg]) => String(msg)); + expect(hints.some((m) => m.includes('workos skills install'))).toBe(true); + expect(hints.some((m) => m.includes('workos mcp install'))).toBe(false); + }); + it('skillsOnly skips MCP detection/install', async () => { vi.mocked(detectAgents).mockReturnValue([claudeAgent as any]); vi.mocked(ui.confirm).mockResolvedValue(true); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 668e858a..efd2a6aa 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -15,6 +15,11 @@ * The consent contract is the whole point: nothing is written to a coding agent * unless the user says yes (or passes --yes). This replaces the auto-install * that a customer called "prompt injection malware". + * + * AUTH-6734 policy: never install silently. The consent prompt defaults to No, + * so an absent-minded Enter (or any non-answer) installs nothing; the only ways + * anything lands are an explicit "yes" at the prompt or an explicit flag + * (`workos setup --yes`, `workos skills install`, `workos mcp install`). */ import { homedir } from 'node:os'; @@ -144,7 +149,9 @@ export async function runSetup(opts: RunSetupOptions): Promise { `scaffold auth and manage WorkOS resources. Nothing is written until you confirm.`, ); - const answer = await ui.confirm({ message: 'Set up now?', initialValue: true }); + // Default MUST stay No (AUTH-6734): installation is opt-in only, so the + // default answer — what an impatient Enter produces — installs nothing. + const answer = await ui.confirm({ message: 'Set up now?', initialValue: false }); // Cancel (ctrl-c) is not a decline — skip silently and ask again next time, // but record it so the cut-off is observable in telemetry. if (isCancel(answer)) { @@ -154,7 +161,7 @@ export async function runSetup(opts: RunSetupOptions): Promise { if (!answer) { if (!isCommand) recordSetupDeclined(); emitSetupEvent(opts.trigger, startedAt, 'declined', { skills: [], mcpInstalled: [], mcpFailed: [] }); - ui.log.hint(`No problem. Run \`${formatWorkOSCommand('setup')}\` anytime.`); + printManualInstallInstructions(wantSkills, wantMcp); return; } } @@ -209,6 +216,24 @@ async function installAndReport( reportResults(skillResult ? { agents: skillAgentNames, count: skillResult.skills.length } : null, mcpResults); } +/** + * A decline is the safe default, not a dead end (AUTH-6734): always leave the + * exact manual-install commands behind so opting in later is self-serve. + * Scoped to what the offer actually covered (--skills-only / --mcp-only). + */ +function printManualInstallInstructions(wantSkills: boolean, wantMcp: boolean): void { + ui.log.hint('Nothing was installed. To install later, run any of:'); + if (wantSkills && wantMcp) { + ui.log.hint(` ${formatWorkOSCommand('setup')} skills + MCP server`); + } + if (wantSkills) { + ui.log.hint(` ${formatWorkOSCommand('skills install')} skills only`); + } + if (wantMcp) { + ui.log.hint(` ${formatWorkOSCommand('mcp install')} MCP server only`); + } +} + interface SkillSummary { agents: string[]; count: number;