@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.
Files changed (78) hide show
  1. package/dist/adapters/claude.js +6 -0
  2. package/dist/adapters/claude.js.map +1 -1
  3. package/dist/cli/worker-runtime.d.ts.map +1 -1
  4. package/dist/cli/worker-runtime.js +123 -0
  5. package/dist/cli/worker-runtime.js.map +1 -1
  6. package/dist/cli/workflow-managed-worker-types.d.ts +10 -0
  7. package/dist/cli/workflow-managed-worker-types.d.ts.map +1 -1
  8. package/dist/cli/workflow-managed-worker.d.ts.map +1 -1
  9. package/dist/cli/workflow-managed-worker.js +333 -49
  10. package/dist/cli/workflow-managed-worker.js.map +1 -1
  11. package/dist/server/http-request-schemas.d.ts +4 -0
  12. package/dist/server/http-request-schemas.d.ts.map +1 -1
  13. package/dist/server/http-request-schemas.js +10 -0
  14. package/dist/server/http-request-schemas.js.map +1 -1
  15. package/dist/server/http-server.d.ts.map +1 -1
  16. package/dist/server/http-server.js +17 -0
  17. package/dist/server/http-server.js.map +1 -1
  18. package/dist/workflows/action-adapters.d.ts +2 -0
  19. package/dist/workflows/action-adapters.d.ts.map +1 -1
  20. package/dist/workflows/action-adapters.js +2 -0
  21. package/dist/workflows/action-adapters.js.map +1 -1
  22. package/dist/workflows/action-provider-adapters.d.ts +6 -2
  23. package/dist/workflows/action-provider-adapters.d.ts.map +1 -1
  24. package/dist/workflows/action-provider-adapters.js +49 -3
  25. package/dist/workflows/action-provider-adapters.js.map +1 -1
  26. package/dist/workflows/action-provider-utils.d.ts +4 -1
  27. package/dist/workflows/action-provider-utils.d.ts.map +1 -1
  28. package/dist/workflows/action-provider-utils.js +22 -2
  29. package/dist/workflows/action-provider-utils.js.map +1 -1
  30. package/dist/workflows/approval-on-reject.js +1 -0
  31. package/dist/workflows/approval-on-reject.js.map +1 -1
  32. package/dist/workflows/daemon-session.js +1 -0
  33. package/dist/workflows/daemon-session.js.map +1 -1
  34. package/dist/workflows/expression.d.ts +3 -0
  35. package/dist/workflows/expression.d.ts.map +1 -1
  36. package/dist/workflows/expression.js +18 -1
  37. package/dist/workflows/expression.js.map +1 -1
  38. package/dist/workflows/inline-agents.d.ts.map +1 -1
  39. package/dist/workflows/inline-agents.js +1 -5
  40. package/dist/workflows/inline-agents.js.map +1 -1
  41. package/dist/workflows/loop-executor.js +1 -0
  42. package/dist/workflows/loop-executor.js.map +1 -1
  43. package/dist/workflows/node-executor.d.ts +6 -0
  44. package/dist/workflows/node-executor.d.ts.map +1 -1
  45. package/dist/workflows/node-executor.js +24 -2
  46. package/dist/workflows/node-executor.js.map +1 -1
  47. package/dist/workflows/node-registry.d.ts.map +1 -1
  48. package/dist/workflows/node-registry.js +25 -6
  49. package/dist/workflows/node-registry.js.map +1 -1
  50. package/dist/workflows/platform-command-applier.d.ts +3 -1
  51. package/dist/workflows/platform-command-applier.d.ts.map +1 -1
  52. package/dist/workflows/platform-command-applier.js +77 -1
  53. package/dist/workflows/platform-command-applier.js.map +1 -1
  54. package/dist/workflows/platform-runtime-command.d.ts +18 -1
  55. package/dist/workflows/platform-runtime-command.d.ts.map +1 -1
  56. package/dist/workflows/platform-runtime-command.js +36 -3
  57. package/dist/workflows/platform-runtime-command.js.map +1 -1
  58. package/dist/workflows/run-types.d.ts +9 -0
  59. package/dist/workflows/run-types.d.ts.map +1 -1
  60. package/dist/workflows/runner-scheduler.d.ts +1 -0
  61. package/dist/workflows/runner-scheduler.d.ts.map +1 -1
  62. package/dist/workflows/runner-scheduler.js +1 -0
  63. package/dist/workflows/runner-scheduler.js.map +1 -1
  64. package/dist/workflows/runner.d.ts +7 -0
  65. package/dist/workflows/runner.d.ts.map +1 -1
  66. package/dist/workflows/runner.js +113 -7
  67. package/dist/workflows/runner.js.map +1 -1
  68. package/dist/workflows/subflow-executor.js +1 -1
  69. package/dist/workflows/subflow-executor.js.map +1 -1
  70. package/dist/workflows/workflow-production-schema.d.ts +12 -0
  71. package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
  72. package/dist/workflows/workflow-production-schema.js +9 -0
  73. package/dist/workflows/workflow-production-schema.js.map +1 -1
  74. package/dist/workflows/workflow-production-types.d.ts +6 -0
  75. package/dist/workflows/workflow-production-types.d.ts.map +1 -1
  76. package/dist/workflows/workflow-schema.d.ts +12 -0
  77. package/dist/workflows/workflow-schema.d.ts.map +1 -1
  78. 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: recordValue(options.runnerPosture?.['execution']) ?? { kind: 'customer-managed' },
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 dataFrom(body);
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 (stringField(response ?? null, 'htmlUrl') ??
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
- numberField(response ?? null, 'number')?.toString());
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
- return pollLocalRun(existingRun.id, async (run) => {
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 materializeRunCredentials(options, assignment);
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.metadata),
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.resource_manifest ?? undefined,
917
- workflowAuthorityContract: assignment.workflow_authority_contract ??
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
- return pollLocalRun(runId, async (run) => {
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, credentialMetadata = []) {
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
- route: assignment.route_snapshot ?? null,
947
- executionProfile: assignment.execution_profile_snapshot ?? null,
948
- workflow: assignment.workflow_snapshot ?? null,
949
- runnerWorkspace: assignment.runner_workspace_snapshot ?? null,
950
- contextReceipts: assignment.context_receipts_snapshot ?? null,
951
- credentials: credentialMetadata,
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
- runtimeSecretEnv[envName] = decryptRunnerWrappedSecret(options.runnerKeyPair, wrappedSecret);
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
- return { runtimeSecretEnv, metadata };
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 = [assignment.execution_profile_snapshot, assignment.workflow_snapshot].filter(isRecord);
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', 'action'].includes(String(node.type ?? '')))
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
- if (node.type !== 'action' || node.status !== 'queued')
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 dataFrom(body);
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 dataFrom(body);
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);