@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.
Files changed (312) hide show
  1. package/README.md +34 -0
  2. package/dist/adapters/claude.d.ts +7 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +71 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex-event-normalizers.d.ts.map +1 -1
  7. package/dist/adapters/codex-event-normalizers.js +55 -2
  8. package/dist/adapters/codex-event-normalizers.js.map +1 -1
  9. package/dist/adapters/codex.d.ts +2 -1
  10. package/dist/adapters/codex.d.ts.map +1 -1
  11. package/dist/adapters/codex.js +35 -0
  12. package/dist/adapters/codex.js.map +1 -1
  13. package/dist/adapters/gemini-cli.d.ts +2 -1
  14. package/dist/adapters/gemini-cli.d.ts.map +1 -1
  15. package/dist/adapters/gemini-cli.js +24 -0
  16. package/dist/adapters/gemini-cli.js.map +1 -1
  17. package/dist/adapters/pty.d.ts +2 -1
  18. package/dist/adapters/pty.d.ts.map +1 -1
  19. package/dist/adapters/pty.js +26 -1
  20. package/dist/adapters/pty.js.map +1 -1
  21. package/dist/cli/args.d.ts.map +1 -1
  22. package/dist/cli/args.js +7 -3
  23. package/dist/cli/args.js.map +1 -1
  24. package/dist/cli/commands.d.ts +3 -0
  25. package/dist/cli/commands.d.ts.map +1 -1
  26. package/dist/cli/commands.js +3 -0
  27. package/dist/cli/commands.js.map +1 -1
  28. package/dist/cli/context-command.js +2 -0
  29. package/dist/cli/context-command.js.map +1 -1
  30. package/dist/cli/context-sync-target.d.ts +2 -0
  31. package/dist/cli/context-sync-target.d.ts.map +1 -1
  32. package/dist/cli/context-sync-target.js +4 -0
  33. package/dist/cli/context-sync-target.js.map +1 -1
  34. package/dist/cli/daemon-client.d.ts +1 -0
  35. package/dist/cli/daemon-client.d.ts.map +1 -1
  36. package/dist/cli/daemon-client.js +13 -1
  37. package/dist/cli/daemon-client.js.map +1 -1
  38. package/dist/cli/global-flags.d.ts.map +1 -1
  39. package/dist/cli/global-flags.js +3 -2
  40. package/dist/cli/global-flags.js.map +1 -1
  41. package/dist/cli/hook-command.d.ts +7 -0
  42. package/dist/cli/hook-command.d.ts.map +1 -1
  43. package/dist/cli/hook-command.js +35 -20
  44. package/dist/cli/hook-command.js.map +1 -1
  45. package/dist/cli/install-command.d.ts +5 -1
  46. package/dist/cli/install-command.d.ts.map +1 -1
  47. package/dist/cli/install-command.js +37 -28
  48. package/dist/cli/install-command.js.map +1 -1
  49. package/dist/cli/lifecycle-commands.d.ts.map +1 -1
  50. package/dist/cli/lifecycle-commands.js +5 -2
  51. package/dist/cli/lifecycle-commands.js.map +1 -1
  52. package/dist/cli/lifecycle-pair-command.d.ts.map +1 -1
  53. package/dist/cli/lifecycle-pair-command.js +51 -6
  54. package/dist/cli/lifecycle-pair-command.js.map +1 -1
  55. package/dist/cli/lifecycle-pair-server.d.ts +2 -0
  56. package/dist/cli/lifecycle-pair-server.d.ts.map +1 -1
  57. package/dist/cli/lifecycle-pair-server.js.map +1 -1
  58. package/dist/cli/lifecycle-status-command.d.ts.map +1 -1
  59. package/dist/cli/lifecycle-status-command.js +43 -0
  60. package/dist/cli/lifecycle-status-command.js.map +1 -1
  61. package/dist/cli/setup-command.d.ts.map +1 -1
  62. package/dist/cli/setup-command.js +8 -8
  63. package/dist/cli/setup-command.js.map +1 -1
  64. package/dist/cli/team-resource-command.d.ts +2 -0
  65. package/dist/cli/team-resource-command.d.ts.map +1 -0
  66. package/dist/cli/team-resource-command.js +364 -0
  67. package/dist/cli/team-resource-command.js.map +1 -0
  68. package/dist/cli/watch-command.d.ts +3 -0
  69. package/dist/cli/watch-command.d.ts.map +1 -0
  70. package/dist/cli/watch-command.js +42 -0
  71. package/dist/cli/watch-command.js.map +1 -0
  72. package/dist/cli/worker-command.d.ts +3 -0
  73. package/dist/cli/worker-command.d.ts.map +1 -0
  74. package/dist/cli/worker-command.js +132 -0
  75. package/dist/cli/worker-command.js.map +1 -0
  76. package/dist/cli/worker-profile.d.ts +65 -0
  77. package/dist/cli/worker-profile.d.ts.map +1 -0
  78. package/dist/cli/worker-profile.js +228 -0
  79. package/dist/cli/worker-profile.js.map +1 -0
  80. package/dist/cli/worker-runtime.d.ts +19 -0
  81. package/dist/cli/worker-runtime.d.ts.map +1 -0
  82. package/dist/cli/worker-runtime.js +606 -0
  83. package/dist/cli/worker-runtime.js.map +1 -0
  84. package/dist/cli/workflow-managed-worker-format.d.ts +4 -3
  85. package/dist/cli/workflow-managed-worker-format.d.ts.map +1 -1
  86. package/dist/cli/workflow-managed-worker-format.js +71 -10
  87. package/dist/cli/workflow-managed-worker-format.js.map +1 -1
  88. package/dist/cli/workflow-managed-worker-types.d.ts +17 -0
  89. package/dist/cli/workflow-managed-worker-types.d.ts.map +1 -1
  90. package/dist/cli/workflow-managed-worker.d.ts.map +1 -1
  91. package/dist/cli/workflow-managed-worker.js +568 -39
  92. package/dist/cli/workflow-managed-worker.js.map +1 -1
  93. package/dist/config-resolution/provider-defaults.d.ts.map +1 -1
  94. package/dist/config-resolution/provider-defaults.js +4 -0
  95. package/dist/config-resolution/provider-defaults.js.map +1 -1
  96. package/dist/config-resolution/schema.d.ts +2 -0
  97. package/dist/config-resolution/schema.d.ts.map +1 -1
  98. package/dist/config-resolution/schema.js +2 -0
  99. package/dist/config-resolution/schema.js.map +1 -1
  100. package/dist/config-resolution/types.d.ts +1 -1
  101. package/dist/config-resolution/types.d.ts.map +1 -1
  102. package/dist/context/local-edge-sync.d.ts +2 -0
  103. package/dist/context/local-edge-sync.d.ts.map +1 -1
  104. package/dist/context/local-edge-sync.js +2 -0
  105. package/dist/context/local-edge-sync.js.map +1 -1
  106. package/dist/context-providers/confluence-provider.d.ts +3 -0
  107. package/dist/context-providers/confluence-provider.d.ts.map +1 -0
  108. package/dist/context-providers/confluence-provider.js +170 -0
  109. package/dist/context-providers/confluence-provider.js.map +1 -0
  110. package/dist/context-providers/notion-provider.d.ts +3 -0
  111. package/dist/context-providers/notion-provider.d.ts.map +1 -0
  112. package/dist/context-providers/notion-provider.js +157 -0
  113. package/dist/context-providers/notion-provider.js.map +1 -0
  114. package/dist/context-providers/registry.d.ts.map +1 -1
  115. package/dist/context-providers/registry.js +9 -1
  116. package/dist/context-providers/registry.js.map +1 -1
  117. package/dist/context-providers/types.d.ts +18 -0
  118. package/dist/context-providers/types.d.ts.map +1 -1
  119. package/dist/core/config-schema.d.ts +96 -3
  120. package/dist/core/config-schema.d.ts.map +1 -1
  121. package/dist/core/config-schema.js +53 -0
  122. package/dist/core/config-schema.js.map +1 -1
  123. package/dist/core/config.d.ts +88 -0
  124. package/dist/core/config.d.ts.map +1 -1
  125. package/dist/core/config.js +6 -0
  126. package/dist/core/config.js.map +1 -1
  127. package/dist/core/daemon.d.ts +5 -1
  128. package/dist/core/daemon.d.ts.map +1 -1
  129. package/dist/core/daemon.js +8 -0
  130. package/dist/core/daemon.js.map +1 -1
  131. package/dist/core/interfaces.d.ts +137 -1
  132. package/dist/core/interfaces.d.ts.map +1 -1
  133. package/dist/core/permission-coordinator.d.ts.map +1 -1
  134. package/dist/core/permission-coordinator.js +4 -2
  135. package/dist/core/permission-coordinator.js.map +1 -1
  136. package/dist/core/session-manager.d.ts.map +1 -1
  137. package/dist/core/session-manager.js +47 -20
  138. package/dist/core/session-manager.js.map +1 -1
  139. package/dist/core/types.d.ts +31 -0
  140. package/dist/core/types.d.ts.map +1 -1
  141. package/dist/core/types.js.map +1 -1
  142. package/dist/discovery/watcher.d.ts.map +1 -1
  143. package/dist/discovery/watcher.js +51 -8
  144. package/dist/discovery/watcher.js.map +1 -1
  145. package/dist/index.d.ts +2 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +12 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/plugins/loader.d.ts.map +1 -1
  150. package/dist/plugins/loader.js +10 -1
  151. package/dist/plugins/loader.js.map +1 -1
  152. package/dist/security/child-env.d.ts +3 -0
  153. package/dist/security/child-env.d.ts.map +1 -0
  154. package/dist/security/child-env.js +44 -0
  155. package/dist/security/child-env.js.map +1 -0
  156. package/dist/server/http-request-schemas.d.ts +6 -0
  157. package/dist/server/http-request-schemas.d.ts.map +1 -1
  158. package/dist/server/http-request-schemas.js +15 -0
  159. package/dist/server/http-request-schemas.js.map +1 -1
  160. package/dist/server/http-server.d.ts.map +1 -1
  161. package/dist/server/http-server.js +4 -2
  162. package/dist/server/http-server.js.map +1 -1
  163. package/dist/workflows/action-adapters.d.ts.map +1 -1
  164. package/dist/workflows/action-adapters.js +42 -0
  165. package/dist/workflows/action-adapters.js.map +1 -1
  166. package/dist/workflows/action-provider-adapters.d.ts.map +1 -1
  167. package/dist/workflows/action-provider-adapters.js +156 -13
  168. package/dist/workflows/action-provider-adapters.js.map +1 -1
  169. package/dist/workflows/action-provider-utils.d.ts +6 -0
  170. package/dist/workflows/action-provider-utils.d.ts.map +1 -1
  171. package/dist/workflows/action-provider-utils.js +21 -0
  172. package/dist/workflows/action-provider-utils.js.map +1 -1
  173. package/dist/workflows/approval-actor.d.ts +6 -0
  174. package/dist/workflows/approval-actor.d.ts.map +1 -0
  175. package/dist/workflows/approval-actor.js +27 -0
  176. package/dist/workflows/approval-actor.js.map +1 -0
  177. package/dist/workflows/approval-on-reject.js +1 -0
  178. package/dist/workflows/approval-on-reject.js.map +1 -1
  179. package/dist/workflows/checkout-node.d.ts +21 -0
  180. package/dist/workflows/checkout-node.d.ts.map +1 -0
  181. package/dist/workflows/checkout-node.js +181 -0
  182. package/dist/workflows/checkout-node.js.map +1 -0
  183. package/dist/workflows/context-node-resolver.d.ts +48 -1
  184. package/dist/workflows/context-node-resolver.d.ts.map +1 -1
  185. package/dist/workflows/context-node-resolver.js +405 -8
  186. package/dist/workflows/context-node-resolver.js.map +1 -1
  187. package/dist/workflows/context-update-targets.d.ts +12 -0
  188. package/dist/workflows/context-update-targets.d.ts.map +1 -0
  189. package/dist/workflows/context-update-targets.js +52 -0
  190. package/dist/workflows/context-update-targets.js.map +1 -0
  191. package/dist/workflows/daemon-session.d.ts +11 -0
  192. package/dist/workflows/daemon-session.d.ts.map +1 -1
  193. package/dist/workflows/daemon-session.js +168 -6
  194. package/dist/workflows/daemon-session.js.map +1 -1
  195. package/dist/workflows/event-types.d.ts +1 -1
  196. package/dist/workflows/event-types.d.ts.map +1 -1
  197. package/dist/workflows/expression.d.ts +4 -0
  198. package/dist/workflows/expression.d.ts.map +1 -1
  199. package/dist/workflows/expression.js +1 -1
  200. package/dist/workflows/expression.js.map +1 -1
  201. package/dist/workflows/git-publish-node.d.ts +23 -0
  202. package/dist/workflows/git-publish-node.d.ts.map +1 -0
  203. package/dist/workflows/git-publish-node.js +237 -0
  204. package/dist/workflows/git-publish-node.js.map +1 -0
  205. package/dist/workflows/inline-agent-types.d.ts +7 -0
  206. package/dist/workflows/inline-agent-types.d.ts.map +1 -1
  207. package/dist/workflows/inline-agents.d.ts +4 -1
  208. package/dist/workflows/inline-agents.d.ts.map +1 -1
  209. package/dist/workflows/inline-agents.js +24 -1
  210. package/dist/workflows/inline-agents.js.map +1 -1
  211. package/dist/workflows/loop-executor.js +1 -0
  212. package/dist/workflows/loop-executor.js.map +1 -1
  213. package/dist/workflows/node-executor.d.ts +7 -0
  214. package/dist/workflows/node-executor.d.ts.map +1 -1
  215. package/dist/workflows/node-executor.js +135 -7
  216. package/dist/workflows/node-executor.js.map +1 -1
  217. package/dist/workflows/node-registry.d.ts.map +1 -1
  218. package/dist/workflows/node-registry.js +553 -4
  219. package/dist/workflows/node-registry.js.map +1 -1
  220. package/dist/workflows/parser.d.ts.map +1 -1
  221. package/dist/workflows/parser.js +70 -5
  222. package/dist/workflows/parser.js.map +1 -1
  223. package/dist/workflows/platform-command-applier.d.ts.map +1 -1
  224. package/dist/workflows/platform-command-applier.js +5 -14
  225. package/dist/workflows/platform-command-applier.js.map +1 -1
  226. package/dist/workflows/platform-context-client.d.ts +74 -0
  227. package/dist/workflows/platform-context-client.d.ts.map +1 -0
  228. package/dist/workflows/platform-context-client.js +228 -0
  229. package/dist/workflows/platform-context-client.js.map +1 -0
  230. package/dist/workflows/platform-sync-format.d.ts +1 -1
  231. package/dist/workflows/platform-sync-format.d.ts.map +1 -1
  232. package/dist/workflows/platform-sync-format.js +3 -1
  233. package/dist/workflows/platform-sync-format.js.map +1 -1
  234. package/dist/workflows/platform-sync-payload.d.ts +1 -0
  235. package/dist/workflows/platform-sync-payload.d.ts.map +1 -1
  236. package/dist/workflows/platform-sync-payload.js +160 -8
  237. package/dist/workflows/platform-sync-payload.js.map +1 -1
  238. package/dist/workflows/platform-sync.d.ts.map +1 -1
  239. package/dist/workflows/platform-sync.js +10 -1
  240. package/dist/workflows/platform-sync.js.map +1 -1
  241. package/dist/workflows/plugin-loader.d.ts.map +1 -1
  242. package/dist/workflows/plugin-loader.js +3 -0
  243. package/dist/workflows/plugin-loader.js.map +1 -1
  244. package/dist/workflows/preflight.d.ts.map +1 -1
  245. package/dist/workflows/preflight.js +9 -1
  246. package/dist/workflows/preflight.js.map +1 -1
  247. package/dist/workflows/prompt-output.d.ts +6 -2
  248. package/dist/workflows/prompt-output.d.ts.map +1 -1
  249. package/dist/workflows/prompt-output.js +8 -2
  250. package/dist/workflows/prompt-output.js.map +1 -1
  251. package/dist/workflows/run-preparation.d.ts +24 -0
  252. package/dist/workflows/run-preparation.d.ts.map +1 -0
  253. package/dist/workflows/run-preparation.js +258 -0
  254. package/dist/workflows/run-preparation.js.map +1 -0
  255. package/dist/workflows/run-types.d.ts +11 -0
  256. package/dist/workflows/run-types.d.ts.map +1 -1
  257. package/dist/workflows/runner-scheduler.d.ts +2 -0
  258. package/dist/workflows/runner-scheduler.d.ts.map +1 -1
  259. package/dist/workflows/runner-scheduler.js +35 -4
  260. package/dist/workflows/runner-scheduler.js.map +1 -1
  261. package/dist/workflows/runner-shared.d.ts.map +1 -1
  262. package/dist/workflows/runner-shared.js +9 -0
  263. package/dist/workflows/runner-shared.js.map +1 -1
  264. package/dist/workflows/runner.d.ts +2 -0
  265. package/dist/workflows/runner.d.ts.map +1 -1
  266. package/dist/workflows/runner.js +171 -6
  267. package/dist/workflows/runner.js.map +1 -1
  268. package/dist/workflows/runtime-helpers.d.ts +20 -1
  269. package/dist/workflows/runtime-helpers.d.ts.map +1 -1
  270. package/dist/workflows/runtime-helpers.js +98 -3
  271. package/dist/workflows/runtime-helpers.js.map +1 -1
  272. package/dist/workflows/session-output.d.ts +9 -0
  273. package/dist/workflows/session-output.d.ts.map +1 -1
  274. package/dist/workflows/session-output.js +156 -0
  275. package/dist/workflows/session-output.js.map +1 -1
  276. package/dist/workflows/session-policy.d.ts +30 -0
  277. package/dist/workflows/session-policy.d.ts.map +1 -0
  278. package/dist/workflows/session-policy.js +45 -0
  279. package/dist/workflows/session-policy.js.map +1 -0
  280. package/dist/workflows/structured-outputs.d.ts +5 -0
  281. package/dist/workflows/structured-outputs.d.ts.map +1 -1
  282. package/dist/workflows/structured-outputs.js +82 -3
  283. package/dist/workflows/structured-outputs.js.map +1 -1
  284. package/dist/workflows/subflow-executor.js +5 -1
  285. package/dist/workflows/subflow-executor.js.map +1 -1
  286. package/dist/workflows/types.d.ts +107 -4
  287. package/dist/workflows/types.d.ts.map +1 -1
  288. package/dist/workflows/workflow-authority-contract.d.ts +25 -0
  289. package/dist/workflows/workflow-authority-contract.d.ts.map +1 -0
  290. package/dist/workflows/workflow-authority-contract.js +366 -0
  291. package/dist/workflows/workflow-authority-contract.js.map +1 -0
  292. package/dist/workflows/workflow-executor-schema.d.ts +1 -1
  293. package/dist/workflows/workflow-production-schema.d.ts +62 -10
  294. package/dist/workflows/workflow-production-schema.d.ts.map +1 -1
  295. package/dist/workflows/workflow-production-schema.js +63 -4
  296. package/dist/workflows/workflow-production-schema.js.map +1 -1
  297. package/dist/workflows/workflow-production-types.d.ts +42 -3
  298. package/dist/workflows/workflow-production-types.d.ts.map +1 -1
  299. package/dist/workflows/workflow-schema-common.d.ts +248 -2
  300. package/dist/workflows/workflow-schema-common.d.ts.map +1 -1
  301. package/dist/workflows/workflow-schema-common.js +58 -2
  302. package/dist/workflows/workflow-schema-common.js.map +1 -1
  303. package/dist/workflows/workflow-schema.d.ts +1966 -16
  304. package/dist/workflows/workflow-schema.d.ts.map +1 -1
  305. package/dist/workflows/workflow-schema.js +97 -3
  306. package/dist/workflows/workflow-schema.js.map +1 -1
  307. package/docs/releasing.md +4 -1
  308. package/node_modules/@viewportai/context-engine/schemas/context_event_v1.schema.json +2 -0
  309. package/node_modules/@viewportai/context-engine/src/repo/candidates.js +2 -0
  310. package/node_modules/@viewportai/context-engine/src/repo/events.js +15 -1
  311. package/node_modules/@viewportai/context-engine/src/repo/vault.js +18 -2
  312. 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 { approvalActor, approvalExecutionGrant, approvalExpectedActionDigest, approvalMessage, capabilityPayload, dataFrom, localRunToSyncPayload, progressSyncEveryMs, readRun, } from './workflow-managed-worker-format.js';
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
- stats.completed += resumed.status === 'completed' ? 1 : 0;
72
- stats.failed += resumed.status === 'failed' || resumed.status === 'canceled' ? 1 : 0;
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 server = getFlag('server') ?? process.env['VIEWPORT_SERVER_URL'] ?? process.env['VPD_SERVER_URL'];
144
- const workspaceId = getFlag('workspace') ?? getFlag('resource') ?? process.env['VIEWPORT_WORKSPACE_ID'];
145
- const executorId = getFlag('executor') ?? process.env['VIEWPORT_MANAGED_EXECUTOR_ID'];
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('Usage: vpd workflow worker --server <url> --workspace <id> --executor <id> --credential <token> [--workdir <path>] [--agent-command <command>] [--action-command <command>] [--provider-actions] [--once]');
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') ?? process.env['VIEWPORT_MANAGED_EXECUTOR_ACCESS_MODE']),
158
- runnerProfile: getFlag('runner-profile') ?? process.env['VIEWPORT_MANAGED_EXECUTOR_PROFILE'] ?? undefined,
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
- agents: listFlagValue(getFlag('agents')),
169
- models: listFlagValue(getFlag('models')),
170
- integrations: listFlagValue(getFlag('integrations')),
171
- secrets: listFlagValue(getFlag('secrets')),
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
- transport: { mode: options.accessMode },
183
- execution: { kind: 'customer-managed' },
184
- version: process.env['npm_package_version'] ?? null,
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 'relay';
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 assignmentInputs(assignment) {
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 = assignment.nodes?.find(isApprovedManagedGateNode);
1007
+ const approved = await approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId);
558
1008
  if (approved) {
559
- return resumeApprovedLocalRun(options, platformRunId, localRunId, approved, assignmentClaimToken);
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
- async function approvedNodeForAssignment(options, platformRunId, assignmentClaimToken) {
1041
+ const alreadyResolvedApprovalRuns = new WeakSet();
1042
+ async function approvedNodeForAssignment(options, platformRunId, assignmentClaimToken, localRunId) {
574
1043
  const assignment = await getAssignment(options, platformRunId, assignmentClaimToken);
575
- return assignment.nodes?.find(isApprovedManagedGateNode) ?? null;
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
- await daemonJson('POST', `/api/workflows/runs/${encodeURIComponent(localRunId)}/approvals/${encodeURIComponent(approved.node_key)}`, {
579
- approved: true,
580
- message: approvalMessage(approved),
581
- actor: approvalActor(approved),
582
- expectedActionDigest: approvalExpectedActionDigest(approved),
583
- executionGrant: approvalExecutionGrant(approved),
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 isApprovedManagedGateNode(node) {
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) {