@playcraft/cli 0.1.22 → 0.1.24

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 (278) hide show
  1. package/README.md +3 -38
  2. package/dist/agent/local-backend.js +1 -1
  3. package/dist/cli-root-help.js +0 -1
  4. package/dist/commands/build.js +4 -9
  5. package/dist/commands/create.js +33 -26
  6. package/dist/commands/image.js +6 -2
  7. package/dist/commands/login.js +42 -84
  8. package/dist/commands/skills.js +0 -2
  9. package/dist/commands/tools-generation.js +4 -2
  10. package/dist/commands/tools.js +0 -2
  11. package/dist/config.js +4 -59
  12. package/dist/index.js +1 -6
  13. package/dist/remix/clone.js +0 -2
  14. package/dist/remix/init-template.js +0 -2
  15. package/dist/remix/pull.js +0 -2
  16. package/dist/remix/push.js +1 -21
  17. package/dist/utils/agent-api-client.js +29 -148
  18. package/dist/utils/playcraft-http-client.js +4 -6
  19. package/package.json +4 -10
  20. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +61 -23
  21. package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +61 -23
  22. package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +61 -23
  23. package/dist/auth/account-activation.js +0 -157
  24. package/dist/auth/messages.js +0 -30
  25. package/dist/commands/platform-skills.generated.js +0 -19
  26. package/dist/commands/tools-batch.js +0 -59
  27. package/dist/commands/workspace-runtime.js +0 -54
  28. package/dist/project-skills/commands.js +0 -72
  29. package/dist/project-skills/lifecycle.js +0 -28
  30. package/dist/project-skills/local-run.js +0 -112
  31. package/dist/project-skills/local-store.js +0 -149
  32. package/dist/project-skills/messages.js +0 -72
  33. package/dist/project-skills/reconcile.js +0 -62
  34. package/dist/project-skills/remote-cache.js +0 -51
  35. package/dist/project-skills/validation.js +0 -19
  36. package/dist/utils/environment-endpoints.js +0 -17
  37. package/dist/utils/tool-operation-journal.js +0 -124
  38. package/dist/workspace-runtime/codex/app-server.js +0 -571
  39. package/dist/workspace-runtime/codex/config-toml.js +0 -108
  40. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +0 -123
  41. package/dist/workspace-runtime/codex/launcher-client.js +0 -163
  42. package/dist/workspace-runtime/codex/loopback.js +0 -143
  43. package/dist/workspace-runtime/codex/model-broker.js +0 -239
  44. package/dist/workspace-runtime/codex/native-adapter.js +0 -9
  45. package/dist/workspace-runtime/main.js +0 -10
  46. package/dist/workspace-runtime/persistence/journal.js +0 -355
  47. package/dist/workspace-runtime/persistence/sqlite-client.js +0 -61
  48. package/dist/workspace-runtime/persistence/sqlite-worker.js +0 -1015
  49. package/dist/workspace-runtime/previews/publish.js +0 -259
  50. package/dist/workspace-runtime/previews/single-file.js +0 -43
  51. package/dist/workspace-runtime/processes/managed-writes.js +0 -69
  52. package/dist/workspace-runtime/production/contracts.js +0 -17
  53. package/dist/workspace-runtime/production/controller.js +0 -207
  54. package/dist/workspace-runtime/production/dependencies.js +0 -60
  55. package/dist/workspace-runtime/production/native.js +0 -70
  56. package/dist/workspace-runtime/production/processes.js +0 -86
  57. package/dist/workspace-runtime/production/save-command.js +0 -20
  58. package/dist/workspace-runtime/production/stages.js +0 -175
  59. package/dist/workspace-runtime/production/storage.js +0 -32
  60. package/dist/workspace-runtime/serve.js +0 -199
  61. package/dist/workspace-runtime/server/auth.js +0 -21
  62. package/dist/workspace-runtime/server/connectivity.js +0 -201
  63. package/dist/workspace-runtime/server/desktop-host.js +0 -429
  64. package/dist/workspace-runtime/server/desktop-preview-config.js +0 -81
  65. package/dist/workspace-runtime/server/desktop-preview-service.js +0 -157
  66. package/dist/workspace-runtime/server/desktop-preview-static.js +0 -53
  67. package/dist/workspace-runtime/server/desktop-preview.js +0 -189
  68. package/dist/workspace-runtime/server/dispatch.js +0 -845
  69. package/dist/workspace-runtime/server/endpoints.js +0 -19
  70. package/dist/workspace-runtime/server/execution-group.js +0 -85
  71. package/dist/workspace-runtime/server/http.js +0 -919
  72. package/dist/workspace-runtime/server/lock.js +0 -53
  73. package/dist/workspace-runtime/server/rfb-watch-gate.js +0 -168
  74. package/dist/workspace-runtime/server/terminal-host.js +0 -294
  75. package/dist/workspace-runtime/server/types.js +0 -1
  76. package/dist/workspace-runtime/workspaces/attachments.js +0 -109
  77. package/dist/workspace-runtime/workspaces/batch-commit.js +0 -142
  78. package/dist/workspace-runtime/workspaces/cli-credentials.js +0 -111
  79. package/dist/workspace-runtime/workspaces/context-error.js +0 -2
  80. package/dist/workspace-runtime/workspaces/dependencies.js +0 -264
  81. package/dist/workspace-runtime/workspaces/developer-command.js +0 -64
  82. package/dist/workspace-runtime/workspaces/directories.js +0 -106
  83. package/dist/workspace-runtime/workspaces/directory-access.js +0 -20
  84. package/dist/workspace-runtime/workspaces/directory-maintenance.js +0 -111
  85. package/dist/workspace-runtime/workspaces/directory-stats.js +0 -49
  86. package/dist/workspace-runtime/workspaces/empty-template.js +0 -32
  87. package/dist/workspace-runtime/workspaces/files.js +0 -113
  88. package/dist/workspace-runtime/workspaces/git-credential-broker.js +0 -312
  89. package/dist/workspace-runtime/workspaces/git-credential-helper.js +0 -68
  90. package/dist/workspace-runtime/workspaces/git-direct-store.js +0 -155
  91. package/dist/workspace-runtime/workspaces/git.js +0 -471
  92. package/dist/workspace-runtime/workspaces/json5-edit.js +0 -170
  93. package/dist/workspace-runtime/workspaces/parameters.js +0 -272
  94. package/dist/workspace-runtime/workspaces/prepare.js +0 -263
  95. package/dist/workspace-runtime/workspaces/registry.js +0 -192
  96. package/dist/workspace-runtime/workspaces/revisions.js +0 -31
  97. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +0 -46
  98. package/dist/workspace-runtime/workspaces/skills.js +0 -179
  99. package/project-template-v2/.claude/agents/artist.md +0 -82
  100. package/project-template-v2/.claude/agents/developer.md +0 -153
  101. package/project-template-v2/.claude/agents/game-designer.md +0 -264
  102. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +0 -533
  103. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +0 -153
  104. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +0 -27
  105. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +0 -208
  106. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +0 -81
  107. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +0 -432
  108. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +0 -252
  109. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +0 -266
  110. package/project-template-v2/.claude/agents/refs/framework-mda.md +0 -147
  111. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +0 -123
  112. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +0 -88
  113. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +0 -67
  114. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +0 -120
  115. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +0 -300
  116. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +0 -495
  117. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +0 -1141
  118. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +0 -111
  119. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +0 -365
  120. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +0 -412
  121. package/project-template-v2/.claude/agents/reviewer.md +0 -75
  122. package/project-template-v2/.claude/agents/technical-artist.md +0 -86
  123. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +0 -243
  124. package/project-template-v2/.claude/hooks/user-prompt.mjs +0 -133
  125. package/project-template-v2/.claude/settings.json +0 -33
  126. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +0 -161
  127. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +0 -270
  128. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +0 -177
  129. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +0 -354
  130. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +0 -148
  131. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +0 -56
  132. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +0 -62
  133. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
  134. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +0 -309
  135. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +0 -182
  136. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +0 -123
  137. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +0 -205
  138. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +0 -280
  139. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +0 -184
  140. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +0 -306
  141. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +0 -298
  142. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +0 -123
  143. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +0 -254
  144. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +0 -324
  145. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +0 -97
  146. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +0 -118
  147. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +0 -137
  148. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +0 -397
  149. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +0 -296
  150. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +0 -122
  151. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +0 -219
  152. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +0 -373
  153. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +0 -161
  154. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +0 -215
  155. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +0 -382
  156. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +0 -423
  157. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +0 -158
  158. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +0 -100
  159. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +0 -167
  160. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +0 -270
  161. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +0 -236
  162. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +0 -130
  163. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +0 -390
  164. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +0 -23
  165. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +0 -39
  166. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +0 -87
  167. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +0 -240
  168. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +0 -226
  169. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +0 -308
  170. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +0 -275
  171. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +0 -134
  172. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +0 -142
  173. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +0 -153
  174. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +0 -386
  175. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +0 -182
  176. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +0 -72
  177. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +0 -121
  178. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -102
  179. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +0 -127
  180. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +0 -106
  181. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +0 -128
  182. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +0 -67
  183. package/project-template-v2/.claude/snapshot.mjs +0 -263
  184. package/project-template-v2/.playcraft/skills.lock.json +0 -152
  185. package/project-template-v2/CLAUDE.md +0 -146
  186. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  187. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  188. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  189. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  190. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  191. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  192. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  193. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  194. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  195. package/project-template-v2/assets/models/.gitkeep +0 -0
  196. package/project-template-v2/docs/harness/iteration-1/context-flow.md +0 -254
  197. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +0 -91
  198. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +0 -214
  199. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +0 -75
  200. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +0 -63
  201. package/project-template-v2/docs/harness/iteration-2/context-flow.md +0 -223
  202. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +0 -129
  203. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +0 -267
  204. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +0 -164
  205. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +0 -115
  206. package/project-template-v2/docs/harness/orchestrator-flow.md +0 -364
  207. package/project-template-v2/docs/project-state.json +0 -60
  208. package/project-template-v2/docs/project-state.md +0 -72
  209. package/project-template-v2/docs/standards/README.md +0 -225
  210. package/project-template-v2/docs/standards/agent-behavior-standards.md +0 -174
  211. package/project-template-v2/docs/standards/artifacts/design-brief.md +0 -19
  212. package/project-template-v2/docs/standards/artifacts/design.md +0 -22
  213. package/project-template-v2/docs/standards/artifacts/game-code.md +0 -41
  214. package/project-template-v2/docs/standards/artifacts/todo-list.md +0 -41
  215. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +0 -343
  216. package/project-template-v2/game/index.ts +0 -18
  217. package/project-template-v2/globals.d.ts +0 -51
  218. package/project-template-v2/index.css +0 -34
  219. package/project-template-v2/index.html +0 -18
  220. package/project-template-v2/main.ts +0 -9
  221. package/project-template-v2/package.json +0 -46
  222. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +0 -31
  223. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +0 -86
  224. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +0 -153
  225. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +0 -70
  226. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +0 -91
  227. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +0 -131
  228. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +0 -80
  229. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +0 -90
  230. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +0 -93
  231. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +0 -23
  232. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +0 -230
  233. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +0 -58
  234. package/project-template-v2/skills/_shared/scripts/state-get.mjs +0 -339
  235. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +0 -39
  236. package/project-template-v2/skills/_shared/scripts/state-set.mjs +0 -94
  237. package/project-template-v2/skills/_shared/scripts/state-store.mjs +0 -783
  238. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +0 -88
  239. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +0 -130
  240. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +0 -33
  241. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +0 -47
  242. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +0 -268
  243. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +0 -285
  244. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +0 -161
  245. package/project-template-v2/skills/playable-autoplay/SKILL.md +0 -176
  246. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +0 -4
  247. package/project-template-v2/skills/playable-debug/SKILL.md +0 -116
  248. package/project-template-v2/skills/playable-debug/agents/openai.yaml +0 -4
  249. package/project-template-v2/skills/playable-debug/references/debug-config.md +0 -40
  250. package/project-template-v2/skills/playable-record/SKILL.md +0 -140
  251. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +0 -104
  252. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +0 -141
  253. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +0 -425
  254. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +0 -261
  255. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +0 -398
  256. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +0 -322
  257. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +0 -105
  258. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +0 -230
  259. package/project-template-v2/skills/playable-validate/SKILL.md +0 -61
  260. package/project-template-v2/skills/playable-validate/validation-rules.md +0 -14
  261. package/project-template-v2/skills/playable-verify-ui/SKILL.md +0 -63
  262. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +0 -1
  263. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +0 -218
  264. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +0 -51
  265. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +0 -59
  266. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +0 -106
  267. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +0 -116
  268. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +0 -59
  269. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +0 -64
  270. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +0 -58
  271. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +0 -333
  272. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  273. package/project-template-v2/tsconfig.json +0 -20
  274. package/project-template-v2/vite.config.ts +0 -27
  275. package/project-templates/batch-locks/phaser.yaml +0 -922
  276. package/project-templates/batch-locks/threejs.yaml +0 -978
  277. package/project-templates/empty/AGENTS.md +0 -23
  278. package/project-templates/empty/gitignore +0 -31
@@ -1,919 +0,0 @@
1
- import { readObservedFile, withObservedFile } from '../workspaces/revisions.js';
2
- import { registerWorkspace, revisionScope, sessionWorkspaceRoot, workspaceRoot, WorkspaceContextError } from '../workspaces/registry.js';
3
- import { randomUUID } from 'node:crypto';
4
- import { workspaceRegistrationRequestSchema, HTTP_STATUS_BY_ERROR, assertLeaseFence, buildHandshakeDocument, capability, findUntrustedRuntimeFields, gitFileRefSchema, hashOperationPayload, isLostThreadError, sourceAckSchema, operationAcceptRequestSchema, operationKindSchema, operationScopeKey, resolveIdempotentAccept, nativeHistoryReadSchema, runtimeError, runtimeConnectivityDocumentSchema, unavailableRuntimeCapabilities, } from '@playcraft/common/cloud-workspace/runtime';
5
- import { controlTokenMatches, extractControlToken } from './auth.js';
6
- import { applyRuntimeEndpointPatch } from './endpoints.js';
7
- import { canExecuteKind, executeOperation } from './dispatch.js';
8
- import { InvalidWorkspacePathError, listWorkspaceFiles, } from '../workspaces/files.js';
9
- import { GitWorkspaceError, readWorkspaceGitDiff, readWorkspaceGitFile, readWorkspaceGitGraph, readWorkspaceGitStatus, } from '../workspaces/git.js';
10
- import { readWorkspaceParameters } from '../workspaces/parameters.js';
11
- import { readDependencies } from '../workspaces/dependencies.js';
12
- import { desktopHost, desktopRoute } from './desktop-host.js';
13
- import { ensureCliCredentials } from '../workspaces/cli-credentials.js';
14
- import { readAttachmentUpload, uploadWorkspaceAttachment } from '../workspaces/attachments.js';
15
- import { readWorkspaceSkills } from '../workspaces/skills.js';
16
- import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.js';
17
- import { join } from 'node:path';
18
- import { terminalHost } from './terminal-host.js';
19
- import { productionCapability, productionDirectoryBusy, executeSandboxProduction, sandboxProductionReceipt } from '../production/controller.js';
20
- import { cleanupWorkspaceDirectories, syncWorkspaceDirectories } from '../workspaces/directory-maintenance.js';
21
- import { registerWorkspaceDirectory, restoreWorkspaceDirectory } from '../workspaces/directories.js';
22
- import { readWorkspaceDirectoryStats } from '../workspaces/directory-stats.js';
23
- import { probeRuntimeConnectivity } from './connectivity.js';
24
- export async function handleRuntimeRequest(state, request, response) {
25
- const url = new URL(request.url ?? '/', 'http://127.0.0.1');
26
- const directoryName = url.searchParams.get('directoryName') ?? 'basic';
27
- const access = authorizeRuntime(state, request);
28
- if (!access) {
29
- writeError(response, runtimeError('unauthenticated', 'control_token_required'));
30
- return;
31
- }
32
- if (access === 'read' && !readSourcePath(request.method ?? 'GET', url.pathname)) {
33
- writeJson(response, 403, { error: 'source_read_only' });
34
- return;
35
- }
36
- try {
37
- if (request.method === 'GET' && url.pathname === '/runtime/production/capability') {
38
- writeJson(response, 200, await productionCapability(state));
39
- return;
40
- }
41
- if (request.method === 'POST' && ['/runtime/production/stages', '/runtime/production/stop', '/runtime/production/reconcile'].includes(url.pathname)) {
42
- writeJson(response, 200, await executeSandboxProduction(state, await readJson(request)));
43
- return;
44
- }
45
- if (request.method === 'GET' && url.pathname.startsWith('/runtime/production/stages/')) {
46
- writeJson(response, 200, await sandboxProductionReceipt(state, decodeURIComponent(url.pathname.slice('/runtime/production/stages/'.length)), {
47
- workspaceId: url.searchParams.get('workspaceId') ?? '', directoryName, taskId: url.searchParams.get('taskId') ?? '',
48
- attemptId: url.searchParams.get('attemptId') ?? '', owner: url.searchParams.get('owner') ?? '', fence: Number(url.searchParams.get('fence')),
49
- }));
50
- return;
51
- }
52
- if (request.method === 'GET' && url.pathname === '/runtime/health') {
53
- writeJson(response, 200, { status: 'ok', handshake: handshake(state) });
54
- return;
55
- }
56
- if (request.method === 'GET' && url.pathname === '/runtime/connectivity') {
57
- if (writeDestroyed(state, response))
58
- return;
59
- const parsed = runtimeConnectivityDocumentSchema.shape.workspaceId.safeParse(url.searchParams.get('workspaceId'));
60
- if (!parsed.success) {
61
- writeJson(response, 400, { error: 'invalid_workspace' });
62
- return;
63
- }
64
- const workspaceId = parsed.data;
65
- await sessionWorkspaceRoot(state, workspaceId, directoryName);
66
- writeJson(response, 200, await probeRuntimeConnectivity(state, workspaceId));
67
- return;
68
- }
69
- if (request.method === 'GET' && url.pathname === '/runtime/identity') {
70
- const { runtimeId, userId, environment, generation } = state.config;
71
- writeJson(response, 200, {
72
- runtimeId,
73
- userId,
74
- environment,
75
- generation,
76
- runtimeEpoch: state.runtimeEpoch,
77
- instanceToken: state.instanceToken,
78
- sourceReadToken: state.journal.sourceReadToken(),
79
- workspaceMode: state.config.workspaceMode,
80
- executionGroup: {
81
- replaced: state.journal.executionGroup().replaced,
82
- stopConfirmed: state.journal.executionGroup().stopConfirmed,
83
- },
84
- });
85
- return;
86
- }
87
- if (request.method === 'GET' && url.pathname === '/runtime/approvals') {
88
- writeJson(response, 200, { approvals: state.journal.listApprovals() });
89
- return;
90
- }
91
- if (request.method === 'POST' && url.pathname === '/runtime/workspaces/register') {
92
- if (writeDestroyed(state, response))
93
- return;
94
- const parsed = workspaceRegistrationRequestSchema.safeParse(await readJson(request));
95
- if (!parsed.success || parsed.data.generation !== state.config.generation || parsed.data.runtimeEpoch !== state.runtimeEpoch) {
96
- writeJson(response, 409, { error: 'invalid_workspace_registration' });
97
- return;
98
- }
99
- const binding = await registerWorkspace(state, parsed.data.binding, parsed.data.leaseFence, parsed.data.preparation);
100
- if (parsed.data.cliCredentials)
101
- await ensureCliCredentials(state);
102
- writeJson(response, 200, { workspaceId: binding.workspaceId, status: 'registered' });
103
- return;
104
- }
105
- if (request.method === 'POST' && url.pathname === '/runtime/workspaces/directories/register') {
106
- if (writeDestroyed(state, response))
107
- return;
108
- const body = asRecord(await readJson(request));
109
- if (body.generation !== state.config.generation || body.runtimeEpoch !== state.runtimeEpoch || !Number.isSafeInteger(body.leaseFence)) {
110
- writeJson(response, 409, { error: 'invalid_workspace_registration' });
111
- return;
112
- }
113
- writeJson(response, 200, await registerWorkspaceDirectory(state, body.directory, Number(body.leaseFence)));
114
- return;
115
- }
116
- const maintenance = /^\/runtime\/workspaces\/([a-f0-9-]{36})\/directories\/(cleanup|sync|stats)$/.exec(url.pathname);
117
- if ((request.method === 'GET' || request.method === 'POST') && maintenance?.[2] === 'stats') {
118
- if (writeDestroyed(state, response))
119
- return;
120
- writeJson(response, 200, await readWorkspaceDirectoryStats(state, maintenance[1]));
121
- return;
122
- }
123
- if (request.method === 'POST' && maintenance) {
124
- if (writeDestroyed(state, response))
125
- return;
126
- const body = asRecord(await readJson(request));
127
- const workspaceId = maintenance[1];
128
- writeJson(response, 200, maintenance[2] === 'cleanup'
129
- ? await cleanupWorkspaceDirectories(state, workspaceId, body.directoryNames)
130
- : await syncWorkspaceDirectories(state, workspaceId, body.commitHash, body.targetDirectoryNames));
131
- return;
132
- }
133
- if (request.method === 'POST' && url.pathname === '/runtime/endpoints') {
134
- try {
135
- writeJson(response, 200, await applyRuntimeEndpointPatch(state, await readJson(request)));
136
- }
137
- catch {
138
- writeError(response, runtimeError('invalid_config', 'endpoint_patch_rejected'));
139
- }
140
- return;
141
- }
142
- if (request.method === 'GET' && url.pathname === '/runtime/handshake') {
143
- writeJson(response, 200, handshake(state));
144
- return;
145
- }
146
- const historyPath = sessionHistoryPath(url.pathname);
147
- if (request.method === 'GET' && historyPath) {
148
- await handleSessionHistory(state, response, historyPath.workspaceId, historyPath.sessionId);
149
- return;
150
- }
151
- if (isUnimplementedWorkbenchPath(url.pathname)) {
152
- if (writeDestroyed(state, response))
153
- return;
154
- writeError(response, runtimeError('capability_unavailable', 'capability_not_implemented'));
155
- return;
156
- }
157
- if (url.pathname === '/runtime/events' || url.pathname.startsWith('/runtime/events/')) {
158
- await handleSourceEvents(state, request, response, url);
159
- return;
160
- }
161
- if (request.method === 'POST' && url.pathname === '/runtime/attachments') {
162
- if (writeDestroyed(state, response))
163
- return;
164
- const workspaceId = url.searchParams.get('workspaceId') ?? '';
165
- if (await productionDirectoryBusy(state, workspaceId, directoryName)) {
166
- writeJson(response, 409, { error: 'batch_workspace_readonly' });
167
- return;
168
- }
169
- const root = await workspaceRoot(state, workspaceId, directoryName);
170
- try {
171
- writeJson(response, 200, await uploadWorkspaceAttachment(root, await readAttachmentUpload(request)));
172
- }
173
- catch {
174
- writeJson(response, 400, { error: 'invalid_attachment' });
175
- }
176
- return;
177
- }
178
- if (request.method === 'GET' && url.pathname === '/runtime/files') {
179
- if (writeDestroyed(state, response))
180
- return;
181
- const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
182
- const root = await workspaceRoot(state, workspaceId, directoryName);
183
- const listed = await listWorkspaceFiles(root, state.journal.fileRevisions(revisionScope(state, workspaceId, directoryName)));
184
- console.log(`[workspace-files] GET /runtime/files workspaceId=${workspaceId ?? '-'} root=${root} files=${listed.files.length}`);
185
- writeJson(response, 200, listed);
186
- return;
187
- }
188
- if (request.method === 'GET' && url.pathname === '/runtime/file') {
189
- if (writeDestroyed(state, response))
190
- return;
191
- const relativePath = url.searchParams.get('path') ?? '';
192
- try {
193
- writeJson(response, 200, await readObservedFile(state, url.searchParams.get('workspaceId') ?? undefined, relativePath, directoryName));
194
- }
195
- catch (error) {
196
- if (error instanceof InvalidWorkspacePathError) {
197
- writeJson(response, 400, { error: 'invalid_workspace_path' });
198
- return;
199
- }
200
- if (error.code === 'ENOENT') {
201
- writeJson(response, 404, { error: 'file_not_found' });
202
- return;
203
- }
204
- throw error;
205
- }
206
- return;
207
- }
208
- if (request.method === 'GET' && url.pathname === '/runtime/git/status') {
209
- await writeGitRead(state, response, async () => readWorkspaceGitStatus(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName)));
210
- return;
211
- }
212
- if (request.method === 'GET' && url.pathname === '/runtime/git/diff') {
213
- await writeGitRead(state, response, async () => readWorkspaceGitDiff(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName)));
214
- return;
215
- }
216
- if (request.method === 'GET' && url.pathname === '/runtime/git/file') {
217
- const ref = gitFileRefSchema.safeParse(url.searchParams.get('ref'));
218
- if (!ref.success) {
219
- writeJson(response, 400, { error: 'invalid_git_ref' });
220
- return;
221
- }
222
- await writeGitRead(state, response, async () => readWorkspaceGitFile(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName), url.searchParams.get('path') ?? '', ref.data));
223
- return;
224
- }
225
- if (request.method === 'GET' && url.pathname === '/runtime/dependencies') {
226
- if (writeDestroyed(state, response))
227
- return;
228
- const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
229
- await workspaceRoot(state, workspaceId, directoryName);
230
- writeJson(response, 200, readDependencies(state, revisionScope(state, workspaceId, directoryName) ?? 'workspace'));
231
- return;
232
- }
233
- if (request.method === 'GET' && url.pathname === '/runtime/skills') {
234
- if (writeDestroyed(state, response))
235
- return;
236
- const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
237
- try {
238
- const view = await state.journal.exclusive(`workspace:${revisionScope(state, workspaceId, directoryName) ?? 'single'}`, async () => readWorkspaceSkills(await workspaceRoot(state, workspaceId, directoryName), join(state.config.stateDir, 'skill-artifacts'), entry => loadWorkspaceSkillArtifact(state, workspaceId ?? 'workspace', entry, directoryName)));
239
- writeJson(response, 200, view);
240
- }
241
- catch (error) {
242
- if (error instanceof WorkspaceContextError)
243
- throw error;
244
- writeJson(response, 409, { error: error instanceof Error && error.message.startsWith('workspace_skills_') ? error.message : 'workspace_skills_unavailable' });
245
- }
246
- return;
247
- }
248
- if (request.method === 'GET' && url.pathname === '/runtime/git/graph') {
249
- await writeGitRead(state, response, async () => readWorkspaceGitGraph(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName)));
250
- return;
251
- }
252
- if (request.method === 'GET' && url.pathname === '/runtime/parameters') {
253
- if (writeDestroyed(state, response))
254
- return;
255
- writeJson(response, 200, await readObservedParameters(state, url.searchParams.get('workspaceId') ?? undefined, directoryName));
256
- return;
257
- }
258
- if (request.method === 'GET' && url.pathname === '/runtime/terminals') {
259
- if (writeDestroyed(state, response))
260
- return;
261
- writeJson(response, 200, {
262
- terminals: state.journal.listTerminals(url.searchParams.get('workspaceId') ?? undefined).filter(terminal => (terminal.directoryName ?? 'basic') === directoryName),
263
- });
264
- return;
265
- }
266
- if (url.pathname.startsWith('/runtime/terminals/')) {
267
- await handleTerminalIdentity(state, request, response, url.pathname.slice('/runtime/terminals/'.length));
268
- return;
269
- }
270
- if (desktopRoute(url.pathname)) {
271
- await desktopHost(state).handleHttp(request, response, url);
272
- return;
273
- }
274
- if (request.method === 'GET' && url.pathname === '/runtime/operations') {
275
- const kind = operationKindSchema.safeParse(url.searchParams.get('kind'));
276
- const clientRequestId = url.searchParams.get('clientRequestId');
277
- if (!kind.success || !clientRequestId) {
278
- writeJson(response, 400, { error: 'invalid_operation_lookup' });
279
- return;
280
- }
281
- const operation = state.journal.getOperation(operationScopeKey({
282
- environment: state.config.environment,
283
- userId: state.config.userId,
284
- kind: kind.data,
285
- clientRequestId,
286
- workspaceId: url.searchParams.get('workspaceId') ?? undefined,
287
- sessionId: url.searchParams.get('sessionId') ?? undefined,
288
- }));
289
- writeJson(response, operation ? 200 : 404, operation ?? { error: 'operation_not_found' });
290
- return;
291
- }
292
- if (request.method === 'GET' && url.pathname.startsWith('/runtime/operations/')) {
293
- const operationId = url.pathname.slice('/runtime/operations/'.length);
294
- const operation = state.journal.getOperationById(operationId);
295
- if (!operation) {
296
- writeJson(response, 404, { error: 'operation_not_found' });
297
- return;
298
- }
299
- writeJson(response, 200, operation);
300
- return;
301
- }
302
- if (request.method === 'POST' && url.pathname === '/runtime/operations') {
303
- await acceptOperation(state, request, response);
304
- return;
305
- }
306
- writeJson(response, 404, { error: 'not_found' });
307
- }
308
- catch (error) {
309
- if (error instanceof WorkspaceContextError) {
310
- writeJson(response, 409, { error: error.message });
311
- return;
312
- }
313
- writeJson(response, 500, {
314
- error: 'internal_error',
315
- reason: error instanceof Error ? error.message : 'unknown',
316
- });
317
- }
318
- }
319
- function handshake(state) {
320
- const destroyed = state.journal.destroyed();
321
- const conversationReady = Boolean(state.native?.ready()) && !destroyed;
322
- const filesReady = !destroyed;
323
- const gitReady = state.gitAvailable && !destroyed;
324
- const capabilities = unavailableRuntimeCapabilities('adapter_not_started').map((item) => {
325
- if (item.name === 'conversation') {
326
- if (conversationReady)
327
- return capability('conversation', true);
328
- return capability('conversation', false, destroyed ? 'runtime_destroyed' : state.nativeError ?? 'adapter_not_started');
329
- }
330
- if (item.name === 'files') {
331
- return filesReady
332
- ? capability('files', true)
333
- : capability('files', false, 'runtime_destroyed');
334
- }
335
- if (item.name === 'git') {
336
- if (gitReady)
337
- return capability('git', true);
338
- if (destroyed)
339
- return capability('git', false, 'runtime_destroyed');
340
- if (!state.gitAvailable)
341
- return capability('git', false, 'git_binary_missing');
342
- return item;
343
- }
344
- if (item.name === 'terminal') {
345
- return destroyed
346
- ? capability('terminal', false, 'runtime_destroyed')
347
- : capability('terminal', true);
348
- }
349
- if (item.name === 'desktop') {
350
- if (destroyed)
351
- return capability('desktop', false, 'runtime_destroyed');
352
- const status = desktopHost(state).snapshot().status;
353
- return status === 'ready' ? capability('desktop', true) : capability('desktop', false, status);
354
- }
355
- if (item.name === 'parameters') {
356
- return destroyed
357
- ? capability('parameters', false, 'runtime_destroyed')
358
- : capability('parameters', true);
359
- }
360
- if (item.name === 'artifacts.singleFile') {
361
- return destroyed
362
- ? capability('artifacts.singleFile', false, 'runtime_destroyed')
363
- : capability('artifacts.singleFile', true);
364
- }
365
- return item;
366
- });
367
- return buildHandshakeDocument({
368
- protocolMajor: state.config.protocolMajor,
369
- protocolMinor: state.config.protocolMinor,
370
- cliVersion: state.config.cliVersion,
371
- codexVersion: state.config.codexVersion,
372
- imageDigest: state.config.imageDigest,
373
- phase: destroyed ? 'unavailable' : conversationReady ? 'ready' : 'awaiting_native',
374
- generations: {
375
- generation: state.config.generation,
376
- runtimeEpoch: state.runtimeEpoch,
377
- leaseFence: state.acceptedLeaseFence,
378
- },
379
- capabilities,
380
- });
381
- }
382
- async function acceptOperation(state, request, response) {
383
- const body = await readJson(request);
384
- if (!body || typeof body !== 'object' || Array.isArray(body)) {
385
- writeJson(response, 400, { error: 'invalid_operation' });
386
- return;
387
- }
388
- // leaseFence 由 Runtime Server 派发,本控制面单独校验;其余代次/身份/根目录字段仍只来自引导配置。
389
- const untrusted = findUntrustedRuntimeFields(body).filter((field) => field !== 'leaseFence');
390
- if (untrusted.length > 0) {
391
- writeError(response, runtimeError('untrusted_runtime_field', untrusted.join(',')));
392
- return;
393
- }
394
- const incomingFence = Number(body.leaseFence ?? state.acceptedLeaseFence);
395
- if (!Number.isInteger(incomingFence) || incomingFence < 0) {
396
- writeJson(response, 400, { error: 'invalid_lease_fence' });
397
- return;
398
- }
399
- if (assertLeaseFence(incomingFence, state.acceptedLeaseFence) !== 'ok') {
400
- writeError(response, runtimeError('stale_lease', 'leaseFence is behind the accepted dispatcher'));
401
- return;
402
- }
403
- const { leaseFence: _leaseFence, ...requestBody } = body;
404
- const parsed = operationAcceptRequestSchema.safeParse(requestBody);
405
- if (!parsed.success) {
406
- writeJson(response, 400, { error: 'invalid_operation', issues: parsed.error.issues });
407
- return;
408
- }
409
- if (parsed.data.kind.startsWith('session.') && !parsed.data.sessionId) {
410
- writeJson(response, 400, { error: 'invalid_operation', reason: 'sessionId_required' });
411
- return;
412
- }
413
- if (parsed.data.directoryName === 'root' && !parsed.data.kind.startsWith('session.') && parsed.data.kind !== 'runtime.ensure') {
414
- writeJson(response, 400, { error: 'invalid_workspace_directory' });
415
- return;
416
- }
417
- if (state.config.workspaceMode === 'multi' && parsed.data.kind === 'runtime.ensure' && parsed.data.workspaceId)
418
- await restoreWorkspaceDirectory(state, parsed.data.workspaceId, parsed.data.directoryName);
419
- if (state.config.workspaceMode === 'multi')
420
- await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
421
- const mutation = !['runtime.ensure', 'runtime.destroy', 'session.interrupt', 'session.approval.respond'].includes(parsed.data.kind);
422
- if (mutation && await productionDirectoryBusy(state, parsed.data.workspaceId, parsed.data.directoryName)) {
423
- writeJson(response, 409, { error: 'batch_workspace_readonly' });
424
- return;
425
- }
426
- const payloadHash = hashOperationPayload(parsed.data.payload);
427
- const scopeKey = operationScopeKey({
428
- environment: state.config.environment,
429
- userId: state.config.userId,
430
- kind: parsed.data.kind,
431
- clientRequestId: parsed.data.clientRequestId,
432
- workspaceId: parsed.data.workspaceId,
433
- directoryName: parsed.data.directoryName,
434
- sessionId: parsed.data.sessionId,
435
- });
436
- const accept = () => state.journal.exclusive(scopeKey, async () => {
437
- // Recheck after a concurrent dispatcher may have advanced the fence while this request waited.
438
- if (assertLeaseFence(incomingFence, state.acceptedLeaseFence) !== 'ok') {
439
- writeError(response, runtimeError('stale_lease', 'leaseFence is behind the accepted dispatcher'));
440
- return;
441
- }
442
- const existing = state.journal.getOperation(scopeKey);
443
- const decision = resolveIdempotentAccept(existing, payloadHash);
444
- if (decision.action === 'conflict') {
445
- writeError(response, runtimeError('idempotency_conflict', 'clientRequestId reused with a different payload', {
446
- operationId: decision.operation.operationId,
447
- }));
448
- return;
449
- }
450
- // A turn refused for a lost thread never reached Codex. Sent again with the conversation, it
451
- // runs afresh under the same request, so the message is shown and answered only once.
452
- const retryWithRecovery = decision.action === 'reuse' && parsed.data.kind === 'session.turn'
453
- && Boolean(parsed.data.sessionRecovery) && decision.operation.status === 'failed'
454
- && isLostThreadError(decision.operation.error);
455
- if (decision.action === 'reuse' && !retryWithRecovery) {
456
- if (decision.operation.status === 'uncertain') {
457
- writeError(response, runtimeError('uncertain_operation', decision.operation.error ?? 'native_receipt_lost', {
458
- operationId: decision.operation.operationId,
459
- }));
460
- return;
461
- }
462
- writeJson(response, 200, decision.operation);
463
- return;
464
- }
465
- if (parsed.data.kind === 'session.create') {
466
- const creation = state.journal.findSessionCreation(parsed.data.sessionId);
467
- const session = state.journal.getSession(parsed.data.sessionId);
468
- if (creation || session) {
469
- writeJson(response, 409, { error: 'session_already_bound' });
470
- return;
471
- }
472
- }
473
- else if (parsed.data.kind === 'terminal.create') {
474
- const payload = asRecord(parsed.data.payload);
475
- const terminalId = typeof payload.terminalId === 'string' ? payload.terminalId.trim() : '';
476
- if (!terminalId) {
477
- writeJson(response, 400, { error: 'invalid_operation', reason: 'terminalId_required' });
478
- return;
479
- }
480
- const creation = state.journal.findTerminalCreation(terminalId);
481
- const terminal = state.journal.getTerminal(terminalId);
482
- if (creation && operationScopeKey(creation) !== scopeKey) {
483
- writeJson(response, 409, { error: 'terminal_already_bound' });
484
- return;
485
- }
486
- if (terminal && !creation) {
487
- writeJson(response, 409, { error: 'terminal_already_bound' });
488
- return;
489
- }
490
- }
491
- else if (parsed.data.kind.startsWith('session.') && state.config.workspaceMode === 'multi') {
492
- const session = state.journal.getSession(parsed.data.sessionId);
493
- // A replaced sandbox has no binding yet; the recovering turn creates it.
494
- const rebinding = !session && parsed.data.kind === 'session.turn' && parsed.data.sessionRecovery;
495
- if (!rebinding && (!session || session.workspaceId !== parsed.data.workspaceId
496
- || (session.directoryName ?? 'basic') !== (parsed.data.directoryName ?? 'basic'))) {
497
- writeJson(response, 409, { error: 'session_workspace_mismatch' });
498
- return;
499
- }
500
- }
501
- const now = new Date().toISOString();
502
- const executable = canExecuteKind(state, parsed.data.kind);
503
- const payloadRecord = asRecord(parsed.data.payload);
504
- const operation = {
505
- operationId: randomUUID(),
506
- environment: state.config.environment,
507
- userId: state.config.userId,
508
- clientRequestId: parsed.data.clientRequestId,
509
- kind: parsed.data.kind,
510
- status: executable ? 'queued' : 'failed',
511
- payloadHash,
512
- workspaceId: parsed.data.workspaceId,
513
- directoryName: parsed.data.directoryName,
514
- sessionId: parsed.data.sessionId,
515
- expectedRevision: parsed.data.expectedRevision,
516
- generation: state.config.generation,
517
- runtimeEpoch: state.runtimeEpoch,
518
- leaseFence: incomingFence,
519
- terminalId: parsed.data.kind === 'terminal.create' && typeof payloadRecord.terminalId === 'string'
520
- ? payloadRecord.terminalId.trim()
521
- : undefined,
522
- cwd: parsed.data.kind === 'terminal.create' && typeof payloadRecord.cwd === 'string'
523
- ? payloadRecord.cwd
524
- : undefined,
525
- error: executable ? undefined : 'capability_unavailable',
526
- createdAt: now,
527
- updatedAt: now,
528
- };
529
- if (!executable) {
530
- const saved = await state.journal.putOperation(scopeKey, operation);
531
- writeError(response, runtimeError('capability_unavailable', unavailableReason(state, parsed.data.kind), {
532
- operationId: saved.operationId,
533
- }));
534
- return;
535
- }
536
- state.acceptedLeaseFence = Math.max(state.acceptedLeaseFence, incomingFence);
537
- await state.journal.setAcceptedLeaseFence(state.acceptedLeaseFence);
538
- const queued = await state.journal.putOperation(scopeKey, operation);
539
- const executed = await executeOperation(state, scopeKey, queued, parsed.data.payload, parsed.data.sessionRecovery);
540
- if (executed.status === 'uncertain') {
541
- writeError(response, runtimeError('uncertain_operation', executed.error ?? 'native_receipt_lost', {
542
- operationId: executed.operationId,
543
- }));
544
- return;
545
- }
546
- writeJson(response, executed.status === 'failed' ? 409 : 202, executed);
547
- });
548
- const acceptBound = async () => {
549
- if (mutation && await productionDirectoryBusy(state, parsed.data.workspaceId, parsed.data.directoryName)) {
550
- writeJson(response, 409, { error: 'batch_workspace_readonly' });
551
- return;
552
- }
553
- if (state.config.workspaceMode === 'multi')
554
- await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
555
- if (parsed.data.kind === 'session.create')
556
- await state.journal.exclusive(`session:${parsed.data.sessionId}`, accept);
557
- else if (parsed.data.kind === 'terminal.create') {
558
- const terminalId = typeof asRecord(parsed.data.payload).terminalId === 'string'
559
- ? String(asRecord(parsed.data.payload).terminalId).trim()
560
- : '';
561
- if (terminalId)
562
- await state.journal.exclusive(`terminal:${terminalId}`, accept);
563
- else
564
- await accept();
565
- }
566
- else
567
- await accept();
568
- };
569
- // Production admission and maintenance use this same lock before checking durable occupancy.
570
- if (mutation && state.config.workspaceMode === 'multi')
571
- await state.journal.exclusive(`directory:${parsed.data.workspaceId}:${parsed.data.directoryName ?? 'basic'}`, acceptBound);
572
- else
573
- await acceptBound();
574
- }
575
- async function writeGitRead(state, response, read) {
576
- if (writeDestroyed(state, response))
577
- return;
578
- if (!state.gitAvailable) {
579
- writeError(response, runtimeError('capability_unavailable', 'git_binary_missing'));
580
- return;
581
- }
582
- try {
583
- writeJson(response, 200, await read());
584
- }
585
- catch (error) {
586
- if (error instanceof GitWorkspaceError && error.code === 'invalid_workspace_path') {
587
- writeJson(response, 400, { error: 'invalid_workspace_path' });
588
- return;
589
- }
590
- const reason = error instanceof GitWorkspaceError ? error.code : 'git_command_failed';
591
- writeError(response, runtimeError('capability_unavailable', reason));
592
- }
593
- }
594
- async function readJson(request) {
595
- const chunks = [];
596
- for await (const chunk of request) {
597
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
598
- }
599
- if (chunks.length === 0)
600
- return {};
601
- try {
602
- return JSON.parse(Buffer.concat(chunks).toString('utf8'));
603
- }
604
- catch {
605
- return null;
606
- }
607
- }
608
- function writeDestroyed(state, response) {
609
- if (!state.journal.destroyed())
610
- return false;
611
- writeError(response, runtimeError('capability_unavailable', 'runtime_destroyed'));
612
- return true;
613
- }
614
- async function handleTerminalIdentity(state, request, response, remainder) {
615
- if (writeDestroyed(state, response))
616
- return;
617
- const [terminalId, action] = remainder.split('/');
618
- if (!terminalId || action && action !== 'connect') {
619
- writeJson(response, 404, { error: 'not_found' });
620
- return;
621
- }
622
- const terminal = state.journal.getTerminal(terminalId);
623
- if (!terminal) {
624
- writeJson(response, 404, { error: 'terminal_not_found' });
625
- return;
626
- }
627
- if (request.method === 'GET' && action === 'connect') {
628
- if (terminal.status === 'destroyed') {
629
- writeError(response, runtimeError('capability_unavailable', 'terminal_destroyed'));
630
- return;
631
- }
632
- writeError(response, runtimeError('capability_unavailable', 'websocket_required'));
633
- return;
634
- }
635
- if (request.method === 'GET' && !action) {
636
- writeJson(response, 200, terminal);
637
- return;
638
- }
639
- if (request.method === 'DELETE' && !action) {
640
- const destroyed = terminal.status === 'destroyed'
641
- ? terminal
642
- : await state.journal.putTerminal({
643
- ...terminal,
644
- status: 'destroyed',
645
- destroyedAt: new Date().toISOString(),
646
- });
647
- terminalHost(state).revoke(destroyed.terminalId, 'terminal_destroyed');
648
- writeJson(response, 200, { terminalId: destroyed.terminalId, status: destroyed.status });
649
- return;
650
- }
651
- writeJson(response, 404, { error: 'not_found' });
652
- }
653
- function unavailableReason(state, kind) {
654
- if (state.journal.destroyed())
655
- return 'runtime_destroyed';
656
- return state.nativeError ?? 'adapter_not_started';
657
- }
658
- function sessionHistoryPath(pathname) {
659
- const match = pathname.match(/^\/runtime\/workspaces\/([^/]+)\/sessions\/([^/]+)\/history$/);
660
- if (!match?.[1] || !match[2])
661
- return undefined;
662
- try {
663
- return {
664
- workspaceId: decodeURIComponent(match[1]),
665
- sessionId: decodeURIComponent(match[2]),
666
- };
667
- }
668
- catch {
669
- return undefined;
670
- }
671
- }
672
- function isUnimplementedWorkbenchPath(pathname) {
673
- const parts = pathname.split('/').filter(Boolean);
674
- return parts[0] === 'runtime' && parts.includes('previews');
675
- }
676
- async function handleSessionHistory(state, response, workspaceId, sessionId) {
677
- if (writeDestroyed(state, response))
678
- return;
679
- const session = state.journal.getSession(sessionId);
680
- if (!session || (session.workspaceId && session.workspaceId !== workspaceId)) {
681
- writeJson(response, 404, { error: 'session_not_found' });
682
- return;
683
- }
684
- if (state.config.workspaceMode === 'multi' && session.workspaceId !== workspaceId) {
685
- writeJson(response, 404, { error: 'session_not_found' });
686
- return;
687
- }
688
- const readThread = state.native?.readThread;
689
- if (!readThread) {
690
- writeError(response, runtimeError('capability_unavailable', 'native_history_unavailable'));
691
- return;
692
- }
693
- let read;
694
- try {
695
- read = await readThread({ threadId: session.threadId, sessionId });
696
- }
697
- catch {
698
- writeError(response, runtimeError('capability_unavailable', 'native_history_unavailable'));
699
- return;
700
- }
701
- if (!read || read.threadId !== session.threadId) {
702
- writeError(response, runtimeError('capability_unavailable', 'native_history_unavailable'));
703
- return;
704
- }
705
- const body = nativeHistoryReadSchema.safeParse({
706
- source: 'native',
707
- sessionId,
708
- workspaceId,
709
- threadId: read.threadId,
710
- ...(read.threadName ? { threadName: read.threadName } : {}),
711
- truncated: read.truncated,
712
- items: read.items,
713
- });
714
- if (!body.success) {
715
- writeError(response, runtimeError('capability_unavailable', 'native_history_unavailable'));
716
- return;
717
- }
718
- writeJson(response, 200, body.data);
719
- }
720
- async function readObservedParameters(state, workspaceId, directoryName = 'basic') {
721
- const root = await workspaceRoot(state, workspaceId, directoryName);
722
- const scope = revisionScope(state, workspaceId, directoryName);
723
- const first = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
724
- for (const path of [first.schemaPath, first.dataPath]) {
725
- if (path)
726
- await withObservedFile(state, workspaceId, path, async () => undefined, directoryName);
727
- }
728
- return readWorkspaceParameters(root, state.journal.fileRevisions(scope));
729
- }
730
- function writeError(response, error) {
731
- writeJson(response, HTTP_STATUS_BY_ERROR[error.code], error);
732
- }
733
- function writeJson(response, status, body) {
734
- const payload = JSON.stringify(body);
735
- response.writeHead(status, {
736
- 'content-type': 'application/json; charset=utf-8',
737
- 'content-length': Buffer.byteLength(payload),
738
- });
739
- response.end(payload);
740
- }
741
- function authorizeRuntime(state, request) {
742
- const presented = extractControlToken(request.headers);
743
- if (controlTokenMatches(state.config.controlToken, presented))
744
- return 'control';
745
- const dedicated = headerValue(request.headers['x-playcraft-source-read-token']);
746
- const candidate = dedicated || presented;
747
- if (candidate && controlTokenMatches(state.journal.sourceReadToken(), candidate))
748
- return 'read';
749
- return null;
750
- }
751
- function readSourcePath(method, pathname) {
752
- if (method === 'GET' && pathname === '/runtime/events')
753
- return true;
754
- if (method === 'POST' && pathname === '/runtime/events/replay-pins')
755
- return true;
756
- if (method === 'DELETE' && pathname.startsWith('/runtime/events/replay-pins/'))
757
- return true;
758
- return false;
759
- }
760
- async function handleSourceEvents(state, request, response, url) {
761
- if (request.method === 'POST' && url.pathname === '/runtime/events/replay-pins') {
762
- const pin = await state.journal.createReplayPin({
763
- runtimeId: state.config.runtimeId,
764
- generation: state.config.generation,
765
- runtimeEpoch: state.runtimeEpoch,
766
- ttlMs: Number(asRecord(await readJson(request)).ttlMs) || undefined,
767
- });
768
- writeJson(response, 201, pin);
769
- return;
770
- }
771
- if (request.method === 'DELETE' && url.pathname.startsWith('/runtime/events/replay-pins/')) {
772
- await state.journal.deleteReplayPin(decodeURIComponent(url.pathname.slice('/runtime/events/replay-pins/'.length)));
773
- writeJson(response, 200, { deleted: true });
774
- return;
775
- }
776
- if (request.method === 'POST' && url.pathname === '/runtime/events/ack') {
777
- const parsed = sourceAckSchema.safeParse(await readJson(request));
778
- if (!parsed.success) {
779
- writeJson(response, 400, { error: 'invalid_source_ack' });
780
- return;
781
- }
782
- try {
783
- await state.journal.acknowledgeSource(parsed.data);
784
- }
785
- catch (error) {
786
- writeJson(response, 409, { code: error instanceof Error ? error.message : 'ack_rejected' });
787
- return;
788
- }
789
- writeJson(response, 200, { ok: true, acceptedLeaseFence: state.journal.acceptedLeaseFence() });
790
- return;
791
- }
792
- if (request.method === 'GET' && url.pathname === '/runtime/events') {
793
- const after = Number(url.searchParams.get('after') ?? '0');
794
- const safeAfter = Number.isInteger(after) && after >= 0 ? after : 0;
795
- const epochQuery = url.searchParams.get('runtimeEpoch');
796
- const runtimeEpoch = epochQuery === null ? state.runtimeEpoch : Number(epochQuery);
797
- const query = {
798
- runtimeId: state.config.runtimeId,
799
- generation: state.config.generation,
800
- runtimeEpoch: Number.isInteger(runtimeEpoch) ? runtimeEpoch : state.runtimeEpoch,
801
- after: safeAfter,
802
- };
803
- if (String(request.headers.accept ?? '').includes('text/event-stream')) {
804
- writeSourceStream(state, request, response, query);
805
- return;
806
- }
807
- if (epochQuery === null) {
808
- const events = state.journal.listEvents(safeAfter);
809
- const page = state.journal.readSourcePage({ ...query, limit: 1 });
810
- writeJson(response, 200, {
811
- events,
812
- retainedFrom: page.ok ? page.retainedFrom : page.retainedFrom,
813
- highWater: page.ok ? page.highWater : page.highWater,
814
- nextAfter: events.at(-1)?.source.sourceSequence ?? safeAfter,
815
- runtimeId: query.runtimeId,
816
- generation: query.generation,
817
- runtimeEpoch: query.runtimeEpoch,
818
- });
819
- return;
820
- }
821
- const page = state.journal.readSourcePage(query);
822
- if (!page.ok) {
823
- writeJson(response, 409, {
824
- code: page.code,
825
- retainedFrom: page.retainedFrom,
826
- highWater: page.highWater,
827
- coveredThrough: page.coveredThrough,
828
- });
829
- return;
830
- }
831
- writeJson(response, 200, {
832
- events: page.events,
833
- retainedFrom: page.retainedFrom,
834
- highWater: page.highWater,
835
- nextAfter: page.nextAfter,
836
- runtimeId: page.runtimeId,
837
- generation: page.generation,
838
- runtimeEpoch: page.runtimeEpoch,
839
- });
840
- return;
841
- }
842
- writeJson(response, 404, { error: 'not_found' });
843
- }
844
- function writeSourceStream(state, request, response, query) {
845
- let cursor = query.after;
846
- const buffered = [];
847
- const matches = (event) => event.source.runtimeId === query.runtimeId &&
848
- event.source.generation === query.generation &&
849
- event.source.runtimeEpoch === query.runtimeEpoch &&
850
- event.source.sourceSequence > cursor;
851
- const unsubscribe = state.journal.onSourceEvent((event) => {
852
- if (matches(event))
853
- buffered.push(event);
854
- });
855
- const page = state.journal.readSourcePage({ ...query, limit: 500 });
856
- response.writeHead(200, {
857
- 'content-type': 'text/event-stream; charset=utf-8',
858
- 'cache-control': 'no-cache, no-transform',
859
- connection: 'keep-alive',
860
- 'x-accel-buffering': 'no',
861
- });
862
- const writeEvent = (name, data) => {
863
- response.write(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
864
- };
865
- if (!page.ok) {
866
- writeEvent('source.gap', {
867
- code: page.code,
868
- retainedFrom: page.retainedFrom,
869
- highWater: page.highWater,
870
- coveredThrough: page.coveredThrough,
871
- });
872
- response.end();
873
- unsubscribe();
874
- return;
875
- }
876
- writeEvent('source.ready', {
877
- runtimeId: page.runtimeId,
878
- generation: page.generation,
879
- runtimeEpoch: page.runtimeEpoch,
880
- retainedFrom: page.retainedFrom,
881
- highWater: page.highWater,
882
- nextAfter: page.nextAfter,
883
- });
884
- for (const event of page.events) {
885
- writeEvent('source', event);
886
- cursor = event.source.sourceSequence;
887
- }
888
- for (const event of buffered) {
889
- if (!matches(event))
890
- continue;
891
- writeEvent('source', event);
892
- cursor = event.source.sourceSequence;
893
- }
894
- unsubscribe();
895
- const follow = state.journal.onSourceEvent((event) => {
896
- if (!matches(event))
897
- return;
898
- writeEvent('source', event);
899
- cursor = event.source.sourceSequence;
900
- });
901
- const heartbeat = setInterval(() => {
902
- response.write(`: ping\n\n`);
903
- }, 10_000);
904
- heartbeat.unref?.();
905
- const stop = () => {
906
- clearInterval(heartbeat);
907
- follow();
908
- };
909
- request.on('close', stop);
910
- response.on('close', stop);
911
- }
912
- function headerValue(value) {
913
- return Array.isArray(value) ? value[0] : value;
914
- }
915
- function asRecord(value) {
916
- return value && typeof value === 'object' && !Array.isArray(value)
917
- ? value
918
- : {};
919
- }