@opengsd/gsd-pi 1.7.0 → 1.8.1-dev.897b3c90

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 (465) hide show
  1. package/README.md +8 -6
  2. package/dist/cli.js +5 -1
  3. package/dist/headless-events.d.ts +2 -1
  4. package/dist/headless-events.js +11 -3
  5. package/dist/headless-milestone-readiness.d.ts +29 -0
  6. package/dist/headless-milestone-readiness.js +161 -0
  7. package/dist/headless.js +25 -2
  8. package/dist/print-mode-exit.d.ts +12 -0
  9. package/dist/print-mode-exit.js +14 -0
  10. package/dist/resources/.managed-resources-content-hash +1 -1
  11. package/dist/resources/GSD-WORKFLOW.md +16 -47
  12. package/dist/resources/extensions/browser-tools/capture.js +2 -77
  13. package/dist/resources/extensions/browser-tools/screenshot-constraints.js +83 -0
  14. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -2
  15. package/dist/resources/extensions/claude-code-cli/turn-assembler.js +30 -2
  16. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -19
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -0
  19. package/dist/resources/extensions/gsd/auto-prompts.js +35 -1
  20. package/dist/resources/extensions/gsd/auto.js +5 -1
  21. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +78 -4
  22. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -26
  23. package/dist/resources/extensions/gsd/command-feedback.js +16 -12
  24. package/dist/resources/extensions/gsd/compat/compat-marker.js +19 -2
  25. package/dist/resources/extensions/gsd/db/command-queue.js +7 -2
  26. package/dist/resources/extensions/gsd/db/engine.js +51 -16
  27. package/dist/resources/extensions/gsd/db/writers/cascades.js +5 -1
  28. package/dist/resources/extensions/gsd/db/writers/status.js +13 -3
  29. package/dist/resources/extensions/gsd/db-base-schema.js +28 -0
  30. package/dist/resources/extensions/gsd/db-migration-steps.js +30 -0
  31. package/dist/resources/extensions/gsd/db-schema-metadata.js +4 -1
  32. package/dist/resources/extensions/gsd/docs/preferences-reference.md +4 -4
  33. package/dist/resources/extensions/gsd/doctor-history.js +65 -0
  34. package/dist/resources/extensions/gsd/doctor-state-checks.js +509 -0
  35. package/dist/resources/extensions/gsd/doctor-workspace-checks.js +91 -0
  36. package/dist/resources/extensions/gsd/doctor.js +14 -653
  37. package/dist/resources/extensions/gsd/flat-phase-migration.js +156 -21
  38. package/dist/resources/extensions/gsd/gsd-db.js +187 -45
  39. package/dist/resources/extensions/gsd/guided-flow.js +29 -10
  40. package/dist/resources/extensions/gsd/markdown-renderer.js +16 -2
  41. package/dist/resources/extensions/gsd/md-importer.js +109 -16
  42. package/dist/resources/extensions/gsd/migrate/command.js +19 -162
  43. package/dist/resources/extensions/gsd/migrate/execution.js +98 -0
  44. package/dist/resources/extensions/gsd/migrate/plan.js +49 -0
  45. package/dist/resources/extensions/gsd/migrate/planning-writer.js +20 -19
  46. package/dist/resources/extensions/gsd/migrate/presentation.js +64 -0
  47. package/dist/resources/extensions/gsd/migration-auto-check.js +4 -0
  48. package/dist/resources/extensions/gsd/parallel-eligibility.js +5 -4
  49. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +28 -43
  50. package/dist/resources/extensions/gsd/paths.js +4 -1
  51. package/dist/resources/extensions/gsd/phase-anchor.js +7 -2
  52. package/dist/resources/extensions/gsd/post-execution-checks.js +4 -4
  53. package/dist/resources/extensions/gsd/prompts/refine-slice.md +2 -2
  54. package/dist/resources/extensions/gsd/safety/evidence-collector.js +26 -1
  55. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +30 -5
  56. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +27 -13
  57. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +13 -3
  58. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +54 -14
  59. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +48 -5
  60. package/dist/resources/extensions/gsd/stop-notice.js +15 -1
  61. package/dist/resources/extensions/gsd/sync-lock.js +13 -7
  62. package/dist/resources/extensions/gsd/tools/complete-task.js +82 -1
  63. package/dist/resources/extensions/gsd/tools/plan-task.js +13 -15
  64. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +18 -15
  65. package/dist/resources/extensions/gsd/tools/replan-task.js +128 -0
  66. package/dist/resources/extensions/gsd/tools/rework-brief.js +74 -0
  67. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -0
  68. package/dist/resources/extensions/gsd/unit-registry.js +4 -0
  69. package/dist/resources/extensions/gsd/uok/plan-v2.js +19 -3
  70. package/dist/resources/extensions/gsd/worktree-state-projection.js +6 -0
  71. package/dist/resources/extensions/mac-tools/index.js +31 -9
  72. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  73. package/dist/web/standalone/.next/BUILD_ID +1 -1
  74. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  75. package/dist/web/standalone/.next/build-manifest.json +3 -3
  76. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  77. package/dist/web/standalone/.next/required-server-files.json +3 -3
  78. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  79. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  89. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  91. package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -4
  92. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -4
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  98. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  105. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  117. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  139. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  144. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  145. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  147. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  148. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  149. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  150. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  151. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  152. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  153. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  154. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  155. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  158. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  161. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  163. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  164. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  166. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  168. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  169. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  170. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  171. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  172. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  173. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  174. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  175. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  176. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  177. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  178. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  179. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  180. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  181. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  182. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  183. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  184. package/dist/web/standalone/.next/server/app/index.html +1 -1
  185. package/dist/web/standalone/.next/server/app/index.rsc +5 -5
  186. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  187. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +5 -5
  188. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  189. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -4
  190. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  191. package/dist/web/standalone/.next/server/app/page.js +2 -2
  192. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  193. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  194. package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
  195. package/dist/web/standalone/.next/server/chunks/8357.js +3 -3
  196. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  197. package/dist/web/standalone/.next/server/middleware.js +3 -3
  198. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  199. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  200. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  201. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
  202. package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
  203. package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
  204. package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
  205. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
  206. package/dist/web/standalone/.next/static/css/3b1ceb7abfb9a230.css +1 -0
  207. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  208. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  209. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  210. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  211. package/dist/web/standalone/package.json +0 -1
  212. package/dist/web/standalone/server.js +1 -1
  213. package/dist/web-mode.js +31 -3
  214. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  215. package/integrations/hermes/pyproject.toml +1 -1
  216. package/package.json +12 -10
  217. package/packages/cloud-mcp-gateway/package.json +2 -2
  218. package/packages/contracts/dist/workflow.d.ts +14 -0
  219. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  220. package/packages/contracts/dist/workflow.js +16 -0
  221. package/packages/contracts/dist/workflow.js.map +1 -1
  222. package/packages/contracts/package.json +1 -1
  223. package/packages/daemon/dist/cloud-config.d.ts +1 -1
  224. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  225. package/packages/daemon/dist/cloud-config.js +30 -8
  226. package/packages/daemon/dist/cloud-config.js.map +1 -1
  227. package/packages/daemon/dist/cloud-config.test.js +69 -1
  228. package/packages/daemon/dist/cloud-config.test.js.map +1 -1
  229. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -1
  230. package/packages/daemon/dist/local-tool-executor.js +2 -0
  231. package/packages/daemon/dist/local-tool-executor.js.map +1 -1
  232. package/packages/daemon/package.json +6 -6
  233. package/packages/gsd-agent-core/package.json +5 -5
  234. package/packages/gsd-agent-modes/package.json +7 -7
  235. package/packages/gsd-cloud/README.md +18 -4
  236. package/packages/gsd-cloud/bin/gsd-cloud.js +6 -5
  237. package/packages/gsd-cloud/dist/cli.d.ts +5 -0
  238. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -0
  239. package/packages/gsd-cloud/dist/cli.js +160 -0
  240. package/packages/gsd-cloud/dist/cli.js.map +1 -0
  241. package/packages/gsd-cloud/dist/cloud-config.d.ts +19 -0
  242. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -0
  243. package/packages/gsd-cloud/dist/cloud-config.js +231 -0
  244. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -0
  245. package/packages/gsd-cloud/dist/cloud-config.test.d.ts +2 -0
  246. package/packages/gsd-cloud/dist/cloud-config.test.d.ts.map +1 -0
  247. package/packages/gsd-cloud/dist/cloud-config.test.js +118 -0
  248. package/packages/gsd-cloud/dist/cloud-config.test.js.map +1 -0
  249. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.d.ts +2 -0
  250. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.d.ts.map +1 -0
  251. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.js +37 -0
  252. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.js.map +1 -0
  253. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +35 -0
  254. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -0
  255. package/packages/gsd-cloud/dist/cloud-runtime.js +258 -0
  256. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -0
  257. package/packages/gsd-cloud/dist/cloud-runtime.test.d.ts +2 -0
  258. package/packages/gsd-cloud/dist/cloud-runtime.test.d.ts.map +1 -0
  259. package/packages/gsd-cloud/dist/cloud-runtime.test.js +88 -0
  260. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -0
  261. package/packages/gsd-cloud/dist/cloud-token.d.ts +3 -0
  262. package/packages/gsd-cloud/dist/cloud-token.d.ts.map +1 -0
  263. package/packages/gsd-cloud/dist/cloud-token.js +37 -0
  264. package/packages/gsd-cloud/dist/cloud-token.js.map +1 -0
  265. package/packages/gsd-cloud/dist/config.d.ts +17 -0
  266. package/packages/gsd-cloud/dist/config.d.ts.map +1 -0
  267. package/packages/gsd-cloud/dist/config.js +146 -0
  268. package/packages/gsd-cloud/dist/config.js.map +1 -0
  269. package/packages/gsd-cloud/dist/device-flow.d.ts +24 -0
  270. package/packages/gsd-cloud/dist/device-flow.d.ts.map +1 -0
  271. package/packages/gsd-cloud/dist/device-flow.js +128 -0
  272. package/packages/gsd-cloud/dist/device-flow.js.map +1 -0
  273. package/packages/gsd-cloud/dist/device-flow.test.d.ts +2 -0
  274. package/packages/gsd-cloud/dist/device-flow.test.d.ts.map +1 -0
  275. package/packages/gsd-cloud/dist/device-flow.test.js +155 -0
  276. package/packages/gsd-cloud/dist/device-flow.test.js.map +1 -0
  277. package/packages/gsd-cloud/dist/executors/claude-executor.d.ts +6 -0
  278. package/packages/gsd-cloud/dist/executors/claude-executor.d.ts.map +1 -0
  279. package/packages/gsd-cloud/dist/executors/claude-executor.js +15 -0
  280. package/packages/gsd-cloud/dist/executors/claude-executor.js.map +1 -0
  281. package/packages/gsd-cloud/dist/executors/codex-executor.d.ts +6 -0
  282. package/packages/gsd-cloud/dist/executors/codex-executor.d.ts.map +1 -0
  283. package/packages/gsd-cloud/dist/executors/codex-executor.js +15 -0
  284. package/packages/gsd-cloud/dist/executors/codex-executor.js.map +1 -0
  285. package/packages/gsd-cloud/dist/executors/executor.d.ts +28 -0
  286. package/packages/gsd-cloud/dist/executors/executor.d.ts.map +1 -0
  287. package/packages/gsd-cloud/dist/executors/executor.js +11 -0
  288. package/packages/gsd-cloud/dist/executors/executor.js.map +1 -0
  289. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.d.ts +34 -0
  290. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.d.ts.map +1 -0
  291. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.js +179 -0
  292. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.js.map +1 -0
  293. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.d.ts +2 -0
  294. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.d.ts.map +1 -0
  295. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.js +44 -0
  296. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.js.map +1 -0
  297. package/packages/gsd-cloud/dist/executors/index.d.ts +17 -0
  298. package/packages/gsd-cloud/dist/executors/index.d.ts.map +1 -0
  299. package/packages/gsd-cloud/dist/executors/index.js +29 -0
  300. package/packages/gsd-cloud/dist/executors/index.js.map +1 -0
  301. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.d.ts +36 -0
  302. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.d.ts.map +1 -0
  303. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.js +176 -0
  304. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.js.map +1 -0
  305. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.d.ts +2 -0
  306. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.d.ts.map +1 -0
  307. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.js +34 -0
  308. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.js.map +1 -0
  309. package/packages/gsd-cloud/dist/index.d.ts +7 -0
  310. package/packages/gsd-cloud/dist/index.d.ts.map +1 -0
  311. package/packages/gsd-cloud/dist/index.js +8 -0
  312. package/packages/gsd-cloud/dist/index.js.map +1 -0
  313. package/packages/gsd-cloud/dist/inject-gateway.d.ts +16 -0
  314. package/packages/gsd-cloud/dist/inject-gateway.d.ts.map +1 -0
  315. package/packages/gsd-cloud/dist/inject-gateway.js +29 -0
  316. package/packages/gsd-cloud/dist/inject-gateway.js.map +1 -0
  317. package/packages/gsd-cloud/dist/inject-gateway.test.d.ts +2 -0
  318. package/packages/gsd-cloud/dist/inject-gateway.test.d.ts.map +1 -0
  319. package/packages/gsd-cloud/dist/inject-gateway.test.js +41 -0
  320. package/packages/gsd-cloud/dist/inject-gateway.test.js.map +1 -0
  321. package/packages/gsd-cloud/dist/logger.d.ts +25 -0
  322. package/packages/gsd-cloud/dist/logger.d.ts.map +1 -0
  323. package/packages/gsd-cloud/dist/logger.js +72 -0
  324. package/packages/gsd-cloud/dist/logger.js.map +1 -0
  325. package/packages/gsd-cloud/dist/types.d.ts +51 -0
  326. package/packages/gsd-cloud/dist/types.d.ts.map +1 -0
  327. package/packages/gsd-cloud/dist/types.js +9 -0
  328. package/packages/gsd-cloud/dist/types.js.map +1 -0
  329. package/packages/gsd-cloud/package.json +7 -5
  330. package/packages/mcp-server/README.md +8 -0
  331. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  332. package/packages/mcp-server/dist/workflow-tools.js +64 -2
  333. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  334. package/packages/mcp-server/package.json +4 -4
  335. package/packages/native/package.json +1 -1
  336. package/packages/pi-agent-core/package.json +1 -1
  337. package/packages/pi-ai/dist/models.generated.d.ts +60 -0
  338. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  339. package/packages/pi-ai/dist/models.generated.js +54 -0
  340. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  341. package/packages/pi-ai/dist/providers/amazon-bedrock.js +1 -0
  342. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  343. package/packages/pi-ai/dist/providers/anthropic.js +19 -0
  344. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  345. package/packages/pi-ai/package.json +1 -1
  346. package/packages/pi-coding-agent/package.json +7 -7
  347. package/packages/pi-tui/package.json +2 -2
  348. package/packages/rpc-client/package.json +2 -2
  349. package/pkg/package.json +1 -1
  350. package/src/resources/GSD-WORKFLOW.md +16 -47
  351. package/src/resources/extensions/browser-tools/capture.ts +2 -86
  352. package/src/resources/extensions/browser-tools/screenshot-constraints.ts +94 -0
  353. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -1
  354. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +108 -0
  355. package/src/resources/extensions/claude-code-cli/turn-assembler.ts +30 -2
  356. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  357. package/src/resources/extensions/gsd/auto-dispatch.ts +86 -19
  358. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -0
  359. package/src/resources/extensions/gsd/auto-prompts.ts +38 -1
  360. package/src/resources/extensions/gsd/auto.ts +5 -1
  361. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +91 -4
  362. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -24
  363. package/src/resources/extensions/gsd/command-feedback.ts +19 -12
  364. package/src/resources/extensions/gsd/compat/compat-marker.ts +21 -4
  365. package/src/resources/extensions/gsd/db/command-queue.ts +7 -1
  366. package/src/resources/extensions/gsd/db/engine.ts +53 -17
  367. package/src/resources/extensions/gsd/db/writers/cascades.ts +5 -1
  368. package/src/resources/extensions/gsd/db/writers/status.ts +25 -8
  369. package/src/resources/extensions/gsd/db-base-schema.ts +30 -0
  370. package/src/resources/extensions/gsd/db-migration-steps.ts +32 -0
  371. package/src/resources/extensions/gsd/db-schema-metadata.ts +4 -1
  372. package/src/resources/extensions/gsd/docs/preferences-reference.md +4 -4
  373. package/src/resources/extensions/gsd/doctor-history.ts +81 -0
  374. package/src/resources/extensions/gsd/doctor-state-checks.ts +527 -0
  375. package/src/resources/extensions/gsd/doctor-workspace-checks.ts +97 -0
  376. package/src/resources/extensions/gsd/doctor.ts +14 -673
  377. package/src/resources/extensions/gsd/flat-phase-migration.ts +160 -19
  378. package/src/resources/extensions/gsd/gsd-db.ts +255 -52
  379. package/src/resources/extensions/gsd/guided-flow.ts +39 -10
  380. package/src/resources/extensions/gsd/markdown-renderer.ts +23 -7
  381. package/src/resources/extensions/gsd/md-importer.ts +116 -13
  382. package/src/resources/extensions/gsd/migrate/command.ts +21 -227
  383. package/src/resources/extensions/gsd/migrate/execution.ts +136 -0
  384. package/src/resources/extensions/gsd/migrate/plan.ts +101 -0
  385. package/src/resources/extensions/gsd/migrate/planning-writer.ts +21 -23
  386. package/src/resources/extensions/gsd/migrate/presentation.ts +83 -0
  387. package/src/resources/extensions/gsd/migration-auto-check.ts +3 -0
  388. package/src/resources/extensions/gsd/parallel-eligibility.ts +5 -4
  389. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +37 -40
  390. package/src/resources/extensions/gsd/paths.ts +4 -1
  391. package/src/resources/extensions/gsd/phase-anchor.ts +8 -2
  392. package/src/resources/extensions/gsd/post-execution-checks.ts +4 -4
  393. package/src/resources/extensions/gsd/prompts/refine-slice.md +2 -2
  394. package/src/resources/extensions/gsd/safety/evidence-collector.ts +28 -1
  395. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +31 -5
  396. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +30 -13
  397. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +16 -3
  398. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +67 -20
  399. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +47 -5
  400. package/src/resources/extensions/gsd/state-reconciliation/types.ts +2 -2
  401. package/src/resources/extensions/gsd/stop-notice.ts +18 -1
  402. package/src/resources/extensions/gsd/sync-lock.ts +14 -6
  403. package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +96 -1
  404. package/src/resources/extensions/gsd/tests/command-feedback.test.ts +12 -0
  405. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +104 -0
  406. package/src/resources/extensions/gsd/tests/complete-task.test.ts +286 -0
  407. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +76 -1
  408. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +186 -0
  409. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +29 -0
  410. package/src/resources/extensions/gsd/tests/discuss-routing-fixes.test.ts +123 -0
  411. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +50 -0
  412. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +49 -0
  413. package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +25 -0
  414. package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +81 -0
  415. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +22 -0
  416. package/src/resources/extensions/gsd/tests/external-edit-scoped-import.test.ts +262 -0
  417. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +202 -0
  418. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +29 -0
  419. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +25 -0
  420. package/src/resources/extensions/gsd/tests/import-slice-milestone-completion-preserve.test.ts +116 -0
  421. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +53 -0
  422. package/src/resources/extensions/gsd/tests/migrate-plan.test.ts +68 -0
  423. package/src/resources/extensions/gsd/tests/migrate-presentation.test.ts +98 -0
  424. package/src/resources/extensions/gsd/tests/migrate-safety-audit.test.ts +1 -1
  425. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +53 -0
  426. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +55 -0
  427. package/src/resources/extensions/gsd/tests/parallel-eligibility-file-overlap.test.ts +146 -0
  428. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +59 -0
  429. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -0
  430. package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +32 -4
  431. package/src/resources/extensions/gsd/tests/plan-task.test.ts +26 -23
  432. package/src/resources/extensions/gsd/tests/planning-writer.test.ts +47 -1
  433. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +22 -0
  434. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +18 -3
  435. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +50 -0
  436. package/src/resources/extensions/gsd/tests/replan-task-handler.test.ts +103 -0
  437. package/src/resources/extensions/gsd/tests/rework-brief.test.ts +197 -0
  438. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +21 -0
  439. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +177 -25
  440. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +17 -0
  441. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +19 -0
  442. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +34 -0
  443. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +93 -1
  444. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -0
  445. package/src/resources/extensions/gsd/tests/workflow-migration-gates.test.ts +139 -0
  446. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +36 -0
  447. package/src/resources/extensions/gsd/tests/write-gate-seam.test.ts +52 -0
  448. package/src/resources/extensions/gsd/tools/complete-task.ts +120 -0
  449. package/src/resources/extensions/gsd/tools/plan-task.ts +14 -16
  450. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +18 -17
  451. package/src/resources/extensions/gsd/tools/replan-task.ts +160 -0
  452. package/src/resources/extensions/gsd/tools/rework-brief.ts +91 -0
  453. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +92 -0
  454. package/src/resources/extensions/gsd/types.ts +11 -0
  455. package/src/resources/extensions/gsd/unit-registry.ts +4 -0
  456. package/src/resources/extensions/gsd/uok/plan-v2.ts +20 -3
  457. package/src/resources/extensions/gsd/worktree-state-projection.ts +12 -0
  458. package/src/resources/extensions/mac-tools/index.ts +52 -9
  459. package/src/resources/extensions/mac-tools/tests/mac-screenshot-payload.test.cjs +70 -0
  460. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
  461. package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
  462. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
  463. package/dist/web/standalone/.next/static/css/18c2fdf261bfc1b7.css +0 -1
  464. /package/dist/web/standalone/.next/static/{enWQFRY-aLTUq3wmxG2Qz → TezjdjSHBVRRr54XjAvte}/_buildManifest.js +0 -0
  465. /package/dist/web/standalone/.next/static/{enWQFRY-aLTUq3wmxG2Qz → TezjdjSHBVRRr54XjAvte}/_ssgManifest.js +0 -0
@@ -0,0 +1,160 @@
1
+ // Project/App: Open GSD
2
+ // File Purpose: Standalone gsd-cloud CLI — login / status / connect / disconnect.
3
+ //
4
+ // Wires device-flow + CloudRuntime + a selected Executor adapter DIRECTLY. It has
5
+ // no dependency on any Daemon or Orchestrator class; the only cloud behaviour is
6
+ // the WS relay client driving the local GSD runtime through the Executor seam.
7
+ import { parseArgs } from "node:util";
8
+ import { resolveConfigPath, loadConfig } from "./config.js";
9
+ import { Logger } from "./logger.js";
10
+ import { clearCloudConfig, exchangePairingCode, redactedCloudStatus, saveCloudConfig, } from "./cloud-config.js";
11
+ import { runDeviceFlow } from "./device-flow.js";
12
+ import { CloudRuntime } from "./cloud-runtime.js";
13
+ import { selectExecutor } from "./executors/index.js";
14
+ export async function handleCloudCommand(argv, opts) {
15
+ if (argv[0] === "--help" || argv[0] === "-h") {
16
+ process.stdout.write(formatUsage(opts.binaryName));
17
+ process.exit(0);
18
+ }
19
+ const command = argv[0];
20
+ const { values } = parseArgs({
21
+ args: argv.slice(1),
22
+ options: {
23
+ config: { type: "string", short: "c" },
24
+ gateway: { type: "string" },
25
+ code: { type: "string" },
26
+ "runtime-name": { type: "string" },
27
+ verbose: { type: "boolean", short: "v", default: false },
28
+ help: { type: "boolean", short: "h", default: false },
29
+ },
30
+ strict: true,
31
+ });
32
+ if (values.help || !command) {
33
+ process.stdout.write(formatUsage(opts.binaryName));
34
+ process.exit(0);
35
+ }
36
+ const configPath = resolveConfigPath(values.config);
37
+ if (command === "status") {
38
+ process.stdout.write(`${JSON.stringify(redactedCloudStatus(loadConfig(configPath)), null, 2)}\n`);
39
+ return;
40
+ }
41
+ if (command === "disconnect") {
42
+ clearCloudConfig(configPath);
43
+ process.stdout.write(`${opts.binaryName}: cloud runtime disconnected locally.\n`);
44
+ return;
45
+ }
46
+ if (command === "login") {
47
+ if (!values.gateway) {
48
+ throw new Error("login requires --gateway");
49
+ }
50
+ const runtimeName = values["runtime-name"];
51
+ const { deviceToken, runtimeId, gatewayUrl } = await runDeviceFlow({
52
+ gatewayUrl: values.gateway,
53
+ configPath,
54
+ runtimeName,
55
+ binaryName: opts.binaryName,
56
+ });
57
+ const config = saveCloudConfig(configPath, {
58
+ gateway_url: gatewayUrl,
59
+ device_token: deviceToken,
60
+ runtime_id: runtimeId,
61
+ ...(runtimeName ? { runtime_name: runtimeName } : {}),
62
+ enabled: true,
63
+ });
64
+ process.stdout.write(`${opts.binaryName}: cloud runtime ${runtimeId} paired — connecting...\n`);
65
+ await runCloudRuntime(config, opts.binaryName, values.verbose);
66
+ return;
67
+ }
68
+ if (command === "pair") {
69
+ if (!values.gateway || !values.code) {
70
+ throw new Error("pair requires --gateway and --code");
71
+ }
72
+ const runtimeName = values["runtime-name"];
73
+ const result = await exchangePairingCode({
74
+ gatewayUrl: values.gateway,
75
+ code: values.code,
76
+ runtimeName,
77
+ });
78
+ saveCloudConfig(configPath, {
79
+ gateway_url: values.gateway,
80
+ device_token: result.deviceToken,
81
+ runtime_id: result.runtimeId,
82
+ ...(runtimeName ? { runtime_name: runtimeName } : {}),
83
+ enabled: true,
84
+ });
85
+ process.stdout.write(`${opts.binaryName}: paired cloud runtime ${result.runtimeId}.\n`);
86
+ return;
87
+ }
88
+ if (command === "connect") {
89
+ const config = loadConfig(configPath);
90
+ if (!config.cloud?.device_token || !config.cloud.runtime_id) {
91
+ throw new Error("cloud runtime is not paired; run `login` first");
92
+ }
93
+ await runCloudRuntime(config, opts.binaryName, values.verbose);
94
+ return;
95
+ }
96
+ throw new Error(`Unknown cloud runtime command: ${command}`);
97
+ }
98
+ /**
99
+ * Start the WS relay and block until the process is signalled. This is the whole
100
+ * "daemon" for the standalone agent: one CloudRuntime + one Executor, no Discord,
101
+ * no scanner, no session-manager class.
102
+ */
103
+ async function runCloudRuntime(config, binaryName, verbose) {
104
+ if (!config.cloud)
105
+ throw new Error("cloud runtime is not configured");
106
+ if (config.cloud.enabled === false) {
107
+ throw new Error("cloud runtime is disabled in config; set cloud.enabled to true to connect");
108
+ }
109
+ const logger = new Logger({
110
+ filePath: config.log.file,
111
+ level: config.log.level,
112
+ verbose,
113
+ });
114
+ const executor = selectExecutor(logger);
115
+ const runtime = new CloudRuntime(config.cloud, executor, logger);
116
+ await runtime.start();
117
+ process.stdout.write(`${binaryName}: connected to ${config.cloud.gateway_url}. Press Ctrl+C to stop.\n`);
118
+ await new Promise((resolve) => {
119
+ const shutdown = () => {
120
+ runtime.stop();
121
+ void Promise.resolve(executor.close?.()).finally(() => {
122
+ void logger.close().finally(() => resolve());
123
+ });
124
+ };
125
+ process.once("SIGINT", shutdown);
126
+ process.once("SIGTERM", shutdown);
127
+ });
128
+ }
129
+ export function formatUsage(binaryName) {
130
+ return `Usage: ${binaryName} login [--gateway <url>] [--runtime-name <name>] [--config <path>]
131
+ ${binaryName} status [--config <path>]
132
+ ${binaryName} pair --gateway <url> --code <code> [--runtime-name <name>] [--config <path>]
133
+ ${binaryName} connect [--config <path>] [--verbose]
134
+ ${binaryName} disconnect [--config <path>]
135
+
136
+ Commands:
137
+ login (Recommended) Browser-based pairing — opens an approval URL in the
138
+ terminal, polls for authorization, then auto-connects. Defaults to
139
+ the public GSD Cloud gateway.
140
+ status Show current cloud runtime configuration and connection status.
141
+ pair Exchange a pairing code for a device token (headless/CI environments).
142
+ connect Connect using a previously paired device token.
143
+ disconnect Remove cloud runtime configuration from the local config file.
144
+
145
+ Options:
146
+ --config <path> Path to YAML config file (default: ~/.gsd/daemon.yaml)
147
+ --gateway <url> Cloud gateway URL (login defaults to https://cloud.opengsd.net)
148
+ --code <code> Pairing code from the gateway (pair only)
149
+ --runtime-name <name> Friendly name for this local GSD runtime
150
+ --verbose Print log entries to stderr in addition to the log file
151
+ --help Show this help message and exit
152
+
153
+ Environment:
154
+ GSD_CLOUD_PROJECTS Path-delimiter separated project dirs to advertise
155
+ (default: current working directory)
156
+ GSD_CLI_PATH Path to the gsd binary (default: gsd on PATH)
157
+ GSD_CLOUD_EXECUTOR Backend adapter: gsd-pi (default), codex, claude
158
+ `;
159
+ }
160
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["// Project/App: Open GSD\n// File Purpose: Standalone gsd-cloud CLI — login / status / connect / disconnect.\n//\n// Wires device-flow + CloudRuntime + a selected Executor adapter DIRECTLY. It has\n// no dependency on any Daemon or Orchestrator class; the only cloud behaviour is\n// the WS relay client driving the local GSD runtime through the Executor seam.\n\nimport { parseArgs } from \"node:util\";\nimport { resolveConfigPath, loadConfig } from \"./config.js\";\nimport { Logger } from \"./logger.js\";\nimport {\n clearCloudConfig,\n exchangePairingCode,\n redactedCloudStatus,\n saveCloudConfig,\n} from \"./cloud-config.js\";\nimport { runDeviceFlow } from \"./device-flow.js\";\nimport { CloudRuntime } from \"./cloud-runtime.js\";\nimport { selectExecutor } from \"./executors/index.js\";\nimport type { DaemonConfig } from \"./types.js\";\n\nexport async function handleCloudCommand(argv: string[], opts: {\n binaryName: string;\n}): Promise<void> {\n if (argv[0] === \"--help\" || argv[0] === \"-h\") {\n process.stdout.write(formatUsage(opts.binaryName));\n process.exit(0);\n }\n\n const command = argv[0];\n const { values } = parseArgs({\n args: argv.slice(1),\n options: {\n config: { type: \"string\", short: \"c\" },\n gateway: { type: \"string\" },\n code: { type: \"string\" },\n \"runtime-name\": { type: \"string\" },\n verbose: { type: \"boolean\", short: \"v\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n strict: true,\n });\n\n if (values.help || !command) {\n process.stdout.write(formatUsage(opts.binaryName));\n process.exit(0);\n }\n\n const configPath = resolveConfigPath(values.config);\n\n if (command === \"status\") {\n process.stdout.write(`${JSON.stringify(redactedCloudStatus(loadConfig(configPath)), null, 2)}\\n`);\n return;\n }\n\n if (command === \"disconnect\") {\n clearCloudConfig(configPath);\n process.stdout.write(`${opts.binaryName}: cloud runtime disconnected locally.\\n`);\n return;\n }\n\n if (command === \"login\") {\n if (!values.gateway) {\n throw new Error(\"login requires --gateway\");\n }\n const runtimeName = values[\"runtime-name\"];\n const { deviceToken, runtimeId, gatewayUrl } = await runDeviceFlow({\n gatewayUrl: values.gateway,\n configPath,\n runtimeName,\n binaryName: opts.binaryName,\n });\n const config = saveCloudConfig(configPath, {\n gateway_url: gatewayUrl,\n device_token: deviceToken,\n runtime_id: runtimeId,\n ...(runtimeName ? { runtime_name: runtimeName } : {}),\n enabled: true,\n });\n process.stdout.write(`${opts.binaryName}: cloud runtime ${runtimeId} paired — connecting...\\n`);\n await runCloudRuntime(config, opts.binaryName, values.verbose);\n return;\n }\n\n if (command === \"pair\") {\n if (!values.gateway || !values.code) {\n throw new Error(\"pair requires --gateway and --code\");\n }\n const runtimeName = values[\"runtime-name\"];\n const result = await exchangePairingCode({\n gatewayUrl: values.gateway,\n code: values.code,\n runtimeName,\n });\n saveCloudConfig(configPath, {\n gateway_url: values.gateway,\n device_token: result.deviceToken,\n runtime_id: result.runtimeId,\n ...(runtimeName ? { runtime_name: runtimeName } : {}),\n enabled: true,\n });\n process.stdout.write(`${opts.binaryName}: paired cloud runtime ${result.runtimeId}.\\n`);\n return;\n }\n\n if (command === \"connect\") {\n const config = loadConfig(configPath);\n if (!config.cloud?.device_token || !config.cloud.runtime_id) {\n throw new Error(\"cloud runtime is not paired; run `login` first\");\n }\n await runCloudRuntime(config, opts.binaryName, values.verbose);\n return;\n }\n\n throw new Error(`Unknown cloud runtime command: ${command}`);\n}\n\n/**\n * Start the WS relay and block until the process is signalled. This is the whole\n * \"daemon\" for the standalone agent: one CloudRuntime + one Executor, no Discord,\n * no scanner, no session-manager class.\n */\nasync function runCloudRuntime(config: DaemonConfig, binaryName: string, verbose: boolean): Promise<void> {\n if (!config.cloud) throw new Error(\"cloud runtime is not configured\");\n if (config.cloud.enabled === false) {\n throw new Error(\"cloud runtime is disabled in config; set cloud.enabled to true to connect\");\n }\n const logger = new Logger({\n filePath: config.log.file,\n level: config.log.level,\n verbose,\n });\n const executor = selectExecutor(logger);\n const runtime = new CloudRuntime(config.cloud, executor, logger);\n await runtime.start();\n process.stdout.write(`${binaryName}: connected to ${config.cloud.gateway_url}. Press Ctrl+C to stop.\\n`);\n\n await new Promise<void>((resolve) => {\n const shutdown = () => {\n runtime.stop();\n void Promise.resolve(executor.close?.()).finally(() => {\n void logger.close().finally(() => resolve());\n });\n };\n process.once(\"SIGINT\", shutdown);\n process.once(\"SIGTERM\", shutdown);\n });\n}\n\nexport function formatUsage(binaryName: string): string {\n return `Usage: ${binaryName} login [--gateway <url>] [--runtime-name <name>] [--config <path>]\n ${binaryName} status [--config <path>]\n ${binaryName} pair --gateway <url> --code <code> [--runtime-name <name>] [--config <path>]\n ${binaryName} connect [--config <path>] [--verbose]\n ${binaryName} disconnect [--config <path>]\n\nCommands:\n login (Recommended) Browser-based pairing — opens an approval URL in the\n terminal, polls for authorization, then auto-connects. Defaults to\n the public GSD Cloud gateway.\n status Show current cloud runtime configuration and connection status.\n pair Exchange a pairing code for a device token (headless/CI environments).\n connect Connect using a previously paired device token.\n disconnect Remove cloud runtime configuration from the local config file.\n\nOptions:\n --config <path> Path to YAML config file (default: ~/.gsd/daemon.yaml)\n --gateway <url> Cloud gateway URL (login defaults to https://cloud.opengsd.net)\n --code <code> Pairing code from the gateway (pair only)\n --runtime-name <name> Friendly name for this local GSD runtime\n --verbose Print log entries to stderr in addition to the log file\n --help Show this help message and exit\n\nEnvironment:\n GSD_CLOUD_PROJECTS Path-delimiter separated project dirs to advertise\n (default: current working directory)\n GSD_CLI_PATH Path to the gsd binary (default: gsd on PATH)\n GSD_CLOUD_EXECUTOR Backend adapter: gsd-pi (default), codex, claude\n`;\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { LookupFunction } from "node:net";
2
+ import type { DaemonConfig } from "./types.js";
3
+ export interface PairingExchangeResult {
4
+ runtimeId: string;
5
+ deviceToken: string;
6
+ }
7
+ export declare function exchangePairingCode(params: {
8
+ gatewayUrl: string;
9
+ code: string;
10
+ runtimeName?: string;
11
+ }): Promise<PairingExchangeResult>;
12
+ export declare function parseCloudGatewayUrl(value: string): URL;
13
+ export declare function saveCloudConfig(configPath: string, nextCloud: NonNullable<DaemonConfig["cloud"]>): DaemonConfig;
14
+ export declare function clearCloudConfig(configPath: string): DaemonConfig;
15
+ export declare function redactedCloudStatus(config: DaemonConfig): Record<string, unknown>;
16
+ export declare function validateGatewayNetworkTarget(url: URL): void;
17
+ export declare function createGatewayLookup(url: URL): LookupFunction;
18
+ export declare function postJsonToValidatedGateway(url: URL, payload: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;
19
+ //# sourceMappingURL=cloud-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.d.ts","sourceRoot":"","sources":["../src/cloud-config.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAK/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAUjC;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CA2BvD;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,YAAY,CAgB/G;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,CAWjE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWjF;AAeD,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAK3D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAkC5D;AAiCD,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,GAAE,MAAmC,GAC7C,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsClC"}
@@ -0,0 +1,231 @@
1
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { lookup } from "node:dns";
3
+ import { request as httpRequest } from "node:http";
4
+ import { request as httpsRequest } from "node:https";
5
+ import { isIP } from "node:net";
6
+ import { dirname } from "node:path";
7
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
8
+ import { loadConfig } from "./config.js";
9
+ import { protectCloudDeviceToken } from "./cloud-token.js";
10
+ export async function exchangePairingCode(params) {
11
+ const pairingUrl = new URL("/pairing/exchange", parseCloudGatewayUrl(params.gatewayUrl));
12
+ const body = await postJsonToValidatedGateway(pairingUrl, {
13
+ code: params.code,
14
+ runtimeName: params.runtimeName,
15
+ });
16
+ if (typeof body.runtimeId !== "string" || typeof body.deviceToken !== "string") {
17
+ throw new Error("Pairing response did not include runtimeId and deviceToken");
18
+ }
19
+ return { runtimeId: body.runtimeId, deviceToken: body.deviceToken };
20
+ }
21
+ export function parseCloudGatewayUrl(value) {
22
+ let url;
23
+ try {
24
+ url = new URL(value);
25
+ }
26
+ catch {
27
+ throw new Error("Cloud gateway URL must be an absolute HTTP(S) URL");
28
+ }
29
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
30
+ throw new Error("Cloud gateway URL must use http or https");
31
+ }
32
+ if (url.username || url.password) {
33
+ throw new Error("Cloud gateway URL must not include credentials");
34
+ }
35
+ if (url.hash) {
36
+ throw new Error("Cloud gateway URL must not include a fragment");
37
+ }
38
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
39
+ throw new Error("Plain HTTP cloud gateway URLs are only allowed for localhost");
40
+ }
41
+ if (url.protocol === "https:" && isPrivateIpHost(url.hostname)) {
42
+ throw new Error("Cloud gateway URL must not target private or loopback IP addresses");
43
+ }
44
+ url.pathname = url.pathname.replace(/\/+$/, "");
45
+ url.search = "";
46
+ return url;
47
+ }
48
+ export function saveCloudConfig(configPath, nextCloud) {
49
+ let raw = {};
50
+ try {
51
+ raw = parseYaml(readFileSync(configPath, "utf-8")) ?? {};
52
+ }
53
+ catch {
54
+ raw = {};
55
+ }
56
+ const { device_token: deviceToken, ...cloud } = nextCloud;
57
+ raw.cloud = {
58
+ ...cloud,
59
+ gateway_url: parseCloudGatewayUrl(nextCloud.gateway_url).toString(),
60
+ ...(deviceToken ? { device_token_encrypted: protectCloudDeviceToken(deviceToken) } : {}),
61
+ };
62
+ mkdirSync(dirname(configPath), { recursive: true });
63
+ writeConfigFile(configPath, stringifyYaml(raw));
64
+ return loadConfig(configPath);
65
+ }
66
+ export function clearCloudConfig(configPath) {
67
+ let raw = {};
68
+ try {
69
+ raw = parseYaml(readFileSync(configPath, "utf-8")) ?? {};
70
+ }
71
+ catch {
72
+ raw = {};
73
+ }
74
+ delete raw.cloud;
75
+ mkdirSync(dirname(configPath), { recursive: true });
76
+ writeConfigFile(configPath, stringifyYaml(raw));
77
+ return loadConfig(configPath);
78
+ }
79
+ export function redactedCloudStatus(config) {
80
+ const cloud = config.cloud;
81
+ if (!cloud)
82
+ return { configured: false };
83
+ return {
84
+ configured: true,
85
+ enabled: cloud.enabled ?? true,
86
+ gateway_url: cloud.gateway_url,
87
+ runtime_id: cloud.runtime_id ?? null,
88
+ runtime_name: cloud.runtime_name ?? null,
89
+ ["device_" + "token"]: cloud.device_token ? "[redacted]" : null,
90
+ };
91
+ }
92
+ function isLoopbackHost(hostname) {
93
+ const host = hostname.toLowerCase();
94
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
95
+ }
96
+ function isPrivateIpHost(hostname) {
97
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
98
+ if (host === "localhost")
99
+ return true;
100
+ if (isIP(host) === 4)
101
+ return isPrivateIpv4(host);
102
+ if (isIP(host) === 6)
103
+ return isPrivateIpv6(host);
104
+ return false;
105
+ }
106
+ export function validateGatewayNetworkTarget(url) {
107
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname))
108
+ return;
109
+ if (isPrivateIpHost(url.hostname)) {
110
+ throw new Error("Cloud gateway URL must not target private or loopback IP addresses");
111
+ }
112
+ }
113
+ export function createGatewayLookup(url) {
114
+ const allowLoopback = url.protocol === "http:" && isLoopbackHost(url.hostname);
115
+ const rejected = (address) => !address || (!allowLoopback && isPrivateIpHost(address));
116
+ // Node's socket connect (autoSelectFamily / Happy Eyeballs, default-on since
117
+ // Node 20) calls a custom lookup with `all: true` and expects an ARRAY
118
+ // callback. Forcing `all: false` + a scalar callback made every real gateway
119
+ // request throw "Invalid IP address: undefined". Honor both forms; the SSRF
120
+ // guard is applied to every resolved address (reject the whole lookup if any
121
+ // is private/loopback — an attacker-controlled resolver must not slip one in).
122
+ return ((hostname, options, callback) => {
123
+ const wantsAll = typeof options === "object" && options !== null && options.all === true;
124
+ const base = typeof options === "number" ? { family: options } : { ...options };
125
+ if (wantsAll) {
126
+ lookup(hostname, { ...base, all: true }, (err, addresses) => {
127
+ if (err)
128
+ return callback(err);
129
+ const list = addresses;
130
+ if (!list.length || list.some((entry) => rejected(entry.address))) {
131
+ return callback(new Error("Cloud gateway URL resolved to a private or loopback address"));
132
+ }
133
+ callback(null, list);
134
+ });
135
+ return;
136
+ }
137
+ lookup(hostname, { ...base, all: false }, (err, address, family) => {
138
+ if (err)
139
+ return callback(err, address, family);
140
+ if (rejected(address)) {
141
+ return callback(new Error("Cloud gateway URL resolved to a private or loopback address"), address, family);
142
+ }
143
+ callback(null, address, family);
144
+ });
145
+ });
146
+ }
147
+ function isPrivateIpv4(host) {
148
+ const octets = host.split(".").map((part) => Number(part));
149
+ if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
150
+ return true;
151
+ const [a, b] = octets;
152
+ if (a === 10 || a === 127 || a === 0)
153
+ return true;
154
+ if (a === 169 && b === 254)
155
+ return true;
156
+ if (a === 172 && b >= 16 && b <= 31)
157
+ return true;
158
+ if (a === 192 && b === 168)
159
+ return true;
160
+ if (a === 100 && b >= 64 && b <= 127)
161
+ return true;
162
+ if (a === 192 && b === 0)
163
+ return true;
164
+ if (a === 198 && (b === 18 || b === 19))
165
+ return true;
166
+ if (a >= 224)
167
+ return true;
168
+ return false;
169
+ }
170
+ function isPrivateIpv6(host) {
171
+ return host === "::"
172
+ || host === "::1"
173
+ || host.startsWith("fc")
174
+ || host.startsWith("fd")
175
+ || host.startsWith("fe80:")
176
+ || host.startsWith("2001:db8:");
177
+ }
178
+ function writeConfigFile(configPath, contents) {
179
+ writeFileSync(configPath, contents, { encoding: "utf-8", mode: 0o600 });
180
+ chmodSync(configPath, 0o600);
181
+ }
182
+ const GATEWAY_REQUEST_TIMEOUT_MS = 30_000;
183
+ export function postJsonToValidatedGateway(url, payload, timeoutMs = GATEWAY_REQUEST_TIMEOUT_MS) {
184
+ validateGatewayNetworkTarget(url);
185
+ const body = JSON.stringify(payload);
186
+ const requestImpl = url.protocol === "https:" ? httpsRequest : httpRequest;
187
+ return new Promise((resolve, reject) => {
188
+ const req = requestImpl({
189
+ protocol: url.protocol,
190
+ hostname: url.hostname,
191
+ port: url.port,
192
+ path: `${url.pathname}${url.search}`,
193
+ method: "POST",
194
+ headers: {
195
+ "content-type": "application/json",
196
+ "content-length": Buffer.byteLength(body),
197
+ },
198
+ lookup: createGatewayLookup(url),
199
+ }, (res) => {
200
+ const statusCode = res.statusCode ?? 0;
201
+ const chunks = [];
202
+ res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
203
+ res.on("end", () => {
204
+ const responseText = Buffer.concat(chunks).toString("utf-8");
205
+ const parsed = parseJsonObject(responseText);
206
+ if (statusCode < 200 || statusCode >= 300) {
207
+ reject(new Error(typeof parsed.error === "string" ? parsed.error : `Pairing failed with HTTP ${statusCode}`));
208
+ return;
209
+ }
210
+ resolve(parsed);
211
+ });
212
+ });
213
+ req.setTimeout(timeoutMs, () => {
214
+ req.destroy(new Error(`Gateway request timed out after ${timeoutMs}ms`));
215
+ });
216
+ req.on("error", reject);
217
+ req.end(body);
218
+ });
219
+ }
220
+ function parseJsonObject(value) {
221
+ try {
222
+ const parsed = JSON.parse(value);
223
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
224
+ ? parsed
225
+ : {};
226
+ }
227
+ catch {
228
+ return {};
229
+ }
230
+ }
231
+ //# sourceMappingURL=cloud-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.js","sourceRoot":"","sources":["../src/cloud-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { lookup } from \"node:dns\";\nimport type { LookupOptions } from \"node:dns\";\nimport { request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { isIP } from \"node:net\";\nimport type { LookupFunction } from \"node:net\";\nimport { dirname } from \"node:path\";\nimport { parse as parseYaml, stringify as stringifyYaml } from \"yaml\";\nimport { loadConfig } from \"./config.js\";\nimport { protectCloudDeviceToken } from \"./cloud-token.js\";\nimport type { DaemonConfig } from \"./types.js\";\n\nexport interface PairingExchangeResult {\n runtimeId: string;\n deviceToken: string;\n}\n\nexport async function exchangePairingCode(params: {\n gatewayUrl: string;\n code: string;\n runtimeName?: string;\n}): Promise<PairingExchangeResult> {\n const pairingUrl = new URL(\"/pairing/exchange\", parseCloudGatewayUrl(params.gatewayUrl));\n const body = await postJsonToValidatedGateway(pairingUrl, {\n code: params.code,\n runtimeName: params.runtimeName,\n });\n if (typeof body.runtimeId !== \"string\" || typeof body.deviceToken !== \"string\") {\n throw new Error(\"Pairing response did not include runtimeId and deviceToken\");\n }\n return { runtimeId: body.runtimeId, deviceToken: body.deviceToken };\n}\n\nexport function parseCloudGatewayUrl(value: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new Error(\"Cloud gateway URL must be an absolute HTTP(S) URL\");\n }\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(\"Cloud gateway URL must use http or https\");\n }\n if (url.username || url.password) {\n throw new Error(\"Cloud gateway URL must not include credentials\");\n }\n if (url.hash) {\n throw new Error(\"Cloud gateway URL must not include a fragment\");\n }\n if (url.protocol === \"http:\" && !isLoopbackHost(url.hostname)) {\n throw new Error(\"Plain HTTP cloud gateway URLs are only allowed for localhost\");\n }\n if (url.protocol === \"https:\" && isPrivateIpHost(url.hostname)) {\n throw new Error(\"Cloud gateway URL must not target private or loopback IP addresses\");\n }\n\n url.pathname = url.pathname.replace(/\\/+$/, \"\");\n url.search = \"\";\n return url;\n}\n\nexport function saveCloudConfig(configPath: string, nextCloud: NonNullable<DaemonConfig[\"cloud\"]>): DaemonConfig {\n let raw: Record<string, unknown> = {};\n try {\n raw = parseYaml(readFileSync(configPath, \"utf-8\")) as Record<string, unknown> ?? {};\n } catch {\n raw = {};\n }\n const { device_token: deviceToken, ...cloud } = nextCloud;\n raw.cloud = {\n ...cloud,\n gateway_url: parseCloudGatewayUrl(nextCloud.gateway_url).toString(),\n ...(deviceToken ? { device_token_encrypted: protectCloudDeviceToken(deviceToken) } : {}),\n };\n mkdirSync(dirname(configPath), { recursive: true });\n writeConfigFile(configPath, stringifyYaml(raw));\n return loadConfig(configPath);\n}\n\nexport function clearCloudConfig(configPath: string): DaemonConfig {\n let raw: Record<string, unknown> = {};\n try {\n raw = parseYaml(readFileSync(configPath, \"utf-8\")) as Record<string, unknown> ?? {};\n } catch {\n raw = {};\n }\n delete raw.cloud;\n mkdirSync(dirname(configPath), { recursive: true });\n writeConfigFile(configPath, stringifyYaml(raw));\n return loadConfig(configPath);\n}\n\nexport function redactedCloudStatus(config: DaemonConfig): Record<string, unknown> {\n const cloud = config.cloud;\n if (!cloud) return { configured: false };\n return {\n configured: true,\n enabled: cloud.enabled ?? true,\n gateway_url: cloud.gateway_url,\n runtime_id: cloud.runtime_id ?? null,\n runtime_name: cloud.runtime_name ?? null,\n [\"device_\" + \"token\"]: cloud.device_token ? \"[redacted]\" : null,\n };\n}\n\nfunction isLoopbackHost(hostname: string): boolean {\n const host = hostname.toLowerCase();\n return host === \"localhost\" || host === \"127.0.0.1\" || host === \"::1\" || host === \"[::1]\";\n}\n\nfunction isPrivateIpHost(hostname: string): boolean {\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n if (host === \"localhost\") return true;\n if (isIP(host) === 4) return isPrivateIpv4(host);\n if (isIP(host) === 6) return isPrivateIpv6(host);\n return false;\n}\n\nexport function validateGatewayNetworkTarget(url: URL): void {\n if (url.protocol === \"http:\" && isLoopbackHost(url.hostname)) return;\n if (isPrivateIpHost(url.hostname)) {\n throw new Error(\"Cloud gateway URL must not target private or loopback IP addresses\");\n }\n}\n\nexport function createGatewayLookup(url: URL): LookupFunction {\n const allowLoopback = url.protocol === \"http:\" && isLoopbackHost(url.hostname);\n const rejected = (address: string): boolean =>\n !address || (!allowLoopback && isPrivateIpHost(address));\n // Node's socket connect (autoSelectFamily / Happy Eyeballs, default-on since\n // Node 20) calls a custom lookup with `all: true` and expects an ARRAY\n // callback. Forcing `all: false` + a scalar callback made every real gateway\n // request throw \"Invalid IP address: undefined\". Honor both forms; the SSRF\n // guard is applied to every resolved address (reject the whole lookup if any\n // is private/loopback — an attacker-controlled resolver must not slip one in).\n return ((hostname: string, options: unknown, callback: (...args: unknown[]) => void) => {\n const wantsAll =\n typeof options === \"object\" && options !== null && (options as LookupOptions).all === true;\n const base: LookupOptions =\n typeof options === \"number\" ? { family: options } : { ...(options as LookupOptions) };\n if (wantsAll) {\n lookup(hostname, { ...base, all: true }, (err, addresses) => {\n if (err) return callback(err);\n const list = addresses as Array<{ address: string; family: number }>;\n if (!list.length || list.some((entry) => rejected(entry.address))) {\n return callback(new Error(\"Cloud gateway URL resolved to a private or loopback address\"));\n }\n callback(null, list);\n });\n return;\n }\n lookup(hostname, { ...base, all: false }, (err, address, family) => {\n if (err) return callback(err, address, family);\n if (rejected(address)) {\n return callback(new Error(\"Cloud gateway URL resolved to a private or loopback address\"), address, family);\n }\n callback(null, address, family);\n });\n }) as unknown as LookupFunction;\n}\n\nfunction isPrivateIpv4(host: string): boolean {\n const octets = host.split(\".\").map((part) => Number(part));\n if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true;\n const [a, b] = octets;\n if (a === 10 || a === 127 || a === 0) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n if (a === 192 && b === 0) return true;\n if (a === 198 && (b === 18 || b === 19)) return true;\n if (a >= 224) return true;\n return false;\n}\n\nfunction isPrivateIpv6(host: string): boolean {\n return host === \"::\"\n || host === \"::1\"\n || host.startsWith(\"fc\")\n || host.startsWith(\"fd\")\n || host.startsWith(\"fe80:\")\n || host.startsWith(\"2001:db8:\");\n}\n\nfunction writeConfigFile(configPath: string, contents: string): void {\n writeFileSync(configPath, contents, { encoding: \"utf-8\", mode: 0o600 });\n chmodSync(configPath, 0o600);\n}\n\nconst GATEWAY_REQUEST_TIMEOUT_MS = 30_000;\n\nexport function postJsonToValidatedGateway(\n url: URL,\n payload: Record<string, unknown>,\n timeoutMs: number = GATEWAY_REQUEST_TIMEOUT_MS,\n): Promise<Record<string, unknown>> {\n validateGatewayNetworkTarget(url);\n const body = JSON.stringify(payload);\n const requestImpl = url.protocol === \"https:\" ? httpsRequest : httpRequest;\n\n return new Promise((resolve, reject) => {\n const req = requestImpl({\n protocol: url.protocol,\n hostname: url.hostname,\n port: url.port,\n path: `${url.pathname}${url.search}`,\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"content-length\": Buffer.byteLength(body),\n },\n lookup: createGatewayLookup(url),\n }, (res) => {\n const statusCode = res.statusCode ?? 0;\n const chunks: Buffer[] = [];\n res.on(\"data\", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));\n res.on(\"end\", () => {\n const responseText = Buffer.concat(chunks).toString(\"utf-8\");\n const parsed = parseJsonObject(responseText);\n if (statusCode < 200 || statusCode >= 300) {\n reject(new Error(typeof parsed.error === \"string\" ? parsed.error : `Pairing failed with HTTP ${statusCode}`));\n return;\n }\n resolve(parsed);\n });\n });\n\n req.setTimeout(timeoutMs, () => {\n req.destroy(new Error(`Gateway request timed out after ${timeoutMs}ms`));\n });\n req.on(\"error\", reject);\n req.end(body);\n });\n}\n\nfunction parseJsonObject(value: string): Record<string, unknown> {\n try {\n const parsed = JSON.parse(value) as unknown;\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {};\n } catch {\n return {};\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cloud-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.test.d.ts","sourceRoot":"","sources":["../src/cloud-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
1
+ // Project/App: Open GSD
2
+ // File Purpose: Tests for the cloud gateway config module. Two independent suites:
3
+ // 1. SSRF-guard matrix for parseCloudGatewayUrl / validateGatewayNetworkTarget —
4
+ // loosening any branch (plain-HTTP carve-out, private/loopback IP rejection,
5
+ // protocol/credential/fragment checks) turns a case red.
6
+ // 2. postJsonToValidatedGateway's request timeout — a hung gateway must reject
7
+ // rather than hang the pairing/device flow.
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { createServer } from "node:http";
11
+ import { parseCloudGatewayUrl, validateGatewayNetworkTarget, postJsonToValidatedGateway, } from "./cloud-config.js";
12
+ // Each rejected case pairs an untrusted input with the substring the guard must
13
+ // report — so a message change is deliberate, not accidental.
14
+ const REJECTED = [
15
+ // Plain HTTP is only allowed for loopback hosts.
16
+ { input: "http://evil.example", message: /only allowed for localhost/ },
17
+ // RFC1918 private ranges over HTTPS.
18
+ { input: "https://10.0.0.1", message: /private or loopback/ },
19
+ { input: "https://192.168.1.1", message: /private or loopback/ },
20
+ { input: "https://172.16.0.1", message: /private or loopback/ },
21
+ // Link-local / cloud metadata endpoint.
22
+ { input: "https://169.254.169.254", message: /private or loopback/ },
23
+ // Loopback over HTTPS (IPv4 and IPv6 forms) and the localhost hostname.
24
+ { input: "https://127.0.0.1", message: /private or loopback/ },
25
+ { input: "https://[::1]", message: /private or loopback/ },
26
+ { input: "https://localhost", message: /private or loopback/ },
27
+ // Non-HTTP protocol.
28
+ { input: "ftp://gateway.example", message: /must use http or https/ },
29
+ // Embedded credentials / fragments are rejected outright.
30
+ { input: "https://user:pass@gateway.example", message: /must not include credentials/ },
31
+ { input: "https://gateway.example/#frag", message: /must not include a fragment/ },
32
+ // Unparseable / non-absolute inputs.
33
+ { input: "", message: /absolute HTTP\(S\) URL/ },
34
+ { input: "not a url", message: /absolute HTTP\(S\) URL/ },
35
+ ];
36
+ for (const { input, message } of REJECTED) {
37
+ test(`parseCloudGatewayUrl rejects ${JSON.stringify(input)}`, () => {
38
+ assert.throws(() => parseCloudGatewayUrl(input), message);
39
+ });
40
+ }
41
+ // Accepted: a public HTTPS host and the documented local-dev plain-HTTP loopback carve-out.
42
+ const ACCEPTED = [
43
+ { input: "https://cloud-gateway.opengsd.net", hostname: "cloud-gateway.opengsd.net", protocol: "https:" },
44
+ { input: "http://localhost:8787", hostname: "localhost", protocol: "http:" },
45
+ { input: "http://127.0.0.1:8787", hostname: "127.0.0.1", protocol: "http:" },
46
+ ];
47
+ for (const { input, hostname, protocol } of ACCEPTED) {
48
+ test(`parseCloudGatewayUrl accepts ${JSON.stringify(input)}`, () => {
49
+ const url = parseCloudGatewayUrl(input);
50
+ assert.equal(url.hostname, hostname);
51
+ assert.equal(url.protocol, protocol);
52
+ // Round-trips to a usable absolute URL for the same host.
53
+ assert.equal(new URL(url.toString()).hostname, hostname);
54
+ });
55
+ }
56
+ // validateGatewayNetworkTarget is the request-time defense-in-depth guard applied to
57
+ // already-constructed URLs (device code/token endpoints). Cover its branches directly.
58
+ test("validateGatewayNetworkTarget rejects private/loopback targets", () => {
59
+ assert.throws(() => validateGatewayNetworkTarget(new URL("https://169.254.169.254")), /private or loopback/);
60
+ assert.throws(() => validateGatewayNetworkTarget(new URL("https://10.0.0.1")), /private or loopback/);
61
+ assert.throws(() => validateGatewayNetworkTarget(new URL("https://[::1]")), /private or loopback/);
62
+ });
63
+ test("validateGatewayNetworkTarget allows a public HTTPS host and http loopback", () => {
64
+ assert.doesNotThrow(() => validateGatewayNetworkTarget(new URL("https://cloud-gateway.opengsd.net")));
65
+ assert.doesNotThrow(() => validateGatewayNetworkTarget(new URL("http://localhost:8787")));
66
+ assert.doesNotThrow(() => validateGatewayNetworkTarget(new URL("http://127.0.0.1:8787")));
67
+ });
68
+ // Start a loopback server, returning its base URL; skips the test under sandbox EPERM.
69
+ async function listenLoopback(t, handler) {
70
+ const server = createServer(handler);
71
+ try {
72
+ await new Promise((resolve, reject) => {
73
+ server.once("error", reject);
74
+ server.listen(0, "127.0.0.1", resolve);
75
+ });
76
+ }
77
+ catch (err) {
78
+ if (err.code === "EPERM") {
79
+ t.skip("loopback listen is blocked in this sandbox");
80
+ return null;
81
+ }
82
+ throw err;
83
+ }
84
+ const address = server.address();
85
+ assert.ok(address && typeof address === "object");
86
+ return {
87
+ baseUrl: `http://127.0.0.1:${address.port}`,
88
+ close: () => new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve())),
89
+ };
90
+ }
91
+ test("postJsonToValidatedGateway rejects when the gateway never responds", async (t) => {
92
+ // Handler that accepts the request but never sends a response — the hung-server case.
93
+ const server = await listenLoopback(t, () => { });
94
+ if (!server)
95
+ return;
96
+ try {
97
+ await assert.rejects(postJsonToValidatedGateway(new URL(`${server.baseUrl}/x`), {}, 200), /timed out after 200ms/);
98
+ }
99
+ finally {
100
+ await server.close();
101
+ }
102
+ });
103
+ test("postJsonToValidatedGateway resolves a fast healthy response before the timeout", async (t) => {
104
+ const server = await listenLoopback(t, (_req, res) => {
105
+ res.writeHead(200, { "content-type": "application/json" });
106
+ res.end("{}");
107
+ });
108
+ if (!server)
109
+ return;
110
+ try {
111
+ const body = await postJsonToValidatedGateway(new URL(`${server.baseUrl}/x`), {}, 200);
112
+ assert.deepEqual(body, {});
113
+ }
114
+ finally {
115
+ await server.close();
116
+ }
117
+ });
118
+ //# sourceMappingURL=cloud-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-config.test.js","sourceRoot":"","sources":["../src/cloud-config.test.ts"],"names":[],"mappings":"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","sourcesContent":["// Project/App: Open GSD\n// File Purpose: Tests for the cloud gateway config module. Two independent suites:\n// 1. SSRF-guard matrix for parseCloudGatewayUrl / validateGatewayNetworkTarget —\n// loosening any branch (plain-HTTP carve-out, private/loopback IP rejection,\n// protocol/credential/fragment checks) turns a case red.\n// 2. postJsonToValidatedGateway's request timeout — a hung gateway must reject\n// rather than hang the pairing/device flow.\nimport { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { createServer, type RequestListener } from \"node:http\";\nimport {\n parseCloudGatewayUrl,\n validateGatewayNetworkTarget,\n postJsonToValidatedGateway,\n} from \"./cloud-config.js\";\n\n// Each rejected case pairs an untrusted input with the substring the guard must\n// report — so a message change is deliberate, not accidental.\nconst REJECTED: Array<{ input: string; message: RegExp }> = [\n // Plain HTTP is only allowed for loopback hosts.\n { input: \"http://evil.example\", message: /only allowed for localhost/ },\n // RFC1918 private ranges over HTTPS.\n { input: \"https://10.0.0.1\", message: /private or loopback/ },\n { input: \"https://192.168.1.1\", message: /private or loopback/ },\n { input: \"https://172.16.0.1\", message: /private or loopback/ },\n // Link-local / cloud metadata endpoint.\n { input: \"https://169.254.169.254\", message: /private or loopback/ },\n // Loopback over HTTPS (IPv4 and IPv6 forms) and the localhost hostname.\n { input: \"https://127.0.0.1\", message: /private or loopback/ },\n { input: \"https://[::1]\", message: /private or loopback/ },\n { input: \"https://localhost\", message: /private or loopback/ },\n // Non-HTTP protocol.\n { input: \"ftp://gateway.example\", message: /must use http or https/ },\n // Embedded credentials / fragments are rejected outright.\n { input: \"https://user:pass@gateway.example\", message: /must not include credentials/ },\n { input: \"https://gateway.example/#frag\", message: /must not include a fragment/ },\n // Unparseable / non-absolute inputs.\n { input: \"\", message: /absolute HTTP\\(S\\) URL/ },\n { input: \"not a url\", message: /absolute HTTP\\(S\\) URL/ },\n];\n\nfor (const { input, message } of REJECTED) {\n test(`parseCloudGatewayUrl rejects ${JSON.stringify(input)}`, () => {\n assert.throws(() => parseCloudGatewayUrl(input), message);\n });\n}\n\n// Accepted: a public HTTPS host and the documented local-dev plain-HTTP loopback carve-out.\nconst ACCEPTED: Array<{ input: string; hostname: string; protocol: string }> = [\n { input: \"https://cloud-gateway.opengsd.net\", hostname: \"cloud-gateway.opengsd.net\", protocol: \"https:\" },\n { input: \"http://localhost:8787\", hostname: \"localhost\", protocol: \"http:\" },\n { input: \"http://127.0.0.1:8787\", hostname: \"127.0.0.1\", protocol: \"http:\" },\n];\n\nfor (const { input, hostname, protocol } of ACCEPTED) {\n test(`parseCloudGatewayUrl accepts ${JSON.stringify(input)}`, () => {\n const url = parseCloudGatewayUrl(input);\n assert.equal(url.hostname, hostname);\n assert.equal(url.protocol, protocol);\n // Round-trips to a usable absolute URL for the same host.\n assert.equal(new URL(url.toString()).hostname, hostname);\n });\n}\n\n// validateGatewayNetworkTarget is the request-time defense-in-depth guard applied to\n// already-constructed URLs (device code/token endpoints). Cover its branches directly.\ntest(\"validateGatewayNetworkTarget rejects private/loopback targets\", () => {\n assert.throws(() => validateGatewayNetworkTarget(new URL(\"https://169.254.169.254\")), /private or loopback/);\n assert.throws(() => validateGatewayNetworkTarget(new URL(\"https://10.0.0.1\")), /private or loopback/);\n assert.throws(() => validateGatewayNetworkTarget(new URL(\"https://[::1]\")), /private or loopback/);\n});\n\ntest(\"validateGatewayNetworkTarget allows a public HTTPS host and http loopback\", () => {\n assert.doesNotThrow(() => validateGatewayNetworkTarget(new URL(\"https://cloud-gateway.opengsd.net\")));\n assert.doesNotThrow(() => validateGatewayNetworkTarget(new URL(\"http://localhost:8787\")));\n assert.doesNotThrow(() => validateGatewayNetworkTarget(new URL(\"http://127.0.0.1:8787\")));\n});\n\n// Start a loopback server, returning its base URL; skips the test under sandbox EPERM.\nasync function listenLoopback(\n t: { skip: (msg: string) => void },\n handler: RequestListener,\n): Promise<{ baseUrl: string; close: () => Promise<void> } | null> {\n const server = createServer(handler);\n try {\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", resolve);\n });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EPERM\") {\n t.skip(\"loopback listen is blocked in this sandbox\");\n return null;\n }\n throw err;\n }\n const address = server.address();\n assert.ok(address && typeof address === \"object\");\n return {\n baseUrl: `http://127.0.0.1:${address.port}`,\n close: () => new Promise<void>((resolve, reject) => server.close((err) => err ? reject(err) : resolve())),\n };\n}\n\ntest(\"postJsonToValidatedGateway rejects when the gateway never responds\", async (t) => {\n // Handler that accepts the request but never sends a response — the hung-server case.\n const server = await listenLoopback(t, () => { /* never call res.end */ });\n if (!server) return;\n try {\n await assert.rejects(\n postJsonToValidatedGateway(new URL(`${server.baseUrl}/x`), {}, 200),\n /timed out after 200ms/,\n );\n } finally {\n await server.close();\n }\n});\n\ntest(\"postJsonToValidatedGateway resolves a fast healthy response before the timeout\", async (t) => {\n const server = await listenLoopback(t, (_req, res) => {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(\"{}\");\n });\n if (!server) return;\n try {\n const body = await postJsonToValidatedGateway(new URL(`${server.baseUrl}/x`), {}, 200);\n assert.deepEqual(body, {});\n } finally {\n await server.close();\n }\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cloud-gateway-lookup.test.d.ts.map