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.
- package/bin/oysterun.mjs +64 -17
- package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
- package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
- package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
- package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
- package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
- package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
- package/dev/client/connectivity/managed-host-browser.mjs +10 -0
- package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
- package/dev/client/connectivity/session-transition-state.mjs +18 -6
- package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
- package/dev/client/web/index.html +5367 -609
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
- package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
- package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
- package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
- package/dev/client/web-chat/dist/index.html +1 -2
- package/docs/routec/dev/cloud_deplyments.md +5 -0
- package/docs/routec/dev/npm_native_dependency_install.md +6 -2
- package/docs/routec/dev/npmjs_publish.md +55 -19
- package/docs/routec/dev/p262_setup_input_file.md +117 -0
- package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
- package/docs/routec/host_setup.md +39 -10
- package/docs/routec/oysterun_operation_interface_spec.md +78 -2
- package/docs/routec/provider_contract.md +35 -5
- package/host-service/README.md +36 -0
- package/host-service/adapters/claude-code-adapter.mjs +48 -5
- package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
- package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
- package/host-service/agent-config.mjs +16 -5
- package/host-service/agent-folder-availability.mjs +41 -0
- package/host-service/apns-complete-message-dispatcher.mjs +1 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
- package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
- package/host-service/callback-durable-store.mjs +354 -47
- package/host-service/callback-foundation.mjs +21 -1
- package/host-service/callback-host-integration.mjs +345 -30
- package/host-service/claude-acp-model-discovery.mjs +23 -18
- package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
- package/host-service/cli/installer-recovery-guidance.mjs +20 -0
- package/host-service/cli/prepare-restart-restore.mjs +2 -0
- package/host-service/cli/product-cli.mjs +34 -33
- package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
- package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
- package/host-service/cli/run-host-lifecycle.mjs +2 -0
- package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
- package/host-service/codex-model-discovery.mjs +23 -18
- package/host-service/complete-cli-uninstall.mjs +176 -0
- package/host-service/complete-message-notification-predicate.mjs +8 -6
- package/host-service/complete-message-notification-title.mjs +4 -0
- package/host-service/completed-session-unread.mjs +425 -42
- package/host-service/completed-turn-navigation.mjs +444 -144
- package/host-service/completed-turn-notification-observer.mjs +116 -0
- package/host-service/completed-turn-service.mjs +445 -0
- package/host-service/config.mjs +115 -9
- package/host-service/desired-open-session-state.mjs +331 -0
- package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
- package/host-service/frp-agent.mjs +9 -9
- package/host-service/grok-acp-model-discovery.mjs +31 -33
- package/host-service/host-lifecycle-recovery.mjs +37 -0
- package/host-service/host-port-resolution.mjs +98 -0
- package/host-service/host-runtime-diagnostics.mjs +18 -1
- package/host-service/initial-preflight-recovery.mjs +71 -0
- package/host-service/installer-recovery-guidance.mjs +85 -0
- package/host-service/installer-reinstall-bindings.mjs +49 -0
- package/host-service/installer-reinstall-target.mjs +49 -0
- package/host-service/legacy-continuity-observation.mjs +33 -0
- package/host-service/lifecycle-compat-cli.mjs +13 -0
- package/host-service/lifecycle-compat-preload.mjs +89 -0
- package/host-service/lifecycle-compatibility.mjs +164 -0
- package/host-service/lifecycle-continuity-assets.mjs +222 -0
- package/host-service/lifecycle-continuity-probe.mjs +97 -0
- package/host-service/lifecycle-continuity-projection.mjs +29 -0
- package/host-service/lifecycle-continuity-runner.mjs +192 -0
- package/host-service/lifecycle-continuity.mjs +493 -0
- package/host-service/lifecycle-service-render-bundle.mjs +120 -0
- package/host-service/lifecycle-user-path.mjs +34 -0
- package/host-service/link-annotations.mjs +10 -0
- package/host-service/matrix-credential-issuer.mjs +32 -1
- package/host-service/matrix-event-correlator.mjs +166 -27
- package/host-service/matrix-facade.mjs +96 -19
- package/host-service/matrix-room-binding.mjs +24 -0
- package/host-service/matrix-transcript-read-adapter.mjs +17 -3
- package/host-service/model-discovery-guardian.mjs +40 -0
- package/host-service/model-discovery-process.mjs +141 -0
- package/host-service/onboard-lifecycle-status.mjs +28 -0
- package/host-service/onboard.mjs +479 -0
- package/host-service/package.json +2 -1
- package/host-service/portable-scheduler-definitions.mjs +28 -0
- package/host-service/product-runtime-host-origin.mjs +22 -0
- package/host-service/provider-model-params.mjs +14 -17
- package/host-service/provider-model-refresh-runner.mjs +124 -169
- package/host-service/provider-model-refresh-schedule.mjs +11 -0
- package/host-service/provider-resume-contract.mjs +9 -3
- package/host-service/restart-restore-state.mjs +25 -0
- package/host-service/retained-host-state-holder.mjs +57 -0
- package/host-service/routec-matrix-sqlite-store.mjs +155 -10
- package/host-service/routec-matrix-storage-adapter.mjs +208 -25
- package/host-service/routec-matrix-storage-paths.mjs +12 -0
- package/host-service/routec-request-delay-window.mjs +55 -0
- package/host-service/scheduler-acp-provider-runner.mjs +2 -1
- package/host-service/scheduler-direct-provider-runner.mjs +71 -9
- package/host-service/scheduler-run-timeout-policy.mjs +85 -0
- package/host-service/scheduler-service.mjs +96 -9
- package/host-service/scheduler-store.mjs +9 -2
- package/host-service/server.mjs +2202 -552
- package/host-service/service-definition-migration.mjs +10 -3
- package/host-service/session-collection-service.mjs +320 -0
- package/host-service/session-history.mjs +99 -15
- package/host-service/session-list-preview-service.mjs +87 -12
- package/host-service/session-list-projection-service.mjs +182 -0
- package/host-service/session-loop-definitions.mjs +702 -75
- package/host-service/session-manager.mjs +473 -38
- package/host-service/session-switcher-projection.mjs +11 -3
- package/host-service/session-tags.mjs +451 -0
- package/host-service/session-transcript-target.mjs +103 -0
- package/host-service/setup-input-file.mjs +91 -0
- package/host-service/setup-provider-model-defaults.mjs +53 -0
- package/host-service/setup.mjs +133 -101
- package/host-service/stopped-user-host-package.mjs +60 -0
- package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
- package/host-service/update-reminder-state.mjs +7 -0
- package/host-service/voice-input-receipt-store.mjs +60 -0
- package/package.json +1 -1
- package/static/oysterun-social-preview.png +0 -0
- package/static/site/assets/oysterun-social-preview.png +0 -0
- package/static/site/blog/en/index.html +17 -0
- package/static/site/blog/generated_manifest.json +9 -9
- package/static/site/blog/index.html +18 -0
- package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
- package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
- package/static/site/blog/zh/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
- package/static/site/docs/en/index.html +17 -0
- package/static/site/docs/generated_manifest.json +60 -60
- package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
- package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
- package/static/site/docs/index.html +18 -0
- package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
- package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
- package/static/site/docs/read-large-tool-results/en/index.html +17 -0
- package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
- package/static/site/docs/send-images-and-media/en/index.html +17 -0
- package/static/site/docs/send-images-and-media/zh/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
- package/static/site/docs/understand-agent-activity/en/index.html +19 -2
- package/static/site/docs/understand-agent-activity/en/index.md +2 -2
- package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
- package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
- package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
- package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
- package/static/site/docs/zh/index.html +17 -0
- package/static/site/en/index.html +18 -1
- package/static/site/en/index.md +1 -1
- package/static/site/faq/en/index.html +17 -0
- package/static/site/faq/index.html +18 -0
- package/static/site/faq/zh/index.html +17 -0
- package/static/site/getting-started/en/index.html +23 -1
- package/static/site/getting-started/en/index.md +7 -1
- package/static/site/getting-started/index.html +18 -0
- package/static/site/getting-started/zh/index.html +23 -1
- package/static/site/getting-started/zh/index.md +7 -1
- package/static/site/index.html +18 -0
- package/static/site/install.sh +676 -127
- package/static/site/overview/en/index.html +18 -1
- package/static/site/overview/en/index.md +2 -2
- package/static/site/overview/index.html +18 -0
- package/static/site/overview/zh/index.html +18 -1
- package/static/site/overview/zh/index.md +2 -2
- package/static/site/privacy/en/index.html +17 -0
- package/static/site/privacy/index.html +18 -0
- package/static/site/privacy/zh/index.html +17 -0
- package/static/site/zh/index.html +18 -1
- package/static/site/zh/index.md +1 -1
- package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
- package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
- package/static/site_source/downloads/install.sh +676 -127
- package/static/site_source/manifests/host_install.json +2 -10
- package/static/site_source/pages/getting-started/en.html +6 -1
- package/static/site_source/pages/getting-started/zh.html +6 -1
- package/static/site_source/pages/home/en.html +1 -1
- package/static/site_source/pages/home/zh.html +1 -1
- package/static/site_source/pages/home_main.html +3 -2
- package/static/site_source/pages/overview/en.html +1 -1
- package/static/site_source/pages/overview/zh.html +1 -1
- package/tool_scripts/install_oysterun_update.mjs +78 -13
- package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
- package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
- package/tool_scripts/oysterun_release_service.sh +5 -0
- package/tool_scripts/oysterun_service_common.sh +59 -24
- package/tool_scripts/oysterun_setup_input.sh +97 -0
- package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
- package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
- package/tool_scripts/resolve_user_host_port.mjs +15 -0
- package/tool_scripts/restart_oysterun.sh +8 -2
- package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
- package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
- package/tool_scripts/sync_host_stack_config.mjs +9 -6
- package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
- package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
- package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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 } = {}) {
|