@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.
- package/README.md +5 -5
- package/dist/adapters/claude.d.ts +1 -0
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +1 -1
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts +3 -0
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +298 -32
- package/dist/adapters/codex.js.map +1 -1
- package/dist/agents/codex-defaults.d.ts.map +1 -1
- package/dist/agents/codex-defaults.js +1 -1
- package/dist/agents/codex-defaults.js.map +1 -1
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +8 -2
- package/dist/agents/codex.js.map +1 -1
- package/dist/cli/commands.d.ts +1 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +1 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/lifecycle-pair-command.js +1 -1
- package/dist/cli/lifecycle-pair-command.js.map +1 -1
- package/dist/cli/managed-session-verification-contract.d.ts +46 -0
- package/dist/cli/managed-session-verification-contract.d.ts.map +1 -0
- package/dist/cli/managed-session-verification-contract.js +2 -0
- package/dist/cli/managed-session-verification-contract.js.map +1 -0
- package/dist/cli/signal-command.d.ts +2 -0
- package/dist/cli/signal-command.d.ts.map +1 -0
- package/dist/cli/signal-command.js +258 -0
- package/dist/cli/signal-command.js.map +1 -0
- package/dist/cli/worker-command.js +6 -4
- package/dist/cli/worker-command.js.map +1 -1
- package/dist/cli/worker-runtime.d.ts +14 -1
- package/dist/cli/worker-runtime.d.ts.map +1 -1
- package/dist/cli/worker-runtime.js +824 -55
- package/dist/cli/worker-runtime.js.map +1 -1
- package/dist/cli/workflow-commands.d.ts.map +1 -1
- package/dist/cli/workflow-commands.js +1 -6
- package/dist/cli/workflow-commands.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/server/http-request-schemas.d.ts +1 -0
- package/dist/server/http-request-schemas.d.ts.map +1 -1
- package/dist/server/http-request-schemas.js +1 -0
- package/dist/server/http-request-schemas.js.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +1 -0
- package/dist/server/http-server.js.map +1 -1
- package/dist/workflows/context-node-resolver.d.ts +1 -0
- package/dist/workflows/context-node-resolver.d.ts.map +1 -1
- package/dist/workflows/context-node-resolver.js +265 -2
- package/dist/workflows/context-node-resolver.js.map +1 -1
- package/dist/workflows/daemon-session.js +16 -1
- package/dist/workflows/daemon-session.js.map +1 -1
- package/dist/workflows/event-types.d.ts +1 -1
- package/dist/workflows/event-types.d.ts.map +1 -1
- package/dist/workflows/node-executor.js +2 -1
- package/dist/workflows/node-executor.js.map +1 -1
- package/dist/workflows/platform-context-client.d.ts +12 -27
- package/dist/workflows/platform-context-client.d.ts.map +1 -1
- package/dist/workflows/platform-context-client.internal.d.ts +23 -0
- package/dist/workflows/platform-context-client.internal.d.ts.map +1 -0
- package/dist/workflows/platform-context-client.internal.js +124 -0
- package/dist/workflows/platform-context-client.internal.js.map +1 -0
- package/dist/workflows/platform-context-client.js +102 -58
- package/dist/workflows/platform-context-client.js.map +1 -1
- package/dist/workflows/platform-context-client.types.d.ts +41 -0
- package/dist/workflows/platform-context-client.types.d.ts.map +1 -0
- package/dist/workflows/platform-context-client.types.js +2 -0
- package/dist/workflows/platform-context-client.types.js.map +1 -0
- package/dist/workflows/run-types.d.ts +2 -0
- package/dist/workflows/run-types.d.ts.map +1 -1
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +1 -0
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/workflow-production-schema.d.ts +1 -0
- package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
- package/dist/workflows/workflow-production-schema.js +1 -0
- package/dist/workflows/workflow-production-schema.js.map +1 -1
- package/dist/workflows/workflow-production-types.d.ts +1 -0
- package/dist/workflows/workflow-production-types.d.ts.map +1 -1
- package/dist/workflows/workflow-schema.d.ts +1 -0
- package/dist/workflows/workflow-schema.d.ts.map +1 -1
- package/package.json +2 -1
- package/schemas/workflow-v1.schema.json +4 -0
- package/dist/cli/workflow-managed-worker-format.d.ts +0 -13
- package/dist/cli/workflow-managed-worker-format.d.ts.map +0 -1
- package/dist/cli/workflow-managed-worker-format.js +0 -158
- package/dist/cli/workflow-managed-worker-format.js.map +0 -1
- package/dist/cli/workflow-managed-worker-types.d.ts +0 -137
- package/dist/cli/workflow-managed-worker-types.d.ts.map +0 -1
- package/dist/cli/workflow-managed-worker-types.js +0 -2
- package/dist/cli/workflow-managed-worker-types.js.map +0 -1
- package/dist/cli/workflow-managed-worker-util.d.ts +0 -6
- package/dist/cli/workflow-managed-worker-util.d.ts.map +0 -1
- package/dist/cli/workflow-managed-worker-util.js +0 -31
- package/dist/cli/workflow-managed-worker-util.js.map +0 -1
- package/dist/cli/workflow-managed-worker.d.ts +0 -2
- package/dist/cli/workflow-managed-worker.d.ts.map +0 -1
- package/dist/cli/workflow-managed-worker.js +0 -2036
- 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
|
-
|
|
54
|
-
|
|
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:
|
|
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:
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
1008
|
+
inputs: hostedWorkflowInputs(profile, lease),
|
|
533
1009
|
resourceId: profile.workspaceId,
|
|
534
|
-
runtimeTargetId:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...
|
|
892
|
-
agents: Object.fromEntries(
|
|
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
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
'
|
|
927
|
-
|
|
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
|
}
|