oysterun 1.6.0-beta.2 → 1.6.0-beta.21

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 (242) hide show
  1. package/bin/oysterun.mjs +64 -17
  2. package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
  3. package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
  4. package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
  5. package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
  6. package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
  7. package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
  8. package/dev/client/connectivity/managed-host-browser.mjs +10 -0
  9. package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
  10. package/dev/client/connectivity/session-transition-state.mjs +18 -6
  11. package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
  12. package/dev/client/web/index.html +5367 -609
  13. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
  14. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
  15. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
  16. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
  17. package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
  18. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
  19. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
  20. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
  21. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
  22. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
  23. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
  24. package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
  25. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
  26. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
  27. package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
  28. package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
  29. package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
  30. package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
  31. package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
  32. package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
  33. package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
  34. package/dev/client/web-chat/dist/index.html +1 -2
  35. package/docs/routec/dev/cloud_deplyments.md +5 -0
  36. package/docs/routec/dev/npm_native_dependency_install.md +6 -2
  37. package/docs/routec/dev/npmjs_publish.md +55 -19
  38. package/docs/routec/dev/p262_setup_input_file.md +117 -0
  39. package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
  40. package/docs/routec/host_setup.md +39 -10
  41. package/docs/routec/oysterun_operation_interface_spec.md +78 -2
  42. package/docs/routec/provider_contract.md +35 -5
  43. package/host-service/README.md +36 -0
  44. package/host-service/adapters/claude-code-adapter.mjs +48 -5
  45. package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
  46. package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
  47. package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
  48. package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
  49. package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
  50. package/host-service/agent-config.mjs +16 -5
  51. package/host-service/agent-folder-availability.mjs +41 -0
  52. package/host-service/apns-complete-message-dispatcher.mjs +1 -0
  53. package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
  54. package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
  55. package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
  56. package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
  57. package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
  58. package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
  59. package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
  60. package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
  61. package/host-service/callback-durable-store.mjs +354 -47
  62. package/host-service/callback-foundation.mjs +21 -1
  63. package/host-service/callback-host-integration.mjs +345 -30
  64. package/host-service/claude-acp-model-discovery.mjs +23 -18
  65. package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
  66. package/host-service/cli/installer-recovery-guidance.mjs +20 -0
  67. package/host-service/cli/prepare-restart-restore.mjs +2 -0
  68. package/host-service/cli/product-cli.mjs +34 -33
  69. package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
  70. package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
  71. package/host-service/cli/run-host-lifecycle.mjs +2 -0
  72. package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
  73. package/host-service/codex-model-discovery.mjs +23 -18
  74. package/host-service/complete-cli-uninstall.mjs +176 -0
  75. package/host-service/complete-message-notification-predicate.mjs +8 -6
  76. package/host-service/complete-message-notification-title.mjs +4 -0
  77. package/host-service/completed-session-unread.mjs +425 -42
  78. package/host-service/completed-turn-navigation.mjs +444 -144
  79. package/host-service/completed-turn-notification-observer.mjs +116 -0
  80. package/host-service/completed-turn-service.mjs +445 -0
  81. package/host-service/config.mjs +115 -9
  82. package/host-service/desired-open-session-state.mjs +331 -0
  83. package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
  84. package/host-service/frp-agent.mjs +9 -9
  85. package/host-service/grok-acp-model-discovery.mjs +31 -33
  86. package/host-service/host-lifecycle-recovery.mjs +37 -0
  87. package/host-service/host-port-resolution.mjs +98 -0
  88. package/host-service/host-runtime-diagnostics.mjs +18 -1
  89. package/host-service/initial-preflight-recovery.mjs +71 -0
  90. package/host-service/installer-recovery-guidance.mjs +85 -0
  91. package/host-service/installer-reinstall-bindings.mjs +49 -0
  92. package/host-service/installer-reinstall-target.mjs +49 -0
  93. package/host-service/legacy-continuity-observation.mjs +33 -0
  94. package/host-service/lifecycle-compat-cli.mjs +13 -0
  95. package/host-service/lifecycle-compat-preload.mjs +89 -0
  96. package/host-service/lifecycle-compatibility.mjs +164 -0
  97. package/host-service/lifecycle-continuity-assets.mjs +222 -0
  98. package/host-service/lifecycle-continuity-probe.mjs +97 -0
  99. package/host-service/lifecycle-continuity-projection.mjs +29 -0
  100. package/host-service/lifecycle-continuity-runner.mjs +192 -0
  101. package/host-service/lifecycle-continuity.mjs +493 -0
  102. package/host-service/lifecycle-service-render-bundle.mjs +120 -0
  103. package/host-service/lifecycle-user-path.mjs +34 -0
  104. package/host-service/link-annotations.mjs +10 -0
  105. package/host-service/matrix-credential-issuer.mjs +32 -1
  106. package/host-service/matrix-event-correlator.mjs +166 -27
  107. package/host-service/matrix-facade.mjs +96 -19
  108. package/host-service/matrix-room-binding.mjs +24 -0
  109. package/host-service/matrix-transcript-read-adapter.mjs +17 -3
  110. package/host-service/model-discovery-guardian.mjs +40 -0
  111. package/host-service/model-discovery-process.mjs +141 -0
  112. package/host-service/onboard-lifecycle-status.mjs +28 -0
  113. package/host-service/onboard.mjs +479 -0
  114. package/host-service/package.json +2 -1
  115. package/host-service/portable-scheduler-definitions.mjs +28 -0
  116. package/host-service/product-runtime-host-origin.mjs +22 -0
  117. package/host-service/provider-model-params.mjs +14 -17
  118. package/host-service/provider-model-refresh-runner.mjs +124 -169
  119. package/host-service/provider-model-refresh-schedule.mjs +11 -0
  120. package/host-service/provider-resume-contract.mjs +9 -3
  121. package/host-service/restart-restore-state.mjs +25 -0
  122. package/host-service/retained-host-state-holder.mjs +57 -0
  123. package/host-service/routec-matrix-sqlite-store.mjs +155 -10
  124. package/host-service/routec-matrix-storage-adapter.mjs +208 -25
  125. package/host-service/routec-matrix-storage-paths.mjs +12 -0
  126. package/host-service/routec-request-delay-window.mjs +55 -0
  127. package/host-service/scheduler-acp-provider-runner.mjs +2 -1
  128. package/host-service/scheduler-direct-provider-runner.mjs +71 -9
  129. package/host-service/scheduler-run-timeout-policy.mjs +85 -0
  130. package/host-service/scheduler-service.mjs +96 -9
  131. package/host-service/scheduler-store.mjs +9 -2
  132. package/host-service/server.mjs +2202 -552
  133. package/host-service/service-definition-migration.mjs +10 -3
  134. package/host-service/session-collection-service.mjs +320 -0
  135. package/host-service/session-history.mjs +99 -15
  136. package/host-service/session-list-preview-service.mjs +87 -12
  137. package/host-service/session-list-projection-service.mjs +182 -0
  138. package/host-service/session-loop-definitions.mjs +702 -75
  139. package/host-service/session-manager.mjs +473 -38
  140. package/host-service/session-switcher-projection.mjs +11 -3
  141. package/host-service/session-tags.mjs +451 -0
  142. package/host-service/session-transcript-target.mjs +103 -0
  143. package/host-service/setup-input-file.mjs +91 -0
  144. package/host-service/setup-provider-model-defaults.mjs +53 -0
  145. package/host-service/setup.mjs +133 -101
  146. package/host-service/stopped-user-host-package.mjs +60 -0
  147. package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
  148. package/host-service/update-reminder-state.mjs +7 -0
  149. package/host-service/voice-input-receipt-store.mjs +60 -0
  150. package/package.json +1 -1
  151. package/static/oysterun-social-preview.png +0 -0
  152. package/static/site/assets/oysterun-social-preview.png +0 -0
  153. package/static/site/blog/en/index.html +17 -0
  154. package/static/site/blog/generated_manifest.json +9 -9
  155. package/static/site/blog/index.html +18 -0
  156. package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
  157. package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
  158. package/static/site/blog/zh/index.html +17 -0
  159. package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
  160. package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
  161. package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
  162. package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
  163. package/static/site/docs/en/index.html +17 -0
  164. package/static/site/docs/generated_manifest.json +60 -60
  165. package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
  166. package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
  167. package/static/site/docs/index.html +18 -0
  168. package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
  169. package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
  170. package/static/site/docs/read-large-tool-results/en/index.html +17 -0
  171. package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
  172. package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
  173. package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
  174. package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
  175. package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
  176. package/static/site/docs/send-images-and-media/en/index.html +17 -0
  177. package/static/site/docs/send-images-and-media/zh/index.html +17 -0
  178. package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
  179. package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
  180. package/static/site/docs/understand-agent-activity/en/index.html +19 -2
  181. package/static/site/docs/understand-agent-activity/en/index.md +2 -2
  182. package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
  183. package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
  184. package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
  185. package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
  186. package/static/site/docs/zh/index.html +17 -0
  187. package/static/site/en/index.html +18 -1
  188. package/static/site/en/index.md +1 -1
  189. package/static/site/faq/en/index.html +17 -0
  190. package/static/site/faq/index.html +18 -0
  191. package/static/site/faq/zh/index.html +17 -0
  192. package/static/site/getting-started/en/index.html +23 -1
  193. package/static/site/getting-started/en/index.md +7 -1
  194. package/static/site/getting-started/index.html +18 -0
  195. package/static/site/getting-started/zh/index.html +23 -1
  196. package/static/site/getting-started/zh/index.md +7 -1
  197. package/static/site/index.html +18 -0
  198. package/static/site/install.sh +676 -127
  199. package/static/site/overview/en/index.html +18 -1
  200. package/static/site/overview/en/index.md +2 -2
  201. package/static/site/overview/index.html +18 -0
  202. package/static/site/overview/zh/index.html +18 -1
  203. package/static/site/overview/zh/index.md +2 -2
  204. package/static/site/privacy/en/index.html +17 -0
  205. package/static/site/privacy/index.html +18 -0
  206. package/static/site/privacy/zh/index.html +17 -0
  207. package/static/site/zh/index.html +18 -1
  208. package/static/site/zh/index.md +1 -1
  209. package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
  210. package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
  211. package/static/site_source/downloads/install.sh +676 -127
  212. package/static/site_source/manifests/host_install.json +2 -10
  213. package/static/site_source/pages/getting-started/en.html +6 -1
  214. package/static/site_source/pages/getting-started/zh.html +6 -1
  215. package/static/site_source/pages/home/en.html +1 -1
  216. package/static/site_source/pages/home/zh.html +1 -1
  217. package/static/site_source/pages/home_main.html +3 -2
  218. package/static/site_source/pages/overview/en.html +1 -1
  219. package/static/site_source/pages/overview/zh.html +1 -1
  220. package/tool_scripts/install_oysterun_update.mjs +78 -13
  221. package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
  222. package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
  223. package/tool_scripts/oysterun_release_service.sh +5 -0
  224. package/tool_scripts/oysterun_service_common.sh +59 -24
  225. package/tool_scripts/oysterun_setup_input.sh +97 -0
  226. package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
  227. package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
  228. package/tool_scripts/resolve_user_host_port.mjs +15 -0
  229. package/tool_scripts/restart_oysterun.sh +8 -2
  230. package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
  231. package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
  232. package/tool_scripts/sync_host_stack_config.mjs +9 -6
  233. package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
  234. package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
  235. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
  236. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
  237. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
  238. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
  239. package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
  240. package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
  241. package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
  242. package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
@@ -495,6 +495,13 @@ require_host_port_configured() {
495
495
  exit 1
496
496
  }
497
497
 
498
+ resolve_bound_user_host_pid() {
499
+ local port_node=""
500
+ port_node="$(require_node_runtime "Host process binding")" || return 1
501
+ "${port_node}" "${ROOT_DIR}/tool_scripts/resolve_user_host_port.mjs" \
502
+ "${CONFIG_PATH}" "${RUN_DIR}/host-runtime.json" "${CONFIG_DIR}" process
503
+ }
504
+
498
505
  configure_stack_runtime() {
499
506
  validate_stack
500
507
 
@@ -528,7 +535,11 @@ configure_stack_runtime() {
528
535
  BACKEND_PORT="8000"
529
536
  BACKEND_INTERNAL_URL="http://127.0.0.1:8000"
530
537
  BACKEND_PUBLIC_BASE_URL="http://localhost:8000"
531
- HOST_PORT="${PRODUCTION_HOST_PORT}"
538
+ local port_node=""
539
+ port_node="$(require_node_runtime "Host port resolution")" || return 1
540
+ HOST_PORT="$("${port_node}" "${ROOT_DIR}/tool_scripts/resolve_user_host_port.mjs" \
541
+ "${CONFIG_PATH}" "${RUN_DIR}/host-runtime.json" "${CONFIG_DIR}" port \
542
+ "${HOST_ORIGIN_FILE}" "${HOST_PID_FILE}" "${HOME}/Library/LaunchAgents/${HOST_LABEL}.plist")" || return 1
532
543
  ;;
533
544
  staging)
534
545
  STACK_ROOT="${STACKS_DIR}/${STACK_NAME}"
@@ -1815,7 +1826,6 @@ wait_for_http() {
1815
1826
  local pid="$3"
1816
1827
  local log_file="$4"
1817
1828
  local launch_label="${5:-}"
1818
- local attempts=0
1819
1829
 
1820
1830
  until curl -fsS "${url}" >/dev/null 2>&1; do
1821
1831
  local current_pid="${pid}"
@@ -1825,15 +1835,6 @@ wait_for_http() {
1825
1835
 
1826
1836
  if [[ -n "${current_pid}" ]] && ! pid_is_running "${current_pid}" 2>/dev/null; then
1827
1837
  if [[ -n "${launch_label}" ]] && launchctl_label_exists "${launch_label}"; then
1828
- attempts=$((attempts + 1))
1829
- if [[ "${attempts}" -ge 60 ]]; then
1830
- echo "[oysterun-service] error: timed out waiting for ${label} at ${url}" >&2
1831
- if [[ -f "${log_file}" ]]; then
1832
- echo "[oysterun-service] Last ${label} log lines:" >&2
1833
- tail -n 40 "${log_file}" >&2 || true
1834
- fi
1835
- exit 1
1836
- fi
1837
1838
  sleep 1
1838
1839
  continue
1839
1840
  fi
@@ -1845,16 +1846,6 @@ wait_for_http() {
1845
1846
  exit 1
1846
1847
  fi
1847
1848
 
1848
- attempts=$((attempts + 1))
1849
- if [[ "${attempts}" -ge 60 ]]; then
1850
- echo "[oysterun-service] error: timed out waiting for ${label} at ${url}" >&2
1851
- if [[ -f "${log_file}" ]]; then
1852
- echo "[oysterun-service] Last ${label} log lines:" >&2
1853
- tail -n 40 "${log_file}" >&2 || true
1854
- fi
1855
- exit 1
1856
- fi
1857
-
1858
1849
  sleep 1
1859
1850
  done
1860
1851
  }
@@ -2013,17 +2004,22 @@ sqlite_sidecar_paths_for_db() {
2013
2004
  stack_host_state_paths() {
2014
2005
  sqlite_sidecar_paths_for_db "${HOST_TRANSCRIPTS_DB_PATH}"
2015
2006
  sqlite_sidecar_paths_for_db "${MATRIX_DB_PATH}"
2007
+ local matrix_sqlite_path node_bin
2008
+ node_bin="$(require_node_runtime "Matrix database holder inspection")" || return 1
2009
+ matrix_sqlite_path="$("${node_bin}" --input-type=module -e 'const { routeCMatrixSQLiteStoragePath } = await import(process.argv[1]); console.log(routeCMatrixSQLiteStoragePath(process.argv[2]));' "${ROOT_DIR}/host-service/routec-matrix-storage-paths.mjs" "${MATRIX_DB_PATH}")" || return 1
2010
+ sqlite_sidecar_paths_for_db "${matrix_sqlite_path}"
2016
2011
  }
2017
2012
 
2018
2013
  stack_host_state_holder_pids() {
2019
2014
  local paths=()
2020
- local path
2015
+ local path state_paths
2016
+ state_paths="$(stack_host_state_paths)" || return 1
2021
2017
 
2022
2018
  while IFS= read -r path; do
2023
2019
  if [[ -n "${path}" && -e "${path}" ]]; then
2024
2020
  paths+=("${path}")
2025
2021
  fi
2026
- done < <(stack_host_state_paths)
2022
+ done <<< "${state_paths}"
2027
2023
 
2028
2024
  if (( ${#paths[@]} == 0 )); then
2029
2025
  return 0
@@ -2067,7 +2063,7 @@ stop_stale_stack_host_state_holders() {
2067
2063
  local holders=""
2068
2064
  local candidates=()
2069
2065
 
2070
- holders="$(stack_host_state_holder_pids)"
2066
+ holders="$(stack_host_state_holder_pids)" || return 1
2071
2067
  if [[ -z "${holders}" ]]; then
2072
2068
  return 0
2073
2069
  fi
@@ -2080,6 +2076,45 @@ stop_stale_stack_host_state_holders() {
2080
2076
  continue
2081
2077
  fi
2082
2078
  if pid_matches_oysterun_host_server "${pid}"; then
2079
+ local holder_node holder_check_status
2080
+ holder_node="$(require_node_runtime "Host database holder ownership")" || return 1
2081
+ if "${holder_node}" --input-type=module -e '
2082
+ import { readFileSync } from "node:fs";
2083
+ const [modulePath, runtimePath, pid, command, cwd, configDir, activePackage, stackName] = process.argv.slice(1);
2084
+ try {
2085
+ const { assertHostStateHolder } = await import(modulePath);
2086
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
2087
+ const holder = { pid, command, cwd, configDir, activePackage, stackName };
2088
+ try { assertHostStateHolder(runtime, holder); }
2089
+ catch (error) {
2090
+ if (!process.env.OYSTERUN_RETAINED_HOST_CWD) throw error;
2091
+ const { assertReceiptBoundSourceHolder } = await import(`${activePackage}/host-service/retained-host-state-holder.mjs`);
2092
+ assertReceiptBoundSourceHolder(runtime, holder, {
2093
+ operationId: process.env.OYSTERUN_LIFECYCLE_OPERATION_ID,
2094
+ receiptPath: process.env.OYSTERUN_CONTINUITY_RECEIPT,
2095
+ expectedCwd: process.env.OYSTERUN_RETAINED_HOST_CWD,
2096
+ });
2097
+ }
2098
+ } catch (error) {
2099
+ // launchd may have stopped this exact holder between the first
2100
+ // ps/cwd observation and this check. Absence is not a live match.
2101
+ const { processDefinitelyAbsent } = await import(`${activePackage}/host-service/initial-preflight-recovery.mjs`);
2102
+ if (processDefinitelyAbsent(Number(pid))) {
2103
+ console.error("[oysterun-service] Selected database holder exited during cleanup; no process was signalled.");
2104
+ process.exitCode = 3;
2105
+ } else {
2106
+ console.error(`[oysterun-service] ${error.message}`);
2107
+ process.exitCode = 1;
2108
+ }
2109
+ }
2110
+ ' "${ROOT_DIR}/host-service/installer-reinstall-bindings.mjs" "${RUN_DIR}/host-runtime.json" "${pid}" "$(process_command_for_pid "${pid}")" "$(process_cwd_for_pid "${pid}")" "${CONFIG_DIR}" "${ROOT_DIR}" "${STACK_NAME}"; then
2111
+ :
2112
+ else
2113
+ holder_check_status=$?
2114
+ # Internal absence result: exclude it from the later signal list.
2115
+ [[ "${holder_check_status}" -ne 3 ]] || continue
2116
+ return 1
2117
+ fi
2083
2118
  candidates+=("${pid}")
2084
2119
  else
2085
2120
  echo "[oysterun-service] Warning: stack ${STACK_NAME} state files are held by non-Host PID ${pid}; leaving it running" >&2
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # Canonical read-only setup-file/pre-input validation; also embedded in install.sh.
3
+ setup_input_error() { printf '%s\n' "$1" >&2; return 1; }
4
+ setup_config_has() {
5
+ local file="$1" key="$2" kind="$3" value
6
+ [[ -f "$file" ]] || return 1
7
+ if [[ "$(uname -s)" == Darwin ]]; then
8
+ local expected="$kind"; [[ "$kind" != direct ]] || expected=string
9
+ value=$(/usr/bin/plutil -extract "$key" raw -expect "$expected" -o - "$file" 2>/dev/null) || return 1
10
+ case "$kind" in bool) return 0;; direct) [[ "$value" == direct ]];; integer) [[ "$value" =~ ^[0-9]+$ ]] && (( value > 0 && value <= 65535 ));; *) [[ "$value" =~ [^[:space:]] ]];; esac
11
+ else
12
+ command -v python3 >/dev/null || { setup_input_error 'Read-only Ubuntu configuration preflight requires python3; no changes made'; return 2; }
13
+ python3 - "$file" "$key" "$kind" <<'PY'
14
+ import json,sys
15
+ try:
16
+ c=json.load(open(sys.argv[1]));v=c.get(sys.argv[2]);kind=sys.argv[3];ok=isinstance(v,bool) if kind=='bool' else (type(v)==int and 0<v<=65535) if kind=='integer' else v=='direct' if kind=='direct' else isinstance(v,str) and bool(v.strip())
17
+ except Exception: ok=False
18
+ sys.exit(0 if ok else 1)
19
+ PY
20
+ fi
21
+ }
22
+ setup_config_ready() {
23
+ local dir="$1" cfg="$1/config.json"
24
+ setup_config_has "$cfg" port integer && setup_config_has "$cfg" dashboard_user string && setup_config_has "$cfg" dashboard_password_hash string && setup_config_has "$cfg" default_browse_path string || return 1
25
+ if setup_config_has "$cfg" connection_mode direct; then
26
+ setup_config_has "$cfg" direct_host_url string || setup_config_has "$cfg" public_base_url string
27
+ else
28
+ setup_config_has "$cfg" managed_endpoint string &&
29
+ { setup_config_has "$dir/cloud_identity.json" device_id string || setup_config_has "$dir/cloud_identity.json" host_id string || setup_config_has "$cfg" device_id string || setup_config_has "$cfg" host_id string; } &&
30
+ { setup_config_has "$dir/cloud_identity.json" device_token string || setup_config_has "$dir/cloud_identity.json" host_credential string || setup_config_has "$cfg" device_token string || setup_config_has "$cfg" host_credential string; }
31
+ fi
32
+ }
33
+ setup_input_validate() {
34
+ set +x
35
+ set +a
36
+ unset setup_input_snapshot
37
+ setup_input_snapshot=''
38
+ local source="$1" uid="$2" config="$3" line key value mode owner missing='' seen='|' name='' root='' password='' consent=''
39
+ export -n password line value
40
+ if [[ -f "$config/config.json" && "$(uname -s)" == Linux ]] && ! command -v python3 >/dev/null; then setup_input_error "Read-only Ubuntu configuration preflight requires python3; no changes made"; return 1; fi
41
+ if [[ -f "$config/config.json" ]]; then
42
+ if [[ "$(uname -s)" == Darwin ]]; then
43
+ /usr/bin/osascript -l JavaScript -e 'function run(argv) { ObjC.import("Foundation"); const text = $.NSString.stringWithContentsOfFileEncodingError(argv[0], $.NSUTF8StringEncoding, null).js; const value = JSON.parse(text); if (!value || Array.isArray(value) || typeof value !== "object") throw Error("invalid configuration"); return "valid"; }' "$config/config.json" >/dev/null 2>&1 || { setup_input_error 'Existing configuration is invalid JSON; preserve it and restore the configuration before retrying'; return 1; }
44
+ else
45
+ python3 -c 'import json,sys; value=json.load(open(sys.argv[1])); assert isinstance(value,dict)' "$config/config.json" >/dev/null 2>&1 || { setup_input_error 'Existing configuration is invalid JSON; preserve it and restore the configuration before retrying'; return 1; }
46
+ fi
47
+ fi
48
+ if [[ -n "$source" ]]; then
49
+ [[ "$source" == /* && -f "$source" && ! -L "$source" ]] || { setup_input_error 'Setup input must be an absolute regular file, not a symlink'; return 1; }
50
+ local parent parent_path="${source%/*}"; [[ -n "$parent_path" ]] || parent_path=/
51
+ parent=$(cd -P "$parent_path" && pwd) || return 1
52
+ [[ "$parent" == "$parent_path" ]] || { setup_input_error "Setup input path must not traverse symlinks"; return 1; }
53
+ exec 9< "$source" || { setup_input_error 'Setup input is not readable'; return 1; }
54
+ local opened=/dev/fd/9; [[ "$(uname -s)" != Linux ]] || opened=/proc/self/fd/9
55
+ if [[ "$(uname -s)" == Darwin ]]; then
56
+ owner=$(/usr/bin/stat -Lf %u "$opened"); mode=$(/usr/bin/stat -Lf %Lp "$opened")
57
+ else
58
+ owner=$(/usr/bin/stat -Lc %u "$opened"); mode=$(/usr/bin/stat -Lc %a "$opened")
59
+ fi
60
+ if ! [[ -f "$opened" && -n "$uid" && "$owner" == "$uid" && ( "$mode" == 600 || "$mode" == 400 ) ]]; then
61
+ exec 9<&-; setup_input_error 'Setup input must belong to the selected source owner and have private permissions (0600 or 0400)'; return 1
62
+ fi
63
+ if IFS= read -r -d '' setup_input_snapshot <&9; then
64
+ exec 9<&-; unset setup_input_snapshot; setup_input_error 'Setup input must be UTF-8 text without NULs'; return 1
65
+ fi
66
+ exec 9<&-
67
+ builtin printf '%s' "$setup_input_snapshot" | /usr/bin/iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1 || { unset setup_input_snapshot; setup_input_error 'Setup input must be UTF-8 text without NULs'; return 1; }
68
+ while IFS= read -r line || [[ -n "$line" ]]; do
69
+ [[ -z "$line" || "$line" == \#* ]] && continue
70
+ [[ "$line" == *=* ]] || { setup_input_error 'Invalid setup input format'; return 1; }
71
+ key=${line%%=*}; value=${line#*=}
72
+ case "$key" in display_name|default_browse_root|dashboard_password|daily_telemetry_enabled) ;; *) setup_input_error 'Unknown setup input field'; return 1;; esac
73
+ [[ "$seen" != *"|$key|"* ]] || { setup_input_error 'Duplicate setup input field'; return 1; }; seen="$seen$key|"
74
+ case "$key" in
75
+ display_name) name="$value" ;;
76
+ default_browse_root) [[ -z "$value" || "$value" == /* ]] || { setup_input_error 'Setup Browse Root must be absolute'; return 1; }; root="$value" ;;
77
+ dashboard_password) password="$value" ;;
78
+ daily_telemetry_enabled) [[ "$value" == true || "$value" == false ]] || { setup_input_error 'Setup telemetry consent must be true or false'; return 1; }; consent="$value" ;;
79
+ esac
80
+ done < <(builtin printf '%s' "$setup_input_snapshot")
81
+ [[ -z "$root" || -d "$root" ]] || { setup_input_error "Setup Browse Root must be an existing directory"; return 1; }
82
+ fi
83
+ setup_config_ready "$config" && return 0
84
+ setup_config_has "$config/config.json" display_name string || [[ "$name" =~ [^[:space:]] ]] || missing="$missing display_name"
85
+ setup_config_has "$config/config.json" default_browse_path string || [[ -n "$root" ]] || missing="$missing default_browse_root"
86
+ setup_config_has "$config/config.json" dashboard_password_hash string || [[ "$password" =~ [^[:space:]] ]] || missing="$missing dashboard_password"
87
+ if ! { setup_config_has "$config/config.json" daily_telemetry_enabled bool && setup_config_has "$config/config.json" daily_telemetry_consent_recorded_at string; } && [[ -z "$consent" ]]; then missing="$missing daily_telemetry_enabled"; fi
88
+ [[ -z "$missing" ]] || { setup_input_error "Missing onboarding inputs:$missing. Resume the same command from a terminal or with --setup-input-file /absolute/private-file"; return 1; }
89
+ }
90
+
91
+ # Only the invoking-root account-creation branch may emit this memory snapshot.
92
+ # Anonymous pipes retain root ownership and read-only access in the target child.
93
+ setup_input_emit_for_uid() {
94
+ set +x
95
+ builtin printf 'oysterun-root-setup-v1:%s\n' "$1"
96
+ builtin printf '%s' "$setup_input_snapshot"
97
+ }
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { compatibilityEnvironment } from "../host-service/lifecycle-compatibility.mjs";
3
+ import { runContinuityTransaction } from "./oysterun_continuity_transaction.mjs";
2
4
 
3
5
  import { spawnSync } from "node:child_process";
4
6
  import {
@@ -287,7 +289,7 @@ function replaceCliLink({ globalPrefix, packageRoot }) {
287
289
  symlinkSync(relative(binDir, join(packageRoot, "bin", "oysterun.mjs")), cliPath);
288
290
  }
289
291
 
290
- function runServiceCommand({
292
+ export function runServiceCommand({
291
293
  identity,
292
294
  activeRoot,
293
295
  stack,
@@ -295,6 +297,7 @@ function runServiceCommand({
295
297
  operationId,
296
298
  expectedRetainedCurrentHostCwd = null,
297
299
  expectedTargetServiceDefinitionSha256 = null,
300
+ continuityPath = null,
298
301
  }) {
299
302
  const cli = join(activeRoot, "bin", "oysterun.mjs");
300
303
  const result = spawnSync(
@@ -317,12 +320,13 @@ function runServiceCommand({
317
320
  ],
318
321
  {
319
322
  cwd: activeRoot,
320
- env: buildNodeNpmChildEnvironment(identity, {
323
+ env: compatibilityEnvironment(process.env.OYSTERUN_CONFIG_DIR || identity.config_dir, buildNodeNpmChildEnvironment(identity, {
321
324
  ...process.env,
322
325
  OYSTERUN_RELEASE_STACK: stack,
323
326
  OYSTERUN_STACK: stack,
324
327
  OYSTERUN_LIFECYCLE_OPERATION_ID: operationId,
325
- }),
328
+ ...(continuityPath ? { OYSTERUN_CONTINUITY_RECEIPT: continuityPath } : {}),
329
+ })),
326
330
  encoding: "utf8",
327
331
  stdio: "pipe",
328
332
  }
@@ -353,6 +357,12 @@ function runServiceCommand({
353
357
  }
354
358
 
355
359
  export async function runRestartOnlyTransaction(args, overrides = {}) {
360
+ const continuityOperation = readUpdateOperationState({ configDir: args.config_dir });
361
+ if (continuityOperation.operation_id === args.operation_id && continuityOperation.continuity_path) {
362
+ return runContinuityTransaction(args, { ...overrides,
363
+ runCanonicalService: (options) => runServiceCommand(options) });
364
+ }
365
+
356
366
  const durableOperation = readUpdateOperationState({ configDir: args.config_dir });
357
367
  if (durableOperation.operation_id !== args.operation_id) {
358
368
  throw new Error("restart-only operation state changed before execution");
@@ -683,6 +693,12 @@ function restorePriorPackage({
683
693
  }
684
694
 
685
695
  export async function runUpdateRestartTransaction(args, overrides = {}) {
696
+ const continuityOperation = readUpdateOperationState({ configDir: args.config_dir });
697
+ if (continuityOperation.operation_id === args.operation_id && continuityOperation.continuity_path) {
698
+ return runContinuityTransaction(args, { ...overrides,
699
+ runCanonicalService: (options) => runServiceCommand(options) });
700
+ }
701
+
686
702
  const receipt = readJsonObject(args.result_path, "update preparation receipt");
687
703
  const durableOperation = readUpdateOperationState({ configDir: args.config_dir });
688
704
  if (
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ // One-time 1.5.7 -> reviewed beta adapter. This is intentionally not exposed
3
+ // through oysterun's public command table or Host Preferences.
4
+ import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawnSync } from "node:child_process";
8
+ import {
9
+ continuityDigest, continuityFailure, continuityReceiptPath, prepareContinuity,
10
+ bindFreshContinuityBaseline, readContinuity, writeContinuityAtomic, recordContinuityRecovery,
11
+ qualifyContinuitySessions,
12
+ } from "../host-service/lifecycle-continuity.mjs";
13
+ import { digestContinuityPackage, finalizeContinuityAssets } from "../host-service/lifecycle-continuity-assets.mjs";
14
+ import { createContinuityProbeGrant, fetchContinuityObservation } from "../host-service/lifecycle-continuity-probe.mjs";
15
+ import { resolveNodeNpmRuntimeIdentity, buildNodeNpmChildEnvironment } from "../host-service/node-npm-runtime-identity.mjs";
16
+ import { buildServiceRenderBundle, getServiceDefinitionIdentity } from "../host-service/lifecycle-service-render-bundle.mjs";
17
+ import { assertServiceMigrationInvariants, commitServiceDefinitionMigration, renderServiceDefinition } from "../host-service/service-definition-migration.mjs";
18
+ import { readHostRuntimeMetadata } from "../host-service/host-runtime-diagnostics.mjs";
19
+ import { readHostRestartRestoreState } from "../host-service/restart-restore-state.mjs";
20
+ import { readUpdateOperationState, writeUpdateOperationState } from "../host-service/update-reminder-state.mjs";
21
+ import { installOysterunUpdate } from "./install_oysterun_update.mjs";
22
+ import { runServiceCommand } from "./oysterun_update_restart_runner.mjs";
23
+ import { inspectContinuityListener, runContinuityTransaction } from "./oysterun_continuity_transaction.mjs";
24
+ import { compareOysterunSemver } from "../host-service/npm-update-policy.mjs";
25
+
26
+ const SCRIPT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
27
+ const fail = (check, message) => { throw continuityFailure("legacy_bootstrap", check, message); };
28
+ const inside = (parent, child) => { const r = relative(parent, child); return r === "" || (!r.startsWith("..") && !isAbsolute(r)); };
29
+ function privateJson(path, { requirePrivate = true } = {}) {
30
+ const stat = lstatSync(path);
31
+ if (!stat.isFile() || stat.isSymbolicLink() || (requirePrivate && (stat.mode & 0o077))) fail("private_plan", "Plan and authentication input must be private regular files.");
32
+ return JSON.parse(readFileSync(path, "utf8"));
33
+ }
34
+
35
+ export function validateP233BootstrapPlan(path, sha256, { controllerRoot = SCRIPT_ROOT } = {}) {
36
+ if (!isAbsolute(path) || !/^[a-f0-9]{64}$/.test(sha256 || "") || continuityDigest(readFileSync(path)) !== sha256) fail("plan_hash", "The exact reviewed plan hash is required.");
37
+ const p = privateJson(path);
38
+ if (p.schema !== "oysterun.p233.legacy_bootstrap.v1" || p.source_version !== "1.5.7" ||
39
+ !/^1\.6\.0-beta\.\d+$/.test(p.target_version || "") || compareOysterunSemver(p.target_version, "1.6.0-beta.2") !== 1) fail("version_boundary", "P233 only migrates 1.5.7 to the exact newly reviewed 1.6.0 beta.");
40
+ for (const key of ["prefix", "config_dir", "browser_root", "controller_root", "artifact_path", "credential_path", "node_bin", "npm_cli", "service_definition_path"]) {
41
+ if (!isAbsolute(p[key] || "")) fail("absolute_geometry", `The reviewed ${key} must be absolute.`);
42
+ }
43
+ if (!/^[A-Za-z0-9-]+$/.test(p.operation_id || "") || !Number.isInteger(p.port) || p.port < 1 || p.port > 65535) fail("operation_geometry", "Operation identity or exact port is invalid.");
44
+ if (resolve(controllerRoot) !== p.controller_root || inside(p.prefix, p.controller_root) || inside(p.prefix, p.artifact_path) ||
45
+ inside(p.prefix, path) || inside(p.prefix, p.credential_path)) fail("external_control_plane", "Controller, plan, artifact and authentication input must remain outside the active npm prefix.");
46
+ if (p.mode === "production") {
47
+ if (p.prefix !== "/opt/homebrew" || p.port !== 8802 || p.stack !== "production" ||
48
+ p.node_bin !== "/opt/homebrew/opt/node@22/bin/node" ||
49
+ p.npm_cli !== "/opt/homebrew/opt/node@22/lib/node_modules/npm/bin/npm-cli.js" ||
50
+ p.config_dir !== join(p.owner_home, ".oysterun") || p.production_release !== "OWNER_RELEASED_P233_EXACT_ARTIFACT" ||
51
+ p.legacy_maintenance_window?.session_lifecycle_quiesced !== true) fail("production_boundary", "Production requires the exact reviewed legacy geometry, released artifact plan and an exclusive Session lifecycle maintenance window.");
52
+ } else if (p.mode === "task_owned") {
53
+ const owner = privateJson(p.ownership_path);
54
+ if (owner.task !== "P233" || p.port === 8802 || p.stack === "production" ||
55
+ !inside(owner.task_root, p.prefix) || !inside(owner.task_root, p.config_dir) || !inside(owner.task_root, p.browser_root) ||
56
+ owner.prefix !== p.prefix || owner.config_dir !== p.config_dir || owner.port !== p.port || owner.stack !== p.stack ||
57
+ owner.service_definition !== p.service_definition_path) fail("task_ownership", "Rehearsal must bind only fresh P233 task-owned resources.");
58
+ } else fail("mode", "The plan must explicitly select production or task_owned.");
59
+ const service = getServiceDefinitionIdentity({ stackName: p.stack, configDir: p.config_dir, homeDir: p.owner_home });
60
+ if (service.definitionPath !== p.service_definition_path || service.serviceLabel !== p.service_label) fail("definition_geometry", "The service definition does not match the canonical stack geometry.");
61
+ if (continuityDigest(readFileSync(p.artifact_path)) !== p.artifact_sha256 || digestContinuityPackage(p.controller_root) !== p.controller_tree_sha256) fail("artifact_hash", "Reviewed artifact or immutable external controller changed.");
62
+ const manifest = spawnSync("/usr/bin/tar", ["-xOf", p.artifact_path, "package/package.json"], { encoding: "utf8" });
63
+ if (manifest.status !== 0 || JSON.parse(manifest.stdout).version !== p.target_version) fail("artifact_version", "Artifact manifest does not match the exact reviewed target.");
64
+ const identity = resolveNodeNpmRuntimeIdentity({ nodeBin: p.node_bin, npmCli: p.npm_cli });
65
+ if (!/^v22\./.test(identity.node_version) || identity.toolchain_identity_digest !== p.runtime_identity_digest ||
66
+ realpathSync(process.execPath) !== identity.node_realpath) fail("node22_identity", "Controller and Host require the exact reviewed Node 22/npm runtime identity.");
67
+ const rawConfig = JSON.parse(readFileSync(join(p.config_dir, "config.json"), "utf8"));
68
+ // Historical setup writes this existing identity with its own mode. Do not
69
+ // reconfigure the Host merely to read it; new control inputs stay private.
70
+ const cloudIdentity = privateJson(join(p.config_dir, "cloud_identity.json"), { requirePrivate: false });
71
+ const config = { ...rawConfig, device_id: cloudIdentity.host_id || cloudIdentity.device_id || rawConfig.device_id };
72
+ if (config.device_id !== p.device_id || realpathSync(config.default_browse_path) !== p.browser_root ||
73
+ config.managed_endpoint !== p.managed_endpoint || !p.managed_endpoint?.startsWith("https://")) fail("host_identity", "Existing device, Browser Root or managed route differs from the reviewed plan.");
74
+ return { plan: p, identity, config, path, sha256 };
75
+ }
76
+
77
+ export function createNormalHostReader(plan) {
78
+ const credentials = privateJson(plan.credential_path);
79
+ if (typeof credentials.username !== "string" || typeof credentials.password !== "string") fail("normal_login_input", "Provide normal Host username/password in the private authentication input.");
80
+ return async (origin, pathname, { method = "GET", body } = {}) => {
81
+ if (![new URL(plan.managed_endpoint).origin, `http://127.0.0.1:${plan.port}`].includes(origin)) fail("read_origin", "Read target is outside the retained Host routes.");
82
+ const login = await fetch(new URL("/auth/login", origin), { method: "POST", headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify(credentials), redirect: "error", signal: AbortSignal.timeout(10_000) });
84
+ if (!login.ok) throw continuityFailure("authentication", "http_status", `Normal Host login returned HTTP ${login.status}.`);
85
+ const authenticated = await login.json();
86
+ if (!authenticated.token) fail("login_token", "Normal Host login did not issue a session token.");
87
+ const response = await fetch(new URL(pathname, origin), { method, headers: { Authorization: `Bearer ${authenticated.token}`, "Content-Type": "application/json" },
88
+ ...(body ? { body: JSON.stringify(body) } : {}), redirect: "error", signal: AbortSignal.timeout(15_000) });
89
+ if (!response.ok) throw continuityFailure("legacy_read", "http_status", `Bounded Host operation returned HTTP ${response.status}.`);
90
+ return response.json();
91
+ };
92
+ }
93
+
94
+ export function createP233LegacyObservation({ plan, identity, config, readNormal }) {
95
+ return async ({ origin, retained = null }) => {
96
+ const metadata = readHostRuntimeMetadata(plan.config_dir);
97
+ const pids = inspectContinuityListener(plan.port);
98
+ if (pids.length !== 1 || pids[0] !== metadata?.pid || metadata.port !== plan.port ||
99
+ metadata.config_dir !== plan.config_dir || metadata.node_npm_identity_digest !== identity.toolchain_identity_digest) fail("legacy_runtime_identity", "Live legacy Host does not match its retained process/runtime authority.");
100
+ const rows = (await readNormal(origin, "/sessions?include_previews=false")).sessions;
101
+ const sessions = [];
102
+ for (const row of rows.filter((row) => row.alive || row.ready)) {
103
+ const id = row.sessionId || row.id;
104
+ const status = await readNormal(origin, `/session/status?session_id=${encodeURIComponent(id)}`);
105
+ const prior = retained?.sessions.find((entry) => entry.session_id === id);
106
+ let anchors = prior?.history_anchors;
107
+ if (!anchors) {
108
+ const tail = await readNormal(origin, `/session/messages?session_id=${encodeURIComponent(id)}&agent_id=${encodeURIComponent(status.agent_id)}&recent_tail=true&limit=2`);
109
+ anchors = tail.messages.map((message) => ({ event_id: message.matrix_event_id }));
110
+ }
111
+ const history = [];
112
+ for (const anchor of anchors) {
113
+ const context = await readNormal(origin, `/session/messages-around?session_id=${encodeURIComponent(id)}&event_id=${encodeURIComponent(anchor.event_id)}&before=0&after=0`);
114
+ if (!context.target_found) fail("legacy_history", "A retained committed Matrix anchor is missing.");
115
+ history.push({ event_id: anchor.event_id, sha256: continuityDigest(context.target_message) });
116
+ }
117
+ sessions.push({ session_id: id, agent_id: status.agent_id, agent_folder: realpathSync(status.cwd), provider: status.provider,
118
+ provider_resume_id: status.provider_resume_id, provider_thread_id: status.provider_thread_id || status.provider_resume_id,
119
+ alive: status.alive, ready: status.ready, history_anchors: history });
120
+ }
121
+ const host = { version: metadata.package_version, pid: metadata.pid, port: metadata.port, stack: plan.stack,
122
+ package_root: metadata.repo_root,
123
+ process_cwd: (() => {
124
+ const cwd = spawnSync("/usr/sbin/lsof", ["-a", "-p", String(metadata.pid), "-d", "cwd", "-Fn"], { encoding: "utf8" });
125
+ const values = cwd.stdout.split("\n").filter((line) => line.startsWith("n")).map((line) => line.slice(1));
126
+ if (cwd.status !== 0 || values.length !== 1) fail("legacy_process_cwd", "Cannot prove the exact legacy process cwd.");
127
+ return values[0];
128
+ })(),
129
+ node_version: metadata.node_version, runtime_identity_digest: metadata.node_npm_identity_digest,
130
+ device_id: config.device_id, config_dir: plan.config_dir, host_folder: plan.config_dir, browser_root: plan.browser_root,
131
+ managed_endpoint: plan.managed_endpoint, direct_host_url: config.direct_host_url || config.public_base_url || null,
132
+ service_definition_path: plan.service_definition_path, service_definition_sha256: continuityDigest(readFileSync(plan.service_definition_path)) };
133
+ const restore = readHostRestartRestoreState({ configDir: plan.config_dir });
134
+ return { host, sessions, baseline: { host, sessions, captured_at: new Date().toISOString() },
135
+ restore_result: restore?.restore_result, restart_state: restore?.state, restart_id: restore?.restart_id,
136
+ authenticated_user_path: { pid: metadata.pid }, route: { origin, device_id: plan.device_id,
137
+ operation_id: plan.operation_id, authenticated: true, http_status: 200, authentication: "normal_dashboard_login" } };
138
+ };
139
+ }
140
+
141
+ export async function executeP233Bootstrap(action, reviewed) {
142
+ if (!["forward", "recover", "rollback", "finalize"].includes(action)) fail("action", "Use exactly forward, recover, rollback or finalize.");
143
+ const { plan: p, identity, config } = reviewed;
144
+ const activeRoot = join(p.prefix, "lib", "node_modules", "oysterun");
145
+ const path = continuityReceiptPath(p.config_dir, p.operation_id);
146
+ const resultPath = join(p.config_dir, "operation_logs", `p233-${p.operation_id}-preparation.json`);
147
+ const healthUrl = `http://127.0.0.1:${p.port}/health`;
148
+ if (action !== "forward") {
149
+ const retained = readContinuity(path, p.operation_id);
150
+ if (retained.reviewed_plan_sha256 !== reviewed.sha256 || retained.controller_tree_sha256 !== p.controller_tree_sha256) fail("recovery_authority", "Every continuation, including finalize, requires the exact original reviewed plan and controller.");
151
+ }
152
+ Object.assign(process.env, buildNodeNpmChildEnvironment(identity, { ...process.env,
153
+ OYSTERUN_CONFIG_DIR: p.config_dir, OYSTERUN_PORT: String(p.port), OYSTERUN_STACK: p.stack,
154
+ OYSTERUN_RELEASE_STACK: p.stack, OYSTERUN_STACKS_DIR: p.stacks_dir,
155
+ OYSTERUN_UPDATE_REGISTRY_URL: p.registry?.override_active ? p.registry.url : "",
156
+ OYSTERUN_UPDATE_REGISTRY_STATE_PATH: p.registry_state_path || "",
157
+ }));
158
+ if (!p.registry_state_path) delete process.env.OYSTERUN_UPDATE_REGISTRY_STATE_PATH;
159
+ if (!p.registry?.override_active) delete process.env.OYSTERUN_UPDATE_REGISTRY_URL;
160
+ if (action === "finalize") return finalizeContinuityAssets({ path, operationId: p.operation_id, configDir: p.config_dir, activePackageRoot: activeRoot });
161
+ const readNormal = createNormalHostReader(p);
162
+ const legacyObserve = createP233LegacyObservation({ plan: p, identity, config, readNormal });
163
+ const observe = async (options) => {
164
+ const metadata = readHostRuntimeMetadata(p.config_dir);
165
+ if (metadata?.package_version === "1.5.7") return legacyObserve({ ...options, retained: readContinuity(path, p.operation_id).baseline });
166
+ await readNormal(options.origin, "/sessions?include_previews=false");
167
+ return fetchContinuityObservation(options);
168
+ };
169
+ if (action === "forward") {
170
+ if (existsSync(path)) fail("existing_operation", "Forward already has a durable operation; use recover with the same reviewed plan.");
171
+ const prior = readUpdateOperationState({ configDir: p.config_dir });
172
+ if (prior.running || prior.continuity_path && !["CLEANED", "ROLLED_BACK_HEALTHY"].includes(readContinuity(prior.continuity_path, prior.operation_id).state)) fail("operation_conflict", "Another running or retained continuity operation owns this Host; no package was changed.");
173
+ const initial = await legacyObserve({ origin: new URL(healthUrl).origin });
174
+ if (initial.host.version !== "1.5.7") fail("legacy_source", "This one-time adapter only starts on the exact 1.5.7 source.");
175
+ if (initial.host.process_cwd !== join(activeRoot, "host-service")) fail("legacy_active_cwd", "Legacy Host must run from its canonical active root before bootstrap.");
176
+ prepareContinuity({ path, operationId: p.operation_id, transactionKind: "legacy_bootstrap",
177
+ source: { version: "1.5.7", runtime_identity_digest: p.runtime_identity_digest,
178
+ package_tree_sha256: digestContinuityPackage(activeRoot), service_definition_sha256: initial.host.service_definition_sha256 },
179
+ target: { version: p.target_version, runtime_identity_digest: p.runtime_identity_digest, artifact_sha256: p.artifact_sha256 }, baseline: initial.baseline,
180
+ authority: { reviewed_plan_sha256: reviewed.sha256, controller_tree_sha256: p.controller_tree_sha256 } });
181
+ createContinuityProbeGrant(p.config_dir, p.operation_id);
182
+ writeContinuityAtomic(path, { ...readContinuity(path, p.operation_id), reviewed_plan_sha256: reviewed.sha256,
183
+ controller_tree_sha256: p.controller_tree_sha256 });
184
+ const sourceBundlePath = `${path}.source-render.json`;
185
+ const sourceBundle = buildServiceRenderBundle({ toolchain: identity, stackName: p.stack, registry: p.registry,
186
+ packageRoot: activeRoot, packageVersion: "1.5.7", configDir: p.config_dir,
187
+ configPath: join(p.config_dir, "config.json"), port: p.port });
188
+ writeContinuityAtomic(sourceBundlePath, sourceBundle);
189
+ try {
190
+ await installOysterunUpdate({ package_spec: p.artifact_path, expected_version: p.target_version,
191
+ active_package_root: activeRoot, node_bin: p.node_bin, npm_cli: p.npm_cli, identity_digest: p.runtime_identity_digest,
192
+ config_dir: p.config_dir, result_path: resultPath, operation_id: p.operation_id, retain_for_restart: "1",
193
+ continuity_path: path, service_definition_path: p.service_definition_path,
194
+ source_service_bundle_path: sourceBundlePath, service_definition_authority_path: join(p.config_dir, "service-definition-authority.json"),
195
+ }, { beforeActivation: async () => {
196
+ // The historical coordinator is asked to prepare while its source
197
+ // package/definition still agree, before the active-root exchange.
198
+ const prepared = await readNormal(new URL(healthUrl).origin, "/admin/restart-prepare", {
199
+ method: "POST", body: { trigger: "p233_one_time_external_bootstrap" } });
200
+ const fresh = await legacyObserve({ origin: new URL(healthUrl).origin });
201
+ bindFreshContinuityBaseline(path, p.operation_id, fresh.baseline, prepared.restart_restore?.restart_id,
202
+ readHostRestartRestoreState({ configDir: p.config_dir }));
203
+ writeUpdateOperationState({ operation_id: p.operation_id, continuity_path: path, status: "restart_scheduled",
204
+ transaction_kind: "update_latest_and_restart", current_version: "1.5.7", target_version: p.target_version,
205
+ restart_id: prepared.restart_restore?.restart_id, running: true, lifecycle_owner_pid: process.pid,
206
+ restart_attempted: false, result_path: resultPath }, { configDir: p.config_dir });
207
+ } });
208
+ const receipt = readContinuity(path, p.operation_id);
209
+ const targetBundle = buildServiceRenderBundle({ toolchain: identity, stackName: p.stack, registry: p.registry,
210
+ packageRoot: activeRoot, packageVersion: p.target_version, configDir: p.config_dir, configPath: join(p.config_dir, "config.json"), port: p.port });
211
+ assertServiceMigrationInvariants(sourceBundle, targetBundle);
212
+ const rendered = renderServiceDefinition(targetBundle);
213
+ const snapshot = JSON.parse(readFileSync(receipt.resources.service_definition_snapshot_path, "utf8"));
214
+ writeContinuityAtomic(path, { ...receipt, target: { ...receipt.target, service_definition_sha256: rendered.sha256 } });
215
+ commitServiceDefinitionMigration({ definitionPath: p.service_definition_path, snapshot, rendered });
216
+ writeContinuityAtomic(join(p.config_dir, "service-definition-authority.json"), {
217
+ schema_version: 1, package_version: p.target_version, package_manifest_sha256: targetBundle.artifact.integrity_sha256,
218
+ service_label: p.service_label, service_definition_path: p.service_definition_path,
219
+ service_definition_sha256: rendered.sha256, runtime_identity_digest: p.runtime_identity_digest, committed_at: new Date().toISOString(),
220
+ });
221
+ } catch (error) { recordContinuityRecovery(path, p.operation_id, error); }
222
+ }
223
+ const retained = readContinuity(path, p.operation_id);
224
+ if (retained.reviewed_plan_sha256 !== reviewed.sha256 || retained.controller_tree_sha256 !== p.controller_tree_sha256) fail("recovery_authority", "Recovery must use the exact original reviewed plan and retained controller.");
225
+ const projection = readUpdateOperationState({ configDir: p.config_dir });
226
+ if (projection.operation_id !== p.operation_id) {
227
+ if (projection.running || projection.continuity_path && !["CLEANED", "ROLLED_BACK_HEALTHY"].includes(readContinuity(projection.continuity_path, projection.operation_id).state)) fail("operation_conflict", "Another retained lifecycle operation owns this Host.");
228
+ // An interrupted PREPARED operation may precede the old coordinator's fresh
229
+ // prepare. Rebind its projection; never mint a new continuity operation.
230
+ writeUpdateOperationState({ operation_id: p.operation_id, continuity_path: path, running: false,
231
+ status: "rollback_failed", transaction_kind: "update_latest_and_restart",
232
+ current_version: "1.5.7", target_version: p.target_version, result_path: resultPath }, { configDir: p.config_dir });
233
+ }
234
+ return runContinuityTransaction({ operation_id: p.operation_id, continuity_path: path, result_path: resultPath,
235
+ active_package_root: activeRoot, config_dir: p.config_dir, node_bin: p.node_bin, npm_cli: p.npm_cli,
236
+ identity_digest: p.runtime_identity_digest, stack: p.stack, health_url: healthUrl, restart_id: retained.restart_id,
237
+ ...(action === "rollback" ? { restore_before_restart: "1" } : {}),
238
+ }, { observeContinuity: observe, runCanonicalService: runServiceCommand,
239
+ beforeForwardRestart: async (receipt) => {
240
+ // 1.5.7 has no Session admission reservation. The one-time released
241
+ // maintenance window is mandatory; this final read also rejects any
242
+ // observed live Sessions change after the pre-activation snapshot.
243
+ const live = await legacyObserve({ origin: new URL(healthUrl).origin, retained: receipt.baseline });
244
+ qualifyContinuitySessions(receipt.baseline.sessions, live.sessions, null, { requireRestore: false });
245
+ },
246
+ });
247
+ }
248
+
249
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
250
+ const [action, flag, path, hashFlag, sha256, ...extra] = process.argv.slice(2);
251
+ if (flag !== "--plan" || hashFlag !== "--sha256" || extra.length) fail("command_geometry", "Usage: node p233_legacy_bootstrap.mjs <forward|recover|rollback|finalize> --plan <absolute-private-plan> --sha256 <reviewed-plan-sha256>");
252
+ const result = await executeP233Bootstrap(action, validateP233BootstrapPlan(path, sha256));
253
+ process.stdout.write(`${JSON.stringify({ operation_id: result.operation_id, state: result.state, status: result.status })}\n`);
254
+ }
@@ -0,0 +1,15 @@
1
+ import { readUserHostPort, assertUserHostProcess, assertUserHostServicePid } from "../host-service/host-port-resolution.mjs";
2
+ import { readFileSync } from "node:fs";
3
+ try {
4
+ const [configPath, runtimePath, configDir, mode, ...existingServicePaths] = process.argv.slice(2);
5
+ if (!configPath || !runtimePath || !configDir) throw new Error("Exact config and runtime paths are required.");
6
+ const port = readUserHostPort({ configPath, runtimePath, configDir, existingServicePaths });
7
+ if (mode === "process") {
8
+ process.stdout.write(String(assertUserHostProcess(JSON.parse(readFileSync(runtimePath, "utf8")), configDir)));
9
+ } else if (mode === "service-pid") {
10
+ process.stdout.write(String(assertUserHostServicePid(JSON.parse(readFileSync(runtimePath, "utf8")), configDir, existingServicePaths[0])));
11
+ } else process.stdout.write(String(port));
12
+ } catch (error) {
13
+ process.stderr.write(`[oysterun-service] ${error.message}\n`);
14
+ process.exitCode = 1;
15
+ }
@@ -51,8 +51,14 @@ preflight_host_restart_readiness \
51
51
  "" \
52
52
  "${EXPECTED_TARGET_SERVICE_DEFINITION_SHA256}"
53
53
 
54
+ # P233 records the real stop attempt only after every preflight check passed.
55
+ if [[ -n "${OYSTERUN_CONTINUITY_RECEIPT:-}" ]]; then
56
+ "${OYSTERUN_NODE_BIN:?}" "${SCRIPT_DIR}/../host-service/lifecycle-continuity.mjs" \
57
+ mark-restart "${OYSTERUN_CONTINUITY_RECEIPT}" "${ALLOW_OPERATION_ID}"
58
+ fi
59
+
54
60
  if [[ "${#SERVICE_ARGS[@]}" -gt 0 ]]; then
55
- if ! "${SCRIPT_DIR}/stop_oysterun.sh" "${SERVICE_ARGS[@]}"; then
61
+ if ! OYSTERUN_RETAINED_HOST_CWD="${EXPECTED_RETAINED_CURRENT_HOST_CWD}" "${SCRIPT_DIR}/stop_oysterun.sh" "${SERVICE_ARGS[@]}"; then
56
62
  printf '[oysterun-service] P226_CHECK stage=stop check=canonical_service_stop result=failed code=service_stop_failed\n' >&2
57
63
  exit 1
58
64
  fi
@@ -62,7 +68,7 @@ if [[ "${#SERVICE_ARGS[@]}" -gt 0 ]]; then
62
68
  exit 1
63
69
  fi
64
70
  else
65
- if ! "${SCRIPT_DIR}/stop_oysterun.sh"; then
71
+ if ! OYSTERUN_RETAINED_HOST_CWD="${EXPECTED_RETAINED_CURRENT_HOST_CWD}" "${SCRIPT_DIR}/stop_oysterun.sh"; then
66
72
  printf '[oysterun-service] P226_CHECK stage=stop check=canonical_service_stop result=failed code=service_stop_failed\n' >&2
67
73
  exit 1
68
74
  fi
@@ -263,7 +263,8 @@ function readJsonObject(path, { missingFallback = null } = {}) {
263
263
 
264
264
  function writeJsonObject(path, value) {
265
265
  mkdirSync(dirname(path), { recursive: true });
266
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
266
+ if (existsSync(path)) chmodSync(path, 0o600);
267
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
267
268
  }
268
269
 
269
270
  function normalizeCredentialString(value, label, { required = false } = {}) {