@agentworkforce/deploy 0.0.0 → 3.0.3

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 (43) hide show
  1. package/dist/bundle.js +19 -2
  2. package/dist/bundle.js.map +1 -1
  3. package/dist/bundle.test.js +40 -0
  4. package/dist/bundle.test.js.map +1 -1
  5. package/dist/deploy.d.ts.map +1 -1
  6. package/dist/deploy.js +52 -26
  7. package/dist/deploy.js.map +1 -1
  8. package/dist/deploy.test.js +126 -10
  9. package/dist/deploy.test.js.map +1 -1
  10. package/dist/index.d.ts +7 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +57 -3
  13. package/dist/index.js.map +1 -1
  14. package/dist/login.d.ts +30 -0
  15. package/dist/login.d.ts.map +1 -1
  16. package/dist/login.js +263 -0
  17. package/dist/login.js.map +1 -1
  18. package/dist/modes/cloud.d.ts +13 -14
  19. package/dist/modes/cloud.d.ts.map +1 -1
  20. package/dist/modes/cloud.js +717 -15
  21. package/dist/modes/cloud.js.map +1 -1
  22. package/dist/modes/cloud.test.d.ts +2 -0
  23. package/dist/modes/cloud.test.d.ts.map +1 -0
  24. package/dist/modes/cloud.test.js +506 -0
  25. package/dist/modes/cloud.test.js.map +1 -0
  26. package/dist/modes/dev.d.ts.map +1 -1
  27. package/dist/modes/dev.js +2 -0
  28. package/dist/modes/dev.js.map +1 -1
  29. package/dist/modes/input-values.test.d.ts +2 -0
  30. package/dist/modes/input-values.test.d.ts.map +1 -0
  31. package/dist/modes/input-values.test.js +242 -0
  32. package/dist/modes/input-values.test.js.map +1 -0
  33. package/dist/modes/sandbox.d.ts +1 -1
  34. package/dist/modes/sandbox.d.ts.map +1 -1
  35. package/dist/modes/sandbox.js +4 -2
  36. package/dist/modes/sandbox.js.map +1 -1
  37. package/dist/runtime-context.d.ts +3 -0
  38. package/dist/runtime-context.d.ts.map +1 -0
  39. package/dist/runtime-context.js +40 -0
  40. package/dist/runtime-context.js.map +1 -0
  41. package/dist/types.d.ts +28 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +8 -8
@@ -1,21 +1,723 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import { platform } from 'node:os';
6
+ import { resolveWorkspaceToken } from '../login.js';
7
+ const DEFAULT_CLOUD_URL = 'https://agentrelay.com';
8
+ const BUILD_YOUR_OWN_CLOUD_DOCS_URL = 'https://docs.agentworkforce.com/deploy/build-your-own-cloud';
9
+ const USER_AGENT = 'workforce-deploy';
10
+ const MAX_ATTEMPTS = 3;
11
+ const POLL_TIMEOUT_MS = 60_000;
12
+ const POLL_INTERVAL_MS = 2_000;
1
13
  /**
2
- * Workforce-cloud-hosted deploy mode. Uploads the bundle to the workforce
3
- * cloud deployments endpoint and lets the cloud runtime host the agent.
4
- *
5
- * The endpoint (`POST /api/v1/workspaces/:id/deployments`) is part of the
6
- * proactive-runtime backend roadmap and is not yet live. Until it is,
7
- * `--mode cloud` returns a clean error that points users at the working
8
- * modes (`--mode sandbox` and `--mode dev`).
9
- *
10
- * When the endpoint ships, the implementation flow is:
11
- * 1. POST persona.json + agent.bundle.mjs + runner.mjs as multipart.
12
- * 2. Receive `{ deploymentId, statusUrl }`.
13
- * 3. Poll `statusUrl` until the cloud reports `running`.
14
- * 4. Return a handle whose `stop()` calls DELETE on the deployment.
14
+ * Cloud-hosted deploy mode. Uploads the deploy-ready persona bundle to a
15
+ * workforce-compatible cloud endpoint. The implementation is intentionally
16
+ * OSS-generic: callers may point at any compatible runtime with
17
+ * `--cloud-url`, `WORKFORCE_CLOUD_URL`, or `persona.cloud.deployUrl`; the
18
+ * production AgentRelay URL is only the final default.
15
19
  */
16
20
  export const cloudLauncher = {
17
- async launch(_input) {
18
- 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.');
21
+ async launch(input) {
22
+ const cloudUrl = resolveCloudUrl(input);
23
+ const noPrompt = isNoPrompt(input);
24
+ const auth = input.workspaceToken
25
+ ? { token: input.workspaceToken }
26
+ : await resolveWorkspaceToken({
27
+ workspace: input.workspace,
28
+ cloudUrl,
29
+ io: input.io,
30
+ noPrompt
31
+ });
32
+ await ensureHarnessReady({
33
+ cloudUrl,
34
+ workspaceId: input.workspace,
35
+ token: auth.token,
36
+ persona: input.persona,
37
+ io: input.io,
38
+ noPrompt,
39
+ harnessSource: input.harnessSource,
40
+ byokKey: input.byokKey
41
+ });
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
+ const existingPersona = await handleExistingPersona({
51
+ cloudUrl,
52
+ workspaceId: input.workspace,
53
+ token: auth.token,
54
+ personaId: input.persona.id,
55
+ io: input.io,
56
+ noPrompt,
57
+ onExists: input.onExists
58
+ });
59
+ if (existingPersona.cancelled) {
60
+ return {
61
+ id: existingPersona.agentId,
62
+ agentId: existingPersona.agentId,
63
+ deploymentId: 'cancelled',
64
+ status: 'cancelled',
65
+ async stop() {
66
+ /* no-op: user chose not to change the existing hosted persona. */
67
+ },
68
+ done: Promise.resolve({ code: 0 })
69
+ };
70
+ }
71
+ const endpoint = `${cloudUrl}/api/v1/workspaces/${encodeURIComponent(input.workspace)}/deployments`;
72
+ const body = JSON.stringify({
73
+ persona: input.persona,
74
+ bundle: {
75
+ runner: await readFile(input.bundle.runnerPath, 'utf8'),
76
+ agent: await readFile(input.bundle.bundlePath, 'utf8'),
77
+ packageJson: JSON.parse(await readFile(input.bundle.packageJsonPath, 'utf8'))
78
+ },
79
+ inputs: input.inputs ?? readInputsOverride()
80
+ });
81
+ input.io.info(`cloud: deploying persona bundle to ${cloudUrl}`);
82
+ const deployBody = await requestJsonWithRetry(endpoint, {
83
+ method: 'POST',
84
+ headers: jsonHeaders(auth.token),
85
+ body
86
+ }, { action: 'cloud deploy' });
87
+ const agentId = expectString(deployBody.agentId, 'agentId');
88
+ const deploymentId = expectString(deployBody.deploymentId, 'deploymentId');
89
+ const initialStatus = expectStatus(deployBody.status);
90
+ input.io.info(`cloud: deployment ${deploymentId} created for agent ${agentId}`);
91
+ let stopping = false;
92
+ const done = (async () => {
93
+ if (initialStatus === 'active')
94
+ return { code: 0 };
95
+ if (initialStatus === 'failed')
96
+ return { code: 1 };
97
+ try {
98
+ const finalStatus = await pollAgentStatus({
99
+ cloudUrl,
100
+ workspaceId: input.workspace,
101
+ agentId,
102
+ token: auth.token,
103
+ io: input.io,
104
+ onLog: input.onLog
105
+ });
106
+ return { code: finalStatus === 'active' ? 0 : 1 };
107
+ }
108
+ catch (err) {
109
+ if (!stopping) {
110
+ input.io.error(`cloud: status polling failed: ${err instanceof Error ? err.message : String(err)}`);
111
+ }
112
+ return { code: 1 };
113
+ }
114
+ })();
115
+ const stop = async () => {
116
+ if (stopping)
117
+ return;
118
+ stopping = true;
119
+ await deleteAgent({
120
+ cloudUrl,
121
+ workspaceId: input.workspace,
122
+ agentId,
123
+ token: auth.token,
124
+ action: 'cloud stop'
125
+ });
126
+ };
127
+ return {
128
+ id: agentId,
129
+ agentId,
130
+ deploymentId,
131
+ status: initialStatus,
132
+ stop,
133
+ done
134
+ };
19
135
  }
20
136
  };
137
+ function resolveCloudUrl(input) {
138
+ const fromInput = input.cloudUrl?.trim();
139
+ const fromEnv = process.env.WORKFORCE_DEPLOY_CLOUD_URL?.trim()
140
+ || process.env.WORKFORCE_CLOUD_URL?.trim();
141
+ const fromPersona = readPersonaCloudDeployUrl(input.persona);
142
+ const raw = fromInput || fromEnv || fromPersona || DEFAULT_CLOUD_URL;
143
+ const resolved = normalizeCloudUrl(raw);
144
+ if (resolved !== DEFAULT_CLOUD_URL) {
145
+ input.io.info(`cloud: using custom cloud URL ${resolved}. Build your own cloud docs: ${BUILD_YOUR_OWN_CLOUD_DOCS_URL}`);
146
+ }
147
+ return resolved;
148
+ }
149
+ function isNoPrompt(input) {
150
+ if (input.noPrompt)
151
+ return true;
152
+ const raw = process.env.WORKFORCE_DEPLOY_NO_PROMPT?.trim().toLowerCase();
153
+ return raw === '1' || raw === 'true' || raw === 'yes';
154
+ }
155
+ async function ensureHarnessReady(args) {
156
+ const source = await resolveHarnessSource(args);
157
+ const modelProvider = deriveModelProvider(args.persona);
158
+ if (source === 'plan') {
159
+ await saveProviderCredential({
160
+ cloudUrl: args.cloudUrl,
161
+ token: args.token,
162
+ modelProvider,
163
+ authType: 'relay_managed'
164
+ });
165
+ args.io.info(`cloud: using workforce plan credentials for ${args.persona.harness}`);
166
+ return;
167
+ }
168
+ if (source === 'byok') {
169
+ const key = await resolveByokKey(args);
170
+ await saveProviderCredential({
171
+ cloudUrl: args.cloudUrl,
172
+ token: args.token,
173
+ modelProvider,
174
+ authType: 'byo_api_key',
175
+ apiKey: key
176
+ });
177
+ args.io.info(`cloud: using BYOK credentials for ${args.persona.harness}`);
178
+ return;
179
+ }
180
+ await ensureHarnessOauth(args);
181
+ }
182
+ async function resolveHarnessSource(args) {
183
+ if (args.harnessSource)
184
+ return args.harnessSource;
185
+ const fromEnv = process.env.WORKFORCE_DEPLOY_HARNESS_SOURCE?.trim();
186
+ if (fromEnv)
187
+ return expectHarnessSource(fromEnv);
188
+ const available = await isHarnessOauthConnected(args);
189
+ if (available)
190
+ return 'oauth';
191
+ if (args.noPrompt) {
192
+ 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.`);
193
+ }
194
+ const answer = await args.io.prompt(`${args.persona.harness} credentials are not connected. Choose harness source (plan/byok/oauth)`, { defaultValue: 'plan' });
195
+ return expectHarnessSource(answer);
196
+ }
197
+ 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, {
200
+ method: 'GET',
201
+ headers: {
202
+ authorization: `Bearer ${args.token}`,
203
+ 'user-agent': USER_AGENT
204
+ }
205
+ });
206
+ if (res.status === 404 || res.status === 405)
207
+ return false;
208
+ if (res.status === 401) {
209
+ throw new Error('cloud harness check failed: unauthorized. Run `workforce login` and retry.');
210
+ }
211
+ if (!res.ok) {
212
+ throw new Error(`cloud harness check failed: ${res.status} ${await responseExcerpt(res)}`);
213
+ }
214
+ const body = (await res.json());
215
+ return providerCredentialsReady(body);
216
+ }
217
+ async function resolveByokKey(args) {
218
+ if (args.byokKey?.trim())
219
+ return args.byokKey.trim();
220
+ const fromEnv = process.env.WORKFORCE_DEPLOY_BYOK_KEY?.trim();
221
+ if (fromEnv)
222
+ return fromEnv;
223
+ if (args.noPrompt) {
224
+ throw new Error(`cloud: --harness-source byok requires --byok-key or WORKFORCE_DEPLOY_BYOK_KEY for ${args.persona.harness}`);
225
+ }
226
+ const answer = await args.io.prompt(`API key for ${args.persona.harness}`);
227
+ if (!answer.trim()) {
228
+ throw new Error(`cloud: missing BYOK API key for ${args.persona.harness}`);
229
+ }
230
+ return answer.trim();
231
+ }
232
+ async function ensureHarnessOauth(args) {
233
+ if (await isHarnessOauthConnected(args)) {
234
+ args.io.info(`cloud: ${args.persona.harness} credentials already connected`);
235
+ return;
236
+ }
237
+ if (args.noPrompt) {
238
+ throw new Error(`cloud: ${args.persona.harness} OAuth credentials are not connected. Run without --no-prompt or choose --harness-source plan/byok.`);
239
+ }
240
+ const ok = await args.io.confirm(`Connect ${args.persona.harness} credentials now? (opens browser)`, { defaultValue: true });
241
+ if (!ok) {
242
+ throw new Error(`cloud: ${args.persona.harness} credentials are required for deploy`);
243
+ }
244
+ 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();
313
+ }
314
+ });
315
+ }
316
+ async function handleExistingPersona(args) {
317
+ const existing = await findExistingAgent(args);
318
+ if (!existing)
319
+ return { cancelled: false };
320
+ const choice = await resolveOnExists(args);
321
+ if (choice === 'cancel') {
322
+ args.io.info(`cloud: deploy cancelled because persona ${args.personaId} already exists`);
323
+ return { cancelled: true, agentId: existing.id };
324
+ }
325
+ if (choice === 'update') {
326
+ args.io.info(`cloud: updating existing persona ${args.personaId}`);
327
+ return { cancelled: false };
328
+ }
329
+ args.io.info(`cloud: destroying existing persona ${args.personaId} before deploy`);
330
+ await deleteAgent({
331
+ cloudUrl: args.cloudUrl,
332
+ workspaceId: args.workspaceId,
333
+ token: args.token,
334
+ agentId: existing.id,
335
+ action: 'cloud existing persona destroy'
336
+ });
337
+ return { cancelled: false };
338
+ }
339
+ async function findExistingAgent(args) {
340
+ const url = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/agents?persona_slug=${encodeURIComponent(args.personaId)}`;
341
+ const res = await fetch(url, {
342
+ method: 'GET',
343
+ headers: {
344
+ authorization: `Bearer ${args.token}`,
345
+ 'user-agent': USER_AGENT
346
+ }
347
+ });
348
+ if (res.status === 404 || res.status === 405)
349
+ return null;
350
+ if (res.status === 401) {
351
+ throw new Error('cloud existing persona check failed: unauthorized. Run `workforce login` and retry.');
352
+ }
353
+ if (!res.ok) {
354
+ throw new Error(`cloud existing persona check failed: ${res.status} ${await responseExcerpt(res)}`);
355
+ }
356
+ return parseExistingAgent((await res.json()));
357
+ }
358
+ async function resolveOnExists(args) {
359
+ if (args.onExists)
360
+ return args.onExists;
361
+ const fromEnv = process.env.WORKFORCE_DEPLOY_ON_EXISTS?.trim();
362
+ if (fromEnv)
363
+ return expectOnExistsChoice(fromEnv);
364
+ if (args.noPrompt) {
365
+ return 'cancel';
366
+ }
367
+ const answer = await args.io.prompt(`Persona ${args.personaId} already exists. Choose update, destroy, or cancel`, { defaultValue: 'cancel' });
368
+ return expectOnExistsChoice(answer);
369
+ }
370
+ async function deleteAgent(args) {
371
+ const destroyUrl = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/agents/${encodeURIComponent(args.agentId)}/destroy`;
372
+ const res = await fetch(destroyUrl, {
373
+ method: 'POST',
374
+ headers: {
375
+ authorization: `Bearer ${args.token}`,
376
+ 'user-agent': USER_AGENT
377
+ }
378
+ });
379
+ if (res.status === 401) {
380
+ throw new Error(`${args.action} failed: unauthorized. Run \`workforce login\` and retry.`);
381
+ }
382
+ if (res.status === 404 || res.status === 405) {
383
+ throw new Error(`${args.action} failed: destroy not yet wired; cancel and run with --force-replace later.`);
384
+ }
385
+ if (!res.ok) {
386
+ throw new Error(`${args.action} failed: ${res.status} ${await responseExcerpt(res)}`);
387
+ }
388
+ }
389
+ function parseExistingAgent(body) {
390
+ const direct = parseAgentLike(body.agent);
391
+ if (direct)
392
+ return direct;
393
+ if (Array.isArray(body.agents)) {
394
+ for (const agent of body.agents) {
395
+ const parsed = parseAgentLike(agent);
396
+ if (parsed)
397
+ return parsed;
398
+ }
399
+ }
400
+ return null;
401
+ }
402
+ function parseAgentLike(value) {
403
+ if (!value || typeof value !== 'object' || Array.isArray(value))
404
+ return null;
405
+ const record = value;
406
+ if (typeof record.id !== 'string' || !record.id.trim())
407
+ return null;
408
+ return {
409
+ id: record.id,
410
+ ...(typeof record.status === 'string' ? { status: record.status } : {})
411
+ };
412
+ }
413
+ async function saveProviderCredential(args) {
414
+ await requestJsonWithRetry(`${args.cloudUrl}/api/v1/users/me/provider_credentials`, {
415
+ method: 'POST',
416
+ headers: jsonHeaders(args.token),
417
+ body: JSON.stringify({
418
+ model_provider: args.modelProvider,
419
+ auth_type: args.authType,
420
+ ...(args.apiKey ? { api_key: args.apiKey } : {})
421
+ })
422
+ }, { action: 'cloud provider credentials save' });
423
+ }
424
+ function deriveModelProvider(persona) {
425
+ const model = typeof persona.model === 'string' ? persona.model.trim() : '';
426
+ const [provider] = model.split(/[/:]/, 1);
427
+ if (provider?.trim())
428
+ return provider.trim();
429
+ return persona.harness;
430
+ }
431
+ function providerCredentialsReady(body) {
432
+ const candidates = [
433
+ body.credential,
434
+ ...(Array.isArray(body.credentials) ? body.credentials : []),
435
+ ...(Array.isArray(body.providerCredentials) ? body.providerCredentials : []),
436
+ body
437
+ ];
438
+ return candidates.some((candidate) => {
439
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
440
+ return false;
441
+ const record = candidate;
442
+ return record.connected === true
443
+ || record.status === 'connected'
444
+ || record.status === 'active'
445
+ || Boolean(record.credentialStoredAt)
446
+ || Boolean(record.createdAt)
447
+ || typeof record.id === 'string';
448
+ });
449
+ }
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
+ function expectHarnessSource(value) {
532
+ const normalized = value.trim().toLowerCase();
533
+ if (normalized === 'plan' || normalized === 'byok' || normalized === 'oauth') {
534
+ return normalized;
535
+ }
536
+ throw new Error(`cloud: harness source must be one of plan|byok|oauth; got "${value}"`);
537
+ }
538
+ function expectOnExistsChoice(value) {
539
+ const normalized = value.trim().toLowerCase();
540
+ if (normalized === 'update' || normalized === 'destroy' || normalized === 'cancel') {
541
+ return normalized;
542
+ }
543
+ throw new Error(`cloud: on-exists must be one of update|destroy|cancel; got "${value}"`);
544
+ }
545
+ function readFirstString(body, fields) {
546
+ const record = body;
547
+ for (const field of fields) {
548
+ const value = record[field];
549
+ if (typeof value === 'string' && value.trim()) {
550
+ return value.trim();
551
+ }
552
+ }
553
+ return undefined;
554
+ }
555
+ async function pollUntil(check, timeoutMessage) {
556
+ const deadline = Date.now() + pollTimeoutMs();
557
+ while (Date.now() < deadline) {
558
+ if (await check())
559
+ return;
560
+ await sleep(pollIntervalMs());
561
+ }
562
+ throw new Error(timeoutMessage);
563
+ }
564
+ function readPersonaCloudDeployUrl(persona) {
565
+ const cloud = persona.cloud;
566
+ if (cloud !== null && typeof cloud === 'object' && 'deployUrl' in cloud) {
567
+ const deployUrl = cloud.deployUrl;
568
+ if (typeof deployUrl === 'string' && deployUrl.trim()) {
569
+ return deployUrl.trim();
570
+ }
571
+ }
572
+ return undefined;
573
+ }
574
+ function normalizeCloudUrl(url) {
575
+ const trimmed = url.trim();
576
+ if (!trimmed)
577
+ return DEFAULT_CLOUD_URL;
578
+ return trimmed.replace(/\/+$/, '');
579
+ }
580
+ function readInputsOverride() {
581
+ const raw = process.env.WORKFORCE_DEPLOY_INPUTS_JSON?.trim();
582
+ if (!raw)
583
+ return undefined;
584
+ let parsed;
585
+ try {
586
+ parsed = JSON.parse(raw);
587
+ }
588
+ catch {
589
+ throw new Error('WORKFORCE_DEPLOY_INPUTS_JSON is not valid JSON');
590
+ }
591
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
592
+ throw new Error('WORKFORCE_DEPLOY_INPUTS_JSON must be a JSON object');
593
+ }
594
+ const out = {};
595
+ for (const [key, value] of Object.entries(parsed)) {
596
+ if (typeof value !== 'string') {
597
+ throw new Error(`WORKFORCE_DEPLOY_INPUTS_JSON.${key} must be a string`);
598
+ }
599
+ out[key] = value;
600
+ }
601
+ return out;
602
+ }
603
+ async function pollAgentStatus(args) {
604
+ const statusUrl = `${args.cloudUrl}/api/v1/workspaces/${encodeURIComponent(args.workspaceId)}/agents/${encodeURIComponent(args.agentId)}`;
605
+ const deadline = Date.now() + pollTimeoutMs();
606
+ let lastStatus = 'starting';
607
+ while (Date.now() < deadline) {
608
+ await sleep(pollIntervalMs());
609
+ const body = await requestJsonWithRetry(statusUrl, {
610
+ method: 'GET',
611
+ headers: {
612
+ authorization: `Bearer ${args.token}`,
613
+ 'user-agent': USER_AGENT
614
+ }
615
+ }, { action: 'cloud status poll' });
616
+ const status = expectStatus(body.status);
617
+ if (status !== lastStatus) {
618
+ emitLog(args, `cloud: status ${status}`);
619
+ lastStatus = status;
620
+ }
621
+ if (status === 'active' || status === 'failed')
622
+ return status;
623
+ }
624
+ throw new Error(`timed out after ${pollTimeoutMs() / 1000}s waiting for agent ${args.agentId}`);
625
+ }
626
+ function jsonHeaders(token) {
627
+ return {
628
+ authorization: `Bearer ${token}`,
629
+ 'content-type': 'application/json',
630
+ 'user-agent': USER_AGENT
631
+ };
632
+ }
633
+ async function requestJsonWithRetry(url, init, opts) {
634
+ let lastError;
635
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
636
+ try {
637
+ const res = await fetch(url, init);
638
+ if (res.status === 401) {
639
+ throw new Error(`${opts.action} failed: unauthorized. Run \`workforce login\` and retry.`);
640
+ }
641
+ if (res.status >= 500 && attempt < MAX_ATTEMPTS) {
642
+ lastError = new Error(`${opts.action} failed: ${res.status} ${await responseExcerpt(res)}`);
643
+ }
644
+ else if (!res.ok) {
645
+ throw new Error(`${opts.action} failed: ${res.status} ${await responseExcerpt(res)}`);
646
+ }
647
+ else {
648
+ return (await res.json());
649
+ }
650
+ }
651
+ catch (err) {
652
+ lastError = err;
653
+ if (attempt === MAX_ATTEMPTS || !isRetryableError(err)) {
654
+ throw err;
655
+ }
656
+ }
657
+ await sleep(backoffMs(attempt));
658
+ }
659
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
660
+ }
661
+ function isRetryableError(err) {
662
+ if (!(err instanceof Error))
663
+ return false;
664
+ if (err.message.includes('unauthorized'))
665
+ return false;
666
+ if (err.message.includes('failed: 4'))
667
+ return false;
668
+ return true;
669
+ }
670
+ function backoffMs(attempt) {
671
+ const override = numberFromEnv('WORKFORCE_DEPLOY_RETRY_BACKOFF_MS');
672
+ return override ?? attempt * 500;
673
+ }
674
+ function sleep(ms) {
675
+ return new Promise((resolve) => setTimeout(resolve, ms));
676
+ }
677
+ function emitLog(args, line) {
678
+ args.onLog?.(line);
679
+ args.io.info(line);
680
+ }
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
+ function pollTimeoutMs() {
695
+ return numberFromEnv('WORKFORCE_DEPLOY_POLL_TIMEOUT_MS') ?? POLL_TIMEOUT_MS;
696
+ }
697
+ function pollIntervalMs() {
698
+ return numberFromEnv('WORKFORCE_DEPLOY_POLL_INTERVAL_MS') ?? POLL_INTERVAL_MS;
699
+ }
700
+ function numberFromEnv(name) {
701
+ const raw = process.env[name]?.trim();
702
+ if (!raw)
703
+ return undefined;
704
+ const parsed = Number(raw);
705
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
706
+ }
707
+ async function responseExcerpt(res) {
708
+ const text = await res.text().catch(() => '');
709
+ return text.trim().slice(0, 500);
710
+ }
711
+ function expectString(value, field) {
712
+ if (typeof value !== 'string' || !value.trim()) {
713
+ throw new Error(`cloud deploy response missing ${field}`);
714
+ }
715
+ return value;
716
+ }
717
+ function expectStatus(value) {
718
+ if (value === 'starting' || value === 'active' || value === 'failed' || value === 'cancelled') {
719
+ return value;
720
+ }
721
+ throw new Error(`cloud deploy response has unknown status "${String(value)}"`);
722
+ }
21
723
  //# sourceMappingURL=cloud.js.map