@agentworkforce/deploy 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { clearStoredWorkspaceToken, loadWorkspaceToken, resolveWorkspaceToken, writeStoredWorkspaceToken } from './login.js';
7
+ import { createBufferedIO } from './io.js';
8
+ async function withLoginEnv(env, fn) {
9
+ const previous = {
10
+ WORKFORCE_LOGIN_FILE: process.env.WORKFORCE_LOGIN_FILE,
11
+ WORKFORCE_DISABLE_KEYCHAIN: process.env.WORKFORCE_DISABLE_KEYCHAIN,
12
+ WORKFORCE_WORKSPACE_ID: process.env.WORKFORCE_WORKSPACE_ID,
13
+ WORKFORCE_WORKSPACE_TOKEN: process.env.WORKFORCE_WORKSPACE_TOKEN
14
+ };
15
+ process.env.WORKFORCE_DISABLE_KEYCHAIN = '1';
16
+ if (env.loginFile === undefined)
17
+ delete process.env.WORKFORCE_LOGIN_FILE;
18
+ else
19
+ process.env.WORKFORCE_LOGIN_FILE = env.loginFile;
20
+ if (env.workspaceId === undefined)
21
+ delete process.env.WORKFORCE_WORKSPACE_ID;
22
+ else
23
+ process.env.WORKFORCE_WORKSPACE_ID = env.workspaceId;
24
+ if (env.workspaceToken === undefined)
25
+ delete process.env.WORKFORCE_WORKSPACE_TOKEN;
26
+ else
27
+ process.env.WORKFORCE_WORKSPACE_TOKEN = env.workspaceToken;
28
+ try {
29
+ return await fn();
30
+ }
31
+ finally {
32
+ for (const [key, value] of Object.entries(previous)) {
33
+ if (value === undefined) {
34
+ delete process.env[key];
35
+ }
36
+ else {
37
+ process.env[key] = value;
38
+ }
39
+ }
40
+ }
41
+ }
42
+ test('workspace token store writes and reads the active workspace token', async () => {
43
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'wf-login-store-'));
44
+ const loginFile = path.join(dir, 'login.json');
45
+ try {
46
+ await withLoginEnv({ loginFile }, async () => {
47
+ await writeStoredWorkspaceToken({
48
+ workspaceSlug: 'acme',
49
+ workspaceId: 'ws-123',
50
+ token: 'tok-stored',
51
+ cloudUrl: 'https://cloud.example.test'
52
+ });
53
+ const raw = JSON.parse(await readFile(loginFile, 'utf8'));
54
+ assert.equal(raw.workspace, 'acme');
55
+ assert.equal(raw.workspaceId, 'ws-123');
56
+ assert.equal(raw.token, 'tok-stored');
57
+ assert.equal((await loadWorkspaceToken('acme'))?.token, 'tok-stored');
58
+ });
59
+ }
60
+ finally {
61
+ await rm(dir, { recursive: true, force: true });
62
+ }
63
+ });
64
+ test('resolveWorkspaceToken prefers env token before stored login', async () => {
65
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'wf-login-precedence-'));
66
+ const loginFile = path.join(dir, 'login.json');
67
+ try {
68
+ await withLoginEnv({ loginFile }, async () => {
69
+ await writeStoredWorkspaceToken({ workspace: 'stored', token: 'tok-stored' });
70
+ });
71
+ await withLoginEnv({
72
+ loginFile,
73
+ workspaceId: 'env-ws',
74
+ workspaceToken: 'tok-env'
75
+ }, async () => {
76
+ assert.deepEqual(await resolveWorkspaceToken({
77
+ cloudUrl: 'https://cloud.example.test',
78
+ io: createBufferedIO()
79
+ }), { token: 'tok-env', workspace: 'env-ws' });
80
+ });
81
+ }
82
+ finally {
83
+ await rm(dir, { recursive: true, force: true });
84
+ }
85
+ });
86
+ test('resolveWorkspaceToken reads stored token and fails clearly with --no-prompt', async () => {
87
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'wf-login-resolve-'));
88
+ const loginFile = path.join(dir, 'login.json');
89
+ try {
90
+ await withLoginEnv({ loginFile }, async () => {
91
+ await writeStoredWorkspaceToken({ workspace: 'stored', token: 'tok-stored' });
92
+ assert.deepEqual(await resolveWorkspaceToken({
93
+ workspace: 'stored',
94
+ cloudUrl: 'https://cloud.example.test',
95
+ io: createBufferedIO(),
96
+ noPrompt: true
97
+ }), { token: 'tok-stored', workspace: 'stored' });
98
+ });
99
+ await withLoginEnv({ loginFile: path.join(dir, 'missing.json') }, async () => {
100
+ await assert.rejects(resolveWorkspaceToken({
101
+ workspace: 'missing',
102
+ cloudUrl: 'https://cloud.example.test',
103
+ io: createBufferedIO(),
104
+ noPrompt: true
105
+ }), /run `agentworkforce login`/);
106
+ });
107
+ }
108
+ finally {
109
+ await rm(dir, { recursive: true, force: true });
110
+ }
111
+ });
112
+ test('clearStoredWorkspaceToken removes the stored token file', async () => {
113
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'wf-login-clear-'));
114
+ const loginFile = path.join(dir, 'login.json');
115
+ try {
116
+ await withLoginEnv({ loginFile }, async () => {
117
+ await writeStoredWorkspaceToken({ workspace: 'stored', token: 'tok-stored' });
118
+ await clearStoredWorkspaceToken('stored');
119
+ assert.equal(await loadWorkspaceToken('stored'), null);
120
+ });
121
+ }
122
+ finally {
123
+ await rm(dir, { recursive: true, force: true });
124
+ }
125
+ });
126
+ //# sourceMappingURL=login.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.test.js","sourceRoot":"","sources":["../src/login.test.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { CloudApiClient, connectProvider, readStoredAuth, refreshStoredAuth, type StoredAuth } from '@agent-relay/cloud';
1
2
  import type { ModeLaunchHandle, ModeLauncher } from '../types.js';
2
3
  type CloudDeployStatus = 'starting' | 'active' | 'failed' | 'cancelled';
3
4
  export interface CloudRunHandle extends ModeLaunchHandle {
@@ -5,6 +6,14 @@ export interface CloudRunHandle extends ModeLaunchHandle {
5
6
  deploymentId: string;
6
7
  status: CloudDeployStatus;
7
8
  }
9
+ type CloudApiClientLike = Pick<CloudApiClient, 'fetch'>;
10
+ type CloudCredentialDeps = {
11
+ readStoredAuth: typeof readStoredAuth;
12
+ refreshStoredAuth: typeof refreshStoredAuth;
13
+ connectProvider: typeof connectProvider;
14
+ createCloudApiClient(auth: StoredAuth, apiUrl: string): CloudApiClientLike;
15
+ };
16
+ export declare function configureCloudCredentialDepsForTest(overrides: Partial<CloudCredentialDeps>): () => void;
8
17
  /**
9
18
  * Cloud-hosted deploy mode. Uploads the deploy-ready persona bundle to a
10
19
  * workforce-compatible cloud endpoint. The implementation is intentionally
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/modes/cloud.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,gBAAgB,EAChB,YAAY,EACb,MAAM,aAAa,CAAC;AAarB,KAAK,iBAAiB,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAIxE,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AA+CD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,YAmI3B,CAAC"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/modes/cloud.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,eAAe,EAEf,cAAc,EACd,iBAAiB,EACjB,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAEV,gBAAgB,EAChB,YAAY,EACb,MAAM,aAAa,CAAC;AAYrB,KAAK,iBAAiB,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAIxE,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAsCD,KAAK,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAExD,KAAK,mBAAmB,GAAG;IACzB,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,eAAe,EAAE,OAAO,eAAe,CAAC;IACxC,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAAC;CAC5E,CAAC;AAkBF,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACtC,MAAM,IAAI,CAMZ;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,YA8H3B,CAAC"}
@@ -1,15 +1,32 @@
1
- import { spawn } from 'node:child_process';
2
- import { randomUUID } from 'node:crypto';
3
1
  import { readFile } from 'node:fs/promises';
4
- import { createServer } from 'node:http';
5
- import { platform } from 'node:os';
2
+ import { CloudApiClient, connectProvider, defaultApiUrl, readStoredAuth, refreshStoredAuth } from '@agent-relay/cloud';
6
3
  import { resolveWorkspaceToken } from '../login.js';
7
- const DEFAULT_CLOUD_URL = 'https://agentrelay.com';
8
4
  const BUILD_YOUR_OWN_CLOUD_DOCS_URL = 'https://docs.agentworkforce.com/deploy/build-your-own-cloud';
9
5
  const USER_AGENT = 'workforce-deploy';
10
6
  const MAX_ATTEMPTS = 3;
11
7
  const POLL_TIMEOUT_MS = 60_000;
12
8
  const POLL_INTERVAL_MS = 2_000;
9
+ const defaultCloudCredentialDeps = {
10
+ readStoredAuth,
11
+ refreshStoredAuth,
12
+ connectProvider,
13
+ createCloudApiClient(auth, apiUrl) {
14
+ return new CloudApiClient({
15
+ apiUrl,
16
+ accessToken: auth.accessToken,
17
+ refreshToken: auth.refreshToken,
18
+ accessTokenExpiresAt: auth.accessTokenExpiresAt
19
+ });
20
+ }
21
+ };
22
+ let cloudCredentialDeps = defaultCloudCredentialDeps;
23
+ export function configureCloudCredentialDepsForTest(overrides) {
24
+ const previous = cloudCredentialDeps;
25
+ cloudCredentialDeps = { ...cloudCredentialDeps, ...overrides };
26
+ return () => {
27
+ cloudCredentialDeps = previous;
28
+ };
29
+ }
13
30
  /**
14
31
  * Cloud-hosted deploy mode. Uploads the deploy-ready persona bundle to a
15
32
  * workforce-compatible cloud endpoint. The implementation is intentionally
@@ -29,7 +46,7 @@ export const cloudLauncher = {
29
46
  io: input.io,
30
47
  noPrompt
31
48
  });
32
- await ensureHarnessReady({
49
+ const credentialSelections = await ensureHarnessReady({
33
50
  cloudUrl,
34
51
  workspaceId: input.workspace,
35
52
  token: auth.token,
@@ -39,14 +56,6 @@ export const cloudLauncher = {
39
56
  harnessSource: input.harnessSource,
40
57
  byokKey: input.byokKey
41
58
  });
42
- await ensureCloudIntegrations({
43
- cloudUrl,
44
- workspaceId: input.workspace,
45
- token: auth.token,
46
- persona: input.persona,
47
- io: input.io,
48
- noPrompt
49
- });
50
59
  const existingPersona = await handleExistingPersona({
51
60
  cloudUrl,
52
61
  workspaceId: input.workspace,
@@ -76,6 +85,10 @@ export const cloudLauncher = {
76
85
  agent: await readFile(input.bundle.bundlePath, 'utf8'),
77
86
  packageJson: JSON.parse(await readFile(input.bundle.packageJsonPath, 'utf8'))
78
87
  },
88
+ // Keep both casings until all cloud deploy endpoints converge; older
89
+ // previews read snake_case, while current routes read camelCase.
90
+ credentialSelections,
91
+ credential_selections: credentialSelections,
79
92
  inputs: input.inputs ?? readInputsOverride()
80
93
  });
81
94
  input.io.info(`cloud: deploying persona bundle to ${cloudUrl}`);
@@ -139,9 +152,9 @@ function resolveCloudUrl(input) {
139
152
  const fromEnv = process.env.WORKFORCE_DEPLOY_CLOUD_URL?.trim()
140
153
  || process.env.WORKFORCE_CLOUD_URL?.trim();
141
154
  const fromPersona = readPersonaCloudDeployUrl(input.persona);
142
- const raw = fromInput || fromEnv || fromPersona || DEFAULT_CLOUD_URL;
155
+ const raw = fromInput || fromEnv || fromPersona || defaultApiUrl();
143
156
  const resolved = normalizeCloudUrl(raw);
144
- if (resolved !== DEFAULT_CLOUD_URL) {
157
+ if (resolved !== normalizeCloudUrl(defaultApiUrl())) {
145
158
  input.io.info(`cloud: using custom cloud URL ${resolved}. Build your own cloud docs: ${BUILD_YOUR_OWN_CLOUD_DOCS_URL}`);
146
159
  }
147
160
  return resolved;
@@ -156,28 +169,31 @@ async function ensureHarnessReady(args) {
156
169
  const source = await resolveHarnessSource(args);
157
170
  const modelProvider = deriveModelProvider(args.persona);
158
171
  if (source === 'plan') {
159
- await saveProviderCredential({
172
+ const credentialId = await saveProviderCredential({
160
173
  cloudUrl: args.cloudUrl,
174
+ workspaceId: args.workspaceId,
161
175
  token: args.token,
162
176
  modelProvider,
163
177
  authType: 'relay_managed'
164
178
  });
165
179
  args.io.info(`cloud: using workforce plan credentials for ${args.persona.harness}`);
166
- return;
180
+ return { [modelProvider]: credentialId };
167
181
  }
168
182
  if (source === 'byok') {
169
183
  const key = await resolveByokKey(args);
170
- await saveProviderCredential({
184
+ const credentialId = await saveProviderCredential({
171
185
  cloudUrl: args.cloudUrl,
186
+ workspaceId: args.workspaceId,
172
187
  token: args.token,
173
188
  modelProvider,
174
189
  authType: 'byo_api_key',
175
190
  apiKey: key
176
191
  });
177
192
  args.io.info(`cloud: using BYOK credentials for ${args.persona.harness}`);
178
- return;
193
+ return { [modelProvider]: credentialId };
179
194
  }
180
195
  await ensureHarnessOauth(args);
196
+ return {};
181
197
  }
182
198
  async function resolveHarnessSource(args) {
183
199
  if (args.harnessSource)
@@ -195,13 +211,14 @@ async function resolveHarnessSource(args) {
195
211
  return expectHarnessSource(answer);
196
212
  }
197
213
  async function isHarnessOauthConnected(args) {
198
- const url = `${args.cloudUrl}/api/v1/users/me/provider_credentials?model_provider=${encodeURIComponent(deriveModelProvider(args.persona))}`;
199
- const res = await fetch(url, {
214
+ const auth = await readUsableCloudAuth(args.cloudUrl);
215
+ if (!auth)
216
+ return false;
217
+ const client = cloudCredentialDeps.createCloudApiClient(auth, args.cloudUrl);
218
+ const path = `/api/v1/users/me/provider_credentials?model_provider=${encodeURIComponent(deriveModelProvider(args.persona))}`;
219
+ const res = await client.fetch(path, {
200
220
  method: 'GET',
201
- headers: {
202
- authorization: `Bearer ${args.token}`,
203
- 'user-agent': USER_AGENT
204
- }
221
+ headers: { 'user-agent': USER_AGENT }
205
222
  });
206
223
  if (res.status === 404 || res.status === 405)
207
224
  return false;
@@ -242,76 +259,17 @@ async function ensureHarnessOauth(args) {
242
259
  throw new Error(`cloud: ${args.persona.harness} credentials are required for deploy`);
243
260
  }
244
261
  const modelProvider = deriveModelProvider(args.persona);
245
- const startUrl = `${args.cloudUrl}/api/v1/users/me/provider_credentials/auth-session`;
246
- const body = await requestJsonWithRetry(startUrl, {
247
- method: 'POST',
248
- headers: jsonHeaders(args.token),
249
- body: JSON.stringify({
250
- model_provider: modelProvider,
251
- provider: args.persona.harness,
252
- language: 'typescript'
253
- })
254
- }, { action: 'cloud harness OAuth start' });
255
- const connectUrl = readFirstString(body, ['connectLink', 'authUrl', 'url', 'sandboxUrl']);
256
- if (connectUrl) {
257
- args.io.info(`cloud: open ${connectUrl} to finish ${args.persona.harness} OAuth`);
258
- tryOpenBrowser(connectUrl);
259
- }
260
- await pollUntil(() => isHarnessOauthConnected(args), `timed out waiting for ${args.persona.harness} OAuth credentials`);
261
- args.io.info(`cloud: ${args.persona.harness} credentials connected`);
262
- }
263
- async function ensureCloudIntegrations(args) {
264
- const providers = Object.keys(args.persona.integrations ?? {});
265
- for (const provider of providers) {
266
- const ready = await isIntegrationReady({ ...args, provider });
267
- if (ready) {
268
- args.io.info(`cloud: integrations.${provider} ready`);
269
- continue;
270
- }
271
- if (args.noPrompt) {
272
- throw new Error(`cloud: integrations.${provider} is not connected. Run without --no-prompt or connect it before deploying.`);
273
- }
274
- const ok = await args.io.confirm(`Connect ${provider} in workforce cloud now? (opens browser)`, { defaultValue: true });
275
- if (!ok) {
276
- throw new Error(`cloud: integrations.${provider} is required for deploy`);
277
- }
278
- await connectIntegration({ ...args, provider });
279
- await pollUntil(() => isIntegrationReady({ ...args, provider }), `timed out waiting for integrations.${provider} to become ready`);
280
- args.io.info(`cloud: integrations.${provider} connected`);
281
- }
282
- }
283
- async function isIntegrationReady(args) {
284
- const url = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/integrations?provider=${encodeURIComponent(args.provider)}`;
285
- const res = await fetch(url, {
286
- method: 'GET',
287
- headers: {
288
- authorization: `Bearer ${args.token}`,
289
- 'user-agent': USER_AGENT
290
- }
291
- });
292
- if (res.status === 401) {
293
- throw new Error('cloud integration check failed: unauthorized. Run `workforce login` and retry.');
294
- }
295
- if (res.status === 404)
296
- return false;
297
- if (!res.ok) {
298
- throw new Error(`cloud integration check failed: ${res.status} ${await responseExcerpt(res)}`);
299
- }
300
- const body = (await res.json());
301
- return integrationReady(body, args.provider);
302
- }
303
- async function connectIntegration(args) {
304
- await waitForOAuthCallback({
305
- action: `integrations.${args.provider}`,
306
- io: args.io,
307
- buildUrl(returnTo) {
308
- const url = new URL('/integrations', args.cloudUrl);
309
- url.searchParams.set('provider', args.provider);
310
- url.searchParams.set('workspace', args.workspaceId);
311
- url.searchParams.set('return_to', returnTo);
312
- return url.toString();
262
+ await cloudCredentialDeps.connectProvider({
263
+ provider: modelProvider,
264
+ apiUrl: args.cloudUrl,
265
+ language: 'typescript',
266
+ io: {
267
+ log: (...parts) => args.io.info(parts.map(String).join(' ')),
268
+ error: (...parts) => args.io.error(parts.map(String).join(' '))
313
269
  }
314
270
  });
271
+ await pollUntil(() => isHarnessOauthConnected(args), `timed out waiting for ${args.persona.harness} OAuth credentials`);
272
+ args.io.info(`cloud: ${args.persona.harness} credentials connected`);
315
273
  }
316
274
  async function handleExistingPersona(args) {
317
275
  const existing = await findExistingAgent(args);
@@ -411,23 +369,67 @@ function parseAgentLike(value) {
411
369
  };
412
370
  }
413
371
  async function saveProviderCredential(args) {
414
- await requestJsonWithRetry(`${args.cloudUrl}/api/v1/users/me/provider_credentials`, {
372
+ if (args.authType === 'relay_managed') {
373
+ const url = new URL(`${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/provider-credentials/managed`);
374
+ url.searchParams.set('provider', args.modelProvider);
375
+ const body = await requestJsonWithRetry(url.toString(), {
376
+ method: 'POST',
377
+ headers: jsonHeaders(args.token)
378
+ }, { action: 'cloud managed provider credentials' });
379
+ return readCredentialId(body);
380
+ }
381
+ const body = await requestJsonWithRetry(`${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/provider-credentials/byok`, {
415
382
  method: 'POST',
416
383
  headers: jsonHeaders(args.token),
417
384
  body: JSON.stringify({
385
+ // Keep both casings during the deploy-v1 rollout for the same mixed
386
+ // preview/production route compatibility as the deploy payload above.
387
+ modelProvider: args.modelProvider,
418
388
  model_provider: args.modelProvider,
419
- auth_type: args.authType,
420
- ...(args.apiKey ? { api_key: args.apiKey } : {})
389
+ key: args.apiKey,
390
+ api_key: args.apiKey
421
391
  })
422
- }, { action: 'cloud provider credentials save' });
392
+ }, { action: 'cloud BYOK provider credentials' });
393
+ return readCredentialId(body);
423
394
  }
424
395
  function deriveModelProvider(persona) {
425
396
  const model = typeof persona.model === 'string' ? persona.model.trim() : '';
397
+ if (!model)
398
+ return persona.harness;
399
+ const lower = model.toLowerCase();
400
+ if (matchesProviderToken(lower, ['anthropic', 'claude']))
401
+ return 'anthropic';
402
+ if (matchesProviderToken(lower, ['openai', 'codex', 'gpt']))
403
+ return 'openai';
404
+ if (matchesProviderToken(lower, ['google', 'gemini']))
405
+ return 'google';
406
+ if (matchesProviderToken(lower, ['openrouter', 'opencode']))
407
+ return 'openrouter';
426
408
  const [provider] = model.split(/[/:]/, 1);
427
409
  if (provider?.trim())
428
- return provider.trim();
410
+ return provider.trim().toLowerCase();
429
411
  return persona.harness;
430
412
  }
413
+ function matchesProviderToken(model, tokens) {
414
+ return tokens.some((token) => new RegExp(`^${escapeRegExp(token)}($|[/:._-])`).test(model));
415
+ }
416
+ function escapeRegExp(value) {
417
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
418
+ }
419
+ function readCredentialId(body) {
420
+ const direct = readFirstString(body, ['providerCredentialId', 'provider_credential_id', 'credentialId', 'id']);
421
+ if (direct)
422
+ return direct;
423
+ for (const field of ['credential', 'providerCredential']) {
424
+ const nested = body[field];
425
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
426
+ const nestedId = readFirstString(nested, ['id', 'providerCredentialId', 'provider_credential_id']);
427
+ if (nestedId)
428
+ return nestedId;
429
+ }
430
+ }
431
+ throw new Error('cloud provider credentials response missing credential id');
432
+ }
431
433
  function providerCredentialsReady(body) {
432
434
  const candidates = [
433
435
  body.credential,
@@ -447,87 +449,6 @@ function providerCredentialsReady(body) {
447
449
  || typeof record.id === 'string';
448
450
  });
449
451
  }
450
- function integrationReady(body, provider) {
451
- const candidates = [
452
- ...(Array.isArray(body.integrations) ? body.integrations : []),
453
- body
454
- ];
455
- return candidates.some((candidate) => {
456
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
457
- return false;
458
- const record = candidate;
459
- const recordProvider = typeof record.provider === 'string' ? record.provider : provider;
460
- if (recordProvider !== provider)
461
- return false;
462
- return record.ready === true
463
- || record.state === 'ready'
464
- || record.state === 'connected'
465
- || typeof record.connectionId === 'string'
466
- || typeof record.currentConnectionId === 'string';
467
- });
468
- }
469
- async function waitForOAuthCallback(args) {
470
- const state = randomUUID();
471
- await new Promise((resolve, reject) => {
472
- let settled = false;
473
- const timeout = setTimeout(() => {
474
- settleError(new Error(`timed out waiting for ${args.action} OAuth callback`));
475
- }, pollTimeoutMs()).unref();
476
- const server = createServer((request, response) => {
477
- const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
478
- if (requestUrl.pathname !== '/callback') {
479
- response.statusCode = 404;
480
- response.end('not found');
481
- return;
482
- }
483
- if (requestUrl.searchParams.get('state') !== state) {
484
- response.statusCode = 400;
485
- response.end('invalid state');
486
- settleError(new Error(`${args.action} OAuth callback returned an invalid state`));
487
- return;
488
- }
489
- const error = requestUrl.searchParams.get('error');
490
- if (error) {
491
- response.statusCode = 400;
492
- response.end('OAuth failed');
493
- settleError(new Error(error));
494
- return;
495
- }
496
- response.statusCode = 200;
497
- response.end('workforce OAuth complete; you can close this tab');
498
- settleOk();
499
- });
500
- function settleOk() {
501
- if (settled)
502
- return;
503
- settled = true;
504
- clearTimeout(timeout);
505
- server.close();
506
- resolve();
507
- }
508
- function settleError(error) {
509
- if (settled)
510
- return;
511
- settled = true;
512
- clearTimeout(timeout);
513
- server.close();
514
- reject(error);
515
- }
516
- server.on('error', settleError);
517
- server.listen(0, '127.0.0.1', () => {
518
- const address = server.address();
519
- if (!address || typeof address === 'string') {
520
- settleError(new Error(`failed to start ${args.action} OAuth callback server`));
521
- return;
522
- }
523
- const callback = new URL('/callback', `http://127.0.0.1:${address.port}`);
524
- callback.searchParams.set('state', state);
525
- const connectUrl = args.buildUrl(callback.toString());
526
- args.io.info(`cloud: open ${connectUrl} to finish ${args.action} OAuth`);
527
- tryOpenBrowser(connectUrl);
528
- });
529
- });
530
- }
531
452
  function expectHarnessSource(value) {
532
453
  const normalized = value.trim().toLowerCase();
533
454
  if (normalized === 'plan' || normalized === 'byok' || normalized === 'oauth') {
@@ -574,9 +495,33 @@ function readPersonaCloudDeployUrl(persona) {
574
495
  function normalizeCloudUrl(url) {
575
496
  const trimmed = url.trim();
576
497
  if (!trimmed)
577
- return DEFAULT_CLOUD_URL;
498
+ return normalizeCloudUrl(defaultApiUrl());
578
499
  return trimmed.replace(/\/+$/, '');
579
500
  }
501
+ async function readUsableCloudAuth(apiUrl) {
502
+ let auth = await cloudCredentialDeps.readStoredAuth().catch(() => null);
503
+ if (!auth)
504
+ return null;
505
+ if (isAuthExpired(auth.accessTokenExpiresAt)) {
506
+ auth = await cloudCredentialDeps.refreshStoredAuth(auth).catch((err) => {
507
+ console.warn(`cloud: stored auth refresh failed: ${formatErrorMessage(err)}`);
508
+ return null;
509
+ });
510
+ }
511
+ if (!auth)
512
+ return null;
513
+ return {
514
+ ...auth,
515
+ apiUrl
516
+ };
517
+ }
518
+ function formatErrorMessage(err) {
519
+ return err instanceof Error ? err.message : String(err);
520
+ }
521
+ function isAuthExpired(expiresAt) {
522
+ const millis = Date.parse(expiresAt);
523
+ return Number.isNaN(millis) || millis <= Date.now() + 60_000;
524
+ }
580
525
  function readInputsOverride() {
581
526
  const raw = process.env.WORKFORCE_DEPLOY_INPUTS_JSON?.trim();
582
527
  if (!raw)
@@ -678,19 +623,6 @@ function emitLog(args, line) {
678
623
  args.onLog?.(line);
679
624
  args.io.info(line);
680
625
  }
681
- function tryOpenBrowser(url) {
682
- const command = platform() === 'darwin'
683
- ? 'open'
684
- : platform() === 'win32'
685
- ? 'cmd'
686
- : 'xdg-open';
687
- const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
688
- const child = spawn(command, args, { stdio: 'ignore', detached: true });
689
- child.on('error', () => {
690
- // URL is printed; browser launch is best-effort.
691
- });
692
- child.unref();
693
- }
694
626
  function pollTimeoutMs() {
695
627
  return numberFromEnv('WORKFORCE_DEPLOY_POLL_TIMEOUT_MS') ?? POLL_TIMEOUT_MS;
696
628
  }