@viewportai/daemon 0.27.0 → 0.28.0

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 (102) hide show
  1. package/README.md +5 -5
  2. package/dist/adapters/claude.d.ts +1 -0
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +1 -1
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex.d.ts +3 -0
  7. package/dist/adapters/codex.d.ts.map +1 -1
  8. package/dist/adapters/codex.js +298 -32
  9. package/dist/adapters/codex.js.map +1 -1
  10. package/dist/agents/codex-defaults.d.ts.map +1 -1
  11. package/dist/agents/codex-defaults.js +1 -1
  12. package/dist/agents/codex-defaults.js.map +1 -1
  13. package/dist/agents/codex.d.ts.map +1 -1
  14. package/dist/agents/codex.js +8 -2
  15. package/dist/agents/codex.js.map +1 -1
  16. package/dist/cli/commands.d.ts +1 -0
  17. package/dist/cli/commands.d.ts.map +1 -1
  18. package/dist/cli/commands.js +1 -0
  19. package/dist/cli/commands.js.map +1 -1
  20. package/dist/cli/lifecycle-pair-command.js +1 -1
  21. package/dist/cli/lifecycle-pair-command.js.map +1 -1
  22. package/dist/cli/managed-session-verification-contract.d.ts +46 -0
  23. package/dist/cli/managed-session-verification-contract.d.ts.map +1 -0
  24. package/dist/cli/managed-session-verification-contract.js +2 -0
  25. package/dist/cli/managed-session-verification-contract.js.map +1 -0
  26. package/dist/cli/signal-command.d.ts +2 -0
  27. package/dist/cli/signal-command.d.ts.map +1 -0
  28. package/dist/cli/signal-command.js +258 -0
  29. package/dist/cli/signal-command.js.map +1 -0
  30. package/dist/cli/worker-command.js +6 -4
  31. package/dist/cli/worker-command.js.map +1 -1
  32. package/dist/cli/worker-runtime.d.ts +14 -1
  33. package/dist/cli/worker-runtime.d.ts.map +1 -1
  34. package/dist/cli/worker-runtime.js +824 -55
  35. package/dist/cli/worker-runtime.js.map +1 -1
  36. package/dist/cli/workflow-commands.d.ts.map +1 -1
  37. package/dist/cli/workflow-commands.js +1 -6
  38. package/dist/cli/workflow-commands.js.map +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +6 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/server/http-request-schemas.d.ts +1 -0
  44. package/dist/server/http-request-schemas.d.ts.map +1 -1
  45. package/dist/server/http-request-schemas.js +1 -0
  46. package/dist/server/http-request-schemas.js.map +1 -1
  47. package/dist/server/http-server.d.ts.map +1 -1
  48. package/dist/server/http-server.js +1 -0
  49. package/dist/server/http-server.js.map +1 -1
  50. package/dist/workflows/context-node-resolver.d.ts +1 -0
  51. package/dist/workflows/context-node-resolver.d.ts.map +1 -1
  52. package/dist/workflows/context-node-resolver.js +265 -2
  53. package/dist/workflows/context-node-resolver.js.map +1 -1
  54. package/dist/workflows/daemon-session.js +16 -1
  55. package/dist/workflows/daemon-session.js.map +1 -1
  56. package/dist/workflows/event-types.d.ts +1 -1
  57. package/dist/workflows/event-types.d.ts.map +1 -1
  58. package/dist/workflows/node-executor.js +2 -1
  59. package/dist/workflows/node-executor.js.map +1 -1
  60. package/dist/workflows/platform-context-client.d.ts +12 -27
  61. package/dist/workflows/platform-context-client.d.ts.map +1 -1
  62. package/dist/workflows/platform-context-client.internal.d.ts +23 -0
  63. package/dist/workflows/platform-context-client.internal.d.ts.map +1 -0
  64. package/dist/workflows/platform-context-client.internal.js +124 -0
  65. package/dist/workflows/platform-context-client.internal.js.map +1 -0
  66. package/dist/workflows/platform-context-client.js +102 -58
  67. package/dist/workflows/platform-context-client.js.map +1 -1
  68. package/dist/workflows/platform-context-client.types.d.ts +41 -0
  69. package/dist/workflows/platform-context-client.types.d.ts.map +1 -0
  70. package/dist/workflows/platform-context-client.types.js +2 -0
  71. package/dist/workflows/platform-context-client.types.js.map +1 -0
  72. package/dist/workflows/run-types.d.ts +2 -0
  73. package/dist/workflows/run-types.d.ts.map +1 -1
  74. package/dist/workflows/runner.d.ts.map +1 -1
  75. package/dist/workflows/runner.js +1 -0
  76. package/dist/workflows/runner.js.map +1 -1
  77. package/dist/workflows/workflow-production-schema.d.ts +1 -0
  78. package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
  79. package/dist/workflows/workflow-production-schema.js +1 -0
  80. package/dist/workflows/workflow-production-schema.js.map +1 -1
  81. package/dist/workflows/workflow-production-types.d.ts +1 -0
  82. package/dist/workflows/workflow-production-types.d.ts.map +1 -1
  83. package/dist/workflows/workflow-schema.d.ts +1 -0
  84. package/dist/workflows/workflow-schema.d.ts.map +1 -1
  85. package/package.json +2 -1
  86. package/schemas/workflow-v1.schema.json +4 -0
  87. package/dist/cli/workflow-managed-worker-format.d.ts +0 -13
  88. package/dist/cli/workflow-managed-worker-format.d.ts.map +0 -1
  89. package/dist/cli/workflow-managed-worker-format.js +0 -158
  90. package/dist/cli/workflow-managed-worker-format.js.map +0 -1
  91. package/dist/cli/workflow-managed-worker-types.d.ts +0 -137
  92. package/dist/cli/workflow-managed-worker-types.d.ts.map +0 -1
  93. package/dist/cli/workflow-managed-worker-types.js +0 -2
  94. package/dist/cli/workflow-managed-worker-types.js.map +0 -1
  95. package/dist/cli/workflow-managed-worker-util.d.ts +0 -6
  96. package/dist/cli/workflow-managed-worker-util.d.ts.map +0 -1
  97. package/dist/cli/workflow-managed-worker-util.js +0 -31
  98. package/dist/cli/workflow-managed-worker-util.js.map +0 -1
  99. package/dist/cli/workflow-managed-worker.d.ts +0 -2
  100. package/dist/cli/workflow-managed-worker.d.ts.map +0 -1
  101. package/dist/cli/workflow-managed-worker.js +0 -2036
  102. package/dist/cli/workflow-managed-worker.js.map +0 -1
@@ -1,7 +1,10 @@
1
1
  import crypto from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
2
3
  import { constants as fsConstants } from 'node:fs';
3
4
  import fs from 'node:fs/promises';
5
+ import os from 'node:os';
4
6
  import path from 'node:path';
7
+ import { WebSocket } from 'ws';
5
8
  import YAML from 'yaml';
6
9
  import { ConfigManager } from '../core/config.js';
7
10
  import { Daemon } from '../core/daemon.js';
@@ -13,6 +16,43 @@ import { WorkflowRunStore } from '../workflows/store.js';
13
16
  import { transportFetch } from './network.js';
14
17
  import { acquireWorkerProcessLock } from './worker-process-lock.js';
15
18
  import { readWorkerPairingRecord, workerProfileIntegrity, } from './worker-profile.js';
19
+ function runtimeContextTargetIdValue(primary, fallback) {
20
+ return stringValue(primary?.['runtime_context_target_id'] ??
21
+ primary?.['runtimeContextTargetId'] ??
22
+ primary?.['runtime_target_id'] ??
23
+ primary?.['runtimeTargetId'] ??
24
+ fallback?.['runtime_context_target_id'] ??
25
+ fallback?.['runtimeContextTargetId'] ??
26
+ fallback?.['runtime_target_id'] ??
27
+ fallback?.['runtimeTargetId']);
28
+ }
29
+ function hostedRuntimeContextTargetId(profile, lease) {
30
+ return (lease.runtimeTargetId ??
31
+ (profile.managedExecutorId ? `managed_executor:${profile.managedExecutorId}` : undefined));
32
+ }
33
+ function hostedWorkflowInputs(profile, lease) {
34
+ const base = { ...(lease.inputSnapshot ?? {}) };
35
+ const runtimeTargetId = hostedRuntimeContextTargetId(profile, lease);
36
+ if (!profile.serverUrl ||
37
+ !profile.workspaceId ||
38
+ !lease.assignmentClaimToken ||
39
+ !runtimeTargetId) {
40
+ return Object.keys(base).length > 0 ? base : undefined;
41
+ }
42
+ const viewport = isWorkflowInputRecord(base['viewport']) ? { ...base['viewport'] } : {};
43
+ viewport['runtimeContextTarget'] = {
44
+ schema: 'viewport.runtime_context_target/v1',
45
+ serverUrl: profile.serverUrl,
46
+ workspaceId: profile.workspaceId,
47
+ runtimeTargetId,
48
+ credential: lease.assignmentClaimToken,
49
+ };
50
+ base['viewport'] = viewport;
51
+ return base;
52
+ }
53
+ function isWorkflowInputRecord(value) {
54
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
55
+ }
16
56
  class HttpPollingTransport {
17
57
  profile;
18
58
  mode;
@@ -39,9 +79,145 @@ class HttpPollingTransport {
39
79
  return fetchHostedAssignmentHttp(this.profile, lease);
40
80
  }
41
81
  }
82
+ class RelayWorkerTransport {
83
+ profile;
84
+ mode = 'relay';
85
+ ws = null;
86
+ pending = new Map();
87
+ constructor(profile) {
88
+ this.profile = profile;
89
+ }
90
+ async claim(body) {
91
+ return claimLeaseHttp(this.profile, body, (request) => this.dispatchHostedManagedExecutorRequest(request));
92
+ }
93
+ async heartbeat(options) {
94
+ return heartbeatHttp(this.profile, {
95
+ ...options,
96
+ transport: 'relay',
97
+ }, (request) => this.dispatchHostedManagedExecutorRequest(request));
98
+ }
99
+ async sync(lease, execution) {
100
+ return syncLeaseHttp(this.profile, lease, execution, (request) => this.dispatchHostedManagedExecutorRequest(request));
101
+ }
102
+ async cleanup(lease) {
103
+ return cleanupLeaseHttp(this.profile, lease);
104
+ }
105
+ async pollRuntimeCommands(lease) {
106
+ return fetchHostedAssignmentHttp(this.profile, lease, (request) => this.dispatchHostedManagedExecutorRequest(request));
107
+ }
108
+ async dispatchHostedManagedExecutorRequest(request) {
109
+ const ws = await this.connection();
110
+ const requestId = crypto.randomUUID();
111
+ const frame = {
112
+ type: 'viewport.worker_transport.request/v1',
113
+ requestId,
114
+ method: request.method,
115
+ path: request.requestPath,
116
+ headers: request.headers,
117
+ body: request.serialized,
118
+ };
119
+ return new Promise((resolve, reject) => {
120
+ const timeout = setTimeout(() => {
121
+ this.pending.delete(requestId);
122
+ reject(new Error(`Relay worker transport request ${request.path} timed out.`));
123
+ }, relayWorkerRequestTimeoutMs());
124
+ this.pending.set(requestId, { resolve, reject, timeout });
125
+ ws.send(JSON.stringify(frame), (error) => {
126
+ if (error) {
127
+ clearTimeout(timeout);
128
+ this.pending.delete(requestId);
129
+ reject(error);
130
+ }
131
+ });
132
+ });
133
+ }
134
+ async connection() {
135
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
136
+ return this.ws;
137
+ }
138
+ if (!isHostedManagedExecutorProfile(this.profile)) {
139
+ throw new Error('Relay worker transport requires a hosted managed executor profile.');
140
+ }
141
+ const tokenResponse = await hostedManagedExecutorFetch(this.profile, 'POST', 'relay-token', {
142
+ credential: this.profile.credential,
143
+ ttl_seconds: 3600,
144
+ });
145
+ const tokenPayload = (await tokenResponse.json());
146
+ const token = stringValue(tokenPayload['relayToken']);
147
+ const claims = recordValue(tokenPayload['claims']);
148
+ const claimRelayWsBaseUrl = stringValue(claims?.['relayWsBaseUrl']);
149
+ if (!token) {
150
+ throw new Error('Relay worker transport token response did not include a relay token.');
151
+ }
152
+ const relayWsBaseUrl = this.profile.relayWsBaseUrl ??
153
+ process.env['VIEWPORT_RELAY_WS_BASE_URL'] ??
154
+ process.env['VPD_RELAY_WS_BASE_URL'] ??
155
+ claimRelayWsBaseUrl ??
156
+ relayWsBaseUrlFromServerUrl(this.profile.serverUrl);
157
+ const url = new URL(relayWsBaseUrl);
158
+ url.searchParams.set('role', 'worker');
159
+ url.searchParams.set('workspaceId', this.profile.workspaceId);
160
+ const ws = new WebSocket(url, {
161
+ headers: {
162
+ Authorization: `Bearer ${token}`,
163
+ },
164
+ });
165
+ await new Promise((resolve, reject) => {
166
+ const timeout = setTimeout(() => reject(new Error('Relay worker transport connection timed out.')), relayWorkerConnectionTimeoutMs());
167
+ ws.once('open', () => {
168
+ clearTimeout(timeout);
169
+ resolve();
170
+ });
171
+ ws.once('error', (error) => {
172
+ clearTimeout(timeout);
173
+ reject(error);
174
+ });
175
+ });
176
+ ws.on('message', (raw) => {
177
+ this.handleMessage(raw.toString('utf8'));
178
+ });
179
+ ws.on('close', () => {
180
+ for (const [requestId, pending] of this.pending.entries()) {
181
+ clearTimeout(pending.timeout);
182
+ pending.reject(new Error('Relay worker transport connection closed.'));
183
+ this.pending.delete(requestId);
184
+ }
185
+ this.ws = null;
186
+ });
187
+ this.ws = ws;
188
+ return ws;
189
+ }
190
+ handleMessage(text) {
191
+ let frame;
192
+ try {
193
+ const parsed = JSON.parse(text);
194
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
195
+ return;
196
+ frame = parsed;
197
+ }
198
+ catch {
199
+ return;
200
+ }
201
+ if (frame['type'] !== 'viewport.worker_transport.response/v1')
202
+ return;
203
+ const requestId = stringValue(frame['requestId']);
204
+ if (!requestId)
205
+ return;
206
+ const pending = this.pending.get(requestId);
207
+ if (!pending)
208
+ return;
209
+ this.pending.delete(requestId);
210
+ clearTimeout(pending.timeout);
211
+ const status = typeof frame['status'] === 'number' ? frame['status'] : 502;
212
+ const headers = recordValue(frame['headers']);
213
+ const body = typeof frame['body'] === 'string' ? frame['body'] : '';
214
+ const responseBody = status === 204 || status === 205 || status === 304 ? null : body;
215
+ pending.resolve(new Response(responseBody, { status, headers }));
216
+ }
217
+ }
42
218
  const DEFAULT_HOSTED_LEASE_SECONDS = 1_800;
43
219
  export async function runStandaloneWorker(options) {
44
- const bootstrap = await loadWorkerRuntimeBootstrap(options.bootstrapPath);
220
+ const bootstrap = await loadWorkerRuntimeBootstrap(options.bootstrapPath, options.registrationProfilePath);
45
221
  const profile = bootstrap.profile;
46
222
  let processLock = null;
47
223
  try {
@@ -50,10 +226,9 @@ export async function runStandaloneWorker(options) {
50
226
  if (transport === 'inbound') {
51
227
  validateInboundWorkerGate(profile);
52
228
  }
53
- if (transport === 'relay') {
54
- throw new Error('Relay worker transport is not supported by the standalone runtime yet.');
55
- }
56
- const workerTransport = new HttpPollingTransport(profile, transport);
229
+ const workerTransport = transport === 'relay'
230
+ ? new RelayWorkerTransport(profile)
231
+ : new HttpPollingTransport(profile, transport);
57
232
  processLock =
58
233
  options.lifecycle === 'persistent' && !options.once
59
234
  ? acquireWorkerProcessLock({
@@ -129,6 +304,7 @@ export async function runStandaloneWorker(options) {
129
304
  else {
130
305
  await workerTransport.sync(lease, execution);
131
306
  }
307
+ await maybeExecuteHostedSessionVerification(profile, workerTransport, lease, execution);
132
308
  if (execution.status === 'completed') {
133
309
  result.completed += 1;
134
310
  }
@@ -161,6 +337,7 @@ export async function runStandaloneWorker(options) {
161
337
  async function executeBootstrapLease(profile, transport, lease) {
162
338
  const execution = await executeClaim(profile, transport, lease);
163
339
  await transport.sync(lease, execution);
340
+ await maybeExecuteHostedSessionVerification(profile, transport, lease, execution);
164
341
  await transport.cleanup(lease);
165
342
  return {
166
343
  claimed: 1,
@@ -198,14 +375,209 @@ async function executeClaim(profile, transport, lease) {
198
375
  if (!isHostedManagedExecutorProfile(profile)) {
199
376
  return { status: 'completed' };
200
377
  }
201
- return executeHostedWorkflowClaim(profile, transport, lease);
378
+ return withGatewayLeaseProcessEnv(lease, () => executeHostedWorkflowClaim(profile, transport, lease));
202
379
  }
203
- async function loadWorkerRuntimeBootstrap(bootstrapPath) {
380
+ async function loadWorkerRuntimeBootstrap(bootstrapPath, registrationProfilePath) {
204
381
  if (bootstrapPath) {
205
382
  return loadSandboxBootstrap(bootstrapPath);
206
383
  }
384
+ if (registrationProfilePath) {
385
+ return {
386
+ profile: await loadManagedExecutorRuntimeProfile(registrationProfilePath),
387
+ };
388
+ }
207
389
  return { profile: await loadWorkerRuntimeProfile() };
208
390
  }
391
+ async function loadManagedExecutorRuntimeProfile(profilePath) {
392
+ const profile = await readManagedExecutorRegistrationProfile(profilePath);
393
+ const credential = await credentialFromManagedExecutorProfile(profile);
394
+ const missing = [];
395
+ if (!profile.serverUrl)
396
+ missing.push('server URL');
397
+ if (!profile.workspaceId)
398
+ missing.push('workspace id');
399
+ if (!profile.executorId)
400
+ missing.push('managed executor id');
401
+ if (!credential)
402
+ missing.push('managed executor credential');
403
+ if (missing.length > 0) {
404
+ throw new Error(`Managed executor registration profile is missing ${missing.join(', ')}.`);
405
+ }
406
+ const identity = await ensureManagedExecutorIdentity(profile.identityKeyPath ??
407
+ managedExecutorIdentityPath(profile.workspaceId, profile.executorId));
408
+ const workspaceRoot = managedExecutorWorkspaceRoot(profile);
409
+ await fs.mkdir(workspaceRoot, { recursive: true, mode: 0o700 });
410
+ const runnerPool = stringValue(profile.capabilities?.['runner_pool']) ??
411
+ stringValue(profile.capabilities?.['runnerPool']) ??
412
+ profile.runnerProfile;
413
+ const capabilities = {
414
+ ...(profile.capabilities ?? {}),
415
+ ...(runnerPool ? { runner_pool: runnerPool } : {}),
416
+ ...(profile.runnerPosture ? { runner_posture: profile.runnerPosture } : {}),
417
+ };
418
+ return {
419
+ serverUrl: profile.serverUrl.replace(/\/+$/, ''),
420
+ serverId: profile.serverId,
421
+ lifecycle: 'persistent',
422
+ transport: workerTransportValue(profile.accessMode) ?? 'polling',
423
+ workspaceId: profile.workspaceId,
424
+ managedExecutorId: profile.executorId,
425
+ credential: credential,
426
+ workspaceRoot,
427
+ identityKeyPath: identity.path,
428
+ publicKeyFingerprint: identity.publicKeyFingerprint,
429
+ capabilities,
430
+ };
431
+ }
432
+ async function readManagedExecutorRegistrationProfile(profilePath) {
433
+ const resolved = resolveProfilePath(profilePath);
434
+ const parsed = JSON.parse(await fs.readFile(resolved, 'utf8'));
435
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
436
+ throw new Error(`Managed executor registration profile is not a JSON object: ${resolved}`);
437
+ }
438
+ const record = parsed;
439
+ const schema = stringValue(record['schema']);
440
+ if (schema && schema !== 'viewport.managed_executor_registration/v1') {
441
+ throw new Error(`Unsupported managed executor registration profile schema: ${schema}`);
442
+ }
443
+ const daemon = recordValue(record['daemon']);
444
+ const worker = recordValue(daemon?.['worker']);
445
+ return {
446
+ serverUrl: stringValue(record['server_url']) ??
447
+ stringValue(record['serverUrl']) ??
448
+ stringValue(worker?.['serverUrl']) ??
449
+ stringValue(worker?.['server_url']),
450
+ serverId: stringValue(record['server_id']) ??
451
+ stringValue(record['serverId']) ??
452
+ stringValue(record['control_plane_id']) ??
453
+ stringValue(worker?.['serverId']) ??
454
+ stringValue(worker?.['server_id']) ??
455
+ stringValue(worker?.['control_plane_id']),
456
+ workspaceId: stringValue(record['workspace_id']) ??
457
+ stringValue(record['workspaceId']) ??
458
+ stringValue(worker?.['workspaceId']) ??
459
+ stringValue(worker?.['workspace_id']),
460
+ executorId: stringValue(record['managed_executor_id']) ??
461
+ stringValue(record['executor_id']) ??
462
+ stringValue(record['executorId']) ??
463
+ stringValue(worker?.['managedExecutorId']) ??
464
+ stringValue(worker?.['managed_executor_id']),
465
+ credential: stringValue(record['credential']) ?? stringValue(worker?.['credential']),
466
+ credentialFile: stringValue(record['credential_file']) ??
467
+ stringValue(record['credentialFile']) ??
468
+ stringValue(worker?.['credentialFile']) ??
469
+ stringValue(worker?.['credential_file']),
470
+ accessMode: stringValue(record['access_mode']) ??
471
+ stringValue(record['accessMode']) ??
472
+ stringValue(worker?.['accessMode']) ??
473
+ stringValue(worker?.['access_mode']) ??
474
+ stringValue(worker?.['transport']),
475
+ runnerProfile: stringValue(record['runner_profile']) ??
476
+ stringValue(record['runnerProfile']) ??
477
+ stringValue(worker?.['runnerProfile']) ??
478
+ stringValue(worker?.['runner_profile']),
479
+ runnerPosture: recordValue(record['runner_posture']) ??
480
+ recordValue(record['runnerPosture']) ??
481
+ recordValue(worker?.['runnerPosture']) ??
482
+ recordValue(worker?.['runner_posture']) ??
483
+ undefined,
484
+ workspaceRoot: stringValue(record['workspace_root']) ??
485
+ stringValue(record['workspaceRoot']) ??
486
+ stringValue(record['workdir']) ??
487
+ stringValue(worker?.['workspaceRoot']) ??
488
+ stringValue(worker?.['workspace_root']) ??
489
+ stringValue(worker?.['workdir']),
490
+ identityKeyPath: stringValue(record['identity_key_path']) ??
491
+ stringValue(record['identityKeyPath']) ??
492
+ stringValue(worker?.['identityKeyPath']) ??
493
+ stringValue(worker?.['identity_key_path']),
494
+ capabilities: recordValue(record['capabilities']) ?? recordValue(worker?.['capabilities']) ?? undefined,
495
+ };
496
+ }
497
+ async function credentialFromManagedExecutorProfile(profile) {
498
+ if (profile.credentialFile) {
499
+ const value = (await fs.readFile(resolveProfilePath(profile.credentialFile), 'utf8')).trim();
500
+ if (!value) {
501
+ throw new Error(`Managed executor credential file is empty: ${resolveProfilePath(profile.credentialFile)}`);
502
+ }
503
+ return value;
504
+ }
505
+ return (profile.credential ??
506
+ process.env['VIEWPORT_MANAGED_EXECUTOR_TOKEN'] ??
507
+ process.env['VPD_MANAGED_EXECUTOR_TOKEN']);
508
+ }
509
+ function managedExecutorWorkspaceRoot(profile) {
510
+ if (profile.workspaceRoot)
511
+ return path.resolve(resolveProfilePath(profile.workspaceRoot));
512
+ const safeName = `${safeFilename(profile.workspaceId ?? 'workspace')}-${safeFilename(profile.executorId ?? 'executor')}`;
513
+ return path.join(os.homedir(), '.viewport', 'managed-executors', 'workspaces', safeName);
514
+ }
515
+ function managedExecutorIdentityPath(workspaceId, executorId) {
516
+ const safeName = `${safeFilename(workspaceId)}-${safeFilename(executorId)}.json`;
517
+ return path.join(os.homedir(), '.viewport', 'managed-executors', 'identities', safeName);
518
+ }
519
+ async function ensureManagedExecutorIdentity(identityPath) {
520
+ const resolved = resolveProfilePath(identityPath);
521
+ try {
522
+ const parsed = JSON.parse(await fs.readFile(resolved, 'utf8'));
523
+ if (parsed.algorithm?.toLowerCase() === 'ed25519' &&
524
+ typeof parsed.publicKey === 'string' &&
525
+ typeof parsed.privateKey === 'string') {
526
+ return {
527
+ publicKey: parsed.publicKey,
528
+ privateKey: parsed.privateKey,
529
+ publicKeyFingerprint: normalizeWorkerFingerprint(parsed.publicKeyFingerprint) ??
530
+ publicKeyFingerprint(parsed.publicKey),
531
+ path: resolved,
532
+ };
533
+ }
534
+ }
535
+ catch {
536
+ // Generate below.
537
+ }
538
+ const pair = crypto.generateKeyPairSync('ed25519');
539
+ const publicKey = pair.publicKey.export({ format: 'pem', type: 'spki' }).toString();
540
+ const privateKey = pair.privateKey.export({ format: 'pem', type: 'pkcs8' }).toString();
541
+ const record = {
542
+ version: 1,
543
+ algorithm: 'ed25519',
544
+ publicKey,
545
+ privateKey,
546
+ publicKeyFingerprint: publicKeyFingerprint(publicKey),
547
+ createdAt: new Date().toISOString(),
548
+ };
549
+ await fs.mkdir(path.dirname(resolved), { recursive: true, mode: 0o700 });
550
+ await fs.writeFile(resolved, `${JSON.stringify(record, null, 2)}\n`, {
551
+ encoding: 'utf8',
552
+ mode: 0o600,
553
+ });
554
+ await fs.chmod(resolved, 0o600);
555
+ return { ...record, path: resolved };
556
+ }
557
+ function publicKeyFingerprint(publicKeyPem) {
558
+ const key = crypto.createPublicKey(publicKeyPem);
559
+ const der = key.export({ format: 'der', type: 'spki' });
560
+ return crypto.createHash('sha256').update(der).digest('hex');
561
+ }
562
+ function normalizeWorkerFingerprint(value) {
563
+ if (typeof value !== 'string')
564
+ return undefined;
565
+ const normalized = value
566
+ .trim()
567
+ .toLowerCase()
568
+ .replace(/^sha256:/, '');
569
+ return /^[a-f0-9]{64}$/.test(normalized) ? normalized : undefined;
570
+ }
571
+ function resolveProfilePath(profilePath) {
572
+ if (profilePath === '~')
573
+ return os.homedir();
574
+ if (profilePath.startsWith('~/'))
575
+ return path.join(os.homedir(), profilePath.slice(2));
576
+ return path.resolve(profilePath);
577
+ }
578
+ function safeFilename(value) {
579
+ return value.replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '') || 'default';
580
+ }
209
581
  async function loadWorkerRuntimeProfile() {
210
582
  const manager = new ConfigManager();
211
583
  await manager.load();
@@ -236,6 +608,7 @@ async function loadWorkerRuntimeProfile() {
236
608
  credential: worker.credential ??
237
609
  process.env['VIEWPORT_MANAGED_EXECUTOR_TOKEN'] ??
238
610
  process.env['VPD_MANAGED_EXECUTOR_TOKEN'],
611
+ relayWsBaseUrl: process.env['VIEWPORT_RELAY_WS_BASE_URL'] ?? process.env['VPD_RELAY_WS_BASE_URL'],
239
612
  workspaceRoot: worker.workspaceRoot,
240
613
  identityKeyPath: worker.identityKeyPath,
241
614
  publicKeyFingerprint: worker.publicKeyFingerprint,
@@ -258,6 +631,7 @@ async function loadSandboxBootstrap(bootstrapPath) {
258
631
  workspaceId: requiredString(raw['workspace_id'] ?? raw['workspaceId'], 'workspace_id'),
259
632
  managedExecutorId: requiredString(raw['executor_id'] ?? raw['executorId'], 'executor_id'),
260
633
  credential: requiredString(raw['credential'], 'credential'),
634
+ relayWsBaseUrl: stringValue(raw['relay_ws_base_url'] ?? raw['relayWsBaseUrl'] ?? raw['relay_url'] ?? raw['relayUrl']),
261
635
  workspaceRoot,
262
636
  identityKeyPath: identityFile.path,
263
637
  publicKeyFingerprint: identityFile.publicKeyFingerprint,
@@ -279,10 +653,13 @@ function claimedLeaseFromBootstrap(rawLease) {
279
653
  const id = requiredString(rawLease['id'] ?? rawLease['lease_id'] ?? rawLease['leaseId'], 'lease.id');
280
654
  return {
281
655
  id,
656
+ agentSessionId: stringValue(rawLease['agent_session_id'] ?? rawLease['agentSessionId']),
282
657
  runId: stringValue(rawLease['workflow_run_id'] ?? rawLease['run_id'] ?? rawLease['runId']),
283
658
  runtimeRunId: stringValue(rawLease['runtime_run_id'] ?? rawLease['runtimeRunId']),
284
659
  leaseToken: stringValue(rawLease['lease_token'] ?? rawLease['leaseToken']),
285
660
  assignmentClaimToken: stringValue(rawLease['assignment_claim_token'] ?? rawLease['assignmentClaimToken']),
661
+ sessionVerificationContract: sessionVerificationContractValue(rawLease['session_verification_contract'] ?? rawLease['sessionVerificationContract']),
662
+ gateway: gatewayLeaseValue(rawLease['gateway'] ?? rawLease['gatewayLease']),
286
663
  yamlSnapshot: stringValue(rawLease['yaml_snapshot'] ?? rawLease['yamlSnapshot']),
287
664
  sourceRef: stringValue(rawLease['source_ref'] ?? rawLease['sourceRef']),
288
665
  directoryPath: stringValue(rawLease['directory_path'] ?? rawLease['directoryPath']),
@@ -291,10 +668,99 @@ function claimedLeaseFromBootstrap(rawLease) {
291
668
  workflowAuthorityContract: recordValue(rawLease['workflow_authority_contract'] ?? rawLease['workflowAuthorityContract']),
292
669
  executionProfileSnapshot: recordValue(rawLease['execution_profile_snapshot'] ?? rawLease['executionProfileSnapshot']),
293
670
  workflowSnapshot: recordValue(rawLease['workflow_snapshot'] ?? rawLease['workflowSnapshot']),
294
- runtimeTargetId: stringValue(rawLease['runtime_target_id'] ?? rawLease['runtimeTargetId']),
671
+ runtimeTargetId: runtimeContextTargetIdValue(rawLease),
295
672
  dataCapturePolicy: dataCapturePolicyValue(rawLease['data_capture_policy'] ?? rawLease['dataCapturePolicy']),
296
673
  };
297
674
  }
675
+ function gatewayLeaseValue(value) {
676
+ const record = recordValue(value);
677
+ if (!record)
678
+ return undefined;
679
+ const provider = stringValue(record['provider']);
680
+ if (provider !== 'anthropic' && provider !== 'openai' && provider !== 'gemini') {
681
+ return undefined;
682
+ }
683
+ const gatewayBaseUrl = stringValue(record['gateway_base_url'] ??
684
+ record['gatewayBaseUrl'] ??
685
+ record['base_url'] ??
686
+ record['baseUrl']);
687
+ const virtualKey = recordValue(record['virtual_key'] ?? record['virtualKey']);
688
+ const token = stringValue(virtualKey?.['token']);
689
+ const modelAllow = arrayOfStrings(record['model_allow'] ?? record['modelAllow']);
690
+ if (!gatewayBaseUrl || !token || modelAllow.length === 0) {
691
+ return undefined;
692
+ }
693
+ return {
694
+ gatewayBaseUrl: gatewayBaseUrl.replace(/\/+$/, ''),
695
+ provider,
696
+ modelAllow,
697
+ virtualKey: { token },
698
+ };
699
+ }
700
+ async function withGatewayLeaseProcessEnv(lease, callback) {
701
+ const env = gatewayLeaseEnv(lease.gateway);
702
+ const previous = new Map();
703
+ for (const [key, value] of Object.entries(env)) {
704
+ previous.set(key, process.env[key]);
705
+ process.env[key] = value;
706
+ }
707
+ try {
708
+ return await callback();
709
+ }
710
+ finally {
711
+ for (const [key, value] of previous.entries()) {
712
+ if (value === undefined) {
713
+ delete process.env[key];
714
+ }
715
+ else {
716
+ process.env[key] = value;
717
+ }
718
+ }
719
+ }
720
+ }
721
+ function gatewayLeaseEnv(gateway) {
722
+ if (!gateway)
723
+ return {};
724
+ const model = gateway.modelAllow[0] ?? '';
725
+ const base = gateway.gatewayBaseUrl.replace(/\/+$/, '');
726
+ const common = {
727
+ VIEWPORT_GATEWAY_BASE_URL: base,
728
+ VIEWPORT_LLM_PROVIDER: gateway.provider,
729
+ VIEWPORT_LLM_MODEL: model,
730
+ VIEWPORT_LLM_VIRTUAL_KEY: gateway.virtualKey.token,
731
+ };
732
+ if (gateway.provider === 'openai') {
733
+ return {
734
+ ...common,
735
+ CODEX_API_KEY: gateway.virtualKey.token,
736
+ OPENAI_API_KEY: gateway.virtualKey.token,
737
+ OPENAI_BASE_URL: `${base}/openai/v1`,
738
+ };
739
+ }
740
+ if (gateway.provider === 'anthropic') {
741
+ return {
742
+ ...common,
743
+ ANTHROPIC_API_KEY: gateway.virtualKey.token,
744
+ ANTHROPIC_BASE_URL: `${base}/anthropic`,
745
+ };
746
+ }
747
+ return {
748
+ ...common,
749
+ GEMINI_API_KEY: gateway.virtualKey.token,
750
+ GEMINI_BASE_URL: `${base}/gemini/v1beta/openai`,
751
+ GOOGLE_GENERATIVE_AI_API_KEY: gateway.virtualKey.token,
752
+ };
753
+ }
754
+ function gatewayLeaseCredentialEnv(gateway) {
755
+ if (!gateway)
756
+ return {};
757
+ const env = gatewayLeaseEnv(gateway);
758
+ const aliases = {};
759
+ for (const [name, value] of Object.entries(env)) {
760
+ aliases[envNameForCredentialRef(name)] = value;
761
+ }
762
+ return aliases;
763
+ }
298
764
  async function materializeBootstrapIdentity(identity, workspaceRoot) {
299
765
  if (!identity) {
300
766
  throw new Error('Sandbox bootstrap file is missing identity.');
@@ -336,13 +802,13 @@ function validateInboundWorkerGate(profile) {
336
802
  }
337
803
  throw new Error('Inbound worker transport listener is not implemented yet; do not enable inbound without the signed listener proof.');
338
804
  }
339
- async function claimLeaseHttp(profile, body) {
805
+ async function claimLeaseHttp(profile, body, dispatcher) {
340
806
  const leaseSeconds = positiveInteger(body['leaseSeconds']) ?? DEFAULT_HOSTED_LEASE_SECONDS;
341
807
  const response = isHostedManagedExecutorProfile(profile)
342
808
  ? await hostedManagedExecutorFetch(profile, 'POST', 'claim', {
343
809
  credential: profile.credential,
344
810
  lease_seconds: leaseSeconds,
345
- })
811
+ }, undefined, undefined, [], dispatcher)
346
812
  : await workerFetch(profile, 'workers/claim', body);
347
813
  if (response.status === 204)
348
814
  return null;
@@ -361,10 +827,18 @@ async function claimLeaseHttp(profile, body) {
361
827
  }
362
828
  return {
363
829
  id,
830
+ agentSessionId: stringValue(data['agent_session_id'] ??
831
+ data['agentSessionId'] ??
832
+ rawLease['agent_session_id'] ??
833
+ rawLease['agentSessionId']),
364
834
  runId: stringValue(data['id'] ?? rawLease['workflow_run_id'] ?? rawLease['run_id'] ?? rawLease['runId']),
365
835
  runtimeRunId: stringValue(data['runtime_run_id'] ?? rawLease['runtime_run_id']),
366
836
  leaseToken: stringValue(rawLease['lease_token'] ?? rawLease['leaseToken']),
367
837
  assignmentClaimToken: stringValue(data['assignment_claim_token']),
838
+ sessionVerificationContract: sessionVerificationContractValue(data['session_verification_contract'] ??
839
+ data['sessionVerificationContract'] ??
840
+ rawLease['session_verification_contract'] ??
841
+ rawLease['sessionVerificationContract']),
368
842
  yamlSnapshot: stringValue(data['yaml_snapshot'] ?? rawLease['yaml_snapshot']),
369
843
  sourceRef: stringValue(data['source_ref'] ?? rawLease['source_ref']),
370
844
  directoryPath: stringValue(data['directory_path'] ?? rawLease['directory_path']),
@@ -373,11 +847,11 @@ async function claimLeaseHttp(profile, body) {
373
847
  workflowAuthorityContract: recordValue(data['workflow_authority_contract'] ?? rawLease['workflow_authority_contract']),
374
848
  executionProfileSnapshot: recordValue(data['execution_profile_snapshot'] ?? rawLease['execution_profile_snapshot']),
375
849
  workflowSnapshot: recordValue(data['workflow_snapshot'] ?? rawLease['workflow_snapshot']),
376
- runtimeTargetId: stringValue(data['runtime_target_id'] ?? rawLease['runtime_target_id']),
850
+ runtimeTargetId: runtimeContextTargetIdValue(data, rawLease),
377
851
  dataCapturePolicy: dataCapturePolicyValue(data['data_capture_policy'] ?? rawLease['data_capture_policy']),
378
852
  };
379
853
  }
380
- async function heartbeatHttp(profile, options) {
854
+ async function heartbeatHttp(profile, options, dispatcher) {
381
855
  const capabilityPayload = managedExecutorCapabilities(profile.capabilities);
382
856
  if (isHostedManagedExecutorProfile(profile)) {
383
857
  await hostedManagedExecutorFetch(profile, 'POST', 'heartbeat', {
@@ -387,7 +861,9 @@ async function heartbeatHttp(profile, options) {
387
861
  access_mode: options.transport,
388
862
  runner_mode: options.lifecycle === 'ephemeral' ? 'viewport_managed' : 'self_hosted',
389
863
  runner_provider: options.lifecycle === 'ephemeral' ? 'viewport_cloud' : 'local',
390
- context_execution_mode: options.lifecycle === 'ephemeral' ? 'viewport_managed' : 'customer_managed_context_worker',
864
+ context_execution_mode: options.lifecycle === 'ephemeral'
865
+ ? 'viewport_managed'
866
+ : 'customer_managed_context_worker',
391
867
  credential_mode: options.lifecycle === 'ephemeral' ? 'run_scoped_grant' : 'runner_local',
392
868
  runner_profile: stringValue(capabilityPayload['runner_pool']) ??
393
869
  stringValue(capabilityPayload['runnerPool']) ??
@@ -399,7 +875,7 @@ async function heartbeatHttp(profile, options) {
399
875
  },
400
876
  },
401
877
  capabilities: capabilityPayload,
402
- });
878
+ }, undefined, undefined, [], dispatcher);
403
879
  return;
404
880
  }
405
881
  await workerRequest(profile, 'workers/heartbeat', {
@@ -412,7 +888,7 @@ async function heartbeatHttp(profile, options) {
412
888
  capabilities: profile.capabilities,
413
889
  });
414
890
  }
415
- async function syncLeaseHttp(profile, lease, execution) {
891
+ async function syncLeaseHttp(profile, lease, execution, dispatcher) {
416
892
  const status = execution.status;
417
893
  if (isHostedManagedExecutorProfile(profile)) {
418
894
  if (!lease.runId) {
@@ -466,7 +942,7 @@ async function syncLeaseHttp(profile, lease, execution) {
466
942
  message: `vpd worker marked run ${status}`,
467
943
  },
468
944
  ],
469
- }, lease.assignmentClaimToken, lease.leaseToken);
945
+ }, lease.assignmentClaimToken, lease.leaseToken, [], dispatcher);
470
946
  return;
471
947
  }
472
948
  await workerRequest(profile, `workers/leases/${encodeURIComponent(lease.id)}/sync`, {
@@ -496,7 +972,7 @@ async function executeHostedWorkflowClaim(profile, transport, lease) {
496
972
  if (existing) {
497
973
  if (existing.status === 'blocked') {
498
974
  const body = await transport.pollRuntimeCommands(lease);
499
- const runtimeSecretEnv = await materializeHostedRunCredentials(profile, lease);
975
+ const runtimeSecretEnv = await runtimeSecretEnvForHostedRun(profile, lease, transport);
500
976
  const applied = await daemon.workflowRunner.applyRuntimeCommandBody(existing.id, body, {
501
977
  runtimeSecretEnv,
502
978
  });
@@ -524,15 +1000,16 @@ async function executeHostedWorkflowClaim(profile, transport, lease) {
524
1000
  const directoryPath = path.resolve(lease.directoryPath ?? profile.workspaceRoot);
525
1001
  await fs.mkdir(directoryPath, { recursive: true });
526
1002
  const directory = await daemon.directoryManager.register(directoryPath);
527
- const runtimeSecretEnv = await materializeHostedRunCredentials(profile, lease);
1003
+ const runtimeSecretEnv = await runtimeSecretEnvForHostedRun(profile, lease, transport);
528
1004
  const run = await daemon.workflowRunner.startRun({
529
1005
  workflowYaml: lease.yamlSnapshot,
530
1006
  workflowSourceRef: lease.sourceRef ?? `viewport://managed-executor/${lease.runId ?? lease.id}`,
531
1007
  directoryId: directory.id,
532
- inputs: lease.inputSnapshot,
1008
+ inputs: hostedWorkflowInputs(profile, lease),
533
1009
  resourceId: profile.workspaceId,
534
- runtimeTargetId: lease.runtimeTargetId ?? profile.managedExecutorId,
1010
+ runtimeTargetId: hostedRuntimeContextTargetId(profile, lease),
535
1011
  platformRunId: lease.runId,
1012
+ agentSessionId: lease.agentSessionId,
536
1013
  resourceManifest: lease.resourceManifest,
537
1014
  workflowAuthorityContract: lease.workflowAuthorityContract,
538
1015
  dataCapturePolicy: lease.dataCapturePolicy,
@@ -562,7 +1039,7 @@ async function executeHostedWorkflowClaim(profile, transport, lease) {
562
1039
  };
563
1040
  }
564
1041
  }
565
- async function materializeHostedRunCredentials(profile, lease) {
1042
+ async function materializeHostedRunCredentials(profile, lease, dispatcher) {
566
1043
  if (!lease.runId) {
567
1044
  throw new Error('Hosted managed executor credential materialization requires a workflow run id.');
568
1045
  }
@@ -576,7 +1053,7 @@ async function materializeHostedRunCredentials(profile, lease) {
576
1053
  credential: profile.credential,
577
1054
  handle,
578
1055
  ...repositoryForCredentialHandle(lease, handle),
579
- }, lease.assignmentClaimToken, lease.leaseToken);
1056
+ }, lease.assignmentClaimToken, lease.leaseToken, [], dispatcher);
580
1057
  const parsed = (await response.json());
581
1058
  const data = parsed['data'] && typeof parsed['data'] === 'object'
582
1059
  ? parsed['data']
@@ -588,6 +1065,16 @@ async function materializeHostedRunCredentials(profile, lease) {
588
1065
  }
589
1066
  return runtimeSecretEnv;
590
1067
  }
1068
+ async function runtimeSecretEnvForHostedRun(profile, lease, transport) {
1069
+ const dispatcher = transport instanceof RelayWorkerTransport
1070
+ ? (request) => transport.dispatchHostedManagedExecutorRequest(request)
1071
+ : undefined;
1072
+ return {
1073
+ ...(await materializeHostedRunCredentials(profile, lease, dispatcher)),
1074
+ ...gatewayLeaseCredentialEnv(lease.gateway),
1075
+ ...gatewayLeaseEnv(lease.gateway),
1076
+ };
1077
+ }
591
1078
  function credentialHandlesFromLease(lease) {
592
1079
  const workflow = yamlSnapshotDocument(lease);
593
1080
  const handles = new Set();
@@ -743,7 +1230,7 @@ async function resumeBlockedHostedExecution(profile, transport, lease, execution
743
1230
  }
744
1231
  return execution;
745
1232
  }
746
- const runtimeSecretEnv = await materializeHostedRunCredentials(profile, lease);
1233
+ const runtimeSecretEnv = await runtimeSecretEnvForHostedRun(profile, lease);
747
1234
  const applied = await daemon.workflowRunner.applyRuntimeCommandBody(workflowRunId, body, {
748
1235
  runtimeSecretEnv,
749
1236
  });
@@ -772,11 +1259,252 @@ async function resumeBlockedHostedExecution(profile, transport, lease, execution
772
1259
  }
773
1260
  return execution;
774
1261
  }
775
- async function fetchHostedAssignmentHttp(profile, lease) {
1262
+ async function maybeExecuteHostedSessionVerification(profile, transport, lease, execution) {
1263
+ if (!isHostedManagedExecutorProfile(profile))
1264
+ return;
1265
+ if (execution.status !== 'completed' || !execution.run)
1266
+ return;
1267
+ const contract = await executableHostedSessionVerificationContract(transport, lease);
1268
+ if (!contract || !verificationRunnerMayExecute(contract))
1269
+ return;
1270
+ const agentSessionId = verificationAgentSessionId(contract);
1271
+ if (!agentSessionId)
1272
+ return;
1273
+ const commands = verificationCommands(contract);
1274
+ if (commands.length === 0)
1275
+ return;
1276
+ const runDirectoryPath = execution.run.directoryPath ?? lease.directoryPath ?? profile.workspaceRoot;
1277
+ const defaultCommandDirectory = verificationDefaultCommandDirectory(execution.run);
1278
+ const commandResults = [];
1279
+ const artifactRefs = [];
1280
+ for (const command of commands) {
1281
+ const name = verificationCommandName(command, commandResults.length + 1);
1282
+ const commandText = verificationCommandText(command);
1283
+ let result;
1284
+ let executionError;
1285
+ try {
1286
+ const cwd = verificationCommandCwd(runDirectoryPath, command, defaultCommandDirectory);
1287
+ result = await runShellCommand(commandText, '', cwd);
1288
+ }
1289
+ catch (error) {
1290
+ executionError = errorMessage(error);
1291
+ result = { exitCode: 1, stdout: '', stderr: '' };
1292
+ }
1293
+ const stdoutDigest = sha256Text(result.stdout);
1294
+ const stderrDigest = sha256Text(result.stderr);
1295
+ const status = result.exitCode === 0 ? 'passed' : 'failed';
1296
+ artifactRefs.push(`verification:${name}:stdout:${stdoutDigest}`);
1297
+ if (result.stderr.trim() !== '') {
1298
+ artifactRefs.push(`verification:${name}:stderr:${stderrDigest}`);
1299
+ }
1300
+ commandResults.push({
1301
+ schema: 'viewport.verification_command_result/v1',
1302
+ name,
1303
+ status,
1304
+ required: command.required !== false,
1305
+ exit_code: result.exitCode,
1306
+ command_sha256: sha256Text(commandText),
1307
+ stdout_sha256: stdoutDigest,
1308
+ stderr_sha256: stderrDigest,
1309
+ stdout_bytes: Buffer.byteLength(result.stdout, 'utf8'),
1310
+ stderr_bytes: Buffer.byteLength(result.stderr, 'utf8'),
1311
+ working_directory: verificationCommandWorkingDirectory(command) ??
1312
+ verificationDisplayWorkingDirectory(runDirectoryPath, defaultCommandDirectory),
1313
+ raw_output_included: false,
1314
+ ...(executionError ? { error: executionError } : {}),
1315
+ });
1316
+ }
1317
+ const requiredFailures = commandResults.filter((result) => result['required'] !== false && result['status'] !== 'passed');
1318
+ const passedCount = commandResults.filter((result) => result['status'] === 'passed').length;
1319
+ const status = requiredFailures.length > 0 ? 'failed' : 'passed';
1320
+ const summary = status === 'passed'
1321
+ ? `${passedCount}/${commandResults.length} verification commands passed.`
1322
+ : `${requiredFailures.length} required verification command(s) failed.`;
1323
+ await postHostedSessionVerificationAttempt(profile, transport, lease, contract, agentSessionId, {
1324
+ status,
1325
+ attempt_kind: 'verification',
1326
+ summary,
1327
+ artifact_refs: artifactRefs.slice(0, 50),
1328
+ verification_pack: {
1329
+ schema: 'viewport.verification_pack_result/v1',
1330
+ source_schema: contract.schema ?? null,
1331
+ agent_session_id: agentSessionId,
1332
+ workflow_run_id: lease.runId,
1333
+ policy_hash: typeof contract['policy_hash'] === 'string' ? contract['policy_hash'] : null,
1334
+ command_results: commandResults,
1335
+ required_artifacts: verificationRequiredArtifacts(contract),
1336
+ raw_command_output_included: false,
1337
+ agent_self_assessment_used: false,
1338
+ },
1339
+ repair_recommendation: status === 'passed'
1340
+ ? { action: 'none' }
1341
+ : {
1342
+ action: 'ask_human',
1343
+ failed_commands: requiredFailures.map((result) => result['name']),
1344
+ },
1345
+ });
1346
+ }
1347
+ async function executableHostedSessionVerificationContract(transport, lease) {
1348
+ const contract = lease.sessionVerificationContract;
1349
+ if (contract && verificationRunnerMayExecute(contract))
1350
+ return contract;
1351
+ if (!leaseMayHaveSessionVerification(lease))
1352
+ return contract ?? null;
1353
+ const refreshed = await transport.pollRuntimeCommands(lease);
1354
+ return sessionVerificationContractFromBody(refreshed) ?? contract ?? null;
1355
+ }
1356
+ function leaseMayHaveSessionVerification(lease) {
1357
+ const policyPin = recordValue(lease.workflowSnapshot?.['product20_policy_pin']);
1358
+ return Boolean(lease.agentSessionId ||
1359
+ stringValue(policyPin?.['agent_session_id']) ||
1360
+ lease.sessionVerificationContract);
1361
+ }
1362
+ function sessionVerificationContractFromBody(body) {
1363
+ const record = recordValue(body);
1364
+ const data = recordValue(record?.['data']);
1365
+ return (sessionVerificationContractValue(data?.['session_verification_contract'] ??
1366
+ data?.['sessionVerificationContract'] ??
1367
+ record?.['session_verification_contract'] ??
1368
+ record?.['sessionVerificationContract']) ?? null);
1369
+ }
1370
+ function sessionVerificationContractValue(value) {
1371
+ const record = recordValue(value);
1372
+ if (!record)
1373
+ return undefined;
1374
+ return record;
1375
+ }
1376
+ function verificationRunnerMayExecute(contract) {
1377
+ const access = contract.access_model ?? contract.accessModel ?? {};
1378
+ return access.runner_may_execute_commands === true || access.runnerMayExecuteCommands === true;
1379
+ }
1380
+ function verificationAgentSessionId(contract) {
1381
+ return contract.agent_session_id ?? contract.agentSessionId ?? null;
1382
+ }
1383
+ function verificationCommands(contract) {
1384
+ return (contract.commands ?? []).filter((command) => verificationCommandText(command) !== '');
1385
+ }
1386
+ function verificationCommandName(command, index) {
1387
+ return command.name?.trim() || `verification-${index}`;
1388
+ }
1389
+ function verificationCommandText(command) {
1390
+ return command.command?.trim() ?? '';
1391
+ }
1392
+ function verificationCommandWorkingDirectory(command) {
1393
+ return command.working_directory?.trim() || command.workingDirectory?.trim() || null;
1394
+ }
1395
+ function verificationCommandCwd(runDirectoryPath, command, defaultDirectoryPath) {
1396
+ const workingDirectory = verificationCommandWorkingDirectory(command);
1397
+ const root = path.resolve(runDirectoryPath);
1398
+ const resolved = workingDirectory
1399
+ ? path.resolve(root, workingDirectory)
1400
+ : path.resolve(defaultDirectoryPath ?? root);
1401
+ const relative = path.relative(root, resolved);
1402
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
1403
+ throw new Error('Verification command working_directory must stay inside the run directory.');
1404
+ }
1405
+ return resolved;
1406
+ }
1407
+ function verificationDefaultCommandDirectory(run) {
1408
+ const root = path.resolve(run.directoryPath);
1409
+ for (const node of Object.values(run.nodes ?? {})) {
1410
+ if (node.type !== 'checkout')
1411
+ continue;
1412
+ const candidate = typeof node.outputs?.['path'] === 'string' ? node.outputs['path'] : null;
1413
+ if (!candidate)
1414
+ continue;
1415
+ const resolved = path.resolve(candidate);
1416
+ const relative = path.relative(root, resolved);
1417
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
1418
+ return resolved;
1419
+ }
1420
+ }
1421
+ return root;
1422
+ }
1423
+ function verificationDisplayWorkingDirectory(runDirectoryPath, directoryPath) {
1424
+ const root = path.resolve(runDirectoryPath);
1425
+ const resolved = path.resolve(directoryPath);
1426
+ const relative = path.relative(root, resolved);
1427
+ if (relative === '')
1428
+ return '.';
1429
+ if (relative.startsWith('..') || path.isAbsolute(relative))
1430
+ return '.';
1431
+ return relative;
1432
+ }
1433
+ function verificationRequiredArtifacts(contract) {
1434
+ return contract.required_artifacts ?? contract.requiredArtifacts ?? [];
1435
+ }
1436
+ async function runShellCommand(command, stdin, cwd = process.cwd()) {
1437
+ return new Promise((resolve, reject) => {
1438
+ const child = spawn('sh', ['-lc', command], {
1439
+ cwd,
1440
+ stdio: ['pipe', 'pipe', 'pipe'],
1441
+ env: process.env,
1442
+ });
1443
+ const stdout = [];
1444
+ const stderr = [];
1445
+ child.stdout.on('data', (chunk) => {
1446
+ stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1447
+ });
1448
+ child.stderr.on('data', (chunk) => {
1449
+ stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1450
+ });
1451
+ child.on('error', reject);
1452
+ child.on('close', (code) => {
1453
+ resolve({
1454
+ exitCode: code ?? 1,
1455
+ stdout: Buffer.concat(stdout).toString('utf8'),
1456
+ stderr: Buffer.concat(stderr).toString('utf8'),
1457
+ });
1458
+ });
1459
+ child.stdin.end(stdin);
1460
+ });
1461
+ }
1462
+ function sha256Text(value) {
1463
+ return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
1464
+ }
1465
+ function errorMessage(error) {
1466
+ return error instanceof Error ? error.message : String(error);
1467
+ }
1468
+ async function postHostedSessionVerificationAttempt(profile, transport, lease, contract, agentSessionId, payload) {
1469
+ if (!lease.runId) {
1470
+ throw new Error('Hosted session verification requires a workflow run id.');
1471
+ }
1472
+ const runLeaseToken = lease.assignmentClaimToken ?? lease.leaseToken;
1473
+ if (!runLeaseToken) {
1474
+ throw new Error('Hosted session verification requires a run lease token.');
1475
+ }
1476
+ const suffix = hostedVerificationRuntimePathSuffix(profile, contract, agentSessionId) ??
1477
+ `workflow-runs/${encodeURIComponent(lease.runId)}/agent-sessions/${encodeURIComponent(agentSessionId)}/verification-attempts`;
1478
+ const dispatcher = transport instanceof RelayWorkerTransport
1479
+ ? (request) => transport.dispatchHostedManagedExecutorRequest(request)
1480
+ : undefined;
1481
+ await hostedManagedExecutorFetch(profile, 'POST', suffix, {
1482
+ credential: profile.credential,
1483
+ ...payload,
1484
+ }, undefined, runLeaseToken, [], dispatcher);
1485
+ }
1486
+ function hostedVerificationRuntimePathSuffix(profile, contract, agentSessionId) {
1487
+ const runtimeTool = contract.runtime_tool ?? contract.runtimeTool ?? {};
1488
+ const endpoint = runtimeTool.runtime_endpoint ?? runtimeTool.runtimeEndpoint;
1489
+ if (typeof endpoint !== 'string' || endpoint.trim() === '')
1490
+ return null;
1491
+ const normalized = endpoint.trim();
1492
+ const prefix = `/api/runtime/workspaces/${encodeURIComponent(profile.workspaceId ?? '')}/managed-executors/${encodeURIComponent(profile.managedExecutorId ?? '')}/`;
1493
+ if (normalized.startsWith(prefix)) {
1494
+ return normalized.slice(prefix.length);
1495
+ }
1496
+ const fallback = `/workflow-runs/`;
1497
+ const index = normalized.indexOf(fallback);
1498
+ if (index >= 0 && normalized.includes(`/agent-sessions/${encodeURIComponent(agentSessionId)}/`)) {
1499
+ return normalized.slice(index + 1);
1500
+ }
1501
+ return null;
1502
+ }
1503
+ async function fetchHostedAssignmentHttp(profile, lease, dispatcher) {
776
1504
  if (!lease.runId) {
777
1505
  throw new Error('Hosted managed executor assignment polling requires a workflow run id.');
778
1506
  }
779
- const response = await hostedManagedExecutorFetch(profile, 'GET', `workflow-runs/${encodeURIComponent(lease.runId)}`, {}, lease.assignmentClaimToken, lease.leaseToken, [429]);
1507
+ const response = await hostedManagedExecutorFetch(profile, 'GET', `workflow-runs/${encodeURIComponent(lease.runId)}`, {}, lease.assignmentClaimToken, lease.leaseToken, [429], dispatcher);
780
1508
  if (response.status === 429) {
781
1509
  await sleep(retryAfterMs(response));
782
1510
  return { runtime_commands: [], _viewport_worker_retry: true };
@@ -884,22 +1612,26 @@ function isHostedManagedExecutorProfile(profile) {
884
1612
  return Boolean(profile.workspaceId && profile.managedExecutorId && profile.credential);
885
1613
  }
886
1614
  function managedExecutorCapabilities(capabilities) {
1615
+ const { schema: _schema, ...normalized } = capabilities;
887
1616
  const agents = capabilities['agents'];
888
1617
  if (!Array.isArray(agents))
889
- return capabilities;
1618
+ return normalized;
1619
+ const objectAgents = agents
1620
+ .filter((agent) => typeof agent === 'object' && agent !== null && !Array.isArray(agent))
1621
+ .map((agent) => {
1622
+ const record = agent;
1623
+ const id = stringValue(record['id']);
1624
+ return id ? [id, record] : null;
1625
+ })
1626
+ .filter((entry) => entry !== null);
1627
+ if (objectAgents.length === 0)
1628
+ return normalized;
890
1629
  return {
891
- ...capabilities,
892
- agents: Object.fromEntries(agents
893
- .filter((agent) => typeof agent === 'object' && agent !== null && !Array.isArray(agent))
894
- .map((agent) => {
895
- const record = agent;
896
- const id = stringValue(record['id']);
897
- return id ? [id, record] : null;
898
- })
899
- .filter((entry) => entry !== null)),
1630
+ ...normalized,
1631
+ agents: Object.fromEntries(objectAgents),
900
1632
  };
901
1633
  }
902
- async function hostedManagedExecutorFetch(profile, method, path, body, assignmentClaimToken, runLeaseToken, allowedStatuses = []) {
1634
+ async function hostedManagedExecutorFetch(profile, method, path, body, assignmentClaimToken, runLeaseToken, allowedStatuses = [], dispatcher) {
903
1635
  if (!profile.workspaceId || !profile.managedExecutorId || !profile.credential) {
904
1636
  throw new Error('Hosted managed executor profile is missing workspace, executor, or credential.');
905
1637
  }
@@ -910,26 +1642,37 @@ async function hostedManagedExecutorFetch(profile, method, path, body, assignmen
910
1642
  const maxAttempts = hostedManagedExecutorMaxAttempts();
911
1643
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
912
1644
  const signed = await signWorkerRequest(profile, method, requestPath, serialized);
913
- const response = await transportFetch(url, {
1645
+ const headers = {
1646
+ Authorization: `Bearer ${profile.credential}`,
1647
+ Accept: 'application/json',
1648
+ 'Content-Type': 'application/json',
1649
+ ...(runLeaseToken
1650
+ ? { 'X-Viewport-Run-Lease': runLeaseToken }
1651
+ : assignmentClaimToken
1652
+ ? { 'X-Viewport-Assignment-Claim': assignmentClaimToken }
1653
+ : {}),
1654
+ 'X-Viewport-Worker-Fingerprint': profile.publicKeyFingerprint,
1655
+ 'X-Viewport-Worker-Timestamp': signed.timestamp,
1656
+ 'X-Viewport-Worker-Nonce': signed.nonce,
1657
+ 'X-Viewport-Worker-Body-SHA256': signed.bodySha256,
1658
+ 'X-Viewport-Worker-Signature': signed.signature,
1659
+ ...(signed.serverId ? { 'X-Viewport-Server-Id': signed.serverId } : {}),
1660
+ };
1661
+ const request = {
914
1662
  method,
915
- headers: {
916
- Authorization: `Bearer ${profile.credential}`,
917
- Accept: 'application/json',
918
- 'Content-Type': 'application/json',
919
- ...(runLeaseToken
920
- ? { 'X-Viewport-Run-Lease': runLeaseToken }
921
- : assignmentClaimToken
922
- ? { 'X-Viewport-Assignment-Claim': assignmentClaimToken }
923
- : {}),
924
- 'X-Viewport-Worker-Fingerprint': profile.publicKeyFingerprint,
925
- 'X-Viewport-Worker-Timestamp': signed.timestamp,
926
- 'X-Viewport-Worker-Nonce': signed.nonce,
927
- 'X-Viewport-Worker-Body-SHA256': signed.bodySha256,
928
- 'X-Viewport-Worker-Signature': signed.signature,
929
- ...(signed.serverId ? { 'X-Viewport-Server-Id': signed.serverId } : {}),
930
- },
931
- ...(method === 'GET' ? {} : { body: serialized }),
932
- });
1663
+ path,
1664
+ requestPath,
1665
+ url,
1666
+ serialized,
1667
+ headers,
1668
+ };
1669
+ const response = dispatcher
1670
+ ? await dispatcher(request)
1671
+ : await transportFetch(url, {
1672
+ method,
1673
+ headers,
1674
+ ...(method === 'GET' ? {} : { body: serialized }),
1675
+ });
933
1676
  if (response.ok || allowedStatuses.includes(response.status)) {
934
1677
  return response;
935
1678
  }
@@ -970,6 +1713,27 @@ function retryAfterMs(response) {
970
1713
  }
971
1714
  return 5_000;
972
1715
  }
1716
+ function relayWorkerConnectionTimeoutMs() {
1717
+ return positiveIntegerFromEnv('VIEWPORT_RELAY_WORKER_CONNECT_TIMEOUT_MS') ?? 10_000;
1718
+ }
1719
+ function relayWorkerRequestTimeoutMs() {
1720
+ return positiveIntegerFromEnv('VIEWPORT_RELAY_WORKER_REQUEST_TIMEOUT_MS') ?? 60_000;
1721
+ }
1722
+ function positiveIntegerFromEnv(name) {
1723
+ const value = process.env[name];
1724
+ if (!value)
1725
+ return undefined;
1726
+ const parsed = Number.parseInt(value, 10);
1727
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
1728
+ }
1729
+ function relayWsBaseUrlFromServerUrl(serverUrl) {
1730
+ const parsed = new URL(serverUrl);
1731
+ const protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
1732
+ const hostname = parsed.hostname.startsWith('api.')
1733
+ ? `relay.${parsed.hostname.slice(4)}`
1734
+ : parsed.hostname;
1735
+ return `${protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ''}/ws`;
1736
+ }
973
1737
  function sleep(ms) {
974
1738
  return new Promise((resolve) => setTimeout(resolve, ms));
975
1739
  }
@@ -1002,6 +1766,11 @@ function recordValue(value) {
1002
1766
  return undefined;
1003
1767
  return value;
1004
1768
  }
1769
+ function arrayOfStrings(value) {
1770
+ return Array.isArray(value)
1771
+ ? value.filter((item) => typeof item === 'string' && item.trim() !== '')
1772
+ : [];
1773
+ }
1005
1774
  function isRecord(value) {
1006
1775
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
1007
1776
  }