Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
36b9c1e
feat(vertex): accept pasted GCP credential JSON in the API-key slot (…
GSL-R Oct 9, 2026
369f7f2
refactor(vertex): pass the marker account per request; review fixes (…
GSL-R Oct 9, 2026
12a0adc
fix(vertex): preserve the marker through routing; harden paste handli…
GSL-R Oct 9, 2026
857ed8c
docs: attest review readiness checklist (4/4 ticked)
GSL-R Oct 10, 2026
8c025fd
docs: attest readiness 4/4; trigger gate re-evaluation
GSL-R Oct 10, 2026
0f0ef85
fix(gcp-adc): suppress the GOOGLE_CLOUD_API_KEY fallback for named ma…
GSL-R Oct 10, 2026
f87521b
test(vertex): cover trailing credential paste validation
GSL-R Oct 10, 2026
dd99887
Merge remote-tracking branch 'refs/remotes/prana-upstream/dev' into f…
GSL-R Oct 10, 2026
c3defe8
fix(gcp-adc): validate markers and clean up credential lifecycle
GSL-R Oct 10, 2026
9dd9131
Merge branch 'dev' of https://github.com/lidge-jun/opencodex into fea…
GSL-R Oct 11, 2026
2905bf5
fix(gcp-adc): clean credentials after provider batch and overwrite co…
GSL-R Oct 11, 2026
fca8053
Merge remote-tracking branch 'upstream/dev' into feat/vertex-sa-json-…
GSL-R Oct 11, 2026
950260f
fix(management): include durable provider markers in DELETE cleanup
GSL-R Oct 11, 2026
da9d2fc
fix(gcp-adc): validate required fields in parseGcpCredentialJson befo…
GSL-R Oct 11, 2026
f919a6a
style(management): keep GCP cleanup within file-size gate
GSL-R Oct 11, 2026
df1a6b0
merge dev: preserve Vertex and live topology config contracts
GSL-R Oct 11, 2026
1f43977
fix(vertex): reject credential JSON before generic provider persistence
lidge-jun Oct 11, 2026
93825e8
fix(vertex): preserve GCP keychain payloads across key migrations
lidge-jun Oct 11, 2026
8d36e31
fix(vertex): serialize credential writes with marker commits and cleanup
lidge-jun Oct 11, 2026
9941f54
merge dev: preserve both test layout inventories
GSL-R Oct 12, 2026
686b6ba
fix(cli): clean removed GCP credentials after provider overwrite
GSL-R Oct 12, 2026
c81a6f7
fix(cli): collect durable GCP markers under overwrite lock
GSL-R Oct 12, 2026
dc6e85b
fix(cli): validate raw keys before registry auth ignores them
GSL-R Oct 12, 2026
21b9aa3
merge dev: refresh GCP credential validation and reuse durable marker…
GSL-R Oct 13, 2026
200e2d9
fix(gcp-adc): isolate blank markers during credential cleanup
GSL-R Oct 13, 2026
dc8c113
merge dev: preserve both test layout inventories
GSL-R Oct 13, 2026
59c4e2c
merge dev: preserve sharded test inventory and guarded key recovery
GSL-R Oct 13, 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
19 changes: 19 additions & 0 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,25 @@ model key.

## Storing keys in the OS keychain

For Vertex, the dedicated API-key pool input (`POST /api/providers/keys`) accepts complete GCP
`service_account` or `authorized_user` credential JSON after the provider has been created.
The provider create/edit form and `ocx provider add --api-key` refuse credential JSON before
saving: create the provider without a key, then add the JSON through its key pool. Paste the
JSON contents, not a Windows, Unix, or relative file path. Multiple
JSON objects separated by commas or whitespace are stored together as separate pool entries;
the first becomes active. Configuration contains only `gcp-sa:<account>` references, never the
credential JSON. Mixed JSON/literal-key bulk pastes are rejected as a whole; add literal keys
separately. Storage failures leave the configuration unchanged and report a fixed error without
echoing credential material. Empty marker accounts are invalid and do not fall back to host credentials.
Removing a key or provider, or replacing a provider with `ocx provider add --force`, deletes its unreferenced GCP credentials after the configuration is
saved; a credential referenced by another provider is retained. If OS-store cleanup fails, a
warning requests manual inspection without undoing the committed configuration removal.
Ordinary keychain store/restore actions leave GCP markers and JSON in the OS store. Restoring
credential JSON through a `keychain:` alias is refused, and cleanup preserves accounts still
referenced through either marker spelling.
Credential writes and marker persistence share the cleanup transaction lock, so concurrent
removal cannot delete the stored JSON between read-back and the marker commit.

By default a provider's `apiKey` and `apiKeyPool` sit in `config.json` (mode 0600, atomic writes).
If you would rather keep the key material out of the file, move it into the OS credential store:

Expand Down
4 changes: 2 additions & 2 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"version": 1,
"root": "tests",
"explicitShards": ["layout-clients.json"],
"explicit": { "service-log.test.ts": "service",
"codex-history-pending-wal.test.ts": "codex-integration", "responses-anthropic-billing-send-budget.test.ts": "responses", "anthropic-billing-ownership.test.ts": "adapters/anthropic", "server-anthropic-billing.test.ts": "server", "server-anthropic-billing-boundaries.test.ts": "server", "router-anthropic-billing-failover.test.ts": "routing", "anthropic-billing-refusal.test.ts": "adapters/anthropic", "messages-request-id-headers.test.ts": "claude-integration", "bounded-file-read.test.ts": "lib", "responses-undeclared-tool-guard-inbound.test.ts": "responses",
"explicit": { "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "service-log.test.ts": "service",
"codex-history-pending-wal.test.ts": "codex-integration", "responses-anthropic-billing-send-budget.test.ts": "responses", "anthropic-billing-ownership.test.ts": "adapters/anthropic", "server-anthropic-billing.test.ts": "server", "server-anthropic-billing-boundaries.test.ts": "server", "router-anthropic-billing-failover.test.ts": "routing", "anthropic-billing-refusal.test.ts": "adapters/anthropic", "messages-request-id-headers.test.ts": "claude-integration", "bounded-file-read.test.ts": "lib", "responses-undeclared-tool-guard-inbound.test.ts": "responses",
"messages-request-id-endpoint.test.ts": "claude-integration",
"anthropic-revoked-token.test.ts": "adapters/anthropic",
"messages-revoked-token.test.ts": "claude-integration",
Expand Down
18 changes: 15 additions & 3 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
import type { OcxTool } from "../types";
import { contentPartsToText, parseDataUrl } from "./image";
import { getVertexAccessToken } from "../lib/gcp-adc";
import { GCP_CREDENTIAL_MARKER_PREFIX, gcpCredentialMarkerAccount, getVertexAccessToken } from "../lib/gcp-adc";
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
import { isVertexTruncatedTurn, vertexTruncationErrorEvent } from "./google-truncation";
Expand Down Expand Up @@ -174,7 +174,15 @@ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean):

/** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
function resolveVertexApiKey(optKey?: string): string | undefined {
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
// A `gcp-sa:` marker is the keychain-backed credential reference (see gcp-adc): it is not
// API-key material, so it must not be sent as x-goog-api-key — the ADC branch below handles it.
// AND the marker means the operator named one credential explicitly: the GOOGLE_CLOUD_API_KEY
// env fallback must NOT fire (it would silently route this provider's inference through the
// ambient env key instead of the configured service account).
if (optKey && optKey.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A"
? optKey
: undefined;
return realKey || process.env.GOOGLE_CLOUD_API_KEY;
}

Expand Down Expand Up @@ -1197,7 +1205,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
if (locationError) throw new Error(locationError);
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
const token = await getVertexAccessToken();
// The marker account travels per-request as an argument (never process-global state): a
// module-level "active marker" written at routing time would race across concurrent
// requests for different providers and could resolve one call with another's credential.
const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
const token = await getVertexAccessToken({ markerAccount });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
headers["Authorization"] = `Bearer ${token}`;
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
}
Expand Down
25 changes: 23 additions & 2 deletions src/cli/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
* show <name> Show provider config details (secrets masked)
* set-default <name> Change the default provider
*/
import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate, withConfigMutationLockSync } from "../config";
import { hasOwnProvider, isValidProviderName, loadConfig, readConfigAdmissionSnapshot, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate, withConfigMutationLockSync } from "../config";
import { apiKeyTransportConfigError, modelCapabilitiesConfigError, mergeModelCapabilities } from "../config/provider-validation";
import { hasHelpFlag, printSubcommandUsage } from "./help";
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
import { providerConfigSeed } from "../providers/derive";
import { assertAnthropicInstanceLoginConfig } from "../oauth/store-anthropic-instance";
import { dropProviderCustomModels } from "../providers/provider-id-rewrite";
import { cleanupRemovedGcpCredentials, providerGcpCredentialMarkers } from "../providers/gcp-credential-cleanup";
import { providerCredentialInputError } from "../providers/api-keys";
import type { OcxProviderConfig } from "../types";
import { findLiveProxy } from "../server/proxy-liveness";
import { syncModelsToCodex } from "../codex/sync";
Expand Down Expand Up @@ -223,6 +225,12 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
if (registryEntry) {
provConfig = providerConfigSeed(registryEntry);
if (apiKey) {
const candidateError = providerCredentialInputError({ apiKey });
if (candidateError) {
console.error(`Error: ${candidateError}`);
process.exitCode = 2;
return;
}
if ((authMode ?? registryEntry.authKind) === "forward") {
console.warn(`Warning: provider "${name}" uses ChatGPT login (forward auth); --api-key is ignored.`);
} else if ((authMode ?? registryEntry.authKind) === "oauth") {
Expand All @@ -249,6 +257,12 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
}

if (responsesPath !== undefined) provConfig.responsesPath = responsesPath;
const credentialError = providerCredentialInputError(provConfig);
if (credentialError) {
console.error(`Error: ${credentialError}`);
process.exitCode = 2;
return;
}
if (authMode !== undefined) provConfig.authMode = authMode as OcxProviderConfig["authMode"];
if (name === "anthropic2") {
if (provConfig.adapter !== "anthropic" || provConfig.authMode !== "oauth") delete provConfig.anthropicOAuthInstance;
Expand Down Expand Up @@ -328,12 +342,17 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
}
const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
let markerCollision = false;
let removedCredentials: string[] = [];
withConfigMutationLockSync(() => {
if (provConfig.anthropicOAuthInstance) {
try { assertAnthropicInstanceLoginConfig(config, name); }
catch { markerCollision = true; return; }
}
initializeProviderModelSelection(name, provConfig, existingProvider, config);
const durable = readConfigAdmissionSnapshot();
const durableProvider = durable.kind === "read" && durable.diagnostics.source === "file"
&& durable.diagnostics.error === null ? durable.diagnostics.config.providers[name] : undefined;
removedCredentials = [config.providers[name], durableProvider].flatMap(provider => provider ? providerGcpCredentialMarkers(provider) : []);
config.providers[name] = provConfig;
if (setDefault) config.defaultProvider = name;
validateAndSave(config);
Expand All @@ -344,6 +363,7 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
return;
}

cleanupRemovedGcpCredentials(removedCredentials);
let sync: LocalSyncResult | undefined;
if (wantsSync) {
try {
Expand Down Expand Up @@ -440,10 +460,11 @@ function handleRemove(args: string[]): void {
process.exit(1);
}

const removedCredentials = providerGcpCredentialMarkers(config.providers[name]!);
delete config.providers[name];
const droppedCustomModels = dropProviderCustomModels(config, name);
validateAndSave(config);

cleanupRemovedGcpCredentials(removedCredentials);

if (wantsJson) {
console.log(JSON.stringify({
Expand Down
Loading
Loading