Skip to content
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
00603d3
fix(codex): write refresh tokens to openai id
shuv1337 Jan 14, 2026
78fff0a
fix(auth): fallback to legacy codex entry
shuv1337 Jan 14, 2026
5667123
feat(config): add openai multi-account flag
shuv1337 Jan 14, 2026
7e7acc2
feat(plugin): add optional oauth email field
shuv1337 Jan 14, 2026
a660b76
fix(auth): migrate legacy codex oauth entry
shuv1337 Jan 14, 2026
8cb16e3
feat(plugin): add optional oauth name field
shuv1337 Jan 14, 2026
8bedb9a
feat(auth): add email field to OAuth schema
shuv1337 Jan 14, 2026
67002a1
feat(auth): add name field to OAuth schema
shuv1337 Jan 14, 2026
aa9aaf9
feat(auth): add plan field to OAuth schema
shuv1337 Jan 14, 2026
578d8b8
feat(auth): add orgName field to OAuth schema
shuv1337 Jan 14, 2026
af70e14
feat(auth): persist OAuth metadata fields in ProviderAuth.callback
shuv1337 Jan 14, 2026
d7496e3
test(auth): add codex-migration.test.ts with 5 tests
shuv1337 Jan 14, 2026
624d2ec
docs(agents): add testing notes for running tests from packages/opencode
shuv1337 Jan 14, 2026
4263482
test(auth): add token refresh migration test for openai provider ID
shuv1337 Jan 14, 2026
88bbc1a
docs(prd): mark migration tests as complete
shuv1337 Jan 14, 2026
ccbecb6
feat(codex): extract user info from id_token claims
shuv1337 Jan 14, 2026
7b4e652
docs(plugin): add JSDoc comments for OAuth metadata fields
shuv1337 Jan 14, 2026
5ac4b59
feat(codex): add ChatGPT user info fetch and plan normalization
shuv1337 Jan 14, 2026
96956f0
feat(codex): spawn background task to fetch ChatGPT user info
shuv1337 Jan 14, 2026
4043d10
feat(server): add GET /auth/info/:providerID endpoint
shuv1337 Jan 14, 2026
bf7f23f
feat(tui): load auth info during bootstrap
shuv1337 Jan 14, 2026
968112f
feat(tui): create AccountBadge component
shuv1337 Jan 14, 2026
07ccfce
feat(tui): integrate AccountBadge in footer
shuv1337 Jan 14, 2026
82311f3
feat(tui): show auth status in provider dialog
shuv1337 Jan 14, 2026
5cc3f96
feat(config): add skills registry configuration schema
shuv1337 Jan 14, 2026
df300ff
feat(skill): create skill registry module
shuv1337 Jan 14, 2026
5b136fe
Sync upstream v1.1.20 (#301)
shuv1337 Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

- Avoid testing logic directly inside Solid.js `.tsx` files if they import JSX runtimes, as `bun test` may fail with `jsxDEV` errors.
- Separate pure logic into `.ts` files (e.g., `theme-utils.ts`) and test those instead.
- Run tests from `packages/opencode` directory - the repo root has a guard file that prevents test execution.
- `bun run lint` in `packages/opencode` runs the full test suite with coverage.

## Upstream Merge Operations

Expand Down
18 changes: 17 additions & 1 deletion packages/opencode/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import z from "zod"
import { Log } from "../util/log"

export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"

const log = Log.create({ service: "auth" })

export namespace Auth {
export const Oauth = z
.object({
Expand All @@ -14,6 +17,10 @@ export namespace Auth {
expires: z.number(),
accountId: z.string().optional(),
enterpriseUrl: z.string().optional(),
email: z.string().optional(),
name: z.string().optional(),
plan: z.string().optional(),
orgName: z.string().optional(),
})
.meta({ ref: "OAuth" })

Expand All @@ -39,7 +46,16 @@ export namespace Auth {

export async function get(providerID: string) {
const auth = await all()
return auth[providerID]
const entry = auth[providerID]
if (entry || providerID !== "openai") return entry
const legacy = auth.codex
if (legacy) {
log.info("auth migration: using legacy codex entry", { providerID: "openai" })
}
if (legacy?.type === "oauth") {
await set("openai", legacy)
}
return legacy
Comment on lines +50 to +58
Copy link

Choose a reason for hiding this comment

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

style: Consider adding a lock to prevent race conditions if Auth.get("openai") called multiple times rapidly - each call could trigger duplicate migrations before the first set() completes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/opencode/src/auth/index.ts
Line: 50:58

Comment:
**style:** Consider adding a lock to prevent race conditions if `Auth.get("openai")` called multiple times rapidly - each call could trigger duplicate migrations before the first `set()` completes.

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

}

export async function all(): Promise<Record<string, Info>> {
Expand Down
22 changes: 22 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/account-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createMemo } from "solid-js"
import { useSync } from "@tui/context/sync"
import { useTheme } from "../context/theme"

export function AccountBadge() {
const sync = useSync()
const { theme } = useTheme()

const authInfo = createMemo(() => sync.data.provider_auth_info.openai)

return () => {
const info = authInfo()
if (!info?.authenticated || !info.email) return null

return (
<box flexDirection="row" gap={1} alignItems="center">
<text>{info.email}</text>
{info.plan && <text fg={theme.primary}>[{info.plan.charAt(0).toUpperCase() + info.plan.slice(1)}]</text>}
</box>
)
}
}
31 changes: 21 additions & 10 deletions packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,27 @@ export function createDialogProviderOptions() {
return pipe(
sync.data.provider_next.all,
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
map((provider) => ({
title: provider.name,
value: provider.id,
description: {
opencode: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
}[provider.id],
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {
map((provider) => {
const authInfo = sync.data.provider_auth_info[provider.id]
const isOAuthConnected = authInfo?.authenticated && authInfo?.email

let description: string
if (isOAuthConnected) {
description = `Connected: ${authInfo.email}${authInfo.plan ? ` [${authInfo.plan}]` : ""}`
} else {
description = {
opencode: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
}[provider.id] || ""
}

return {
title: provider.name,
value: provider.id,
description,
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {
const methods = sync.data.provider_auth[provider.id] ?? [
{
type: "api",
Expand Down
38 changes: 36 additions & 2 deletions packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
formatter: FormatterStatus[]
vcs: VcsInfo | undefined
path: Path
provider_auth_info: Record<
string,
{
authenticated: boolean
type?: string
email?: string
plan?: string
accountId?: string
}
>
}>({
provider_next: {
all: [],
Expand Down Expand Up @@ -107,6 +117,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
formatter: [],
vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" },
provider_auth_info: {},
})

const sdk = useSDK()
Expand All @@ -121,7 +132,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const requests = store.permission[event.properties.sessionID]
if (!requests) break
// Note: upstream uses requestID, SDK may show permissionID until regenerated
const match = Binary.search(requests, (event.properties as unknown as { requestID: string }).requestID, (r) => r.id)
const match = Binary.search(
requests,
(event.properties as unknown as { requestID: string }).requestID,
(r) => r.id,
)
if (!match.found) break
setStore(
"permission",
Expand Down Expand Up @@ -369,14 +384,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
// TODO: Re-enable after SDK regeneration (Phase 15) - sdk.client.experimental.resource.list()
(sdk.client as { experimental?: { resource: { list: () => Promise<{ data?: Record<string, McpResource> }> } } }).experimental?.resource.list().then((x) => setStore("mcp_resource", reconcile(x?.data ?? {}))),
(
sdk.client as {
experimental?: { resource: { list: () => Promise<{ data?: Record<string, McpResource> }> } }
}
).experimental?.resource
.list()
.then((x) => setStore("mcp_resource", reconcile(x?.data ?? {}))),
sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))),
sdk.client.session.status().then((x) => {
setStore("session_status", reconcile(x.data!))
}),
sdk.client.provider.auth().then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))),
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))),
(
sdk.client.auth as unknown as {
info: (opts: {
path: { providerID: string }
}) => Promise<{
data: { authenticated: boolean; type?: string; email?: string; plan?: string; accountId?: string }
}>
}
)
.info({ path: { providerID: "openai" } })
.then((x) => {
setStore("provider_auth_info", "openai", x.data)
}),
]).then(() => {
setStore("status", "complete")
})
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { useDirectory } from "../../context/directory"
import { useConnected } from "../../component/dialog-model"
import { createStore } from "solid-js/store"
import { useRoute } from "../../context/route"
import { AccountBadge } from "../../component/account-badge"
import { useLocal } from "../../context/local"

export function Footer() {
const { theme } = useTheme()
const sync = useSync()
const route = useRoute()
const local = useLocal()
const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
const lsp = createMemo(() => Object.keys(sync.data.lsp))
Expand All @@ -19,6 +22,12 @@ export function Footer() {
})
const directory = useDirectory()
const connected = useConnected()
const currentModel = createMemo(() => local.model.current())
const showAccountBadge = createMemo(() => {
const model = currentModel()
const authInfo = sync.data.provider_auth_info.openai
return model?.providerID === "openai" && authInfo?.authenticated && authInfo?.email
})

const [store, setStore] = createStore({
welcome: false,
Expand Down Expand Up @@ -50,6 +59,9 @@ export function Footer() {
<box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
<text fg={theme.textMuted}>{directory()}</text>
<box gap={2} flexDirection="row" flexShrink={0}>
<Show when={showAccountBadge()}>
<AccountBadge />
</Show>
<Switch>
<Match when={store.welcome}>
<text fg={theme.text}>
Expand Down
29 changes: 23 additions & 6 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,11 @@ export namespace Config {
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
openai_multi_account: z
.boolean()
.optional()
.default(false)
.describe("Enable multi-account storage and switching for OpenAI OAuth"),
openTelemetry: z
.boolean()
.optional()
Expand All @@ -1055,12 +1060,24 @@ export namespace Config {
.optional()
.describe("Tools that should only be available to primary agents."),
continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
mcp_timeout: z
.number()
.int()
.positive()
.optional()
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
mcp_timeout: z.number().int().optional().describe("Timeout in milliseconds for MCP server initialization"),
skills: z
.object({
registries: z
.array(
z.object({
id: z.string(),
type: z.enum(["github", "clawdhub", "url"]),
url: z.string().url(),
enabled: z.boolean().optional().default(true),
globs: z.array(z.string()).optional().default(["*/SKILL.md", "skills/**/SKILL.md"]),
}),
)
.optional(),
default_scope: z.enum(["user", "project"]).optional().default("project"),
auto_update: z.boolean().optional().default(false),
})
.optional(),
})
.optional(),
})
Expand Down
Loading
Loading