@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,2036 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { constants, createHash, generateKeyPairSync, privateDecrypt, randomUUID, sign, } from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import YAML from 'yaml';
|
|
7
|
-
import { getArgs, getFlag, hasFlag } from './args.js';
|
|
8
|
-
import { daemonFetch, isDaemonRunning } from './daemon-client.js';
|
|
9
|
-
import { isJsonMode, printJson } from './command-shared.js';
|
|
10
|
-
import { parseCsvList, parseTlsVerifyMode, transportFetch } from './network.js';
|
|
11
|
-
import { commandPollSeconds, delay, listFlagValue, positiveIntFlagValue, safeText, } from './workflow-managed-worker-util.js';
|
|
12
|
-
import { executeProviderAction, WorkflowActionError, } from '../workflows/action-provider-adapters.js';
|
|
13
|
-
import { envNameForCredentialRef } from '../workflows/action-provider-utils.js';
|
|
14
|
-
import { sanitizeActionInput } from '../workflows/action-digest.js';
|
|
15
|
-
import { approvalActor, approvalExecutionGrant, approvalFeedback, approvalExpectedActionDigest, approvalMessage, capabilityPayload, dataFrom, localRunToSyncPayload, progressSyncEveryMs, readRun, } from './workflow-managed-worker-format.js';
|
|
16
|
-
import { acquireWorkerProcessLock } from './worker-process-lock.js';
|
|
17
|
-
export async function workflowWorker() {
|
|
18
|
-
if (hasFlag('help') || getArgs().includes('-h')) {
|
|
19
|
-
console.log(workflowWorkerUsage());
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const options = resolveWorkerOptions();
|
|
23
|
-
if (!(await isDaemonRunning())) {
|
|
24
|
-
throw new Error('Daemon is not running. Start it first with `vpd start`.');
|
|
25
|
-
}
|
|
26
|
-
await validateDaemonAgentCapabilities(options);
|
|
27
|
-
await syncDaemonModelCapabilities(options);
|
|
28
|
-
if (hasFlag('doctor')) {
|
|
29
|
-
await heartbeat(options, 'online', 'idle');
|
|
30
|
-
await safeHeartbeat(options, 'offline', 'offline');
|
|
31
|
-
if (isJsonMode()) {
|
|
32
|
-
printJson({
|
|
33
|
-
command: 'workflow worker doctor',
|
|
34
|
-
ok: true,
|
|
35
|
-
accessMode: options.accessMode,
|
|
36
|
-
runnerProfile: options.runnerProfile ?? null,
|
|
37
|
-
runnerPool: options.runnerPool ?? null,
|
|
38
|
-
capabilities: options.capabilities,
|
|
39
|
-
});
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
console.log('Workflow worker doctor passed.');
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (hasFlag('preflight')) {
|
|
46
|
-
if (isJsonMode()) {
|
|
47
|
-
printJson({
|
|
48
|
-
command: 'workflow worker',
|
|
49
|
-
ok: true,
|
|
50
|
-
preflight: true,
|
|
51
|
-
capabilities: options.capabilities,
|
|
52
|
-
});
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
console.log('Workflow worker preflight passed.');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const stats = {
|
|
59
|
-
claimed: 0,
|
|
60
|
-
actionReplaysClaimed: 0,
|
|
61
|
-
actionReplaysCompleted: 0,
|
|
62
|
-
completed: 0,
|
|
63
|
-
blocked: 0,
|
|
64
|
-
failed: 0,
|
|
65
|
-
};
|
|
66
|
-
let failed = false;
|
|
67
|
-
const processLock = options.once ? undefined : acquireWorkerProcessLock(options);
|
|
68
|
-
try {
|
|
69
|
-
do {
|
|
70
|
-
await heartbeat(options, 'online', 'idle');
|
|
71
|
-
const assignment = await claimAssignment(options);
|
|
72
|
-
if (!assignment) {
|
|
73
|
-
const actionReplay = await claimActionReplay(options);
|
|
74
|
-
if (actionReplay) {
|
|
75
|
-
stats.actionReplaysClaimed += 1;
|
|
76
|
-
const completedReplay = await completeActionReplay(options, actionReplay);
|
|
77
|
-
if (completedReplay.status === 'completed') {
|
|
78
|
-
stats.actionReplaysCompleted += 1;
|
|
79
|
-
stats.completed += 1;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
stats.failed += 1;
|
|
83
|
-
}
|
|
84
|
-
if (options.once ||
|
|
85
|
-
(options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns)) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (options.once)
|
|
91
|
-
break;
|
|
92
|
-
await delay(options.sleepSeconds * 1000);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
stats.claimed += 1;
|
|
96
|
-
if (assignment.status === 'canceled') {
|
|
97
|
-
const canceledRun = assignmentCanceledBeforeStartRun(assignment);
|
|
98
|
-
clearRunCredentialMaterial(assignment.id);
|
|
99
|
-
await syncLocalRun(options, assignment.id, canceledRun, assignment.assignment_claim_token);
|
|
100
|
-
stats.failed += 1;
|
|
101
|
-
if (options.once ||
|
|
102
|
-
(options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns)) {
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
let localRun;
|
|
108
|
-
try {
|
|
109
|
-
localRun = await runAssignmentLocally(options, assignment);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
stats.failed += 1;
|
|
113
|
-
const failureRun = assignmentExecutionFailureRun(assignment, error);
|
|
114
|
-
clearRunCredentialMaterial(assignment.id);
|
|
115
|
-
try {
|
|
116
|
-
await syncLocalRun(options, assignment.id, failureRun, assignment.assignment_claim_token);
|
|
117
|
-
}
|
|
118
|
-
catch (syncError) {
|
|
119
|
-
throw new Error(`Worker failed assignment ${assignment.id} before local run start (${errorMessage(error)}) and could not sync the failure (${errorMessage(syncError)}).`);
|
|
120
|
-
}
|
|
121
|
-
if (options.once ||
|
|
122
|
-
(options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns)) {
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (localRun.status === 'blocked' && !options.once) {
|
|
128
|
-
const approved = await approvedNodeForAssignment(options, assignment.id, assignment.assignment_claim_token, localRun.id);
|
|
129
|
-
if (approved) {
|
|
130
|
-
const resumed = await resumeApprovedLocalRun(options, assignment.id, localRun.id, approved, assignment.assignment_claim_token);
|
|
131
|
-
const blockedIds = blockedNodeIds(resumed);
|
|
132
|
-
const shouldKeepWaiting = resumed.status === 'blocked' &&
|
|
133
|
-
(!alreadyResolvedApprovalRuns.has(resumed) || !blockedIds.has(approved.node_key)) &&
|
|
134
|
-
(!blockedIds.has(approved.node_key) ||
|
|
135
|
-
managedApprovalDecision(approved) === 'request_changes');
|
|
136
|
-
const finalRun = shouldKeepWaiting
|
|
137
|
-
? await waitForApprovalAndResume(options, assignment.id, localRun.id, assignment.assignment_claim_token)
|
|
138
|
-
: resumed;
|
|
139
|
-
stats.completed += finalRun.status === 'completed' ? 1 : 0;
|
|
140
|
-
stats.failed += finalRun.status === 'failed' || finalRun.status === 'canceled' ? 1 : 0;
|
|
141
|
-
if (options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns) {
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
let synced;
|
|
148
|
-
try {
|
|
149
|
-
synced = await syncLocalRun(options, assignment.id, localRun, assignment.assignment_claim_token);
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (localRun.status === 'canceled' && isPlatformAccessDenied(error)) {
|
|
153
|
-
stats.failed += 1;
|
|
154
|
-
if (options.once ||
|
|
155
|
-
(options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns)) {
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
if (synced.status === 'blocked') {
|
|
163
|
-
stats.blocked += 1;
|
|
164
|
-
if (!options.once) {
|
|
165
|
-
const resumed = await waitForApprovalAndResume(options, assignment.id, localRun.id, assignment.assignment_claim_token);
|
|
166
|
-
stats.completed += resumed.status === 'completed' ? 1 : 0;
|
|
167
|
-
stats.failed += resumed.status === 'failed' || resumed.status === 'canceled' ? 1 : 0;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else if (synced.status === 'completed') {
|
|
171
|
-
stats.completed += 1;
|
|
172
|
-
}
|
|
173
|
-
else if (synced.status === 'failed' || synced.status === 'canceled') {
|
|
174
|
-
stats.failed += 1;
|
|
175
|
-
}
|
|
176
|
-
if (options.once ||
|
|
177
|
-
(options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns)) {
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
} while (true);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
failed = true;
|
|
184
|
-
await safeHeartbeat(options, 'stale', 'degraded');
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
processLock?.release();
|
|
189
|
-
await safeHeartbeat(options, 'offline', failed ? 'degraded' : 'offline');
|
|
190
|
-
}
|
|
191
|
-
if (isJsonMode()) {
|
|
192
|
-
printJson({ command: 'workflow worker', ok: stats.failed === 0, stats });
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
console.log(`Workflow worker stopped. Claimed ${stats.claimed} workflow run(s), replayed ${stats.actionReplaysCompleted}/${stats.actionReplaysClaimed} action(s), completed ${stats.completed}, blocked ${stats.blocked}, failed ${stats.failed}.`);
|
|
196
|
-
}
|
|
197
|
-
function totalClaimed(stats) {
|
|
198
|
-
return stats.claimed + stats.actionReplaysClaimed;
|
|
199
|
-
}
|
|
200
|
-
async function validateDaemonAgentCapabilities(options) {
|
|
201
|
-
if (options.capabilities.agents.length === 0)
|
|
202
|
-
return;
|
|
203
|
-
const response = await daemonFetch('/api/agents', {
|
|
204
|
-
method: 'GET',
|
|
205
|
-
timeoutMs: 30_000,
|
|
206
|
-
});
|
|
207
|
-
if (!response?.ok) {
|
|
208
|
-
throw new Error(`Daemon request failed: ${response?.status ?? 'no response'} ${await safeText(response ?? undefined)}`);
|
|
209
|
-
}
|
|
210
|
-
const body = (await response.json());
|
|
211
|
-
const availableAgents = new Set((body.agents ?? []).flatMap((agent) => {
|
|
212
|
-
if (typeof agent === 'string')
|
|
213
|
-
return [agent];
|
|
214
|
-
if (!agent || typeof agent !== 'object')
|
|
215
|
-
return [];
|
|
216
|
-
if (agent.available === false)
|
|
217
|
-
return [];
|
|
218
|
-
return typeof agent.id === 'string' ? [agent.id] : [];
|
|
219
|
-
}));
|
|
220
|
-
const missing = options.capabilities.agents.filter((agent) => !availableAgents.has(agent));
|
|
221
|
-
if (missing.length === 0)
|
|
222
|
-
return;
|
|
223
|
-
throw new Error(`Daemon is missing workflow agent adapter(s): ${missing.join(', ')}. Start the daemon with the matching built-in agent installed, or configure a custom command agent with VIEWPORT_CUSTOM_AGENT_COMMAND.`);
|
|
224
|
-
}
|
|
225
|
-
async function syncDaemonModelCapabilities(options) {
|
|
226
|
-
const response = await daemonFetch('/api/models', {
|
|
227
|
-
method: 'GET',
|
|
228
|
-
timeoutMs: 30_000,
|
|
229
|
-
});
|
|
230
|
-
if (!response?.ok) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const body = (await response.json());
|
|
234
|
-
const catalog = daemonModelCatalog(body.models ?? []);
|
|
235
|
-
const allModels = [...new Set(Object.values(catalog).flat())];
|
|
236
|
-
if (allModels.length === 0) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (options.capabilities.agents.length === 0) {
|
|
240
|
-
options.capabilities.agents = Object.keys(catalog);
|
|
241
|
-
}
|
|
242
|
-
options.capabilities.models = allModels;
|
|
243
|
-
options.capabilities.agentModels = catalog;
|
|
244
|
-
}
|
|
245
|
-
function daemonModelCatalog(models) {
|
|
246
|
-
const catalog = {};
|
|
247
|
-
for (const model of models) {
|
|
248
|
-
const agentId = stringValue(model.agentId) ?? stringValue(model.agent_id);
|
|
249
|
-
const value = stringValue(model.value) ?? stringValue(model.id);
|
|
250
|
-
if (!agentId || !value)
|
|
251
|
-
continue;
|
|
252
|
-
catalog[agentId] = [...new Set([...(catalog[agentId] ?? []), value])];
|
|
253
|
-
}
|
|
254
|
-
return catalog;
|
|
255
|
-
}
|
|
256
|
-
function resolveWorkerOptions() {
|
|
257
|
-
const registrationProfile = readRegistrationProfile();
|
|
258
|
-
const server = getFlag('server') ??
|
|
259
|
-
process.env['VIEWPORT_SERVER_URL'] ??
|
|
260
|
-
process.env['VPD_SERVER_URL'] ??
|
|
261
|
-
registrationProfile?.serverUrl;
|
|
262
|
-
const workspaceId = getFlag('workspace') ??
|
|
263
|
-
getFlag('resource') ??
|
|
264
|
-
process.env['VIEWPORT_WORKSPACE_ID'] ??
|
|
265
|
-
registrationProfile?.workspaceId;
|
|
266
|
-
const serverId = getFlag('server-id') ??
|
|
267
|
-
process.env['VIEWPORT_SERVER_ID'] ??
|
|
268
|
-
process.env['VPD_SERVER_ID'] ??
|
|
269
|
-
registrationProfile?.serverId;
|
|
270
|
-
const executorId = getFlag('executor') ??
|
|
271
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_ID'] ??
|
|
272
|
-
registrationProfile?.executorId;
|
|
273
|
-
const credential = getFlag('credential') ??
|
|
274
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_TOKEN'] ??
|
|
275
|
-
process.env['VPD_MANAGED_EXECUTOR_TOKEN'] ??
|
|
276
|
-
credentialFromFile(getFlag('credential-file') ??
|
|
277
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_TOKEN_FILE'] ??
|
|
278
|
-
process.env['VPD_MANAGED_EXECUTOR_TOKEN_FILE'] ??
|
|
279
|
-
registrationProfile?.credentialFile) ??
|
|
280
|
-
registrationProfile?.credential;
|
|
281
|
-
if (!server || !workspaceId || !executorId || !credential) {
|
|
282
|
-
throw new Error(workflowWorkerUsage());
|
|
283
|
-
}
|
|
284
|
-
const profileCapabilities = registrationProfile?.capabilities ?? {};
|
|
285
|
-
const profileRunnerPool = stringValue(profileCapabilities['runner_pool']) ??
|
|
286
|
-
stringValue(profileCapabilities['runnerPool']);
|
|
287
|
-
const runnerKeyPair = loadOrCreateRunnerKeyPair(workspaceId, executorId);
|
|
288
|
-
const signingIdentity = loadSigningIdentity(registrationProfile);
|
|
289
|
-
const detected = detectLocalCapabilities();
|
|
290
|
-
const sleepSeconds = positiveIntFlagValue(getFlag('sleep')) ?? 5;
|
|
291
|
-
return {
|
|
292
|
-
server: server.replace(/\/+$/, ''),
|
|
293
|
-
serverId,
|
|
294
|
-
workspaceId,
|
|
295
|
-
executorId,
|
|
296
|
-
credential,
|
|
297
|
-
accessMode: managedWorkerAccessMode(getFlag('access-mode') ??
|
|
298
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_ACCESS_MODE'] ??
|
|
299
|
-
registrationProfile?.accessMode),
|
|
300
|
-
runnerProfile: getFlag('runner-profile') ??
|
|
301
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_PROFILE'] ??
|
|
302
|
-
registrationProfile?.runnerProfile ??
|
|
303
|
-
undefined,
|
|
304
|
-
runnerPosture: registrationProfile?.runnerPosture,
|
|
305
|
-
workerSessionId: randomUUID(),
|
|
306
|
-
runnerKeyPair,
|
|
307
|
-
signingIdentity,
|
|
308
|
-
runnerPool: getFlag('runner-pool') ??
|
|
309
|
-
process.env['VIEWPORT_MANAGED_RUNNER_POOL'] ??
|
|
310
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_RUNNER_POOL'] ??
|
|
311
|
-
profileRunnerPool ??
|
|
312
|
-
undefined,
|
|
313
|
-
workdir: getFlag('workdir') ? path.resolve(getFlag('workdir')) : undefined,
|
|
314
|
-
leaseSeconds: positiveIntFlagValue(getFlag('lease')) ?? 300,
|
|
315
|
-
sleepSeconds,
|
|
316
|
-
commandSleepSeconds: commandPollSeconds(positiveIntFlagValue(getFlag('command-sleep')) ??
|
|
317
|
-
positiveIntFlagValue(process.env['VIEWPORT_MANAGED_EXECUTOR_COMMAND_SLEEP_SECONDS']), sleepSeconds),
|
|
318
|
-
maxRuns: positiveIntFlagValue(getFlag('max-runs')),
|
|
319
|
-
once: hasFlag('once'),
|
|
320
|
-
capabilities: {
|
|
321
|
-
runnerPool: getFlag('runner-pool') ??
|
|
322
|
-
process.env['VIEWPORT_MANAGED_RUNNER_POOL'] ??
|
|
323
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_RUNNER_POOL'] ??
|
|
324
|
-
profileRunnerPool ??
|
|
325
|
-
undefined,
|
|
326
|
-
agentCommand: getFlag('agent-command') ?? process.env['VIEWPORT_MANAGED_AGENT_COMMAND'],
|
|
327
|
-
actionCommand: getFlag('action-command') ?? process.env['VIEWPORT_MANAGED_ACTION_COMMAND'],
|
|
328
|
-
providerActions: hasFlag('provider-actions') || process.env['VIEWPORT_MANAGED_PROVIDER_ACTIONS'] === '1',
|
|
329
|
-
tools: [
|
|
330
|
-
...new Set([
|
|
331
|
-
...detected.tools,
|
|
332
|
-
...listFlagOrProfile('tools', profileCapabilities['tools']),
|
|
333
|
-
]),
|
|
334
|
-
],
|
|
335
|
-
agents: listFlagOrProfile('agents', profileCapabilities['agents']),
|
|
336
|
-
models: listFlagOrProfile('models', profileCapabilities['models']),
|
|
337
|
-
agentModels: agentModelsFromProfile(profileCapabilities['agents']),
|
|
338
|
-
integrations: [
|
|
339
|
-
...new Set([
|
|
340
|
-
...detected.integrations,
|
|
341
|
-
...listFlagOrProfile('integrations', profileCapabilities['integrations']),
|
|
342
|
-
]),
|
|
343
|
-
],
|
|
344
|
-
secrets: listFlagOrProfile('secrets', profileCapabilities['secrets']),
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
function workflowWorkerUsage() {
|
|
349
|
-
return 'Usage: vpd workflow worker --server <url> --workspace <id> --executor <id> (--credential-file <path>|--credential <token>) [--workdir <path>] [--runner-pool <pool>] [--agent-command <command>] [--action-command <command>] [--provider-actions] [--sleep <seconds>] [--command-sleep <seconds>] [--doctor|--preflight|--once]\n vpd workflow worker --registration-profile <path> [--sleep <seconds>] [--command-sleep <seconds>] [--doctor|--preflight|--once]';
|
|
350
|
-
}
|
|
351
|
-
function readRegistrationProfile() {
|
|
352
|
-
const profilePath = getFlag('registration-profile') ?? process.env['VIEWPORT_MANAGED_EXECUTOR_PROFILE_FILE'];
|
|
353
|
-
if (!profilePath)
|
|
354
|
-
return null;
|
|
355
|
-
const resolved = resolveProfilePath(profilePath);
|
|
356
|
-
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
357
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
358
|
-
throw new Error(`Managed executor registration profile is not a JSON object: ${resolved}`);
|
|
359
|
-
}
|
|
360
|
-
const record = parsed;
|
|
361
|
-
const daemon = recordValue(record['daemon']);
|
|
362
|
-
const worker = recordValue(daemon?.['worker']);
|
|
363
|
-
const schema = stringValue(record['schema']);
|
|
364
|
-
if (schema && schema !== 'viewport.managed_executor_registration/v1') {
|
|
365
|
-
throw new Error(`Unsupported managed executor registration profile schema: ${schema}`);
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
serverUrl: stringValue(record['server_url']) ??
|
|
369
|
-
stringValue(record['serverUrl']) ??
|
|
370
|
-
stringValue(worker?.['serverUrl']) ??
|
|
371
|
-
stringValue(worker?.['server_url']),
|
|
372
|
-
serverId: stringValue(record['server_id']) ??
|
|
373
|
-
stringValue(record['serverId']) ??
|
|
374
|
-
stringValue(record['control_plane_id']) ??
|
|
375
|
-
stringValue(worker?.['serverId']) ??
|
|
376
|
-
stringValue(worker?.['server_id']) ??
|
|
377
|
-
stringValue(worker?.['control_plane_id']),
|
|
378
|
-
workspaceId: stringValue(record['workspace_id']) ??
|
|
379
|
-
stringValue(record['workspaceId']) ??
|
|
380
|
-
stringValue(worker?.['workspaceId']) ??
|
|
381
|
-
stringValue(worker?.['workspace_id']),
|
|
382
|
-
executorId: stringValue(record['managed_executor_id']) ??
|
|
383
|
-
stringValue(record['executor_id']) ??
|
|
384
|
-
stringValue(record['executorId']) ??
|
|
385
|
-
stringValue(worker?.['managedExecutorId']) ??
|
|
386
|
-
stringValue(worker?.['managed_executor_id']),
|
|
387
|
-
credential: stringValue(record['credential']) ?? stringValue(worker?.['credential']),
|
|
388
|
-
credentialFile: stringValue(record['credential_file']) ??
|
|
389
|
-
stringValue(record['credentialFile']) ??
|
|
390
|
-
stringValue(worker?.['credentialFile']) ??
|
|
391
|
-
stringValue(worker?.['credential_file']),
|
|
392
|
-
accessMode: stringValue(record['access_mode']) ??
|
|
393
|
-
stringValue(record['accessMode']) ??
|
|
394
|
-
stringValue(worker?.['accessMode']) ??
|
|
395
|
-
stringValue(worker?.['access_mode']) ??
|
|
396
|
-
stringValue(worker?.['transport']),
|
|
397
|
-
runnerProfile: stringValue(record['runner_profile']) ??
|
|
398
|
-
stringValue(record['runnerProfile']) ??
|
|
399
|
-
stringValue(worker?.['runnerProfile']) ??
|
|
400
|
-
stringValue(worker?.['runner_profile']),
|
|
401
|
-
runnerPosture: recordValue(record['runner_posture']) ??
|
|
402
|
-
recordValue(record['runnerPosture']) ??
|
|
403
|
-
recordValue(worker?.['runnerPosture']) ??
|
|
404
|
-
recordValue(worker?.['runner_posture']),
|
|
405
|
-
identityKeyPath: stringValue(record['identity_key_path']) ??
|
|
406
|
-
stringValue(record['identityKeyPath']) ??
|
|
407
|
-
stringValue(worker?.['identityKeyPath']) ??
|
|
408
|
-
stringValue(worker?.['identity_key_path']),
|
|
409
|
-
capabilities: record['capabilities'] &&
|
|
410
|
-
typeof record['capabilities'] === 'object' &&
|
|
411
|
-
!Array.isArray(record['capabilities'])
|
|
412
|
-
? record['capabilities']
|
|
413
|
-
: (recordValue(worker?.['capabilities']) ?? undefined),
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
function credentialFromFile(filePath) {
|
|
417
|
-
if (!filePath)
|
|
418
|
-
return undefined;
|
|
419
|
-
const resolved = path.resolve(filePath);
|
|
420
|
-
let value;
|
|
421
|
-
try {
|
|
422
|
-
value = fs.readFileSync(resolved, 'utf8').trim();
|
|
423
|
-
}
|
|
424
|
-
catch (error) {
|
|
425
|
-
throw new Error(`Unable to read managed executor credential file ${resolved}: ${errorMessage(error)}`);
|
|
426
|
-
}
|
|
427
|
-
if (!value) {
|
|
428
|
-
throw new Error(`Managed executor credential file is empty: ${resolved}`);
|
|
429
|
-
}
|
|
430
|
-
return value;
|
|
431
|
-
}
|
|
432
|
-
function resolveProfilePath(profilePath) {
|
|
433
|
-
if (profilePath === '~')
|
|
434
|
-
return os.homedir();
|
|
435
|
-
if (profilePath.startsWith('~/'))
|
|
436
|
-
return path.join(os.homedir(), profilePath.slice(2));
|
|
437
|
-
return path.resolve(profilePath);
|
|
438
|
-
}
|
|
439
|
-
function loadOrCreateRunnerKeyPair(workspaceId, executorId) {
|
|
440
|
-
const keyDir = path.join(os.homedir(), '.viewport', 'runner-keys');
|
|
441
|
-
fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
|
|
442
|
-
const safeName = `${safeFilename(workspaceId)}-${safeFilename(executorId)}.json`;
|
|
443
|
-
const keyPath = path.join(keyDir, safeName);
|
|
444
|
-
if (fs.existsSync(keyPath)) {
|
|
445
|
-
const parsed = JSON.parse(fs.readFileSync(keyPath, 'utf8'));
|
|
446
|
-
if (isRunnerKeyPair(parsed, keyPath))
|
|
447
|
-
return parsed;
|
|
448
|
-
}
|
|
449
|
-
const pair = generateKeyPairSync('rsa', {
|
|
450
|
-
modulusLength: 2048,
|
|
451
|
-
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
452
|
-
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
453
|
-
});
|
|
454
|
-
const keyPair = {
|
|
455
|
-
schema: 'viewport.runner_keypair/v1',
|
|
456
|
-
algorithm: 'RSA-OAEP-256',
|
|
457
|
-
publicKeyPem: pair.publicKey,
|
|
458
|
-
privateKeyPem: pair.privateKey,
|
|
459
|
-
fingerprint: publicKeyFingerprint(pair.publicKey),
|
|
460
|
-
path: keyPath,
|
|
461
|
-
};
|
|
462
|
-
fs.writeFileSync(keyPath, JSON.stringify(keyPair, null, 2), { mode: 0o600 });
|
|
463
|
-
return keyPair;
|
|
464
|
-
}
|
|
465
|
-
function isRunnerKeyPair(value, keyPath) {
|
|
466
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
467
|
-
return false;
|
|
468
|
-
const record = value;
|
|
469
|
-
if (record['schema'] !== 'viewport.runner_keypair/v1' ||
|
|
470
|
-
record['algorithm'] !== 'RSA-OAEP-256' ||
|
|
471
|
-
typeof record['publicKeyPem'] !== 'string' ||
|
|
472
|
-
typeof record['privateKeyPem'] !== 'string' ||
|
|
473
|
-
typeof record['fingerprint'] !== 'string') {
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
|
-
if (typeof record['path'] !== 'string') {
|
|
477
|
-
record['path'] = keyPath;
|
|
478
|
-
}
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
function publicKeyFingerprint(publicKeyPem) {
|
|
482
|
-
const body = publicKeyPem
|
|
483
|
-
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
484
|
-
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
485
|
-
.replace(/\s+/g, '');
|
|
486
|
-
const der = Buffer.from(body, 'base64');
|
|
487
|
-
return `sha256:${createHash('sha256').update(der).digest('hex')}`;
|
|
488
|
-
}
|
|
489
|
-
function loadSigningIdentity(registrationProfile) {
|
|
490
|
-
const identityPath = getFlag('identity-key') ??
|
|
491
|
-
process.env['VIEWPORT_WORKER_IDENTITY_FILE'] ??
|
|
492
|
-
process.env['VIEWPORT_MANAGED_EXECUTOR_IDENTITY_FILE'] ??
|
|
493
|
-
registrationProfile?.identityKeyPath;
|
|
494
|
-
if (!identityPath)
|
|
495
|
-
return undefined;
|
|
496
|
-
const resolved = resolveProfilePath(identityPath);
|
|
497
|
-
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
498
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
499
|
-
throw new Error(`Worker identity file is not a JSON object: ${resolved}`);
|
|
500
|
-
}
|
|
501
|
-
const record = parsed;
|
|
502
|
-
const algorithm = stringValue(record['algorithm'])?.toLowerCase();
|
|
503
|
-
const publicKeyPem = stringValue(record['public_key_pem']) ??
|
|
504
|
-
stringValue(record['publicKeyPem']) ??
|
|
505
|
-
stringValue(record['publicKey']) ??
|
|
506
|
-
stringValue(record['public_key']);
|
|
507
|
-
const privateKeyPem = stringValue(record['private_key_pem']) ??
|
|
508
|
-
stringValue(record['privateKeyPem']) ??
|
|
509
|
-
stringValue(record['privateKey']) ??
|
|
510
|
-
stringValue(record['private_key']);
|
|
511
|
-
const fingerprint = normalizeWorkerFingerprint(stringValue(record['fingerprint']) ??
|
|
512
|
-
stringValue(record['publicKeyFingerprint']) ??
|
|
513
|
-
stringValue(record['public_key_fingerprint']) ??
|
|
514
|
-
'');
|
|
515
|
-
if (algorithm !== 'ed25519' ||
|
|
516
|
-
!publicKeyPem ||
|
|
517
|
-
!privateKeyPem ||
|
|
518
|
-
!/^[a-f0-9]{64}$/i.test(fingerprint)) {
|
|
519
|
-
throw new Error(`Worker identity file is not a supported ed25519 identity: ${resolved}`);
|
|
520
|
-
}
|
|
521
|
-
return {
|
|
522
|
-
algorithm: 'ed25519',
|
|
523
|
-
publicKeyPem,
|
|
524
|
-
privateKeyPem,
|
|
525
|
-
fingerprint: fingerprint.toLowerCase(),
|
|
526
|
-
serverId: stringValue(record['server_id']) ??
|
|
527
|
-
stringValue(record['serverId']) ??
|
|
528
|
-
stringValue(record['control_plane_id']) ??
|
|
529
|
-
registrationProfile?.serverId,
|
|
530
|
-
path: resolved,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
function normalizeWorkerFingerprint(fingerprint) {
|
|
534
|
-
return fingerprint.startsWith('sha256:') ? fingerprint.slice('sha256:'.length) : fingerprint;
|
|
535
|
-
}
|
|
536
|
-
function safeFilename(value) {
|
|
537
|
-
return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'runner';
|
|
538
|
-
}
|
|
539
|
-
function detectLocalCapabilities() {
|
|
540
|
-
const tools = ['git', 'node', 'pnpm', 'docker', 'gh'].filter(commandExists);
|
|
541
|
-
const integrations = [
|
|
542
|
-
...(commandExists('gh') ? ['github'] : []),
|
|
543
|
-
...(process.env['NOTION_TOKEN'] ? ['notion'] : []),
|
|
544
|
-
...(process.env['CONFLUENCE_API_TOKEN'] && process.env['CONFLUENCE_BASE_URL']
|
|
545
|
-
? ['confluence']
|
|
546
|
-
: []),
|
|
547
|
-
];
|
|
548
|
-
return { tools, integrations };
|
|
549
|
-
}
|
|
550
|
-
function commandExists(command) {
|
|
551
|
-
return (spawnSync('sh', ['-lc', `command -v ${shellQuote(command)} >/dev/null 2>&1`], {
|
|
552
|
-
stdio: 'ignore',
|
|
553
|
-
}).status === 0);
|
|
554
|
-
}
|
|
555
|
-
function shellQuote(value) {
|
|
556
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
557
|
-
}
|
|
558
|
-
function listFlagOrProfile(flag, profileValue) {
|
|
559
|
-
const fromFlag = listFlagValue(getFlag(flag));
|
|
560
|
-
return fromFlag.length > 0 ? fromFlag : stringList(profileValue);
|
|
561
|
-
}
|
|
562
|
-
function stringList(value) {
|
|
563
|
-
if (Array.isArray(value)) {
|
|
564
|
-
return value.filter((entry) => typeof entry === 'string' && entry.trim() !== '');
|
|
565
|
-
}
|
|
566
|
-
if (value && typeof value === 'object') {
|
|
567
|
-
return Object.entries(value)
|
|
568
|
-
.map(([key, entry]) => {
|
|
569
|
-
if (typeof entry === 'string')
|
|
570
|
-
return entry;
|
|
571
|
-
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
572
|
-
return stringValue(entry['id']) ?? key;
|
|
573
|
-
}
|
|
574
|
-
return key;
|
|
575
|
-
})
|
|
576
|
-
.filter((entry) => typeof entry === 'string' && entry.trim() !== '');
|
|
577
|
-
}
|
|
578
|
-
return [];
|
|
579
|
-
}
|
|
580
|
-
function agentModelsFromProfile(agents) {
|
|
581
|
-
if (!agents || typeof agents !== 'object' || Array.isArray(agents))
|
|
582
|
-
return undefined;
|
|
583
|
-
const entries = Object.entries(agents)
|
|
584
|
-
.map(([key, entry]) => {
|
|
585
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
586
|
-
return null;
|
|
587
|
-
const record = entry;
|
|
588
|
-
const id = stringValue(record['id']) ?? key;
|
|
589
|
-
const models = stringList(record['models']);
|
|
590
|
-
return id.trim() !== '' && models.length > 0 ? [id, models] : null;
|
|
591
|
-
})
|
|
592
|
-
.filter((entry) => entry !== null);
|
|
593
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
594
|
-
}
|
|
595
|
-
function stringValue(value) {
|
|
596
|
-
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
597
|
-
}
|
|
598
|
-
function errorMessage(error) {
|
|
599
|
-
return error instanceof Error ? error.message : String(error);
|
|
600
|
-
}
|
|
601
|
-
async function heartbeat(options, status, healthStatus) {
|
|
602
|
-
await platformJson(options, 'POST', 'heartbeat', {
|
|
603
|
-
status,
|
|
604
|
-
health_status: healthStatus,
|
|
605
|
-
access_mode: options.accessMode,
|
|
606
|
-
runner_profile: options.runnerProfile ?? options.runnerPool ?? null,
|
|
607
|
-
runner_posture: {
|
|
608
|
-
...(options.runnerPosture ?? {}),
|
|
609
|
-
transport: {
|
|
610
|
-
...recordValue(options.runnerPosture?.['transport']),
|
|
611
|
-
mode: options.accessMode,
|
|
612
|
-
},
|
|
613
|
-
execution: {
|
|
614
|
-
...(recordValue(options.runnerPosture?.['execution']) ?? { kind: 'customer-managed' }),
|
|
615
|
-
worker_session_id: options.workerSessionId,
|
|
616
|
-
},
|
|
617
|
-
secrets: {
|
|
618
|
-
...recordValue(options.runnerPosture?.['secrets']),
|
|
619
|
-
modes: [
|
|
620
|
-
...new Set([
|
|
621
|
-
'runner_local',
|
|
622
|
-
'runner_encrypted',
|
|
623
|
-
'run_scoped_grant',
|
|
624
|
-
...stringList(recordValue(options.runnerPosture?.['secrets'])?.['modes']),
|
|
625
|
-
]),
|
|
626
|
-
],
|
|
627
|
-
public_key: {
|
|
628
|
-
schema: 'viewport.runner_public_key/v1',
|
|
629
|
-
algorithm: options.runnerKeyPair.algorithm,
|
|
630
|
-
public_key_pem: options.runnerKeyPair.publicKeyPem,
|
|
631
|
-
fingerprint: options.runnerKeyPair.fingerprint,
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
model_credentials: {
|
|
635
|
-
...recordValue(options.runnerPosture?.['model_credentials']),
|
|
636
|
-
anthropic: process.env['ANTHROPIC_API_KEY'] ? 'available' : 'missing',
|
|
637
|
-
openai: process.env['OPENAI_API_KEY'] ? 'available' : 'missing',
|
|
638
|
-
},
|
|
639
|
-
repo_credentials: {
|
|
640
|
-
...recordValue(options.runnerPosture?.['repo_credentials']),
|
|
641
|
-
runner_local: process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'] || commandExists('gh')
|
|
642
|
-
? 'available'
|
|
643
|
-
: 'missing',
|
|
644
|
-
run_scoped_grant: 'available',
|
|
645
|
-
},
|
|
646
|
-
context_worker: {
|
|
647
|
-
...recordValue(options.runnerPosture?.['context_worker']),
|
|
648
|
-
enabled: true,
|
|
649
|
-
supports: [
|
|
650
|
-
...new Set([
|
|
651
|
-
'git',
|
|
652
|
-
...(process.env['NOTION_TOKEN'] ? ['notion'] : []),
|
|
653
|
-
...(process.env['CONFLUENCE_API_TOKEN'] && process.env['CONFLUENCE_BASE_URL']
|
|
654
|
-
? ['confluence']
|
|
655
|
-
: []),
|
|
656
|
-
]),
|
|
657
|
-
],
|
|
658
|
-
},
|
|
659
|
-
version: stringValue(options.runnerPosture?.['version']) ??
|
|
660
|
-
process.env['npm_package_version'] ??
|
|
661
|
-
null,
|
|
662
|
-
},
|
|
663
|
-
capabilities: capabilityPayload(options.capabilities),
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
async function safeHeartbeat(options, status, healthStatus) {
|
|
667
|
-
try {
|
|
668
|
-
await heartbeat(options, status, healthStatus);
|
|
669
|
-
}
|
|
670
|
-
catch {
|
|
671
|
-
// The worker is exiting or already degraded; do not mask the primary result.
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
function managedWorkerAccessMode(value) {
|
|
675
|
-
if (value === 'polling' || value === 'direct' || value === 'relay')
|
|
676
|
-
return value;
|
|
677
|
-
return 'polling';
|
|
678
|
-
}
|
|
679
|
-
async function claimAssignment(options) {
|
|
680
|
-
const response = await platformFetch(options, 'POST', 'claim', {
|
|
681
|
-
lease_seconds: options.leaseSeconds,
|
|
682
|
-
worker_session_id: options.workerSessionId,
|
|
683
|
-
});
|
|
684
|
-
if (response.status === 204)
|
|
685
|
-
return null;
|
|
686
|
-
const body = await responseJson(response);
|
|
687
|
-
return assignmentFrom(body);
|
|
688
|
-
}
|
|
689
|
-
async function claimActionReplay(options) {
|
|
690
|
-
if ((!options.capabilities.actionCommand && !options.capabilities.providerActions) ||
|
|
691
|
-
options.capabilities.integrations.length === 0) {
|
|
692
|
-
return null;
|
|
693
|
-
}
|
|
694
|
-
const response = await platformFetch(options, 'POST', 'action-replays/claim', {
|
|
695
|
-
lease_seconds: options.leaseSeconds,
|
|
696
|
-
});
|
|
697
|
-
if (response.status === 204)
|
|
698
|
-
return null;
|
|
699
|
-
const body = await responseJson(response);
|
|
700
|
-
return dataFrom(body);
|
|
701
|
-
}
|
|
702
|
-
async function completeActionReplay(options, assignment) {
|
|
703
|
-
if (!options.capabilities.actionCommand && !options.capabilities.providerActions) {
|
|
704
|
-
throw new Error('Action replay execution is not configured.');
|
|
705
|
-
}
|
|
706
|
-
if (!assignment.claim_token) {
|
|
707
|
-
throw new Error(`Managed action replay ${assignment.id} is missing claim_token.`);
|
|
708
|
-
}
|
|
709
|
-
await heartbeat(options, 'online', 'busy');
|
|
710
|
-
const result = options.capabilities.actionCommand
|
|
711
|
-
? await runActionReplayCommand(options.capabilities.actionCommand, assignment)
|
|
712
|
-
: await runProviderActionReplay(assignment);
|
|
713
|
-
const body = await platformJson(options, 'PATCH', `action-replays/${encodeURIComponent(assignment.id)}/complete`, result, undefined, { 'X-Viewport-Action-Replay-Claim': assignment.claim_token });
|
|
714
|
-
return dataFrom(body);
|
|
715
|
-
}
|
|
716
|
-
async function runProviderActionReplay(assignment) {
|
|
717
|
-
const actionInput = actionInputFromReplay(assignment);
|
|
718
|
-
if (!actionInput) {
|
|
719
|
-
return {
|
|
720
|
-
status: 'failed',
|
|
721
|
-
idempotency_key: assignment.idempotency_key ?? undefined,
|
|
722
|
-
payload_digest: assignment.action_digest ?? undefined,
|
|
723
|
-
provider_response: assignment.provider_response ?? undefined,
|
|
724
|
-
error: 'Action replay is missing the original action proposal payload.',
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
const nodeId = assignment.action_proposal?.node_key ?? assignment.workflow_run_node_id ?? assignment.id;
|
|
728
|
-
const node = {
|
|
729
|
-
type: 'action',
|
|
730
|
-
adapter: assignment.adapter,
|
|
731
|
-
action: normalizeReplayAction(assignment.adapter, assignment.action),
|
|
732
|
-
with: actionInput,
|
|
733
|
-
idempotencyKey: assignment.idempotency_key ?? undefined,
|
|
734
|
-
requiresApproval: false,
|
|
735
|
-
};
|
|
736
|
-
const run = actionReplayRunRecord(assignment);
|
|
737
|
-
try {
|
|
738
|
-
const result = await executeProviderAction(run, nodeId, node, actionInput, {
|
|
739
|
-
idempotencyKey: assignment.idempotency_key ?? undefined,
|
|
740
|
-
});
|
|
741
|
-
if (!result) {
|
|
742
|
-
return {
|
|
743
|
-
status: 'failed',
|
|
744
|
-
idempotency_key: assignment.idempotency_key ?? undefined,
|
|
745
|
-
payload_digest: assignment.action_digest ?? undefined,
|
|
746
|
-
provider_response: assignment.provider_response ?? undefined,
|
|
747
|
-
error: `No built-in provider action adapter exists for ${assignment.adapter}.${assignment.action}.`,
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
const action = recordValue(result.metadata['action']);
|
|
751
|
-
const response = recordValue(action?.['response']);
|
|
752
|
-
const providerReconciliation = recordValue(action?.['provider_reconciliation']) ??
|
|
753
|
-
recordValue(action?.['providerReconciliation']);
|
|
754
|
-
return {
|
|
755
|
-
status: 'succeeded',
|
|
756
|
-
provider_reference: replayProviderReference(response),
|
|
757
|
-
provider_url: replayProviderUrl(response),
|
|
758
|
-
idempotency_key: assignment.idempotency_key ?? stringField(action, 'idempotencyKey'),
|
|
759
|
-
payload_digest: assignment.action_digest ?? stringField(action, 'digest'),
|
|
760
|
-
payload: {
|
|
761
|
-
action: {
|
|
762
|
-
adapter: assignment.adapter,
|
|
763
|
-
action: assignment.action,
|
|
764
|
-
input: sanitizeActionInput(actionInput),
|
|
765
|
-
response: response ?? null,
|
|
766
|
-
},
|
|
767
|
-
},
|
|
768
|
-
provider_response: response ?? result.metadata,
|
|
769
|
-
provider_reconciliation: providerReconciliation,
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
catch (error) {
|
|
773
|
-
if (error instanceof WorkflowActionError) {
|
|
774
|
-
const action = recordValue(error.result.metadata['action']);
|
|
775
|
-
const response = recordValue(action?.['response']);
|
|
776
|
-
const providerReconciliation = recordValue(action?.['provider_reconciliation']) ??
|
|
777
|
-
recordValue(action?.['providerReconciliation']);
|
|
778
|
-
return {
|
|
779
|
-
status: 'failed',
|
|
780
|
-
idempotency_key: assignment.idempotency_key ?? stringField(action, 'idempotencyKey'),
|
|
781
|
-
payload_digest: assignment.action_digest ?? stringField(action, 'digest'),
|
|
782
|
-
payload: {
|
|
783
|
-
action: {
|
|
784
|
-
adapter: assignment.adapter,
|
|
785
|
-
action: assignment.action,
|
|
786
|
-
input: sanitizeActionInput(actionInput),
|
|
787
|
-
response: response ?? null,
|
|
788
|
-
},
|
|
789
|
-
},
|
|
790
|
-
provider_response: response ?? error.result.metadata,
|
|
791
|
-
provider_reconciliation: providerReconciliation,
|
|
792
|
-
error: error.message,
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
throw error;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
function actionInputFromReplay(assignment) {
|
|
799
|
-
const proposalPayload = assignment.action_proposal?.payload;
|
|
800
|
-
const proposalInput = workflowInputRecord(proposalPayload);
|
|
801
|
-
if (proposalInput)
|
|
802
|
-
return proposalInput;
|
|
803
|
-
const payload = assignment.payload ?? {};
|
|
804
|
-
const actionPayload = recordValue(payload['action_payload']) ?? recordValue(payload['action']);
|
|
805
|
-
const replayPayload = workflowInputRecord(actionPayload);
|
|
806
|
-
return replayPayload ?? null;
|
|
807
|
-
}
|
|
808
|
-
function normalizeReplayAction(adapter, action) {
|
|
809
|
-
if (adapter === 'github' && action === 'pull_request.create')
|
|
810
|
-
return 'create_pull_request';
|
|
811
|
-
if (adapter === 'github' && action === 'issue.comment')
|
|
812
|
-
return 'comment_issue';
|
|
813
|
-
if (adapter === 'jira' && action === 'issue.comment')
|
|
814
|
-
return 'comment_issue';
|
|
815
|
-
if (adapter === 'slack' && action === 'message')
|
|
816
|
-
return 'post_message';
|
|
817
|
-
return action;
|
|
818
|
-
}
|
|
819
|
-
function replayProviderReference(response) {
|
|
820
|
-
return (numberField(response ?? null, 'number')?.toString() ??
|
|
821
|
-
stringField(response ?? null, 'ts') ??
|
|
822
|
-
stringField(response ?? null, 'id') ??
|
|
823
|
-
stringField(response ?? null, 'channel') ??
|
|
824
|
-
stringField(response ?? null, 'htmlUrl') ??
|
|
825
|
-
stringField(response ?? null, 'apiUrl') ??
|
|
826
|
-
stringField(response ?? null, 'url'));
|
|
827
|
-
}
|
|
828
|
-
function replayProviderUrl(response) {
|
|
829
|
-
return (stringField(response ?? null, 'htmlUrl') ??
|
|
830
|
-
stringField(response ?? null, 'apiUrl') ??
|
|
831
|
-
stringField(response ?? null, 'url'));
|
|
832
|
-
}
|
|
833
|
-
function workflowInputRecord(value) {
|
|
834
|
-
if (!isRecord(value))
|
|
835
|
-
return null;
|
|
836
|
-
return value;
|
|
837
|
-
}
|
|
838
|
-
function actionReplayRunRecord(assignment) {
|
|
839
|
-
const now = Date.now();
|
|
840
|
-
return {
|
|
841
|
-
id: assignment.workflow_run_id ?? assignment.id,
|
|
842
|
-
workflowName: 'action-replay',
|
|
843
|
-
sourceType: 'viewport_snapshot',
|
|
844
|
-
sourcePath: `viewport://action-replay/${assignment.id}`,
|
|
845
|
-
digest: assignment.action_digest ?? `action-replay:${assignment.id}`,
|
|
846
|
-
schema: 'viewport.workflow/v1',
|
|
847
|
-
yamlSnapshot: '',
|
|
848
|
-
directoryId: 'action-replay',
|
|
849
|
-
directoryPath: process.cwd(),
|
|
850
|
-
machineId: 'managed-executor',
|
|
851
|
-
initiation: 'cli',
|
|
852
|
-
status: 'running',
|
|
853
|
-
inputs: {},
|
|
854
|
-
preflight: { ok: true, issues: [] },
|
|
855
|
-
nodes: {},
|
|
856
|
-
artifacts: [],
|
|
857
|
-
events: [],
|
|
858
|
-
createdAt: now,
|
|
859
|
-
startedAt: now,
|
|
860
|
-
updatedAt: now,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
async function runActionReplayCommand(command, assignment) {
|
|
864
|
-
const input = {
|
|
865
|
-
id: assignment.id,
|
|
866
|
-
adapter: assignment.adapter,
|
|
867
|
-
action: assignment.action,
|
|
868
|
-
idempotency_key: assignment.idempotency_key ?? null,
|
|
869
|
-
action_digest: assignment.action_digest ?? null,
|
|
870
|
-
source_runtime_event_id: assignment.source_runtime_event_id ?? null,
|
|
871
|
-
workflow_run_id: assignment.workflow_run_id ?? null,
|
|
872
|
-
workflow_action_proposal_id: assignment.workflow_action_proposal_id ?? null,
|
|
873
|
-
source_execution_receipt_id: assignment.source_execution_receipt_id ?? null,
|
|
874
|
-
payload: assignment.payload ?? {},
|
|
875
|
-
provider_response: assignment.provider_response ?? null,
|
|
876
|
-
};
|
|
877
|
-
const result = await runShellCommand(command, `${JSON.stringify(input)}\n`);
|
|
878
|
-
if (result.exitCode !== 0) {
|
|
879
|
-
return {
|
|
880
|
-
status: 'failed',
|
|
881
|
-
idempotency_key: assignment.idempotency_key ?? undefined,
|
|
882
|
-
payload_digest: assignment.action_digest ?? undefined,
|
|
883
|
-
provider_response: outputResponse(result),
|
|
884
|
-
error: result.stderr || result.stdout || `Action replay command exited ${result.exitCode}.`,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
const parsed = parseCommandJson(result.stdout);
|
|
888
|
-
if (parsed === 'invalid') {
|
|
889
|
-
return {
|
|
890
|
-
status: 'failed',
|
|
891
|
-
idempotency_key: assignment.idempotency_key ?? undefined,
|
|
892
|
-
payload_digest: assignment.action_digest ?? undefined,
|
|
893
|
-
provider_response: outputResponse(result),
|
|
894
|
-
error: 'Action replay command stdout was not valid JSON.',
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
const status = replayStatus(parsed?.['status']);
|
|
898
|
-
return {
|
|
899
|
-
status,
|
|
900
|
-
provider_reference: stringField(parsed, 'provider_reference'),
|
|
901
|
-
provider_url: stringField(parsed, 'provider_url'),
|
|
902
|
-
idempotency_key: stringField(parsed, 'idempotency_key') ?? assignment.idempotency_key ?? undefined,
|
|
903
|
-
payload_digest: stringField(parsed, 'payload_digest') ?? assignment.action_digest ?? undefined,
|
|
904
|
-
payload: recordField(parsed, 'payload') ?? assignment.payload ?? undefined,
|
|
905
|
-
provider_response: recordField(parsed, 'provider_response') ?? outputResponse(result),
|
|
906
|
-
provider_reconciliation: recordField(parsed, 'provider_reconciliation'),
|
|
907
|
-
error: status === 'succeeded' ? undefined : (stringField(parsed, 'error') ?? result.stderr),
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
function replayStatus(value) {
|
|
911
|
-
if (value === 'failed' || value === 'dead_letter')
|
|
912
|
-
return value;
|
|
913
|
-
return 'succeeded';
|
|
914
|
-
}
|
|
915
|
-
function parseCommandJson(stdout) {
|
|
916
|
-
const trimmed = stdout.trim();
|
|
917
|
-
if (!trimmed)
|
|
918
|
-
return null;
|
|
919
|
-
try {
|
|
920
|
-
const parsed = JSON.parse(trimmed);
|
|
921
|
-
return isRecord(parsed) ? parsed : 'invalid';
|
|
922
|
-
}
|
|
923
|
-
catch {
|
|
924
|
-
return 'invalid';
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
function stringField(record, key) {
|
|
928
|
-
const value = record?.[key];
|
|
929
|
-
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
930
|
-
}
|
|
931
|
-
function recordField(record, key) {
|
|
932
|
-
const value = record?.[key];
|
|
933
|
-
return isRecord(value) ? value : undefined;
|
|
934
|
-
}
|
|
935
|
-
function recordValue(value) {
|
|
936
|
-
return isRecord(value) ? value : undefined;
|
|
937
|
-
}
|
|
938
|
-
function numberField(record, key) {
|
|
939
|
-
const value = record?.[key];
|
|
940
|
-
return typeof value === 'number' ? value : undefined;
|
|
941
|
-
}
|
|
942
|
-
function outputResponse(result) {
|
|
943
|
-
return {
|
|
944
|
-
stdout: result.stdout,
|
|
945
|
-
stderr: result.stderr,
|
|
946
|
-
exit_code: result.exitCode,
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
async function runShellCommand(command, stdin) {
|
|
950
|
-
return new Promise((resolve, reject) => {
|
|
951
|
-
const child = spawn('sh', ['-lc', command], {
|
|
952
|
-
cwd: process.cwd(),
|
|
953
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
954
|
-
env: process.env,
|
|
955
|
-
});
|
|
956
|
-
const stdout = [];
|
|
957
|
-
const stderr = [];
|
|
958
|
-
child.stdout.on('data', (chunk) => {
|
|
959
|
-
stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
960
|
-
});
|
|
961
|
-
child.stderr.on('data', (chunk) => {
|
|
962
|
-
stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
963
|
-
});
|
|
964
|
-
child.on('error', reject);
|
|
965
|
-
child.on('close', (code) => {
|
|
966
|
-
resolve({
|
|
967
|
-
exitCode: code ?? 1,
|
|
968
|
-
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
969
|
-
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
970
|
-
});
|
|
971
|
-
});
|
|
972
|
-
child.stdin.end(stdin);
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
async function runAssignmentLocally(options, assignment) {
|
|
976
|
-
if (!assignment.yaml_snapshot) {
|
|
977
|
-
throw new Error(`Managed workflow assignment ${assignment.id} is missing yaml_snapshot.`);
|
|
978
|
-
}
|
|
979
|
-
await heartbeat(options, 'online', 'busy');
|
|
980
|
-
const existingRun = await readExistingLocalRun(assignment.runtime_run_id);
|
|
981
|
-
if (existingRun) {
|
|
982
|
-
if (existingRun.status === 'running' || existingRun.status === 'queued') {
|
|
983
|
-
const completed = await pollLocalRun(existingRun.id, async (run) => {
|
|
984
|
-
try {
|
|
985
|
-
await syncLocalRun(options, assignment.id, run, assignment.assignment_claim_token);
|
|
986
|
-
}
|
|
987
|
-
catch (error) {
|
|
988
|
-
if (isPlatformAccessDenied(error)) {
|
|
989
|
-
return cancelLocalRun(existingRun.id);
|
|
990
|
-
}
|
|
991
|
-
throw error;
|
|
992
|
-
}
|
|
993
|
-
return cancelLocalRunIfAssignmentCanceled(options, assignment.id, existingRun.id, assignment.assignment_claim_token);
|
|
994
|
-
}, progressSyncEveryMs(options.leaseSeconds));
|
|
995
|
-
if (terminalRunStatus(completed.status)) {
|
|
996
|
-
clearRunCredentialMaterial(assignment.id);
|
|
997
|
-
}
|
|
998
|
-
return completed;
|
|
999
|
-
}
|
|
1000
|
-
if (terminalRunStatus(existingRun.status)) {
|
|
1001
|
-
clearRunCredentialMaterial(assignment.id);
|
|
1002
|
-
}
|
|
1003
|
-
return existingRun;
|
|
1004
|
-
}
|
|
1005
|
-
if (assignmentHasPlatformProgress(assignment)) {
|
|
1006
|
-
return missingLocalStateRun(assignment);
|
|
1007
|
-
}
|
|
1008
|
-
const directory = await ensureDirectory(options.workdir ?? assignment.directory_path ?? process.cwd());
|
|
1009
|
-
const material = await materializeAndCacheRunCredentials(options, assignment);
|
|
1010
|
-
const started = await daemonJson('POST', '/api/workflows/runs', {
|
|
1011
|
-
workflowYaml: assignment.yaml_snapshot,
|
|
1012
|
-
workflowSourceRef: assignment.source_ref ?? `viewport://managed-executor/${assignment.id}`,
|
|
1013
|
-
directoryId: directory.id,
|
|
1014
|
-
inputs: assignmentInputs(options, assignment, material),
|
|
1015
|
-
runtimeSecretEnv: material.runtimeSecretEnv,
|
|
1016
|
-
runtimeSecretFiles: material.runtimeSecretFiles,
|
|
1017
|
-
resourceId: options.workspaceId,
|
|
1018
|
-
runtimeTargetId: assignment.runtime_target_id ?? undefined,
|
|
1019
|
-
platformRunId: assignment.id,
|
|
1020
|
-
resourceManifest: assignmentResourceManifest(assignment) ?? undefined,
|
|
1021
|
-
workflowAuthorityContract: assignmentWorkflowAuthorityContract(assignment) ??
|
|
1022
|
-
recordChildValue(recordChildValue(assignment.input_snapshot, 'viewport'), 'workflowAuthorityContract') ??
|
|
1023
|
-
undefined,
|
|
1024
|
-
initiation: 'cli',
|
|
1025
|
-
dataCapturePolicy: {
|
|
1026
|
-
transcripts: assignment.data_capture_policy?.transcripts ?? 'none',
|
|
1027
|
-
logs: assignment.data_capture_policy?.logs ?? 'metadata',
|
|
1028
|
-
artifacts: assignment.data_capture_policy?.artifacts ?? 'metadata',
|
|
1029
|
-
},
|
|
1030
|
-
});
|
|
1031
|
-
const runId = readRun(started).id;
|
|
1032
|
-
const completed = await pollLocalRun(runId, async (run) => {
|
|
1033
|
-
try {
|
|
1034
|
-
await syncLocalRun(options, assignment.id, run, assignment.assignment_claim_token);
|
|
1035
|
-
}
|
|
1036
|
-
catch (error) {
|
|
1037
|
-
if (isPlatformAccessDenied(error)) {
|
|
1038
|
-
return cancelLocalRun(runId);
|
|
1039
|
-
}
|
|
1040
|
-
throw error;
|
|
1041
|
-
}
|
|
1042
|
-
return cancelLocalRunIfAssignmentCanceled(options, assignment.id, runId, assignment.assignment_claim_token);
|
|
1043
|
-
}, progressSyncEveryMs(options.leaseSeconds));
|
|
1044
|
-
if (terminalRunStatus(completed.status)) {
|
|
1045
|
-
clearRunCredentialMaterial(assignment.id);
|
|
1046
|
-
}
|
|
1047
|
-
return completed;
|
|
1048
|
-
}
|
|
1049
|
-
function assignmentHasPlatformProgress(assignment) {
|
|
1050
|
-
if (typeof assignment.runtime_run_id === 'string' && assignment.runtime_run_id.trim() !== '') {
|
|
1051
|
-
return true;
|
|
1052
|
-
}
|
|
1053
|
-
return (assignment.nodes ?? []).some((node) => {
|
|
1054
|
-
const status = node.status;
|
|
1055
|
-
return (status === 'running' ||
|
|
1056
|
-
status === 'blocked' ||
|
|
1057
|
-
status === 'completed' ||
|
|
1058
|
-
status === 'failed' ||
|
|
1059
|
-
status === 'canceled');
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
function missingLocalStateRun(assignment) {
|
|
1063
|
-
const now = Date.now();
|
|
1064
|
-
const runtimeRunId = typeof assignment.runtime_run_id === 'string' && assignment.runtime_run_id.trim() !== ''
|
|
1065
|
-
? assignment.runtime_run_id
|
|
1066
|
-
: `missing-local-state-${assignment.id}`;
|
|
1067
|
-
const message = 'Worker cannot safely resume this assignment because the local runtime state is missing. Start the original worker profile or rerun the workflow from the platform to avoid duplicate side effects.';
|
|
1068
|
-
return {
|
|
1069
|
-
id: runtimeRunId,
|
|
1070
|
-
workflowName: 'managed-assignment-recovery',
|
|
1071
|
-
sourceType: 'viewport_snapshot',
|
|
1072
|
-
sourcePath: assignment.source_ref ?? `viewport://managed-executor/${assignment.id}`,
|
|
1073
|
-
digest: `managed-assignment-recovery:${assignment.id}`,
|
|
1074
|
-
schema: 'viewport.workflow/v1',
|
|
1075
|
-
yamlSnapshot: assignment.yaml_snapshot ?? '',
|
|
1076
|
-
directoryId: 'managed-assignment-recovery',
|
|
1077
|
-
directoryPath: assignment.directory_path ?? process.cwd(),
|
|
1078
|
-
resourceId: undefined,
|
|
1079
|
-
runtimeTargetId: assignment.runtime_target_id ?? undefined,
|
|
1080
|
-
platformRunId: assignment.id,
|
|
1081
|
-
machineId: 'managed-executor',
|
|
1082
|
-
dataCapturePolicy: {
|
|
1083
|
-
transcripts: assignment.data_capture_policy?.transcripts ?? 'none',
|
|
1084
|
-
logs: assignment.data_capture_policy?.logs ?? 'metadata',
|
|
1085
|
-
artifacts: assignment.data_capture_policy?.artifacts ?? 'metadata',
|
|
1086
|
-
},
|
|
1087
|
-
initiation: 'cli',
|
|
1088
|
-
status: 'failed',
|
|
1089
|
-
inputs: assignment.input_snapshot ?? {},
|
|
1090
|
-
preflight: {
|
|
1091
|
-
ok: false,
|
|
1092
|
-
issues: [
|
|
1093
|
-
{
|
|
1094
|
-
kind: 'node',
|
|
1095
|
-
name: 'managed-assignment-recovery',
|
|
1096
|
-
message,
|
|
1097
|
-
},
|
|
1098
|
-
],
|
|
1099
|
-
},
|
|
1100
|
-
nodes: {},
|
|
1101
|
-
artifacts: [],
|
|
1102
|
-
events: [
|
|
1103
|
-
{
|
|
1104
|
-
id: 'managed-assignment-recovery-local-state-missing',
|
|
1105
|
-
runId: runtimeRunId,
|
|
1106
|
-
timestamp: now,
|
|
1107
|
-
type: 'run-failed',
|
|
1108
|
-
message,
|
|
1109
|
-
data: {
|
|
1110
|
-
platform_run_id: assignment.id,
|
|
1111
|
-
runtime_run_id: assignment.runtime_run_id ?? null,
|
|
1112
|
-
existing_node_statuses: Object.fromEntries((assignment.nodes ?? []).map((node) => [node.node_key, node.status ?? 'unknown'])),
|
|
1113
|
-
recovery_policy: 'fail_closed_missing_local_state',
|
|
1114
|
-
},
|
|
1115
|
-
},
|
|
1116
|
-
],
|
|
1117
|
-
createdAt: now,
|
|
1118
|
-
startedAt: now,
|
|
1119
|
-
updatedAt: now,
|
|
1120
|
-
completedAt: now,
|
|
1121
|
-
error: message,
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
function assignmentExecutionFailureRun(assignment, error) {
|
|
1125
|
-
const now = Date.now();
|
|
1126
|
-
const runtimeRunId = `assignment-start-failed-${assignment.id}`;
|
|
1127
|
-
const message = `Worker failed before it could start or resume the local workflow run: ${errorMessage(error)}`;
|
|
1128
|
-
return {
|
|
1129
|
-
id: runtimeRunId,
|
|
1130
|
-
workflowName: 'managed-assignment-start-failure',
|
|
1131
|
-
sourceType: 'viewport_snapshot',
|
|
1132
|
-
sourcePath: assignment.source_ref ?? `viewport://managed-executor/${assignment.id}`,
|
|
1133
|
-
digest: `managed-assignment-start-failure:${assignment.id}`,
|
|
1134
|
-
schema: 'viewport.workflow/v1',
|
|
1135
|
-
yamlSnapshot: assignment.yaml_snapshot ?? '',
|
|
1136
|
-
directoryId: 'managed-assignment-start-failure',
|
|
1137
|
-
directoryPath: assignment.directory_path ?? process.cwd(),
|
|
1138
|
-
resourceId: undefined,
|
|
1139
|
-
runtimeTargetId: assignment.runtime_target_id ?? undefined,
|
|
1140
|
-
platformRunId: assignment.id,
|
|
1141
|
-
machineId: 'managed-executor',
|
|
1142
|
-
dataCapturePolicy: {
|
|
1143
|
-
transcripts: assignment.data_capture_policy?.transcripts ?? 'none',
|
|
1144
|
-
logs: assignment.data_capture_policy?.logs ?? 'metadata',
|
|
1145
|
-
artifacts: assignment.data_capture_policy?.artifacts ?? 'metadata',
|
|
1146
|
-
},
|
|
1147
|
-
initiation: 'cli',
|
|
1148
|
-
status: 'failed',
|
|
1149
|
-
inputs: assignment.input_snapshot ?? {},
|
|
1150
|
-
preflight: {
|
|
1151
|
-
ok: false,
|
|
1152
|
-
issues: [
|
|
1153
|
-
{
|
|
1154
|
-
kind: 'node',
|
|
1155
|
-
name: 'managed-assignment-start-failure',
|
|
1156
|
-
message,
|
|
1157
|
-
},
|
|
1158
|
-
],
|
|
1159
|
-
},
|
|
1160
|
-
nodes: {},
|
|
1161
|
-
artifacts: [],
|
|
1162
|
-
events: [
|
|
1163
|
-
{
|
|
1164
|
-
id: 'managed-assignment-start-failure',
|
|
1165
|
-
runId: runtimeRunId,
|
|
1166
|
-
timestamp: now,
|
|
1167
|
-
type: 'run-failed',
|
|
1168
|
-
message,
|
|
1169
|
-
data: {
|
|
1170
|
-
platform_run_id: assignment.id,
|
|
1171
|
-
runtime_run_id: runtimeRunId,
|
|
1172
|
-
recovery_policy: 'sync_failed_assignment_start',
|
|
1173
|
-
},
|
|
1174
|
-
},
|
|
1175
|
-
],
|
|
1176
|
-
createdAt: now,
|
|
1177
|
-
startedAt: now,
|
|
1178
|
-
updatedAt: now,
|
|
1179
|
-
completedAt: now,
|
|
1180
|
-
error: message,
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
function assignmentCanceledBeforeStartRun(assignment) {
|
|
1184
|
-
const now = Date.now();
|
|
1185
|
-
const runtimeRunId = `assignment-canceled-before-start-${assignment.id}`;
|
|
1186
|
-
const message = 'Managed workflow assignment was canceled before local execution started.';
|
|
1187
|
-
return {
|
|
1188
|
-
id: runtimeRunId,
|
|
1189
|
-
workflowName: 'managed-assignment-canceled',
|
|
1190
|
-
sourceType: 'viewport_snapshot',
|
|
1191
|
-
sourcePath: assignment.source_ref ?? `viewport://managed-executor/${assignment.id}`,
|
|
1192
|
-
digest: `managed-assignment-canceled:${assignment.id}`,
|
|
1193
|
-
schema: 'viewport.workflow/v1',
|
|
1194
|
-
yamlSnapshot: assignment.yaml_snapshot ?? '',
|
|
1195
|
-
directoryId: 'managed-assignment-canceled',
|
|
1196
|
-
directoryPath: assignment.directory_path ?? process.cwd(),
|
|
1197
|
-
resourceId: undefined,
|
|
1198
|
-
runtimeTargetId: assignment.runtime_target_id ?? undefined,
|
|
1199
|
-
platformRunId: assignment.id,
|
|
1200
|
-
machineId: 'managed-executor',
|
|
1201
|
-
dataCapturePolicy: {
|
|
1202
|
-
transcripts: assignment.data_capture_policy?.transcripts ?? 'none',
|
|
1203
|
-
logs: assignment.data_capture_policy?.logs ?? 'metadata',
|
|
1204
|
-
artifacts: assignment.data_capture_policy?.artifacts ?? 'metadata',
|
|
1205
|
-
},
|
|
1206
|
-
initiation: 'cli',
|
|
1207
|
-
status: 'canceled',
|
|
1208
|
-
inputs: assignment.input_snapshot ?? {},
|
|
1209
|
-
preflight: {
|
|
1210
|
-
ok: false,
|
|
1211
|
-
issues: [
|
|
1212
|
-
{
|
|
1213
|
-
kind: 'node',
|
|
1214
|
-
name: 'managed-assignment-canceled',
|
|
1215
|
-
message,
|
|
1216
|
-
},
|
|
1217
|
-
],
|
|
1218
|
-
},
|
|
1219
|
-
nodes: {},
|
|
1220
|
-
artifacts: [],
|
|
1221
|
-
events: [
|
|
1222
|
-
{
|
|
1223
|
-
id: 'managed-assignment-canceled-before-start',
|
|
1224
|
-
runId: runtimeRunId,
|
|
1225
|
-
timestamp: now,
|
|
1226
|
-
type: 'run-canceled',
|
|
1227
|
-
message,
|
|
1228
|
-
data: {
|
|
1229
|
-
platform_run_id: assignment.id,
|
|
1230
|
-
runtime_run_id: runtimeRunId,
|
|
1231
|
-
recovery_policy: 'skip_local_execution_for_canceled_assignment',
|
|
1232
|
-
},
|
|
1233
|
-
},
|
|
1234
|
-
],
|
|
1235
|
-
createdAt: now,
|
|
1236
|
-
startedAt: now,
|
|
1237
|
-
updatedAt: now,
|
|
1238
|
-
completedAt: now,
|
|
1239
|
-
error: message,
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
function recordChildValue(value, key) {
|
|
1243
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1244
|
-
return undefined;
|
|
1245
|
-
const entry = value[key];
|
|
1246
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
1247
|
-
return undefined;
|
|
1248
|
-
return entry;
|
|
1249
|
-
}
|
|
1250
|
-
function assignmentInputs(options, assignment, material = {
|
|
1251
|
-
runtimeSecretEnv: {},
|
|
1252
|
-
runtimeSecretFiles: {},
|
|
1253
|
-
metadata: [],
|
|
1254
|
-
}) {
|
|
1255
|
-
const inputs = { ...(assignment.input_snapshot ?? {}) };
|
|
1256
|
-
inputs['viewport'] = {
|
|
1257
|
-
...(isRecord(inputs['viewport']) ? inputs['viewport'] : {}),
|
|
1258
|
-
platformRunId: assignment.id,
|
|
1259
|
-
serverUrl: options.server,
|
|
1260
|
-
appUrl: appUrlFromServer(options.server),
|
|
1261
|
-
schemaVersions: assignment.schema_versions ?? null,
|
|
1262
|
-
target: assignmentTargetSnapshot(assignment) ?? null,
|
|
1263
|
-
route: assignmentRouteSnapshot(assignment) ?? null,
|
|
1264
|
-
executionProfile: assignmentExecutionProfileSnapshot(assignment) ?? null,
|
|
1265
|
-
workflow: assignmentWorkflowSnapshot(assignment) ?? null,
|
|
1266
|
-
runnerWorkspace: assignmentRunnerWorkspaceSnapshot(assignment) ?? null,
|
|
1267
|
-
contextReceipts: assignmentContextReceiptsSnapshot(assignment) ?? null,
|
|
1268
|
-
credentials: material.metadata,
|
|
1269
|
-
};
|
|
1270
|
-
return inputs;
|
|
1271
|
-
}
|
|
1272
|
-
function appUrlFromServer(serverUrl) {
|
|
1273
|
-
try {
|
|
1274
|
-
const url = new URL(serverUrl);
|
|
1275
|
-
if (url.hostname.startsWith('api.')) {
|
|
1276
|
-
url.hostname = `app.${url.hostname.slice('api.'.length)}`;
|
|
1277
|
-
}
|
|
1278
|
-
else if (url.hostname === 'getviewport.com') {
|
|
1279
|
-
url.hostname = 'app.getviewport.com';
|
|
1280
|
-
}
|
|
1281
|
-
else if (url.hostname === 'getviewport.test') {
|
|
1282
|
-
url.hostname = 'app.getviewport.test';
|
|
1283
|
-
}
|
|
1284
|
-
else {
|
|
1285
|
-
return 'https://app.getviewport.com';
|
|
1286
|
-
}
|
|
1287
|
-
url.pathname = '';
|
|
1288
|
-
url.search = '';
|
|
1289
|
-
url.hash = '';
|
|
1290
|
-
return url.toString().replace(/\/+$/, '');
|
|
1291
|
-
}
|
|
1292
|
-
catch {
|
|
1293
|
-
return 'https://app.getviewport.com';
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
const runCredentialMaterialCache = new Map();
|
|
1297
|
-
const runCredentialProcessEnvCache = new Map();
|
|
1298
|
-
function terminalRunStatus(status) {
|
|
1299
|
-
return status === 'completed' || status === 'failed' || status === 'canceled';
|
|
1300
|
-
}
|
|
1301
|
-
async function materializeAndCacheRunCredentials(options, assignment) {
|
|
1302
|
-
const material = await materializeRunCredentials(options, assignment);
|
|
1303
|
-
runCredentialMaterialCache.set(assignment.id, material);
|
|
1304
|
-
installRunCredentialProcessEnv(assignment.id, material.runtimeSecretEnv);
|
|
1305
|
-
return material;
|
|
1306
|
-
}
|
|
1307
|
-
function installRunCredentialProcessEnv(runId, runtimeSecretEnv) {
|
|
1308
|
-
const entries = Object.entries(runtimeSecretEnv);
|
|
1309
|
-
if (entries.length === 0)
|
|
1310
|
-
return;
|
|
1311
|
-
const previous = runCredentialProcessEnvCache.get(runId) ?? {};
|
|
1312
|
-
for (const [key, value] of entries) {
|
|
1313
|
-
if (!(key in previous)) {
|
|
1314
|
-
previous[key] = process.env[key];
|
|
1315
|
-
}
|
|
1316
|
-
process.env[key] = value;
|
|
1317
|
-
}
|
|
1318
|
-
runCredentialProcessEnvCache.set(runId, previous);
|
|
1319
|
-
}
|
|
1320
|
-
function clearRunCredentialMaterial(runId) {
|
|
1321
|
-
const material = runCredentialMaterialCache.get(runId);
|
|
1322
|
-
runCredentialMaterialCache.delete(runId);
|
|
1323
|
-
if (material) {
|
|
1324
|
-
for (const filePath of Object.values(material.runtimeSecretFiles)) {
|
|
1325
|
-
try {
|
|
1326
|
-
fs.rmSync(filePath, { force: true });
|
|
1327
|
-
}
|
|
1328
|
-
catch {
|
|
1329
|
-
// Best-effort cleanup; the run-scoped directory is removed next.
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
try {
|
|
1334
|
-
fs.rmSync(path.join(process.env['VIEWPORT_HOME'] ?? path.join(os.homedir(), '.viewport'), 'run-secrets', runId), { recursive: true, force: true });
|
|
1335
|
-
}
|
|
1336
|
-
catch {
|
|
1337
|
-
// Best-effort cleanup.
|
|
1338
|
-
}
|
|
1339
|
-
const previous = runCredentialProcessEnvCache.get(runId);
|
|
1340
|
-
if (!previous)
|
|
1341
|
-
return;
|
|
1342
|
-
for (const [key, value] of Object.entries(previous)) {
|
|
1343
|
-
if (value === undefined) {
|
|
1344
|
-
delete process.env[key];
|
|
1345
|
-
}
|
|
1346
|
-
else {
|
|
1347
|
-
process.env[key] = value;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
runCredentialProcessEnvCache.delete(runId);
|
|
1351
|
-
}
|
|
1352
|
-
async function materializeRunCredentials(options, assignment) {
|
|
1353
|
-
const handles = collectCredentialHandles(assignment);
|
|
1354
|
-
if (handles.length === 0)
|
|
1355
|
-
return { runtimeSecretEnv: {}, runtimeSecretFiles: {}, metadata: [] };
|
|
1356
|
-
const runtimeSecretEnv = {};
|
|
1357
|
-
const runtimeSecretFiles = {};
|
|
1358
|
-
const metadata = [];
|
|
1359
|
-
for (const handle of handles) {
|
|
1360
|
-
const response = await materializeCredential(options, assignment, handle);
|
|
1361
|
-
const envName = envNameForCredentialRef(handle);
|
|
1362
|
-
const secret = stringField(response, 'secret');
|
|
1363
|
-
if (secret) {
|
|
1364
|
-
runtimeSecretEnv[envName] = secret;
|
|
1365
|
-
runtimeSecretFiles[envName] = await writeRunCredentialSecretFile(assignment.id, envName, secret);
|
|
1366
|
-
}
|
|
1367
|
-
const wrappedSecret = recordField(response, 'wrapped_secret');
|
|
1368
|
-
if (wrappedSecret) {
|
|
1369
|
-
const decrypted = decryptRunnerWrappedSecret(options.runnerKeyPair, wrappedSecret);
|
|
1370
|
-
runtimeSecretEnv[envName] = decrypted;
|
|
1371
|
-
runtimeSecretFiles[envName] = await writeRunCredentialSecretFile(assignment.id, envName, decrypted);
|
|
1372
|
-
}
|
|
1373
|
-
metadata.push({
|
|
1374
|
-
handle,
|
|
1375
|
-
envName,
|
|
1376
|
-
kind: stringField(response, 'kind') ?? null,
|
|
1377
|
-
storagePosture: stringField(response, 'storage_posture') ?? null,
|
|
1378
|
-
materialAvailable: response['material_available'] === true,
|
|
1379
|
-
runtimeSecretAvailable: typeof runtimeSecretEnv[envName] === 'string',
|
|
1380
|
-
runnerLocalRequired: response['runner_local_required'] === true,
|
|
1381
|
-
provider: stringField(response, 'provider') ?? null,
|
|
1382
|
-
credentialId: stringField(response, 'credential_id') ?? numberField(response, 'credential_id') ?? null,
|
|
1383
|
-
scopes: response['scopes'],
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
const missingRuntimeSecrets = metadata.filter((entry) => entry.materialAvailable && !entry.runnerLocalRequired && !entry.runtimeSecretAvailable);
|
|
1387
|
-
if (missingRuntimeSecrets.length > 0) {
|
|
1388
|
-
const handles = missingRuntimeSecrets.map((entry) => entry.handle).join(', ');
|
|
1389
|
-
throw new Error(`Credential materialization for ${assignment.id} returned material_available without runtime secret material for: ${handles}`);
|
|
1390
|
-
}
|
|
1391
|
-
return { runtimeSecretEnv, runtimeSecretFiles, metadata };
|
|
1392
|
-
}
|
|
1393
|
-
async function writeRunCredentialSecretFile(runId, envName, secret) {
|
|
1394
|
-
const root = path.join(process.env['VIEWPORT_HOME'] ?? path.join(os.homedir(), '.viewport'), 'run-secrets', runId);
|
|
1395
|
-
await fs.promises.mkdir(root, { recursive: true, mode: 0o700 });
|
|
1396
|
-
const filePath = path.join(root, envName);
|
|
1397
|
-
await fs.promises.writeFile(filePath, secret, { mode: 0o600 });
|
|
1398
|
-
return filePath;
|
|
1399
|
-
}
|
|
1400
|
-
async function materializeCredential(options, assignment, handle) {
|
|
1401
|
-
if (!assignment.assignment_claim_token) {
|
|
1402
|
-
throw new Error(`Managed workflow assignment ${assignment.id} is missing claim_token.`);
|
|
1403
|
-
}
|
|
1404
|
-
const body = await platformJson(options, 'POST', `workflow-runs/${encodeURIComponent(assignment.id)}/credential-material`, {
|
|
1405
|
-
credential: options.credential,
|
|
1406
|
-
handle,
|
|
1407
|
-
...repositoryForCredentialMaterialization(assignment, handle),
|
|
1408
|
-
}, assignment.assignment_claim_token);
|
|
1409
|
-
const data = dataFrom(body);
|
|
1410
|
-
if (!isRecord(data)) {
|
|
1411
|
-
throw new Error(`Credential material response for ${handle} was not an object.`);
|
|
1412
|
-
}
|
|
1413
|
-
return data;
|
|
1414
|
-
}
|
|
1415
|
-
function repositoryForCredentialMaterialization(assignment, handle) {
|
|
1416
|
-
const actionRepositories = repositoriesFromActionCredentialRef(assignment, handle);
|
|
1417
|
-
if (actionRepositories.length === 1 && actionRepositories[0]) {
|
|
1418
|
-
return { repository: actionRepositories[0] };
|
|
1419
|
-
}
|
|
1420
|
-
if (actionRepositories.length > 1) {
|
|
1421
|
-
return { repositories: actionRepositories };
|
|
1422
|
-
}
|
|
1423
|
-
const checkoutEntries = [
|
|
1424
|
-
...credentialEntriesFrom(pathValue(asRecord(assignment.execution_profile_snapshot), ['credentials', 'repo_checkout'])),
|
|
1425
|
-
...credentialEntriesFrom(pathValue(asRecord(assignment.workflow_snapshot), ['credentials', 'repo_checkout'])),
|
|
1426
|
-
];
|
|
1427
|
-
const explicit = checkoutEntries.find((entry) => {
|
|
1428
|
-
if (!isRecord(entry))
|
|
1429
|
-
return entry === handle;
|
|
1430
|
-
const entryHandle = stringField(entry, 'handle') ??
|
|
1431
|
-
stringField(entry, 'ref') ??
|
|
1432
|
-
stringField(entry, 'credential_ref');
|
|
1433
|
-
return entryHandle === handle;
|
|
1434
|
-
});
|
|
1435
|
-
const explicitRepo = explicit && isRecord(explicit)
|
|
1436
|
-
? (stringField(explicit, 'repository') ?? stringField(explicit, 'repo'))
|
|
1437
|
-
: null;
|
|
1438
|
-
if (explicitRepo)
|
|
1439
|
-
return { repository: explicitRepo };
|
|
1440
|
-
const allowed = allowedRepositoriesFromAssignment(assignment);
|
|
1441
|
-
return allowed.length === 1 && allowed[0] ? { repository: allowed[0] } : {};
|
|
1442
|
-
}
|
|
1443
|
-
function allowedRepositoriesFromAssignment(assignment) {
|
|
1444
|
-
const candidates = assignmentWorkflowAuthorityContracts(assignment).map((contract) => pathValue(contract, ['repos', 'allowed']));
|
|
1445
|
-
return [
|
|
1446
|
-
...new Set(candidates
|
|
1447
|
-
.flatMap((value) => (Array.isArray(value) ? value : []))
|
|
1448
|
-
.filter((value) => typeof value === 'string' && value.trim() !== '')
|
|
1449
|
-
.map((value) => value.trim())),
|
|
1450
|
-
];
|
|
1451
|
-
}
|
|
1452
|
-
function decryptRunnerWrappedSecret(keyPair, wrapped) {
|
|
1453
|
-
const schema = stringField(wrapped, 'schema');
|
|
1454
|
-
const algorithm = stringField(wrapped, 'algorithm');
|
|
1455
|
-
const fingerprint = stringField(wrapped, 'runner_public_key_fingerprint');
|
|
1456
|
-
const ciphertext = stringField(wrapped, 'ciphertext');
|
|
1457
|
-
if (schema !== 'viewport.runner_wrapped_secret/v1' ||
|
|
1458
|
-
algorithm !== 'RSA-OAEP-256' ||
|
|
1459
|
-
!fingerprint ||
|
|
1460
|
-
!ciphertext) {
|
|
1461
|
-
throw new Error('Runner-encrypted credential material is malformed.');
|
|
1462
|
-
}
|
|
1463
|
-
if (fingerprint !== keyPair.fingerprint) {
|
|
1464
|
-
throw new Error(`Runner-encrypted credential was wrapped for ${fingerprint}, but this runner key is ${keyPair.fingerprint}. Rotate or re-wrap the credential for this runner pool.`);
|
|
1465
|
-
}
|
|
1466
|
-
return privateDecrypt({
|
|
1467
|
-
key: keyPair.privateKeyPem,
|
|
1468
|
-
oaepHash: 'sha256',
|
|
1469
|
-
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
1470
|
-
}, Buffer.from(ciphertext, 'base64')).toString('utf8');
|
|
1471
|
-
}
|
|
1472
|
-
function collectCredentialHandles(assignment) {
|
|
1473
|
-
const snapshots = [
|
|
1474
|
-
assignmentTargetSnapshot(assignment),
|
|
1475
|
-
assignmentExecutionProfileSnapshot(assignment),
|
|
1476
|
-
assignmentWorkflowSnapshot(assignment),
|
|
1477
|
-
yamlSnapshotDocument(assignment),
|
|
1478
|
-
].filter(isRecord);
|
|
1479
|
-
const handles = new Set();
|
|
1480
|
-
for (const snapshot of snapshots) {
|
|
1481
|
-
for (const handle of [
|
|
1482
|
-
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'include'])),
|
|
1483
|
-
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'repo_checkout'])),
|
|
1484
|
-
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'mcp_api'])),
|
|
1485
|
-
...credentialRefsFromCredentialMap(snapshot['credentials']),
|
|
1486
|
-
...credentialRefsFrom(snapshot['credential_refs']),
|
|
1487
|
-
...actionCredentialRefs(snapshot['nodes']),
|
|
1488
|
-
]) {
|
|
1489
|
-
handles.add(handle);
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
for (const contract of assignmentWorkflowAuthorityContracts(assignment)) {
|
|
1493
|
-
for (const handle of credentialRefsFrom(pathValue(asRecord(contract), ['credentials', 'provider_actions']))) {
|
|
1494
|
-
handles.add(handle);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
return [...handles].sort();
|
|
1498
|
-
}
|
|
1499
|
-
function credentialRefsFromCredentialMap(entries) {
|
|
1500
|
-
if (!isRecord(entries))
|
|
1501
|
-
return [];
|
|
1502
|
-
return Object.values(entries).flatMap((entry) => {
|
|
1503
|
-
if (!isRecord(entry))
|
|
1504
|
-
return [];
|
|
1505
|
-
const mode = stringField(entry, 'mode') ?? stringField(entry, 'storage_posture');
|
|
1506
|
-
if (mode && !['viewport_brokered', 'viewport_managed'].includes(mode))
|
|
1507
|
-
return [];
|
|
1508
|
-
const handle = stringField(entry, 'handle') ??
|
|
1509
|
-
stringField(entry, 'ref') ??
|
|
1510
|
-
stringField(entry, 'credential_ref');
|
|
1511
|
-
return handle ? [handle] : [];
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
function credentialRefsFrom(entries) {
|
|
1515
|
-
return credentialEntriesFrom(entries).flatMap((entry) => {
|
|
1516
|
-
if (typeof entry === 'string' && entry.trim() !== '')
|
|
1517
|
-
return [entry];
|
|
1518
|
-
if (!isRecord(entry))
|
|
1519
|
-
return [];
|
|
1520
|
-
for (const key of ['handle', 'ref', 'credential_ref']) {
|
|
1521
|
-
const value = stringField(entry, key);
|
|
1522
|
-
if (value)
|
|
1523
|
-
return [value];
|
|
1524
|
-
}
|
|
1525
|
-
return [];
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
function credentialEntriesFrom(entries) {
|
|
1529
|
-
return Array.isArray(entries) ? entries : [];
|
|
1530
|
-
}
|
|
1531
|
-
function actionCredentialRefs(nodes) {
|
|
1532
|
-
if (!isRecord(nodes))
|
|
1533
|
-
return [];
|
|
1534
|
-
return Object.values(nodes).flatMap((node) => {
|
|
1535
|
-
if (!isRecord(node) || stringField(node, 'type') !== 'action')
|
|
1536
|
-
return [];
|
|
1537
|
-
const withValue = isRecord(node['with']) ? node['with'] : {};
|
|
1538
|
-
const credentialRef = stringField(withValue, 'credential_ref') ?? stringField(withValue, 'credentialRef');
|
|
1539
|
-
return credentialRef ? [credentialRef] : [];
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
function repositoriesFromActionCredentialRef(assignment, handle) {
|
|
1543
|
-
const workflow = yamlSnapshotDocument(assignment);
|
|
1544
|
-
const nodes = isRecord(workflow) ? workflow['nodes'] : undefined;
|
|
1545
|
-
if (!isRecord(nodes))
|
|
1546
|
-
return [];
|
|
1547
|
-
const repositories = new Set();
|
|
1548
|
-
for (const node of Object.values(nodes)) {
|
|
1549
|
-
if (!isRecord(node) || stringField(node, 'type') !== 'action')
|
|
1550
|
-
continue;
|
|
1551
|
-
const withValue = isRecord(node['with']) ? node['with'] : {};
|
|
1552
|
-
const credentialRef = stringField(withValue, 'credential_ref') ?? stringField(withValue, 'credentialRef');
|
|
1553
|
-
if (credentialRef !== handle)
|
|
1554
|
-
continue;
|
|
1555
|
-
const repository = stringField(withValue, 'repository') ?? stringField(withValue, 'repo');
|
|
1556
|
-
const rendered = renderCredentialTemplate(repository, assignment);
|
|
1557
|
-
if (rendered)
|
|
1558
|
-
repositories.add(rendered);
|
|
1559
|
-
}
|
|
1560
|
-
return [...repositories].sort();
|
|
1561
|
-
}
|
|
1562
|
-
function renderCredentialTemplate(value, assignment) {
|
|
1563
|
-
if (!value)
|
|
1564
|
-
return null;
|
|
1565
|
-
const inputs = isRecord(assignment.input_snapshot) ? assignment.input_snapshot : {};
|
|
1566
|
-
const rendered = value.replace(/\{\{\s*inputs\.([A-Za-z0-9_]+)\s*\}\}/g, (_match, key) => {
|
|
1567
|
-
const input = inputs[key];
|
|
1568
|
-
return typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean'
|
|
1569
|
-
? String(input)
|
|
1570
|
-
: '';
|
|
1571
|
-
});
|
|
1572
|
-
const trimmed = rendered.trim();
|
|
1573
|
-
return trimmed === '' || trimmed.includes('{{') ? null : trimmed;
|
|
1574
|
-
}
|
|
1575
|
-
function yamlSnapshotDocument(assignment) {
|
|
1576
|
-
if (!assignment.yaml_snapshot)
|
|
1577
|
-
return null;
|
|
1578
|
-
try {
|
|
1579
|
-
return YAML.parse(assignment.yaml_snapshot);
|
|
1580
|
-
}
|
|
1581
|
-
catch {
|
|
1582
|
-
return null;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
function asRecord(value) {
|
|
1586
|
-
return isRecord(value) ? value : {};
|
|
1587
|
-
}
|
|
1588
|
-
function pathValue(value, pathParts) {
|
|
1589
|
-
let current = value;
|
|
1590
|
-
for (const part of pathParts) {
|
|
1591
|
-
if (!isRecord(current))
|
|
1592
|
-
return undefined;
|
|
1593
|
-
current = current[part];
|
|
1594
|
-
}
|
|
1595
|
-
return current;
|
|
1596
|
-
}
|
|
1597
|
-
function isRecord(value) {
|
|
1598
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
1599
|
-
}
|
|
1600
|
-
async function readExistingLocalRun(runId) {
|
|
1601
|
-
if (!runId)
|
|
1602
|
-
return null;
|
|
1603
|
-
const response = await daemonFetch(`/api/workflows/runs/${encodeURIComponent(runId)}`, {
|
|
1604
|
-
method: 'GET',
|
|
1605
|
-
timeoutMs: 30_000,
|
|
1606
|
-
});
|
|
1607
|
-
if (response?.status === 404)
|
|
1608
|
-
return null;
|
|
1609
|
-
if (!response?.ok) {
|
|
1610
|
-
throw new Error(`Daemon request failed: ${response?.status ?? 'no response'} ${await safeText(response ?? undefined)}`);
|
|
1611
|
-
}
|
|
1612
|
-
return readRun(await response.json());
|
|
1613
|
-
}
|
|
1614
|
-
async function waitForApprovalAndResume(options, platformRunId, localRunId, assignmentClaimToken) {
|
|
1615
|
-
while (true) {
|
|
1616
|
-
await heartbeat(options, 'online', 'busy');
|
|
1617
|
-
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
1618
|
-
const commandRun = await applyBrokerActionCompletedCommands(options, platformRunId, assignment, localRunId, assignmentClaimToken);
|
|
1619
|
-
if (commandRun) {
|
|
1620
|
-
if (commandRun.status !== 'blocked')
|
|
1621
|
-
return commandRun;
|
|
1622
|
-
await delay(options.commandSleepSeconds * 1000);
|
|
1623
|
-
continue;
|
|
1624
|
-
}
|
|
1625
|
-
const approved = await approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId);
|
|
1626
|
-
if (approved) {
|
|
1627
|
-
const resumed = await resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken);
|
|
1628
|
-
if (resumed.status !== 'blocked')
|
|
1629
|
-
return resumed;
|
|
1630
|
-
const blockedIds = blockedNodeIds(resumed);
|
|
1631
|
-
if (alreadyResolvedApprovalRuns.has(resumed) && !blockedIds.has(approved.node_key)) {
|
|
1632
|
-
const nextApproved = await approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId);
|
|
1633
|
-
if (nextApproved &&
|
|
1634
|
-
nextApproved.node_key !== approved.node_key &&
|
|
1635
|
-
blockedIds.has(nextApproved.node_key)) {
|
|
1636
|
-
await delay(options.commandSleepSeconds * 1000);
|
|
1637
|
-
continue;
|
|
1638
|
-
}
|
|
1639
|
-
await delay(options.commandSleepSeconds * 1000);
|
|
1640
|
-
return resumed;
|
|
1641
|
-
}
|
|
1642
|
-
if (blockedIds.has(approved.node_key) &&
|
|
1643
|
-
managedApprovalDecision(approved) !== 'request_changes') {
|
|
1644
|
-
return resumed;
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
if (assignment.status === 'canceled' || assignment.status === 'failed') {
|
|
1648
|
-
const run = await cancelLocalRun(localRunId);
|
|
1649
|
-
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
1650
|
-
return run;
|
|
1651
|
-
}
|
|
1652
|
-
const current = await readExistingLocalRun(localRunId);
|
|
1653
|
-
if (current) {
|
|
1654
|
-
await syncLocalRun(options, platformRunId, current, assignmentClaimToken);
|
|
1655
|
-
}
|
|
1656
|
-
await delay(options.commandSleepSeconds * 1000);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
async function cancelLocalRunIfAssignmentCanceled(options, platformRunId, localRunId, assignmentClaimToken) {
|
|
1660
|
-
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
1661
|
-
if (assignment.status !== 'canceled' && assignment.status !== 'failed')
|
|
1662
|
-
return undefined;
|
|
1663
|
-
return cancelLocalRun(localRunId);
|
|
1664
|
-
}
|
|
1665
|
-
async function cancelLocalRun(localRunId) {
|
|
1666
|
-
const canceled = await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/cancel`, {
|
|
1667
|
-
message: 'Managed workflow assignment was canceled from Viewport.',
|
|
1668
|
-
actor: { name: 'Viewport', source: 'managed-executor' },
|
|
1669
|
-
});
|
|
1670
|
-
return readRun(canceled);
|
|
1671
|
-
}
|
|
1672
|
-
async function applyBrokerActionCompletedCommands(options, platformRunId, assignment, localRunId, assignmentClaimToken) {
|
|
1673
|
-
const localRun = await readExistingLocalRun(localRunId);
|
|
1674
|
-
const commands = (assignment.runtime_commands ?? []).filter((command) => command['type'] === 'workflow.action_completed' &&
|
|
1675
|
-
!brokerActionCommandAlreadyApplied(localRun, command));
|
|
1676
|
-
if (commands.length === 0)
|
|
1677
|
-
return null;
|
|
1678
|
-
const response = await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/runtime-commands`, { runtime_commands: commands });
|
|
1679
|
-
const run = readRun(response);
|
|
1680
|
-
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
1681
|
-
if (terminalRunStatus(run.status)) {
|
|
1682
|
-
clearRunCredentialMaterial(platformRunId);
|
|
1683
|
-
}
|
|
1684
|
-
return run;
|
|
1685
|
-
}
|
|
1686
|
-
function brokerActionCommandAlreadyApplied(run, command) {
|
|
1687
|
-
const nodeKey = stringValue(command['workflow_node_id']);
|
|
1688
|
-
if (!nodeKey)
|
|
1689
|
-
return false;
|
|
1690
|
-
const node = run?.nodes?.[nodeKey];
|
|
1691
|
-
if (!node || node.status !== 'completed')
|
|
1692
|
-
return false;
|
|
1693
|
-
const receipt = recordValue(node.metadata?.['executionReceipt']);
|
|
1694
|
-
const receiptKey = stringValue(receipt?.['receipt_key']);
|
|
1695
|
-
const commandReceiptKey = stringValue(command['receipt_key']);
|
|
1696
|
-
return Boolean(receiptKey && commandReceiptKey && receiptKey === commandReceiptKey);
|
|
1697
|
-
}
|
|
1698
|
-
const alreadyResolvedApprovalRuns = new WeakSet();
|
|
1699
|
-
async function approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId) {
|
|
1700
|
-
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
1701
|
-
if (!localRunId) {
|
|
1702
|
-
return (managedApprovalNodeFromRuntimeCommands(assignment, new Set()) ??
|
|
1703
|
-
assignment.nodes?.find(isResolvedManagedGateNode) ??
|
|
1704
|
-
null);
|
|
1705
|
-
}
|
|
1706
|
-
const localRun = await readExistingLocalRun(localRunId);
|
|
1707
|
-
const blockedIds = blockedNodeIds(localRun);
|
|
1708
|
-
const commandNode = managedApprovalNodeFromRuntimeCommands(assignment, blockedIds);
|
|
1709
|
-
if (commandNode)
|
|
1710
|
-
return commandNode;
|
|
1711
|
-
const approvedNodes = assignment.nodes?.filter(isResolvedManagedGateNode) ?? [];
|
|
1712
|
-
if (approvedNodes.length === 0)
|
|
1713
|
-
return null;
|
|
1714
|
-
if (blockedIds.size > 0) {
|
|
1715
|
-
return approvedNodes.find((node) => blockedIds.has(node.node_key)) ?? null;
|
|
1716
|
-
}
|
|
1717
|
-
return approvedNodes[0] ?? null;
|
|
1718
|
-
}
|
|
1719
|
-
function managedApprovalNodeFromRuntimeCommands(assignment, blockedIds) {
|
|
1720
|
-
for (const command of assignment.runtime_commands ?? []) {
|
|
1721
|
-
if (command['type'] !== 'workflow.approval_decision')
|
|
1722
|
-
continue;
|
|
1723
|
-
const nodeKey = stringValue(command['workflow_node_id']);
|
|
1724
|
-
if (!nodeKey || (blockedIds.size > 0 && !blockedIds.has(nodeKey)))
|
|
1725
|
-
continue;
|
|
1726
|
-
const approved = command['approved'] === true;
|
|
1727
|
-
const decision = stringValue(command['decision']);
|
|
1728
|
-
return {
|
|
1729
|
-
node_key: nodeKey,
|
|
1730
|
-
type: 'plan',
|
|
1731
|
-
status: 'blocked',
|
|
1732
|
-
metadata: {
|
|
1733
|
-
approval: {
|
|
1734
|
-
approved,
|
|
1735
|
-
decision: approved
|
|
1736
|
-
? 'approve'
|
|
1737
|
-
: decision === 'request_changes'
|
|
1738
|
-
? 'request_changes'
|
|
1739
|
-
: 'reject',
|
|
1740
|
-
message: stringValue(command['message']),
|
|
1741
|
-
actor: recordValue(command['actor']),
|
|
1742
|
-
feedback: recordValue(command['feedback']),
|
|
1743
|
-
approval_decision_key: stringValue(command['approval_decision_key']),
|
|
1744
|
-
expected_action_digest: stringValue(command['expected_action_digest']),
|
|
1745
|
-
execution_grant: recordValue(command['execution_grant']),
|
|
1746
|
-
},
|
|
1747
|
-
},
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
return null;
|
|
1751
|
-
}
|
|
1752
|
-
function blockedNodeIds(run) {
|
|
1753
|
-
return new Set(Object.values(run?.nodes ?? {})
|
|
1754
|
-
.filter((node) => node.status === 'blocked')
|
|
1755
|
-
.map((node) => node.id));
|
|
1756
|
-
}
|
|
1757
|
-
async function resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken) {
|
|
1758
|
-
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
1759
|
-
const localRun = await readExistingLocalRun(localRunId);
|
|
1760
|
-
const materialAssignment = localRun
|
|
1761
|
-
? assignmentWithLocalRunSnapshot(assignment, localRun, assignmentClaimToken)
|
|
1762
|
-
: {
|
|
1763
|
-
...assignment,
|
|
1764
|
-
assignment_claim_token: assignment.assignment_claim_token ?? assignmentClaimToken ?? null,
|
|
1765
|
-
};
|
|
1766
|
-
const cachedMaterial = runCredentialMaterialCache.get(platformRunId);
|
|
1767
|
-
const material = cachedMaterial && hasRuntimeSecrets(cachedMaterial)
|
|
1768
|
-
? cachedMaterial
|
|
1769
|
-
: await materializeAndCacheRunCredentials(options, materialAssignment);
|
|
1770
|
-
try {
|
|
1771
|
-
await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/approvals/${encodeURIComponent(approved.node_key)}`, {
|
|
1772
|
-
approved: managedApprovalApproved(approved),
|
|
1773
|
-
decision: managedApprovalDecision(approved),
|
|
1774
|
-
message: approvalMessage(approved),
|
|
1775
|
-
actor: approvalActor(approved),
|
|
1776
|
-
expectedActionDigest: approvalExpectedActionDigest(approved),
|
|
1777
|
-
executionGrant: approvalExecutionGrant(approved),
|
|
1778
|
-
feedback: approvalFeedback(approved),
|
|
1779
|
-
runtimeSecretEnv: material.runtimeSecretEnv,
|
|
1780
|
-
runtimeSecretFiles: material.runtimeSecretFiles,
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
catch (error) {
|
|
1784
|
-
if (!isNonFatalApprovalResumeError(error))
|
|
1785
|
-
throw error;
|
|
1786
|
-
const current = await readExistingLocalRun(localRunId);
|
|
1787
|
-
if (!current)
|
|
1788
|
-
throw error;
|
|
1789
|
-
alreadyResolvedApprovalRuns.add(current);
|
|
1790
|
-
await syncLocalRun(options, platformRunId, current, assignmentClaimToken);
|
|
1791
|
-
return current;
|
|
1792
|
-
}
|
|
1793
|
-
const resumed = await pollLocalRun(localRunId, async (run) => {
|
|
1794
|
-
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
1795
|
-
}, progressSyncEveryMs(options.leaseSeconds));
|
|
1796
|
-
await syncLocalRun(options, platformRunId, resumed, assignmentClaimToken);
|
|
1797
|
-
if (terminalRunStatus(resumed.status)) {
|
|
1798
|
-
clearRunCredentialMaterial(platformRunId);
|
|
1799
|
-
}
|
|
1800
|
-
return resumed;
|
|
1801
|
-
}
|
|
1802
|
-
function hasRuntimeSecrets(material) {
|
|
1803
|
-
return Object.keys(material.runtimeSecretEnv).length > 0;
|
|
1804
|
-
}
|
|
1805
|
-
function assignmentWithLocalRunSnapshot(assignment, localRun, assignmentClaimToken) {
|
|
1806
|
-
return {
|
|
1807
|
-
...assignment,
|
|
1808
|
-
assignment_claim_token: assignment.assignment_claim_token ?? assignmentClaimToken ?? null,
|
|
1809
|
-
yaml_snapshot: localRun.yamlSnapshot || assignment.yaml_snapshot,
|
|
1810
|
-
directory_path: localRun.directoryPath || assignment.directory_path,
|
|
1811
|
-
input_snapshot: localRun.inputs ?? assignment.input_snapshot,
|
|
1812
|
-
resource_manifest: localRun.resourceManifest ?? assignmentResourceManifest(assignment),
|
|
1813
|
-
workflow_authority_contract: localRun.workflowAuthorityContract ??
|
|
1814
|
-
assignmentWorkflowAuthorityContract(assignment) ??
|
|
1815
|
-
undefined,
|
|
1816
|
-
};
|
|
1817
|
-
}
|
|
1818
|
-
function assignmentTargetSnapshot(assignment) {
|
|
1819
|
-
return assignment.target_snapshot ?? assignment.targetSnapshot ?? null;
|
|
1820
|
-
}
|
|
1821
|
-
function assignmentRouteSnapshot(assignment) {
|
|
1822
|
-
return assignment.route_snapshot ?? assignment.routeSnapshot ?? null;
|
|
1823
|
-
}
|
|
1824
|
-
function assignmentExecutionProfileSnapshot(assignment) {
|
|
1825
|
-
return assignment.execution_profile_snapshot ?? assignment.executionProfileSnapshot ?? null;
|
|
1826
|
-
}
|
|
1827
|
-
function assignmentWorkflowSnapshot(assignment) {
|
|
1828
|
-
return assignment.workflow_snapshot ?? assignment.workflowSnapshot ?? null;
|
|
1829
|
-
}
|
|
1830
|
-
function assignmentRunnerWorkspaceSnapshot(assignment) {
|
|
1831
|
-
return assignment.runner_workspace_snapshot ?? assignment.runnerWorkspaceSnapshot ?? null;
|
|
1832
|
-
}
|
|
1833
|
-
function assignmentResourceManifest(assignment) {
|
|
1834
|
-
return assignment.resource_manifest ?? assignment.resourceManifest ?? null;
|
|
1835
|
-
}
|
|
1836
|
-
function assignmentContextReceiptsSnapshot(assignment) {
|
|
1837
|
-
return assignment.context_receipts_snapshot ?? assignment.contextReceiptsSnapshot ?? null;
|
|
1838
|
-
}
|
|
1839
|
-
function assignmentWorkflowAuthorityContract(assignment) {
|
|
1840
|
-
return assignmentWorkflowAuthorityContracts(assignment)[0] ?? null;
|
|
1841
|
-
}
|
|
1842
|
-
function assignmentWorkflowAuthorityContracts(assignment) {
|
|
1843
|
-
return [
|
|
1844
|
-
assignment.workflow_authority_contract ?? null,
|
|
1845
|
-
assignment.workflowAuthorityContract ?? null,
|
|
1846
|
-
recordChildValue(assignmentTargetSnapshot(assignment), 'workflow_authority_contract'),
|
|
1847
|
-
recordChildValue(assignmentTargetSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1848
|
-
recordChildValue(assignmentRouteSnapshot(assignment), 'workflow_authority_contract'),
|
|
1849
|
-
recordChildValue(assignmentRouteSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1850
|
-
recordChildValue(assignmentExecutionProfileSnapshot(assignment), 'workflow_authority_contract'),
|
|
1851
|
-
recordChildValue(assignmentExecutionProfileSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1852
|
-
recordChildValue(assignmentWorkflowSnapshot(assignment), 'workflow_authority_contract'),
|
|
1853
|
-
recordChildValue(assignmentWorkflowSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1854
|
-
recordChildValue(assignmentRunnerWorkspaceSnapshot(assignment), 'workflow_authority_contract'),
|
|
1855
|
-
recordChildValue(assignmentRunnerWorkspaceSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1856
|
-
recordChildValue(recordChildValue(asRecord(assignment.input_snapshot), 'viewport'), 'workflow_authority_contract'),
|
|
1857
|
-
recordChildValue(recordChildValue(asRecord(assignment.input_snapshot), 'viewport'), 'workflowAuthorityContract'),
|
|
1858
|
-
].filter(isRecord);
|
|
1859
|
-
}
|
|
1860
|
-
function isNonFatalApprovalResumeError(error) {
|
|
1861
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1862
|
-
return (message.includes('Workflow node is not awaiting approval') ||
|
|
1863
|
-
message.includes('The proposed action changed before approval'));
|
|
1864
|
-
}
|
|
1865
|
-
function isResolvedManagedGateNode(node) {
|
|
1866
|
-
if (!['approval', 'gate', 'plan'].includes(String(node.type ?? '')))
|
|
1867
|
-
return false;
|
|
1868
|
-
if (node.status === 'completed')
|
|
1869
|
-
return true;
|
|
1870
|
-
const approval = node.metadata?.['approval'];
|
|
1871
|
-
if ((node.type === 'plan' || node.type === 'gate' || node.type === 'approval') &&
|
|
1872
|
-
node.status === 'blocked' &&
|
|
1873
|
-
approval &&
|
|
1874
|
-
typeof approval === 'object' &&
|
|
1875
|
-
'approved' in approval) {
|
|
1876
|
-
return true;
|
|
1877
|
-
}
|
|
1878
|
-
return false;
|
|
1879
|
-
}
|
|
1880
|
-
function managedApprovalApproved(node) {
|
|
1881
|
-
const approval = node.metadata?.['approval'];
|
|
1882
|
-
if (approval && typeof approval === 'object' && 'approved' in approval) {
|
|
1883
|
-
return approval.approved === true;
|
|
1884
|
-
}
|
|
1885
|
-
return true;
|
|
1886
|
-
}
|
|
1887
|
-
function managedApprovalDecision(node) {
|
|
1888
|
-
const approval = node.metadata?.['approval'];
|
|
1889
|
-
if (approval && typeof approval === 'object') {
|
|
1890
|
-
const approved = approval.approved;
|
|
1891
|
-
const decision = approval.decision;
|
|
1892
|
-
if (approved === false) {
|
|
1893
|
-
if (decision === 'request_changes' || decision === 'changes_requested')
|
|
1894
|
-
return 'request_changes';
|
|
1895
|
-
return 'reject';
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
return 'approve';
|
|
1899
|
-
}
|
|
1900
|
-
async function getAssignment(options, platformRunId, assignmentClaimToken) {
|
|
1901
|
-
const body = await platformJson(options, 'GET', `workflow-runs/${encodeURIComponent(platformRunId)}`, undefined, assignmentClaimToken);
|
|
1902
|
-
return assignmentFrom(body);
|
|
1903
|
-
}
|
|
1904
|
-
async function syncLocalRun(options, platformRunId, run, assignmentClaimToken) {
|
|
1905
|
-
const body = await platformJson(options, 'PATCH', `workflow-runs/${encodeURIComponent(platformRunId)}/sync`, localRunToSyncPayload(run, { includeApprovalDecisions: false }), assignmentClaimToken);
|
|
1906
|
-
return assignmentFrom(body);
|
|
1907
|
-
}
|
|
1908
|
-
function assignmentFrom(body) {
|
|
1909
|
-
const data = dataFrom(body);
|
|
1910
|
-
if (!isRecord(data))
|
|
1911
|
-
return data;
|
|
1912
|
-
if (isRecord(body) && Array.isArray(body['runtime_commands'])) {
|
|
1913
|
-
return {
|
|
1914
|
-
...data,
|
|
1915
|
-
runtime_commands: body['runtime_commands'],
|
|
1916
|
-
};
|
|
1917
|
-
}
|
|
1918
|
-
return data;
|
|
1919
|
-
}
|
|
1920
|
-
async function ensureDirectory(directoryPath) {
|
|
1921
|
-
const resolvedPath = path.resolve(directoryPath);
|
|
1922
|
-
const directories = (await daemonJson('GET', '/api/directories'));
|
|
1923
|
-
const existing = directories.find((directory) => directory.path === resolvedPath);
|
|
1924
|
-
if (existing)
|
|
1925
|
-
return existing;
|
|
1926
|
-
await fs.promises.mkdir(resolvedPath, { recursive: true, mode: 0o700 });
|
|
1927
|
-
const created = (await daemonJson('POST', '/api/directories', { path: resolvedPath }));
|
|
1928
|
-
if (!created.id)
|
|
1929
|
-
throw new Error(`Failed to register workflow worker directory: ${resolvedPath}`);
|
|
1930
|
-
return { id: created.id, path: resolvedPath };
|
|
1931
|
-
}
|
|
1932
|
-
async function pollLocalRun(runId, onProgress, progressEveryMs = 30_000) {
|
|
1933
|
-
let nextProgressAt = 0;
|
|
1934
|
-
while (true) {
|
|
1935
|
-
const body = await daemonJson('GET', `/api/workflows/runs/${encodeURIComponent(runId)}`);
|
|
1936
|
-
const run = readRun(body);
|
|
1937
|
-
if (['completed', 'failed', 'blocked', 'canceled'].includes(run.status))
|
|
1938
|
-
return run;
|
|
1939
|
-
if (onProgress && Date.now() >= nextProgressAt) {
|
|
1940
|
-
const progressRun = await onProgress(run);
|
|
1941
|
-
if (progressRun)
|
|
1942
|
-
return progressRun;
|
|
1943
|
-
nextProgressAt = Date.now() + progressEveryMs;
|
|
1944
|
-
}
|
|
1945
|
-
await delay(500);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
class PlatformRequestError extends Error {
|
|
1949
|
-
status;
|
|
1950
|
-
constructor(status, message) {
|
|
1951
|
-
super(message);
|
|
1952
|
-
this.status = status;
|
|
1953
|
-
this.name = 'PlatformRequestError';
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
function isPlatformAccessDenied(error) {
|
|
1957
|
-
return error instanceof PlatformRequestError && [401, 402, 403, 404].includes(error.status);
|
|
1958
|
-
}
|
|
1959
|
-
async function daemonJson(method, urlPath, body) {
|
|
1960
|
-
const response = await daemonFetch(urlPath, {
|
|
1961
|
-
method,
|
|
1962
|
-
...(body !== undefined
|
|
1963
|
-
? { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
|
|
1964
|
-
: {}),
|
|
1965
|
-
timeoutMs: 30_000,
|
|
1966
|
-
});
|
|
1967
|
-
if (!response?.ok) {
|
|
1968
|
-
throw new Error(`Daemon request failed: ${response?.status ?? 'no response'} ${await safeText(response ?? undefined)}`);
|
|
1969
|
-
}
|
|
1970
|
-
return response.json();
|
|
1971
|
-
}
|
|
1972
|
-
async function platformJson(options, method, pathSuffix, body, assignmentClaimToken, extraHeaders) {
|
|
1973
|
-
return responseJson(await platformFetch(options, method, pathSuffix, body, assignmentClaimToken, extraHeaders));
|
|
1974
|
-
}
|
|
1975
|
-
async function platformFetch(options, method, pathSuffix, body, assignmentClaimToken, extraHeaders) {
|
|
1976
|
-
const url = `${baseManagedUrl(options)}/${pathSuffix}`;
|
|
1977
|
-
const bodyText = body !== undefined ? JSON.stringify(body) : undefined;
|
|
1978
|
-
const response = await transportFetch(url, {
|
|
1979
|
-
method,
|
|
1980
|
-
headers: {
|
|
1981
|
-
Authorization: `Bearer ${options.credential}`,
|
|
1982
|
-
Accept: 'application/json',
|
|
1983
|
-
...(assignmentClaimToken ? { 'X-Viewport-Assignment-Claim': assignmentClaimToken } : {}),
|
|
1984
|
-
...(extraHeaders ?? {}),
|
|
1985
|
-
...workerSignatureHeaders(options, method, url, bodyText),
|
|
1986
|
-
...(bodyText !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
1987
|
-
},
|
|
1988
|
-
...(bodyText !== undefined ? { body: bodyText } : {}),
|
|
1989
|
-
timeoutMs: 30_000,
|
|
1990
|
-
tlsVerify: parseTlsVerifyMode(process.env['VPD_SERVER_TLS_VERIFY']) ?? 'auto',
|
|
1991
|
-
caCertPath: process.env['VPD_SERVER_CA_CERT'],
|
|
1992
|
-
tlsPins: parseCsvList(process.env['VPD_SERVER_TLS_PINS']),
|
|
1993
|
-
});
|
|
1994
|
-
if (!response.ok && response.status !== 204) {
|
|
1995
|
-
throw new PlatformRequestError(response.status, `Platform request failed: HTTP ${response.status} ${await response.text()}`);
|
|
1996
|
-
}
|
|
1997
|
-
return response;
|
|
1998
|
-
}
|
|
1999
|
-
function workerSignatureHeaders(options, method, url, bodyText) {
|
|
2000
|
-
const identity = options.signingIdentity;
|
|
2001
|
-
if (!identity)
|
|
2002
|
-
return {};
|
|
2003
|
-
const timestamp = new Date().toISOString();
|
|
2004
|
-
const nonce = randomUUID();
|
|
2005
|
-
const bodySha256 = createHash('sha256')
|
|
2006
|
-
.update(bodyText ?? '')
|
|
2007
|
-
.digest('hex');
|
|
2008
|
-
const pathName = new URL(url).pathname;
|
|
2009
|
-
const serverId = identity.serverId ?? options.serverId;
|
|
2010
|
-
const canonical = [
|
|
2011
|
-
method.toUpperCase(),
|
|
2012
|
-
pathName,
|
|
2013
|
-
bodySha256,
|
|
2014
|
-
nonce,
|
|
2015
|
-
timestamp,
|
|
2016
|
-
...(serverId ? [serverId] : []),
|
|
2017
|
-
].join('\n');
|
|
2018
|
-
const signature = sign(null, Buffer.from(canonical), identity.privateKeyPem).toString('base64');
|
|
2019
|
-
return {
|
|
2020
|
-
'X-Viewport-Worker-Fingerprint': identity.fingerprint,
|
|
2021
|
-
'X-Viewport-Worker-Timestamp': timestamp,
|
|
2022
|
-
'X-Viewport-Worker-Nonce': nonce,
|
|
2023
|
-
'X-Viewport-Worker-Body-SHA256': bodySha256,
|
|
2024
|
-
'X-Viewport-Worker-Signature': signature,
|
|
2025
|
-
...(serverId ? { 'X-Viewport-Server-Id': serverId } : {}),
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
async function responseJson(response) {
|
|
2029
|
-
if (response.status === 204)
|
|
2030
|
-
return null;
|
|
2031
|
-
return response.json();
|
|
2032
|
-
}
|
|
2033
|
-
function baseManagedUrl(options) {
|
|
2034
|
-
return `${options.server}/api/runtime/workspaces/${encodeURIComponent(options.workspaceId)}/managed-executors/${encodeURIComponent(options.executorId)}`;
|
|
2035
|
-
}
|
|
2036
|
-
//# sourceMappingURL=workflow-managed-worker.js.map
|