@agentworkforce/deploy 3.0.2 → 3.0.4

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.
Files changed (42) hide show
  1. package/dist/connect.d.ts +11 -0
  2. package/dist/connect.d.ts.map +1 -1
  3. package/dist/connect.js +120 -2
  4. package/dist/connect.js.map +1 -1
  5. package/dist/connect.test.d.ts +2 -0
  6. package/dist/connect.test.d.ts.map +1 -0
  7. package/dist/connect.test.js +93 -0
  8. package/dist/connect.test.js.map +1 -0
  9. package/dist/deploy.d.ts.map +1 -1
  10. package/dist/deploy.js +52 -19
  11. package/dist/deploy.js.map +1 -1
  12. package/dist/deploy.test.js +132 -11
  13. package/dist/deploy.test.js.map +1 -1
  14. package/dist/index.d.ts +4 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/login.d.ts +41 -2
  19. package/dist/login.d.ts.map +1 -1
  20. package/dist/login.js +265 -6
  21. package/dist/login.js.map +1 -1
  22. package/dist/login.test.d.ts +2 -0
  23. package/dist/login.test.d.ts.map +1 -0
  24. package/dist/login.test.js +126 -0
  25. package/dist/login.test.js.map +1 -0
  26. package/dist/modes/cloud.d.ts +22 -14
  27. package/dist/modes/cloud.d.ts.map +1 -1
  28. package/dist/modes/cloud.js +635 -57
  29. package/dist/modes/cloud.js.map +1 -1
  30. package/dist/modes/cloud.test.d.ts +2 -0
  31. package/dist/modes/cloud.test.d.ts.map +1 -0
  32. package/dist/modes/cloud.test.js +519 -0
  33. package/dist/modes/cloud.test.js.map +1 -0
  34. package/dist/modes/input-values.test.js +17 -5
  35. package/dist/modes/input-values.test.js.map +1 -1
  36. package/dist/modes/sandbox.d.ts +1 -1
  37. package/dist/modes/sandbox.d.ts.map +1 -1
  38. package/dist/modes/sandbox.js +2 -2
  39. package/dist/modes/sandbox.js.map +1 -1
  40. package/dist/types.d.ts +27 -3
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +4 -3
@@ -1,77 +1,655 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { CloudApiClient, connectProvider, defaultApiUrl, readStoredAuth, refreshStoredAuth } from '@agent-relay/cloud';
3
+ import { resolveWorkspaceToken } from '../login.js';
4
+ const BUILD_YOUR_OWN_CLOUD_DOCS_URL = 'https://docs.agentworkforce.com/deploy/build-your-own-cloud';
5
+ const USER_AGENT = 'workforce-deploy';
6
+ const MAX_ATTEMPTS = 3;
7
+ const POLL_TIMEOUT_MS = 60_000;
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
+ }
2
30
  /**
3
- * Workforce-cloud-hosted deploy mode. Uploads the bundle to the workforce
4
- * cloud deployments endpoint and lets the cloud runtime host the agent.
5
- *
6
- * The endpoint (`POST /api/v1/workspaces/:id/deployments`) is part of the
7
- * proactive-runtime backend roadmap and is not yet live. Until it is,
8
- * `--mode cloud` returns a clean error that points users at the working
9
- * modes (`--mode sandbox` and `--mode dev`).
10
- *
11
- * When the endpoint ships, the implementation flow is:
12
- * 1. POST persona.json + agent.bundle.mjs + runner.mjs as multipart.
13
- * 2. Receive `{ deploymentId, statusUrl }`.
14
- * 3. Poll `statusUrl` until the cloud reports `running`.
15
- * 4. Return a handle whose `stop()` calls DELETE on the deployment.
31
+ * Cloud-hosted deploy mode. Uploads the deploy-ready persona bundle to a
32
+ * workforce-compatible cloud endpoint. The implementation is intentionally
33
+ * OSS-generic: callers may point at any compatible runtime with
34
+ * `--cloud-url`, `WORKFORCE_CLOUD_URL`, or `persona.cloud.deployUrl`; the
35
+ * production AgentRelay URL is only the final default.
16
36
  */
17
37
  export const cloudLauncher = {
18
38
  async launch(input) {
19
- const cloudUrl = (input.cloudUrl ?? process.env.WORKFORCE_CLOUD_URL)?.replace(/\/$/, '');
20
- const token = process.env.WORKFORCE_WORKSPACE_TOKEN;
21
- if (cloudUrl && token) {
22
- return postCloudDeployment(input, cloudUrl, token);
39
+ const cloudUrl = resolveCloudUrl(input);
40
+ const noPrompt = isNoPrompt(input);
41
+ const auth = input.workspaceToken
42
+ ? { token: input.workspaceToken }
43
+ : await resolveWorkspaceToken({
44
+ workspace: input.workspace,
45
+ cloudUrl,
46
+ io: input.io,
47
+ noPrompt
48
+ });
49
+ const credentialSelections = await ensureHarnessReady({
50
+ cloudUrl,
51
+ workspaceId: input.workspace,
52
+ token: auth.token,
53
+ persona: input.persona,
54
+ io: input.io,
55
+ noPrompt,
56
+ harnessSource: input.harnessSource,
57
+ byokKey: input.byokKey
58
+ });
59
+ const existingPersona = await handleExistingPersona({
60
+ cloudUrl,
61
+ workspaceId: input.workspace,
62
+ token: auth.token,
63
+ personaId: input.persona.id,
64
+ io: input.io,
65
+ noPrompt,
66
+ onExists: input.onExists
67
+ });
68
+ if (existingPersona.cancelled) {
69
+ return {
70
+ id: existingPersona.agentId,
71
+ agentId: existingPersona.agentId,
72
+ deploymentId: 'cancelled',
73
+ status: 'cancelled',
74
+ async stop() {
75
+ /* no-op: user chose not to change the existing hosted persona. */
76
+ },
77
+ done: Promise.resolve({ code: 0 })
78
+ };
23
79
  }
24
- throw new Error('--mode cloud is not yet available: the workforce cloud deployments endpoint is in progress. Use --mode sandbox (Daytona) or --mode dev (local) today.');
80
+ const endpoint = `${cloudUrl}/api/v1/workspaces/${encodeURIComponent(input.workspace)}/deployments`;
81
+ const body = JSON.stringify({
82
+ persona: input.persona,
83
+ bundle: {
84
+ runner: await readFile(input.bundle.runnerPath, 'utf8'),
85
+ agent: await readFile(input.bundle.bundlePath, 'utf8'),
86
+ packageJson: JSON.parse(await readFile(input.bundle.packageJsonPath, 'utf8'))
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,
92
+ inputs: input.inputs ?? readInputsOverride()
93
+ });
94
+ input.io.info(`cloud: deploying persona bundle to ${cloudUrl}`);
95
+ const deployBody = await requestJsonWithRetry(endpoint, {
96
+ method: 'POST',
97
+ headers: jsonHeaders(auth.token),
98
+ body
99
+ }, { action: 'cloud deploy' });
100
+ const agentId = expectString(deployBody.agentId, 'agentId');
101
+ const deploymentId = expectString(deployBody.deploymentId, 'deploymentId');
102
+ const initialStatus = expectStatus(deployBody.status);
103
+ input.io.info(`cloud: deployment ${deploymentId} created for agent ${agentId}`);
104
+ let stopping = false;
105
+ const done = (async () => {
106
+ if (initialStatus === 'active')
107
+ return { code: 0 };
108
+ if (initialStatus === 'failed')
109
+ return { code: 1 };
110
+ try {
111
+ const finalStatus = await pollAgentStatus({
112
+ cloudUrl,
113
+ workspaceId: input.workspace,
114
+ agentId,
115
+ token: auth.token,
116
+ io: input.io,
117
+ onLog: input.onLog
118
+ });
119
+ return { code: finalStatus === 'active' ? 0 : 1 };
120
+ }
121
+ catch (err) {
122
+ if (!stopping) {
123
+ input.io.error(`cloud: status polling failed: ${err instanceof Error ? err.message : String(err)}`);
124
+ }
125
+ return { code: 1 };
126
+ }
127
+ })();
128
+ const stop = async () => {
129
+ if (stopping)
130
+ return;
131
+ stopping = true;
132
+ await deleteAgent({
133
+ cloudUrl,
134
+ workspaceId: input.workspace,
135
+ agentId,
136
+ token: auth.token,
137
+ action: 'cloud stop'
138
+ });
139
+ };
140
+ return {
141
+ id: agentId,
142
+ agentId,
143
+ deploymentId,
144
+ status: initialStatus,
145
+ stop,
146
+ done
147
+ };
25
148
  }
26
149
  };
27
- const CLOUD_DEPLOY_TIMEOUT_MS = 30_000;
28
- async function postCloudDeployment(input, cloudUrl, workspaceToken) {
29
- const controller = new AbortController();
30
- const timeout = setTimeout(() => controller.abort(), CLOUD_DEPLOY_TIMEOUT_MS);
31
- let res;
32
- try {
33
- res = await fetch(`${cloudUrl}/api/v1/workspaces/${encodeURIComponent(input.workspace)}/deployments`, {
34
- method: 'POST',
35
- headers: {
36
- authorization: `Bearer ${workspaceToken}`,
37
- 'content-type': 'application/json'
38
- },
39
- body: JSON.stringify({
40
- persona: input.persona,
41
- bundle: {
42
- runner: await readFile(input.bundle.runnerPath, 'utf8'),
43
- agent: await readFile(input.bundle.bundlePath, 'utf8'),
44
- packageJson: JSON.parse(await readFile(input.bundle.packageJsonPath, 'utf8'))
45
- },
46
- ...(input.inputs && Object.keys(input.inputs).length > 0 ? { inputs: input.inputs } : {})
47
- }),
48
- signal: controller.signal
150
+ function resolveCloudUrl(input) {
151
+ const fromInput = input.cloudUrl?.trim();
152
+ const fromEnv = process.env.WORKFORCE_DEPLOY_CLOUD_URL?.trim()
153
+ || process.env.WORKFORCE_CLOUD_URL?.trim();
154
+ const fromPersona = readPersonaCloudDeployUrl(input.persona);
155
+ const raw = fromInput || fromEnv || fromPersona || defaultApiUrl();
156
+ const resolved = normalizeCloudUrl(raw);
157
+ if (resolved !== normalizeCloudUrl(defaultApiUrl())) {
158
+ input.io.info(`cloud: using custom cloud URL ${resolved}. Build your own cloud docs: ${BUILD_YOUR_OWN_CLOUD_DOCS_URL}`);
159
+ }
160
+ return resolved;
161
+ }
162
+ function isNoPrompt(input) {
163
+ if (input.noPrompt)
164
+ return true;
165
+ const raw = process.env.WORKFORCE_DEPLOY_NO_PROMPT?.trim().toLowerCase();
166
+ return raw === '1' || raw === 'true' || raw === 'yes';
167
+ }
168
+ async function ensureHarnessReady(args) {
169
+ const source = await resolveHarnessSource(args);
170
+ const modelProvider = deriveModelProvider(args.persona);
171
+ if (source === 'plan') {
172
+ const credentialId = await saveProviderCredential({
173
+ cloudUrl: args.cloudUrl,
174
+ workspaceId: args.workspaceId,
175
+ token: args.token,
176
+ modelProvider,
177
+ authType: 'relay_managed'
49
178
  });
179
+ args.io.info(`cloud: using workforce plan credentials for ${args.persona.harness}`);
180
+ return { [modelProvider]: credentialId };
50
181
  }
51
- catch (err) {
52
- if (err instanceof Error && err.name === 'AbortError') {
53
- throw new Error(`Cloud deploy failed: request timed out after ${CLOUD_DEPLOY_TIMEOUT_MS / 1000}s`);
54
- }
55
- throw err;
182
+ if (source === 'byok') {
183
+ const key = await resolveByokKey(args);
184
+ const credentialId = await saveProviderCredential({
185
+ cloudUrl: args.cloudUrl,
186
+ workspaceId: args.workspaceId,
187
+ token: args.token,
188
+ modelProvider,
189
+ authType: 'byo_api_key',
190
+ apiKey: key
191
+ });
192
+ args.io.info(`cloud: using BYOK credentials for ${args.persona.harness}`);
193
+ return { [modelProvider]: credentialId };
56
194
  }
57
- finally {
58
- clearTimeout(timeout);
195
+ await ensureHarnessOauth(args);
196
+ return {};
197
+ }
198
+ async function resolveHarnessSource(args) {
199
+ if (args.harnessSource)
200
+ return args.harnessSource;
201
+ const fromEnv = process.env.WORKFORCE_DEPLOY_HARNESS_SOURCE?.trim();
202
+ if (fromEnv)
203
+ return expectHarnessSource(fromEnv);
204
+ const available = await isHarnessOauthConnected(args);
205
+ if (available)
206
+ return 'oauth';
207
+ if (args.noPrompt) {
208
+ 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.`);
209
+ }
210
+ const answer = await args.io.prompt(`${args.persona.harness} credentials are not connected. Choose harness source (plan/byok/oauth)`, { defaultValue: 'plan' });
211
+ return expectHarnessSource(answer);
212
+ }
213
+ async function isHarnessOauthConnected(args) {
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, {
220
+ method: 'GET',
221
+ headers: { 'user-agent': USER_AGENT }
222
+ });
223
+ if (res.status === 404 || res.status === 405)
224
+ return false;
225
+ if (res.status === 401) {
226
+ throw new Error('cloud harness check failed: unauthorized. Run `workforce login` and retry.');
59
227
  }
60
228
  if (!res.ok) {
61
- throw new Error(`Cloud deploy failed: ${res.status} ${await res.text()}`);
229
+ throw new Error(`cloud harness check failed: ${res.status} ${await responseExcerpt(res)}`);
62
230
  }
63
231
  const body = (await res.json());
64
- const id = body.deploymentId ?? body.agentId;
65
- if (!id) {
66
- throw new Error(`Cloud deploy failed: response missing deploymentId/agentId`);
232
+ return providerCredentialsReady(body);
233
+ }
234
+ async function resolveByokKey(args) {
235
+ if (args.byokKey?.trim())
236
+ return args.byokKey.trim();
237
+ const fromEnv = process.env.WORKFORCE_DEPLOY_BYOK_KEY?.trim();
238
+ if (fromEnv)
239
+ return fromEnv;
240
+ if (args.noPrompt) {
241
+ throw new Error(`cloud: --harness-source byok requires --byok-key or WORKFORCE_DEPLOY_BYOK_KEY for ${args.persona.harness}`);
242
+ }
243
+ const answer = await args.io.prompt(`API key for ${args.persona.harness}`);
244
+ if (!answer.trim()) {
245
+ throw new Error(`cloud: missing BYOK API key for ${args.persona.harness}`);
246
+ }
247
+ return answer.trim();
248
+ }
249
+ async function ensureHarnessOauth(args) {
250
+ if (await isHarnessOauthConnected(args)) {
251
+ args.io.info(`cloud: ${args.persona.harness} credentials already connected`);
252
+ return;
67
253
  }
68
- input.io.info(`cloud: ${body.status ?? 'submitted'}`);
254
+ if (args.noPrompt) {
255
+ throw new Error(`cloud: ${args.persona.harness} OAuth credentials are not connected. Run without --no-prompt or choose --harness-source plan/byok.`);
256
+ }
257
+ const ok = await args.io.confirm(`Connect ${args.persona.harness} credentials now? (opens browser)`, { defaultValue: true });
258
+ if (!ok) {
259
+ throw new Error(`cloud: ${args.persona.harness} credentials are required for deploy`);
260
+ }
261
+ const modelProvider = deriveModelProvider(args.persona);
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(' '))
269
+ }
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`);
273
+ }
274
+ async function handleExistingPersona(args) {
275
+ const existing = await findExistingAgent(args);
276
+ if (!existing)
277
+ return { cancelled: false };
278
+ const choice = await resolveOnExists(args);
279
+ if (choice === 'cancel') {
280
+ args.io.info(`cloud: deploy cancelled because persona ${args.personaId} already exists`);
281
+ return { cancelled: true, agentId: existing.id };
282
+ }
283
+ if (choice === 'update') {
284
+ args.io.info(`cloud: updating existing persona ${args.personaId}`);
285
+ return { cancelled: false };
286
+ }
287
+ args.io.info(`cloud: destroying existing persona ${args.personaId} before deploy`);
288
+ await deleteAgent({
289
+ cloudUrl: args.cloudUrl,
290
+ workspaceId: args.workspaceId,
291
+ token: args.token,
292
+ agentId: existing.id,
293
+ action: 'cloud existing persona destroy'
294
+ });
295
+ return { cancelled: false };
296
+ }
297
+ async function findExistingAgent(args) {
298
+ const url = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/agents?persona_slug=${encodeURIComponent(args.personaId)}`;
299
+ const res = await fetch(url, {
300
+ method: 'GET',
301
+ headers: {
302
+ authorization: `Bearer ${args.token}`,
303
+ 'user-agent': USER_AGENT
304
+ }
305
+ });
306
+ if (res.status === 404 || res.status === 405)
307
+ return null;
308
+ if (res.status === 401) {
309
+ throw new Error('cloud existing persona check failed: unauthorized. Run `workforce login` and retry.');
310
+ }
311
+ if (!res.ok) {
312
+ throw new Error(`cloud existing persona check failed: ${res.status} ${await responseExcerpt(res)}`);
313
+ }
314
+ return parseExistingAgent((await res.json()));
315
+ }
316
+ async function resolveOnExists(args) {
317
+ if (args.onExists)
318
+ return args.onExists;
319
+ const fromEnv = process.env.WORKFORCE_DEPLOY_ON_EXISTS?.trim();
320
+ if (fromEnv)
321
+ return expectOnExistsChoice(fromEnv);
322
+ if (args.noPrompt) {
323
+ return 'cancel';
324
+ }
325
+ const answer = await args.io.prompt(`Persona ${args.personaId} already exists. Choose update, destroy, or cancel`, { defaultValue: 'cancel' });
326
+ return expectOnExistsChoice(answer);
327
+ }
328
+ async function deleteAgent(args) {
329
+ const destroyUrl = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/agents/${encodeURIComponent(args.agentId)}/destroy`;
330
+ const res = await fetch(destroyUrl, {
331
+ method: 'POST',
332
+ headers: {
333
+ authorization: `Bearer ${args.token}`,
334
+ 'user-agent': USER_AGENT
335
+ }
336
+ });
337
+ if (res.status === 401) {
338
+ throw new Error(`${args.action} failed: unauthorized. Run \`workforce login\` and retry.`);
339
+ }
340
+ if (res.status === 404 || res.status === 405) {
341
+ throw new Error(`${args.action} failed: destroy not yet wired; cancel and run with --force-replace later.`);
342
+ }
343
+ if (!res.ok) {
344
+ throw new Error(`${args.action} failed: ${res.status} ${await responseExcerpt(res)}`);
345
+ }
346
+ }
347
+ function parseExistingAgent(body) {
348
+ const direct = parseAgentLike(body.agent);
349
+ if (direct)
350
+ return direct;
351
+ if (Array.isArray(body.agents)) {
352
+ for (const agent of body.agents) {
353
+ const parsed = parseAgentLike(agent);
354
+ if (parsed)
355
+ return parsed;
356
+ }
357
+ }
358
+ return null;
359
+ }
360
+ function parseAgentLike(value) {
361
+ if (!value || typeof value !== 'object' || Array.isArray(value))
362
+ return null;
363
+ const record = value;
364
+ if (typeof record.id !== 'string' || !record.id.trim())
365
+ return null;
69
366
  return {
70
- id,
71
- async stop() {
72
- throw new Error('cloud deployment stop is not wired yet');
73
- },
74
- done: Promise.resolve({ code: body.status === 'failed' ? 1 : 0 })
367
+ id: record.id,
368
+ ...(typeof record.status === 'string' ? { status: record.status } : {})
75
369
  };
76
370
  }
371
+ async function saveProviderCredential(args) {
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`, {
382
+ method: 'POST',
383
+ headers: jsonHeaders(args.token),
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,
388
+ model_provider: args.modelProvider,
389
+ key: args.apiKey,
390
+ api_key: args.apiKey
391
+ })
392
+ }, { action: 'cloud BYOK provider credentials' });
393
+ return readCredentialId(body);
394
+ }
395
+ function deriveModelProvider(persona) {
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';
408
+ const [provider] = model.split(/[/:]/, 1);
409
+ if (provider?.trim())
410
+ return provider.trim().toLowerCase();
411
+ return persona.harness;
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
+ }
433
+ function providerCredentialsReady(body) {
434
+ const candidates = [
435
+ body.credential,
436
+ ...(Array.isArray(body.credentials) ? body.credentials : []),
437
+ ...(Array.isArray(body.providerCredentials) ? body.providerCredentials : []),
438
+ body
439
+ ];
440
+ return candidates.some((candidate) => {
441
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
442
+ return false;
443
+ const record = candidate;
444
+ return record.connected === true
445
+ || record.status === 'connected'
446
+ || record.status === 'active'
447
+ || Boolean(record.credentialStoredAt)
448
+ || Boolean(record.createdAt)
449
+ || typeof record.id === 'string';
450
+ });
451
+ }
452
+ function expectHarnessSource(value) {
453
+ const normalized = value.trim().toLowerCase();
454
+ if (normalized === 'plan' || normalized === 'byok' || normalized === 'oauth') {
455
+ return normalized;
456
+ }
457
+ throw new Error(`cloud: harness source must be one of plan|byok|oauth; got "${value}"`);
458
+ }
459
+ function expectOnExistsChoice(value) {
460
+ const normalized = value.trim().toLowerCase();
461
+ if (normalized === 'update' || normalized === 'destroy' || normalized === 'cancel') {
462
+ return normalized;
463
+ }
464
+ throw new Error(`cloud: on-exists must be one of update|destroy|cancel; got "${value}"`);
465
+ }
466
+ function readFirstString(body, fields) {
467
+ const record = body;
468
+ for (const field of fields) {
469
+ const value = record[field];
470
+ if (typeof value === 'string' && value.trim()) {
471
+ return value.trim();
472
+ }
473
+ }
474
+ return undefined;
475
+ }
476
+ async function pollUntil(check, timeoutMessage) {
477
+ const deadline = Date.now() + pollTimeoutMs();
478
+ while (Date.now() < deadline) {
479
+ if (await check())
480
+ return;
481
+ await sleep(pollIntervalMs());
482
+ }
483
+ throw new Error(timeoutMessage);
484
+ }
485
+ function readPersonaCloudDeployUrl(persona) {
486
+ const cloud = persona.cloud;
487
+ if (cloud !== null && typeof cloud === 'object' && 'deployUrl' in cloud) {
488
+ const deployUrl = cloud.deployUrl;
489
+ if (typeof deployUrl === 'string' && deployUrl.trim()) {
490
+ return deployUrl.trim();
491
+ }
492
+ }
493
+ return undefined;
494
+ }
495
+ function normalizeCloudUrl(url) {
496
+ const trimmed = url.trim();
497
+ if (!trimmed)
498
+ return normalizeCloudUrl(defaultApiUrl());
499
+ return trimmed.replace(/\/+$/, '');
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
+ }
525
+ function readInputsOverride() {
526
+ const raw = process.env.WORKFORCE_DEPLOY_INPUTS_JSON?.trim();
527
+ if (!raw)
528
+ return undefined;
529
+ let parsed;
530
+ try {
531
+ parsed = JSON.parse(raw);
532
+ }
533
+ catch {
534
+ throw new Error('WORKFORCE_DEPLOY_INPUTS_JSON is not valid JSON');
535
+ }
536
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
537
+ throw new Error('WORKFORCE_DEPLOY_INPUTS_JSON must be a JSON object');
538
+ }
539
+ const out = {};
540
+ for (const [key, value] of Object.entries(parsed)) {
541
+ if (typeof value !== 'string') {
542
+ throw new Error(`WORKFORCE_DEPLOY_INPUTS_JSON.${key} must be a string`);
543
+ }
544
+ out[key] = value;
545
+ }
546
+ return out;
547
+ }
548
+ async function pollAgentStatus(args) {
549
+ const statusUrl = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/agents/${encodeURIComponent(args.agentId)}`;
550
+ const deadline = Date.now() + pollTimeoutMs();
551
+ let lastStatus = 'starting';
552
+ while (Date.now() < deadline) {
553
+ await sleep(pollIntervalMs());
554
+ const body = await requestJsonWithRetry(statusUrl, {
555
+ method: 'GET',
556
+ headers: {
557
+ authorization: `Bearer ${args.token}`,
558
+ 'user-agent': USER_AGENT
559
+ }
560
+ }, { action: 'cloud status poll' });
561
+ const status = expectStatus(body.status);
562
+ if (status !== lastStatus) {
563
+ emitLog(args, `cloud: status ${status}`);
564
+ lastStatus = status;
565
+ }
566
+ if (status === 'active' || status === 'failed')
567
+ return status;
568
+ }
569
+ throw new Error(`timed out after ${pollTimeoutMs() / 1000}s waiting for agent ${args.agentId}`);
570
+ }
571
+ function jsonHeaders(token) {
572
+ return {
573
+ authorization: `Bearer ${token}`,
574
+ 'content-type': 'application/json',
575
+ 'user-agent': USER_AGENT
576
+ };
577
+ }
578
+ async function requestJsonWithRetry(url, init, opts) {
579
+ let lastError;
580
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
581
+ try {
582
+ const res = await fetch(url, init);
583
+ if (res.status === 401) {
584
+ throw new Error(`${opts.action} failed: unauthorized. Run \`workforce login\` and retry.`);
585
+ }
586
+ if (res.status >= 500 && attempt < MAX_ATTEMPTS) {
587
+ lastError = new Error(`${opts.action} failed: ${res.status} ${await responseExcerpt(res)}`);
588
+ }
589
+ else if (!res.ok) {
590
+ throw new Error(`${opts.action} failed: ${res.status} ${await responseExcerpt(res)}`);
591
+ }
592
+ else {
593
+ return (await res.json());
594
+ }
595
+ }
596
+ catch (err) {
597
+ lastError = err;
598
+ if (attempt === MAX_ATTEMPTS || !isRetryableError(err)) {
599
+ throw err;
600
+ }
601
+ }
602
+ await sleep(backoffMs(attempt));
603
+ }
604
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
605
+ }
606
+ function isRetryableError(err) {
607
+ if (!(err instanceof Error))
608
+ return false;
609
+ if (err.message.includes('unauthorized'))
610
+ return false;
611
+ if (err.message.includes('failed: 4'))
612
+ return false;
613
+ return true;
614
+ }
615
+ function backoffMs(attempt) {
616
+ const override = numberFromEnv('WORKFORCE_DEPLOY_RETRY_BACKOFF_MS');
617
+ return override ?? attempt * 500;
618
+ }
619
+ function sleep(ms) {
620
+ return new Promise((resolve) => setTimeout(resolve, ms));
621
+ }
622
+ function emitLog(args, line) {
623
+ args.onLog?.(line);
624
+ args.io.info(line);
625
+ }
626
+ function pollTimeoutMs() {
627
+ return numberFromEnv('WORKFORCE_DEPLOY_POLL_TIMEOUT_MS') ?? POLL_TIMEOUT_MS;
628
+ }
629
+ function pollIntervalMs() {
630
+ return numberFromEnv('WORKFORCE_DEPLOY_POLL_INTERVAL_MS') ?? POLL_INTERVAL_MS;
631
+ }
632
+ function numberFromEnv(name) {
633
+ const raw = process.env[name]?.trim();
634
+ if (!raw)
635
+ return undefined;
636
+ const parsed = Number(raw);
637
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
638
+ }
639
+ async function responseExcerpt(res) {
640
+ const text = await res.text().catch(() => '');
641
+ return text.trim().slice(0, 500);
642
+ }
643
+ function expectString(value, field) {
644
+ if (typeof value !== 'string' || !value.trim()) {
645
+ throw new Error(`cloud deploy response missing ${field}`);
646
+ }
647
+ return value;
648
+ }
649
+ function expectStatus(value) {
650
+ if (value === 'starting' || value === 'active' || value === 'failed' || value === 'cancelled') {
651
+ return value;
652
+ }
653
+ throw new Error(`cloud deploy response has unknown status "${String(value)}"`);
654
+ }
77
655
  //# sourceMappingURL=cloud.js.map