@viewportai/daemon 0.25.3 → 0.25.5
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/adapters/claude.js +6 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/cli/worker-runtime.d.ts.map +1 -1
- package/dist/cli/worker-runtime.js +123 -0
- package/dist/cli/worker-runtime.js.map +1 -1
- package/dist/cli/workflow-managed-worker-types.d.ts +10 -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 +333 -49
- package/dist/cli/workflow-managed-worker.js.map +1 -1
- package/dist/server/http-request-schemas.d.ts +4 -0
- package/dist/server/http-request-schemas.d.ts.map +1 -1
- package/dist/server/http-request-schemas.js +10 -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 +17 -0
- package/dist/server/http-server.js.map +1 -1
- package/dist/workflows/action-adapters.d.ts +2 -0
- package/dist/workflows/action-adapters.d.ts.map +1 -1
- package/dist/workflows/action-adapters.js +2 -0
- package/dist/workflows/action-adapters.js.map +1 -1
- package/dist/workflows/action-provider-adapters.d.ts +6 -2
- package/dist/workflows/action-provider-adapters.d.ts.map +1 -1
- package/dist/workflows/action-provider-adapters.js +49 -3
- package/dist/workflows/action-provider-adapters.js.map +1 -1
- package/dist/workflows/action-provider-utils.d.ts +4 -1
- package/dist/workflows/action-provider-utils.d.ts.map +1 -1
- package/dist/workflows/action-provider-utils.js +22 -2
- package/dist/workflows/action-provider-utils.js.map +1 -1
- package/dist/workflows/approval-on-reject.js +1 -0
- package/dist/workflows/approval-on-reject.js.map +1 -1
- package/dist/workflows/daemon-session.js +1 -0
- package/dist/workflows/daemon-session.js.map +1 -1
- package/dist/workflows/expression.d.ts +3 -0
- package/dist/workflows/expression.d.ts.map +1 -1
- package/dist/workflows/expression.js +18 -1
- package/dist/workflows/expression.js.map +1 -1
- package/dist/workflows/inline-agents.d.ts.map +1 -1
- package/dist/workflows/inline-agents.js +1 -5
- 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 +6 -0
- package/dist/workflows/node-executor.d.ts.map +1 -1
- package/dist/workflows/node-executor.js +24 -2
- 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 +25 -6
- package/dist/workflows/node-registry.js.map +1 -1
- package/dist/workflows/platform-command-applier.d.ts +3 -1
- package/dist/workflows/platform-command-applier.d.ts.map +1 -1
- package/dist/workflows/platform-command-applier.js +77 -1
- package/dist/workflows/platform-command-applier.js.map +1 -1
- package/dist/workflows/platform-runtime-command.d.ts +18 -1
- package/dist/workflows/platform-runtime-command.d.ts.map +1 -1
- package/dist/workflows/platform-runtime-command.js +36 -3
- package/dist/workflows/platform-runtime-command.js.map +1 -1
- package/dist/workflows/run-types.d.ts +9 -0
- package/dist/workflows/run-types.d.ts.map +1 -1
- package/dist/workflows/runner-scheduler.d.ts +1 -0
- package/dist/workflows/runner-scheduler.d.ts.map +1 -1
- package/dist/workflows/runner-scheduler.js +1 -0
- package/dist/workflows/runner-scheduler.js.map +1 -1
- package/dist/workflows/runner.d.ts +7 -0
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +113 -7
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/subflow-executor.js +1 -1
- package/dist/workflows/subflow-executor.js.map +1 -1
- package/dist/workflows/workflow-production-schema.d.ts +12 -0
- package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
- package/dist/workflows/workflow-production-schema.js +9 -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 +12 -0
- package/dist/workflows/workflow-schema.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { constants, createHash, generateKeyPairSync, privateDecrypt, randomUUID,
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import YAML from 'yaml';
|
|
6
7
|
import { getArgs, getFlag, hasFlag } from './args.js';
|
|
7
8
|
import { daemonFetch, isDaemonRunning } from './daemon-client.js';
|
|
8
9
|
import { isJsonMode, printJson } from './command-shared.js';
|
|
@@ -247,6 +248,7 @@ function resolveWorkerOptions() {
|
|
|
247
248
|
registrationProfile?.runnerProfile ??
|
|
248
249
|
undefined,
|
|
249
250
|
runnerPosture: registrationProfile?.runnerPosture,
|
|
251
|
+
workerSessionId: randomUUID(),
|
|
250
252
|
runnerKeyPair,
|
|
251
253
|
signingIdentity,
|
|
252
254
|
runnerPool: getFlag('runner-pool') ??
|
|
@@ -531,7 +533,10 @@ async function heartbeat(options, status, healthStatus) {
|
|
|
531
533
|
...recordValue(options.runnerPosture?.['transport']),
|
|
532
534
|
mode: options.accessMode,
|
|
533
535
|
},
|
|
534
|
-
execution:
|
|
536
|
+
execution: {
|
|
537
|
+
...(recordValue(options.runnerPosture?.['execution']) ?? { kind: 'customer-managed' }),
|
|
538
|
+
worker_session_id: options.workerSessionId,
|
|
539
|
+
},
|
|
535
540
|
secrets: {
|
|
536
541
|
...recordValue(options.runnerPosture?.['secrets']),
|
|
537
542
|
modes: [
|
|
@@ -597,11 +602,12 @@ function managedWorkerAccessMode(value) {
|
|
|
597
602
|
async function claimAssignment(options) {
|
|
598
603
|
const response = await platformFetch(options, 'POST', 'claim', {
|
|
599
604
|
lease_seconds: options.leaseSeconds,
|
|
605
|
+
worker_session_id: options.workerSessionId,
|
|
600
606
|
});
|
|
601
607
|
if (response.status === 204)
|
|
602
608
|
return null;
|
|
603
609
|
const body = await responseJson(response);
|
|
604
|
-
return
|
|
610
|
+
return assignmentFrom(body);
|
|
605
611
|
}
|
|
606
612
|
async function claimActionReplay(options) {
|
|
607
613
|
if ((!options.capabilities.actionCommand && !options.capabilities.providerActions) ||
|
|
@@ -734,12 +740,13 @@ function normalizeReplayAction(adapter, action) {
|
|
|
734
740
|
return action;
|
|
735
741
|
}
|
|
736
742
|
function replayProviderReference(response) {
|
|
737
|
-
return (
|
|
738
|
-
stringField(response ?? null, 'apiUrl') ??
|
|
739
|
-
stringField(response ?? null, 'url') ??
|
|
740
|
-
stringField(response ?? null, 'channel') ??
|
|
743
|
+
return (numberField(response ?? null, 'number')?.toString() ??
|
|
741
744
|
stringField(response ?? null, 'ts') ??
|
|
742
|
-
|
|
745
|
+
stringField(response ?? null, 'id') ??
|
|
746
|
+
stringField(response ?? null, 'channel') ??
|
|
747
|
+
stringField(response ?? null, 'htmlUrl') ??
|
|
748
|
+
stringField(response ?? null, 'apiUrl') ??
|
|
749
|
+
stringField(response ?? null, 'url'));
|
|
743
750
|
}
|
|
744
751
|
function replayProviderUrl(response) {
|
|
745
752
|
return (stringField(response ?? null, 'htmlUrl') ??
|
|
@@ -896,38 +903,46 @@ async function runAssignmentLocally(options, assignment) {
|
|
|
896
903
|
const existingRun = await readExistingLocalRun(assignment.runtime_run_id);
|
|
897
904
|
if (existingRun) {
|
|
898
905
|
if (existingRun.status === 'running' || existingRun.status === 'queued') {
|
|
899
|
-
|
|
906
|
+
const completed = await pollLocalRun(existingRun.id, async (run) => {
|
|
900
907
|
await syncLocalRun(options, assignment.id, run, assignment.assignment_claim_token);
|
|
901
908
|
}, progressSyncEveryMs(options.leaseSeconds));
|
|
909
|
+
if (terminalRunStatus(completed.status)) {
|
|
910
|
+
clearRunCredentialMaterial(assignment.id);
|
|
911
|
+
}
|
|
912
|
+
return completed;
|
|
913
|
+
}
|
|
914
|
+
if (terminalRunStatus(existingRun.status)) {
|
|
915
|
+
clearRunCredentialMaterial(assignment.id);
|
|
902
916
|
}
|
|
903
917
|
return existingRun;
|
|
904
918
|
}
|
|
905
919
|
const directory = await ensureDirectory(options.workdir ?? assignment.directory_path ?? process.cwd());
|
|
906
|
-
const material = await
|
|
920
|
+
const material = await materializeAndCacheRunCredentials(options, assignment);
|
|
907
921
|
const started = await daemonJson('POST', '/api/workflows/runs', {
|
|
908
922
|
workflowYaml: assignment.yaml_snapshot,
|
|
909
923
|
workflowSourceRef: assignment.source_ref ?? `viewport://managed-executor/${assignment.id}`,
|
|
910
924
|
directoryId: directory.id,
|
|
911
|
-
inputs: assignmentInputs(assignment, material
|
|
925
|
+
inputs: assignmentInputs(assignment, material),
|
|
912
926
|
runtimeSecretEnv: material.runtimeSecretEnv,
|
|
927
|
+
runtimeSecretFiles: material.runtimeSecretFiles,
|
|
913
928
|
resourceId: options.workspaceId,
|
|
914
929
|
runtimeTargetId: assignment.runtime_target_id ?? undefined,
|
|
915
930
|
platformRunId: assignment.id,
|
|
916
|
-
resourceManifest: assignment
|
|
917
|
-
workflowAuthorityContract: assignment
|
|
918
|
-
recordChildValue(assignment.route_snapshot, 'workflow_authority_contract') ??
|
|
919
|
-
recordChildValue(assignment.execution_profile_snapshot, 'workflow_authority_contract') ??
|
|
920
|
-
recordChildValue(assignment.workflow_snapshot, 'workflow_authority_contract') ??
|
|
921
|
-
recordChildValue(assignment.runner_workspace_snapshot, 'workflow_authority_contract') ??
|
|
931
|
+
resourceManifest: assignmentResourceManifest(assignment) ?? undefined,
|
|
932
|
+
workflowAuthorityContract: assignmentWorkflowAuthorityContract(assignment) ??
|
|
922
933
|
recordChildValue(recordChildValue(assignment.input_snapshot, 'viewport'), 'workflowAuthorityContract') ??
|
|
923
934
|
undefined,
|
|
924
935
|
initiation: 'cli',
|
|
925
936
|
dataCapturePolicy: assignment.data_capture_policy ?? undefined,
|
|
926
937
|
});
|
|
927
938
|
const runId = readRun(started).id;
|
|
928
|
-
|
|
939
|
+
const completed = await pollLocalRun(runId, async (run) => {
|
|
929
940
|
await syncLocalRun(options, assignment.id, run, assignment.assignment_claim_token);
|
|
930
941
|
}, progressSyncEveryMs(options.leaseSeconds));
|
|
942
|
+
if (terminalRunStatus(completed.status)) {
|
|
943
|
+
clearRunCredentialMaterial(assignment.id);
|
|
944
|
+
}
|
|
945
|
+
return completed;
|
|
931
946
|
}
|
|
932
947
|
function recordChildValue(value, key) {
|
|
933
948
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -937,26 +952,88 @@ function recordChildValue(value, key) {
|
|
|
937
952
|
return undefined;
|
|
938
953
|
return entry;
|
|
939
954
|
}
|
|
940
|
-
function assignmentInputs(assignment,
|
|
955
|
+
function assignmentInputs(assignment, material = {
|
|
956
|
+
runtimeSecretEnv: {},
|
|
957
|
+
runtimeSecretFiles: {},
|
|
958
|
+
metadata: [],
|
|
959
|
+
}) {
|
|
941
960
|
const inputs = { ...(assignment.input_snapshot ?? {}) };
|
|
942
961
|
inputs['viewport'] = {
|
|
943
962
|
...(isRecord(inputs['viewport']) ? inputs['viewport'] : {}),
|
|
944
963
|
platformRunId: assignment.id,
|
|
945
964
|
schemaVersions: assignment.schema_versions ?? null,
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
965
|
+
target: assignmentTargetSnapshot(assignment) ?? null,
|
|
966
|
+
route: assignmentRouteSnapshot(assignment) ?? null,
|
|
967
|
+
executionProfile: assignmentExecutionProfileSnapshot(assignment) ?? null,
|
|
968
|
+
workflow: assignmentWorkflowSnapshot(assignment) ?? null,
|
|
969
|
+
runnerWorkspace: assignmentRunnerWorkspaceSnapshot(assignment) ?? null,
|
|
970
|
+
contextReceipts: assignmentContextReceiptsSnapshot(assignment) ?? null,
|
|
971
|
+
credentials: material.metadata,
|
|
952
972
|
};
|
|
953
973
|
return inputs;
|
|
954
974
|
}
|
|
975
|
+
const runCredentialMaterialCache = new Map();
|
|
976
|
+
const runCredentialProcessEnvCache = new Map();
|
|
977
|
+
function terminalRunStatus(status) {
|
|
978
|
+
return status === 'completed' || status === 'failed' || status === 'canceled';
|
|
979
|
+
}
|
|
980
|
+
async function materializeAndCacheRunCredentials(options, assignment) {
|
|
981
|
+
const material = await materializeRunCredentials(options, assignment);
|
|
982
|
+
runCredentialMaterialCache.set(assignment.id, material);
|
|
983
|
+
installRunCredentialProcessEnv(assignment.id, material.runtimeSecretEnv);
|
|
984
|
+
return material;
|
|
985
|
+
}
|
|
986
|
+
function installRunCredentialProcessEnv(runId, runtimeSecretEnv) {
|
|
987
|
+
const entries = Object.entries(runtimeSecretEnv);
|
|
988
|
+
if (entries.length === 0)
|
|
989
|
+
return;
|
|
990
|
+
const previous = runCredentialProcessEnvCache.get(runId) ?? {};
|
|
991
|
+
for (const [key, value] of entries) {
|
|
992
|
+
if (!(key in previous)) {
|
|
993
|
+
previous[key] = process.env[key];
|
|
994
|
+
}
|
|
995
|
+
process.env[key] = value;
|
|
996
|
+
}
|
|
997
|
+
runCredentialProcessEnvCache.set(runId, previous);
|
|
998
|
+
}
|
|
999
|
+
function clearRunCredentialMaterial(runId) {
|
|
1000
|
+
const material = runCredentialMaterialCache.get(runId);
|
|
1001
|
+
runCredentialMaterialCache.delete(runId);
|
|
1002
|
+
if (material) {
|
|
1003
|
+
for (const filePath of Object.values(material.runtimeSecretFiles)) {
|
|
1004
|
+
try {
|
|
1005
|
+
fs.rmSync(filePath, { force: true });
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
// Best-effort cleanup; the run-scoped directory is removed next.
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
fs.rmSync(path.join(process.env['VIEWPORT_HOME'] ?? path.join(os.homedir(), '.viewport'), 'run-secrets', runId), { recursive: true, force: true });
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// Best-effort cleanup.
|
|
1017
|
+
}
|
|
1018
|
+
const previous = runCredentialProcessEnvCache.get(runId);
|
|
1019
|
+
if (!previous)
|
|
1020
|
+
return;
|
|
1021
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
1022
|
+
if (value === undefined) {
|
|
1023
|
+
delete process.env[key];
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
process.env[key] = value;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
runCredentialProcessEnvCache.delete(runId);
|
|
1030
|
+
}
|
|
955
1031
|
async function materializeRunCredentials(options, assignment) {
|
|
956
1032
|
const handles = collectCredentialHandles(assignment);
|
|
957
1033
|
if (handles.length === 0)
|
|
958
|
-
return { runtimeSecretEnv: {}, metadata: [] };
|
|
1034
|
+
return { runtimeSecretEnv: {}, runtimeSecretFiles: {}, metadata: [] };
|
|
959
1035
|
const runtimeSecretEnv = {};
|
|
1036
|
+
const runtimeSecretFiles = {};
|
|
960
1037
|
const metadata = [];
|
|
961
1038
|
for (const handle of handles) {
|
|
962
1039
|
const response = await materializeCredential(options, assignment, handle);
|
|
@@ -964,10 +1041,13 @@ async function materializeRunCredentials(options, assignment) {
|
|
|
964
1041
|
const secret = stringField(response, 'secret');
|
|
965
1042
|
if (secret) {
|
|
966
1043
|
runtimeSecretEnv[envName] = secret;
|
|
1044
|
+
runtimeSecretFiles[envName] = await writeRunCredentialSecretFile(assignment.id, envName, secret);
|
|
967
1045
|
}
|
|
968
1046
|
const wrappedSecret = recordField(response, 'wrapped_secret');
|
|
969
1047
|
if (wrappedSecret) {
|
|
970
|
-
|
|
1048
|
+
const decrypted = decryptRunnerWrappedSecret(options.runnerKeyPair, wrappedSecret);
|
|
1049
|
+
runtimeSecretEnv[envName] = decrypted;
|
|
1050
|
+
runtimeSecretFiles[envName] = await writeRunCredentialSecretFile(assignment.id, envName, decrypted);
|
|
971
1051
|
}
|
|
972
1052
|
metadata.push({
|
|
973
1053
|
handle,
|
|
@@ -975,13 +1055,26 @@ async function materializeRunCredentials(options, assignment) {
|
|
|
975
1055
|
kind: stringField(response, 'kind') ?? null,
|
|
976
1056
|
storagePosture: stringField(response, 'storage_posture') ?? null,
|
|
977
1057
|
materialAvailable: response['material_available'] === true,
|
|
1058
|
+
runtimeSecretAvailable: typeof runtimeSecretEnv[envName] === 'string',
|
|
978
1059
|
runnerLocalRequired: response['runner_local_required'] === true,
|
|
979
1060
|
provider: stringField(response, 'provider') ?? null,
|
|
980
1061
|
credentialId: stringField(response, 'credential_id') ?? numberField(response, 'credential_id') ?? null,
|
|
981
1062
|
scopes: response['scopes'],
|
|
982
1063
|
});
|
|
983
1064
|
}
|
|
984
|
-
|
|
1065
|
+
const missingRuntimeSecrets = metadata.filter((entry) => entry.materialAvailable && !entry.runnerLocalRequired && !entry.runtimeSecretAvailable);
|
|
1066
|
+
if (missingRuntimeSecrets.length > 0) {
|
|
1067
|
+
const handles = missingRuntimeSecrets.map((entry) => entry.handle).join(', ');
|
|
1068
|
+
throw new Error(`Credential materialization for ${assignment.id} returned material_available without runtime secret material for: ${handles}`);
|
|
1069
|
+
}
|
|
1070
|
+
return { runtimeSecretEnv, runtimeSecretFiles, metadata };
|
|
1071
|
+
}
|
|
1072
|
+
async function writeRunCredentialSecretFile(runId, envName, secret) {
|
|
1073
|
+
const root = path.join(process.env['VIEWPORT_HOME'] ?? path.join(os.homedir(), '.viewport'), 'run-secrets', runId);
|
|
1074
|
+
await fs.promises.mkdir(root, { recursive: true, mode: 0o700 });
|
|
1075
|
+
const filePath = path.join(root, envName);
|
|
1076
|
+
await fs.promises.writeFile(filePath, secret, { mode: 0o600 });
|
|
1077
|
+
return filePath;
|
|
985
1078
|
}
|
|
986
1079
|
async function materializeCredential(options, assignment, handle) {
|
|
987
1080
|
if (!assignment.assignment_claim_token) {
|
|
@@ -999,6 +1092,9 @@ async function materializeCredential(options, assignment, handle) {
|
|
|
999
1092
|
return data;
|
|
1000
1093
|
}
|
|
1001
1094
|
function repositoryForCredentialMaterialization(assignment, handle) {
|
|
1095
|
+
const actionRepository = repositoryFromActionCredentialRef(assignment, handle);
|
|
1096
|
+
if (actionRepository)
|
|
1097
|
+
return { repository: actionRepository };
|
|
1002
1098
|
const checkoutEntries = [
|
|
1003
1099
|
...credentialEntriesFrom(pathValue(asRecord(assignment.execution_profile_snapshot), ['credentials', 'repo_checkout'])),
|
|
1004
1100
|
...credentialEntriesFrom(pathValue(asRecord(assignment.workflow_snapshot), ['credentials', 'repo_checkout'])),
|
|
@@ -1020,19 +1116,7 @@ function repositoryForCredentialMaterialization(assignment, handle) {
|
|
|
1020
1116
|
return allowed.length === 1 && allowed[0] ? { repository: allowed[0] } : {};
|
|
1021
1117
|
}
|
|
1022
1118
|
function allowedRepositoriesFromAssignment(assignment) {
|
|
1023
|
-
const candidates = [
|
|
1024
|
-
pathValue(assignment.workflow_authority_contract ?? {}, ['repos', 'allowed']),
|
|
1025
|
-
pathValue(recordChildValue(assignment.route_snapshot, 'workflow_authority_contract') ?? {}, [
|
|
1026
|
-
'repos',
|
|
1027
|
-
'allowed',
|
|
1028
|
-
]),
|
|
1029
|
-
pathValue(recordChildValue(assignment.execution_profile_snapshot, 'workflow_authority_contract') ?? {}, ['repos', 'allowed']),
|
|
1030
|
-
pathValue(recordChildValue(assignment.workflow_snapshot, 'workflow_authority_contract') ?? {}, [
|
|
1031
|
-
'repos',
|
|
1032
|
-
'allowed',
|
|
1033
|
-
]),
|
|
1034
|
-
pathValue(recordChildValue(assignment.runner_workspace_snapshot, 'workflow_authority_contract') ?? {}, ['repos', 'allowed']),
|
|
1035
|
-
];
|
|
1119
|
+
const candidates = assignmentWorkflowAuthorityContracts(assignment).map((contract) => pathValue(contract, ['repos', 'allowed']));
|
|
1036
1120
|
return [
|
|
1037
1121
|
...new Set(candidates
|
|
1038
1122
|
.flatMap((value) => (Array.isArray(value) ? value : []))
|
|
@@ -1061,20 +1145,47 @@ function decryptRunnerWrappedSecret(keyPair, wrapped) {
|
|
|
1061
1145
|
}, Buffer.from(ciphertext, 'base64')).toString('utf8');
|
|
1062
1146
|
}
|
|
1063
1147
|
function collectCredentialHandles(assignment) {
|
|
1064
|
-
const snapshots = [
|
|
1148
|
+
const snapshots = [
|
|
1149
|
+
assignmentTargetSnapshot(assignment),
|
|
1150
|
+
assignmentExecutionProfileSnapshot(assignment),
|
|
1151
|
+
assignmentWorkflowSnapshot(assignment),
|
|
1152
|
+
yamlSnapshotDocument(assignment),
|
|
1153
|
+
].filter(isRecord);
|
|
1065
1154
|
const handles = new Set();
|
|
1066
1155
|
for (const snapshot of snapshots) {
|
|
1067
1156
|
for (const handle of [
|
|
1068
1157
|
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'include'])),
|
|
1069
1158
|
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'repo_checkout'])),
|
|
1070
1159
|
...credentialRefsFrom(pathValue(snapshot, ['credentials', 'mcp_api'])),
|
|
1160
|
+
...credentialRefsFromCredentialMap(snapshot['credentials']),
|
|
1071
1161
|
...credentialRefsFrom(snapshot['credential_refs']),
|
|
1162
|
+
...actionCredentialRefs(snapshot['nodes']),
|
|
1072
1163
|
]) {
|
|
1073
1164
|
handles.add(handle);
|
|
1074
1165
|
}
|
|
1075
1166
|
}
|
|
1167
|
+
for (const contract of assignmentWorkflowAuthorityContracts(assignment)) {
|
|
1168
|
+
for (const handle of credentialRefsFrom(pathValue(asRecord(contract), ['credentials', 'provider_actions']))) {
|
|
1169
|
+
handles.add(handle);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1076
1172
|
return [...handles].sort();
|
|
1077
1173
|
}
|
|
1174
|
+
function credentialRefsFromCredentialMap(entries) {
|
|
1175
|
+
if (!isRecord(entries))
|
|
1176
|
+
return [];
|
|
1177
|
+
return Object.values(entries).flatMap((entry) => {
|
|
1178
|
+
if (!isRecord(entry))
|
|
1179
|
+
return [];
|
|
1180
|
+
const mode = stringField(entry, 'mode') ?? stringField(entry, 'storage_posture');
|
|
1181
|
+
if (mode && !['viewport_brokered', 'viewport_managed'].includes(mode))
|
|
1182
|
+
return [];
|
|
1183
|
+
const handle = stringField(entry, 'handle') ??
|
|
1184
|
+
stringField(entry, 'ref') ??
|
|
1185
|
+
stringField(entry, 'credential_ref');
|
|
1186
|
+
return handle ? [handle] : [];
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1078
1189
|
function credentialRefsFrom(entries) {
|
|
1079
1190
|
return credentialEntriesFrom(entries).flatMap((entry) => {
|
|
1080
1191
|
if (typeof entry === 'string' && entry.trim() !== '')
|
|
@@ -1092,6 +1203,59 @@ function credentialRefsFrom(entries) {
|
|
|
1092
1203
|
function credentialEntriesFrom(entries) {
|
|
1093
1204
|
return Array.isArray(entries) ? entries : [];
|
|
1094
1205
|
}
|
|
1206
|
+
function actionCredentialRefs(nodes) {
|
|
1207
|
+
if (!isRecord(nodes))
|
|
1208
|
+
return [];
|
|
1209
|
+
return Object.values(nodes).flatMap((node) => {
|
|
1210
|
+
if (!isRecord(node) || stringField(node, 'type') !== 'action')
|
|
1211
|
+
return [];
|
|
1212
|
+
const withValue = isRecord(node['with']) ? node['with'] : {};
|
|
1213
|
+
const credentialRef = stringField(withValue, 'credential_ref') ?? stringField(withValue, 'credentialRef');
|
|
1214
|
+
return credentialRef ? [credentialRef] : [];
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
function repositoryFromActionCredentialRef(assignment, handle) {
|
|
1218
|
+
const workflow = yamlSnapshotDocument(assignment);
|
|
1219
|
+
const nodes = isRecord(workflow) ? workflow['nodes'] : undefined;
|
|
1220
|
+
if (!isRecord(nodes))
|
|
1221
|
+
return null;
|
|
1222
|
+
for (const node of Object.values(nodes)) {
|
|
1223
|
+
if (!isRecord(node) || stringField(node, 'type') !== 'action')
|
|
1224
|
+
continue;
|
|
1225
|
+
const withValue = isRecord(node['with']) ? node['with'] : {};
|
|
1226
|
+
const credentialRef = stringField(withValue, 'credential_ref') ?? stringField(withValue, 'credentialRef');
|
|
1227
|
+
if (credentialRef !== handle)
|
|
1228
|
+
continue;
|
|
1229
|
+
const repository = stringField(withValue, 'repository') ?? stringField(withValue, 'repo');
|
|
1230
|
+
const rendered = renderCredentialTemplate(repository, assignment);
|
|
1231
|
+
if (rendered)
|
|
1232
|
+
return rendered;
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
function renderCredentialTemplate(value, assignment) {
|
|
1237
|
+
if (!value)
|
|
1238
|
+
return null;
|
|
1239
|
+
const inputs = isRecord(assignment.input_snapshot) ? assignment.input_snapshot : {};
|
|
1240
|
+
const rendered = value.replace(/\{\{\s*inputs\.([A-Za-z0-9_]+)\s*\}\}/g, (_match, key) => {
|
|
1241
|
+
const input = inputs[key];
|
|
1242
|
+
return typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean'
|
|
1243
|
+
? String(input)
|
|
1244
|
+
: '';
|
|
1245
|
+
});
|
|
1246
|
+
const trimmed = rendered.trim();
|
|
1247
|
+
return trimmed === '' || trimmed.includes('{{') ? null : trimmed;
|
|
1248
|
+
}
|
|
1249
|
+
function yamlSnapshotDocument(assignment) {
|
|
1250
|
+
if (!assignment.yaml_snapshot)
|
|
1251
|
+
return null;
|
|
1252
|
+
try {
|
|
1253
|
+
return YAML.parse(assignment.yaml_snapshot);
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1095
1259
|
function asRecord(value) {
|
|
1096
1260
|
return isRecord(value) ? value : {};
|
|
1097
1261
|
}
|
|
@@ -1125,6 +1289,13 @@ async function waitForApprovalAndResume(options, platformRunId, localRunId, assi
|
|
|
1125
1289
|
while (true) {
|
|
1126
1290
|
await heartbeat(options, 'online', 'busy');
|
|
1127
1291
|
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
1292
|
+
const commandRun = await applyBrokerActionCompletedCommands(options, platformRunId, assignment, localRunId, assignmentClaimToken);
|
|
1293
|
+
if (commandRun) {
|
|
1294
|
+
if (commandRun.status !== 'blocked')
|
|
1295
|
+
return commandRun;
|
|
1296
|
+
await delay(options.commandSleepSeconds * 1000);
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1128
1299
|
const approved = await approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId);
|
|
1129
1300
|
if (approved) {
|
|
1130
1301
|
const resumed = await resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken);
|
|
@@ -1156,9 +1327,39 @@ async function waitForApprovalAndResume(options, platformRunId, localRunId, assi
|
|
|
1156
1327
|
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
1157
1328
|
return run;
|
|
1158
1329
|
}
|
|
1330
|
+
const current = await readExistingLocalRun(localRunId);
|
|
1331
|
+
if (current) {
|
|
1332
|
+
await syncLocalRun(options, platformRunId, current, assignmentClaimToken);
|
|
1333
|
+
}
|
|
1159
1334
|
await delay(options.commandSleepSeconds * 1000);
|
|
1160
1335
|
}
|
|
1161
1336
|
}
|
|
1337
|
+
async function applyBrokerActionCompletedCommands(options, platformRunId, assignment, localRunId, assignmentClaimToken) {
|
|
1338
|
+
const localRun = await readExistingLocalRun(localRunId);
|
|
1339
|
+
const commands = (assignment.runtime_commands ?? []).filter((command) => command['type'] === 'workflow.action_completed' &&
|
|
1340
|
+
!brokerActionCommandAlreadyApplied(localRun, command));
|
|
1341
|
+
if (commands.length === 0)
|
|
1342
|
+
return null;
|
|
1343
|
+
const response = await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/runtime-commands`, { runtime_commands: commands });
|
|
1344
|
+
const run = readRun(response);
|
|
1345
|
+
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
1346
|
+
if (terminalRunStatus(run.status)) {
|
|
1347
|
+
clearRunCredentialMaterial(platformRunId);
|
|
1348
|
+
}
|
|
1349
|
+
return run;
|
|
1350
|
+
}
|
|
1351
|
+
function brokerActionCommandAlreadyApplied(run, command) {
|
|
1352
|
+
const nodeKey = stringValue(command['workflow_node_id']);
|
|
1353
|
+
if (!nodeKey)
|
|
1354
|
+
return false;
|
|
1355
|
+
const node = run?.nodes?.[nodeKey];
|
|
1356
|
+
if (!node || node.status !== 'completed')
|
|
1357
|
+
return false;
|
|
1358
|
+
const receipt = recordValue(node.metadata?.['executionReceipt']);
|
|
1359
|
+
const receiptKey = stringValue(receipt?.['receipt_key']);
|
|
1360
|
+
const commandReceiptKey = stringValue(command['receipt_key']);
|
|
1361
|
+
return Boolean(receiptKey && commandReceiptKey && receiptKey === commandReceiptKey);
|
|
1362
|
+
}
|
|
1162
1363
|
const alreadyResolvedApprovalRuns = new WeakSet();
|
|
1163
1364
|
async function approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId) {
|
|
1164
1365
|
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
@@ -1219,6 +1420,18 @@ function blockedNodeIds(run) {
|
|
|
1219
1420
|
.map((node) => node.id));
|
|
1220
1421
|
}
|
|
1221
1422
|
async function resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken) {
|
|
1423
|
+
const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
|
|
1424
|
+
const localRun = await readExistingLocalRun(localRunId);
|
|
1425
|
+
const materialAssignment = localRun
|
|
1426
|
+
? assignmentWithLocalRunSnapshot(assignment, localRun, assignmentClaimToken)
|
|
1427
|
+
: {
|
|
1428
|
+
...assignment,
|
|
1429
|
+
assignment_claim_token: assignment.assignment_claim_token ?? assignmentClaimToken ?? null,
|
|
1430
|
+
};
|
|
1431
|
+
const cachedMaterial = runCredentialMaterialCache.get(platformRunId);
|
|
1432
|
+
const material = cachedMaterial && hasRuntimeSecrets(cachedMaterial)
|
|
1433
|
+
? cachedMaterial
|
|
1434
|
+
: await materializeAndCacheRunCredentials(options, materialAssignment);
|
|
1222
1435
|
try {
|
|
1223
1436
|
await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/approvals/${encodeURIComponent(approved.node_key)}`, {
|
|
1224
1437
|
approved: managedApprovalApproved(approved),
|
|
@@ -1228,6 +1441,8 @@ async function resumeApprovedLocalRun(options, platformRunId, localRunId, approv
|
|
|
1228
1441
|
expectedActionDigest: approvalExpectedActionDigest(approved),
|
|
1229
1442
|
executionGrant: approvalExecutionGrant(approved),
|
|
1230
1443
|
feedback: approvalFeedback(approved),
|
|
1444
|
+
runtimeSecretEnv: material.runtimeSecretEnv,
|
|
1445
|
+
runtimeSecretFiles: material.runtimeSecretFiles,
|
|
1231
1446
|
});
|
|
1232
1447
|
}
|
|
1233
1448
|
catch (error) {
|
|
@@ -1244,14 +1459,75 @@ async function resumeApprovedLocalRun(options, platformRunId, localRunId, approv
|
|
|
1244
1459
|
await syncLocalRun(options, platformRunId, run, assignmentClaimToken);
|
|
1245
1460
|
}, progressSyncEveryMs(options.leaseSeconds));
|
|
1246
1461
|
await syncLocalRun(options, platformRunId, resumed, assignmentClaimToken);
|
|
1462
|
+
if (terminalRunStatus(resumed.status)) {
|
|
1463
|
+
clearRunCredentialMaterial(platformRunId);
|
|
1464
|
+
}
|
|
1247
1465
|
return resumed;
|
|
1248
1466
|
}
|
|
1467
|
+
function hasRuntimeSecrets(material) {
|
|
1468
|
+
return Object.keys(material.runtimeSecretEnv).length > 0;
|
|
1469
|
+
}
|
|
1470
|
+
function assignmentWithLocalRunSnapshot(assignment, localRun, assignmentClaimToken) {
|
|
1471
|
+
return {
|
|
1472
|
+
...assignment,
|
|
1473
|
+
assignment_claim_token: assignment.assignment_claim_token ?? assignmentClaimToken ?? null,
|
|
1474
|
+
yaml_snapshot: localRun.yamlSnapshot || assignment.yaml_snapshot,
|
|
1475
|
+
directory_path: localRun.directoryPath || assignment.directory_path,
|
|
1476
|
+
input_snapshot: localRun.inputs ?? assignment.input_snapshot,
|
|
1477
|
+
resource_manifest: localRun.resourceManifest ?? assignmentResourceManifest(assignment),
|
|
1478
|
+
workflow_authority_contract: localRun.workflowAuthorityContract ??
|
|
1479
|
+
assignmentWorkflowAuthorityContract(assignment) ??
|
|
1480
|
+
undefined,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function assignmentTargetSnapshot(assignment) {
|
|
1484
|
+
return assignment.target_snapshot ?? assignment.targetSnapshot ?? null;
|
|
1485
|
+
}
|
|
1486
|
+
function assignmentRouteSnapshot(assignment) {
|
|
1487
|
+
return assignment.route_snapshot ?? assignment.routeSnapshot ?? null;
|
|
1488
|
+
}
|
|
1489
|
+
function assignmentExecutionProfileSnapshot(assignment) {
|
|
1490
|
+
return assignment.execution_profile_snapshot ?? assignment.executionProfileSnapshot ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
function assignmentWorkflowSnapshot(assignment) {
|
|
1493
|
+
return assignment.workflow_snapshot ?? assignment.workflowSnapshot ?? null;
|
|
1494
|
+
}
|
|
1495
|
+
function assignmentRunnerWorkspaceSnapshot(assignment) {
|
|
1496
|
+
return assignment.runner_workspace_snapshot ?? assignment.runnerWorkspaceSnapshot ?? null;
|
|
1497
|
+
}
|
|
1498
|
+
function assignmentResourceManifest(assignment) {
|
|
1499
|
+
return assignment.resource_manifest ?? assignment.resourceManifest ?? null;
|
|
1500
|
+
}
|
|
1501
|
+
function assignmentContextReceiptsSnapshot(assignment) {
|
|
1502
|
+
return assignment.context_receipts_snapshot ?? assignment.contextReceiptsSnapshot ?? null;
|
|
1503
|
+
}
|
|
1504
|
+
function assignmentWorkflowAuthorityContract(assignment) {
|
|
1505
|
+
return assignmentWorkflowAuthorityContracts(assignment)[0] ?? null;
|
|
1506
|
+
}
|
|
1507
|
+
function assignmentWorkflowAuthorityContracts(assignment) {
|
|
1508
|
+
return [
|
|
1509
|
+
assignment.workflow_authority_contract ?? null,
|
|
1510
|
+
assignment.workflowAuthorityContract ?? null,
|
|
1511
|
+
recordChildValue(assignmentTargetSnapshot(assignment), 'workflow_authority_contract'),
|
|
1512
|
+
recordChildValue(assignmentTargetSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1513
|
+
recordChildValue(assignmentRouteSnapshot(assignment), 'workflow_authority_contract'),
|
|
1514
|
+
recordChildValue(assignmentRouteSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1515
|
+
recordChildValue(assignmentExecutionProfileSnapshot(assignment), 'workflow_authority_contract'),
|
|
1516
|
+
recordChildValue(assignmentExecutionProfileSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1517
|
+
recordChildValue(assignmentWorkflowSnapshot(assignment), 'workflow_authority_contract'),
|
|
1518
|
+
recordChildValue(assignmentWorkflowSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1519
|
+
recordChildValue(assignmentRunnerWorkspaceSnapshot(assignment), 'workflow_authority_contract'),
|
|
1520
|
+
recordChildValue(assignmentRunnerWorkspaceSnapshot(assignment), 'workflowAuthorityContract'),
|
|
1521
|
+
recordChildValue(recordChildValue(asRecord(assignment.input_snapshot), 'viewport'), 'workflow_authority_contract'),
|
|
1522
|
+
recordChildValue(recordChildValue(asRecord(assignment.input_snapshot), 'viewport'), 'workflowAuthorityContract'),
|
|
1523
|
+
].filter(isRecord);
|
|
1524
|
+
}
|
|
1249
1525
|
function isAlreadyResolvedApprovalError(error) {
|
|
1250
1526
|
const message = error instanceof Error ? error.message : String(error);
|
|
1251
1527
|
return message.includes('Workflow node is not awaiting approval');
|
|
1252
1528
|
}
|
|
1253
1529
|
function isResolvedManagedGateNode(node) {
|
|
1254
|
-
if (!['approval', 'gate', 'plan'
|
|
1530
|
+
if (!['approval', 'gate', 'plan'].includes(String(node.type ?? '')))
|
|
1255
1531
|
return false;
|
|
1256
1532
|
if (node.status === 'completed')
|
|
1257
1533
|
return true;
|
|
@@ -1263,11 +1539,7 @@ function isResolvedManagedGateNode(node) {
|
|
|
1263
1539
|
'approved' in approval) {
|
|
1264
1540
|
return true;
|
|
1265
1541
|
}
|
|
1266
|
-
|
|
1267
|
-
return false;
|
|
1268
|
-
return (!!approval &&
|
|
1269
|
-
typeof approval === 'object' &&
|
|
1270
|
-
approval.approved === true);
|
|
1542
|
+
return false;
|
|
1271
1543
|
}
|
|
1272
1544
|
function managedApprovalApproved(node) {
|
|
1273
1545
|
const approval = node.metadata?.['approval'];
|
|
@@ -1291,11 +1563,23 @@ function managedApprovalDecision(node) {
|
|
|
1291
1563
|
}
|
|
1292
1564
|
async function getAssignment(options, platformRunId, assignmentClaimToken) {
|
|
1293
1565
|
const body = await platformJson(options, 'GET', `workflow-runs/${encodeURIComponent(platformRunId)}`, undefined, assignmentClaimToken);
|
|
1294
|
-
return
|
|
1566
|
+
return assignmentFrom(body);
|
|
1295
1567
|
}
|
|
1296
1568
|
async function syncLocalRun(options, platformRunId, run, assignmentClaimToken) {
|
|
1297
1569
|
const body = await platformJson(options, 'PATCH', `workflow-runs/${encodeURIComponent(platformRunId)}/sync`, localRunToSyncPayload(run, { includeApprovalDecisions: false }), assignmentClaimToken);
|
|
1298
|
-
return
|
|
1570
|
+
return assignmentFrom(body);
|
|
1571
|
+
}
|
|
1572
|
+
function assignmentFrom(body) {
|
|
1573
|
+
const data = dataFrom(body);
|
|
1574
|
+
if (!isRecord(data))
|
|
1575
|
+
return data;
|
|
1576
|
+
if (isRecord(body) && Array.isArray(body['runtime_commands'])) {
|
|
1577
|
+
return {
|
|
1578
|
+
...data,
|
|
1579
|
+
runtime_commands: body['runtime_commands'],
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
return data;
|
|
1299
1583
|
}
|
|
1300
1584
|
async function ensureDirectory(directoryPath) {
|
|
1301
1585
|
const resolvedPath = path.resolve(directoryPath);
|