diff --git a/docs/plans/deploy-v1-credentials-runtime-checklist.md b/docs/plans/deploy-v1-credentials-runtime-checklist.md index 5a501aab..77990cf3 100644 --- a/docs/plans/deploy-v1-credentials-runtime-checklist.md +++ b/docs/plans/deploy-v1-credentials-runtime-checklist.md @@ -92,7 +92,7 @@ Source spec: `docs/plans/deploy-v1-credentials-and-runtime-spec.md` - [x] Compute markup only for `relay_managed`. - [x] Emit monthly soft-cap warning over $100. - [x] Wire CLI `--harness-source byok --byok-key` to BYOK route before deploy. -- [x] Wire CLI `--harness-source plan` to managed credential route before deploy. +- [x] Wire CLI `--harness-source managed` to managed credential route before deploy (`plan` remains a legacy alias). - [x] Preserve CLI `--harness-source oauth` behavior. - [x] Add cloud BYOK route tests. - [x] Add cloud managed route tests. diff --git a/packages/cli/src/deploy-command.test.ts b/packages/cli/src/deploy-command.test.ts index ebf36a40..f14e8cc0 100644 --- a/packages/cli/src/deploy-command.test.ts +++ b/packages/cli/src/deploy-command.test.ts @@ -366,6 +366,18 @@ test('parseDeployArgs: --reconnect is repeatable and comma-aware', () => { assert.deepEqual(parsed.reconnectProviders, ['slack', 'github', 'linear']); }); +test('parseDeployArgs: --harness-source managed is accepted', () => { + const parsed = parseDeployArgs(['./persona.json', '--harness-source', 'managed']); + + assert.equal(parsed.harnessSource, 'managed'); +}); + +test('parseDeployArgs: legacy --harness-source plan normalizes to managed', () => { + const parsed = parseDeployArgs(['./persona.json', '--harness-source=plan']); + + assert.equal(parsed.harnessSource, 'managed'); +}); + test('parseDeployArgs: malformed --input exits with clean error', () => { const trap = trapExit(); try { diff --git a/packages/cli/src/deploy-command.ts b/packages/cli/src/deploy-command.ts index 8c3fa15a..3d3c43b5 100644 --- a/packages/cli/src/deploy-command.ts +++ b/packages/cli/src/deploy-command.ts @@ -230,7 +230,8 @@ Flags: --dry-run Validate the persona and exit before any side effects --cloud-url Override the workforce cloud base URL --no-prompt Fail instead of prompting for cloud setup - --harness-source Cloud harness source: plan, byok, or oauth + --harness-source Cloud harness source: managed, byok, or oauth + (legacy alias: plan) --byok-key API key for --harness-source byok --on-exists Existing cloud persona behavior: cancel, update, or destroy --input = Override a declared persona input (repeatable) @@ -262,7 +263,6 @@ Flags: -h, --help Print this message `; -const HARNESS_SOURCES = ['plan', 'byok', 'oauth'] as const; const ON_EXISTS_CHOICES = ['update', 'destroy', 'cancel'] as const; export function parseDeployArgs(args: readonly string[]): DeployOptions { @@ -317,9 +317,9 @@ export function parseDeployArgs(args: readonly string[]): DeployOptions { noPrompt = true; noConnect = true; } else if (a === '--harness-source') { - harnessSource = expectChoice('--harness-source', expectValue('--harness-source', args[++i]), HARNESS_SOURCES); + harnessSource = expectHarnessSourceFlag(expectValue('--harness-source', args[++i])); } else if (a.startsWith('--harness-source=')) { - harnessSource = expectChoice('--harness-source', expectInlineValue('--harness-source', a.slice('--harness-source='.length)), HARNESS_SOURCES); + harnessSource = expectHarnessSourceFlag(expectInlineValue('--harness-source', a.slice('--harness-source='.length))); } else if (a === '--byok-key') { byokKey = expectValue('--byok-key', args[++i]); } else if (a.startsWith('--byok-key=')) { @@ -415,6 +415,13 @@ function expectChoice(flag: string, value: string, allowed: re return value as T; } +function expectHarnessSourceFlag(value: string): DeployOptions['harnessSource'] { + const normalized = value.trim().toLowerCase(); + if (normalized === 'plan') return 'managed'; + if (normalized === 'managed' || normalized === 'byok' || normalized === 'oauth') return normalized; + die(`--harness-source: expected one of managed|byok|oauth (legacy alias: plan); got "${value}"`); +} + function parseLoginArgs(args: readonly string[]): { workspace?: string; cloudUrl?: string } { let workspace: string | undefined; let cloudUrl: string | undefined; diff --git a/packages/deploy/src/deploy.test.ts b/packages/deploy/src/deploy.test.ts index 34e79ae0..eaa41c81 100644 --- a/packages/deploy/src/deploy.test.ts +++ b/packages/deploy/src/deploy.test.ts @@ -435,7 +435,23 @@ test('deploy --dry-run rejects useSubscription when cloud mode is not selected', } }); -test('deploy --dry-run rejects useSubscription with workforce plan credentials', async () => { +test('deploy --dry-run rejects useSubscription with workforce managed credentials', async () => { + const { personaPath, cleanup } = await withTempPersona( + basePersonaJson({ useSubscription: true }) + ); + const io = createBufferedIO(); + try { + await assert.rejects( + deploy({ personaPath, mode: 'cloud', dryRun: true, harnessSource: 'managed', io }), + /use --harness-source oauth/ + ); + assert.ok(!io.messages.find((m) => m.message.startsWith('workspace:'))); + } finally { + await cleanup(); + } +}); + +test('deploy --dry-run rejects useSubscription with legacy plan alias', async () => { const { personaPath, cleanup } = await withTempPersona( basePersonaJson({ useSubscription: true }) ); diff --git a/packages/deploy/src/deploy.ts b/packages/deploy/src/deploy.ts index 29185d48..4de1d8c3 100644 --- a/packages/deploy/src/deploy.ts +++ b/packages/deploy/src/deploy.ts @@ -624,7 +624,7 @@ function validateSubscriptionSupport( args: { mode: DeployMode; subscription?: ProviderSubscriptionResolver; - harnessSource?: 'plan' | 'byok' | 'oauth'; + harnessSource?: 'managed' | 'plan' | 'byok' | 'oauth'; } ): void { if (!persona.useSubscription || args.subscription) return; diff --git a/packages/deploy/src/modes/cloud-subscription.test.ts b/packages/deploy/src/modes/cloud-subscription.test.ts index acc405ae..470d53e0 100644 --- a/packages/deploy/src/modes/cloud-subscription.test.ts +++ b/packages/deploy/src/modes/cloud-subscription.test.ts @@ -120,9 +120,9 @@ function subscriptionArgs( // Step 2 — subscription-source tests (tests 1–3) // --------------------------------------------------------------------------- -test('validateCloudSubscriptionSupport throws when harnessSource is plan', () => { +test('validateCloudSubscriptionSupport throws when harnessSource is managed', () => { assert.throws( - () => validateCloudSubscriptionSupport({ persona: persona(), harnessSource: 'plan' }), + () => validateCloudSubscriptionSupport({ persona: persona(), harnessSource: 'managed' }), (err: Error) => { assert.ok( err.message.includes('useSubscription:true'), @@ -137,6 +137,13 @@ test('validateCloudSubscriptionSupport throws when harnessSource is plan', () => ); }); +test('validateCloudSubscriptionSupport throws when harnessSource is legacy plan alias', () => { + assert.throws( + () => validateCloudSubscriptionSupport({ persona: persona(), harnessSource: 'plan' }), + /useSubscription:true/ + ); +}); + test('validateCloudSubscriptionSupport throws when WORKFORCE_DEPLOY_HARNESS_SOURCE=plan', async () => { await withEnv({ WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan' }, async () => { assert.throws( @@ -146,6 +153,15 @@ test('validateCloudSubscriptionSupport throws when WORKFORCE_DEPLOY_HARNESS_SOUR }); }); +test('validateCloudSubscriptionSupport throws when WORKFORCE_DEPLOY_HARNESS_SOURCE=managed', async () => { + await withEnv({ WORKFORCE_DEPLOY_HARNESS_SOURCE: 'managed' }, async () => { + assert.throws( + () => validateCloudSubscriptionSupport({ persona: persona() }), + /useSubscription:true/ + ); + }); +}); + test('ensureCloudSubscriptionReady oauth leg throws "credentials are not connected" under noPrompt when no connected row', async () => { // Default source is oauth when no harnessSource supplied. With no connected // entry in /cloud-agents and noPrompt:true, must throw the subscription diff --git a/packages/deploy/src/modes/cloud.test.ts b/packages/deploy/src/modes/cloud.test.ts index e12ae4ff..0012b98a 100644 --- a/packages/deploy/src/modes/cloud.test.ts +++ b/packages/deploy/src/modes/cloud.test.ts @@ -131,13 +131,13 @@ async function launch(overrides: { persona?: PersonaSpec; env?: Partial>; input?: Partial; - defaultPlanCredential?: boolean; + defaultManagedCredential?: boolean; fetch: (url: string, init: RequestInit | undefined, calls: FetchCall[]) => Response | Promise; }) { const { bundle, cleanup } = await withBundle(); const io = createBufferedIO(); const fetchMock = installFetch((url, init, calls) => { - if (overrides.defaultPlanCredential !== false && url.includes('/provider-credentials/managed')) { + if (overrides.defaultManagedCredential !== false && url.includes('/provider-credentials/managed')) { assert.equal(init?.method, 'POST'); return okJson({ providerCredentialId: 'cred-1' }); } @@ -146,7 +146,7 @@ async function launch(overrides: { try { const handle = await withEnv({ WORKFORCE_WORKSPACE_TOKEN: 'tok', - WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan', + WORKFORCE_DEPLOY_HARNESS_SOURCE: 'managed', WORKFORCE_DEPLOY_POLL_INTERVAL_MS: '0', WORKFORCE_DEPLOY_POLL_TIMEOUT_MS: '50', WORKFORCE_DEPLOY_RETRY_BACKOFF_MS: '0', @@ -321,28 +321,28 @@ test('cloud URL precedence is flag env, cloud env, persona deployUrl, then defau ); }); -test('cloud harness plan and BYOK save provider credentials through the cloud contract', async () => { - const plan = await launch({ - defaultPlanCredential: false, +test('cloud harness managed and BYOK save provider credentials through the cloud contract', async () => { + const managed = await launch({ + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test' }, - input: { harnessSource: 'plan' }, + input: { harnessSource: 'managed' }, fetch(url, init) { if (url.endsWith('/provider-credentials/managed?provider=openai')) { assert.equal(init?.method, 'POST'); assert.equal(init?.body, undefined); - return okJson({ providerCredentialId: 'cred-plan' }); + return okJson({ providerCredentialId: 'cred-managed' }); } if (init?.method === 'GET' && url.endsWith('/deployments')) return okJson({ agents: [] }); if (url.endsWith('/deployments')) { - return okJson({ agentId: 'agent-plan', deploymentId: 'dep-plan', status: 'active' }, 201); + return okJson({ agentId: 'agent-managed', deploymentId: 'dep-managed', status: 'active' }, 201); } throw new Error(`unexpected URL ${url}`); } }); - assert.equal(plan.handle.id, 'agent-plan'); + assert.equal(managed.handle.id, 'agent-managed'); const byok = await launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test' }, input: { harnessSource: 'byok', byokKey: 'sk-test' }, fetch(url, init) { @@ -366,12 +366,87 @@ test('cloud harness plan and BYOK save provider credentials through the cloud co assert.equal(byok.handle.id, 'agent-byok'); }); +test('cloud harness legacy plan alias maps to managed provider credentials', async () => { + const planAlias = await launch({ + defaultManagedCredential: false, + env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test' }, + input: { harnessSource: 'plan' }, + fetch(url, init) { + if (url.endsWith('/provider-credentials/managed?provider=openai')) { + assert.equal(init?.method, 'POST'); + assert.equal(init?.body, undefined); + return okJson({ providerCredentialId: 'cred-managed' }); + } + if (init?.method === 'GET' && url.endsWith('/deployments')) return okJson({ agents: [] }); + if (url.endsWith('/deployments')) { + const body = JSON.parse(String(init?.body)); + assert.deepEqual(body.credentialSelections, { openai: 'cred-managed' }); + return okJson({ agentId: 'agent-managed', deploymentId: 'dep-managed', status: 'active' }, 201); + } + throw new Error(`unexpected URL ${url}`); + } + }); + + assert.equal(planAlias.handle.id, 'agent-managed'); +}); + +test('cloud harness legacy plan env alias maps to managed provider credentials', async () => { + const planAlias = await launch({ + defaultManagedCredential: false, + env: { + WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', + WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan' + }, + fetch(url, init) { + if (url.endsWith('/provider-credentials/managed?provider=openai')) { + assert.equal(init?.method, 'POST'); + return okJson({ providerCredentialId: 'cred-managed-env' }); + } + if (init?.method === 'GET' && url.endsWith('/deployments')) return okJson({ agents: [] }); + if (url.endsWith('/deployments')) { + const body = JSON.parse(String(init?.body)); + assert.deepEqual(body.credentialSelections, { openai: 'cred-managed-env' }); + return okJson({ agentId: 'agent-managed-env', deploymentId: 'dep-managed-env', status: 'active' }, 201); + } + throw new Error(`unexpected URL ${url}`); + } + }); + + assert.equal(planAlias.handle.id, 'agent-managed-env'); +}); + +test('cloud harness prompt default chooses managed provider credentials', async () => { + const prompted = await launch({ + defaultManagedCredential: false, + env: { + WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', + WORKFORCE_DEPLOY_HARNESS_SOURCE: undefined + }, + fetch(url, init) { + if (init?.method === 'GET' && url.endsWith('/cloud-agents')) return okJson({ agents: [] }); + if (url.endsWith('/provider-credentials/managed?provider=openai')) { + assert.equal(init?.method, 'POST'); + return okJson({ providerCredentialId: 'cred-managed-prompt' }); + } + if (init?.method === 'GET' && url.endsWith('/deployments')) return okJson({ agents: [] }); + if (url.endsWith('/deployments')) { + const body = JSON.parse(String(init?.body)); + assert.deepEqual(body.credentialSelections, { openai: 'cred-managed-prompt' }); + return okJson({ agentId: 'agent-managed-prompt', deploymentId: 'dep-managed-prompt', status: 'active' }, 201); + } + throw new Error(`unexpected URL ${url}`); + } + }); + + assert.equal(prompted.handle.id, 'agent-managed-prompt'); +}); + test('cloud BYOK provider detection avoids substring false positives', async () => { // A bare model name without a provider separator (/) should not match // "openai" via substring — the harness-derived provider wins. // The default test persona has harness: 'codex' → HARNESS_TO_PROVIDER → 'openai'. await launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, persona: persona({ model: 'my-openai-alternative' }), env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test' }, input: { harnessSource: 'byok', byokKey: 'sk-test' }, @@ -391,7 +466,7 @@ test('cloud BYOK provider detection avoids substring false positives', async () test('cloud BYOK opencode harness derives opencode provider', async () => { await launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, persona: persona({ harness: 'opencode', model: 'deepseek-v4-flash-free' }), env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test' }, input: { harnessSource: 'byok', byokKey: 'sk-or-test' }, @@ -435,7 +510,7 @@ test('cloud harness OAuth probe hits /api/v1/cloud-agents and honors no-prompt f }); await assert.rejects( launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_NO_PROMPT: '1' @@ -541,7 +616,7 @@ test('cloud harness OAuth probe maps a grok persona to the connected xai credent }); const { calls, handle } = await launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_NO_PROMPT: '1' @@ -592,7 +667,7 @@ test('cloud harness OAuth probe ignores entries with the wrong harness', async ( // Override the persona to claude/anthropic so the expected provider mismatches. await assert.rejects( launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_NO_PROMPT: '1' @@ -765,7 +840,7 @@ test('cloud --reconnect with --no-prompt fails with actionable guidance', async }); await assert.rejects( launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_NO_PROMPT: '1' @@ -836,7 +911,7 @@ test('cloud polling resolves done with code 0 on active and 1 on failed', async const handle = await withEnv({ WORKFORCE_WORKSPACE_TOKEN: 'tok', WORKFORCE_DEPLOY_CLOUD_URL: `https://${finalStatus}.example.test`, - WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan', + WORKFORCE_DEPLOY_HARNESS_SOURCE: 'managed', WORKFORCE_DEPLOY_POLL_INTERVAL_MS: '0', WORKFORCE_DEPLOY_POLL_TIMEOUT_MS: '50', WORKFORCE_DEPLOY_RETRY_BACKOFF_MS: '0' @@ -875,7 +950,7 @@ test('cloud stop calls the destroy agent endpoint', async () => { const handle = await withEnv({ WORKFORCE_WORKSPACE_TOKEN: 'tok', WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', - WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan', + WORKFORCE_DEPLOY_HARNESS_SOURCE: 'managed', WORKFORCE_DEPLOY_POLL_INTERVAL_MS: '0', WORKFORCE_DEPLOY_POLL_TIMEOUT_MS: '50' }, () => cloudLauncher.launch({ @@ -909,7 +984,7 @@ test('cloud launcher leaves integration preflight to the deploy orchestrator', a await withEnv({ WORKFORCE_WORKSPACE_TOKEN: 'tok', WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', - WORKFORCE_DEPLOY_HARNESS_SOURCE: 'plan', + WORKFORCE_DEPLOY_HARNESS_SOURCE: 'managed', WORKFORCE_DEPLOY_POLL_INTERVAL_MS: '0', WORKFORCE_DEPLOY_POLL_TIMEOUT_MS: '50' }, () => cloudLauncher.launch({ @@ -1211,7 +1286,7 @@ function callsForUrl(calls: FetchCall[], suffix: string): number { } test('cloud oauth deploy stamps anthropic credentialSelections from the connected row', async () => { - // workforce#196: the byok/plan legs stamp the credential they create, but + // workforce#196: the byok/managed legs stamp the credential they create, but // the oauth leg deployed with empty selections, so ctx.llm stubbed on // every fire. The connected row id comes back through // /api/v1/cloud-agents (cloud selects it straight from @@ -1241,7 +1316,7 @@ test('cloud oauth deploy stamps anthropic credentialSelections from the connecte try { const { handle, io } = await launch({ persona: persona({ harness: 'claude', model: 'claude-sonnet-4-6' }), - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_HARNESS_SOURCE: 'oauth', @@ -1295,7 +1370,7 @@ test('cloud oauth deploy does NOT stamp openai selections and prints the harness }); try { const { io } = await launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_HARNESS_SOURCE: 'oauth', @@ -1347,7 +1422,7 @@ test('cloud oauth deploy falls back to unstamped when the connected row has no i try { const { handle, io } = await launch({ persona: persona({ harness: 'claude', model: 'claude-sonnet-4-6' }), - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_HARNESS_SOURCE: 'oauth', @@ -1403,7 +1478,7 @@ test('cloud oauth deploy stamps the ACTIVE anthropic row over a newer inactive o try { await launch({ persona: persona({ harness: 'claude', model: 'claude-sonnet-4-6' }), - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_HARNESS_SOURCE: 'oauth', @@ -1452,7 +1527,7 @@ test('cloud oauth deploy cross-stamps a connected anthropic credential for an op }); try { const { io } = await launch({ - defaultPlanCredential: false, + defaultManagedCredential: false, env: { WORKFORCE_DEPLOY_CLOUD_URL: 'https://cloud.example.test', WORKFORCE_DEPLOY_HARNESS_SOURCE: 'oauth', diff --git a/packages/deploy/src/modes/cloud/index.ts b/packages/deploy/src/modes/cloud/index.ts index d0454831..e1ef17f4 100644 --- a/packages/deploy/src/modes/cloud/index.ts +++ b/packages/deploy/src/modes/cloud/index.ts @@ -25,7 +25,8 @@ const POLL_TIMEOUT_MS = 60_000; const POLL_INTERVAL_MS = 2_000; type CloudDeployStatus = 'ready' | 'starting' | 'active' | 'failed' | 'cancelled'; -export type HarnessSource = 'plan' | 'byok' | 'oauth'; +export type HarnessSource = 'managed' | 'plan' | 'byok' | 'oauth'; +type CanonicalHarnessSource = Exclude; type OnExistsChoice = 'update' | 'destroy' | 'cancel'; export interface CloudSubscriptionReadyResult { @@ -301,7 +302,7 @@ async function ensureHarnessReady(args: { } const source = await resolveHarnessSource(args); const modelProvider = deriveModelProvider(args.persona); - if (source === 'plan') { + if (source === 'managed') { const credentialId = await saveProviderCredential({ cloudUrl: args.cloudUrl, workspaceId: args.workspaceId, @@ -309,7 +310,7 @@ async function ensureHarnessReady(args: { modelProvider, authType: 'relay_managed' }); - args.io.info(`cloud: using workforce plan credentials for ${args.persona.harness}`); + args.io.info(`cloud: using workforce managed credentials for ${args.persona.harness}`); return { [modelProvider]: credentialId }; } @@ -340,8 +341,8 @@ async function resolveHarnessSource(args: { noPrompt: boolean; harnessSource?: HarnessSource; byokKey?: string; -}): Promise { - if (args.harnessSource) return args.harnessSource; +}): Promise { + if (args.harnessSource) return normalizeHarnessSource(args.harnessSource); const fromEnv = process.env.WORKFORCE_DEPLOY_HARNESS_SOURCE?.trim(); if (fromEnv) return expectHarnessSource(fromEnv); @@ -350,13 +351,13 @@ async function resolveHarnessSource(args: { if (args.noPrompt) { throw new Error( - `cloud: ${args.persona.harness} credentials are not connected. Re-run with --harness-source plan|byok|oauth, set WORKFORCE_DEPLOY_HARNESS_SOURCE, or run without --no-prompt.` + `cloud: ${args.persona.harness} credentials are not connected. Re-run with --harness-source managed|byok|oauth, set WORKFORCE_DEPLOY_HARNESS_SOURCE, or run without --no-prompt.` ); } const answer = await args.io.prompt( - `${args.persona.harness} credentials are not connected. Choose harness source (plan/byok/oauth)`, - { defaultValue: 'plan' } + `${args.persona.harness} credentials are not connected. Choose harness source (managed/byok/oauth)`, + { defaultValue: 'managed' } ); return expectHarnessSource(answer); } @@ -408,7 +409,7 @@ async function fetchCloudAgents(cloudUrl: string): Promise { +}): Exclude { const rawSource = args.harnessSource ?? process.env.WORKFORCE_DEPLOY_HARNESS_SOURCE?.trim(); const source = rawSource ? expectHarnessSource(rawSource) : 'oauth'; - if (source === 'plan') { + if (source === 'managed') { throw new Error( `persona "${args.persona.id}" sets useSubscription:true; use --harness-source oauth to connect your LLM provider, ` + 'use --harness-source byok with --byok-key, or remove useSubscription to use workforce-billed inference.' @@ -1089,12 +1090,16 @@ function harnessAliasForModelProvider(modelProvider: string): string { } } -function expectHarnessSource(value: string): HarnessSource { +function expectHarnessSource(value: string): CanonicalHarnessSource { const normalized = value.trim().toLowerCase(); - if (normalized === 'plan' || normalized === 'byok' || normalized === 'oauth') { - return normalized; + if (normalized === 'managed' || normalized === 'plan' || normalized === 'byok' || normalized === 'oauth') { + return normalizeHarnessSource(normalized); } - throw new Error(`cloud: harness source must be one of plan|byok|oauth; got "${value}"`); + throw new Error(`cloud: harness source must be one of managed|byok|oauth; got "${value}"`); +} + +function normalizeHarnessSource(source: HarnessSource): CanonicalHarnessSource { + return source === 'plan' ? 'managed' : source; } function expectOnExistsChoice(value: string): OnExistsChoice { diff --git a/packages/deploy/src/types.ts b/packages/deploy/src/types.ts index 83895a01..6ae21a03 100644 --- a/packages/deploy/src/types.ts +++ b/packages/deploy/src/types.ts @@ -26,7 +26,7 @@ export interface DeployOptions { /** Fail instead of prompting for cloud auth/integration setup. */ noPrompt?: boolean; /** Cloud harness credential source. */ - harnessSource?: 'plan' | 'byok' | 'oauth'; + harnessSource?: 'managed' | 'plan' | 'byok' | 'oauth'; /** BYOK API key used when `harnessSource` is `byok`. */ byokKey?: string; /** Existing cloud persona behavior. Defaults to `cancel`. */ @@ -142,7 +142,7 @@ export interface ModeLaunchInput { /** Fail instead of prompting for cloud setup. */ noPrompt?: boolean; /** Cloud harness credential source. */ - harnessSource?: 'plan' | 'byok' | 'oauth'; + harnessSource?: 'managed' | 'plan' | 'byok' | 'oauth'; /** BYOK API key used when `harnessSource` is `byok`. */ byokKey?: string; /**