@viewportai/daemon 0.20.1 → 0.22.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 +34 -0
- package/dist/adapters/claude.d.ts +7 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +71 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex-event-normalizers.d.ts.map +1 -1
- package/dist/adapters/codex-event-normalizers.js +55 -2
- package/dist/adapters/codex-event-normalizers.js.map +1 -1
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +35 -0
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/gemini-cli.d.ts +2 -1
- package/dist/adapters/gemini-cli.d.ts.map +1 -1
- package/dist/adapters/gemini-cli.js +24 -0
- package/dist/adapters/gemini-cli.js.map +1 -1
- package/dist/adapters/pty.d.ts +2 -1
- package/dist/adapters/pty.d.ts.map +1 -1
- package/dist/adapters/pty.js +26 -1
- package/dist/adapters/pty.js.map +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +7 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +3 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/context-command.js +2 -0
- package/dist/cli/context-command.js.map +1 -1
- package/dist/cli/context-sync-target.d.ts +2 -0
- package/dist/cli/context-sync-target.d.ts.map +1 -1
- package/dist/cli/context-sync-target.js +4 -0
- package/dist/cli/context-sync-target.js.map +1 -1
- package/dist/cli/daemon-client.d.ts +1 -0
- package/dist/cli/daemon-client.d.ts.map +1 -1
- package/dist/cli/daemon-client.js +13 -1
- package/dist/cli/daemon-client.js.map +1 -1
- package/dist/cli/global-flags.d.ts.map +1 -1
- package/dist/cli/global-flags.js +3 -2
- package/dist/cli/global-flags.js.map +1 -1
- package/dist/cli/hook-command.d.ts +7 -0
- package/dist/cli/hook-command.d.ts.map +1 -1
- package/dist/cli/hook-command.js +35 -20
- package/dist/cli/hook-command.js.map +1 -1
- package/dist/cli/install-command.d.ts +5 -1
- package/dist/cli/install-command.d.ts.map +1 -1
- package/dist/cli/install-command.js +37 -28
- package/dist/cli/install-command.js.map +1 -1
- package/dist/cli/lifecycle-commands.d.ts.map +1 -1
- package/dist/cli/lifecycle-commands.js +5 -2
- 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 +51 -6
- package/dist/cli/lifecycle-pair-command.js.map +1 -1
- package/dist/cli/lifecycle-pair-server.d.ts +2 -0
- package/dist/cli/lifecycle-pair-server.d.ts.map +1 -1
- package/dist/cli/lifecycle-pair-server.js.map +1 -1
- package/dist/cli/lifecycle-status-command.d.ts.map +1 -1
- package/dist/cli/lifecycle-status-command.js +43 -0
- package/dist/cli/lifecycle-status-command.js.map +1 -1
- package/dist/cli/setup-command.d.ts.map +1 -1
- package/dist/cli/setup-command.js +8 -8
- package/dist/cli/setup-command.js.map +1 -1
- package/dist/cli/team-resource-command.d.ts +2 -0
- package/dist/cli/team-resource-command.d.ts.map +1 -0
- package/dist/cli/team-resource-command.js +364 -0
- package/dist/cli/team-resource-command.js.map +1 -0
- package/dist/cli/watch-command.d.ts +3 -0
- package/dist/cli/watch-command.d.ts.map +1 -0
- package/dist/cli/watch-command.js +42 -0
- package/dist/cli/watch-command.js.map +1 -0
- package/dist/cli/worker-command.d.ts +3 -0
- package/dist/cli/worker-command.d.ts.map +1 -0
- package/dist/cli/worker-command.js +132 -0
- package/dist/cli/worker-command.js.map +1 -0
- package/dist/cli/worker-profile.d.ts +65 -0
- package/dist/cli/worker-profile.d.ts.map +1 -0
- package/dist/cli/worker-profile.js +228 -0
- package/dist/cli/worker-profile.js.map +1 -0
- package/dist/cli/worker-runtime.d.ts +19 -0
- package/dist/cli/worker-runtime.d.ts.map +1 -0
- package/dist/cli/worker-runtime.js +606 -0
- package/dist/cli/worker-runtime.js.map +1 -0
- package/dist/cli/workflow-managed-worker-format.d.ts +4 -3
- package/dist/cli/workflow-managed-worker-format.d.ts.map +1 -1
- package/dist/cli/workflow-managed-worker-format.js +71 -10
- package/dist/cli/workflow-managed-worker-format.js.map +1 -1
- package/dist/cli/workflow-managed-worker-types.d.ts +17 -0
- package/dist/cli/workflow-managed-worker-types.d.ts.map +1 -1
- package/dist/cli/workflow-managed-worker.d.ts.map +1 -1
- package/dist/cli/workflow-managed-worker.js +568 -39
- package/dist/cli/workflow-managed-worker.js.map +1 -1
- package/dist/config-resolution/provider-defaults.d.ts.map +1 -1
- package/dist/config-resolution/provider-defaults.js +4 -0
- package/dist/config-resolution/provider-defaults.js.map +1 -1
- package/dist/config-resolution/schema.d.ts +2 -0
- package/dist/config-resolution/schema.d.ts.map +1 -1
- package/dist/config-resolution/schema.js +2 -0
- package/dist/config-resolution/schema.js.map +1 -1
- package/dist/config-resolution/types.d.ts +1 -1
- package/dist/config-resolution/types.d.ts.map +1 -1
- package/dist/context/local-edge-sync.d.ts +2 -0
- package/dist/context/local-edge-sync.d.ts.map +1 -1
- package/dist/context/local-edge-sync.js +2 -0
- package/dist/context/local-edge-sync.js.map +1 -1
- package/dist/context-providers/confluence-provider.d.ts +3 -0
- package/dist/context-providers/confluence-provider.d.ts.map +1 -0
- package/dist/context-providers/confluence-provider.js +170 -0
- package/dist/context-providers/confluence-provider.js.map +1 -0
- package/dist/context-providers/notion-provider.d.ts +3 -0
- package/dist/context-providers/notion-provider.d.ts.map +1 -0
- package/dist/context-providers/notion-provider.js +157 -0
- package/dist/context-providers/notion-provider.js.map +1 -0
- package/dist/context-providers/registry.d.ts.map +1 -1
- package/dist/context-providers/registry.js +9 -1
- package/dist/context-providers/registry.js.map +1 -1
- package/dist/context-providers/types.d.ts +18 -0
- package/dist/context-providers/types.d.ts.map +1 -1
- package/dist/core/config-schema.d.ts +96 -3
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +53 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/config.d.ts +88 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +6 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/daemon.d.ts +5 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/daemon.js +8 -0
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/interfaces.d.ts +137 -1
- package/dist/core/interfaces.d.ts.map +1 -1
- package/dist/core/permission-coordinator.d.ts.map +1 -1
- package/dist/core/permission-coordinator.js +4 -2
- package/dist/core/permission-coordinator.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +47 -20
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +31 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/discovery/watcher.d.ts.map +1 -1
- package/dist/discovery/watcher.js +51 -8
- package/dist/discovery/watcher.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +10 -1
- package/dist/plugins/loader.js.map +1 -1
- package/dist/security/child-env.d.ts +3 -0
- package/dist/security/child-env.d.ts.map +1 -0
- package/dist/security/child-env.js +44 -0
- package/dist/security/child-env.js.map +1 -0
- package/dist/server/http-request-schemas.d.ts +6 -0
- package/dist/server/http-request-schemas.d.ts.map +1 -1
- package/dist/server/http-request-schemas.js +15 -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 +4 -2
- package/dist/server/http-server.js.map +1 -1
- package/dist/workflows/action-adapters.d.ts.map +1 -1
- package/dist/workflows/action-adapters.js +42 -0
- package/dist/workflows/action-adapters.js.map +1 -1
- package/dist/workflows/action-provider-adapters.d.ts.map +1 -1
- package/dist/workflows/action-provider-adapters.js +156 -13
- package/dist/workflows/action-provider-adapters.js.map +1 -1
- package/dist/workflows/action-provider-utils.d.ts +6 -0
- package/dist/workflows/action-provider-utils.d.ts.map +1 -1
- package/dist/workflows/action-provider-utils.js +21 -0
- package/dist/workflows/action-provider-utils.js.map +1 -1
- package/dist/workflows/approval-actor.d.ts +6 -0
- package/dist/workflows/approval-actor.d.ts.map +1 -0
- package/dist/workflows/approval-actor.js +27 -0
- package/dist/workflows/approval-actor.js.map +1 -0
- package/dist/workflows/approval-on-reject.js +1 -0
- package/dist/workflows/approval-on-reject.js.map +1 -1
- package/dist/workflows/checkout-node.d.ts +21 -0
- package/dist/workflows/checkout-node.d.ts.map +1 -0
- package/dist/workflows/checkout-node.js +181 -0
- package/dist/workflows/checkout-node.js.map +1 -0
- package/dist/workflows/context-node-resolver.d.ts +48 -1
- package/dist/workflows/context-node-resolver.d.ts.map +1 -1
- package/dist/workflows/context-node-resolver.js +405 -8
- package/dist/workflows/context-node-resolver.js.map +1 -1
- package/dist/workflows/context-update-targets.d.ts +12 -0
- package/dist/workflows/context-update-targets.d.ts.map +1 -0
- package/dist/workflows/context-update-targets.js +52 -0
- package/dist/workflows/context-update-targets.js.map +1 -0
- package/dist/workflows/daemon-session.d.ts +11 -0
- package/dist/workflows/daemon-session.d.ts.map +1 -1
- package/dist/workflows/daemon-session.js +168 -6
- 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/expression.d.ts +4 -0
- package/dist/workflows/expression.d.ts.map +1 -1
- package/dist/workflows/expression.js +1 -1
- package/dist/workflows/expression.js.map +1 -1
- package/dist/workflows/git-publish-node.d.ts +23 -0
- package/dist/workflows/git-publish-node.d.ts.map +1 -0
- package/dist/workflows/git-publish-node.js +237 -0
- package/dist/workflows/git-publish-node.js.map +1 -0
- package/dist/workflows/inline-agent-types.d.ts +7 -0
- package/dist/workflows/inline-agent-types.d.ts.map +1 -1
- package/dist/workflows/inline-agents.d.ts +4 -1
- package/dist/workflows/inline-agents.d.ts.map +1 -1
- package/dist/workflows/inline-agents.js +24 -1
- package/dist/workflows/inline-agents.js.map +1 -1
- package/dist/workflows/loop-executor.js +1 -0
- package/dist/workflows/loop-executor.js.map +1 -1
- package/dist/workflows/node-executor.d.ts +7 -0
- package/dist/workflows/node-executor.d.ts.map +1 -1
- package/dist/workflows/node-executor.js +135 -7
- package/dist/workflows/node-executor.js.map +1 -1
- package/dist/workflows/node-registry.d.ts.map +1 -1
- package/dist/workflows/node-registry.js +553 -4
- package/dist/workflows/node-registry.js.map +1 -1
- package/dist/workflows/parser.d.ts.map +1 -1
- package/dist/workflows/parser.js +70 -5
- package/dist/workflows/parser.js.map +1 -1
- package/dist/workflows/platform-command-applier.d.ts.map +1 -1
- package/dist/workflows/platform-command-applier.js +5 -14
- package/dist/workflows/platform-command-applier.js.map +1 -1
- package/dist/workflows/platform-context-client.d.ts +74 -0
- package/dist/workflows/platform-context-client.d.ts.map +1 -0
- package/dist/workflows/platform-context-client.js +228 -0
- package/dist/workflows/platform-context-client.js.map +1 -0
- package/dist/workflows/platform-sync-format.d.ts +1 -1
- package/dist/workflows/platform-sync-format.d.ts.map +1 -1
- package/dist/workflows/platform-sync-format.js +3 -1
- package/dist/workflows/platform-sync-format.js.map +1 -1
- package/dist/workflows/platform-sync-payload.d.ts +1 -0
- package/dist/workflows/platform-sync-payload.d.ts.map +1 -1
- package/dist/workflows/platform-sync-payload.js +160 -8
- package/dist/workflows/platform-sync-payload.js.map +1 -1
- package/dist/workflows/platform-sync.d.ts.map +1 -1
- package/dist/workflows/platform-sync.js +10 -1
- package/dist/workflows/platform-sync.js.map +1 -1
- package/dist/workflows/plugin-loader.d.ts.map +1 -1
- package/dist/workflows/plugin-loader.js +3 -0
- package/dist/workflows/plugin-loader.js.map +1 -1
- package/dist/workflows/preflight.d.ts.map +1 -1
- package/dist/workflows/preflight.js +9 -1
- package/dist/workflows/preflight.js.map +1 -1
- package/dist/workflows/prompt-output.d.ts +6 -2
- package/dist/workflows/prompt-output.d.ts.map +1 -1
- package/dist/workflows/prompt-output.js +8 -2
- package/dist/workflows/prompt-output.js.map +1 -1
- package/dist/workflows/run-preparation.d.ts +24 -0
- package/dist/workflows/run-preparation.d.ts.map +1 -0
- package/dist/workflows/run-preparation.js +258 -0
- package/dist/workflows/run-preparation.js.map +1 -0
- package/dist/workflows/run-types.d.ts +11 -0
- package/dist/workflows/run-types.d.ts.map +1 -1
- package/dist/workflows/runner-scheduler.d.ts +2 -0
- package/dist/workflows/runner-scheduler.d.ts.map +1 -1
- package/dist/workflows/runner-scheduler.js +35 -4
- package/dist/workflows/runner-scheduler.js.map +1 -1
- package/dist/workflows/runner-shared.d.ts.map +1 -1
- package/dist/workflows/runner-shared.js +9 -0
- package/dist/workflows/runner-shared.js.map +1 -1
- package/dist/workflows/runner.d.ts +2 -0
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +171 -6
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/runtime-helpers.d.ts +20 -1
- package/dist/workflows/runtime-helpers.d.ts.map +1 -1
- package/dist/workflows/runtime-helpers.js +98 -3
- package/dist/workflows/runtime-helpers.js.map +1 -1
- package/dist/workflows/session-output.d.ts +9 -0
- package/dist/workflows/session-output.d.ts.map +1 -1
- package/dist/workflows/session-output.js +156 -0
- package/dist/workflows/session-output.js.map +1 -1
- package/dist/workflows/session-policy.d.ts +30 -0
- package/dist/workflows/session-policy.d.ts.map +1 -0
- package/dist/workflows/session-policy.js +45 -0
- package/dist/workflows/session-policy.js.map +1 -0
- package/dist/workflows/structured-outputs.d.ts +5 -0
- package/dist/workflows/structured-outputs.d.ts.map +1 -1
- package/dist/workflows/structured-outputs.js +82 -3
- package/dist/workflows/structured-outputs.js.map +1 -1
- package/dist/workflows/subflow-executor.js +5 -1
- package/dist/workflows/subflow-executor.js.map +1 -1
- package/dist/workflows/types.d.ts +107 -4
- package/dist/workflows/types.d.ts.map +1 -1
- package/dist/workflows/workflow-authority-contract.d.ts +25 -0
- package/dist/workflows/workflow-authority-contract.d.ts.map +1 -0
- package/dist/workflows/workflow-authority-contract.js +366 -0
- package/dist/workflows/workflow-authority-contract.js.map +1 -0
- package/dist/workflows/workflow-executor-schema.d.ts +1 -1
- package/dist/workflows/workflow-production-schema.d.ts +62 -10
- package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
- package/dist/workflows/workflow-production-schema.js +63 -4
- package/dist/workflows/workflow-production-schema.js.map +1 -1
- package/dist/workflows/workflow-production-types.d.ts +42 -3
- package/dist/workflows/workflow-production-types.d.ts.map +1 -1
- package/dist/workflows/workflow-schema-common.d.ts +248 -2
- package/dist/workflows/workflow-schema-common.d.ts.map +1 -1
- package/dist/workflows/workflow-schema-common.js +58 -2
- package/dist/workflows/workflow-schema-common.js.map +1 -1
- package/dist/workflows/workflow-schema.d.ts +1966 -16
- package/dist/workflows/workflow-schema.d.ts.map +1 -1
- package/dist/workflows/workflow-schema.js +97 -3
- package/dist/workflows/workflow-schema.js.map +1 -1
- package/docs/releasing.md +4 -1
- package/node_modules/@viewportai/context-engine/schemas/context_event_v1.schema.json +2 -0
- package/node_modules/@viewportai/context-engine/src/repo/candidates.js +2 -0
- package/node_modules/@viewportai/context-engine/src/repo/events.js +15 -1
- package/node_modules/@viewportai/context-engine/src/repo/vault.js +18 -2
- package/package.json +2 -2
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { constants, createHash, generateKeyPairSync, privateDecrypt } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
2
5
|
import path from 'node:path';
|
|
3
|
-
import { getFlag, hasFlag } from './args.js';
|
|
6
|
+
import { getArgs, getFlag, hasFlag } from './args.js';
|
|
4
7
|
import { daemonFetch, isDaemonRunning } from './daemon-client.js';
|
|
5
8
|
import { isJsonMode, printJson } from './command-shared.js';
|
|
6
9
|
import { parseCsvList, parseTlsVerifyMode, transportFetch } from './network.js';
|
|
7
10
|
import { delay, listFlagValue, positiveIntFlagValue, safeText, } from './workflow-managed-worker-util.js';
|
|
8
11
|
import { executeProviderAction, WorkflowActionError, } from '../workflows/action-provider-adapters.js';
|
|
9
|
-
import {
|
|
12
|
+
import { envNameForCredentialRef } from '../workflows/action-provider-utils.js';
|
|
13
|
+
import { sanitizeActionInput } from '../workflows/action-digest.js';
|
|
14
|
+
import { approvalActor, approvalExecutionGrant, approvalFeedback, approvalExpectedActionDigest, approvalMessage, capabilityPayload, dataFrom, localRunToSyncPayload, progressSyncEveryMs, readRun, } from './workflow-managed-worker-format.js';
|
|
10
15
|
export async function workflowWorker() {
|
|
16
|
+
if (hasFlag('help') || getArgs().includes('-h')) {
|
|
17
|
+
console.log(workflowWorkerUsage());
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
11
20
|
const options = resolveWorkerOptions();
|
|
12
21
|
if (!(await isDaemonRunning())) {
|
|
13
22
|
throw new Error('Daemon is not running. Start it first with `vpd start`.');
|
|
14
23
|
}
|
|
15
24
|
await validateDaemonAgentCapabilities(options);
|
|
25
|
+
await syncDaemonModelCapabilities(options);
|
|
26
|
+
if (hasFlag('doctor')) {
|
|
27
|
+
await heartbeat(options, 'online', 'idle');
|
|
28
|
+
await safeHeartbeat(options, 'offline', 'offline');
|
|
29
|
+
if (isJsonMode()) {
|
|
30
|
+
printJson({
|
|
31
|
+
command: 'workflow worker doctor',
|
|
32
|
+
ok: true,
|
|
33
|
+
accessMode: options.accessMode,
|
|
34
|
+
runnerProfile: options.runnerProfile ?? null,
|
|
35
|
+
runnerPool: options.runnerPool ?? null,
|
|
36
|
+
capabilities: options.capabilities,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log('Workflow worker doctor passed.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
16
43
|
if (hasFlag('preflight')) {
|
|
17
44
|
if (isJsonMode()) {
|
|
18
45
|
printJson({
|
|
@@ -65,11 +92,19 @@ export async function workflowWorker() {
|
|
|
65
92
|
stats.claimed += 1;
|
|
66
93
|
const localRun = await runAssignmentLocally(options, assignment);
|
|
67
94
|
if (localRun.status === 'blocked' && !options.once) {
|
|
68
|
-
const approved = await approvedNodeForAssignment(options, assignment.id, assignment.assignment_claim_token);
|
|
95
|
+
const approved = await approvedNodeForAssignment(options, assignment.id, assignment.assignment_claim_token, localRun.id);
|
|
69
96
|
if (approved) {
|
|
70
97
|
const resumed = await resumeApprovedLocalRun(options, assignment.id, localRun.id, approved, assignment.assignment_claim_token);
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
const blockedIds = blockedNodeIds(resumed);
|
|
99
|
+
const shouldKeepWaiting = resumed.status === 'blocked' &&
|
|
100
|
+
(!alreadyResolvedApprovalRuns.has(resumed) || !blockedIds.has(approved.node_key)) &&
|
|
101
|
+
(!blockedIds.has(approved.node_key) ||
|
|
102
|
+
managedApprovalDecision(approved) === 'request_changes');
|
|
103
|
+
const finalRun = shouldKeepWaiting
|
|
104
|
+
? await waitForApprovalAndResume(options, assignment.id, localRun.id, assignment.assignment_claim_token)
|
|
105
|
+
: resumed;
|
|
106
|
+
stats.completed += finalRun.status === 'completed' ? 1 : 0;
|
|
107
|
+
stats.failed += finalRun.status === 'failed' || finalRun.status === 'canceled' ? 1 : 0;
|
|
73
108
|
if (options.maxRuns !== undefined && totalClaimed(stats) >= options.maxRuns) {
|
|
74
109
|
break;
|
|
75
110
|
}
|
|
@@ -139,39 +174,235 @@ async function validateDaemonAgentCapabilities(options) {
|
|
|
139
174
|
return;
|
|
140
175
|
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.`);
|
|
141
176
|
}
|
|
177
|
+
async function syncDaemonModelCapabilities(options) {
|
|
178
|
+
const response = await daemonFetch('/api/models', {
|
|
179
|
+
method: 'GET',
|
|
180
|
+
timeoutMs: 30_000,
|
|
181
|
+
});
|
|
182
|
+
if (!response?.ok) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const body = (await response.json());
|
|
186
|
+
const catalog = daemonModelCatalog(body.models ?? []);
|
|
187
|
+
const allModels = [...new Set(Object.values(catalog).flat())];
|
|
188
|
+
if (allModels.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
options.capabilities.models = allModels;
|
|
192
|
+
options.capabilities.agentModels = catalog;
|
|
193
|
+
}
|
|
194
|
+
function daemonModelCatalog(models) {
|
|
195
|
+
const catalog = {};
|
|
196
|
+
for (const model of models) {
|
|
197
|
+
const agentId = stringValue(model.agentId) ?? stringValue(model.agent_id);
|
|
198
|
+
const value = stringValue(model.value) ?? stringValue(model.id);
|
|
199
|
+
if (!agentId || !value)
|
|
200
|
+
continue;
|
|
201
|
+
catalog[agentId] = [...new Set([...(catalog[agentId] ?? []), value])];
|
|
202
|
+
}
|
|
203
|
+
return catalog;
|
|
204
|
+
}
|
|
142
205
|
function resolveWorkerOptions() {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
206
|
+
const registrationProfile = readRegistrationProfile();
|
|
207
|
+
const server = getFlag('server') ??
|
|
208
|
+
process.env['VIEWPORT_SERVER_URL'] ??
|
|
209
|
+
process.env['VPD_SERVER_URL'] ??
|
|
210
|
+
registrationProfile?.serverUrl;
|
|
211
|
+
const workspaceId = getFlag('workspace') ??
|
|
212
|
+
getFlag('resource') ??
|
|
213
|
+
process.env['VIEWPORT_WORKSPACE_ID'] ??
|
|
214
|
+
registrationProfile?.workspaceId;
|
|
215
|
+
const executorId = getFlag('executor') ??
|
|
216
|
+
process.env['VIEWPORT_MANAGED_EXECUTOR_ID'] ??
|
|
217
|
+
registrationProfile?.executorId;
|
|
146
218
|
const credential = getFlag('credential') ??
|
|
147
219
|
process.env['VIEWPORT_MANAGED_EXECUTOR_TOKEN'] ??
|
|
148
|
-
process.env['VPD_MANAGED_EXECUTOR_TOKEN']
|
|
220
|
+
process.env['VPD_MANAGED_EXECUTOR_TOKEN'] ??
|
|
221
|
+
registrationProfile?.credential;
|
|
149
222
|
if (!server || !workspaceId || !executorId || !credential) {
|
|
150
|
-
throw new Error(
|
|
223
|
+
throw new Error(workflowWorkerUsage());
|
|
151
224
|
}
|
|
225
|
+
const profileCapabilities = registrationProfile?.capabilities ?? {};
|
|
226
|
+
const profileRunnerPool = stringValue(profileCapabilities['runner_pool']) ??
|
|
227
|
+
stringValue(profileCapabilities['runnerPool']);
|
|
228
|
+
const runnerKeyPair = loadOrCreateRunnerKeyPair(workspaceId, executorId);
|
|
229
|
+
const detected = detectLocalCapabilities();
|
|
152
230
|
return {
|
|
153
231
|
server: server.replace(/\/+$/, ''),
|
|
154
232
|
workspaceId,
|
|
155
233
|
executorId,
|
|
156
234
|
credential,
|
|
157
|
-
accessMode: managedWorkerAccessMode(getFlag('access-mode') ??
|
|
158
|
-
|
|
235
|
+
accessMode: managedWorkerAccessMode(getFlag('access-mode') ??
|
|
236
|
+
process.env['VIEWPORT_MANAGED_EXECUTOR_ACCESS_MODE'] ??
|
|
237
|
+
registrationProfile?.accessMode),
|
|
238
|
+
runnerProfile: getFlag('runner-profile') ??
|
|
239
|
+
process.env['VIEWPORT_MANAGED_EXECUTOR_PROFILE'] ??
|
|
240
|
+
registrationProfile?.runnerProfile ??
|
|
241
|
+
undefined,
|
|
242
|
+
runnerPosture: registrationProfile?.runnerPosture,
|
|
243
|
+
runnerKeyPair,
|
|
244
|
+
runnerPool: getFlag('runner-pool') ??
|
|
245
|
+
process.env['VIEWPORT_MANAGED_RUNNER_POOL'] ??
|
|
246
|
+
process.env['VIEWPORT_MANAGED_EXECUTOR_RUNNER_POOL'] ??
|
|
247
|
+
profileRunnerPool ??
|
|
248
|
+
undefined,
|
|
159
249
|
workdir: getFlag('workdir') ? path.resolve(getFlag('workdir')) : undefined,
|
|
160
250
|
leaseSeconds: positiveIntFlagValue(getFlag('lease')) ?? 300,
|
|
161
251
|
sleepSeconds: positiveIntFlagValue(getFlag('sleep')) ?? 5,
|
|
162
252
|
maxRuns: positiveIntFlagValue(getFlag('max-runs')),
|
|
163
253
|
once: hasFlag('once'),
|
|
164
254
|
capabilities: {
|
|
255
|
+
runnerPool: getFlag('runner-pool') ??
|
|
256
|
+
process.env['VIEWPORT_MANAGED_RUNNER_POOL'] ??
|
|
257
|
+
process.env['VIEWPORT_MANAGED_EXECUTOR_RUNNER_POOL'] ??
|
|
258
|
+
profileRunnerPool ??
|
|
259
|
+
undefined,
|
|
165
260
|
agentCommand: getFlag('agent-command') ?? process.env['VIEWPORT_MANAGED_AGENT_COMMAND'],
|
|
166
261
|
actionCommand: getFlag('action-command') ?? process.env['VIEWPORT_MANAGED_ACTION_COMMAND'],
|
|
167
262
|
providerActions: hasFlag('provider-actions') || process.env['VIEWPORT_MANAGED_PROVIDER_ACTIONS'] === '1',
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
263
|
+
tools: [
|
|
264
|
+
...new Set([
|
|
265
|
+
...detected.tools,
|
|
266
|
+
...listFlagOrProfile('tools', profileCapabilities['tools']),
|
|
267
|
+
]),
|
|
268
|
+
],
|
|
269
|
+
agents: listFlagOrProfile('agents', profileCapabilities['agents']),
|
|
270
|
+
models: listFlagOrProfile('models', profileCapabilities['models']),
|
|
271
|
+
integrations: [
|
|
272
|
+
...new Set([
|
|
273
|
+
...detected.integrations,
|
|
274
|
+
...listFlagOrProfile('integrations', profileCapabilities['integrations']),
|
|
275
|
+
]),
|
|
276
|
+
],
|
|
277
|
+
secrets: listFlagOrProfile('secrets', profileCapabilities['secrets']),
|
|
172
278
|
},
|
|
173
279
|
};
|
|
174
280
|
}
|
|
281
|
+
function workflowWorkerUsage() {
|
|
282
|
+
return 'Usage: vpd workflow worker --server <url> --workspace <id> --executor <id> --credential <token> [--workdir <path>] [--runner-pool <pool>] [--agent-command <command>] [--action-command <command>] [--provider-actions] [--doctor|--preflight|--once]\n vpd workflow worker --registration-profile <path> [--doctor|--preflight|--once]';
|
|
283
|
+
}
|
|
284
|
+
function readRegistrationProfile() {
|
|
285
|
+
const profilePath = getFlag('registration-profile') ?? process.env['VIEWPORT_MANAGED_EXECUTOR_PROFILE_FILE'];
|
|
286
|
+
if (!profilePath)
|
|
287
|
+
return null;
|
|
288
|
+
const resolved = resolveProfilePath(profilePath);
|
|
289
|
+
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
290
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
291
|
+
throw new Error(`Managed executor registration profile is not a JSON object: ${resolved}`);
|
|
292
|
+
}
|
|
293
|
+
const record = parsed;
|
|
294
|
+
const schema = stringValue(record['schema']);
|
|
295
|
+
if (schema && schema !== 'viewport.managed_executor_registration/v1') {
|
|
296
|
+
throw new Error(`Unsupported managed executor registration profile schema: ${schema}`);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
serverUrl: stringValue(record['server_url']) ?? stringValue(record['serverUrl']),
|
|
300
|
+
workspaceId: stringValue(record['workspace_id']) ?? stringValue(record['workspaceId']),
|
|
301
|
+
executorId: stringValue(record['managed_executor_id']) ??
|
|
302
|
+
stringValue(record['executor_id']) ??
|
|
303
|
+
stringValue(record['executorId']),
|
|
304
|
+
credential: stringValue(record['credential']),
|
|
305
|
+
accessMode: stringValue(record['access_mode']) ?? stringValue(record['accessMode']),
|
|
306
|
+
runnerProfile: stringValue(record['runner_profile']) ?? stringValue(record['runnerProfile']),
|
|
307
|
+
runnerPosture: recordValue(record['runner_posture']) ?? recordValue(record['runnerPosture']),
|
|
308
|
+
capabilities: record['capabilities'] &&
|
|
309
|
+
typeof record['capabilities'] === 'object' &&
|
|
310
|
+
!Array.isArray(record['capabilities'])
|
|
311
|
+
? record['capabilities']
|
|
312
|
+
: undefined,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function resolveProfilePath(profilePath) {
|
|
316
|
+
if (profilePath === '~')
|
|
317
|
+
return os.homedir();
|
|
318
|
+
if (profilePath.startsWith('~/'))
|
|
319
|
+
return path.join(os.homedir(), profilePath.slice(2));
|
|
320
|
+
return path.resolve(profilePath);
|
|
321
|
+
}
|
|
322
|
+
function loadOrCreateRunnerKeyPair(workspaceId, executorId) {
|
|
323
|
+
const keyDir = path.join(os.homedir(), '.viewport', 'runner-keys');
|
|
324
|
+
fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
|
|
325
|
+
const safeName = `${safeFilename(workspaceId)}-${safeFilename(executorId)}.json`;
|
|
326
|
+
const keyPath = path.join(keyDir, safeName);
|
|
327
|
+
if (fs.existsSync(keyPath)) {
|
|
328
|
+
const parsed = JSON.parse(fs.readFileSync(keyPath, 'utf8'));
|
|
329
|
+
if (isRunnerKeyPair(parsed, keyPath))
|
|
330
|
+
return parsed;
|
|
331
|
+
}
|
|
332
|
+
const pair = generateKeyPairSync('rsa', {
|
|
333
|
+
modulusLength: 2048,
|
|
334
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
335
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
336
|
+
});
|
|
337
|
+
const keyPair = {
|
|
338
|
+
schema: 'viewport.runner_keypair/v1',
|
|
339
|
+
algorithm: 'RSA-OAEP-256',
|
|
340
|
+
publicKeyPem: pair.publicKey,
|
|
341
|
+
privateKeyPem: pair.privateKey,
|
|
342
|
+
fingerprint: publicKeyFingerprint(pair.publicKey),
|
|
343
|
+
path: keyPath,
|
|
344
|
+
};
|
|
345
|
+
fs.writeFileSync(keyPath, JSON.stringify(keyPair, null, 2), { mode: 0o600 });
|
|
346
|
+
return keyPair;
|
|
347
|
+
}
|
|
348
|
+
function isRunnerKeyPair(value, keyPath) {
|
|
349
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
350
|
+
return false;
|
|
351
|
+
const record = value;
|
|
352
|
+
if (record['schema'] !== 'viewport.runner_keypair/v1' ||
|
|
353
|
+
record['algorithm'] !== 'RSA-OAEP-256' ||
|
|
354
|
+
typeof record['publicKeyPem'] !== 'string' ||
|
|
355
|
+
typeof record['privateKeyPem'] !== 'string' ||
|
|
356
|
+
typeof record['fingerprint'] !== 'string') {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if (typeof record['path'] !== 'string') {
|
|
360
|
+
record['path'] = keyPath;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
function publicKeyFingerprint(publicKeyPem) {
|
|
365
|
+
const body = publicKeyPem
|
|
366
|
+
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
367
|
+
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
368
|
+
.replace(/\s+/g, '');
|
|
369
|
+
const der = Buffer.from(body, 'base64');
|
|
370
|
+
return `sha256:${createHash('sha256').update(der).digest('hex')}`;
|
|
371
|
+
}
|
|
372
|
+
function safeFilename(value) {
|
|
373
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'runner';
|
|
374
|
+
}
|
|
375
|
+
function detectLocalCapabilities() {
|
|
376
|
+
const tools = ['git', 'node', 'pnpm', 'docker', 'gh'].filter(commandExists);
|
|
377
|
+
const integrations = [
|
|
378
|
+
...(commandExists('gh') ? ['github'] : []),
|
|
379
|
+
...(process.env['NOTION_TOKEN'] ? ['notion'] : []),
|
|
380
|
+
...(process.env['CONFLUENCE_API_TOKEN'] && process.env['CONFLUENCE_BASE_URL']
|
|
381
|
+
? ['confluence']
|
|
382
|
+
: []),
|
|
383
|
+
];
|
|
384
|
+
return { tools, integrations };
|
|
385
|
+
}
|
|
386
|
+
function commandExists(command) {
|
|
387
|
+
return (spawnSync('sh', ['-lc', `command -v ${shellQuote(command)} >/dev/null 2>&1`], {
|
|
388
|
+
stdio: 'ignore',
|
|
389
|
+
}).status === 0);
|
|
390
|
+
}
|
|
391
|
+
function shellQuote(value) {
|
|
392
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
393
|
+
}
|
|
394
|
+
function listFlagOrProfile(flag, profileValue) {
|
|
395
|
+
const fromFlag = listFlagValue(getFlag(flag));
|
|
396
|
+
return fromFlag.length > 0 ? fromFlag : stringList(profileValue);
|
|
397
|
+
}
|
|
398
|
+
function stringList(value) {
|
|
399
|
+
if (!Array.isArray(value))
|
|
400
|
+
return [];
|
|
401
|
+
return value.filter((entry) => typeof entry === 'string' && entry.trim() !== '');
|
|
402
|
+
}
|
|
403
|
+
function stringValue(value) {
|
|
404
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
405
|
+
}
|
|
175
406
|
async function heartbeat(options, status, healthStatus) {
|
|
176
407
|
await platformJson(options, 'POST', 'heartbeat', {
|
|
177
408
|
status,
|
|
@@ -179,9 +410,57 @@ async function heartbeat(options, status, healthStatus) {
|
|
|
179
410
|
access_mode: options.accessMode,
|
|
180
411
|
runner_profile: options.runnerProfile ?? null,
|
|
181
412
|
runner_posture: {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
413
|
+
...(options.runnerPosture ?? {}),
|
|
414
|
+
transport: {
|
|
415
|
+
...recordValue(options.runnerPosture?.['transport']),
|
|
416
|
+
mode: options.accessMode,
|
|
417
|
+
},
|
|
418
|
+
execution: recordValue(options.runnerPosture?.['execution']) ?? { kind: 'customer-managed' },
|
|
419
|
+
secrets: {
|
|
420
|
+
...recordValue(options.runnerPosture?.['secrets']),
|
|
421
|
+
modes: [
|
|
422
|
+
...new Set([
|
|
423
|
+
'runner_local',
|
|
424
|
+
'runner_encrypted',
|
|
425
|
+
'run_scoped_grant',
|
|
426
|
+
...stringList(recordValue(options.runnerPosture?.['secrets'])?.['modes']),
|
|
427
|
+
]),
|
|
428
|
+
],
|
|
429
|
+
public_key: {
|
|
430
|
+
schema: 'viewport.runner_public_key/v1',
|
|
431
|
+
algorithm: options.runnerKeyPair.algorithm,
|
|
432
|
+
public_key_pem: options.runnerKeyPair.publicKeyPem,
|
|
433
|
+
fingerprint: options.runnerKeyPair.fingerprint,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
model_credentials: {
|
|
437
|
+
...recordValue(options.runnerPosture?.['model_credentials']),
|
|
438
|
+
anthropic: process.env['ANTHROPIC_API_KEY'] ? 'available' : 'missing',
|
|
439
|
+
openai: process.env['OPENAI_API_KEY'] ? 'available' : 'missing',
|
|
440
|
+
},
|
|
441
|
+
repo_credentials: {
|
|
442
|
+
...recordValue(options.runnerPosture?.['repo_credentials']),
|
|
443
|
+
runner_local: process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'] || commandExists('gh')
|
|
444
|
+
? 'available'
|
|
445
|
+
: 'missing',
|
|
446
|
+
run_scoped_grant: 'available',
|
|
447
|
+
},
|
|
448
|
+
context_worker: {
|
|
449
|
+
...recordValue(options.runnerPosture?.['context_worker']),
|
|
450
|
+
enabled: true,
|
|
451
|
+
supports: [
|
|
452
|
+
...new Set([
|
|
453
|
+
'git',
|
|
454
|
+
...(process.env['NOTION_TOKEN'] ? ['notion'] : []),
|
|
455
|
+
...(process.env['CONFLUENCE_API_TOKEN'] && process.env['CONFLUENCE_BASE_URL']
|
|
456
|
+
? ['confluence']
|
|
457
|
+
: []),
|
|
458
|
+
]),
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
version: stringValue(options.runnerPosture?.['version']) ??
|
|
462
|
+
process.env['npm_package_version'] ??
|
|
463
|
+
null,
|
|
185
464
|
},
|
|
186
465
|
capabilities: capabilityPayload(options.capabilities),
|
|
187
466
|
});
|
|
@@ -197,7 +476,7 @@ async function safeHeartbeat(options, status, healthStatus) {
|
|
|
197
476
|
function managedWorkerAccessMode(value) {
|
|
198
477
|
if (value === 'polling' || value === 'direct' || value === 'relay')
|
|
199
478
|
return value;
|
|
200
|
-
return '
|
|
479
|
+
return 'polling';
|
|
201
480
|
}
|
|
202
481
|
async function claimAssignment(options) {
|
|
203
482
|
const response = await platformFetch(options, 'POST', 'claim', {
|
|
@@ -283,7 +562,7 @@ async function runProviderActionReplay(assignment) {
|
|
|
283
562
|
action: {
|
|
284
563
|
adapter: assignment.adapter,
|
|
285
564
|
action: assignment.action,
|
|
286
|
-
input: actionInput,
|
|
565
|
+
input: sanitizeActionInput(actionInput),
|
|
287
566
|
response: response ?? null,
|
|
288
567
|
},
|
|
289
568
|
},
|
|
@@ -305,7 +584,7 @@ async function runProviderActionReplay(assignment) {
|
|
|
305
584
|
action: {
|
|
306
585
|
adapter: assignment.adapter,
|
|
307
586
|
action: assignment.action,
|
|
308
|
-
input: actionInput,
|
|
587
|
+
input: sanitizeActionInput(actionInput),
|
|
309
588
|
response: response ?? null,
|
|
310
589
|
},
|
|
311
590
|
},
|
|
@@ -503,14 +782,24 @@ async function runAssignmentLocally(options, assignment) {
|
|
|
503
782
|
return existingRun;
|
|
504
783
|
}
|
|
505
784
|
const directory = await ensureDirectory(options.workdir ?? assignment.directory_path ?? process.cwd());
|
|
785
|
+
const material = await materializeRunCredentials(options, assignment);
|
|
506
786
|
const started = await daemonJson('POST', '/api/workflows/runs', {
|
|
507
787
|
workflowYaml: assignment.yaml_snapshot,
|
|
508
788
|
workflowSourceRef: assignment.source_ref ?? `viewport://managed-executor/${assignment.id}`,
|
|
509
789
|
directoryId: directory.id,
|
|
510
|
-
inputs: assignmentInputs(assignment),
|
|
790
|
+
inputs: assignmentInputs(assignment, material.metadata),
|
|
791
|
+
runtimeSecretEnv: material.runtimeSecretEnv,
|
|
511
792
|
resourceId: options.workspaceId,
|
|
512
793
|
runtimeTargetId: assignment.runtime_target_id ?? undefined,
|
|
513
794
|
platformRunId: assignment.id,
|
|
795
|
+
resourceManifest: assignment.resource_manifest ?? undefined,
|
|
796
|
+
workflowAuthorityContract: assignment.workflow_authority_contract ??
|
|
797
|
+
recordChildValue(assignment.route_snapshot, 'workflow_authority_contract') ??
|
|
798
|
+
recordChildValue(assignment.execution_profile_snapshot, 'workflow_authority_contract') ??
|
|
799
|
+
recordChildValue(assignment.workflow_snapshot, 'workflow_authority_contract') ??
|
|
800
|
+
recordChildValue(assignment.runner_workspace_snapshot, 'workflow_authority_contract') ??
|
|
801
|
+
recordChildValue(recordChildValue(assignment.input_snapshot, 'viewport'), 'workflowAuthorityContract') ??
|
|
802
|
+
undefined,
|
|
514
803
|
initiation: 'cli',
|
|
515
804
|
dataCapturePolicy: assignment.data_capture_policy ?? undefined,
|
|
516
805
|
});
|
|
@@ -519,7 +808,15 @@ async function runAssignmentLocally(options, assignment) {
|
|
|
519
808
|
await syncLocalRun(options, assignment.id, run, assignment.assignment_claim_token);
|
|
520
809
|
}, progressSyncEveryMs(options.leaseSeconds));
|
|
521
810
|
}
|
|
522
|
-
function
|
|
811
|
+
function recordChildValue(value, key) {
|
|
812
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
813
|
+
return undefined;
|
|
814
|
+
const entry = value[key];
|
|
815
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
816
|
+
return undefined;
|
|
817
|
+
return entry;
|
|
818
|
+
}
|
|
819
|
+
function assignmentInputs(assignment, credentialMetadata = []) {
|
|
523
820
|
const inputs = { ...(assignment.input_snapshot ?? {}) };
|
|
524
821
|
inputs['viewport'] = {
|
|
525
822
|
...(isRecord(inputs['viewport']) ? inputs['viewport'] : {}),
|
|
@@ -530,9 +827,162 @@ function assignmentInputs(assignment) {
|
|
|
530
827
|
workflow: assignment.workflow_snapshot ?? null,
|
|
531
828
|
runnerWorkspace: assignment.runner_workspace_snapshot ?? null,
|
|
532
829
|
contextReceipts: assignment.context_receipts_snapshot ?? null,
|
|
830
|
+
credentials: credentialMetadata,
|
|
533
831
|
};
|
|
534
832
|
return inputs;
|
|
535
833
|
}
|
|
834
|
+
async function materializeRunCredentials(options, assignment) {
|
|
835
|
+
const handles = collectCredentialHandles(assignment);
|
|
836
|
+
if (handles.length === 0)
|
|
837
|
+
return { runtimeSecretEnv: {}, metadata: [] };
|
|
838
|
+
const runtimeSecretEnv = {};
|
|
839
|
+
const metadata = [];
|
|
840
|
+
for (const handle of handles) {
|
|
841
|
+
const response = await materializeCredential(options, assignment, handle);
|
|
842
|
+
const envName = envNameForCredentialRef(handle);
|
|
843
|
+
const secret = stringField(response, 'secret');
|
|
844
|
+
if (secret) {
|
|
845
|
+
runtimeSecretEnv[envName] = secret;
|
|
846
|
+
}
|
|
847
|
+
const wrappedSecret = recordField(response, 'wrapped_secret');
|
|
848
|
+
if (wrappedSecret) {
|
|
849
|
+
runtimeSecretEnv[envName] = decryptRunnerWrappedSecret(options.runnerKeyPair, wrappedSecret);
|
|
850
|
+
}
|
|
851
|
+
metadata.push({
|
|
852
|
+
handle,
|
|
853
|
+
envName,
|
|
854
|
+
kind: stringField(response, 'kind') ?? null,
|
|
855
|
+
storagePosture: stringField(response, 'storage_posture') ?? null,
|
|
856
|
+
materialAvailable: response['material_available'] === true,
|
|
857
|
+
runnerLocalRequired: response['runner_local_required'] === true,
|
|
858
|
+
provider: stringField(response, 'provider') ?? null,
|
|
859
|
+
credentialId: stringField(response, 'credential_id') ?? numberField(response, 'credential_id') ?? null,
|
|
860
|
+
scopes: response['scopes'],
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
return { runtimeSecretEnv, metadata };
|
|
864
|
+
}
|
|
865
|
+
async function materializeCredential(options, assignment, handle) {
|
|
866
|
+
if (!assignment.assignment_claim_token) {
|
|
867
|
+
throw new Error(`Managed workflow assignment ${assignment.id} is missing claim_token.`);
|
|
868
|
+
}
|
|
869
|
+
const body = await platformJson(options, 'POST', `workflow-runs/${encodeURIComponent(assignment.id)}/credential-material`, {
|
|
870
|
+
credential: options.credential,
|
|
871
|
+
handle,
|
|
872
|
+
...repositoryForCredentialMaterialization(assignment, handle),
|
|
873
|
+
}, assignment.assignment_claim_token);
|
|
874
|
+
const data = dataFrom(body);
|
|
875
|
+
if (!isRecord(data)) {
|
|
876
|
+
throw new Error(`Credential material response for ${handle} was not an object.`);
|
|
877
|
+
}
|
|
878
|
+
return data;
|
|
879
|
+
}
|
|
880
|
+
function repositoryForCredentialMaterialization(assignment, handle) {
|
|
881
|
+
const checkoutEntries = [
|
|
882
|
+
...credentialEntriesFrom(pathValue(asRecord(assignment.execution_profile_snapshot), ['credentials', 'repo_checkout'])),
|
|
883
|
+
...credentialEntriesFrom(pathValue(asRecord(assignment.workflow_snapshot), ['credentials', 'repo_checkout'])),
|
|
884
|
+
];
|
|
885
|
+
const explicit = checkoutEntries.find((entry) => {
|
|
886
|
+
if (!isRecord(entry))
|
|
887
|
+
return entry === handle;
|
|
888
|
+
const entryHandle = stringField(entry, 'handle') ??
|
|
889
|
+
stringField(entry, 'ref') ??
|
|
890
|
+
stringField(entry, 'credential_ref');
|
|
891
|
+
return entryHandle === handle;
|
|
892
|
+
});
|
|
893
|
+
const explicitRepo = explicit && isRecord(explicit)
|
|
894
|
+
? (stringField(explicit, 'repository') ?? stringField(explicit, 'repo'))
|
|
895
|
+
: null;
|
|
896
|
+
if (explicitRepo)
|
|
897
|
+
return { repository: explicitRepo };
|
|
898
|
+
const allowed = allowedRepositoriesFromAssignment(assignment);
|
|
899
|
+
return allowed.length === 1 && allowed[0] ? { repository: allowed[0] } : {};
|
|
900
|
+
}
|
|
901
|
+
function allowedRepositoriesFromAssignment(assignment) {
|
|
902
|
+
const candidates = [
|
|
903
|
+
pathValue(assignment.workflow_authority_contract ?? {}, ['repos', 'allowed']),
|
|
904
|
+
pathValue(recordChildValue(assignment.route_snapshot, 'workflow_authority_contract') ?? {}, [
|
|
905
|
+
'repos',
|
|
906
|
+
'allowed',
|
|
907
|
+
]),
|
|
908
|
+
pathValue(recordChildValue(assignment.execution_profile_snapshot, 'workflow_authority_contract') ?? {}, ['repos', 'allowed']),
|
|
909
|
+
pathValue(recordChildValue(assignment.workflow_snapshot, 'workflow_authority_contract') ?? {}, [
|
|
910
|
+
'repos',
|
|
911
|
+
'allowed',
|
|
912
|
+
]),
|
|
913
|
+
pathValue(recordChildValue(assignment.runner_workspace_snapshot, 'workflow_authority_contract') ?? {}, ['repos', 'allowed']),
|
|
914
|
+
];
|
|
915
|
+
return [
|
|
916
|
+
...new Set(candidates
|
|
917
|
+
.flatMap((value) => (Array.isArray(value) ? value : []))
|
|
918
|
+
.filter((value) => typeof value === 'string' && value.trim() !== '')
|
|
919
|
+
.map((value) => value.trim())),
|
|
920
|
+
];
|
|
921
|
+
}
|
|
922
|
+
function decryptRunnerWrappedSecret(keyPair, wrapped) {
|
|
923
|
+
const schema = stringField(wrapped, 'schema');
|
|
924
|
+
const algorithm = stringField(wrapped, 'algorithm');
|
|
925
|
+
const fingerprint = stringField(wrapped, 'runner_public_key_fingerprint');
|
|
926
|
+
const ciphertext = stringField(wrapped, 'ciphertext');
|
|
927
|
+
if (schema !== 'viewport.runner_wrapped_secret/v1' ||
|
|
928
|
+
algorithm !== 'RSA-OAEP-256' ||
|
|
929
|
+
!fingerprint ||
|
|
930
|
+
!ciphertext) {
|
|
931
|
+
throw new Error('Runner-encrypted credential material is malformed.');
|
|
932
|
+
}
|
|
933
|
+
if (fingerprint !== keyPair.fingerprint) {
|
|
934
|
+
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.`);
|
|
935
|
+
}
|
|
936
|
+
return privateDecrypt({
|
|
937
|
+
key: keyPair.privateKeyPem,
|
|
938
|
+
oaepHash: 'sha256',
|
|
939
|
+
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
940
|
+
}, Buffer.from(ciphertext, 'base64')).toString('utf8');
|
|
941
|
+
}
|
|
942
|
+
function collectCredentialHandles(assignment) {
|
|
943
|
+
const snapshots = [assignment.execution_profile_snapshot, assignment.workflow_snapshot].filter(isRecord);
|
|
944
|
+
const handles = new Set();
|
|
945
|
+
for (const snapshot of snapshots) {
|
|
946
|
+
for (const handle of [
|
|
947
|
+
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'include'])),
|
|
948
|
+
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'repo_checkout'])),
|
|
949
|
+
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'mcp_api'])),
|
|
950
|
+
...credentialRefsFrom(snapshot['credential_refs']),
|
|
951
|
+
]) {
|
|
952
|
+
handles.add(handle);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return [...handles].sort();
|
|
956
|
+
}
|
|
957
|
+
function credentialRefsFrom(entries) {
|
|
958
|
+
return credentialEntriesFrom(entries).flatMap((entry) => {
|
|
959
|
+
if (typeof entry === 'string' && entry.trim() !== '')
|
|
960
|
+
return [entry];
|
|
961
|
+
if (!isRecord(entry))
|
|
962
|
+
return [];
|
|
963
|
+
for (const key of ['handle', 'ref', 'credential_ref']) {
|
|
964
|
+
const value = stringField(entry, key);
|
|
965
|
+
if (value)
|
|
966
|
+
return [value];
|
|
967
|
+
}
|
|
968
|
+
return [];
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
function credentialEntriesFrom(entries) {
|
|
972
|
+
return Array.isArray(entries) ? entries : [];
|
|
973
|
+
}
|
|
974
|
+
function asRecord(value) {
|
|
975
|
+
return isRecord(value) ? value : {};
|
|
976
|
+
}
|
|
977
|
+
function pathValue(value, pathParts) {
|
|
978
|
+
let current = value;
|
|
979
|
+
for (const part of pathParts) {
|
|
980
|
+
if (!isRecord(current))
|
|
981
|
+
return undefined;
|
|
982
|
+
current = current[part];
|
|
983
|
+
}
|
|
984
|
+
return current;
|
|
985
|
+
}
|
|
536
986
|
function isRecord(value) {
|
|
537
987
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
538
988
|
}
|
|
@@ -554,9 +1004,27 @@ async function waitForApprovalAndResume(options, platformRunId, localRunId, assi
|
|
|
554
1004
|
while (true) {
|
|
555
1005
|
await heartbeat(options, 'online', 'busy');
|
|
556
1006
|
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
557
|
-
const approved =
|
|
1007
|
+
const approved = await approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId);
|
|
558
1008
|
if (approved) {
|
|
559
|
-
|
|
1009
|
+
const resumed = await resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken);
|
|
1010
|
+
if (resumed.status !== 'blocked')
|
|
1011
|
+
return resumed;
|
|
1012
|
+
const blockedIds = blockedNodeIds(resumed);
|
|
1013
|
+
if (alreadyResolvedApprovalRuns.has(resumed) && !blockedIds.has(approved.node_key)) {
|
|
1014
|
+
const nextApproved = await approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId);
|
|
1015
|
+
if (nextApproved &&
|
|
1016
|
+
nextApproved.node_key !== approved.node_key &&
|
|
1017
|
+
blockedIds.has(nextApproved.node_key)) {
|
|
1018
|
+
await delay(options.sleepSeconds * 1000);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
await delay(options.sleepSeconds * 1000);
|
|
1022
|
+
return resumed;
|
|
1023
|
+
}
|
|
1024
|
+
if (blockedIds.has(approved.node_key) &&
|
|
1025
|
+
managedApprovalDecision(approved) !== 'request_changes') {
|
|
1026
|
+
return resumed;
|
|
1027
|
+
}
|
|
560
1028
|
}
|
|
561
1029
|
if (assignment.status === 'canceled' || assignment.status === 'failed') {
|
|
562
1030
|
const canceled = await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/cancel`, {
|
|
@@ -570,42 +1038,103 @@ async function waitForApprovalAndResume(options, platformRunId, localRunId, assi
|
|
|
570
1038
|
await delay(options.sleepSeconds * 1000);
|
|
571
1039
|
}
|
|
572
1040
|
}
|
|
573
|
-
|
|
1041
|
+
const alreadyResolvedApprovalRuns = new WeakSet();
|
|
1042
|
+
async function approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId) {
|
|
574
1043
|
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
575
|
-
|
|
1044
|
+
const approvedNodes = assignment.nodes?.filter(isResolvedManagedGateNode) ?? [];
|
|
1045
|
+
if (approvedNodes.length === 0)
|
|
1046
|
+
return null;
|
|
1047
|
+
if (!localRunId)
|
|
1048
|
+
return approvedNodes[0] ?? null;
|
|
1049
|
+
const localRun = await readExistingLocalRun(localRunId);
|
|
1050
|
+
const blockedIds = blockedNodeIds(localRun);
|
|
1051
|
+
if (blockedIds.size > 0) {
|
|
1052
|
+
return approvedNodes.find((node) => blockedIds.has(node.node_key)) ?? null;
|
|
1053
|
+
}
|
|
1054
|
+
return approvedNodes[0] ?? null;
|
|
1055
|
+
}
|
|
1056
|
+
function blockedNodeIds(run) {
|
|
1057
|
+
return new Set(Object.values(run?.nodes ?? {})
|
|
1058
|
+
.filter((node) => node.status === 'blocked')
|
|
1059
|
+
.map((node) => node.id));
|
|
576
1060
|
}
|
|
577
1061
|
async function resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken) {
|
|
578
|
-
|
|
579
|
-
approved
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1062
|
+
try {
|
|
1063
|
+
await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/approvals/${encodeURIComponent(approved.node_key)}`, {
|
|
1064
|
+
approved: managedApprovalApproved(approved),
|
|
1065
|
+
decision: managedApprovalDecision(approved),
|
|
1066
|
+
message: approvalMessage(approved),
|
|
1067
|
+
actor: approvalActor(approved),
|
|
1068
|
+
expectedActionDigest: approvalExpectedActionDigest(approved),
|
|
1069
|
+
executionGrant: approvalExecutionGrant(approved),
|
|
1070
|
+
feedback: approvalFeedback(approved),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
if (!isAlreadyResolvedApprovalError(error))
|
|
1075
|
+
throw error;
|
|
1076
|
+
const current = await readExistingLocalRun(localRunId);
|
|
1077
|
+
if (!current)
|
|
1078
|
+
throw error;
|
|
1079
|
+
alreadyResolvedApprovalRuns.add(current);
|
|
1080
|
+
await syncLocalRun(options, platformRunId, current, assignmentClaimToken);
|
|
1081
|
+
return current;
|
|
1082
|
+
}
|
|
585
1083
|
const resumed = await pollLocalRun(localRunId, async (run) => {
|
|
586
1084
|
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
587
1085
|
}, progressSyncEveryMs(options.leaseSeconds));
|
|
588
1086
|
await syncLocalRun(options, platformRunId, resumed, assignmentClaimToken);
|
|
589
1087
|
return resumed;
|
|
590
1088
|
}
|
|
591
|
-
function
|
|
1089
|
+
function isAlreadyResolvedApprovalError(error) {
|
|
1090
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1091
|
+
return message.includes('Workflow node is not awaiting approval');
|
|
1092
|
+
}
|
|
1093
|
+
function isResolvedManagedGateNode(node) {
|
|
592
1094
|
if (!['approval', 'gate', 'plan', 'action'].includes(String(node.type ?? '')))
|
|
593
1095
|
return false;
|
|
594
1096
|
if (node.status === 'completed')
|
|
595
1097
|
return true;
|
|
1098
|
+
const approval = node.metadata?.['approval'];
|
|
1099
|
+
if ((node.type === 'plan' || node.type === 'gate' || node.type === 'approval') &&
|
|
1100
|
+
node.status === 'blocked' &&
|
|
1101
|
+
approval &&
|
|
1102
|
+
typeof approval === 'object' &&
|
|
1103
|
+
'approved' in approval) {
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
596
1106
|
if (node.type !== 'action' || node.status !== 'queued')
|
|
597
1107
|
return false;
|
|
598
|
-
const approval = node.metadata?.['approval'];
|
|
599
1108
|
return (!!approval &&
|
|
600
1109
|
typeof approval === 'object' &&
|
|
601
1110
|
approval.approved === true);
|
|
602
1111
|
}
|
|
1112
|
+
function managedApprovalApproved(node) {
|
|
1113
|
+
const approval = node.metadata?.['approval'];
|
|
1114
|
+
if (approval && typeof approval === 'object' && 'approved' in approval) {
|
|
1115
|
+
return approval.approved === true;
|
|
1116
|
+
}
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
function managedApprovalDecision(node) {
|
|
1120
|
+
const approval = node.metadata?.['approval'];
|
|
1121
|
+
if (approval && typeof approval === 'object') {
|
|
1122
|
+
const approved = approval.approved;
|
|
1123
|
+
const decision = approval.decision;
|
|
1124
|
+
if (approved === false) {
|
|
1125
|
+
if (decision === 'request_changes' || decision === 'changes_requested')
|
|
1126
|
+
return 'request_changes';
|
|
1127
|
+
return 'reject';
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return 'approve';
|
|
1131
|
+
}
|
|
603
1132
|
async function getAssignment(options, platformRunId, assignmentClaimToken) {
|
|
604
1133
|
const body = await platformJson(options, 'GET', `workflow-runs/${encodeURIComponent(platformRunId)}`, undefined, assignmentClaimToken);
|
|
605
1134
|
return dataFrom(body);
|
|
606
1135
|
}
|
|
607
1136
|
async function syncLocalRun(options, platformRunId, run, assignmentClaimToken) {
|
|
608
|
-
const body = await platformJson(options, 'PATCH', `workflow-runs/${encodeURIComponent(platformRunId)}/sync`, localRunToSyncPayload(run), assignmentClaimToken);
|
|
1137
|
+
const body = await platformJson(options, 'PATCH', `workflow-runs/${encodeURIComponent(platformRunId)}/sync`, localRunToSyncPayload(run, { includeApprovalDecisions: false }), assignmentClaimToken);
|
|
609
1138
|
return dataFrom(body);
|
|
610
1139
|
}
|
|
611
1140
|
async function ensureDirectory(directoryPath) {
|