Skip to content

Commit 6609ef6

Browse files
authored
refactor(auth): Atlas is login/logout, Codex is connect/disconnect (#118)
Splits the two account types onto distinct, symmetric verb pairs to remove the long-standing ambiguity where both 'connect login' and 'login' meant the Atlas account: - Atlas (managed account, wallet, sync) keeps the top-level login / logout / status / sync / devices commands. The back-compat 'connect' umbrella that aliased them is removed. - Codex (ChatGPT subscription) now owns the connect / disconnect verbs. 'connect [codex]' signs in with ChatGPT (reusing runCodexAuthFlow); 'disconnect [codex]' is a NEW command that clears the local openai-codex OAuth credential and best-effort revokes it server-side — the missing counterpart to sign-in. 'keys signin' still works. Updates every user-facing string and prompt that told users to run 'openscience connect login' / 'connect sync' to the top-level 'openscience login' / 'sync' (the connect subcommands no longer exist), including the canonical initialize-atlas-graph SKILL.md so it stays in sync with the embedded copy.
1 parent 63564fd commit 6609ef6

13 files changed

Lines changed: 102 additions & 37 deletions

File tree

backend/cli/skills/research/initialize-atlas-graph/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Run this skill when:
2727
atlas doctor --format=json
2828
```
2929
If it reports unavailable/unauthenticated, tell the user to run
30-
`openscience connect login` — the graph cannot be created without a session.
30+
`openscience login` — the graph cannot be created without a session.
3131

3232
2. **Create or link the graph** (idempotent — safe to re-run; returns the
3333
existing graph if one already exists):
@@ -41,7 +41,7 @@ Run this skill when:
4141
`status`, `message` when known). Relay the fix that matches the kind — do
4242
NOT guess or tell the user to re-login for a network problem:
4343
- `"unauthenticated"` — no session, or the backend rejected the saved key.
44-
Tell the user to run `openscience connect login`.
44+
Tell the user to run `openscience login`.
4545
- `"unreachable"` — the Atlas backend at the printed `host` could not be
4646
reached (network/DNS error or 5xx). The user IS logged in; suggest checking
4747
connectivity and any `OPENSCIENCE_API_BASE`/`SYNSC_API_BASE` override, then

backend/cli/src/agent/prompt/research.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ At the START of the task, before Stage 1, set up the graph:
5757
even if `atlas` is unavailable. On success it prints `{"project_id":"<id>"}` and writes
5858
`.openscience/project.json`; note the project_id. ONLY skip Atlas if it prints
5959
`project_id: null` with an `error` kind — relay the matching fix (`unauthenticated` →
60-
`openscience connect login`; `plan` → app.syntheticsciences.ai/cli; `unreachable`/`backend`
60+
`openscience login`; `plan` → app.syntheticsciences.ai/cli; `unreachable`/`backend`
6161
→ show the message) and continue the work. (Or load the `initialize-atlas-graph` skill,
6262
which wraps this exact command.)
6363
2. **Load prior graph state — optional, best-effort.** Run `atlas doctor --format=json`. If it

backend/cli/src/cli/cmd/auth.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,87 @@ export const AuthCodexCommand = cmd({
536536
},
537537
})
538538

539+
/** Best-effort server-side disconnect of the Codex credential, mirroring
540+
* pushTokensToBackend's POST. Runs BEFORE the local removal so the thk_ key can
541+
* still authenticate the call. Never throws — the local Auth.remove is what
542+
* actually signs the CLI out. */
543+
async function disconnectCodexBackend(): Promise<void> {
544+
const session = await OpenScience.getSession?.()
545+
const thkToken = session?.api_key
546+
if (!thkToken) return
547+
try {
548+
await fetch(`${managedApiBase()}/api/keys/openai-codex`, {
549+
method: "DELETE",
550+
headers: { Authorization: `Bearer ${thkToken}` },
551+
})
552+
} catch {
553+
/* best-effort — local removal below is the source of truth */
554+
}
555+
}
556+
557+
/** `openscience connect [codex]` — sign in with a ChatGPT (Codex) subscription.
558+
* The connect/disconnect verb pair is Codex's; Atlas uses login/logout. */
559+
export const ConnectCommand = cmd({
560+
command: "connect [service]",
561+
describe: "connect a ChatGPT subscription (Codex) — sign in with ChatGPT",
562+
builder: (yargs) =>
563+
yargs.positional("service", {
564+
type: "string",
565+
choices: ["codex"] as const,
566+
default: "codex",
567+
describe: "service to connect (only `codex` today)",
568+
}),
569+
async handler() {
570+
await Instance.provide({
571+
directory: process.cwd(),
572+
async fn() {
573+
UI.empty()
574+
prompts.intro("Connect ChatGPT (Codex)")
575+
const handled = await runCodexAuthFlow()
576+
if (!handled) prompts.outro("Done")
577+
},
578+
})
579+
},
580+
})
581+
582+
/** `openscience disconnect [codex]` — sign out of ChatGPT (Codex): clears the
583+
* local OAuth credential and best-effort revokes it server-side. */
584+
export const DisconnectCommand = cmd({
585+
command: "disconnect [service]",
586+
describe: "disconnect your ChatGPT subscription (Codex)",
587+
builder: (yargs) =>
588+
yargs.positional("service", {
589+
type: "string",
590+
choices: ["codex"] as const,
591+
default: "codex",
592+
describe: "service to disconnect (only `codex` today)",
593+
}),
594+
async handler() {
595+
await Instance.provide({
596+
directory: process.cwd(),
597+
async fn() {
598+
UI.empty()
599+
prompts.intro("Disconnect ChatGPT (Codex)")
600+
601+
const existing = await Auth.get("openai-codex")
602+
const backend = await backendHasCodex()
603+
if (!existing && backend !== true) {
604+
prompts.log.warn("ChatGPT (Codex) isn't connected.")
605+
prompts.outro("Done")
606+
return
607+
}
608+
609+
// Revoke server-side while the thk_ key can still authenticate, then
610+
// drop the local credential (the part that actually signs the CLI out).
611+
await disconnectCodexBackend()
612+
await Auth.remove("openai-codex")
613+
prompts.log.success("Disconnected ChatGPT (Codex)")
614+
prompts.outro("Done")
615+
},
616+
})
617+
},
618+
})
619+
539620
export const AuthLogoutCommand = cmd({
540621
command: ["remove", "rm", "logout"],
541622
describe: "remove a saved provider key",

backend/cli/src/cli/cmd/connect.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -303,18 +303,8 @@ export const DevicesCommand = cmd({
303303
},
304304
})
305305

306-
/** Back-compat umbrella: `openscience connect <login|logout|status|sync|devices>`
307-
* still works and forwards to the promoted top-level commands. */
308-
export const ConnectCommand = cmd({
309-
command: "connect",
310-
describe: "Atlas account (alias for `login` / `logout` / `status` / `sync` / `devices`)",
311-
builder: (yargs) =>
312-
yargs
313-
.command(LoginCommand)
314-
.command(LogoutCommand)
315-
.command(StatusCommand)
316-
.command(SyncCommand)
317-
.command(DevicesCommand)
318-
.demandCommand(),
319-
async handler() {},
320-
})
306+
// Atlas is `login` / `logout` (+ `status` / `sync` / `devices`), all top-level.
307+
// The `connect` / `disconnect` verbs now belong to Codex (see cli/cmd/auth.ts):
308+
// `connect` signs in with ChatGPT, `disconnect` signs out. Keeping the two
309+
// account types on distinct verb pairs removes the old ambiguity where
310+
// `connect login` and `login` both meant the Atlas account.

backend/cli/src/cli/cmd/skill.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ const SkillListCommand = cmd({
308308
UI.println(
309309
"Only a small bundled skill set is visible; this usually means the full OpenScience skill index was not fetched.",
310310
)
311-
UI.println("Run `openscience connect sync` or try again once online/authenticated.")
311+
UI.println("Run `openscience sync` or try again once online/authenticated.")
312312
UI.println("")
313313
} else if (!showAll) {
314314
UI.println("Use `openscience skill list --all` to show bundled OpenScience skills.")

backend/cli/src/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,10 @@ import { EOL } from "os"
3232
import { WebCommand } from "./cli/cmd/web"
3333
import { PrCommand } from "./cli/cmd/pr"
3434
import { SessionCommand } from "./cli/cmd/session"
35-
import {
36-
ConnectCommand,
37-
LoginCommand,
38-
LogoutCommand,
39-
StatusCommand,
40-
SyncCommand,
41-
DevicesCommand,
42-
} from "./cli/cmd/connect"
35+
import { LoginCommand, LogoutCommand, StatusCommand, SyncCommand, DevicesCommand } from "./cli/cmd/connect"
4336
import { ProjectCommand } from "./cli/cmd/project"
4437
import { WalletCommand } from "./cli/cmd/billing"
45-
import { KeysCommand } from "./cli/cmd/auth"
38+
import { KeysCommand, ConnectCommand, DisconnectCommand } from "./cli/cmd/auth"
4639
import { InitCommand, DoctorCommand } from "./cli/onboard"
4740
import { OpenScience } from "./openscience"
4841

@@ -145,6 +138,7 @@ const cli = yargs(hideBin(process.argv))
145138
.command(DoctorCommand)
146139
.command(ProjectCommand)
147140
.command(ConnectCommand)
141+
.command(DisconnectCommand)
148142
.fail((msg, err) => {
149143
if (
150144
msg?.startsWith("Unknown argument") ||

backend/cli/src/openscience/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ if (API_BASE !== DEFAULT_API_BASE) {
3737
}
3838
}
3939

40-
// User-facing URL the CLI prints during `openscience connect login`. Defaults
40+
// User-facing URL the CLI prints during `openscience login`. Defaults
4141
// to the unified Atlas frontend's /cli route — Plan tab, key management,
4242
// and billing all live there. SYNSC_AUTH_URL overrides (e.g. point at a
4343
// staging frontend or the old auth.syntheticsciences.ai surface).
@@ -514,7 +514,7 @@ export namespace OpenScience {
514514
} catch {}
515515
// Union of what this process synced (in-memory map) and what the last
516516
// sync persisted (disk snapshot, replayed by preload-env.ts at boot) —
517-
// a fresh `connect logout` process has only the latter.
517+
// a fresh `logout` process has only the latter.
518518
const synced = await readSyncedSnapshot()
519519
for (const [key, value] of syncedSecretValues.entries()) synced.set(key, value)
520520
for (const name of ["synced-env.json", "openscience-synced.json"]) {

backend/cli/src/openscience/preload-env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Runs BEFORE any provider SDK construction (Anthropic, OpenAI,
55
* @ai-sdk/google) so those SDKs see the correct ANTHROPIC_BASE_URL /
66
* ANTHROPIC_API_KEY / etc. on their own startup, without waiting for
7-
* the asynchronous `openscience connect sync` call later in CLI boot.
7+
* the asynchronous `openscience sync` call later in CLI boot.
88
*
99
* Without this, the first invocation after a fresh terminal session
1010
* would race: sync sets process.env in-process, but the SDK had

backend/cli/src/plugin/codex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
742742
})
743743
// Re-sync after backend now knows about the new codex
744744
// credential, so `openai-codex` shows up in the local
745-
// provider list without a separate `openscience connect sync`.
745+
// provider list without a separate `openscience sync`.
746746
await OpenScience.syncServices?.().catch((e: unknown) => {
747747
log.warn("post-codex-login sync failed", { error: String(e) })
748748
})

backend/cli/src/provider/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export namespace Provider {
120120
if (isAtlasProxyBaseURL(options["baseURL"])) return
121121
throw new Error(
122122
`${provider.id} is using a managed Atlas key without an Atlas proxy URL. ` +
123-
"Run `openscience connect sync` and try again.",
123+
"Run `openscience sync` and try again.",
124124
)
125125
}
126126

0 commit comments

Comments
 (0)