@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.
- 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-process-lock.js +1 -1
- package/dist/cli/worker-process-lock.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 +829 -59
- 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({
|
|
@@ -80,10 +255,8 @@ export async function runStandaloneWorker(options) {
|
|
|
80
255
|
return result;
|
|
81
256
|
}
|
|
82
257
|
if (options.leaseToken) {
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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:
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
1009
|
+
inputs: hostedWorkflowInputs(profile, lease),
|
|
533
1010
|
resourceId: profile.workspaceId,
|
|
534
|
-
runtimeTargetId:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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)),
|
|
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
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
}
|