dsh-codex-auth

September 10, 2026 · View on GitHub

Status: adapted in this unpublished working tree for DSH 0.1.5-rc.1; Workspace Export remains disabled pending a policy-aware binary workspace-write API.

The canonical project language lives in CONTEXT.md. This document records implementation boundaries and the accepted design.

Purpose

dsh-codex-auth is a local, single-user DeepSeek Harness capability bundle. It reuses the ChatGPT login maintained by the official Codex CLI, so Codex remains the login authority and the user does not maintain a second OAuth state.

The bundle provides three independently enabled runtime capabilities from one npm installation:

  1. Codex login-state access and the openai-codex LLM route;
  2. a Codex-backed provider for DSH's stock web_search tool;
  3. Codex-backed image creation, durable display, and reference-image discovery.

The package remains named dsh-codex-auth, and the Web settings section remains GPT Auth because login gates the other two capabilities. Search and image creation are nevertheless capability operations, not authentication operations.

Boundaries

  • Local machine, one user. Real account RPC is enabled only for an explicit 127.0.0.1 Web bind; every absent, all-interface, or unknown bind is inert.
  • The current Codex account is used; account switching is out of scope.
  • The Codex CLI owns login. The plugin neither implements another OAuth flow nor copies the login into Harness credentials.
  • The token is not a general OpenAI Platform API credential.
  • Remote/multi-user relays, account sharing, audio/video generation, and arbitrary media attachments are out of scope.
  • Reference images may come from the current session or the workspace. Direct HTTP(S) reference-image input is out of scope for the first implementation.
  • dsh-codex-auth and the separately published dsh-codex package both own the openai-codex route and cannot be installed together.

Composition

One npm package supplies one client bundle and multiple Host plugin rows:

Runtime pluginResponsibilityPrimary DSH seams
Auth/LLMCodex login status and guidance, request authentication, refresh coordination, openai-codex model routectx.llm, ctx.codexAuth, statically guarded Connection RPC
SearchCodex standalone-search transport and normalizationctx.web, ctx.settings, current Agent context
Imagegenerate_image, list_images, attachment persistence, tool presentation, workspace exportctx.tools, ctx.attachments, ctx.fs, client slots

Search and Image consume a narrow Host-only auth service. They never read the auth file independently and never receive credentials through browser state. Their enabled state and defaults are separate, live settings even though all rows ship in the same bundle.

The existing Auth/LLM implementation remains the owner of the openai-codex route. It reuses PiAiAdapter and the installed pi-ai Codex model catalog. The full catalog remains dynamic; image-tool visibility is further restricted by the selected model's declared input modalities.

Why the Harness credentials seam remains unused

The Codex login is an external, CLI-owned state rather than a Harness-managed secret. The Harness credentials service is not a multi-provider extension registry, and copying the token there would create a second mutable credential state. The adapter and capability plugins therefore resolve request auth through the plugin-owned Host service.

credentialRef remains a value-free informational name. It is not resolved through ctx.credentials, and Models settings cannot overwrite the Codex login.

Codex login-state lifecycle

The auth file is ~/.codex/auth.json, or $CODEX_HOME/auth.json. Tokens never enter Harness settings, logs, tool metadata, session events, or browser RPC.

Every authenticated operation uses one Host coordinator:

  1. A credential cached for at most ten minutes may take a one-stat fast path only while its token remains outside the refresh lead and the path still has the exact inode/size/mtime/ctime version whose bytes produced that credential. A missing or untrusted version is never cached.
  2. Cache misses acquire their Host coordinator's lifecycle-owned in-process singleflight, then briefly acquire the plugin's cross-process lock for DSH consumers. Overlapping hot-reload coordinators never borrow one another's Promise; the file lock coordinates them without coupling their lifecycles.
  3. The locked decision reads the document and file version from one open file descriptor; a pre-lock read is only a diagnostic hint.
  4. A sufficiently fresh token is returned and schedules proactive refresh. Every newly resolved snapshot replaces the previous schedule, including an external login with an earlier expiry.
  5. When refresh is required, the lock is released before calling https://auth.openai.com/oauth/token, so network latency cannot exhaust the writer lock's contention deadline.
  6. After the OAuth reply, the coordinator locks and re-reads again. A fresh newer document wins; otherwise the reply is merged only when the current account and refresh-token lineage still match the decision snapshot. The atomic write preserves unknown fields at owner-only permissions.
  7. If refresh fails, a newer fresh matching-account login may be adopted. A different account, changed refresh lineage, unreadable snapshot, or unusable state fails closed without logging token material.
  8. A lifecycle-owned, unref'd timer performs the same two-phase refresh ahead of expiry and retries transient failures without keeping the process alive.
  9. Disposal aborts request refreshes, proactive refreshes, usage probes, and the CLI version probe. Callers detach even when a transport ignores abort; abortable work has a bounded drain, while an atomic auth-file commit that already started remains joined to teardown until it settles.

Atomic replacement prevents torn files; version-bound snapshots prevent old bytes from being paired with a newer file's metadata. The official Codex process does not participate in the plugin's lock, so cross-client coordination remains recovery-oriented rather than an absolute serialization guarantee.

Login buttons continue to spawn the official codex login browser or device-code flow. Status RPC remains value-free. Account endpoints register exact /api/codex-auth/* Fetch routes through public ctx.connection.fetch.register; the browser uses rpc.call('/api', 'codex-auth/<endpoint>', ...). This avoids the DSH 0.1.5 dedicated-channel owner-context registration failure while preserving Connection authentication and Host/Origin checks. Alpha.5 removed per-channel authority and exposes no public request/carrier authority fact, so the plugin selects its dispatcher once at activation: only exact WebServer host 127.0.0.1 reaches the real account service; missing, 0.0.0.0, and unknown hosts receive a fixed loopback-required handler. Client isLoopback only controls Settings visibility and is not the Host authorization boundary.

LLM route

With its default llmEnabled: true, the package owns exactly one openai-codex adapter route. An installer can set llmEnabled: false while retaining the shared Login State coordinator for Search and Image. PiAiAdapter continues to handle ordinary conversation streaming, function tools, reasoning replay, usage, cancellation, Portable summarization, and input attachment conversion; only opt-in manual or automatic Native Checkpoint generation uses the dedicated transport described below. A read-only LLM waterfall records exact Agent-loop request identity before Runtime projection so one automatic Codex Turn Continuation can be applied without replacing ordinary streaming.

The independently live codex-llm settings namespace owns one narrow policy: longContextEnabled. Its default-off implementation wraps the generated pi-ai catalog without mutating it. When the installed catalog omits GPT-6 Astra, the wrapper prepends the official Codex descriptor cloned from GPT-5.6 Sol. Enabling the policy then replaces only the contextWindow of GPT-6 Astra and the known GPT-5.6 Luna, Sol, and Terra descriptors with 1,000,000. The adapter registration republishes its unchanged route after a settings update so model consumers refresh metadata. This changes DSH token-pressure and compaction decisions; it does not add a backend request parameter or assert account entitlement.

The route defaults to SSE and exposes sse, websocket, and auto transport selection. websocketConnectTimeoutMs bounds the WebSocket handshake, while timeoutMs bounds the SSE response-header phase or the WebSocket message-idle interval; neither setting is presented as a whole-stream deadline, and 0 explicitly disables the corresponding timeout.

Installed pi-ai 0.85.1 does not model Codex standalone search, Responses web_search, or image-generation result items. Search and image creation therefore do not modify or inject payloads into PiAiAdapter; they use dedicated capability plugins and the official Codex standalone endpoints.

Dual Checkpoint compaction

The experimental dsh-codex-auth/compaction entry replaces Basic only inside an explicitly authored custom-preset compaction realm. Its manual and automatic pressure/overflow entries open the same Host operation scope and immediately delegate to Basic; explicit-region calls remain Portable-only. The protected summarization hook completes Basic's Portable call first, while the openai-codex provider wrapper captures the effective marker-free payload, exact model/session, already resolved credential, Adapter generation, safe public header inputs, and existing timeout transport in that scope. Basic continues to own trigger policy, pruning, balanced range selection, retries, durable markers, strict shrink, surface mutation, and cancellation.

An eligible head-anchored prefix causes one direct v2 call to the same Codex Responses URL. The request is rebuilt from an explicit semantic allowlist, excludes generation-only fields, and appends a transient trigger. The dedicated SSE decoder requires terminal completion and exactly one opaque compaction output. Client-side retention keeps recent text-only user groups newest-first under a versioned 64,000-token JSON estimate, permits one Unicode-safe boundary prefix, and appends the artifact last. Replay pricing adds the retained-item estimates to max(floor(base64Length * 3 / 4) - 650, 0) model-visible opaque bytes at four bytes per token; this codec diagnostic is distinct from Basic pressure pricing. The versioned credential-free block must satisfy the 2 MiB codec limit and a conservative shrink precheck before Basic atomically lands it beside Portable text.

Native failures are intentionally swallowed only after Portable success; caller cancellation is rethrown. The direct operation performs no retry and uses a process-local account/model/endpoint/codec breaker. Three transient failures within five minutes open it for ten minutes, 429 follows a one-hour-capped Retry-After, protocol and unsupported final-payload shapes open it for one hour, and half-open admits one probe. Authentication and local oversize/shrink fallback do not count; the breaker never gates ordinary inference. Diagnostics expose usage availability rather than token values. Reported Native usage may persist inside the sensitive checkpoint but is not merged into DSH aggregate usage. Adapter-realm disposal aborts active direct transport and lets scope finally cleanup release captured credentials, payloads, markers, canonical items, and continuation state.

A successful inline automatic commit may retain one nonempty response x-codex-turn-state in process memory for at most 60 seconds. The next Agent-loop request must match session, route, model, hashed account, and Adapter generation; the first mismatch, error/abort, route replacement, or disposal erases it. Manual, explicit-region, direct-maintenance, Portable, title, and auxiliary calls never arm or consume this continuation. Ordinary Codex requests otherwise stay on pi-ai, where the request-local replay module chooses exactly one Native or Portable representation for each durable Dual Checkpoint.

The Dual block is immutable ordinary Session data across JSON restore and public forks; replay transformations occur only in detached request copies. Every compatible earlier checkpoint in a repeated selected prefix expands at its original item position, while an incompatible one contributes one Portable item without preventing a fresh Native checkpoint. Basic preserves later tail messages and owns repeated-pressure convergence or bounded failure. Compatibility is re-evaluated after composed payload callbacks against the final semantic request, while routing IDs, cache keys, transient headers, turn state, and Long Context remain excluded. Pi-ai 0.85.1 can encode deferred GPT-5.6 tools as an additional_tools input item; since that semantic history is not represented in the codec digest, its presence conservatively disables Native creation and replay for that request. Shipped PiAi and direct DeepSeek routes therefore send Portable text without destroying Native state, stock-Basic rollback needs no migration, and a later compatible Codex route may replay the same candidate. Adapter generation/HMR invalidates stale request-local eligibility. An empty text presentation sentinel prevents alpha.5 Chat and Trajectory copy projections from serializing opaque state, although the credential-free block remains sensitive Session/RPC/export data. See ADR 0007.

Delivery and deletion boundary

The npm artifact includes this design, the compaction ADRs, the public Host exports, the Native codec export, and the complete custom-preset example. Package smoke extracts the real tarball outside the checkout, exposes only declared runtime dependencies, and verifies the required and forbidden file/export surface. A separate Vitest configuration covers real v2 creation, same-process turn continuation, restart/resume, repeated compaction, and redacted diagnostics; its launcher refuses CI and requires both an existing Codex Login State and a double explicit quota confirmation, so normal test and check never execute it.

This implementation is a temporary plugin-owned bridge over alpha.5 public seams. Once DSH ships a supported provider-native checkpoint Seam, migrate the codec and replay contract through that Seam, retain Portable fallback compatibility, then delete the declaration-merged carrier, request marker side channel, direct native transport, compatibility pin, and custom Basic replacement rather than maintaining two owners of compaction policy.

Harness integration

Search registers a WebSearchProvider through ctx.web.registerSearchProvider(...). It does not register another model-visible web_search tool. DSH's stock tool remains responsible for its schema, timeout lifecycle, session events, model-facing rendering, citations, and Web result card.

DSH search-provider selection is deployment-global rather than per Agent. The bundle selects the Codex provider globally, so any model using the stock web_search tool may consume the Codex-backed provider.

Request behavior

The provider posts to the fixed first-party Codex endpoint:

https://chatgpt.com/backend-api/codex/alpha/search

For an openai-codex caller, the auxiliary request uses the current Agent's Codex model. For another provider, or when no initiating Agent can be resolved, it uses the configured fallback Codex model.

Default search mode is live; cached and indexed remain selectable. Settings also expose context size, fallback model, and maximum output tokens.

Result behavior

The endpoint guarantees generated output and treats result records as forward-compatible opaque JSON. The provider returns:

  • content from the generated output;
  • deduplicated HTTP(S) source URLs;
  • title or snippet only when a recognized response field contains a trustworthy string;
  • no fabricated dates, titles, snippets, or follow-up page fetches.

DSH applies its requested maximum-result cap. Search network errors and 5xx responses use cancellable exponential backoff for at most five attempts. HTTP 429 is returned immediately rather than retried automatically.

Image Creation

Model-facing tools

The image plugin owns two stable tools:

  • generate_image — create a new image or edit reference images;
  • list_images — page through durable session images when older image context is no longer active.

Both are visible only to Agents using an openai-codex model that declares image input. Calls are not constrained to literal user wording: a user prompt, model judgment, or installed Skill may invoke them.

Backend dispatch

generate_image presents one domain operation while dispatching to two fixed Codex endpoints:

POST https://chatgpt.com/backend-api/codex/images/generations  # no references
POST https://chatgpt.com/backend-api/codex/images/edits        # one or more references

The image model defaults to gpt-image-2. The tool accepts:

  • required prompt;
  • up to five reference descriptors;
  • n from 1 through 10, default 1;
  • size: auto, 1024x1024, 1536x1024, or 1024x1536;
  • quality: auto, low, medium, or high;
  • background: auto, opaque, or transparent.

Tool arguments may override defaults from the Image Creation settings card.

A reference is explicitly discriminated:

{ "kind": "session", "handle": "image:<attachmentId>" }
{ "kind": "workspace", "path": "assets/reference.png" }

Workspace reads go through ctx.fs, respect the active workspace and filesystem policy, and are promoted into the attachment store before the remote request. Session handles resolve only when the referenced attachment belongs to the current session.

Image catalog

Image Handles are stable, model-visible aliases for session-authorized attachment references; users do not manage them directly. Generated tool output places handles next to the corresponding images.

list_images returns newest first, defaults to five images, caps one page at ten, and supports a cursor and origin filter. Each item includes its handle, name when available, dimensions, origin, creation sequence, and actual ImageBlock. Returning image content lets the model visually select a reference after compaction rather than guessing from filenames.

ACP interoperability

DSH alpha.5 owns ACP wire admission and projection (the path was introduced in rc7). For an ACP connection whose configured route resolves to an image-capable openai-codex model, core validates supported inline raster images, persists the complete prompt batch through the attachment store, and records ordinary durable ImageBlocks in user/message. The Image Catalog already scans that event type, so ACP-originated images appear as user assets without a plugin-specific protocol adapter and may later be selected by Image Handle.

ACP projects only committed assistant/message text and image blocks back to the client. This plugin deliberately returns generated and catalog images inside tool/result; those blocks remain durable and model-visible but are not direct ACP output. Sending generated bytes to ACP would require a separate core projection decision rather than duplicating protocol transport inside this plugin.

Availability

A clearly identified Free plan marks Image Creation unavailable. An unknown plan remains attemptable; the backend result is authoritative. The plugin never generates a test image merely to probe entitlement.

Validation and partial success

The image endpoint returns base64 image data. The plugin bounds the encoded and decoded response, verifies each image signature and deployment media policy, and stores valid images through ctx.attachments.saveImage(...).

When a multi-image response contains at least one valid image, valid images are retained and returned with structured warnings for missing or invalid items. The whole call fails only when no valid image remains or the envelope itself is unusable.

Image requests are not automatically retried after dispatch because the server may already have consumed quota and produced an output. Cancellation aborts the client request but has no server-side cancellation protocol.

Durable display and workspace export

The tool's canonical JSON value carries attachment-reference fields. Its pure output renderer reconstructs standard ImageBlocks, allowing image-capable models to receive the result and the session log to retain authorized attachment references.

The generic DSH tool row currently renders non-text blocks as JSON, so the client plugin registers keyed tool.call.toolview ownership for both image tools. A successful generate_image call renders only the stock image gallery: no result card, title, persistence badge, Image Handle, warning copy, or workspace controls. Loading and failure states retain only the feedback needed to explain their state. list_images is model-facing catalog state and deliberately renders no user-facing view.

The image loader reads attachments through the public session-authorized API, creates bounded plugin-owned Blob URLs, and revokes them on connection reset, cache eviction, and plugin teardown.

Current DSH alpha.5 constraint: ctx.fs has no binary write operation. Workspace export is therefore not offered in the result UI. Conversation attachments remain the durable copy, and the plugin does not evade DSH policy with direct node:fs writes. Adding export later requires a policy-aware binary write API in DSH core.

Settings UI

The independently navigable section keeps the name GPT Auth and the stock icon fallback. It contains four cards:

  1. Login — connection state, locally decoded plan, weekly remaining balance/reset time, and browser/device login actions when disconnected;
  2. LLM Context — the live, default-off GPT-6 Astra / GPT-5.6 1M context policy;
  3. Web Search — enabled state, mode, context size, fallback model, and output budget;
  4. Image Creation — enabled state, plan eligibility, model-scope note, and default count/size/quality/background.

Search and Image register live settings under the plugin's DSH settings namespace. Both default enabled after installation. Disabling one immediately removes its model capability for current and future Agents without restarting. Logged-out cards are unavailable rather than probing capability endpoints.

DSH 0.1.5-rc.1 is the minimum and tested settings baseline. The Host installs each namespace through ctx.settings.installSection(), while the browser face owns one top-level settings.section and binds codex-llm, codex-search, and codex-image through ctx.settingsScope. The client registers that privileged section only when ConnectionHandle.isLoopback is true; image result views remain available independently. Historically, rc7 was the first Host baseline that exposed plugin namespaces to Web settings, but it is no longer in this package's supported dependency family.

The Login card combines locally verified connection state with a best-effort, value-free account-usage view. When a usable login exists, the Host requests the fixed /backend-api/wham/usage endpoint with a ten-second Host deadline, identifies the seven-day window by limit_window_seconds, and sends only plan/balance/reset facts to the browser. Cancellation, auth failure, malformed data, a missing weekly window, or upstream failure degrades to unknown values. The UI performs no test search or test generation. Compatibility and private-endpoint disclosures live in repository documentation, not in the settings cards.

Security and privacy

  • Browser RPC and settings never carry bearer, refresh, or ID token values.
  • Account RPC is real only on an explicit 127.0.0.1 Web bind. Other and missing binds receive an inert, value-free denial that never invokes the auth service. An all-interface deployment reached through localhost can therefore show the client Settings row while Host actions remain denied.
  • Remote capability endpoints are fixed under https://chatgpt.com/backend-api/codex, and the read-only account-usage probe is fixed to https://chatgpt.com/backend-api/wham/usage; configuration cannot redirect credentials to another origin.
  • Requests use a plugin-owned originator rather than impersonating the official CLI.
  • Account identity is resolved from the latest auth document with token-claim fallback; no identity value is accepted from the model.
  • Search queries, image prompts, selected handles, and non-secret options are ordinary durable tool inputs. Raw base64 image responses are not logged; durable bytes live in the attachment store.
  • Workspace reads/writes and attachment limits remain deployment policy, not plugin bypasses.
  • Tokens and backend error bodies are redacted from diagnostics.

The Codex backend is not a public, versioned third-party API contract. The project makes no categorical legal conclusion that this use violates or complies with OpenAI terms. It documents the private, revocable, account-gated nature of the integration and remains intended for personal local use.

Protocol baseline and compatibility

The standalone request contracts were derived from official Codex rust-v0.147.0, commit be6e8eac029b183056b7e4402879f15d2c85f61b. That source revision is a maintainer trace and contract-fixture baseline, not a runtime Codex CLI version requirement.

Runtime parsers:

  • validate required envelope fields strictly;
  • ignore unknown forward-compatible fields;
  • preserve bounded, redacted provider diagnostics;
  • do not disable features merely because the installed CLI version changes.

The CLI remains required for login guidance and owns the login state; the plugin cooperatively refreshes that same state. Direct Search and Image requests do not invoke a CLI subprocess.

Failure modes

ConditionBehavior
No usable Codex loginLLM and capability operations fail with auth-required guidance
Account RPC bind is absent, all-interface, or unknownReturn fixed loopback-required without invoking auth/status operations
Refresh token reused while another process updated the fileRe-read and adopt the newer same-account login
Refresh fails with no recoverable stateFail closed; guide the user to codex login
Search provider disabledRemove/disable the search capability live
Search returns output without rich source metadataPreserve output and valid URLs only
Image model lacks image inputDo not expose image tools to that Agent
Free plan is knownMark Image Creation unavailable
Plan is unknownAllow a real user/model-initiated call; surface backend rejection
Some generated images are invalidReturn valid images plus structured warnings
Every generated image is invalidFail the tool call without fabricating output
Workspace reference is unreadable or disallowedFail before making the remote request
dsh-codex also owns the routeFail clearly with mutually exclusive installation guidance

Decisions