From fb5cc21d7165f653d6585cd7b4c3953524575f55 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 15 Jun 2026 15:51:04 -0400 Subject: [PATCH] fix(deploy): map grok/xai models to the xai provider for credential matching deriveModelProvider special-cased anthropic/openai/google/openrouter but had no grok/xai case, so a grok persona's `model: "grok-build"` fell through to the generic `split(/[/:]/)` and resolved to the literal provider key "grok-build". The credential `relay cloud connect xai` stores is keyed `harness: "grok"` (modelProvider "xai"), so "grok-build" matched neither target. The OAuth probe reported "credentials are not connected" even when xai was connected, re-prompted for a browser reconnect that polled for a "grok-build" row that never appears, and the deploy looped (or fell to the plan path and 403'd on workspaces without relay-managed grok). Map grok/xai/x-ai models to provider "xai" and alias "xai" -> "grok" (mirroring anthropic->claude, openai->codex), so the connected harness="grok" entry is recognized and the oauth deploy proceeds. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/deploy/src/modes/cloud.test.ts | 57 ++++++++++++++++++++++++ packages/deploy/src/modes/cloud/index.ts | 3 ++ 2 files changed, 60 insertions(+) diff --git a/packages/deploy/src/modes/cloud.test.ts b/packages/deploy/src/modes/cloud.test.ts index ca61ff87..99e9c807 100644 --- a/packages/deploy/src/modes/cloud.test.ts +++ b/packages/deploy/src/modes/cloud.test.ts @@ -482,6 +482,63 @@ test('cloud harness OAuth probe treats a matching connected entry as ready (skip assert.ok(!calls.some((c) => c.url.includes('/cli/auth'))); }); +test('cloud harness OAuth probe maps a grok persona to the connected xai credential', async () => { + // Regression: a grok persona declares `model: "grok-build"`, but the + // connected credential `relay cloud connect xai` stores is keyed + // `harness: "grok"` (modelProvider "xai"). deriveModelProvider used to + // return the literal model string "grok-build" — which matched neither + // "grok" nor "xai" — so the probe reported "not connected", re-prompted + // for a browser reconnect that never matched, and the deploy looped. + // With grok/xai mapped to provider "xai" (alias "grok"), the connected + // entry is recognized and the deploy proceeds. + const restoreDeps = configureCloudCredentialDepsForTest({ + readStoredAuth: async () => ({ + apiUrl: 'https://cloud.example.test', + accessToken: 'access', + refreshToken: 'refresh', + accessTokenExpiresAt: '2999-01-01T00:00:00.000Z' + }), + createCloudApiClient() { + return { + async fetch(pathname: string) { + assert.equal(pathname, '/api/v1/cloud-agents'); + return okJson({ + agents: [ + { + id: 'cloud-agent-grok', + harness: 'grok', // xai credential is stored under the grok harness alias + status: 'connected', + credentialStoredAt: '2026-06-15T19:15:44.561Z' + } + ] + }); + } + }; + } + }); + + const { calls, handle } = await launch({ + defaultPlanCredential: false, + env: { + WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', + WORKFORCE_DEPLOY_NO_PROMPT: '1' + }, + input: { harnessSource: 'oauth' }, + persona: persona({ harness: 'grok', model: 'grok-build' }), + fetch(url, init) { + if (init?.method === 'GET' && url.endsWith('/deployments')) return okJson({ agents: [] }); + if (url.endsWith('/deployments')) { + return okJson({ agentId: 'agent-grok', deploymentId: 'dep-grok', status: 'active' }, 201); + } + throw new Error(`unexpected URL ${url}`); + } + }).finally(restoreDeps); + + assert.equal(handle.id, 'agent-grok'); + // The connected entry was recognized, so no browser reconnect fired. + assert.ok(!calls.some((c) => c.url.includes('/cli/auth'))); +}); + test('cloud harness OAuth probe ignores entries with the wrong harness', async () => { // If the user has openai connected but the persona's provider is // anthropic, the probe must NOT treat that as readiness — otherwise diff --git a/packages/deploy/src/modes/cloud/index.ts b/packages/deploy/src/modes/cloud/index.ts index e95accd6..6dbef68c 100644 --- a/packages/deploy/src/modes/cloud/index.ts +++ b/packages/deploy/src/modes/cloud/index.ts @@ -912,6 +912,7 @@ function deriveModelProvider(persona: PersonaSpec): string { if (matchesProviderToken(lower, ['openai', 'codex', 'gpt'])) return 'openai'; if (matchesProviderToken(lower, ['google', 'gemini'])) return 'google'; if (matchesProviderToken(lower, ['openrouter', 'opencode'])) return 'openrouter'; + if (matchesProviderToken(lower, ['grok', 'xai', 'x-ai'])) return 'xai'; const [provider] = model.split(/[/:]/, 1); if (provider?.trim()) return provider.trim().toLowerCase(); return harnessFallback; @@ -1017,6 +1018,8 @@ function harnessAliasForModelProvider(modelProvider: string): string { return 'gemini'; case 'openrouter': return 'opencode'; + case 'xai': + return 'grok'; default: return modelProvider; }