Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
90 changes: 0 additions & 90 deletions .github/workflows/dependency-review.yml

This file was deleted.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The commit hook accepts staged-only commit-ready changes, while the push hook re

`npm run verify:local` is the recommended manual gate. It runs `npm run verify`, but skips reruns when the current tree already passed locally.

Pull request GitHub CI keeps only hosted-value checks: clean-room verify, Linux tarball smoke, Windows smoke, dependency review, and secret scanning. `npm audit` still runs in GitHub, but only on default-branch pushes rather than every PR.
Pull request GitHub CI keeps only hosted-value checks: clean-room verify, Linux tarball smoke, Windows smoke, and secret scanning. `npm audit` still runs in GitHub, but only on default-branch pushes rather than every PR.

`npm run verify` is the baseline full gate and runs:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ npm run check:docs

Local git hooks now enforce `npm run verify` before both `git commit` and `git push`. The commit hook accepts staged-only commit-ready changes, and the push hook requires a clean tree so it verifies the exact commits being pushed. `npm run verify:local` runs the same enforcement manually, with a cache so unchanged trees do not rerun the full suite twice in a row.

Pull request CI stays intentionally lean: GitHub still runs clean-room verify, tarball smoke, Windows smoke, dependency review, and secret scanning. Dependency vulnerability auditing via `npm audit` now runs on default-branch pushes instead of every PR.
Pull request CI stays intentionally lean: GitHub still runs clean-room verify, tarball smoke, Windows smoke, and secret scanning. Dependency vulnerability auditing via `npm audit` runs on default-branch pushes instead of every PR.

## Usage Note

Expand Down
2 changes: 1 addition & 1 deletion docs/development/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm run verify

`npm run verify` is the required local gate before commits, pushes, and PR updates. `npm run verify:local` runs that gate with caching, and the installed git hooks enforce it automatically before `git commit` and `git push`. The commit hook accepts staged-only commit-ready changes; the push hook requires a clean tree and derives the touched-file set from the outgoing commits so the local regression-only ratchet matches the actual push surface instead of only `HEAD^`. GitHub Actions still adds extra platform and security jobs beyond the repo-local verify run.

PR GitHub CI is intentionally slimmer than local `verify`: it keeps the clean-room Ubuntu verify job, Linux tarball smoke, Windows smoke, dependency review, and secret scanning. The dependency-review job now fails closed if GitHub Dependency Graph is unavailable so PRs do not silently lose that protection. The separate `npm audit` dependency audit remains GitHub-hosted, but it now runs on default-branch pushes instead of every PR.
PR GitHub CI is intentionally slimmer than local `verify`: it keeps the clean-room Ubuntu verify job, Linux tarball smoke, Windows smoke, and secret scanning. The separate `npm audit` dependency audit remains GitHub-hosted, but it now runs on default-branch pushes instead of every PR.

It now includes strict Biome linting + format checks (including typed promise-safety rules), anti-mock policy checks, a regression-only coverage ratchet, docs drift checks, Node ESM regression checks (source + dist import specifiers), and a built CLI smoke run.

Expand Down
17 changes: 10 additions & 7 deletions docs/development/UPSTREAM_SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ Track the OpenCode and Codex releases this plugin is aligned to, and how to keep

## Current baseline

- OpenCode release: `v1.2.18`
- OpenCode release: `v1.3.0`
- Upstream repo: `https://github.com/anomalyco/opencode`
- Baseline tag commit: tracked in `docs/development/upstream-watch.json`
- Upstream HEAD inspected: GitHub latest release/tag via `npm run check:upstream`
- Native Codex reference file: `packages/opencode/src/plugin/codex.ts`
- Codex upstream repo: `https://github.com/openai/codex`
- Codex upstream release track: `rust-v0.111.0`
- Codex upstream release track: `rust-v0.116.0`
- Local dependency target:
- `@opencode-ai/plugin`: `^1.2.18`
- `@opencode-ai/sdk`: `^1.2.18`
- `@opencode-ai/plugin`: `^1.3.0`
- `@opencode-ai/sdk`: `^1.3.0`

## Latest parity audit (2026-03-05)
## Latest parity audit (2026-03-23)

- Verified OAuth constants and authorize URL semantics against upstream `codex.ts`.
- Verified native callback URI now uses `http://localhost:1455/auth/callback`.
- Verified native headless device-auth requests use `User-Agent: opencode/<version>`.
- Verified request routing parity for `/v1/responses` and `/chat/completions` to Codex responses endpoint.
- Verified live Codex model payload now includes `gpt-5.4`, `gpt-5.3-codex-spark`, `display_name`, `priority`, and `supports_parallel_tool_calls`.
- Verified GPT-5.4 fast mode is represented by request-body `service_tier: "priority"`.
- Verified live Codex model payload now includes `default_reasoning_summary` and newer GPT-5.4-era catalog metadata in addition to `display_name`, `priority`, and `supports_parallel_tool_calls`.
- Verified default prompt caching remains upstream-owned: OpenCode sets `promptCacheKey` from `sessionID`, and Codex sends `prompt_cache_key` from the active conversation ID.
- Verified GPT-5.4 fast mode remains request-body `service_tier: "priority"`; no new HTTP priority header is used on the normal request path.
- Verified normal HTTP request parity for both `native` and `codex` modes continues to use `session_id` and excludes websocket-only `OpenAI-Beta` headers.
- Verified the plugin now uses Codex `default_reasoning_summary` instead of treating `reasoning_summary_format` as the default summary value.
- Parity tests live in `test/codex-native-oauth-parity.test.ts`.

## Sync checklist
Expand Down
32 changes: 16 additions & 16 deletions docs/development/upstream-watch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,42 @@
{
"id": "opencode",
"repo": "anomalyco/opencode",
"baselineTag": "v1.2.18",
"updatedAt": "2026-03-05T19:28:23.006Z",
"baselineTag": "v1.3.0",
"updatedAt": "2026-03-23T16:25:00.338Z",
"files": [
{
"path": "packages/opencode/src/plugin/codex.ts",
"sha256": "b8b9d2ee27b4eafd0971d3a9c213b4c9a6ded050d7e3a5ec2aa94872de7f2fa5",
"sha256": "2f7ddda4b6d8619c883ac227832614f0c2a4dac2cabfe31c737fcd006666752e",
"localArea": "lib/codex-native/",
"reason": "Codex OAuth/auth headers/request routing parity"
},
{
"path": "packages/opencode/src/plugin/index.ts",
"sha256": "826c32adf6e9f570758bda353cc969066f81980092e3e2c38ec8c9681b4a2212",
"sha256": "c3aa395685bceeae29b93b1eb740e762f4449a6834fcc2e6863dd9d1a53a0abe",
"localArea": "index.ts",
"reason": "Built-in plugin boot order affects auth integration behavior"
},
{
"path": "packages/opencode/src/provider/provider.ts",
"sha256": "b0e044c68bd64494fe8b1da577e048154a63d7998ee07c685db8da42c8028544",
"sha256": "53bd3146d0ad5c06f5ae62d9e4c088385c221ab31e8f6a6bba15144dfe5b737c",
"localArea": "lib/model-catalog.ts",
"reason": "OpenAI model surface and provider model wiring parity"
},
{
"path": "packages/opencode/src/provider/auth.ts",
"sha256": "8667a7fbc2ed4a8f7a1a98630b541118a4dd67578db0824b81f947b8f7ba42c5",
"sha256": "ec8be328fcfe43b9129949ceaf93acc6e30f4daaf9c3e280c5ac64b4419e08c4",
"localArea": "lib/storage.ts",
"reason": "OpenAI auth structure/parsing parity signals"
},
{
"path": "packages/opencode/src/provider/transform.ts",
"sha256": "1f3fb8cd7761dd11fd35b48a90e72e7c3d16df22d84df45dba3734518c53db59",
"sha256": "398f78dde2876aa4ba2e23a35a35d6d6cbff663db033c4eb69715c363d683134",
"localArea": "lib/codex-native/request-transform*.ts",
"reason": "Request transform behavior changes affecting auth/model requests"
},
{
"path": "packages/opencode/src/provider/error.ts",
"sha256": "b7977ca752896f7096b546ee3e0c65a4b06ea646d7d417738d36f2eb71bb654e",
"sha256": "23be550ec590ac18b10bf2b6c00abdf68badc2c5e207926da59869aaf4b1c65c",
"localArea": "lib/codex-native/openai-loader-fetch.ts",
"reason": "OpenAI/Codex error semantics and retry-stop signals"
},
Expand All @@ -50,7 +50,7 @@
},
{
"path": "packages/opencode/src/session/message-v2.ts",
"sha256": "f796a51c9dbaee650295bea267c662197ad413ac6f5f678621c5b5c3eca43d22",
"sha256": "1d3c1d014c398cec06fb95e81fa5d2961370e548afa1784886d13fe6e94ee332",
"localArea": "lib/fetch-orchestrator.ts",
"reason": "Streaming error handling and retry behavior parity"
}
Expand All @@ -59,36 +59,36 @@
{
"id": "codex-rs",
"repo": "openai/codex",
"baselineTag": "rust-v0.111.0",
"updatedAt": "2026-03-05T19:28:23.656Z",
"baselineTag": "rust-v0.116.0",
"updatedAt": "2026-03-23T16:25:00.407Z",
"files": [
{
"path": "codex-rs/core/models.json",
"sha256": "9ce0f440d5aecd193212cae9244c03faaf8a6de4e71dac0a5186a1439647c788",
"sha256": "5e6bb2e7e1628967407be673f70ffefe4744711bc856cb12d4fba041203ae22f",
"localArea": "lib/model-catalog.ts",
"reason": "Model catalog defaults and capabilities parity"
},
{
"path": "codex-rs/core/src/auth.rs",
"sha256": "ae9fc33ffe109798f6a4933371f32b789224d53c40f62ce20e3ab776ca8add0e",
"sha256": "4778717d5c53fc65db59e88ae45ba40d1d64478cd3e4fd5bfa2781028916bcd0",
"localArea": "lib/codex-native/oauth-*.ts",
"reason": "OAuth flow constants and auth semantics parity"
},
{
"path": "codex-rs/core/src/client.rs",
"sha256": "32e14e364a6607ff1e057cae16b59fd8f6d48cb3a5ed4932e0527e5932285298",
"sha256": "928da12079d3c0aa3200212ea33be6f950230bab38326eb15f7cc290499d03d1",
"localArea": "lib/codex-native/chat-hooks.ts",
"reason": "Request header/user-agent and endpoint behavior parity"
},
{
"path": "codex-rs/core/src/codex.rs",
"sha256": "da8084e81e46be18e3895fdc5e9470412ddc0b464c837c4ebcbef688101b1ca2",
"sha256": "00b1bf975735d776fbd5b684e8271108cd78df38130add972ae7d50f3901ca62",
"localArea": "lib/codex-native/openai-loader-fetch.ts",
"reason": "Core Codex request/runtime behavior parity"
},
{
"path": "codex-rs/core/src/compact.rs",
"sha256": "6d2cc48d6e0bda2a1d552741aee365d91a8e8a321d0990d279076de03335d3e8",
"sha256": "bb4d0787c7a841b4c99e0deed9256dd1f92e7de5db0b383e8fa14b4a91c92dc1",
"localArea": "lib/codex-native.ts",
"reason": "Compaction prompt and handoff semantics parity"
}
Expand Down
19 changes: 14 additions & 5 deletions lib/codex-native/acquire-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ function isOAuthTokenRefreshError(value: unknown): value is OAuthTokenRefreshErr
const TERMINAL_REFRESH_ERROR_CODES = new Set([
"invalid_grant",
"invalid_refresh_token",
"refresh_token_expired",
"refresh_token_reused",
"refresh_token_invalidated",
"refresh_token_revoked",
"token_revoked"
])
Expand All @@ -33,12 +36,18 @@ function isTerminalRefreshCredentialError(error: unknown): boolean {
}

if (error instanceof Error) {
const message = error.message.trim().toLowerCase()
const oauthMessage =
"oauthMessage" in error && typeof error.oauthMessage === "string" ? error.oauthMessage.trim().toLowerCase() : ""
const message = `${error.message.trim().toLowerCase()} ${oauthMessage}`.trim()
if (message.includes("invalid_grant")) return true
if (message.includes("refresh token") && (message.includes("invalid") || message.includes("expired"))) {
return true
}
if (message.includes("refresh token") && message.includes("revoked")) {
if (
message.includes("refresh token") &&
(message.includes("invalid") ||
message.includes("expired") ||
message.includes("reused") ||
message.includes("revoked") ||
message.includes("invalidated"))
) {
return true
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/codex-native/client-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { CodexOriginator } from "./originator.js"
import { URL } from "node:url"

const DEFAULT_PLUGIN_VERSION = "0.1.0"
const DEFAULT_CODEX_CLIENT_VERSION = "0.111.0"
const DEFAULT_CODEX_CLIENT_VERSION = "0.116.0"
const CODEX_CLIENT_VERSION_CACHE_FILE = path.join(defaultOpencodeCachePath(), "codex-client-version.json")
const CODEX_CLIENT_VERSION_TTL_MS = 60 * 60 * 1000
const CODEX_GITHUB_RELEASES_API = "https://api.github.com/repos/openai/codex/releases/latest"
Expand Down
50 changes: 37 additions & 13 deletions lib/codex-native/oauth-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,39 @@ export type TokenResponse = {
export type OAuthTokenRefreshError = Error & {
status?: number
oauthCode?: string
oauthMessage?: string
}

function parseOAuthErrorPayload(raw: string): { code?: string; message?: string } {
if (!raw) return {}
try {
const payload = JSON.parse(raw) as Record<string, unknown>
const topLevelError = payload.error
if (typeof topLevelError === "string") {
return {
code: topLevelError,
message: typeof payload.error_description === "string" ? payload.error_description : undefined
}
}

if (topLevelError && typeof topLevelError === "object" && !Array.isArray(topLevelError)) {
const nested = topLevelError as Record<string, unknown>
return {
code: typeof nested.code === "string" ? nested.code : undefined,
message:
typeof nested.message === "string"
? nested.message
: typeof payload.error_description === "string"
? payload.error_description
: undefined
}
}
} catch (error) {
if (!(error instanceof SyntaxError)) {
// Best effort parse only.
}
}
return {}
}

export async function fetchWithTimeout(
Expand Down Expand Up @@ -199,24 +232,15 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
)

if (!response.ok) {
let oauthCode: string | undefined
try {
const raw = await response.text()
if (raw) {
const payload = JSON.parse(raw) as Record<string, unknown>
if (typeof payload.error === "string") oauthCode = payload.error
}
} catch (error) {
if (!(error instanceof SyntaxError)) {
// Best effort parse only.
}
// Best effort parse only.
}
const raw = await response.text()
const parsedError = parseOAuthErrorPayload(raw)
const oauthCode = parsedError.code

const detail = oauthCode ? `${oauthCode}` : `status ${response.status}`
const error = new Error(`Token refresh failed (${detail})`) as OAuthTokenRefreshError
error.status = response.status
error.oauthCode = oauthCode
error.oauthMessage = parsedError.message
throw error
}
return (await response.json()) as TokenResponse
Expand Down
2 changes: 1 addition & 1 deletion lib/codex-native/openai-loader-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) {
const internalSelectedModelHeader = input.internalSelectedModelHeader ?? "x-opencode-selected-model-slug"
const internalCollaborationAgentHeader =
input.internalCollaborationAgentHeader ?? "x-opencode-collaboration-agent-kind"
const trustedSubagentValues = new Set(["review", "compact", "collab_spawn"])
const trustedSubagentValues = new Set(["review", "compact", "memory_consolidation", "collab_spawn"])
const quotaTrackerByIdentity = new Map<string, QuotaThresholdTrackerState>()
const quotaRefreshAtByIdentity = new Map<string, number>()
const catalogSyncByScope = new Map<string, CatalogSyncState>()
Expand Down
4 changes: 2 additions & 2 deletions lib/codex-native/reasoning-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function resolveReasoningSummaryValue(input: {
configuredValue?: unknown
configuredSource?: string
supportsReasoningSummaries?: boolean
defaultReasoningSummaryFormat?: string
defaultReasoningSummary?: string
defaultReasoningSummarySource: string
model?: string
}): { value?: ReasoningSummaryValue; diagnostic?: ReasoningSummaryValidationDiagnostic } {
Expand Down Expand Up @@ -88,7 +88,7 @@ export function resolveReasoningSummaryValue(input: {
return {}
}

const defaultValue = inspectReasoningSummaryValue(input.defaultReasoningSummaryFormat)
const defaultValue = inspectReasoningSummaryValue(input.defaultReasoningSummary)
if (defaultValue.state === "invalid" && defaultValue.raw) {
return {
diagnostic: {
Expand Down
Loading
Loading