@viewportai/daemon 0.25.12 → 0.26.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/dist/cli/check-command.d.ts +2 -0
- package/dist/cli/check-command.d.ts.map +1 -0
- package/dist/cli/check-command.js +168 -0
- package/dist/cli/check-command.js.map +1 -0
- package/dist/cli/commands.d.ts +2 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +2 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/diagnostic-command.d.ts +2 -0
- package/dist/cli/diagnostic-command.d.ts.map +1 -0
- package/dist/cli/diagnostic-command.js +102 -0
- package/dist/cli/diagnostic-command.js.map +1 -0
- package/dist/cli/lifecycle-commands.d.ts.map +1 -1
- package/dist/cli/lifecycle-commands.js +2 -1
- package/dist/cli/lifecycle-commands.js.map +1 -1
- package/dist/cli/lifecycle-pair-command.d.ts.map +1 -1
- package/dist/cli/lifecycle-pair-command.js +31 -0
- package/dist/cli/lifecycle-pair-command.js.map +1 -1
- package/dist/cli/policy-schema-validator.d.ts +221 -0
- package/dist/cli/policy-schema-validator.d.ts.map +1 -0
- package/dist/cli/policy-schema-validator.js +280 -0
- package/dist/cli/policy-schema-validator.js.map +1 -0
- package/dist/cli/support-packet.d.ts +7 -0
- package/dist/cli/support-packet.d.ts.map +1 -0
- package/dist/cli/support-packet.js +19 -0
- package/dist/cli/support-packet.js.map +1 -0
- package/dist/cli/worker-command.d.ts.map +1 -1
- package/dist/cli/worker-command.js +367 -14
- package/dist/cli/worker-command.js.map +1 -1
- package/dist/cli/worker-process-lock.d.ts +27 -0
- package/dist/cli/worker-process-lock.d.ts.map +1 -0
- package/dist/cli/worker-process-lock.js +150 -0
- package/dist/cli/worker-process-lock.js.map +1 -0
- package/dist/cli/worker-profile.d.ts +20 -0
- package/dist/cli/worker-profile.d.ts.map +1 -1
- package/dist/cli/worker-profile.js +38 -0
- package/dist/cli/worker-profile.js.map +1 -1
- package/dist/cli/worker-runtime.d.ts +52 -2
- package/dist/cli/worker-runtime.d.ts.map +1 -1
- package/dist/cli/worker-runtime.js +319 -102
- package/dist/cli/worker-runtime.js.map +1 -1
- package/dist/cli/workflow-managed-worker-format.d.ts.map +1 -1
- package/dist/cli/workflow-managed-worker-format.js +3 -1
- package/dist/cli/workflow-managed-worker-format.js.map +1 -1
- package/dist/cli/workflow-managed-worker.d.ts.map +1 -1
- package/dist/cli/workflow-managed-worker.js +157 -20
- package/dist/cli/workflow-managed-worker.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +6 -0
- package/dist/core/logger.js.map +1 -1
- package/dist/core/output.d.ts.map +1 -1
- package/dist/core/output.js +5 -4
- package/dist/core/output.js.map +1 -1
- package/dist/core/redaction.d.ts +4 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +76 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/workflows/action-provider-adapters.js +52 -4
- package/dist/workflows/action-provider-adapters.js.map +1 -1
- package/dist/workflows/checkout-node.d.ts +1 -0
- package/dist/workflows/checkout-node.d.ts.map +1 -1
- package/dist/workflows/checkout-node.js +9 -4
- package/dist/workflows/checkout-node.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/git-publish-node.d.ts +20 -0
- package/dist/workflows/git-publish-node.d.ts.map +1 -1
- package/dist/workflows/git-publish-node.js +87 -3
- package/dist/workflows/git-publish-node.js.map +1 -1
- package/dist/workflows/node-executor.d.ts +6 -0
- package/dist/workflows/node-executor.d.ts.map +1 -1
- package/dist/workflows/node-executor.js +53 -2
- package/dist/workflows/node-executor.js.map +1 -1
- package/dist/workflows/node-registry.d.ts +2 -2
- package/dist/workflows/node-registry.d.ts.map +1 -1
- package/dist/workflows/node-registry.js +129 -5
- package/dist/workflows/node-registry.js.map +1 -1
- package/dist/workflows/parser.d.ts.map +1 -1
- package/dist/workflows/parser.js +17 -1
- package/dist/workflows/parser.js.map +1 -1
- package/dist/workflows/platform-command-applier.d.ts +4 -0
- package/dist/workflows/platform-command-applier.d.ts.map +1 -1
- package/dist/workflows/platform-command-applier.js +152 -1
- package/dist/workflows/platform-command-applier.js.map +1 -1
- package/dist/workflows/platform-runtime-command.d.ts +2 -0
- package/dist/workflows/platform-runtime-command.d.ts.map +1 -1
- package/dist/workflows/platform-runtime-command.js +2 -0
- package/dist/workflows/platform-runtime-command.js.map +1 -1
- package/dist/workflows/platform-sync-payload.js +18 -4
- package/dist/workflows/platform-sync-payload.js.map +1 -1
- package/dist/workflows/platform-sync.js +7 -2
- package/dist/workflows/platform-sync.js.map +1 -1
- package/dist/workflows/policy-enforcement.d.ts +43 -0
- package/dist/workflows/policy-enforcement.d.ts.map +1 -0
- package/dist/workflows/policy-enforcement.js +0 -0
- package/dist/workflows/policy-enforcement.js.map +1 -0
- package/dist/workflows/run-types.d.ts +1 -0
- package/dist/workflows/run-types.d.ts.map +1 -1
- package/dist/workflows/runner.d.ts +4 -1
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +46 -12
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/types.d.ts +33 -0
- package/dist/workflows/types.d.ts.map +1 -1
- package/dist/workflows/workflow-authority-contract.d.ts +1 -1
- package/dist/workflows/workflow-authority-contract.d.ts.map +1 -1
- package/dist/workflows/workflow-authority-contract.js +23 -0
- package/dist/workflows/workflow-authority-contract.js.map +1 -1
- package/dist/workflows/workflow-production-schema.d.ts +13 -0
- package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
- package/dist/workflows/workflow-production-schema.js +26 -0
- package/dist/workflows/workflow-production-schema.js.map +1 -1
- package/dist/workflows/workflow-production-types.d.ts +6 -0
- package/dist/workflows/workflow-production-types.d.ts.map +1 -1
- package/dist/workflows/workflow-schema.d.ts +50 -0
- package/dist/workflows/workflow-schema.d.ts.map +1 -1
- package/dist/workflows/workflow-schema.js +41 -0
- package/dist/workflows/workflow-schema.js.map +1 -1
- package/package.json +6 -3
|
@@ -11,99 +11,169 @@ import { GitTracker } from '../tracking/git-tracker.js';
|
|
|
11
11
|
import { workflowRunToSyncPayload } from '../workflows/platform-sync-payload.js';
|
|
12
12
|
import { WorkflowRunStore } from '../workflows/store.js';
|
|
13
13
|
import { transportFetch } from './network.js';
|
|
14
|
+
import { acquireWorkerProcessLock } from './worker-process-lock.js';
|
|
15
|
+
import { readWorkerPairingRecord, workerProfileIntegrity, } from './worker-profile.js';
|
|
16
|
+
class HttpPollingTransport {
|
|
17
|
+
profile;
|
|
18
|
+
mode;
|
|
19
|
+
constructor(profile, mode) {
|
|
20
|
+
this.profile = profile;
|
|
21
|
+
this.mode = mode;
|
|
22
|
+
}
|
|
23
|
+
claim(body) {
|
|
24
|
+
return claimLeaseHttp(this.profile, body);
|
|
25
|
+
}
|
|
26
|
+
heartbeat(options) {
|
|
27
|
+
return heartbeatHttp(this.profile, {
|
|
28
|
+
...options,
|
|
29
|
+
transport: this.mode,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
sync(lease, execution) {
|
|
33
|
+
return syncLeaseHttp(this.profile, lease, execution);
|
|
34
|
+
}
|
|
35
|
+
cleanup(lease) {
|
|
36
|
+
return cleanupLeaseHttp(this.profile, lease);
|
|
37
|
+
}
|
|
38
|
+
pollRuntimeCommands(lease) {
|
|
39
|
+
return fetchHostedAssignmentHttp(this.profile, lease);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
14
42
|
const DEFAULT_HOSTED_LEASE_SECONDS = 1_800;
|
|
15
43
|
export async function runStandaloneWorker(options) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (transport === 'inbound') {
|
|
20
|
-
validateInboundWorkerGate(profile);
|
|
21
|
-
}
|
|
22
|
-
if (transport === 'relay') {
|
|
23
|
-
throw new Error('Relay worker transport is not supported by the standalone runtime yet.');
|
|
24
|
-
}
|
|
25
|
-
let lastHeartbeatAt = Date.now();
|
|
26
|
-
await heartbeat(profile, {
|
|
27
|
-
status: 'online',
|
|
28
|
-
healthStatus: 'idle',
|
|
29
|
-
lifecycle: options.lifecycle,
|
|
30
|
-
transport,
|
|
31
|
-
});
|
|
32
|
-
if (options.leaseToken) {
|
|
33
|
-
const lease = { id: options.leaseToken, leaseToken: options.leaseToken };
|
|
34
|
-
await syncLease(profile, lease, { status: 'completed' });
|
|
35
|
-
await cleanupLease(profile, lease);
|
|
36
|
-
return { claimed: 1, completed: 1, blocked: 0, failed: 0, cleanup: 1, denied: 0 };
|
|
37
|
-
}
|
|
38
|
-
const result = {
|
|
39
|
-
claimed: 0,
|
|
40
|
-
completed: 0,
|
|
41
|
-
blocked: 0,
|
|
42
|
-
failed: 0,
|
|
43
|
-
cleanup: 0,
|
|
44
|
-
denied: 0,
|
|
45
|
-
};
|
|
44
|
+
const bootstrap = await loadWorkerRuntimeBootstrap(options.bootstrapPath);
|
|
45
|
+
const profile = bootstrap.profile;
|
|
46
|
+
let processLock = null;
|
|
46
47
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
await validateWorkerWorkspaceRoot(profile.workspaceRoot);
|
|
49
|
+
const transport = options.transport ?? profile.transport;
|
|
50
|
+
if (transport === 'inbound') {
|
|
51
|
+
validateInboundWorkerGate(profile);
|
|
52
|
+
}
|
|
53
|
+
if (transport === 'relay') {
|
|
54
|
+
throw new Error('Relay worker transport is not supported by the standalone runtime yet.');
|
|
55
|
+
}
|
|
56
|
+
const workerTransport = new HttpPollingTransport(profile, transport);
|
|
57
|
+
processLock =
|
|
58
|
+
options.lifecycle === 'persistent' && !options.once
|
|
59
|
+
? acquireWorkerProcessLock({
|
|
60
|
+
server: profile.serverUrl,
|
|
61
|
+
workspaceId: profile.workspaceId ?? profile.publicKeyFingerprint,
|
|
62
|
+
executorId: profile.managedExecutorId ?? profile.publicKeyFingerprint,
|
|
63
|
+
runnerProfile: runnerPoolFromCapabilities(profile.capabilities),
|
|
64
|
+
accessMode: transport,
|
|
65
|
+
})
|
|
66
|
+
: null;
|
|
67
|
+
let lastHeartbeatAt = Date.now();
|
|
68
|
+
await workerTransport.heartbeat({
|
|
69
|
+
status: 'online',
|
|
70
|
+
healthStatus: 'idle',
|
|
71
|
+
lifecycle: options.lifecycle,
|
|
72
|
+
});
|
|
73
|
+
if (bootstrap.lease) {
|
|
74
|
+
const result = await executeBootstrapLease(profile, workerTransport, bootstrap.lease);
|
|
75
|
+
await workerTransport.heartbeat({
|
|
76
|
+
status: 'offline',
|
|
77
|
+
healthStatus: 'offline',
|
|
49
78
|
lifecycle: options.lifecycle,
|
|
50
|
-
transport,
|
|
51
|
-
leaseSeconds: options.leaseSeconds,
|
|
52
79
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
if (options.leaseToken) {
|
|
83
|
+
const lease = { id: options.leaseToken, leaseToken: options.leaseToken };
|
|
84
|
+
await workerTransport.sync(lease, { status: 'completed' });
|
|
85
|
+
await workerTransport.cleanup(lease);
|
|
86
|
+
return { claimed: 1, completed: 1, blocked: 0, failed: 0, cleanup: 1, denied: 0 };
|
|
87
|
+
}
|
|
88
|
+
const result = {
|
|
89
|
+
claimed: 0,
|
|
90
|
+
completed: 0,
|
|
91
|
+
blocked: 0,
|
|
92
|
+
failed: 0,
|
|
93
|
+
cleanup: 0,
|
|
94
|
+
denied: 0,
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
while (!options.abortSignal?.aborted) {
|
|
98
|
+
const lease = await workerTransport.claim({
|
|
99
|
+
lifecycle: options.lifecycle,
|
|
100
|
+
transport,
|
|
101
|
+
leaseSeconds: options.leaseSeconds,
|
|
102
|
+
});
|
|
103
|
+
if (!lease) {
|
|
104
|
+
if (options.once || options.lifecycle !== 'persistent')
|
|
105
|
+
break;
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (now - lastHeartbeatAt > 30_000) {
|
|
108
|
+
await workerTransport.heartbeat({
|
|
109
|
+
status: 'online',
|
|
110
|
+
healthStatus: 'idle',
|
|
111
|
+
lifecycle: options.lifecycle,
|
|
112
|
+
});
|
|
113
|
+
lastHeartbeatAt = now;
|
|
114
|
+
}
|
|
115
|
+
await sleepWithAbort(options.pollIntervalMs ?? 5_000, options.abortSignal);
|
|
116
|
+
continue;
|
|
65
117
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (execution.status !== 'blocked') {
|
|
77
|
-
await syncLease(profile, lease, execution);
|
|
118
|
+
result.claimed += 1;
|
|
119
|
+
let execution = await executeClaim(profile, workerTransport, lease);
|
|
120
|
+
if (isHostedManagedExecutorProfile(profile) &&
|
|
121
|
+
execution.status === 'blocked' &&
|
|
122
|
+
execution.run) {
|
|
123
|
+
await workerTransport.sync(lease, execution);
|
|
124
|
+
execution = await resumeBlockedHostedExecution(profile, workerTransport, lease, execution);
|
|
125
|
+
if (execution.status !== 'blocked') {
|
|
126
|
+
await workerTransport.sync(lease, execution);
|
|
127
|
+
}
|
|
78
128
|
}
|
|
129
|
+
else {
|
|
130
|
+
await workerTransport.sync(lease, execution);
|
|
131
|
+
}
|
|
132
|
+
if (execution.status === 'completed') {
|
|
133
|
+
result.completed += 1;
|
|
134
|
+
}
|
|
135
|
+
else if (execution.status === 'blocked') {
|
|
136
|
+
result.blocked += 1;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
result.failed += 1;
|
|
140
|
+
}
|
|
141
|
+
await workerTransport.cleanup(lease);
|
|
142
|
+
result.cleanup += 1;
|
|
143
|
+
if (options.once)
|
|
144
|
+
break;
|
|
79
145
|
}
|
|
80
|
-
else {
|
|
81
|
-
await syncLease(profile, lease, execution);
|
|
82
|
-
}
|
|
83
|
-
if (execution.status === 'completed') {
|
|
84
|
-
result.completed += 1;
|
|
85
|
-
}
|
|
86
|
-
else if (execution.status === 'blocked') {
|
|
87
|
-
result.blocked += 1;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
result.failed += 1;
|
|
91
|
-
}
|
|
92
|
-
await cleanupLease(profile, lease);
|
|
93
|
-
result.cleanup += 1;
|
|
94
|
-
if (options.once)
|
|
95
|
-
break;
|
|
96
146
|
}
|
|
147
|
+
finally {
|
|
148
|
+
await workerTransport.heartbeat({
|
|
149
|
+
status: 'offline',
|
|
150
|
+
healthStatus: 'offline',
|
|
151
|
+
lifecycle: options.lifecycle,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
97
155
|
}
|
|
98
156
|
finally {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
healthStatus: 'offline',
|
|
102
|
-
lifecycle: options.lifecycle,
|
|
103
|
-
transport,
|
|
104
|
-
});
|
|
157
|
+
processLock?.release();
|
|
158
|
+
await bootstrap.cleanup?.();
|
|
105
159
|
}
|
|
106
|
-
|
|
160
|
+
}
|
|
161
|
+
async function executeBootstrapLease(profile, transport, lease) {
|
|
162
|
+
const execution = await executeClaim(profile, transport, lease);
|
|
163
|
+
await transport.sync(lease, execution);
|
|
164
|
+
await transport.cleanup(lease);
|
|
165
|
+
return {
|
|
166
|
+
claimed: 1,
|
|
167
|
+
completed: execution.status === 'completed' ? 1 : 0,
|
|
168
|
+
blocked: execution.status === 'blocked' ? 1 : 0,
|
|
169
|
+
failed: execution.status === 'failed' || execution.status === 'canceled' ? 1 : 0,
|
|
170
|
+
cleanup: 1,
|
|
171
|
+
denied: 0,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function runnerPoolFromCapabilities(capabilities) {
|
|
175
|
+
const value = capabilities['runner_pool'] ?? capabilities['runnerPool'];
|
|
176
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
107
177
|
}
|
|
108
178
|
async function validateWorkerWorkspaceRoot(workspaceRoot) {
|
|
109
179
|
const resolved = path.resolve(workspaceRoot);
|
|
@@ -124,11 +194,17 @@ async function validateWorkerWorkspaceRoot(workspaceRoot) {
|
|
|
124
194
|
throw new Error(`Worker workspace root is not readable and writable: ${resolved}.`);
|
|
125
195
|
}
|
|
126
196
|
}
|
|
127
|
-
async function executeClaim(profile, lease) {
|
|
197
|
+
async function executeClaim(profile, transport, lease) {
|
|
128
198
|
if (!isHostedManagedExecutorProfile(profile)) {
|
|
129
199
|
return { status: 'completed' };
|
|
130
200
|
}
|
|
131
|
-
return executeHostedWorkflowClaim(profile, lease);
|
|
201
|
+
return executeHostedWorkflowClaim(profile, transport, lease);
|
|
202
|
+
}
|
|
203
|
+
async function loadWorkerRuntimeBootstrap(bootstrapPath) {
|
|
204
|
+
if (bootstrapPath) {
|
|
205
|
+
return loadSandboxBootstrap(bootstrapPath);
|
|
206
|
+
}
|
|
207
|
+
return { profile: await loadWorkerRuntimeProfile() };
|
|
132
208
|
}
|
|
133
209
|
async function loadWorkerRuntimeProfile() {
|
|
134
210
|
const manager = new ConfigManager();
|
|
@@ -144,6 +220,11 @@ async function loadWorkerRuntimeProfile() {
|
|
|
144
220
|
if (missing.length > 0) {
|
|
145
221
|
throw new Error(`Worker profile is not configured: missing ${missing.join(', ')}.`);
|
|
146
222
|
}
|
|
223
|
+
const pairing = await readWorkerPairingRecord(worker.stateDir);
|
|
224
|
+
const integrity = workerProfileIntegrity(worker, pairing);
|
|
225
|
+
if (!integrity.ok) {
|
|
226
|
+
throw new Error(`Worker profile does not match the approved pairing record: ${integrity.mismatches.join(', ')}. Run \`vpd worker reset\`, then pair this worker again.`);
|
|
227
|
+
}
|
|
147
228
|
return {
|
|
148
229
|
serverUrl: worker.serverUrl,
|
|
149
230
|
serverId: worker.serverId,
|
|
@@ -161,6 +242,82 @@ async function loadWorkerRuntimeProfile() {
|
|
|
161
242
|
capabilities: worker.capabilities ?? {},
|
|
162
243
|
};
|
|
163
244
|
}
|
|
245
|
+
async function loadSandboxBootstrap(bootstrapPath) {
|
|
246
|
+
const raw = JSON.parse(await fs.readFile(bootstrapPath, 'utf8'));
|
|
247
|
+
if (raw['schema'] !== 'viewport.sandbox_bootstrap/v1') {
|
|
248
|
+
throw new Error('Sandbox bootstrap file must use schema viewport.sandbox_bootstrap/v1.');
|
|
249
|
+
}
|
|
250
|
+
const workspaceRoot = requiredString(raw['workspace_root'] ?? raw['workspaceRoot'], 'workspace_root');
|
|
251
|
+
const identity = recordValue(raw['identity']);
|
|
252
|
+
const identityFile = await materializeBootstrapIdentity(identity, workspaceRoot);
|
|
253
|
+
const profile = {
|
|
254
|
+
serverUrl: requiredString(raw['server_url'] ?? raw['serverUrl'], 'server_url'),
|
|
255
|
+
serverId: stringValue(raw['server_id'] ?? raw['serverId']),
|
|
256
|
+
lifecycle: 'ephemeral',
|
|
257
|
+
transport: workerTransportValue(raw['transport']) ?? 'polling',
|
|
258
|
+
workspaceId: requiredString(raw['workspace_id'] ?? raw['workspaceId'], 'workspace_id'),
|
|
259
|
+
managedExecutorId: requiredString(raw['executor_id'] ?? raw['executorId'], 'executor_id'),
|
|
260
|
+
credential: requiredString(raw['credential'], 'credential'),
|
|
261
|
+
workspaceRoot,
|
|
262
|
+
identityKeyPath: identityFile.path,
|
|
263
|
+
publicKeyFingerprint: identityFile.publicKeyFingerprint,
|
|
264
|
+
capabilities: recordValue(raw['capabilities']) ?? {},
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
profile,
|
|
268
|
+
lease: claimedLeaseFromBootstrap(recordValue(raw['lease'])),
|
|
269
|
+
cleanup: async () => {
|
|
270
|
+
if (identityFile.ephemeral) {
|
|
271
|
+
await fs.rm(identityFile.path, { force: true });
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function claimedLeaseFromBootstrap(rawLease) {
|
|
277
|
+
if (!rawLease)
|
|
278
|
+
return undefined;
|
|
279
|
+
const id = requiredString(rawLease['id'] ?? rawLease['lease_id'] ?? rawLease['leaseId'], 'lease.id');
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
runId: stringValue(rawLease['workflow_run_id'] ?? rawLease['run_id'] ?? rawLease['runId']),
|
|
283
|
+
runtimeRunId: stringValue(rawLease['runtime_run_id'] ?? rawLease['runtimeRunId']),
|
|
284
|
+
leaseToken: stringValue(rawLease['lease_token'] ?? rawLease['leaseToken']),
|
|
285
|
+
assignmentClaimToken: stringValue(rawLease['assignment_claim_token'] ?? rawLease['assignmentClaimToken']),
|
|
286
|
+
yamlSnapshot: stringValue(rawLease['yaml_snapshot'] ?? rawLease['yamlSnapshot']),
|
|
287
|
+
sourceRef: stringValue(rawLease['source_ref'] ?? rawLease['sourceRef']),
|
|
288
|
+
directoryPath: stringValue(rawLease['directory_path'] ?? rawLease['directoryPath']),
|
|
289
|
+
inputSnapshot: recordValue(rawLease['input_snapshot'] ?? rawLease['inputSnapshot']),
|
|
290
|
+
resourceManifest: recordValue(rawLease['resource_manifest'] ?? rawLease['resourceManifest']),
|
|
291
|
+
workflowAuthorityContract: recordValue(rawLease['workflow_authority_contract'] ?? rawLease['workflowAuthorityContract']),
|
|
292
|
+
executionProfileSnapshot: recordValue(rawLease['execution_profile_snapshot'] ?? rawLease['executionProfileSnapshot']),
|
|
293
|
+
workflowSnapshot: recordValue(rawLease['workflow_snapshot'] ?? rawLease['workflowSnapshot']),
|
|
294
|
+
runtimeTargetId: stringValue(rawLease['runtime_target_id'] ?? rawLease['runtimeTargetId']),
|
|
295
|
+
dataCapturePolicy: dataCapturePolicyValue(rawLease['data_capture_policy'] ?? rawLease['dataCapturePolicy']),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function materializeBootstrapIdentity(identity, workspaceRoot) {
|
|
299
|
+
if (!identity) {
|
|
300
|
+
throw new Error('Sandbox bootstrap file is missing identity.');
|
|
301
|
+
}
|
|
302
|
+
const publicKey = requiredString(identity['public_key'] ?? identity['publicKey'], 'identity.public_key');
|
|
303
|
+
const privateKey = requiredString(identity['private_key'] ?? identity['privateKey'], 'identity.private_key');
|
|
304
|
+
const publicKeyFingerprint = requiredString(identity['public_key_fingerprint'] ?? identity['publicKeyFingerprint'], 'identity.public_key_fingerprint');
|
|
305
|
+
const dir = path.join(workspaceRoot, '.viewport', 'bootstrap');
|
|
306
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
307
|
+
const filePath = path.join(dir, `identity-${crypto.randomUUID()}.json`);
|
|
308
|
+
await fs.writeFile(filePath, `${JSON.stringify({ publicKey, privateKey, publicKeyFingerprint })}\n`, { mode: 0o600 });
|
|
309
|
+
return { path: filePath, publicKeyFingerprint, ephemeral: true };
|
|
310
|
+
}
|
|
311
|
+
function workerTransportValue(value) {
|
|
312
|
+
return value === 'polling' || value === 'relay' || value === 'inbound' ? value : undefined;
|
|
313
|
+
}
|
|
314
|
+
function requiredString(value, label) {
|
|
315
|
+
const result = stringValue(value);
|
|
316
|
+
if (!result) {
|
|
317
|
+
throw new Error(`Sandbox bootstrap file is missing ${label}.`);
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
164
321
|
function validateInboundWorkerGate(profile) {
|
|
165
322
|
const enabled = profile.inbound?.enabled === true || process.env['VPD_WORKER_INBOUND_EXPERIMENTAL'] === '1';
|
|
166
323
|
if (!enabled) {
|
|
@@ -179,7 +336,7 @@ function validateInboundWorkerGate(profile) {
|
|
|
179
336
|
}
|
|
180
337
|
throw new Error('Inbound worker transport listener is not implemented yet; do not enable inbound without the signed listener proof.');
|
|
181
338
|
}
|
|
182
|
-
async function
|
|
339
|
+
async function claimLeaseHttp(profile, body) {
|
|
183
340
|
const leaseSeconds = positiveInteger(body['leaseSeconds']) ?? DEFAULT_HOSTED_LEASE_SECONDS;
|
|
184
341
|
const response = isHostedManagedExecutorProfile(profile)
|
|
185
342
|
? await hostedManagedExecutorFetch(profile, 'POST', 'claim', {
|
|
@@ -214,11 +371,13 @@ async function claimLease(profile, body) {
|
|
|
214
371
|
inputSnapshot: recordValue(data['input_snapshot'] ?? rawLease['input_snapshot']),
|
|
215
372
|
resourceManifest: recordValue(data['resource_manifest'] ?? rawLease['resource_manifest']),
|
|
216
373
|
workflowAuthorityContract: recordValue(data['workflow_authority_contract'] ?? rawLease['workflow_authority_contract']),
|
|
374
|
+
executionProfileSnapshot: recordValue(data['execution_profile_snapshot'] ?? rawLease['execution_profile_snapshot']),
|
|
375
|
+
workflowSnapshot: recordValue(data['workflow_snapshot'] ?? rawLease['workflow_snapshot']),
|
|
217
376
|
runtimeTargetId: stringValue(data['runtime_target_id'] ?? rawLease['runtime_target_id']),
|
|
218
377
|
dataCapturePolicy: dataCapturePolicyValue(data['data_capture_policy'] ?? rawLease['data_capture_policy']),
|
|
219
378
|
};
|
|
220
379
|
}
|
|
221
|
-
async function
|
|
380
|
+
async function heartbeatHttp(profile, options) {
|
|
222
381
|
const capabilityPayload = managedExecutorCapabilities(profile.capabilities);
|
|
223
382
|
if (isHostedManagedExecutorProfile(profile)) {
|
|
224
383
|
await hostedManagedExecutorFetch(profile, 'POST', 'heartbeat', {
|
|
@@ -253,7 +412,7 @@ async function heartbeat(profile, options) {
|
|
|
253
412
|
capabilities: profile.capabilities,
|
|
254
413
|
});
|
|
255
414
|
}
|
|
256
|
-
async function
|
|
415
|
+
async function syncLeaseHttp(profile, lease, execution) {
|
|
257
416
|
const status = execution.status;
|
|
258
417
|
if (isHostedManagedExecutorProfile(profile)) {
|
|
259
418
|
if (!lease.runId) {
|
|
@@ -318,7 +477,7 @@ async function syncLease(profile, lease, execution) {
|
|
|
318
477
|
runtime_event_id: `phase8-${lease.id}-${status}`,
|
|
319
478
|
});
|
|
320
479
|
}
|
|
321
|
-
async function executeHostedWorkflowClaim(profile, lease) {
|
|
480
|
+
async function executeHostedWorkflowClaim(profile, transport, lease) {
|
|
322
481
|
if (!lease.leaseToken) {
|
|
323
482
|
return {
|
|
324
483
|
status: 'failed',
|
|
@@ -336,8 +495,11 @@ async function executeHostedWorkflowClaim(profile, lease) {
|
|
|
336
495
|
const existing = await existingHostedRuntimeRun(lease);
|
|
337
496
|
if (existing) {
|
|
338
497
|
if (existing.status === 'blocked') {
|
|
339
|
-
const body = await
|
|
340
|
-
const
|
|
498
|
+
const body = await transport.pollRuntimeCommands(lease);
|
|
499
|
+
const runtimeSecretEnv = await materializeHostedRunCredentials(profile, lease);
|
|
500
|
+
const applied = await daemon.workflowRunner.applyRuntimeCommandBody(existing.id, body, {
|
|
501
|
+
runtimeSecretEnv,
|
|
502
|
+
});
|
|
341
503
|
if (applied > 0) {
|
|
342
504
|
const completed = await waitForWorkflowRun(daemon, existing.id);
|
|
343
505
|
return {
|
|
@@ -430,7 +592,10 @@ function credentialHandlesFromLease(lease) {
|
|
|
430
592
|
const workflow = yamlSnapshotDocument(lease);
|
|
431
593
|
const handles = new Set();
|
|
432
594
|
for (const handle of [
|
|
433
|
-
...
|
|
595
|
+
...workflowNodeCredentialRefs(isRecord(workflow) ? workflow['nodes'] : undefined),
|
|
596
|
+
...topLevelCredentialRefs(isRecord(workflow) ? workflow['credentials'] : undefined),
|
|
597
|
+
...profileCredentialRefs(lease.executionProfileSnapshot),
|
|
598
|
+
...profileCredentialRefs(lease.workflowSnapshot),
|
|
434
599
|
...providerActionCredentialRefs(lease.workflowAuthorityContract),
|
|
435
600
|
]) {
|
|
436
601
|
handles.add(handle);
|
|
@@ -443,29 +608,72 @@ function repositoryForCredentialHandle(lease, handle) {
|
|
|
443
608
|
if (!isRecord(nodes))
|
|
444
609
|
return {};
|
|
445
610
|
for (const node of Object.values(nodes)) {
|
|
446
|
-
if (!isRecord(node)
|
|
611
|
+
if (!isRecord(node))
|
|
447
612
|
continue;
|
|
448
613
|
const withValue = isRecord(node['with']) ? node['with'] : {};
|
|
449
|
-
const credentialRef = stringValue(withValue['credential_ref']) ??
|
|
614
|
+
const credentialRef = stringValue(withValue['credential_ref']) ??
|
|
615
|
+
stringValue(withValue['credentialRef']) ??
|
|
616
|
+
stringValue(node['credential_ref']) ??
|
|
617
|
+
stringValue(node['credentialRef']);
|
|
450
618
|
if (credentialRef !== handle)
|
|
451
619
|
continue;
|
|
452
|
-
const repository = renderLeaseTemplate(
|
|
620
|
+
const repository = renderLeaseTemplate(repositoryTemplateForNode(node, withValue), lease);
|
|
453
621
|
if (repository)
|
|
454
622
|
return { repository };
|
|
455
623
|
}
|
|
456
624
|
return {};
|
|
457
625
|
}
|
|
458
|
-
function
|
|
626
|
+
function workflowNodeCredentialRefs(nodes) {
|
|
459
627
|
if (!isRecord(nodes))
|
|
460
628
|
return [];
|
|
461
629
|
return Object.values(nodes).flatMap((node) => {
|
|
462
|
-
if (!isRecord(node)
|
|
630
|
+
if (!isRecord(node))
|
|
631
|
+
return [];
|
|
632
|
+
const type = stringValue(node['type']);
|
|
633
|
+
if (type !== 'action' && type !== 'checkout' && type !== 'git_publish')
|
|
463
634
|
return [];
|
|
464
635
|
const withValue = isRecord(node['with']) ? node['with'] : {};
|
|
465
|
-
const credentialRef = stringValue(withValue['credential_ref']) ??
|
|
636
|
+
const credentialRef = stringValue(withValue['credential_ref']) ??
|
|
637
|
+
stringValue(withValue['credentialRef']) ??
|
|
638
|
+
stringValue(node['credential_ref']) ??
|
|
639
|
+
stringValue(node['credentialRef']);
|
|
466
640
|
return credentialRef ? [credentialRef] : [];
|
|
467
641
|
});
|
|
468
642
|
}
|
|
643
|
+
function topLevelCredentialRefs(credentials) {
|
|
644
|
+
if (!isRecord(credentials))
|
|
645
|
+
return [];
|
|
646
|
+
const refs = [];
|
|
647
|
+
for (const value of Object.values(credentials)) {
|
|
648
|
+
refs.push(...credentialEntriesFrom(value));
|
|
649
|
+
}
|
|
650
|
+
return refs;
|
|
651
|
+
}
|
|
652
|
+
function profileCredentialRefs(snapshot) {
|
|
653
|
+
const credentials = pathValue(asRecord(snapshot), ['credentials']);
|
|
654
|
+
if (!isRecord(credentials))
|
|
655
|
+
return [];
|
|
656
|
+
const refs = [];
|
|
657
|
+
for (const value of Object.values(credentials)) {
|
|
658
|
+
refs.push(...credentialEntriesFrom(value));
|
|
659
|
+
}
|
|
660
|
+
return refs;
|
|
661
|
+
}
|
|
662
|
+
function credentialEntriesFrom(value) {
|
|
663
|
+
if (!Array.isArray(value))
|
|
664
|
+
return [];
|
|
665
|
+
return value.flatMap((entry) => {
|
|
666
|
+
if (typeof entry === 'string' && entry.trim() !== '')
|
|
667
|
+
return [entry.trim()];
|
|
668
|
+
if (!isRecord(entry))
|
|
669
|
+
return [];
|
|
670
|
+
const handle = stringValue(entry['handle']) ??
|
|
671
|
+
stringValue(entry['ref']) ??
|
|
672
|
+
stringValue(entry['credential_ref']) ??
|
|
673
|
+
stringValue(entry['credentialRef']);
|
|
674
|
+
return handle ? [handle] : [];
|
|
675
|
+
});
|
|
676
|
+
}
|
|
469
677
|
function providerActionCredentialRefs(contract) {
|
|
470
678
|
const entries = pathValue(asRecord(contract), ['credentials', 'provider_actions']);
|
|
471
679
|
if (!Array.isArray(entries))
|
|
@@ -481,6 +689,12 @@ function providerActionCredentialRefs(contract) {
|
|
|
481
689
|
return value ? [value] : [];
|
|
482
690
|
});
|
|
483
691
|
}
|
|
692
|
+
function repositoryTemplateForNode(node, withValue) {
|
|
693
|
+
return (stringValue(withValue['repository']) ??
|
|
694
|
+
stringValue(withValue['repo']) ??
|
|
695
|
+
stringValue(node['repository']) ??
|
|
696
|
+
stringValue(node['repo']));
|
|
697
|
+
}
|
|
484
698
|
function renderLeaseTemplate(value, lease) {
|
|
485
699
|
if (!value)
|
|
486
700
|
return null;
|
|
@@ -514,7 +728,7 @@ async function existingHostedRuntimeRun(lease) {
|
|
|
514
728
|
return null;
|
|
515
729
|
return existing;
|
|
516
730
|
}
|
|
517
|
-
async function resumeBlockedHostedExecution(profile, lease, execution) {
|
|
731
|
+
async function resumeBlockedHostedExecution(profile, transport, lease, execution) {
|
|
518
732
|
if (!execution.daemon || !execution.run || execution.status !== 'blocked') {
|
|
519
733
|
return execution;
|
|
520
734
|
}
|
|
@@ -522,14 +736,17 @@ async function resumeBlockedHostedExecution(profile, lease, execution) {
|
|
|
522
736
|
const workflowRunId = execution.run.id;
|
|
523
737
|
const deadline = Date.now() + 10 * 60_000;
|
|
524
738
|
while (Date.now() < deadline) {
|
|
525
|
-
const body = await
|
|
739
|
+
const body = await transport.pollRuntimeCommands(lease);
|
|
526
740
|
if (!hasRuntimeCommands(body)) {
|
|
527
741
|
if (shouldRetryHostedCommandPoll(body)) {
|
|
528
742
|
continue;
|
|
529
743
|
}
|
|
530
744
|
return execution;
|
|
531
745
|
}
|
|
532
|
-
const
|
|
746
|
+
const runtimeSecretEnv = await materializeHostedRunCredentials(profile, lease);
|
|
747
|
+
const applied = await daemon.workflowRunner.applyRuntimeCommandBody(workflowRunId, body, {
|
|
748
|
+
runtimeSecretEnv,
|
|
749
|
+
});
|
|
533
750
|
if (applied > 0) {
|
|
534
751
|
const completed = await waitForWorkflowRun(daemon, workflowRunId);
|
|
535
752
|
if (completed.status === 'blocked') {
|
|
@@ -538,7 +755,7 @@ async function resumeBlockedHostedExecution(profile, lease, execution) {
|
|
|
538
755
|
run: completed,
|
|
539
756
|
daemon,
|
|
540
757
|
};
|
|
541
|
-
await
|
|
758
|
+
await transport.sync(lease, blockedExecution);
|
|
542
759
|
execution = blockedExecution;
|
|
543
760
|
continue;
|
|
544
761
|
}
|
|
@@ -555,7 +772,7 @@ async function resumeBlockedHostedExecution(profile, lease, execution) {
|
|
|
555
772
|
}
|
|
556
773
|
return execution;
|
|
557
774
|
}
|
|
558
|
-
async function
|
|
775
|
+
async function fetchHostedAssignmentHttp(profile, lease) {
|
|
559
776
|
if (!lease.runId) {
|
|
560
777
|
throw new Error('Hosted managed executor assignment polling requires a workflow run id.');
|
|
561
778
|
}
|
|
@@ -626,7 +843,7 @@ function workflowRunFailure(run) {
|
|
|
626
843
|
retrySafe: false,
|
|
627
844
|
};
|
|
628
845
|
}
|
|
629
|
-
async function
|
|
846
|
+
async function cleanupLeaseHttp(profile, lease) {
|
|
630
847
|
if (isHostedManagedExecutorProfile(profile)) {
|
|
631
848
|
return;
|
|
632
849
|
}
|