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