@pixelbyte-software/pixcode 1.51.1 → 1.51.3

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 (320) hide show
  1. package/CODE_OF_CONDUCT.md +41 -41
  2. package/CONTRIBUTING.md +155 -155
  3. package/LICENSE +718 -718
  4. package/README.de.md +169 -169
  5. package/README.ja.md +167 -167
  6. package/README.ko.md +167 -167
  7. package/README.md +419 -419
  8. package/README.ru.md +169 -169
  9. package/README.tr.md +298 -298
  10. package/README.zh-CN.md +167 -167
  11. package/SECURITY.md +46 -46
  12. package/dist/api-automation.html +110 -110
  13. package/dist/api-docs.html +548 -548
  14. package/dist/assets/{index-DARIZgoD.js → index-17CwxHSZ.js} +185 -185
  15. package/dist/assets/index-B9N-gfOQ.css +32 -0
  16. package/dist/clear-cache.html +85 -85
  17. package/dist/convert-icons.md +52 -52
  18. package/dist/docs.html +308 -308
  19. package/dist/favicon.svg +8 -8
  20. package/dist/features.html +133 -133
  21. package/dist/generate-icons.js +48 -48
  22. package/dist/humans.txt +15 -15
  23. package/dist/icons/codex-white.svg +3 -3
  24. package/dist/icons/codex.svg +3 -3
  25. package/dist/icons/cursor-white.svg +11 -11
  26. package/dist/icons/icon-128x128.svg +9 -9
  27. package/dist/icons/icon-144x144.svg +9 -9
  28. package/dist/icons/icon-152x152.svg +9 -9
  29. package/dist/icons/icon-192x192.svg +9 -9
  30. package/dist/icons/icon-384x384.svg +9 -9
  31. package/dist/icons/icon-512x512.svg +9 -9
  32. package/dist/icons/icon-72x72.svg +9 -9
  33. package/dist/icons/icon-96x96.svg +9 -9
  34. package/dist/icons/icon-template.svg +9 -9
  35. package/dist/icons/qwen-logo.svg +14 -14
  36. package/dist/index.html +59 -59
  37. package/dist/landing.html +268 -268
  38. package/dist/llms-full.txt +119 -119
  39. package/dist/llms.txt +53 -53
  40. package/dist/logo.svg +12 -12
  41. package/dist/manifest.json +60 -60
  42. package/dist/openapi.yaml +1696 -1696
  43. package/dist/orchestration.html +125 -125
  44. package/dist/robots.txt +4 -4
  45. package/dist/site.css +692 -692
  46. package/dist/sitemap.xml +51 -51
  47. package/dist/sw.js +132 -132
  48. package/dist-server/server/cli.js +96 -96
  49. package/dist-server/server/daemon/manager.js +33 -33
  50. package/dist-server/server/daemon-manager.js +64 -64
  51. package/dist-server/server/index.js +125 -4
  52. package/dist-server/server/index.js.map +1 -1
  53. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js +84 -0
  54. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js.map +1 -0
  55. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js +43 -0
  56. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js.map +1 -0
  57. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +55 -1
  58. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
  59. package/dist-server/server/modules/orchestration/index.js +1 -0
  60. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  61. package/dist-server/server/routes/commands.js +25 -25
  62. package/dist-server/server/routes/git.js +17 -17
  63. package/dist-server/server/routes/live-view.js +46 -46
  64. package/dist-server/server/services/hermes-gateway.js +310 -0
  65. package/dist-server/server/services/hermes-gateway.js.map +1 -1
  66. package/dist-server/server/services/public-api-manifest.js +59 -51
  67. package/dist-server/server/services/public-api-manifest.js.map +1 -1
  68. package/package.json +222 -222
  69. package/scripts/fix-node-pty.js +67 -67
  70. package/scripts/github/create-v1.38-issues.mjs +351 -351
  71. package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
  72. package/scripts/hermes/configure-pixcode-mcp.mjs +165 -163
  73. package/scripts/hermes/pixcode-mcp-server.mjs +1009 -958
  74. package/scripts/smoke/changes-panel-layout.mjs +48 -48
  75. package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
  76. package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
  77. package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
  78. package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
  79. package/scripts/smoke/chat-session-state.mjs +19 -19
  80. package/scripts/smoke/code-editor-theme.mjs +55 -55
  81. package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
  82. package/scripts/smoke/command-center-agent-writes.mjs +79 -79
  83. package/scripts/smoke/command-center-non-git.mjs +46 -46
  84. package/scripts/smoke/context-packet.mjs +43 -43
  85. package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
  86. package/scripts/smoke/daemon-entrypoint.mjs +20 -20
  87. package/scripts/smoke/default-landing-routing.mjs +33 -33
  88. package/scripts/smoke/desktop-native-notifications.mjs +30 -30
  89. package/scripts/smoke/desktop-tray-icon.mjs +33 -33
  90. package/scripts/smoke/discord-release-workflow.mjs +24 -24
  91. package/scripts/smoke/git-install-update.mjs +255 -255
  92. package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
  93. package/scripts/smoke/hermes-api-install.mjs +56 -56
  94. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
  95. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -367
  96. package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
  97. package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
  98. package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
  99. package/scripts/smoke/hermes-rest-gateway.mjs +79 -70
  100. package/scripts/smoke/hermes-rest-live.mjs +42 -42
  101. package/scripts/smoke/hermes-roundtrip.mjs +167 -167
  102. package/scripts/smoke/hermes-settings-commands.mjs +349 -346
  103. package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
  104. package/scripts/smoke/live-view-diagnostics.mjs +53 -53
  105. package/scripts/smoke/live-view-environment.mjs +92 -92
  106. package/scripts/smoke/live-view-integration.mjs +450 -450
  107. package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
  108. package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
  109. package/scripts/smoke/model-registry.mjs +36 -36
  110. package/scripts/smoke/multi-project-ui.mjs +45 -45
  111. package/scripts/smoke/multi-worker-slots.mjs +42 -42
  112. package/scripts/smoke/notification-center.mjs +87 -87
  113. package/scripts/smoke/notification-inapp-preference.mjs +23 -23
  114. package/scripts/smoke/notification-taxonomy.mjs +58 -58
  115. package/scripts/smoke/orchestration-api.mjs +172 -172
  116. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
  117. package/scripts/smoke/orchestration-live-run.mjs +176 -176
  118. package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
  119. package/scripts/smoke/orchestration-model-sync.mjs +30 -30
  120. package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
  121. package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
  122. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
  123. package/scripts/smoke/permission-policy.mjs +50 -50
  124. package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -164
  125. package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
  126. package/scripts/smoke/provider-rest-api.mjs +124 -124
  127. package/scripts/smoke/provider-selection-status.mjs +52 -52
  128. package/scripts/smoke/run-state-refresh.mjs +52 -52
  129. package/scripts/smoke/runtime-manager.mjs +99 -99
  130. package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
  131. package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
  132. package/scripts/smoke/static-root-routing.mjs +21 -21
  133. package/scripts/smoke/strict-handoff-compact.mjs +60 -60
  134. package/scripts/smoke/taskmaster-config.mjs +24 -24
  135. package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
  136. package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
  137. package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
  138. package/scripts/smoke/telegram-control.mjs +242 -242
  139. package/scripts/smoke/tunnel-persistence.mjs +56 -56
  140. package/scripts/smoke/update-issue-progress.mjs +69 -69
  141. package/scripts/smoke/update-ux.mjs +55 -55
  142. package/scripts/smoke/v138-completion.mjs +132 -132
  143. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
  144. package/scripts/smoke/v138-diagnostics.mjs +63 -63
  145. package/scripts/smoke/v138-issue-planner.mjs +33 -33
  146. package/scripts/smoke/v143-remote-control.mjs +76 -76
  147. package/scripts/smoke/v144-production-loop.mjs +47 -47
  148. package/scripts/smoke/v145-platformization.mjs +46 -46
  149. package/scripts/smoke/v146-control-room-ui.mjs +150 -150
  150. package/scripts/smoke/version-modal-autoshow.mjs +29 -29
  151. package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
  152. package/scripts/smoke/vscode-workbench-polish.mjs +461 -436
  153. package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
  154. package/scripts/smoke/workflow-templates.mjs +43 -43
  155. package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
  156. package/scripts/update-git-install.mjs +293 -293
  157. package/server/claude-sdk.js +920 -920
  158. package/server/cli.js +1039 -1039
  159. package/server/constants/config.js +4 -4
  160. package/server/cursor-cli.js +344 -344
  161. package/server/daemon/manager.js +563 -563
  162. package/server/daemon-manager.js +964 -964
  163. package/server/database/db.js +921 -921
  164. package/server/database/json-store.js +197 -197
  165. package/server/gemini-cli.js +550 -550
  166. package/server/gemini-response-handler.js +79 -79
  167. package/server/index.js +131 -3
  168. package/server/load-env.js +35 -35
  169. package/server/middleware/auth.js +175 -175
  170. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
  171. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
  172. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
  173. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
  174. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
  175. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
  176. package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -0
  177. package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -0
  178. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
  179. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
  180. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  181. package/server/modules/orchestration/a2a/routes.ts +590 -590
  182. package/server/modules/orchestration/a2a/task-store.ts +178 -178
  183. package/server/modules/orchestration/a2a/types.ts +126 -126
  184. package/server/modules/orchestration/a2a/validator.ts +113 -113
  185. package/server/modules/orchestration/hermes/hermes.routes.ts +642 -583
  186. package/server/modules/orchestration/index.ts +101 -100
  187. package/server/modules/orchestration/preview/port-watcher.ts +112 -112
  188. package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
  189. package/server/modules/orchestration/preview/types.ts +19 -19
  190. package/server/modules/orchestration/security/permission-policy.ts +401 -401
  191. package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
  192. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
  193. package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
  194. package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
  195. package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
  196. package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
  197. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
  198. package/server/modules/orchestration/workflows/context-packet.ts +186 -186
  199. package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
  200. package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
  201. package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
  202. package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
  203. package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
  204. package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
  205. package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
  206. package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
  207. package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
  208. package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
  209. package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
  210. package/server/modules/orchestration/workspace/path-safety.ts +55 -55
  211. package/server/modules/orchestration/workspace/types.ts +52 -52
  212. package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
  213. package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
  214. package/server/modules/providers/index.ts +2 -2
  215. package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
  216. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  217. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  218. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  219. package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
  220. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  221. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  222. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  223. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
  224. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  225. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  226. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  227. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
  228. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  229. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  230. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  231. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
  232. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  233. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
  234. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  235. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
  236. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  237. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  238. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  239. package/server/modules/providers/provider.registry.ts +40 -40
  240. package/server/modules/providers/provider.routes.ts +944 -944
  241. package/server/modules/providers/services/mcp.service.ts +86 -86
  242. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  243. package/server/modules/providers/services/sessions.service.ts +45 -45
  244. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  245. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  246. package/server/modules/providers/shared/provider-configs.ts +142 -142
  247. package/server/modules/providers/tests/mcp.test.ts +293 -293
  248. package/server/openai-codex.js +462 -462
  249. package/server/opencode-cli.js +491 -491
  250. package/server/opencode-response-handler.js +111 -111
  251. package/server/projects.js +3008 -3008
  252. package/server/qwen-code-cli.js +410 -410
  253. package/server/qwen-response-handler.js +73 -73
  254. package/server/routes/agent.js +1435 -1435
  255. package/server/routes/auth.js +159 -159
  256. package/server/routes/codex.js +20 -20
  257. package/server/routes/commands.js +570 -570
  258. package/server/routes/cursor.js +61 -61
  259. package/server/routes/diagnostics.js +41 -41
  260. package/server/routes/gemini.js +25 -25
  261. package/server/routes/git.js +1650 -1650
  262. package/server/routes/live-view.js +411 -411
  263. package/server/routes/mcp-utils.js +13 -13
  264. package/server/routes/messages.js +62 -62
  265. package/server/routes/network.js +125 -125
  266. package/server/routes/platformization.js +212 -212
  267. package/server/routes/plugins.js +320 -320
  268. package/server/routes/production-agent-loop.js +90 -90
  269. package/server/routes/projects.js +917 -917
  270. package/server/routes/public-api.js +34 -34
  271. package/server/routes/qwen.js +27 -27
  272. package/server/routes/remote.js +55 -55
  273. package/server/routes/settings.js +321 -321
  274. package/server/routes/telegram.js +140 -140
  275. package/server/routes/user.js +125 -125
  276. package/server/routes/webhooks.js +63 -63
  277. package/server/services/control-room.js +102 -102
  278. package/server/services/diagnostics.js +165 -165
  279. package/server/services/external-access.js +375 -375
  280. package/server/services/hermes-gateway.js +1562 -1247
  281. package/server/services/hermes-install-jobs.js +729 -729
  282. package/server/services/install-jobs.js +715 -715
  283. package/server/services/live-view.js +956 -956
  284. package/server/services/managed-runtimes.js +493 -493
  285. package/server/services/model-registry.js +144 -144
  286. package/server/services/notification-orchestrator.js +365 -365
  287. package/server/services/notification-taxonomy.js +204 -204
  288. package/server/services/platformization.js +815 -815
  289. package/server/services/production-agent-loop.js +248 -248
  290. package/server/services/provider-cli-versions.js +149 -149
  291. package/server/services/provider-credentials.js +189 -189
  292. package/server/services/provider-models.js +396 -396
  293. package/server/services/public-api-manifest.js +190 -182
  294. package/server/services/remote-connection.js +127 -127
  295. package/server/services/runtime-manager.js +323 -323
  296. package/server/services/startup-update.js +234 -234
  297. package/server/services/telegram/bot.js +331 -331
  298. package/server/services/telegram/control-center.js +979 -979
  299. package/server/services/telegram/telegram-http-client.js +151 -151
  300. package/server/services/telegram/translations.js +340 -340
  301. package/server/services/vapid-keys.js +36 -36
  302. package/server/services/webhooks.js +216 -216
  303. package/server/sessionManager.js +225 -225
  304. package/server/shared/interfaces.ts +54 -54
  305. package/server/shared/types.ts +172 -172
  306. package/server/shared/utils.ts +193 -193
  307. package/server/tsconfig.json +36 -36
  308. package/server/utils/colors.js +21 -21
  309. package/server/utils/commandParser.js +305 -305
  310. package/server/utils/frontmatter.js +18 -18
  311. package/server/utils/gitConfig.js +34 -34
  312. package/server/utils/plugin-loader.js +457 -457
  313. package/server/utils/plugin-process-manager.js +185 -185
  314. package/server/utils/port-access.js +209 -209
  315. package/server/utils/runtime-paths.js +37 -37
  316. package/server/utils/url-detection.js +71 -71
  317. package/server/vite-daemon.js +79 -79
  318. package/shared/modelConstants.js +161 -161
  319. package/shared/networkHosts.js +22 -22
  320. package/dist/assets/index-DMz0zv6T.css +0 -32
@@ -1,586 +1,586 @@
1
- import type { Router } from 'express';
2
- import express from 'express';
3
-
4
- import { workflowRunner } from '@/modules/orchestration/workflows/workflow-runner.js';
5
- import {
6
- type WorkflowReplayScope,
7
- buildWorkflowReplayPlan,
8
- } from '@/modules/orchestration/workflows/workflow-replay.js';
9
- import {
10
- applyWorkflowTemplateToMetadata,
11
- builtInWorkflowTemplates,
12
- getWorkflowTemplate,
13
- } from '@/modules/orchestration/workflows/workflow-templates.js';
14
- import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
15
- import { buildWorkflowTrace } from '@/modules/orchestration/workflows/workflow-trace.js';
16
- import { findPixcodeAppRoot } from '@/modules/orchestration/workflows/workspace-target.js';
17
- import {
18
- listPendingApprovals,
19
- resolvePermissionApproval,
20
- type ApprovalDecisionSource,
21
- } from '@/modules/orchestration/workflows/approval-queue.js';
22
- import {
23
- DEFAULT_PERMISSION_POLICY,
24
- PERMISSION_CAPABILITIES,
25
- PERMISSION_POLICY_MODES,
26
- PIXCODE_PERMISSION_POLICY_PROTOCOL,
27
- evaluatePermissionRequest,
28
- normalizePermissionPolicy,
29
- } from '@/modules/orchestration/security/permission-policy.js';
30
- import { dispatchWebhookEvent } from '@/services/webhooks.js';
31
-
32
- const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
33
-
34
- function isWindowsLikeProjectId(projectId: string): boolean {
35
- return /^[A-Za-z]--/.test(projectId) || /^[A-Za-z]:[\\/]/.test(projectId);
36
- }
37
-
38
- function normalizeProjectId(projectId: string): string {
39
- return projectId.trim().replace(/\\/g, '/').toLowerCase();
40
- }
41
-
42
- function projectIdsMatch(storedProjectId: unknown, requestedProjectId: string): boolean {
43
- if (typeof storedProjectId !== 'string') return false;
44
- if (storedProjectId === requestedProjectId) return true;
45
-
46
- if (!isWindowsLikeProjectId(storedProjectId) && !isWindowsLikeProjectId(requestedProjectId)) {
47
- return false;
48
- }
49
-
50
- return normalizeProjectId(storedProjectId) === normalizeProjectId(requestedProjectId);
51
- }
52
-
53
- function readLimit(value: unknown): number | undefined {
54
- if (typeof value !== 'string') return undefined;
55
- const parsed = Number.parseInt(value, 10);
56
- if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
57
- return Math.min(parsed, 100);
58
- }
59
-
60
- function readMetadata(body: unknown): Record<string, unknown> | undefined {
61
- if (!body || typeof body !== 'object') return undefined;
62
- const metadata = (body as Record<string, unknown>).metadata;
63
- return metadata && typeof metadata === 'object' ? metadata as Record<string, unknown> : undefined;
64
- }
65
-
66
- function readRequestUserId(req: express.Request): string | number | null {
67
- const user = (req as express.Request & { user?: { id?: string | number; userId?: string | number } }).user;
68
- return user?.id ?? user?.userId ?? null;
69
- }
70
-
71
- function readReplayScope(value: unknown): WorkflowReplayScope {
72
- return value === 'run' ? 'run' : 'node';
73
- }
74
-
75
- function readOptionalString(value: unknown): string | undefined {
76
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
77
- }
78
-
79
- function readBooleanFlag(value: unknown): boolean {
80
- return value === true || value === 'true' || value === '1';
81
- }
82
-
83
- function replayOptions(req: express.Request): {
84
- scope: WorkflowReplayScope;
85
- fromNodeId?: string;
86
- approveReplay: boolean;
87
- } {
88
- return {
89
- scope: readReplayScope(req.body?.scope ?? req.query.scope),
90
- fromNodeId: readOptionalString(req.body?.fromNodeId ?? req.query.fromNodeId),
91
- approveReplay: readBooleanFlag(req.body?.approveReplay ?? req.query.approveReplay),
92
- };
93
- }
94
-
95
- function readRunArray(run: { metadata?: Record<string, unknown> }, key: string): Array<Record<string, unknown>> {
96
- const value = run.metadata?.[key];
97
- return Array.isArray(value)
98
- ? value.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object'))
99
- : [];
100
- }
101
-
102
- function updateApproval(
103
- run: { metadata?: Record<string, unknown> },
104
- requestId: string,
105
- patch: Record<string, unknown>,
106
- ): boolean {
107
- const approvals = readRunArray(run, 'pendingPermissionApprovals');
108
- let changed = false;
109
- const nextApprovals = approvals.map((approval) => {
110
- if (approval.id !== requestId) return approval;
111
- changed = true;
112
- return {
113
- ...approval,
114
- ...patch,
115
- };
116
- });
117
- if (!changed) return false;
118
- run.metadata = {
119
- ...run.metadata,
120
- pendingPermissionApprovals: nextApprovals,
121
- };
122
- return true;
123
- }
124
-
125
- function sendRunSnapshot(res: express.Response, runId: string): boolean {
126
- const run = workflowStore.getRun(runId);
127
- if (!run) {
128
- res.write(`event: error\ndata: ${JSON.stringify({ error: { code: 'RUN_NOT_FOUND', message: runId } })}\n\n`);
129
- return true;
130
- }
131
-
132
- res.write(`event: snapshot\ndata: ${JSON.stringify({ run })}\n\n`);
133
- return TERMINAL_RUN_STATES.has(run.status);
134
- }
135
-
136
- export function createWorkflowRouter(): Router {
137
- const router = express.Router();
138
- router.use(express.json({ limit: '2mb' }));
139
-
140
- router.get('/workflows', (_req, res) => {
141
- res.json({ workflows: workflowStore.listWorkflows() });
142
- });
143
-
144
- router.get('/workflows/templates', (_req, res) => {
145
- res.json({
146
- templates: builtInWorkflowTemplates,
147
- });
148
- });
149
-
150
- router.get('/workflows/context', (_req, res) => {
151
- res.json({
152
- appRoot: findPixcodeAppRoot(),
153
- defaultWorkspaceTarget: 'selected_project',
154
- supportedWorkspaceTargets: ['selected_project', 'pixcode_app', 'custom'],
155
- });
156
- });
157
-
158
- router.get('/workflows/runs', (req, res) => {
159
- const projectId = typeof req.query.projectId === 'string' ? req.query.projectId : undefined;
160
- const limit = readLimit(req.query.limit);
161
- const runs = projectId
162
- ? workflowStore.listRuns().filter((run) => projectIdsMatch(run.metadata?.projectId, projectId))
163
- : workflowStore.listRuns();
164
- res.json({ runs: limit ? runs.slice(0, limit) : runs });
165
- });
166
-
167
- router.get('/workflows/runs/:runId/events', (req, res) => {
168
- res.setHeader('Content-Type', 'text/event-stream');
169
- res.setHeader('Cache-Control', 'no-cache, no-transform');
170
- res.setHeader('Connection', 'keep-alive');
171
- res.setHeader('X-Accel-Buffering', 'no');
172
- if (typeof res.flushHeaders === 'function') {
173
- res.flushHeaders();
174
- }
175
- if (res.socket && typeof res.socket.setNoDelay === 'function') {
176
- try {
177
- res.socket.setNoDelay(true);
178
- } catch {
179
- // Non-fatal; SSE still works without disabling Nagle.
180
- }
181
- }
182
-
183
- const terminal = sendRunSnapshot(res, req.params.runId);
184
- if (terminal) {
185
- res.end();
186
- return;
187
- }
188
-
189
- const heartbeat = setInterval(() => {
190
- res.write(': ping\n\n');
191
- }, 15_000);
192
- const timer = setInterval(() => {
193
- const done = sendRunSnapshot(res, req.params.runId);
194
- if (done) {
195
- clearInterval(timer);
196
- clearInterval(heartbeat);
197
- res.end();
198
- }
199
- }, 1_000);
200
-
201
- req.on('close', () => {
202
- clearInterval(timer);
203
- clearInterval(heartbeat);
204
- });
205
- });
206
-
207
- router.get('/workflows/runs/:runId/trace', (req, res) => {
208
- const run = workflowStore.getRun(req.params.runId);
209
- if (!run) {
210
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
211
- return;
212
- }
213
-
214
- res.json({
215
- runId: run.id,
216
- trace: buildWorkflowTrace(run),
217
- });
218
- });
219
-
220
- router.get('/workflows/permission-policy', (_req, res) => {
221
- res.json({
222
- protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
223
- capabilities: PERMISSION_CAPABILITIES,
224
- modes: PERMISSION_POLICY_MODES,
225
- defaultPolicy: DEFAULT_PERMISSION_POLICY,
226
- });
227
- });
228
-
229
- router.post('/workflows/permission-policy/evaluate', (req, res) => {
230
- try {
231
- res.json({
232
- decision: evaluatePermissionRequest({
233
- policy: normalizePermissionPolicy(req.body?.policy),
234
- request: req.body?.request ?? { source: 'api' },
235
- }),
236
- });
237
- } catch (error) {
238
- res.status(400).json({
239
- error: {
240
- code: 'PERMISSION_POLICY_INVALID',
241
- message: error instanceof Error ? error.message : String(error),
242
- },
243
- });
244
- }
245
- });
246
-
247
- router.get('/workflows/runs/:runId/permission-approvals', (req, res) => {
248
- const run = workflowStore.getRun(req.params.runId);
249
- if (!run) {
250
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
251
- return;
252
- }
253
-
254
- res.json({
255
- runId: run.id,
256
- pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
257
- .filter((approval) => approval.status === 'pending'),
258
- approvalHistory: readRunArray(run, 'pendingPermissionApprovals')
259
- .filter((approval) => approval.status !== 'pending'),
260
- });
261
- });
262
-
263
- router.get('/workflows/approvals', (req, res) => {
264
- res.json({
265
- pendingApprovals: listPendingApprovals({
266
- projectId: readOptionalString(req.query.projectId),
267
- includeResolved: readBooleanFlag(req.query.includeResolved),
268
- }),
269
- });
270
- });
271
-
272
- router.post('/workflows/approvals/:approvalId', (req, res) => {
273
- const allow = req.body?.allow === true;
274
- const deny = req.body?.allow === false;
275
- if (!allow && !deny) {
276
- res.status(400).json({
277
- error: {
278
- code: 'PERMISSION_DECISION_REQUIRED',
279
- message: 'Approval queue decisions require allow=true or allow=false.',
280
- },
281
- });
282
- return;
283
- }
284
-
285
- const source = ['ui', 'telegram', 'api'].includes(req.body?.source)
286
- ? req.body.source as ApprovalDecisionSource
287
- : 'api';
288
- const result = resolvePermissionApproval({
289
- approvalId: req.params.approvalId,
290
- allow,
291
- source,
292
- resolvedBy: readRequestUserId(req),
293
- message: readOptionalString(req.body?.message),
294
- });
295
- if (!result) {
296
- res.status(404).json({ error: { code: 'APPROVAL_NOT_FOUND', message: req.params.approvalId } });
297
- return;
298
- }
299
-
300
- dispatchWebhookEvent({
301
- type: 'approval.resolved',
302
- payload: {
303
- approvalId: req.params.approvalId,
304
- allow,
305
- source,
306
- runId: result.runId,
307
- },
308
- });
309
- res.json(result);
310
- });
311
-
312
- router.post('/workflows/runs/:runId/permission-approvals/:requestId', (req, res) => {
313
- const run = workflowStore.getRun(req.params.runId);
314
- if (!run) {
315
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
316
- return;
317
- }
318
-
319
- const allow = req.body?.allow === true;
320
- const deny = req.body?.allow === false;
321
- if (!allow && !deny) {
322
- res.status(400).json({
323
- error: {
324
- code: 'PERMISSION_DECISION_REQUIRED',
325
- message: 'Permission approval requires allow=true or allow=false.',
326
- },
327
- });
328
- return;
329
- }
330
-
331
- const updated = updateApproval(run, req.params.requestId, {
332
- status: allow ? 'allowed' : 'denied',
333
- resolvedAt: Date.now(),
334
- resolvedBy: readRequestUserId(req),
335
- resolutionMessage: readOptionalString(req.body?.message),
336
- });
337
- if (!updated) {
338
- res.status(404).json({ error: { code: 'APPROVAL_NOT_FOUND', message: req.params.requestId } });
339
- return;
340
- }
341
-
342
- workflowStore.setRun(run);
343
- dispatchWebhookEvent({
344
- type: 'approval.resolved',
345
- payload: {
346
- approvalId: req.params.requestId,
347
- allow,
348
- source: 'ui',
349
- runId: run.id,
350
- },
351
- });
352
- res.json({
353
- runId: run.id,
354
- pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
355
- .filter((approval) => approval.status === 'pending'),
356
- approvalHistory: readRunArray(run, 'pendingPermissionApprovals')
357
- .filter((approval) => approval.status !== 'pending'),
358
- });
359
- });
360
-
361
- router.get('/workflows/runs/:runId/replay-plan', (req, res) => {
362
- const run = workflowStore.getRun(req.params.runId);
363
- if (!run) {
364
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
365
- return;
366
- }
367
-
368
- try {
369
- const options = replayOptions(req);
370
- res.json({
371
- replayPlan: buildWorkflowReplayPlan(run, {
372
- scope: options.scope,
373
- fromNodeId: options.fromNodeId,
374
- }),
375
- });
376
- } catch (error) {
377
- res.status(400).json({
378
- error: {
379
- code: 'REPLAY_PLAN_INVALID',
380
- message: error instanceof Error ? error.message : String(error),
381
- },
382
- });
383
- }
384
- });
385
-
386
- router.post('/workflows/runs/:runId/replay', (req, res) => {
387
- const run = workflowStore.getRun(req.params.runId);
388
- if (!run) {
389
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
390
- return;
391
- }
392
-
393
- try {
394
- const options = replayOptions(req);
395
- const replayPlan = buildWorkflowReplayPlan(run, {
396
- scope: options.scope,
397
- fromNodeId: options.fromNodeId,
398
- });
399
-
400
- if (replayPlan.requiresApproval && !options.approveReplay) {
401
- res.status(409).json({
402
- error: {
403
- code: 'REPLAY_APPROVAL_REQUIRED',
404
- message: 'Replay requires explicit approval because prior shell, network, or file-write activity was detected.',
405
- },
406
- replayPlan,
407
- });
408
- return;
409
- }
410
-
411
- const replayRun = workflowRunner.start(
412
- replayPlan.workflow,
413
- replayPlan.input,
414
- {
415
- ...replayPlan.metadata,
416
- userId: readRequestUserId(req) ?? run.metadata?.userId,
417
- replay: {
418
- ...(replayPlan.metadata.replay && typeof replayPlan.metadata.replay === 'object'
419
- ? replayPlan.metadata.replay as Record<string, unknown>
420
- : {}),
421
- approved: options.approveReplay,
422
- approvedAt: options.approveReplay ? Date.now() : undefined,
423
- },
424
- },
425
- );
426
- dispatchWebhookEvent({
427
- type: 'run.started',
428
- payload: {
429
- runId: replayRun.id,
430
- workflowId: replayRun.workflowId,
431
- replayOf: run.id,
432
- },
433
- });
434
- res.status(202).json({
435
- run: replayRun,
436
- replayPlan,
437
- });
438
- } catch (error) {
439
- res.status(400).json({
440
- error: {
441
- code: 'REPLAY_START_FAILED',
442
- message: error instanceof Error ? error.message : String(error),
443
- },
444
- });
445
- }
446
- });
447
-
448
- router.get('/workflows/runs/:runId', (req, res) => {
449
- const run = workflowStore.getRun(req.params.runId);
450
- if (!run) {
451
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
452
- return;
453
- }
454
- res.json(run);
455
- });
456
-
457
- router.post('/workflows/:id/preview', (req, res) => {
458
- const workflow = workflowStore.getWorkflow(req.params.id);
459
- if (!workflow) {
460
- res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: req.params.id } });
461
- return;
462
- }
463
- try {
464
- const runtimeWorkflow = workflowRunner.preview(workflow, readMetadata(req.body));
465
- res.json({
466
- workflow: runtimeWorkflow,
467
- nodeCount: runtimeWorkflow.nodes.length,
468
- nodes: runtimeWorkflow.nodes.map((node) => ({
469
- id: node.id,
470
- adapterId: node.adapterId,
471
- agentInstanceId: node.agentInstanceId,
472
- agentLabel: node.agentLabel,
473
- assignment: node.assignment,
474
- stage: node.stage,
475
- inputs: node.inputs,
476
- onFail: node.onFail,
477
- output: node.output,
478
- model: node.model,
479
- permissionMode: node.permissionMode,
480
- timeoutMs: node.timeoutMs,
481
- })),
482
- });
483
- } catch (error) {
484
- res.status(400).json({
485
- error: {
486
- code: 'WORKFLOW_INVALID',
487
- message: error instanceof Error ? error.message : String(error),
488
- },
489
- });
490
- }
491
- });
492
-
493
- router.post('/workflows/runs/:runId/cancel', async (req, res) => {
494
- const run = await workflowRunner.cancel(req.params.runId);
495
- if (!run) {
496
- res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
497
- return;
498
- }
499
- dispatchWebhookEvent({
500
- type: 'run.canceled',
501
- payload: {
502
- runId: run.id,
503
- workflowId: run.workflowId,
504
- },
505
- });
506
- res.json(run);
507
- });
508
-
509
- router.post('/workflows/:id/runs', (req, res) => {
510
- const workflow = workflowStore.getWorkflow(req.params.id);
511
- if (!workflow) {
512
- res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: req.params.id } });
513
- return;
514
- }
515
- try {
516
- const run = workflowRunner.start(
517
- workflow,
518
- typeof req.body?.input === 'string' ? req.body.input : '',
519
- {
520
- ...readMetadata(req.body),
521
- userId: readRequestUserId(req),
522
- workflowName: workflow.name,
523
- },
524
- );
525
- dispatchWebhookEvent({
526
- type: 'run.started',
527
- payload: {
528
- runId: run.id,
529
- workflowId: run.workflowId,
530
- workflowName: workflow.name,
531
- },
532
- });
533
- res.status(202).json(run);
534
- } catch (error) {
535
- res.status(400).json({
536
- error: {
537
- code: 'WORKFLOW_INVALID',
538
- message: error instanceof Error ? error.message : String(error),
539
- },
540
- });
541
- }
542
- });
543
-
544
- router.post('/workflows/templates/:templateId/runs', (req, res) => {
545
- const template = getWorkflowTemplate(req.params.templateId);
546
- if (!template) {
547
- res.status(404).json({ error: { code: 'WORKFLOW_TEMPLATE_NOT_FOUND', message: req.params.templateId } });
548
- return;
549
- }
550
- const workflow = workflowStore.getWorkflow(template.workflowId);
551
- if (!workflow) {
552
- res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: template.workflowId } });
553
- return;
554
- }
555
-
556
- try {
557
- const run = workflowRunner.start(
558
- workflow,
559
- typeof req.body?.input === 'string' ? req.body.input : '',
560
- {
561
- ...applyWorkflowTemplateToMetadata(template, readMetadata(req.body)),
562
- userId: readRequestUserId(req),
563
- workflowName: workflow.name,
564
- },
565
- );
566
- dispatchWebhookEvent({
567
- type: 'run.started',
568
- payload: {
569
- runId: run.id,
570
- workflowId: run.workflowId,
571
- templateId: template.id,
572
- },
573
- });
574
- res.status(202).json(run);
575
- } catch (error) {
576
- res.status(400).json({
577
- error: {
578
- code: 'WORKFLOW_TEMPLATE_START_FAILED',
579
- message: error instanceof Error ? error.message : String(error),
580
- },
581
- });
582
- }
583
- });
584
-
585
- return router;
586
- }
1
+ import type { Router } from 'express';
2
+ import express from 'express';
3
+
4
+ import { workflowRunner } from '@/modules/orchestration/workflows/workflow-runner.js';
5
+ import {
6
+ type WorkflowReplayScope,
7
+ buildWorkflowReplayPlan,
8
+ } from '@/modules/orchestration/workflows/workflow-replay.js';
9
+ import {
10
+ applyWorkflowTemplateToMetadata,
11
+ builtInWorkflowTemplates,
12
+ getWorkflowTemplate,
13
+ } from '@/modules/orchestration/workflows/workflow-templates.js';
14
+ import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
15
+ import { buildWorkflowTrace } from '@/modules/orchestration/workflows/workflow-trace.js';
16
+ import { findPixcodeAppRoot } from '@/modules/orchestration/workflows/workspace-target.js';
17
+ import {
18
+ listPendingApprovals,
19
+ resolvePermissionApproval,
20
+ type ApprovalDecisionSource,
21
+ } from '@/modules/orchestration/workflows/approval-queue.js';
22
+ import {
23
+ DEFAULT_PERMISSION_POLICY,
24
+ PERMISSION_CAPABILITIES,
25
+ PERMISSION_POLICY_MODES,
26
+ PIXCODE_PERMISSION_POLICY_PROTOCOL,
27
+ evaluatePermissionRequest,
28
+ normalizePermissionPolicy,
29
+ } from '@/modules/orchestration/security/permission-policy.js';
30
+ import { dispatchWebhookEvent } from '@/services/webhooks.js';
31
+
32
+ const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
33
+
34
+ function isWindowsLikeProjectId(projectId: string): boolean {
35
+ return /^[A-Za-z]--/.test(projectId) || /^[A-Za-z]:[\\/]/.test(projectId);
36
+ }
37
+
38
+ function normalizeProjectId(projectId: string): string {
39
+ return projectId.trim().replace(/\\/g, '/').toLowerCase();
40
+ }
41
+
42
+ function projectIdsMatch(storedProjectId: unknown, requestedProjectId: string): boolean {
43
+ if (typeof storedProjectId !== 'string') return false;
44
+ if (storedProjectId === requestedProjectId) return true;
45
+
46
+ if (!isWindowsLikeProjectId(storedProjectId) && !isWindowsLikeProjectId(requestedProjectId)) {
47
+ return false;
48
+ }
49
+
50
+ return normalizeProjectId(storedProjectId) === normalizeProjectId(requestedProjectId);
51
+ }
52
+
53
+ function readLimit(value: unknown): number | undefined {
54
+ if (typeof value !== 'string') return undefined;
55
+ const parsed = Number.parseInt(value, 10);
56
+ if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
57
+ return Math.min(parsed, 100);
58
+ }
59
+
60
+ function readMetadata(body: unknown): Record<string, unknown> | undefined {
61
+ if (!body || typeof body !== 'object') return undefined;
62
+ const metadata = (body as Record<string, unknown>).metadata;
63
+ return metadata && typeof metadata === 'object' ? metadata as Record<string, unknown> : undefined;
64
+ }
65
+
66
+ function readRequestUserId(req: express.Request): string | number | null {
67
+ const user = (req as express.Request & { user?: { id?: string | number; userId?: string | number } }).user;
68
+ return user?.id ?? user?.userId ?? null;
69
+ }
70
+
71
+ function readReplayScope(value: unknown): WorkflowReplayScope {
72
+ return value === 'run' ? 'run' : 'node';
73
+ }
74
+
75
+ function readOptionalString(value: unknown): string | undefined {
76
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
77
+ }
78
+
79
+ function readBooleanFlag(value: unknown): boolean {
80
+ return value === true || value === 'true' || value === '1';
81
+ }
82
+
83
+ function replayOptions(req: express.Request): {
84
+ scope: WorkflowReplayScope;
85
+ fromNodeId?: string;
86
+ approveReplay: boolean;
87
+ } {
88
+ return {
89
+ scope: readReplayScope(req.body?.scope ?? req.query.scope),
90
+ fromNodeId: readOptionalString(req.body?.fromNodeId ?? req.query.fromNodeId),
91
+ approveReplay: readBooleanFlag(req.body?.approveReplay ?? req.query.approveReplay),
92
+ };
93
+ }
94
+
95
+ function readRunArray(run: { metadata?: Record<string, unknown> }, key: string): Array<Record<string, unknown>> {
96
+ const value = run.metadata?.[key];
97
+ return Array.isArray(value)
98
+ ? value.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object'))
99
+ : [];
100
+ }
101
+
102
+ function updateApproval(
103
+ run: { metadata?: Record<string, unknown> },
104
+ requestId: string,
105
+ patch: Record<string, unknown>,
106
+ ): boolean {
107
+ const approvals = readRunArray(run, 'pendingPermissionApprovals');
108
+ let changed = false;
109
+ const nextApprovals = approvals.map((approval) => {
110
+ if (approval.id !== requestId) return approval;
111
+ changed = true;
112
+ return {
113
+ ...approval,
114
+ ...patch,
115
+ };
116
+ });
117
+ if (!changed) return false;
118
+ run.metadata = {
119
+ ...run.metadata,
120
+ pendingPermissionApprovals: nextApprovals,
121
+ };
122
+ return true;
123
+ }
124
+
125
+ function sendRunSnapshot(res: express.Response, runId: string): boolean {
126
+ const run = workflowStore.getRun(runId);
127
+ if (!run) {
128
+ res.write(`event: error\ndata: ${JSON.stringify({ error: { code: 'RUN_NOT_FOUND', message: runId } })}\n\n`);
129
+ return true;
130
+ }
131
+
132
+ res.write(`event: snapshot\ndata: ${JSON.stringify({ run })}\n\n`);
133
+ return TERMINAL_RUN_STATES.has(run.status);
134
+ }
135
+
136
+ export function createWorkflowRouter(): Router {
137
+ const router = express.Router();
138
+ router.use(express.json({ limit: '2mb' }));
139
+
140
+ router.get('/workflows', (_req, res) => {
141
+ res.json({ workflows: workflowStore.listWorkflows() });
142
+ });
143
+
144
+ router.get('/workflows/templates', (_req, res) => {
145
+ res.json({
146
+ templates: builtInWorkflowTemplates,
147
+ });
148
+ });
149
+
150
+ router.get('/workflows/context', (_req, res) => {
151
+ res.json({
152
+ appRoot: findPixcodeAppRoot(),
153
+ defaultWorkspaceTarget: 'selected_project',
154
+ supportedWorkspaceTargets: ['selected_project', 'pixcode_app', 'custom'],
155
+ });
156
+ });
157
+
158
+ router.get('/workflows/runs', (req, res) => {
159
+ const projectId = typeof req.query.projectId === 'string' ? req.query.projectId : undefined;
160
+ const limit = readLimit(req.query.limit);
161
+ const runs = projectId
162
+ ? workflowStore.listRuns().filter((run) => projectIdsMatch(run.metadata?.projectId, projectId))
163
+ : workflowStore.listRuns();
164
+ res.json({ runs: limit ? runs.slice(0, limit) : runs });
165
+ });
166
+
167
+ router.get('/workflows/runs/:runId/events', (req, res) => {
168
+ res.setHeader('Content-Type', 'text/event-stream');
169
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
170
+ res.setHeader('Connection', 'keep-alive');
171
+ res.setHeader('X-Accel-Buffering', 'no');
172
+ if (typeof res.flushHeaders === 'function') {
173
+ res.flushHeaders();
174
+ }
175
+ if (res.socket && typeof res.socket.setNoDelay === 'function') {
176
+ try {
177
+ res.socket.setNoDelay(true);
178
+ } catch {
179
+ // Non-fatal; SSE still works without disabling Nagle.
180
+ }
181
+ }
182
+
183
+ const terminal = sendRunSnapshot(res, req.params.runId);
184
+ if (terminal) {
185
+ res.end();
186
+ return;
187
+ }
188
+
189
+ const heartbeat = setInterval(() => {
190
+ res.write(': ping\n\n');
191
+ }, 15_000);
192
+ const timer = setInterval(() => {
193
+ const done = sendRunSnapshot(res, req.params.runId);
194
+ if (done) {
195
+ clearInterval(timer);
196
+ clearInterval(heartbeat);
197
+ res.end();
198
+ }
199
+ }, 1_000);
200
+
201
+ req.on('close', () => {
202
+ clearInterval(timer);
203
+ clearInterval(heartbeat);
204
+ });
205
+ });
206
+
207
+ router.get('/workflows/runs/:runId/trace', (req, res) => {
208
+ const run = workflowStore.getRun(req.params.runId);
209
+ if (!run) {
210
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
211
+ return;
212
+ }
213
+
214
+ res.json({
215
+ runId: run.id,
216
+ trace: buildWorkflowTrace(run),
217
+ });
218
+ });
219
+
220
+ router.get('/workflows/permission-policy', (_req, res) => {
221
+ res.json({
222
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
223
+ capabilities: PERMISSION_CAPABILITIES,
224
+ modes: PERMISSION_POLICY_MODES,
225
+ defaultPolicy: DEFAULT_PERMISSION_POLICY,
226
+ });
227
+ });
228
+
229
+ router.post('/workflows/permission-policy/evaluate', (req, res) => {
230
+ try {
231
+ res.json({
232
+ decision: evaluatePermissionRequest({
233
+ policy: normalizePermissionPolicy(req.body?.policy),
234
+ request: req.body?.request ?? { source: 'api' },
235
+ }),
236
+ });
237
+ } catch (error) {
238
+ res.status(400).json({
239
+ error: {
240
+ code: 'PERMISSION_POLICY_INVALID',
241
+ message: error instanceof Error ? error.message : String(error),
242
+ },
243
+ });
244
+ }
245
+ });
246
+
247
+ router.get('/workflows/runs/:runId/permission-approvals', (req, res) => {
248
+ const run = workflowStore.getRun(req.params.runId);
249
+ if (!run) {
250
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
251
+ return;
252
+ }
253
+
254
+ res.json({
255
+ runId: run.id,
256
+ pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
257
+ .filter((approval) => approval.status === 'pending'),
258
+ approvalHistory: readRunArray(run, 'pendingPermissionApprovals')
259
+ .filter((approval) => approval.status !== 'pending'),
260
+ });
261
+ });
262
+
263
+ router.get('/workflows/approvals', (req, res) => {
264
+ res.json({
265
+ pendingApprovals: listPendingApprovals({
266
+ projectId: readOptionalString(req.query.projectId),
267
+ includeResolved: readBooleanFlag(req.query.includeResolved),
268
+ }),
269
+ });
270
+ });
271
+
272
+ router.post('/workflows/approvals/:approvalId', (req, res) => {
273
+ const allow = req.body?.allow === true;
274
+ const deny = req.body?.allow === false;
275
+ if (!allow && !deny) {
276
+ res.status(400).json({
277
+ error: {
278
+ code: 'PERMISSION_DECISION_REQUIRED',
279
+ message: 'Approval queue decisions require allow=true or allow=false.',
280
+ },
281
+ });
282
+ return;
283
+ }
284
+
285
+ const source = ['ui', 'telegram', 'api'].includes(req.body?.source)
286
+ ? req.body.source as ApprovalDecisionSource
287
+ : 'api';
288
+ const result = resolvePermissionApproval({
289
+ approvalId: req.params.approvalId,
290
+ allow,
291
+ source,
292
+ resolvedBy: readRequestUserId(req),
293
+ message: readOptionalString(req.body?.message),
294
+ });
295
+ if (!result) {
296
+ res.status(404).json({ error: { code: 'APPROVAL_NOT_FOUND', message: req.params.approvalId } });
297
+ return;
298
+ }
299
+
300
+ dispatchWebhookEvent({
301
+ type: 'approval.resolved',
302
+ payload: {
303
+ approvalId: req.params.approvalId,
304
+ allow,
305
+ source,
306
+ runId: result.runId,
307
+ },
308
+ });
309
+ res.json(result);
310
+ });
311
+
312
+ router.post('/workflows/runs/:runId/permission-approvals/:requestId', (req, res) => {
313
+ const run = workflowStore.getRun(req.params.runId);
314
+ if (!run) {
315
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
316
+ return;
317
+ }
318
+
319
+ const allow = req.body?.allow === true;
320
+ const deny = req.body?.allow === false;
321
+ if (!allow && !deny) {
322
+ res.status(400).json({
323
+ error: {
324
+ code: 'PERMISSION_DECISION_REQUIRED',
325
+ message: 'Permission approval requires allow=true or allow=false.',
326
+ },
327
+ });
328
+ return;
329
+ }
330
+
331
+ const updated = updateApproval(run, req.params.requestId, {
332
+ status: allow ? 'allowed' : 'denied',
333
+ resolvedAt: Date.now(),
334
+ resolvedBy: readRequestUserId(req),
335
+ resolutionMessage: readOptionalString(req.body?.message),
336
+ });
337
+ if (!updated) {
338
+ res.status(404).json({ error: { code: 'APPROVAL_NOT_FOUND', message: req.params.requestId } });
339
+ return;
340
+ }
341
+
342
+ workflowStore.setRun(run);
343
+ dispatchWebhookEvent({
344
+ type: 'approval.resolved',
345
+ payload: {
346
+ approvalId: req.params.requestId,
347
+ allow,
348
+ source: 'ui',
349
+ runId: run.id,
350
+ },
351
+ });
352
+ res.json({
353
+ runId: run.id,
354
+ pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
355
+ .filter((approval) => approval.status === 'pending'),
356
+ approvalHistory: readRunArray(run, 'pendingPermissionApprovals')
357
+ .filter((approval) => approval.status !== 'pending'),
358
+ });
359
+ });
360
+
361
+ router.get('/workflows/runs/:runId/replay-plan', (req, res) => {
362
+ const run = workflowStore.getRun(req.params.runId);
363
+ if (!run) {
364
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
365
+ return;
366
+ }
367
+
368
+ try {
369
+ const options = replayOptions(req);
370
+ res.json({
371
+ replayPlan: buildWorkflowReplayPlan(run, {
372
+ scope: options.scope,
373
+ fromNodeId: options.fromNodeId,
374
+ }),
375
+ });
376
+ } catch (error) {
377
+ res.status(400).json({
378
+ error: {
379
+ code: 'REPLAY_PLAN_INVALID',
380
+ message: error instanceof Error ? error.message : String(error),
381
+ },
382
+ });
383
+ }
384
+ });
385
+
386
+ router.post('/workflows/runs/:runId/replay', (req, res) => {
387
+ const run = workflowStore.getRun(req.params.runId);
388
+ if (!run) {
389
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
390
+ return;
391
+ }
392
+
393
+ try {
394
+ const options = replayOptions(req);
395
+ const replayPlan = buildWorkflowReplayPlan(run, {
396
+ scope: options.scope,
397
+ fromNodeId: options.fromNodeId,
398
+ });
399
+
400
+ if (replayPlan.requiresApproval && !options.approveReplay) {
401
+ res.status(409).json({
402
+ error: {
403
+ code: 'REPLAY_APPROVAL_REQUIRED',
404
+ message: 'Replay requires explicit approval because prior shell, network, or file-write activity was detected.',
405
+ },
406
+ replayPlan,
407
+ });
408
+ return;
409
+ }
410
+
411
+ const replayRun = workflowRunner.start(
412
+ replayPlan.workflow,
413
+ replayPlan.input,
414
+ {
415
+ ...replayPlan.metadata,
416
+ userId: readRequestUserId(req) ?? run.metadata?.userId,
417
+ replay: {
418
+ ...(replayPlan.metadata.replay && typeof replayPlan.metadata.replay === 'object'
419
+ ? replayPlan.metadata.replay as Record<string, unknown>
420
+ : {}),
421
+ approved: options.approveReplay,
422
+ approvedAt: options.approveReplay ? Date.now() : undefined,
423
+ },
424
+ },
425
+ );
426
+ dispatchWebhookEvent({
427
+ type: 'run.started',
428
+ payload: {
429
+ runId: replayRun.id,
430
+ workflowId: replayRun.workflowId,
431
+ replayOf: run.id,
432
+ },
433
+ });
434
+ res.status(202).json({
435
+ run: replayRun,
436
+ replayPlan,
437
+ });
438
+ } catch (error) {
439
+ res.status(400).json({
440
+ error: {
441
+ code: 'REPLAY_START_FAILED',
442
+ message: error instanceof Error ? error.message : String(error),
443
+ },
444
+ });
445
+ }
446
+ });
447
+
448
+ router.get('/workflows/runs/:runId', (req, res) => {
449
+ const run = workflowStore.getRun(req.params.runId);
450
+ if (!run) {
451
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
452
+ return;
453
+ }
454
+ res.json(run);
455
+ });
456
+
457
+ router.post('/workflows/:id/preview', (req, res) => {
458
+ const workflow = workflowStore.getWorkflow(req.params.id);
459
+ if (!workflow) {
460
+ res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: req.params.id } });
461
+ return;
462
+ }
463
+ try {
464
+ const runtimeWorkflow = workflowRunner.preview(workflow, readMetadata(req.body));
465
+ res.json({
466
+ workflow: runtimeWorkflow,
467
+ nodeCount: runtimeWorkflow.nodes.length,
468
+ nodes: runtimeWorkflow.nodes.map((node) => ({
469
+ id: node.id,
470
+ adapterId: node.adapterId,
471
+ agentInstanceId: node.agentInstanceId,
472
+ agentLabel: node.agentLabel,
473
+ assignment: node.assignment,
474
+ stage: node.stage,
475
+ inputs: node.inputs,
476
+ onFail: node.onFail,
477
+ output: node.output,
478
+ model: node.model,
479
+ permissionMode: node.permissionMode,
480
+ timeoutMs: node.timeoutMs,
481
+ })),
482
+ });
483
+ } catch (error) {
484
+ res.status(400).json({
485
+ error: {
486
+ code: 'WORKFLOW_INVALID',
487
+ message: error instanceof Error ? error.message : String(error),
488
+ },
489
+ });
490
+ }
491
+ });
492
+
493
+ router.post('/workflows/runs/:runId/cancel', async (req, res) => {
494
+ const run = await workflowRunner.cancel(req.params.runId);
495
+ if (!run) {
496
+ res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
497
+ return;
498
+ }
499
+ dispatchWebhookEvent({
500
+ type: 'run.canceled',
501
+ payload: {
502
+ runId: run.id,
503
+ workflowId: run.workflowId,
504
+ },
505
+ });
506
+ res.json(run);
507
+ });
508
+
509
+ router.post('/workflows/:id/runs', (req, res) => {
510
+ const workflow = workflowStore.getWorkflow(req.params.id);
511
+ if (!workflow) {
512
+ res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: req.params.id } });
513
+ return;
514
+ }
515
+ try {
516
+ const run = workflowRunner.start(
517
+ workflow,
518
+ typeof req.body?.input === 'string' ? req.body.input : '',
519
+ {
520
+ ...readMetadata(req.body),
521
+ userId: readRequestUserId(req),
522
+ workflowName: workflow.name,
523
+ },
524
+ );
525
+ dispatchWebhookEvent({
526
+ type: 'run.started',
527
+ payload: {
528
+ runId: run.id,
529
+ workflowId: run.workflowId,
530
+ workflowName: workflow.name,
531
+ },
532
+ });
533
+ res.status(202).json(run);
534
+ } catch (error) {
535
+ res.status(400).json({
536
+ error: {
537
+ code: 'WORKFLOW_INVALID',
538
+ message: error instanceof Error ? error.message : String(error),
539
+ },
540
+ });
541
+ }
542
+ });
543
+
544
+ router.post('/workflows/templates/:templateId/runs', (req, res) => {
545
+ const template = getWorkflowTemplate(req.params.templateId);
546
+ if (!template) {
547
+ res.status(404).json({ error: { code: 'WORKFLOW_TEMPLATE_NOT_FOUND', message: req.params.templateId } });
548
+ return;
549
+ }
550
+ const workflow = workflowStore.getWorkflow(template.workflowId);
551
+ if (!workflow) {
552
+ res.status(404).json({ error: { code: 'WORKFLOW_NOT_FOUND', message: template.workflowId } });
553
+ return;
554
+ }
555
+
556
+ try {
557
+ const run = workflowRunner.start(
558
+ workflow,
559
+ typeof req.body?.input === 'string' ? req.body.input : '',
560
+ {
561
+ ...applyWorkflowTemplateToMetadata(template, readMetadata(req.body)),
562
+ userId: readRequestUserId(req),
563
+ workflowName: workflow.name,
564
+ },
565
+ );
566
+ dispatchWebhookEvent({
567
+ type: 'run.started',
568
+ payload: {
569
+ runId: run.id,
570
+ workflowId: run.workflowId,
571
+ templateId: template.id,
572
+ },
573
+ });
574
+ res.status(202).json(run);
575
+ } catch (error) {
576
+ res.status(400).json({
577
+ error: {
578
+ code: 'WORKFLOW_TEMPLATE_START_FAILED',
579
+ message: error instanceof Error ? error.message : String(error),
580
+ },
581
+ });
582
+ }
583
+ });
584
+
585
+ return router;
586
+ }