@fased/fased 0.1.9 → 0.1.10

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 (160) hide show
  1. package/dist/{agents-C4jz1huI.js → agents-CsxQP5xg.js} +8 -8
  2. package/dist/{attachment-normalize-C-VpVpLd.js → attachment-normalize-CSxeaVcP.js} +1 -1
  3. package/dist/{attachment-normalize-DijgHIHy.js → attachment-normalize-f9lQXrZo.js} +1 -1
  4. package/dist/{audio-preflight-CI7BIPwS.js → audio-preflight-BeJd4YPQ.js} +2 -2
  5. package/dist/{audio-preflight-U5m63Clr.js → audio-preflight-CVU3GLti.js} +2 -2
  6. package/dist/{audio-preflight-oqlL2Kux.js → audio-preflight-C_g_wQAc.js} +1 -1
  7. package/dist/{audio-preflight-C6Lb4XU1.js → audio-preflight-DBtzpOL_.js} +1 -1
  8. package/dist/{audio-preflight-DLGJBu9O.js → audio-preflight-DD1zXhwM.js} +2 -2
  9. package/dist/{audit-DwbVJt3i.js → audit-BB2lDlNb.js} +1 -1
  10. package/dist/{audit-BCtx-_dH.js → audit-CDgqnjys.js} +1 -1
  11. package/dist/{auth-DIBzLQ-S.js → auth-Bfgwy6TQ.js} +5 -5
  12. package/dist/{auth-choice-CEeiz47B.js → auth-choice-DbEAsUsi.js} +1 -1
  13. package/dist/{auth-choice-DOqXUqF_.js → auth-choice-DlcbJgp_.js} +1 -1
  14. package/dist/{auth-choice-prompt-yqlvFI-_.js → auth-choice-prompt-CcOEzXoi.js} +1 -1
  15. package/dist/{auth-choice-prompt-FJDk83bS.js → auth-choice-prompt-DMlwK9kF.js} +1 -1
  16. package/dist/build-info.json +3 -3
  17. package/dist/bundled/boot-md/handler.js +1 -1
  18. package/dist/bundled/session-memory/handler.js +1 -1
  19. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  20. package/dist/{channel-web-CBfPvCCy.js → channel-web-CMZFjZzU.js} +1 -1
  21. package/dist/{channel-web-DaircVlX.js → channel-web-qHKfuCHE.js} +1 -1
  22. package/dist/{channels-BygboZOV.js → channels-BoOF2dAy.js} +6 -6
  23. package/dist/{channels-DgozxTBh.js → channels-DvT6s1kl.js} +6 -6
  24. package/dist/{channels-cli-DNz7gmOI.js → channels-cli-CNhEFKbn.js} +5 -5
  25. package/dist/{channels-cli-CY4n5HcO.js → channels-cli-NoUnbEgC.js} +5 -5
  26. package/dist/cli/daemon-cli.js +1 -1
  27. package/dist/{cli-CFgX3wef.js → cli-CE8pVYeA.js} +2 -2
  28. package/dist/{cli-63C0cETT.js → cli-Ctvy4-cz.js} +2 -2
  29. package/dist/{command-registry-DEiX9EPg.js → command-registry-fqlYdOTI.js} +9 -9
  30. package/dist/{completion-cli-CRwAyWQL.js → completion-cli-Bd9iPo4A.js} +1 -1
  31. package/dist/{completion-cli-BzQetGgr.js → completion-cli-CKzB1lB1.js} +2 -2
  32. package/dist/{config-cli-CPl1vPz0.js → config-cli-CQpqSVFS.js} +1 -1
  33. package/dist/{config-cli-C7vsh8Fg.js → config-cli-DUNkddbL.js} +1 -1
  34. package/dist/{configure-BEI_pe6B.js → configure-B2WT8-lE.js} +12 -12
  35. package/dist/{configure-C8lxNKpB.js → configure-DxDNasaD.js} +12 -12
  36. package/dist/control-ui/assets/app-wpJSg6bV.js.map +1 -1
  37. package/dist/{cron-cli-BqD_ZSEC.js → cron-cli-D3Dznk0V.js} +2 -2
  38. package/dist/{cron-cli-C7i4-Whp.js → cron-cli-DYWW7cv1.js} +2 -2
  39. package/dist/{daemon-cli-DFYzYoB8.js → daemon-cli-DDUjmEow.js} +1 -1
  40. package/dist/{daemon-cli-D0AP3mOg.js → daemon-cli-DFjCvbUo.js} +1 -1
  41. package/dist/daemon-cli.js +1 -1
  42. package/dist/{deps-D9aJvXyD.js → deps-4rCQ5SBd.js} +1 -1
  43. package/dist/{deps-ddWLkZAO.js → deps-CBapZnsi.js} +1 -1
  44. package/dist/entry.js +1 -1
  45. package/dist/extensionAPI.js +2 -2
  46. package/dist/{gateway-cli-D_Yd_yWi.js → gateway-cli-BXl9lmch.js} +27 -27
  47. package/dist/{gateway-cli-BJkYYsGs.js → gateway-cli-CNCA_gcT.js} +27 -27
  48. package/dist/{health-DCFLGVDU.js → health-C_g99tp_.js} +2 -2
  49. package/dist/{health-gyJixAyN.js → health-DTTfLYjl.js} +2 -2
  50. package/dist/{heartbeat-runner-Bn9N5npL.js → heartbeat-runner-D04n14H-.js} +1 -1
  51. package/dist/{heartbeat-runner-BFUvc4Y_.js → heartbeat-runner-DXh84Wuy.js} +1 -1
  52. package/dist/{hooks-cli-CGj2FDai.js → hooks-cli-BDpBNlPV.js} +3 -3
  53. package/dist/{hooks-cli-BGFJQWmK.js → hooks-cli-FhVd5CE-.js} +3 -3
  54. package/dist/index.js +7 -7
  55. package/dist/{ipv4-CjO0_Ry3.js → ipv4-Citn8Cno.js} +2 -2
  56. package/dist/{ipv4-CBSPFYQB.js → ipv4-P47TSJIK.js} +2 -2
  57. package/dist/{lifecycle-DFvIF7r0.js → lifecycle-B4RibYD4.js} +1 -1
  58. package/dist/{lifecycle-BBNnNoX_.js → lifecycle-Bgh3V8Jt.js} +1 -1
  59. package/dist/{list.auth-overview-DySKd1cu.js → list.auth-overview-BQzrIpgk.js} +1 -1
  60. package/dist/{list.auth-overview-CpmtoN2D.js → list.auth-overview-fBVXihg0.js} +1 -1
  61. package/dist/llm-slug-generator.js +1 -1
  62. package/dist/{model-catalog-DfLJlqO8.js → model-catalog-B2mRe6cZ.js} +6 -6
  63. package/dist/{models-BbfLyMBF.js → models-D8MTK6SG.js} +4 -4
  64. package/dist/{models-cli-Bu09lWlb.js → models-cli-Bl8xk6-0.js} +4 -4
  65. package/dist/{models-cli-DpmciZv6.js → models-cli-CXdCnyks.js} +5 -5
  66. package/dist/{node-cli-D6iG4Fev.js → node-cli-Cqh8buCU.js} +2 -2
  67. package/dist/{node-cli-GbiqR_FY.js → node-cli-DkgKrYS1.js} +2 -2
  68. package/dist/{onboard-BgU-rNfl.js → onboard-BD0MOrv7.js} +6 -6
  69. package/dist/{onboard-CViA2g_j.js → onboard-Dz8z4vBi.js} +6 -6
  70. package/dist/{onboard-channels-gHaSwzTd.js → onboard-channels-CPh2EJOi.js} +2 -2
  71. package/dist/{onboard-channels-BeKekf-a.js → onboard-channels-DUa2qkms.js} +2 -2
  72. package/dist/{onboard-search-DkOnHZm9.js → onboard-search-BnHMgV2k.js} +5 -5
  73. package/dist/{onboard-search-COuzKj6_.js → onboard-search-CqiDzWQw.js} +5 -5
  74. package/dist/{onboarding-CowfhJUD.js → onboarding-BC-5Pv9o.js} +7 -7
  75. package/dist/{onboarding-nabyJljA.js → onboarding-DgTB_zq7.js} +7 -7
  76. package/dist/{openresponses-http-D42SXipZ.js → openresponses-http-BQa9rI-N.js} +1 -1
  77. package/dist/{openresponses-http-C66CR36w.js → openresponses-http-BYiaPg-e.js} +2 -2
  78. package/dist/{openresponses-http-CdO4hFg_.js → openresponses-http-BirGBJTm.js} +2 -2
  79. package/dist/{openresponses-http-One5KNo8.js → openresponses-http-BsQmM8zM.js} +1 -1
  80. package/dist/{openresponses-http-BBydvP41.js → openresponses-http-CcawkBY4.js} +2 -2
  81. package/dist/{parent-default-help-82dOtJ5b.js → parent-default-help-CpfuGajV.js} +1 -1
  82. package/dist/{parent-default-help-iR__S_d8.js → parent-default-help-DbMf7NQe.js} +1 -1
  83. package/dist/{paths-Dwg5yVao.js → paths-BxKqczZV.js} +9 -9
  84. package/dist/{pi-embedded-BqhMzKsb.js → pi-embedded-Bapdl5Mb.js} +5 -5
  85. package/dist/{pi-embedded-DLEbV2ey.js → pi-embedded-DlBsi5bC.js} +14 -14
  86. package/dist/{plugin-registry-DgLnDYKc.js → plugin-registry-2aqMgXpl.js} +1 -1
  87. package/dist/{plugin-registry-DgnprUWu.js → plugin-registry-C7oTQytM.js} +1 -1
  88. package/dist/plugin-sdk/{audio-preflight-bl33qm06.js → audio-preflight-BbjERcpR.js} +1 -1
  89. package/dist/plugin-sdk/{audio-preflight-B3yRlgpY.js → audio-preflight-Vg4EuYfg.js} +1 -1
  90. package/dist/plugin-sdk/channel-plugin-common.js +1 -1
  91. package/dist/plugin-sdk/{channel-web-BmhttJUt.js → channel-web-JBsCrpEe.js} +1 -1
  92. package/dist/plugin-sdk/command-status.js +1 -1
  93. package/dist/plugin-sdk/index.js +2 -2
  94. package/dist/plugin-sdk/{openresponses-http-BkfMH5vT.js → openresponses-http-dj-a8V7Z.js} +1 -1
  95. package/dist/plugin-sdk/{openresponses-http-DVsmkLyj.js → openresponses-http-ihpGpya5.js} +1 -1
  96. package/dist/plugin-sdk/{pi-model-discovery-runtime-Cv_H-NDs.js → pi-model-discovery-runtime-CddkbYKA.js} +1 -1
  97. package/dist/plugin-sdk/{registry-Dpq_OloZ.js → registry-BhjWgT6A.js} +8 -8
  98. package/dist/plugin-sdk/{reply-ceu7kXX1.js → reply-CvmqbVx1.js} +5 -5
  99. package/dist/plugin-sdk/src/brand.d.ts +2 -2
  100. package/dist/plugin-sdk/{status-BcZw5R2P.js → status-DsTgMPSP.js} +5 -5
  101. package/dist/plugin-sdk/{web-DE0yQyjF.js → web-CLXmBst6.js} +1 -1
  102. package/dist/plugin-sdk/{web-D_ze88LT.js → web-DHSHEbWK.js} +2 -2
  103. package/dist/{plugins-cli-gUow1y1s.js → plugins-cli-CXa9m7-A.js} +5 -5
  104. package/dist/{plugins-cli-Bw8rkcLm.js → plugins-cli-Dj-p00ob.js} +5 -5
  105. package/dist/{program-D7MpzAVA.js → program-B3cXsVO0.js} +7 -7
  106. package/dist/{program-context-BnMmmcLR.js → program-context-Di7qiwOM.js} +24 -24
  107. package/dist/{prompt-select-styled-CZx9cLwR.js → prompt-select-styled-8KBIPNro.js} +4 -4
  108. package/dist/{prompt-select-styled-DEZExK7l.js → prompt-select-styled-C5o0Ff06.js} +4 -4
  109. package/dist/{pw-ai-Cjyb5RBL.js → pw-ai-BzOzOwqG.js} +1 -1
  110. package/dist/{register.agent-B9A7SDz_.js → register.agent-BYkIkPHf.js} +8 -8
  111. package/dist/{register.agent-DlatKWlA.js → register.agent-ll5ykI9x.js} +9 -9
  112. package/dist/{register.configure-Tez2eNbW.js → register.configure-C0GU1zbA.js} +12 -12
  113. package/dist/{register.configure-yNuOFQX6.js → register.configure-DubkYh7h.js} +12 -12
  114. package/dist/{register.maintenance-Q6YI5mk9.js → register.maintenance-Corgqhj5.js} +8 -8
  115. package/dist/{register.maintenance-BzVw1_e5.js → register.maintenance-Pz6desR4.js} +9 -9
  116. package/dist/{register.message-Cda69Xe6.js → register.message-COK8wYD_.js} +3 -3
  117. package/dist/{register.message-D9LYOvON.js → register.message-oi2ewJpl.js} +3 -3
  118. package/dist/{register.onboard-D1a2rQDh.js → register.onboard-C7ysxMn6.js} +14 -14
  119. package/dist/{register.onboard-BoLP06ui.js → register.onboard-zwRY0Dsh.js} +14 -14
  120. package/dist/{register.setup-DhppZhR2.js → register.setup-B1YGF1bz.js} +14 -14
  121. package/dist/{register.setup-D_qD7Gvk.js → register.setup-J2tUgD6C.js} +14 -14
  122. package/dist/{register.status-health-sessions-Csah3svc.js → register.status-health-sessions-DCazjMZa.js} +6 -6
  123. package/dist/{register.status-health-sessions-C0IsjCxS.js → register.status-health-sessions-DmjjMrJZ.js} +6 -6
  124. package/dist/{register.subclis-C4Dh6aQK.js → register.subclis-Bj8pFyAT.js} +16 -16
  125. package/dist/{reply-C3PsVpeQ.js → reply-DQfPM5ia.js} +6 -6
  126. package/dist/{run-main-XXpwNNGn.js → run-main-Cf6N-U3j.js} +14 -14
  127. package/dist/{runtime-helper-grants-BTCk3CzW.js → runtime-helper-grants-DONSfqWx.js} +1 -1
  128. package/dist/{runtime-helper-grants-C10OJEih.js → runtime-helper-grants-DzP6dgWv.js} +1 -1
  129. package/dist/{sandbox-cli-2MuBwFbv.js → sandbox-cli-CqUHfq55.js} +2 -2
  130. package/dist/{sandbox-cli-Bft002rK.js → sandbox-cli-DHHAXWD8.js} +2 -2
  131. package/dist/{security-cli-6CmGeQ7I.js → security-cli-CAEEYR8C.js} +3 -3
  132. package/dist/{security-cli-DpQep_pt.js → security-cli-DNlCSlQR.js} +3 -3
  133. package/dist/{server-OidOCKoj.js → server-B67PKank.js} +2 -2
  134. package/dist/{server-D62kYBqw.js → server-RYyJ2HTj.js} +2 -2
  135. package/dist/{server-cron-DYKXfeLU.js → server-cron-D7rN6EY6.js} +2 -2
  136. package/dist/{server-cron--g6K0O5V.js → server-cron-V9GBrg1S.js} +2 -2
  137. package/dist/{server-node-events-DSvH0zky.js → server-node-events-CAG2ic6e.js} +3 -3
  138. package/dist/{server-node-events-Csbl1Sw5.js → server-node-events-eh5LuLg6.js} +3 -3
  139. package/dist/{status-CEqTBWFX.js → status-BuGyo-1M.js} +4 -4
  140. package/dist/{status-cY8_nNEN.js → status-CTveTSuF.js} +1 -1
  141. package/dist/{status-DDWMOp5e.js → status-DIkbKBxH.js} +4 -4
  142. package/dist/{status-e52jwOFE.js → status-jb8w0SwX.js} +1 -1
  143. package/dist/{tui-K3OJBQvo.js → tui-Dinku0o-.js} +1 -1
  144. package/dist/{tui-cli-DEHMUby1.js → tui-cli-dq7xafwR.js} +3 -3
  145. package/dist/{tui-cli-CNL0cM4e.js → tui-cli-z5WV3SVg.js} +3 -3
  146. package/dist/{tui-B5xfA4bO.js → tui-lmdDZi6Y.js} +1 -1
  147. package/dist/{update-cli-BqiZ9j4m.js → update-cli-BdQ3mRGs.js} +10 -10
  148. package/dist/{update-cli-Cr7BTqe1.js → update-cli-Cq47v0X_.js} +9 -9
  149. package/dist/{update-runner-CycjY65W.js → update-runner-BnRMhKva.js} +1 -1
  150. package/dist/{update-runner-DcpjVgBR.js → update-runner-C0SU1jGT.js} +1 -1
  151. package/dist/{web-CLOmxVxX.js → web-B9YrSF5u.js} +2 -2
  152. package/dist/{web-6MnkoZyD.js → web-BmjUKJI0.js} +3 -3
  153. package/dist/{web-ChgN9rvX.js → web-D9wmjTDC.js} +3 -3
  154. package/dist/{web-D6CU81WZ.js → web-DaMiLN7K.js} +1 -1
  155. package/dist/{web-DhwWGdvc.js → web-sBExtZCA.js} +1 -1
  156. package/dist/{web-search-providers.runtime-D-uepqZ5.js → web-search-providers.runtime-TGCiBzhv.js} +1 -1
  157. package/dist/{web-search-providers.runtime-u6_OyUd6.js → web-search-providers.runtime-gM-8LIxg.js} +1 -1
  158. package/package.json +3 -1
  159. package/scripts/run-node.mjs +294 -0
  160. package/scripts/start-managed.sh +1528 -0
@@ -0,0 +1,1528 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/start-managed.sh
3
+ # Starts the FasedAgent in managed public runtime mode.
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ FASED_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
6
+ INSTALL_MARKER_PATH="${FASED_CONFIG_DIR:-$HOME/.fased}/install-complete.json"
7
+ RUN_NODE_SCRIPT="$FASED_ROOT/scripts/run-node.mjs"
8
+
9
+ node_runtime_ok_for() {
10
+ local node_bin="$1"
11
+ [[ -n "$node_bin" && -x "$node_bin" ]] || return 1
12
+ "$node_bin" -e 'const [major, minor] = process.versions.node.split(".").map(Number); if (major < 22 || (major === 22 && minor < 14)) process.exit(2); try { require("node:sqlite"); } catch { process.exit(3); }' >/dev/null 2>&1
13
+ }
14
+
15
+ resolve_node_bin() {
16
+ if node_runtime_ok_for "${FASED_NODE_BIN:-}"; then
17
+ printf '%s\n' "${FASED_NODE_BIN}"
18
+ return 0
19
+ fi
20
+
21
+ local candidate
22
+ for candidate in \
23
+ "$HOME"/.nvm/versions/node/*/bin/node \
24
+ "$HOME"/.fnm/node-versions/*/installation/bin/node \
25
+ "$HOME"/.volta/bin/node \
26
+ "$HOME"/.asdf/shims/node \
27
+ "$HOME"/.local/share/mise/shims/node \
28
+ /usr/bin/node \
29
+ /usr/local/bin/node \
30
+ /opt/homebrew/bin/node; do
31
+ [[ -e "$candidate" ]] || continue
32
+ if node_runtime_ok_for "$candidate"; then
33
+ printf '%s\n' "$candidate"
34
+ return 0
35
+ fi
36
+ done
37
+
38
+ if command -v node >/dev/null 2>&1 && node_runtime_ok_for "$(command -v node)"; then
39
+ command -v node
40
+ return 0
41
+ fi
42
+ return 1
43
+ }
44
+
45
+ if [[ "${FASED_MANAGED_INTERNAL:-0}" != "1" ]]; then
46
+ if [[ ! -f "$INSTALL_MARKER_PATH" ]]; then
47
+ echo "==> Canonical onboarding not detected ($INSTALL_MARKER_PATH missing)."
48
+ echo "==> Recommended first run: $FASED_ROOT/install.sh"
49
+ if [[ "${FASED_AUTO_INSTALL_ON_START:-0}" == "1" ]]; then
50
+ echo "==> Auto-bootstrap enabled (FASED_AUTO_INSTALL_ON_START=1): running installer in no-start mode..."
51
+ "$FASED_ROOT/install.sh" --no-start
52
+ fi
53
+ fi
54
+ echo "==> Delegating to managed orchestrator (node $RUN_NODE_SCRIPT managed up)..."
55
+ NODE_BIN="$(resolve_node_bin || true)"
56
+ if [[ -z "$NODE_BIN" ]]; then
57
+ echo "==> ERROR: compatible node binary not found. Install Node 24 or Node >=22.14.0 with node:sqlite."
58
+ exit 1
59
+ fi
60
+ export PATH="$(dirname "$NODE_BIN"):$PATH"
61
+ FASED_SKIP_BUILD="${FASED_SKIP_BUILD:-1}" exec "$NODE_BIN" "$RUN_NODE_SCRIPT" managed up "$@"
62
+ fi
63
+
64
+ set -euo pipefail
65
+
66
+ NODE_BIN="$(resolve_node_bin || true)"
67
+ if [[ -z "$NODE_BIN" ]]; then
68
+ echo "[managed] ERROR: compatible node binary not found. Install Node 24 or Node >=22.14.0 with node:sqlite, then reinstall or restart the gateway service."
69
+ exit 1
70
+ fi
71
+
72
+ # Config
73
+ export PATH="$(dirname "$NODE_BIN"):$HOME/.zrok/bin:$PATH"
74
+ export FASED_GATEWAY_MODE=managed
75
+ export FASED_GATEWAY_FAST_START="${FASED_GATEWAY_FAST_START:-1}"
76
+ export FASED_GATEWAY_STARTUP_TRACE="${FASED_GATEWAY_STARTUP_TRACE:-1}"
77
+ export FASED_DISABLE_CONTROL_UI_AUTOBUILD="${FASED_DISABLE_CONTROL_UI_AUTOBUILD:-1}"
78
+ export FASED_FEDERATION_URL=https://ff1.fased.app
79
+ export FASED_GATEWAY_PORT="${FASED_GATEWAY_PORT:-18789}"
80
+ export FASED_CONFIG_DIR="${FASED_CONFIG_DIR:-$HOME/.fased}"
81
+ FASED_GATEWAY_MAX_OLD_SPACE_MB="${FASED_GATEWAY_MAX_OLD_SPACE_MB:-1024}"
82
+ if [[ "${NODE_OPTIONS:-}" != *"--max-old-space-size="* ]]; then
83
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=${FASED_GATEWAY_MAX_OLD_SPACE_MB}"
84
+ fi
85
+ TOKEN_PATH="$FASED_CONFIG_DIR/federation/access-token.json"
86
+ GW_TOKEN_PATH="$FASED_CONFIG_DIR/gateway-secret"
87
+ INITIAL_TOKEN_SIG=""
88
+ LOG_DIR="${FASED_LOG_DIR:-$FASED_CONFIG_DIR/logs}"
89
+ GATEWAY_BOOT_LOG="$LOG_DIR/start-managed-gateway.log"
90
+ ZROK_RUNTIME_LOG="$LOG_DIR/start-managed-zrok.log"
91
+ WALLET_SETUP_LOG="$LOG_DIR/start-managed-wallet.log"
92
+ VERBOSE_STARTUP="${FASED_VERBOSE_STARTUP:-0}"
93
+ # First boots on small VPS can take several minutes (control-ui asset prep, cold fs/cache, etc).
94
+ # Keep timeout generous to avoid restart loops that kill startup progress before listener comes up.
95
+ GATEWAY_READY_TIMEOUT="${FASED_GATEWAY_READY_TIMEOUT:-600}"
96
+ WALLET_BASELINE_MODE="${FASED_WALLET_BASELINE_MODE:-enforce}" # enforce|skip
97
+ WALLET_BASELINE_TIMEOUT_SECONDS="${FASED_WALLET_BASELINE_TIMEOUT_SECONDS:-25}"
98
+ SIGNERD_READY_TIMEOUT_SECONDS="${FASED_SIGNERD_READY_TIMEOUT_SECONDS:-10}"
99
+ ZROK_MONITOR_PID_FILE="$FASED_CONFIG_DIR/.zrok-monitor.pid"
100
+ ZROK_MONITOR_PID=""
101
+ ZROK_INITIAL_START_RETRIES="${FASED_ZROK_INITIAL_START_RETRIES:-5}"
102
+ ZROK_INITIAL_START_RETRY_DELAY_SECONDS="${FASED_ZROK_INITIAL_START_RETRY_DELAY_SECONDS:-2}"
103
+ CLOCK_SYNC_SKEW_THRESHOLD_SECONDS="${FASED_CLOCK_SYNC_SKEW_THRESHOLD_SECONDS:-2}"
104
+
105
+ resolve_gateway_cli_entry() {
106
+ local candidates=(
107
+ "$FASED_ROOT/dist/index.js"
108
+ "$FASED_ROOT/dist/index.mjs"
109
+ "$FASED_ROOT/dist/entry.js"
110
+ "$FASED_ROOT/dist/entry.mjs"
111
+ )
112
+ local candidate
113
+ for candidate in "${candidates[@]}"; do
114
+ if [[ -f "$candidate" ]]; then
115
+ printf "%s" "$candidate"
116
+ return 0
117
+ fi
118
+ done
119
+ return 1
120
+ }
121
+
122
+ mask_secret() {
123
+ local raw="$1"
124
+ local n=${#raw}
125
+ if [[ $n -le 10 ]]; then
126
+ printf "%s" "$raw"
127
+ return
128
+ fi
129
+ printf "%s...%s" "${raw:0:6}" "${raw: -4}"
130
+ }
131
+
132
+ is_gateway_listener_ready() {
133
+ (echo >"/dev/tcp/127.0.0.1/${FASED_GATEWAY_PORT}") >/dev/null 2>&1
134
+ }
135
+
136
+ abs_int() {
137
+ local value="${1:-0}"
138
+ value="${value#-}"
139
+ if [[ -z "$value" ]]; then
140
+ value="0"
141
+ fi
142
+ printf '%s\n' "$value"
143
+ }
144
+
145
+ can_run_privileged_time_sync_command() {
146
+ if [[ "$(id -u)" -eq 0 ]]; then
147
+ return 0
148
+ fi
149
+ command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1
150
+ }
151
+
152
+ run_privileged_time_sync_command() {
153
+ if [[ "$(id -u)" -eq 0 ]]; then
154
+ "$@"
155
+ return $?
156
+ fi
157
+ sudo -n "$@"
158
+ }
159
+
160
+ fetch_remote_epoch_from_http_date() {
161
+ local url="$1"
162
+ local date_header=""
163
+ local remote_epoch=""
164
+ date_header="$(
165
+ curl -fsSI --max-time 10 "$url" 2>/dev/null \
166
+ | tr -d '\r' \
167
+ | awk 'BEGIN{IGNORECASE=1} /^Date:/ { sub(/^Date:[[:space:]]*/, "", $0); print; exit }'
168
+ )"
169
+ if [[ -z "$date_header" ]]; then
170
+ return 1
171
+ fi
172
+ remote_epoch="$(date -u -d "$date_header" +%s 2>/dev/null || true)"
173
+ if [[ -z "$remote_epoch" ]]; then
174
+ return 1
175
+ fi
176
+ printf '%s\n' "$remote_epoch"
177
+ }
178
+
179
+ measure_managed_clock_skew_seconds() {
180
+ local probe_urls=(
181
+ "${ZROK_API_ENDPOINT:-https://zrok.fased.app}"
182
+ "${FASED_FEDERATION_URL:-https://ff1.fased.app}"
183
+ )
184
+ local url=""
185
+ local remote_epoch=""
186
+ local local_epoch=""
187
+ for url in "${probe_urls[@]}"; do
188
+ remote_epoch="$(fetch_remote_epoch_from_http_date "$url" || true)"
189
+ if [[ -z "$remote_epoch" ]]; then
190
+ continue
191
+ fi
192
+ local_epoch="$(date -u +%s)"
193
+ printf '%s\n' "$((remote_epoch - local_epoch))"
194
+ return 0
195
+ done
196
+ return 1
197
+ }
198
+
199
+ zrok_log_indicates_clock_skew() {
200
+ tail -n 80 "$ZROK_RUNTIME_LOG" 2>/dev/null | grep -Fqi "issuedAt of token is in the future"
201
+ }
202
+
203
+ attempt_managed_clock_sync_repair() {
204
+ if ! can_run_privileged_time_sync_command; then
205
+ return 1
206
+ fi
207
+ echo "[tunnel] Attempting automatic host clock sync repair..."
208
+ run_privileged_time_sync_command timedatectl set-ntp true >/dev/null 2>&1 || true
209
+ run_privileged_time_sync_command systemctl restart systemd-timesyncd >/dev/null 2>&1 || \
210
+ run_privileged_time_sync_command systemctl restart chronyd >/dev/null 2>&1 || true
211
+ if command -v chronyc >/dev/null 2>&1; then
212
+ run_privileged_time_sync_command chronyc -a makestep >/dev/null 2>&1 || true
213
+ fi
214
+ sleep 2
215
+ }
216
+
217
+ ensure_managed_clock_sync() {
218
+ local threshold="${CLOCK_SYNC_SKEW_THRESHOLD_SECONDS:-2}"
219
+ local skew_seconds=""
220
+ local remaining_skew=""
221
+
222
+ skew_seconds="$(measure_managed_clock_skew_seconds || true)"
223
+ if [[ -z "$skew_seconds" ]]; then
224
+ return 0
225
+ fi
226
+ if (( $(abs_int "$skew_seconds") < threshold )); then
227
+ return 0
228
+ fi
229
+
230
+ echo "[tunnel] WARNING: Host clock skew detected (${skew_seconds}s versus public control plane)."
231
+ if ! attempt_managed_clock_sync_repair; then
232
+ echo "[tunnel] WARNING: Automatic host clock repair requires root or passwordless sudo."
233
+ return 1
234
+ fi
235
+
236
+ remaining_skew="$(measure_managed_clock_skew_seconds || true)"
237
+ if [[ -n "$remaining_skew" ]] && (( $(abs_int "$remaining_skew") < threshold )); then
238
+ echo "[tunnel] Host clock sync repaired (remaining skew ${remaining_skew}s)."
239
+ return 0
240
+ fi
241
+
242
+ echo "[tunnel] WARNING: Host clock repair did not fully converge (remaining skew ${remaining_skew:-unknown}s)."
243
+ return 1
244
+ }
245
+
246
+ start_initial_zrok_share() {
247
+ local slug="$1"
248
+ local attempts="${ZROK_INITIAL_START_RETRIES:-5}"
249
+ local retry_delay="${ZROK_INITIAL_START_RETRY_DELAY_SECONDS:-2}"
250
+ local attempt=1
251
+
252
+ while (( attempt <= attempts )); do
253
+ echo "[tunnel] Starting tunnel for ${slug} (attempt ${attempt}/${attempts})..."
254
+ "$ZROK_BIN" share reserved "$RES_TOKEN" --headless >>"$ZROK_RUNTIME_LOG" 2>&1 &
255
+ ZROK_PID=$!
256
+ echo "$ZROK_PID" > "$FASED_CONFIG_DIR/.zrok-pid"
257
+ sleep 5
258
+
259
+ if ! kill -0 "$AGENT_PID" 2>/dev/null; then
260
+ if is_gateway_listener_ready; then
261
+ echo "[gateway] WARNING: Gateway launcher exited, but listener is healthy on 127.0.0.1:${FASED_GATEWAY_PORT}; continuing."
262
+ else
263
+ echo "[gateway] ERROR: Gateway process exited during tunnel startup."
264
+ if [[ "$VERBOSE_STARTUP" != "1" ]]; then
265
+ echo "[debug] Last gateway startup logs:"
266
+ tail -n 80 "$GATEWAY_BOOT_LOG" || true
267
+ fi
268
+ return 1
269
+ fi
270
+ fi
271
+
272
+ if ! is_gateway_listener_ready; then
273
+ echo "[gateway] ERROR: Gateway listener on 127.0.0.1:${FASED_GATEWAY_PORT} is not reachable."
274
+ if [[ "$VERBOSE_STARTUP" != "1" ]]; then
275
+ echo "[debug] Last gateway startup logs:"
276
+ tail -n 80 "$GATEWAY_BOOT_LOG" || true
277
+ fi
278
+ return 1
279
+ fi
280
+
281
+ if kill -0 "$ZROK_PID" 2>/dev/null; then
282
+ echo "[tunnel] ✓ Tunnel active."
283
+ return 0
284
+ fi
285
+
286
+ echo "[tunnel] WARNING: zrok share died during startup."
287
+ if [[ "$VERBOSE_STARTUP" != "1" ]]; then
288
+ echo "[debug] Last zrok startup logs:"
289
+ tail -n 40 "$ZROK_RUNTIME_LOG" || true
290
+ fi
291
+ if zrok_log_indicates_clock_skew; then
292
+ echo "[tunnel] Detected zrok auth failure caused by host clock skew."
293
+ ensure_managed_clock_sync || true
294
+ fi
295
+
296
+ if (( attempt == attempts )); then
297
+ echo "[tunnel] ERROR: zrok share failed to stay up after ${attempts} attempts."
298
+ return 1
299
+ fi
300
+
301
+ echo "[tunnel] Retrying zrok startup in ${retry_delay}s..."
302
+ sleep "$retry_delay"
303
+ if [[ "$retry_delay" =~ ^[0-9]+$ ]] && (( retry_delay < 8 )); then
304
+ retry_delay=$((retry_delay * 2))
305
+ fi
306
+ attempt=$((attempt + 1))
307
+ done
308
+
309
+ return 1
310
+ }
311
+
312
+ force_stop_local_gateway() {
313
+ if ! is_gateway_listener_ready; then
314
+ return 0
315
+ fi
316
+ if command -v fuser >/dev/null 2>&1; then
317
+ fuser -k "${FASED_GATEWAY_PORT}/tcp" >/dev/null 2>&1 || true
318
+ fi
319
+ if command -v lsof >/dev/null 2>&1; then
320
+ mapfile -t PORT_PIDS < <(lsof -t -iTCP:"${FASED_GATEWAY_PORT}" -sTCP:LISTEN 2>/dev/null || true)
321
+ if [[ ${#PORT_PIDS[@]} -gt 0 ]]; then
322
+ kill "${PORT_PIDS[@]}" >/dev/null 2>&1 || true
323
+ fi
324
+ fi
325
+ pkill -f "scripts/run-node.mjs gateway" >/dev/null 2>&1 || true
326
+ pkill -f "fased-gateway" >/dev/null 2>&1 || true
327
+ }
328
+
329
+ wait_for_gateway_listener() {
330
+ local retries="$1"
331
+ local count=0
332
+ echo "==> Waiting for local gateway listener on 127.0.0.1:${FASED_GATEWAY_PORT} (max ${retries}s)..."
333
+ while true; do
334
+ if is_gateway_listener_ready; then
335
+ return 0
336
+ fi
337
+ if ! kill -0 "$AGENT_PID" 2>/dev/null; then
338
+ echo "[gateway] ERROR: Gateway process exited before listener became ready."
339
+ if [[ "$VERBOSE_STARTUP" != "1" ]]; then
340
+ echo "[debug] Last gateway startup logs:"
341
+ tail -n 80 "$GATEWAY_BOOT_LOG" || true
342
+ fi
343
+ return 1
344
+ fi
345
+ sleep 1
346
+ count=$((count + 1))
347
+ if [[ $count -ge $retries ]]; then
348
+ echo "[gateway] ERROR: Timed out waiting for local listener on port ${FASED_GATEWAY_PORT}."
349
+ if [[ "$VERBOSE_STARTUP" != "1" ]]; then
350
+ echo "[debug] Last gateway startup logs:"
351
+ tail -n 80 "$GATEWAY_BOOT_LOG" || true
352
+ fi
353
+ return 1
354
+ fi
355
+ done
356
+ }
357
+
358
+ start_gateway_if_needed() {
359
+ if [[ -n "${AGENT_PID:-}" ]] && kill -0 "$AGENT_PID" 2>/dev/null; then
360
+ if is_gateway_listener_ready; then
361
+ return 0
362
+ fi
363
+ fi
364
+
365
+ echo "==> Starting FasedAgent Gateway..."
366
+ # Capture pre-existing token fingerprint so we can detect a real refresh.
367
+ if [[ -f "$TOKEN_PATH" ]]; then
368
+ INITIAL_TOKEN_SIG=$(sha256sum "$TOKEN_PATH" | awk '{print $1}')
369
+ fi
370
+ GATEWAY_ENTRY="$(resolve_gateway_cli_entry || true)"
371
+ if [[ -z "$GATEWAY_ENTRY" ]]; then
372
+ echo "[gateway] ERROR: Unable to resolve built gateway entry under $FASED_ROOT/dist"
373
+ exit 1
374
+ fi
375
+ if [[ "$VERBOSE_STARTUP" == "1" ]]; then
376
+ FASED_SKIP_BUILD=1 "$NODE_BIN" "$GATEWAY_ENTRY" gateway --allow-unconfigured --force --bind loopback --port "$FASED_GATEWAY_PORT" &
377
+ else
378
+ : > "$GATEWAY_BOOT_LOG"
379
+ FASED_SKIP_BUILD=1 "$NODE_BIN" "$GATEWAY_ENTRY" gateway --allow-unconfigured --force --bind loopback --port "$FASED_GATEWAY_PORT" >>"$GATEWAY_BOOT_LOG" 2>&1 &
380
+ fi
381
+ AGENT_PID=$!
382
+
383
+ if ! wait_for_gateway_listener "$GATEWAY_READY_TIMEOUT"; then
384
+ exit 1
385
+ fi
386
+ }
387
+
388
+ # Ensure Gateway Token exists
389
+ mkdir -p "$FASED_CONFIG_DIR"
390
+ mkdir -p "$LOG_DIR"
391
+ : > "$ZROK_RUNTIME_LOG"
392
+ : > "$WALLET_SETUP_LOG"
393
+ SERVICE_GATEWAY_TOKEN="${FASED_GATEWAY_TOKEN:-}"
394
+ if [ ! -f "$GW_TOKEN_PATH" ]; then
395
+ if [[ -n "$SERVICE_GATEWAY_TOKEN" ]]; then
396
+ printf '%s\n' "$SERVICE_GATEWAY_TOKEN" > "$GW_TOKEN_PATH"
397
+ else
398
+ openssl rand -hex 32 > "$GW_TOKEN_PATH"
399
+ fi
400
+ fi
401
+ export FASED_GATEWAY_TOKEN="$(tr -d '\n' < "$GW_TOKEN_PATH")"
402
+
403
+ # 0. Clean up stale state
404
+ # rm -f "$TOKEN_PATH" # (Disabled: Preserve identity for stable Zrok tunnels)
405
+ # Note: --force on gateway start will handle the port conflict,
406
+ # but we still kill zrok to ensure no tunnel conflicts.
407
+ if [[ "${FASED_MANAGED_INTERNAL:-0}" != "1" ]]; then
408
+ FASED_SKIP_BUILD=1 "$NODE_BIN" "$RUN_NODE_SCRIPT" gateway stop >/dev/null 2>&1 || true
409
+ fi
410
+ force_stop_local_gateway
411
+
412
+ # Start the dashboard backend before slower hosted setup. Signer, wallet,
413
+ # federation, and tunnel work must not prevent the owner from opening the UI.
414
+ start_gateway_if_needed
415
+
416
+ # 0a. Start local key signer daemon (fased-signerd) if available
417
+ SIGNERD_BIN="${FASED_CONFIG_DIR}/bin/fased-signerd"
418
+ SIGNERD_SOCKET="${FASED_CONFIG_DIR}/wallet/local-signer.sock"
419
+ SIGNERD_BACKEND_SOCKET="$SIGNERD_SOCKET"
420
+ SIGNERD_MATERIAL_DIR="${FASED_CONFIG_DIR}/wallet"
421
+ SIGNERD_PASSPHRASE_FILE="$SIGNERD_MATERIAL_DIR/passphrase"
422
+ SIGNERD_EVM_KEYSTORE="$SIGNERD_MATERIAL_DIR/keystore-evm.v1.enc"
423
+ SIGNERD_SOL_KEYSTORE="$SIGNERD_MATERIAL_DIR/keystore-solana.v1.enc"
424
+ SIGNERD_LOG="${LOG_DIR}/fased-signerd.log"
425
+ SIGNERD_BROKER_LOG="${LOG_DIR}/local-signer-broker.log"
426
+ SIGNER_ISOLATION_HELPER="/usr/local/sbin/fased-signer-isolation"
427
+ CONFIG_JSON="${FASED_CONFIG_DIR}/fased.json"
428
+ SIGNERD_ENV_FILE="${FASED_CONFIG_DIR}/wallet/signer.env"
429
+ WALLET_REGISTRY_JSON="${FASED_CONFIG_DIR}/wallet/provider-registry.v1.json"
430
+ SIGNERD_STARTUP_MODE="disabled"
431
+ SIGNERD_ERROR=""
432
+
433
+ mark_signerd_degraded() {
434
+ SIGNERD_STARTUP_MODE="degraded"
435
+ SIGNERD_ERROR="$1"
436
+ echo "[signerd] WARNING: $1"
437
+ echo "[signerd] Dashboard stays online; wallet actions remain degraded until signer is fixed."
438
+ }
439
+
440
+ load_wallet_signer_env_from_config() {
441
+ if [[ ! -f "$CONFIG_JSON" ]]; then
442
+ return 0
443
+ fi
444
+ if ! command -v jq >/dev/null 2>&1; then
445
+ return 0
446
+ fi
447
+ while IFS= read -r line; do
448
+ [[ "$line" == *=* ]] || continue
449
+ local key="${line%%=*}"
450
+ local value="${line#*=}"
451
+ if [[ "$key" =~ ^FASED_WALLET_(EVM|SOLANA)_(RPC_URL|KEYSTORE_PATH)(__[A-Za-z0-9_-]+)?$ ]] \
452
+ || [[ "$key" == "FASED_WALLET_CHAINS" ]] \
453
+ || [[ "$key" == "FASED_WALLET_PASSPHRASE_FILE" ]] \
454
+ || [[ "$key" == "FASED_WALLET_LOCAL_SIGNER_SOCKET" ]] \
455
+ || [[ "$key" == "FASED_WALLET_LOCAL_SIGNER_BACKEND_SOCKET" ]] \
456
+ || [[ "$key" == "FASED_WALLET_SIGNER_STATE_DIR" ]] \
457
+ || [[ "$key" == "FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER" ]] \
458
+ || [[ "$key" == "FASED_WALLET_LOCAL_SIGNER_BIN" ]] \
459
+ || [[ "$key" =~ ^FASED_WALLET_LOCAL_SIGNER_(ROLE|DIRECT_SIGNING|CAPS_ENABLED|SOLANA_MAX_PER_TX|SOLANA_MAX_DAILY|SOLANA_ALLOW_PROGRAMS)(__[A-Za-z0-9_-]+)?$ ]]; then
460
+ export "$key=$value"
461
+ fi
462
+ done < <(
463
+ jq -r '
464
+ .env.vars // {}
465
+ | to_entries[]
466
+ | select(
467
+ .key
468
+ | test("^FASED_WALLET_(EVM|SOLANA)_(RPC_URL|KEYSTORE_PATH)(__[A-Za-z0-9_-]+)?$")
469
+ or . == "FASED_WALLET_CHAINS"
470
+ or . == "FASED_WALLET_PASSPHRASE_FILE"
471
+ or . == "FASED_WALLET_LOCAL_SIGNER_SOCKET"
472
+ or . == "FASED_WALLET_LOCAL_SIGNER_BACKEND_SOCKET"
473
+ or . == "FASED_WALLET_SIGNER_STATE_DIR"
474
+ or . == "FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER"
475
+ or . == "FASED_WALLET_LOCAL_SIGNER_BIN"
476
+ or test("^FASED_WALLET_LOCAL_SIGNER_(ROLE|DIRECT_SIGNING|CAPS_ENABLED|SOLANA_MAX_PER_TX|SOLANA_MAX_DAILY|SOLANA_ALLOW_PROGRAMS)(__[A-Za-z0-9_-]+)?$")
477
+ )
478
+ | "\(.key)=\(.value|tostring)"
479
+ ' "$CONFIG_JSON" 2>/dev/null || true
480
+ )
481
+ }
482
+
483
+ load_wallet_signer_env_file() {
484
+ if [[ ! -f "$SIGNERD_ENV_FILE" ]]; then
485
+ return 0
486
+ fi
487
+
488
+ set -a
489
+ # signer.env is generated by onboarding.wallet.ts; only import export lines, not the launch command.
490
+ source <(grep -E '^export FASED_WALLET_' "$SIGNERD_ENV_FILE" || true)
491
+ set +a
492
+ }
493
+
494
+ has_scoped_wallet_env_value() {
495
+ local prefix="$1"
496
+ local name=""
497
+ while IFS= read -r name; do
498
+ [[ -n "$name" ]] || continue
499
+ if [[ -n "${!name:-}" ]]; then
500
+ return 0
501
+ fi
502
+ done < <(compgen -A variable "${prefix}__" || true)
503
+ return 1
504
+ }
505
+
506
+ normalize_wallet_env_suffix() {
507
+ local raw="${1:-}"
508
+ printf '%s' "$raw" \
509
+ | tr '[:upper:]' '[:lower:]' \
510
+ | sed -E 's/[^a-z0-9]+/_/g; s/^_+//; s/_+$//'
511
+ }
512
+
513
+ normalize_wallet_filename_id() {
514
+ local raw="${1:-}"
515
+ printf '%s' "$raw" \
516
+ | tr '[:upper:]' '[:lower:]' \
517
+ | sed -E 's/[^a-z0-9_-]+/-/g; s/^-+//; s/-+$//'
518
+ }
519
+
520
+ hydrate_scoped_wallet_keystore_env_from_registry() {
521
+ local material_dir="$1"
522
+ if [[ ! -f "$WALLET_REGISTRY_JSON" ]] || ! command -v jq >/dev/null 2>&1; then
523
+ return 0
524
+ fi
525
+
526
+ while IFS=$'\t' read -r wallet_id provider_id; do
527
+ [[ -n "$wallet_id" ]] || continue
528
+ if [[ "$provider_id" != "local-socket-signer" && "$provider_id" != "embedded-keystore" ]]; then
529
+ continue
530
+ fi
531
+ local env_suffix
532
+ local file_suffix
533
+ env_suffix="$(normalize_wallet_env_suffix "$wallet_id")"
534
+ file_suffix="$(normalize_wallet_filename_id "$wallet_id")"
535
+ [[ -n "$env_suffix" && -n "$file_suffix" ]] || continue
536
+
537
+ local sol_key="FASED_WALLET_SOLANA_KEYSTORE_PATH__${env_suffix^^}"
538
+ if [[ -z "${!sol_key:-}" ]]; then
539
+ local sol_candidate="${material_dir}/keystore-solana-${file_suffix}.v1.enc"
540
+ if [[ -f "$sol_candidate" ]]; then
541
+ export "$sol_key=$sol_candidate"
542
+ fi
543
+ fi
544
+
545
+ local evm_key="FASED_WALLET_EVM_KEYSTORE_PATH__${env_suffix^^}"
546
+ if [[ -z "${!evm_key:-}" ]]; then
547
+ local evm_candidate="${material_dir}/keystore-evm-${file_suffix}.v1.enc"
548
+ if [[ -f "$evm_candidate" ]]; then
549
+ export "$evm_key=$evm_candidate"
550
+ fi
551
+ fi
552
+ done < <(
553
+ jq -r '
554
+ (.wallets // [])
555
+ | .[]
556
+ | select(.id != null and .providerId != null)
557
+ | "\(.id|tostring)\t\(.providerId|tostring)"
558
+ ' "$WALLET_REGISTRY_JSON" 2>/dev/null || true
559
+ )
560
+ }
561
+
562
+ resolve_signerd_keystore_export() {
563
+ local explicit_value="$1"
564
+ local scoped_prefix="$2"
565
+ local default_path="$3"
566
+
567
+ if has_scoped_wallet_env_value "$scoped_prefix"; then
568
+ printf '\n'
569
+ return 0
570
+ fi
571
+
572
+ if [[ -n "$explicit_value" && "$explicit_value" != "$default_path" ]]; then
573
+ printf '%s\n' "$explicit_value"
574
+ return 0
575
+ fi
576
+
577
+ if [[ -n "$explicit_value" ]]; then
578
+ printf '%s\n' "$explicit_value"
579
+ return 0
580
+ fi
581
+
582
+ printf '%s\n' "$default_path"
583
+ }
584
+
585
+ resolve_wallet_chains_from_config() {
586
+ if [[ ! -f "$CONFIG_JSON" ]] || ! command -v jq >/dev/null 2>&1; then
587
+ printf '%s\n' "evm,solana"
588
+ return 0
589
+ fi
590
+ jq -r '
591
+ [
592
+ ((.env.vars.FASED_WALLET_CHAINS // "") | split(",")[]?),
593
+ (.wallet.runtime.chains // [] | .[]?)
594
+ ]
595
+ | map((.|tostring|ascii_downcase|gsub("^\\s+|\\s+$"; "")))
596
+ | map(select(. == "evm" or . == "solana"))
597
+ | unique
598
+ | if length > 0 then join(",") else "evm,solana" end
599
+ ' "$CONFIG_JSON" 2>/dev/null || printf '%s\n' "evm,solana"
600
+ }
601
+
602
+ resolve_local_signer_sidecar_path() {
603
+ local socket_path="$1"
604
+ local kind="$2"
605
+ local socket_dir
606
+ local socket_name
607
+ local sidecar_base
608
+ socket_dir="$(dirname "$socket_path")"
609
+ socket_name="$(basename "$socket_path")"
610
+ sidecar_base="${socket_name%.sock}"
611
+ if [[ "$sidecar_base" == "$socket_name" ]]; then
612
+ sidecar_base="$socket_name"
613
+ fi
614
+ if [[ "$kind" == "pid" ]]; then
615
+ printf '%s\n' "${socket_dir}/${sidecar_base}.pid"
616
+ else
617
+ printf '%s\n' "${socket_dir}/${sidecar_base}.audit.jsonl"
618
+ fi
619
+ }
620
+
621
+ registry_has_local_signer_wallet() {
622
+ if [[ ! -f "$WALLET_REGISTRY_JSON" ]] || ! command -v jq >/dev/null 2>&1; then
623
+ return 1
624
+ fi
625
+ jq -e '
626
+ (.wallets // [])
627
+ | any(.providerId == "local-socket-signer")
628
+ ' "$WALLET_REGISTRY_JSON" >/dev/null 2>&1
629
+ }
630
+
631
+ has_local_signer_keystore_material() {
632
+ if [[ -n "${FASED_WALLET_SOLANA_KEYSTORE_PATH:-}" ]] || has_scoped_wallet_env_value "FASED_WALLET_SOLANA_KEYSTORE_PATH"; then
633
+ return 0
634
+ fi
635
+ local candidate
636
+ for candidate in "$SIGNERD_MATERIAL_DIR"/keystore-solana*.v1.enc "$SIGNERD_MATERIAL_DIR"/keystore-evm*.v1.enc; do
637
+ [[ -f "$candidate" ]] && return 0
638
+ done
639
+ return 1
640
+ }
641
+
642
+ should_start_signerd() {
643
+ registry_has_local_signer_wallet || has_local_signer_keystore_material
644
+ }
645
+
646
+ collect_existing_signerd_pids() {
647
+ local pid_file app_pid_file
648
+ pid_file="$(resolve_local_signer_sidecar_path "$SIGNERD_BACKEND_SOCKET" "pid")"
649
+ app_pid_file="$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "pid")"
650
+ {
651
+ if [[ -f "$pid_file" ]]; then
652
+ cat "$pid_file" 2>/dev/null || true
653
+ fi
654
+ if [[ "$app_pid_file" != "$pid_file" && -f "$app_pid_file" ]]; then
655
+ cat "$app_pid_file" 2>/dev/null || true
656
+ fi
657
+ pgrep -f "$SIGNERD_BIN" 2>/dev/null || true
658
+ pgrep -f "wallet signer broker" 2>/dev/null || true
659
+ } | awk '/^[0-9]+$/ { if (!seen[$1]++) print $1 }'
660
+ }
661
+
662
+ count_existing_signerd_pids() {
663
+ local count=0
664
+ while IFS= read -r _pid; do
665
+ count=$((count + 1))
666
+ done < <(collect_existing_signerd_pids)
667
+ printf '%s\n' "$count"
668
+ }
669
+
670
+ dump_existing_signerd_processes() {
671
+ pgrep -af "$SIGNERD_BIN" 2>/dev/null || true
672
+ }
673
+
674
+ stop_existing_signerd() {
675
+ local pid_file app_pid_file
676
+ pid_file="$(resolve_local_signer_sidecar_path "$SIGNERD_BACKEND_SOCKET" "pid")"
677
+ app_pid_file="$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "pid")"
678
+ if signer_isolation_helper_available; then
679
+ run_signer_isolation_helper stop "$SIGNERD_BACKEND_SOCKET" "$pid_file" >/dev/null 2>&1 || true
680
+ if [[ "$SIGNERD_SOCKET" != "$SIGNERD_BACKEND_SOCKET" ]]; then
681
+ run_signer_isolation_helper stop "$SIGNERD_SOCKET" "$app_pid_file" >/dev/null 2>&1 || true
682
+ fi
683
+ return 0
684
+ fi
685
+ local pid=""
686
+ while IFS= read -r pid; do
687
+ [[ "$pid" =~ ^[0-9]+$ ]] || continue
688
+ kill "$pid" >/dev/null 2>&1 || true
689
+ done < <(collect_existing_signerd_pids)
690
+ sleep 0.5
691
+ while IFS= read -r pid; do
692
+ [[ "$pid" =~ ^[0-9]+$ ]] || continue
693
+ kill -9 "$pid" >/dev/null 2>&1 || true
694
+ done < <(collect_existing_signerd_pids)
695
+ rm -f "$SIGNERD_SOCKET" "$SIGNERD_BACKEND_SOCKET" "$pid_file" "$app_pid_file"
696
+ }
697
+
698
+ wait_for_signerd_ready() {
699
+ local retries="$1"
700
+ local count=0
701
+ while true; do
702
+ local active_count
703
+ active_count="$(count_existing_signerd_pids)"
704
+ if [[ "$active_count" == "1" && -S "$SIGNERD_BACKEND_SOCKET" ]]; then
705
+ return 0
706
+ fi
707
+ if [[ "$active_count" -gt 1 ]]; then
708
+ echo "[signerd] ERROR: multiple fased-signerd processes detected; refusing to start gateway."
709
+ dump_existing_signerd_processes
710
+ return 1
711
+ fi
712
+ sleep 1
713
+ count=$((count + 1))
714
+ if [[ $count -ge $retries ]]; then
715
+ echo "[signerd] ERROR: fased-signerd did not become healthy within ${retries}s."
716
+ dump_existing_signerd_processes
717
+ tail -n 40 "$SIGNERD_LOG" 2>/dev/null || true
718
+ return 1
719
+ fi
720
+ done
721
+ }
722
+
723
+ wait_for_signer_broker_ready() {
724
+ local retries="$1"
725
+ local count=0
726
+ while true; do
727
+ if [[ -S "$SIGNERD_SOCKET" ]]; then
728
+ return 0
729
+ fi
730
+ sleep 1
731
+ count=$((count + 1))
732
+ if [[ $count -ge $retries ]]; then
733
+ echo "[signerd] ERROR: signer broker did not create app socket within ${retries}s."
734
+ tail -n 40 "$SIGNERD_BROKER_LOG" 2>/dev/null || true
735
+ return 1
736
+ fi
737
+ done
738
+ }
739
+
740
+ collect_wallet_env_args() {
741
+ local key value
742
+ while IFS='=' read -r key value; do
743
+ [[ "$key" == FASED_WALLET_* ]] || continue
744
+ printf '%s=%s\0' "$key" "$value"
745
+ done < <(env)
746
+ }
747
+
748
+ current_app_user() {
749
+ id -un 2>/dev/null || printf '%s\n' "${USER:-app}"
750
+ }
751
+
752
+ signer_isolation_helper_available() {
753
+ [[ -n "${FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER:-}" ]] || return 1
754
+ [[ -x "$SIGNER_ISOLATION_HELPER" ]] || return 1
755
+ command -v sudo >/dev/null 2>&1 || return 1
756
+ }
757
+
758
+ run_signer_isolation_helper() {
759
+ local app_user
760
+ app_user="$(current_app_user)"
761
+ sudo -n -E "$SIGNER_ISOLATION_HELPER" "$app_user" "$FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER" "$@"
762
+ }
763
+
764
+ start_signerd_process() {
765
+ local pid_file audit_log
766
+ pid_file="$(resolve_local_signer_sidecar_path "$SIGNERD_BACKEND_SOCKET" "pid")"
767
+ audit_log="$(resolve_local_signer_sidecar_path "$SIGNERD_BACKEND_SOCKET" "audit")"
768
+ if [[ -n "${FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER:-}" ]]; then
769
+ if signer_isolation_helper_available; then
770
+ run_signer_isolation_helper start-signerd \
771
+ "$SIGNERD_BIN" \
772
+ "$SIGNERD_BACKEND_SOCKET" \
773
+ "$pid_file" \
774
+ "$audit_log" \
775
+ >>"$SIGNERD_LOG" 2>&1 &
776
+ return 0
777
+ fi
778
+ if ! command -v sudo >/dev/null 2>&1; then
779
+ return 1
780
+ fi
781
+ local env_args=()
782
+ while IFS= read -r -d '' env_arg; do
783
+ env_args+=("$env_arg")
784
+ done < <(collect_wallet_env_args)
785
+ sudo -n -u "$FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER" -H env "${env_args[@]}" \
786
+ "$SIGNERD_BIN" \
787
+ -socket "$SIGNERD_BACKEND_SOCKET" \
788
+ -pid-file "$pid_file" \
789
+ -audit-log "$audit_log" \
790
+ >>"$SIGNERD_LOG" 2>&1 &
791
+ return 0
792
+ fi
793
+ "$SIGNERD_BIN" \
794
+ -socket "$SIGNERD_BACKEND_SOCKET" \
795
+ -pid-file "$pid_file" \
796
+ -audit-log "$audit_log" \
797
+ >>"$SIGNERD_LOG" 2>&1 &
798
+ }
799
+
800
+ start_signer_broker_process() {
801
+ if [[ "$SIGNERD_SOCKET" == "$SIGNERD_BACKEND_SOCKET" ]]; then
802
+ return 0
803
+ fi
804
+ local gateway_entry
805
+ gateway_entry="$(resolve_gateway_cli_entry || true)"
806
+ if [[ -z "$gateway_entry" ]]; then
807
+ echo "[signerd] ERROR: signer broker CLI unavailable under $FASED_ROOT/dist"
808
+ return 1
809
+ fi
810
+ if [[ -n "${FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER:-}" ]]; then
811
+ if signer_isolation_helper_available; then
812
+ run_signer_isolation_helper start-broker \
813
+ "$NODE_BIN" \
814
+ "$gateway_entry" \
815
+ "$SIGNERD_SOCKET" \
816
+ "$SIGNERD_BACKEND_SOCKET" \
817
+ "$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "pid")" \
818
+ "$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "audit")" \
819
+ >>"$SIGNERD_BROKER_LOG" 2>&1 &
820
+ return 0
821
+ fi
822
+ if ! command -v sudo >/dev/null 2>&1; then
823
+ return 1
824
+ fi
825
+ local env_args=()
826
+ while IFS= read -r -d '' env_arg; do
827
+ env_args+=("$env_arg")
828
+ done < <(collect_wallet_env_args)
829
+ sudo -n -u "$FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER" -H env "${env_args[@]}" \
830
+ "$NODE_BIN" "$gateway_entry" wallet signer broker \
831
+ --socket "$SIGNERD_SOCKET" \
832
+ --backend-socket "$SIGNERD_BACKEND_SOCKET" \
833
+ --pid-file "$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "pid")" \
834
+ --audit-log "$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "audit")" \
835
+ >>"$SIGNERD_BROKER_LOG" 2>&1 &
836
+ return 0
837
+ fi
838
+ "$NODE_BIN" "$gateway_entry" wallet signer broker \
839
+ --socket "$SIGNERD_SOCKET" \
840
+ --backend-socket "$SIGNERD_BACKEND_SOCKET" \
841
+ --pid-file "$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "pid")" \
842
+ --audit-log "$(resolve_local_signer_sidecar_path "$SIGNERD_SOCKET" "audit")" \
843
+ >>"$SIGNERD_BROKER_LOG" 2>&1 &
844
+ }
845
+
846
+ load_wallet_signer_env_from_config
847
+ load_wallet_signer_env_file
848
+ SIGNERD_BIN="${FASED_WALLET_LOCAL_SIGNER_BIN:-$SIGNERD_BIN}"
849
+ SIGNERD_MATERIAL_DIR="${FASED_WALLET_SIGNER_STATE_DIR:-$SIGNERD_MATERIAL_DIR}"
850
+ if [[ -n "${FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER:-}" ]]; then
851
+ SIGNERD_BROKER_LOG="$(dirname "${FASED_WALLET_LOCAL_SIGNER_SOCKET:-$SIGNERD_SOCKET}")/local-signer-broker.log"
852
+ fi
853
+ hydrate_scoped_wallet_keystore_env_from_registry "$SIGNERD_MATERIAL_DIR"
854
+ if should_start_signerd; then
855
+ SIGNERD_STARTUP_MODE="healthy"
856
+ SIGNERD_SOCKET="${FASED_WALLET_LOCAL_SIGNER_SOCKET:-$SIGNERD_SOCKET}"
857
+ SIGNERD_BACKEND_SOCKET="${FASED_WALLET_LOCAL_SIGNER_BACKEND_SOCKET:-$SIGNERD_SOCKET}"
858
+ SIGNERD_EVM_KEYSTORE="$(resolve_signerd_keystore_export "${FASED_WALLET_EVM_KEYSTORE_PATH:-}" "FASED_WALLET_EVM_KEYSTORE_PATH" "$SIGNERD_MATERIAL_DIR/keystore-evm.v1.enc")"
859
+ SIGNERD_SOL_KEYSTORE="$(resolve_signerd_keystore_export "${FASED_WALLET_SOLANA_KEYSTORE_PATH:-}" "FASED_WALLET_SOLANA_KEYSTORE_PATH" "$SIGNERD_MATERIAL_DIR/keystore-solana.v1.enc")"
860
+ SIGNERD_PASSPHRASE="${FASED_WALLET_PASSPHRASE:-}"
861
+ SIGNERD_PASSPHRASE_FILE="${FASED_WALLET_PASSPHRASE_FILE:-}"
862
+ SIGNERD_CHAINS="${FASED_WALLET_CHAINS:-$(resolve_wallet_chains_from_config)}"
863
+ export FASED_WALLET_LOCAL_SIGNER_SOCKET="$SIGNERD_SOCKET"
864
+ export FASED_WALLET_LOCAL_SIGNER_BACKEND_SOCKET="$SIGNERD_BACKEND_SOCKET"
865
+ export FASED_WALLET_SIGNER_STATE_DIR="$SIGNERD_MATERIAL_DIR"
866
+ export FASED_WALLET_CHAINS="${SIGNERD_CHAINS:-evm,solana}"
867
+ if [[ -n "$SIGNERD_EVM_KEYSTORE" ]]; then
868
+ export FASED_WALLET_EVM_KEYSTORE_PATH="$SIGNERD_EVM_KEYSTORE"
869
+ else
870
+ unset FASED_WALLET_EVM_KEYSTORE_PATH
871
+ fi
872
+ if [[ -n "$SIGNERD_SOL_KEYSTORE" ]]; then
873
+ export FASED_WALLET_SOLANA_KEYSTORE_PATH="$SIGNERD_SOL_KEYSTORE"
874
+ else
875
+ unset FASED_WALLET_SOLANA_KEYSTORE_PATH
876
+ fi
877
+ if [[ -n "$SIGNERD_PASSPHRASE" ]]; then
878
+ export FASED_WALLET_PASSPHRASE="$SIGNERD_PASSPHRASE"
879
+ unset FASED_WALLET_PASSPHRASE_FILE
880
+ else
881
+ SIGNERD_PASSPHRASE_FILE="${SIGNERD_PASSPHRASE_FILE:-$SIGNERD_MATERIAL_DIR/passphrase}"
882
+ export FASED_WALLET_PASSPHRASE_FILE="$SIGNERD_PASSPHRASE_FILE"
883
+ unset FASED_WALLET_PASSPHRASE
884
+ fi
885
+ if [[ -f "$SIGNERD_BIN" ]]; then
886
+ if [[ -S "$SIGNERD_SOCKET" ]] || [[ -S "$SIGNERD_BACKEND_SOCKET" ]] || [[ -f "$(resolve_local_signer_sidecar_path "$SIGNERD_BACKEND_SOCKET" "pid")" ]] || [[ "$(count_existing_signerd_pids)" -gt 0 ]]; then
887
+ echo "==> Restarting fased-signerd to apply current wallet chain/runtime config..."
888
+ stop_existing_signerd
889
+ fi
890
+ echo "==> Starting fased-signerd (Go key signer)..."
891
+ mkdir -p "$LOG_DIR"
892
+ mkdir -p "$(dirname "$SIGNERD_LOG")" "$(dirname "$SIGNERD_BROKER_LOG")" 2>/dev/null || true
893
+ if start_signerd_process; then
894
+ SIGNERD_PID=$!
895
+ else
896
+ mark_signerd_degraded "failed to start isolated fased-signerd as ${FASED_WALLET_LOCAL_SIGNER_RUN_AS_USER:-current user}. Check sudoers and $SIGNERD_LOG"
897
+ SIGNERD_PID=""
898
+ fi
899
+ if [[ -n "$SIGNERD_PID" ]] && wait_for_signerd_ready "$SIGNERD_READY_TIMEOUT_SECONDS"; then
900
+ if ! start_signer_broker_process; then
901
+ mark_signerd_degraded "fased-signerd started but signer broker failed. Check $SIGNERD_BROKER_LOG"
902
+ elif wait_for_signer_broker_ready "$SIGNERD_READY_TIMEOUT_SECONDS"; then
903
+ echo "==> fased-signerd started (PID=$SIGNERD_PID, socket: $SIGNERD_BACKEND_SOCKET)"
904
+ else
905
+ mark_signerd_degraded "fased-signerd broker did not become ready. Check $SIGNERD_BROKER_LOG"
906
+ fi
907
+ else
908
+ mark_signerd_degraded "fased-signerd did not create socket. Check $SIGNERD_LOG"
909
+ fi
910
+ else
911
+ mark_signerd_degraded "fased-signerd is not installed; dashboard stays online. Configure wallet signer from the dashboard or run fased wallet signer setup."
912
+ fi
913
+ else
914
+ unset FASED_WALLET_LOCAL_SIGNER_SOCKET
915
+ unset FASED_WALLET_LOCAL_SIGNER_BACKEND_SOCKET
916
+ unset FASED_WALLET_SIGNER_STATE_DIR
917
+ unset FASED_WALLET_CHAINS
918
+ unset FASED_WALLET_SOLANA_KEYSTORE_PATH
919
+ unset FASED_WALLET_EVM_KEYSTORE_PATH
920
+ unset FASED_WALLET_PASSPHRASE
921
+ unset FASED_WALLET_PASSPHRASE_FILE
922
+ echo "==> Wallet signer not configured; skipping fased-signerd startup."
923
+ fi
924
+ if [[ -f "$ZROK_MONITOR_PID_FILE" ]]; then
925
+ OLD_MONITOR_PID=$(cat "$ZROK_MONITOR_PID_FILE" 2>/dev/null || true)
926
+ if [[ -n "$OLD_MONITOR_PID" ]] && kill -0 "$OLD_MONITOR_PID" 2>/dev/null; then
927
+ kill "$OLD_MONITOR_PID" 2>/dev/null || true
928
+ fi
929
+ rm -f "$ZROK_MONITOR_PID_FILE" || true
930
+ fi
931
+ pkill -f "zrok share" || true
932
+ sleep 1
933
+
934
+ # === Tunnel Helper Functions ===
935
+
936
+ start_health_monitor() {
937
+ local SLUG="$1"
938
+ local RES_TOKEN="$2"
939
+ local retry_delay=30
940
+ local max_retry_delay=300
941
+ while true; do
942
+ sleep "$retry_delay"
943
+ local pid=""
944
+ if [[ -f "$FASED_CONFIG_DIR/.zrok-pid" ]]; then
945
+ pid="$(cat "$FASED_CONFIG_DIR/.zrok-pid" 2>/dev/null || true)"
946
+ fi
947
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
948
+ retry_delay=30
949
+ continue
950
+ fi
951
+
952
+ echo "[tunnel] zrok share is not active, attempting restart..."
953
+ if zrok_log_indicates_clock_skew; then
954
+ echo "[tunnel] Detected zrok auth failure caused by host clock skew."
955
+ ensure_managed_clock_sync || true
956
+ fi
957
+ if [[ -n "$RES_TOKEN" ]]; then
958
+ "$ZROK_BIN" share reserved "$RES_TOKEN" --headless >>"$ZROK_RUNTIME_LOG" 2>&1 &
959
+ else
960
+ "$ZROK_BIN" share public "http://127.0.0.1:${FASED_GATEWAY_PORT}" --unique-name "$SLUG" >>"$ZROK_RUNTIME_LOG" 2>&1 &
961
+ fi
962
+ pid=$!
963
+ echo "$pid" > "$FASED_CONFIG_DIR/.zrok-pid"
964
+ sleep 5
965
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
966
+ echo "[tunnel] ✓ Tunnel active."
967
+ retry_delay=30
968
+ continue
969
+ fi
970
+
971
+ echo "[tunnel] WARNING: zrok share restart failed."
972
+ if zrok_log_indicates_clock_skew; then
973
+ echo "[tunnel] Detected zrok auth failure caused by host clock skew."
974
+ ensure_managed_clock_sync || true
975
+ fi
976
+ if (( retry_delay < max_retry_delay )); then
977
+ retry_delay=$((retry_delay * 2))
978
+ if (( retry_delay > max_retry_delay )); then
979
+ retry_delay=$max_retry_delay
980
+ fi
981
+ fi
982
+ done
983
+ }
984
+
985
+ # === Execution Flow ===
986
+
987
+ MANAGED_TUNNEL_DISABLED=0
988
+ TUNNEL_ERROR=""
989
+ disable_managed_tunnel() {
990
+ MANAGED_TUNNEL_DISABLED=1
991
+ TUNNEL_STARTED=0
992
+ FINAL_URL="N/A"
993
+ TUNNEL_ERROR="$1"
994
+ echo "[tunnel] WARNING: $1"
995
+ echo "[tunnel] Dashboard gateway stays online; Fased Network tunnel remains degraded until repaired."
996
+ }
997
+
998
+ # 1. Wait for the agent to enroll/refresh access token.
999
+ # We do not treat a stale pre-existing token as ready if it has no zrok metadata.
1000
+ echo "==> Waiting for agent enrollment/token refresh (max 60s)..."
1001
+ MAX_RETRIES=60
1002
+ COUNT=0
1003
+ while true; do
1004
+ if [[ -f "$TOKEN_PATH" ]]; then
1005
+ CURRENT_ZROK=$(jq -r '.zrokToken // empty' "$TOKEN_PATH" 2>/dev/null || true)
1006
+ if [[ -n "$CURRENT_ZROK" ]]; then
1007
+ break
1008
+ fi
1009
+
1010
+ CURRENT_SIG=$(sha256sum "$TOKEN_PATH" | awk '{print $1}')
1011
+ # If token changed after startup, proceed even if zrok is still missing.
1012
+ # Recovery logic below will decide strict-mode outcome.
1013
+ if [[ -n "$INITIAL_TOKEN_SIG" && "$CURRENT_SIG" != "$INITIAL_TOKEN_SIG" ]]; then
1014
+ break
1015
+ fi
1016
+
1017
+ # No initial token: once we have a token file, give it a short chance to gain zrok fields.
1018
+ if [[ -z "$INITIAL_TOKEN_SIG" && $COUNT -ge 3 ]]; then
1019
+ break
1020
+ fi
1021
+ fi
1022
+ sleep 1
1023
+ COUNT=$((COUNT + 1))
1024
+ if [[ $COUNT -ge $MAX_RETRIES ]]; then
1025
+ if [[ -f "$TOKEN_PATH" ]]; then
1026
+ echo "[tunnel] WARNING: Token refresh did not complete in time; continuing with current token."
1027
+ break
1028
+ fi
1029
+ disable_managed_tunnel "Agent enrollment/token refresh did not complete in time and no token is available."
1030
+ if [[ "$VERBOSE_STARTUP" != "1" ]]; then
1031
+ echo "[debug] Last gateway startup logs:"
1032
+ tail -n 40 "$GATEWAY_BOOT_LOG" || true
1033
+ fi
1034
+ break
1035
+ fi
1036
+ done
1037
+
1038
+ # === Tunneling (zrok) ===
1039
+
1040
+ FINAL_URL="N/A"
1041
+ TUNNEL_STARTED=0
1042
+ SLUG="N/A"
1043
+ RES_TOKEN=""
1044
+
1045
+ ZROK_VDIR="$HOME/.zrok"
1046
+ ZROK_BIN="$ZROK_VDIR/bin/zrok"
1047
+
1048
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" ]]; then
1049
+
1050
+ # Check if zrok is installed; if not, install it
1051
+ if [[ ! -f "$ZROK_BIN" ]]; then
1052
+ echo "[tunnel] Installing zrok CLI..."
1053
+ echo "[tunnel] Destination: $ZROK_VDIR/bin"
1054
+ mkdir -p "$ZROK_VDIR/bin"
1055
+
1056
+ # Temporarily disable pipefail to capture install errors
1057
+ set +e
1058
+ curl -sLf https://get.openziti.io/zrok/install/get.bash | bash -s -- --install-dir "$ZROK_VDIR/bin" > /tmp/zrok-install.log 2>&1
1059
+ INSTALL_EXIT=$?
1060
+ set -e
1061
+
1062
+ if [[ $INSTALL_EXIT -ne 0 ]]; then
1063
+ echo "[tunnel] ERROR: zrok installation failed (Exit: $INSTALL_EXIT). Log:"
1064
+ cat /tmp/zrok-install.log
1065
+ echo "[tunnel] Attempting manual download as fallback (v1.1.11)..."
1066
+
1067
+ TMP_ZROK_DIR=$(mktemp -d)
1068
+ ZROK_URL="https://github.com/openziti/zrok/releases/download/v1.1.11/zrok_1.1.11_linux_amd64.tar.gz"
1069
+
1070
+ if curl -sLf "$ZROK_URL" -o "$TMP_ZROK_DIR/zrok.tar.gz"; then
1071
+ tar -xzf "$TMP_ZROK_DIR/zrok.tar.gz" -C "$TMP_ZROK_DIR"
1072
+
1073
+ # Find zrok binary safely within the temp dir
1074
+ ZROK_FOUND=$(find "$TMP_ZROK_DIR" -name zrok -type f | head -n 1)
1075
+
1076
+ if [[ -n "$ZROK_FOUND" ]]; then
1077
+ mv "$ZROK_FOUND" "$ZROK_VDIR/bin/"
1078
+ chmod +x "$ZROK_VDIR/bin/zrok"
1079
+ echo "[tunnel] Manual download success."
1080
+ else
1081
+ echo "[tunnel] Extracted but 'zrok' binary not found."
1082
+ ls -R "$TMP_ZROK_DIR"
1083
+ rm -rf "$TMP_ZROK_DIR"
1084
+ disable_managed_tunnel "zrok manual download extracted without a zrok binary."
1085
+ fi
1086
+ else
1087
+ echo "[tunnel] Download failed (curl)."
1088
+ rm -rf "$TMP_ZROK_DIR"
1089
+ disable_managed_tunnel "zrok manual download failed."
1090
+ fi
1091
+ rm -rf "$TMP_ZROK_DIR" /tmp/zrok-install.log
1092
+ fi
1093
+ fi
1094
+
1095
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" && ! -f "$ZROK_BIN" ]]; then
1096
+ disable_managed_tunnel "zrok binary not found at $ZROK_BIN after install."
1097
+ fi
1098
+
1099
+ fi
1100
+
1101
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" ]]; then
1102
+
1103
+ chmod +x "$ZROK_BIN"
1104
+ echo "[tunnel] zrok version: $($ZROK_BIN version)"
1105
+
1106
+ echo "==> Consuming server-issued zrok credentials..."
1107
+
1108
+ # Read zrok token from the enrolled agent's access token
1109
+ if [[ ! -f "$TOKEN_PATH" ]]; then
1110
+ disable_managed_tunnel "Agent enrollment token is unavailable. Tunnel cannot start yet."
1111
+ fi
1112
+
1113
+ fi
1114
+
1115
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" ]]; then
1116
+
1117
+ ZROK_TOKEN=$(jq -r .zrokToken "$TOKEN_PATH")
1118
+ SERVER_SLUG=$(jq -r '.agentSlug // empty' "$TOKEN_PATH")
1119
+ AGENT_HANDLE=$(jq -r '.handle // empty' "$TOKEN_PATH")
1120
+
1121
+ if [[ -n "$SERVER_SLUG" ]]; then
1122
+ SLUG="$SERVER_SLUG"
1123
+ elif [[ -n "$AGENT_HANDLE" ]]; then
1124
+ SLUG=$(echo "$AGENT_HANDLE" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/^-//' | sed 's/-$//' | tr '[:upper:]' '[:lower:]')
1125
+ else
1126
+ SLUG=""
1127
+ fi
1128
+
1129
+ if [[ -z "$SLUG" ]]; then
1130
+ disable_managed_tunnel "Unable to resolve tunnel slug from enrollment token."
1131
+ fi
1132
+
1133
+ fi
1134
+
1135
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" ]]; then
1136
+
1137
+ echo "[tunnel] Target Slug: $SLUG"
1138
+
1139
+ # Reservation persistence
1140
+ RES_FILE="$FASED_CONFIG_DIR/${SLUG}.zrok-reservation"
1141
+ RES_TOKEN=""
1142
+ if [[ -f "$RES_FILE" ]]; then
1143
+ RES_TOKEN=$(cat "$RES_FILE")
1144
+ echo "[tunnel] Found cached reservation: $RES_TOKEN"
1145
+ fi
1146
+
1147
+ # Recovery when exact slug file is missing: use the only reservation token in config dir.
1148
+ if [[ -z "$RES_TOKEN" ]]; then
1149
+ mapfile -t RES_FILES < <(ls "$FASED_CONFIG_DIR"/*.zrok-reservation 2>/dev/null || true)
1150
+ if [[ ${#RES_FILES[@]} -eq 1 ]]; then
1151
+ RES_FILE="${RES_FILES[0]}"
1152
+ RES_TOKEN=$(cat "$RES_FILE")
1153
+ RECOVERED_BASENAME=$(basename "$RES_FILE")
1154
+ RECOVERED_SLUG="${RECOVERED_BASENAME%.zrok-reservation}"
1155
+ if [[ -n "$RECOVERED_SLUG" ]]; then
1156
+ SLUG="$RECOVERED_SLUG"
1157
+ echo "[tunnel] Recovered slug from cached reservation filename: $SLUG"
1158
+ fi
1159
+ echo "[tunnel] Recovered cached reservation: $RES_TOKEN"
1160
+ fi
1161
+ fi
1162
+
1163
+ TUNNEL_STARTED=0
1164
+
1165
+ if [[ "$ZROK_TOKEN" == "null" || -z "$ZROK_TOKEN" ]]; then
1166
+ echo "[tunnel] WARNING: No zrok credentials found in enrollment result."
1167
+ echo "[tunnel] Attempting strict managed recovery using cached reservation + existing zrok identity..."
1168
+ export ZROK_API_ENDPOINT="${ZROK_API_ENDPOINT:-https://zrok.fased.app}"
1169
+ "$ZROK_BIN" config set apiEndpoint "$ZROK_API_ENDPOINT" 2>/dev/null || true
1170
+
1171
+ HAS_ZROK_IDENTITY=0
1172
+ if "$ZROK_BIN" overview --json >/dev/null 2>&1; then
1173
+ HAS_ZROK_IDENTITY=1
1174
+ fi
1175
+
1176
+ if [[ -n "$RES_TOKEN" && "$HAS_ZROK_IDENTITY" -eq 1 ]]; then
1177
+ echo "[tunnel] Recovery prerequisites satisfied (cached reservation found)."
1178
+ FINAL_URL="https://${SLUG}.agents.fased.app"
1179
+ TUNNEL_STARTED=1
1180
+ else
1181
+ echo "[tunnel] ERROR: Managed mode is strict and cannot continue without tunnel credentials."
1182
+ echo "[tunnel] Required for recovery: existing zrok identity + cached *.zrok-reservation token."
1183
+ echo "[tunnel] Remediation:"
1184
+ echo "[tunnel] 1) Check server logs: journalctl -u fased -n 100 --no-pager | grep 'zrok:'"
1185
+ echo "[tunnel] 2) Ensure enroll returns zrokToken/agentSlug"
1186
+ echo "[tunnel] 3) Re-enroll only after server provisioning is healthy"
1187
+ disable_managed_tunnel "No zrok credentials or cached reservation are available yet."
1188
+ fi
1189
+ fi
1190
+
1191
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" && "$ZROK_TOKEN" != "null" && -n "$ZROK_TOKEN" ]]; then
1192
+ echo "[tunnel] Enabling zrok environment..."
1193
+ export ZROK_API_ENDPOINT="${ZROK_API_ENDPOINT:-https://zrok.fased.app}"
1194
+ ensure_managed_clock_sync || true
1195
+ "$ZROK_BIN" config set apiEndpoint "$ZROK_API_ENDPOINT" 2>/dev/null
1196
+
1197
+ # zrok enable (idempotent-ish, or just try)
1198
+ # Remove --force as it's not supported in v1.1+
1199
+ # We ignore error if already enabled, but capture output to be safe
1200
+ "$ZROK_BIN" enable "$ZROK_TOKEN" --description "fased-agent" 2>/dev/null || true
1201
+
1202
+ # If no token, check if we already reserved it (recovery from lost file)
1203
+ if [[ -z "$RES_TOKEN" ]]; then
1204
+ echo "[tunnel] Checking for existing reservation..."
1205
+ OVERVIEW_JSON=$("$ZROK_BIN" overview --json 2>/dev/null || echo "{}")
1206
+ EXISTING_TOKEN=$(echo "$OVERVIEW_JSON" | jq -r --arg SLUG "$SLUG" '(.shares // [])[] | select(.frontendEndpoints[] | contains($SLUG)) | .token' 2>/dev/null | head -n 1 || true)
1207
+
1208
+ if [[ -n "$EXISTING_TOKEN" ]]; then
1209
+ echo "[tunnel] Recovered existing token: $EXISTING_TOKEN"
1210
+ RES_TOKEN="$EXISTING_TOKEN"
1211
+ echo "$RES_TOKEN" > "$RES_FILE"
1212
+ fi
1213
+ fi
1214
+
1215
+ # If still no token, try to reserve
1216
+ if [[ -z "$RES_TOKEN" ]]; then
1217
+ echo "[tunnel] Reserving public share..."
1218
+ # Use --json-output to parse output reliably; separate stderr
1219
+ PARAMS="--unique-name $SLUG --backend-mode proxy --json-output"
1220
+
1221
+ # Capture logic: stdout to variable, stderr to temp file
1222
+ ZROK_LOG="/tmp/zrok-reserve.log"
1223
+ rm -f "$ZROK_LOG"
1224
+
1225
+ if OUT=$("$ZROK_BIN" reserve public "http://127.0.0.1:${FASED_GATEWAY_PORT}" $PARAMS 2> "$ZROK_LOG"); then
1226
+ RES_TOKEN=$(echo "$OUT" | jq -r '.token // empty')
1227
+ else
1228
+ echo "[tunnel] Reserve failed. Log:"
1229
+ cat "$ZROK_LOG"
1230
+ fi
1231
+
1232
+ if [[ -z "$RES_TOKEN" ]]; then
1233
+ echo "[tunnel] Reservation failed or already reserved. Output: $OUT"
1234
+ disable_managed_tunnel "Could not establish reserved tunnel for '$SLUG'."
1235
+ else
1236
+ echo "$RES_TOKEN" > "$RES_FILE"
1237
+ echo "[tunnel] Reserved: $RES_TOKEN"
1238
+ fi
1239
+ fi
1240
+
1241
+ fi
1242
+
1243
+ if [[ "$MANAGED_TUNNEL_DISABLED" != "1" ]]; then
1244
+ FINAL_URL="https://${SLUG}.agents.fased.app"
1245
+ TUNNEL_STARTED=1
1246
+ fi
1247
+
1248
+ if [[ "$TUNNEL_STARTED" -eq 1 ]]; then
1249
+ if ! start_initial_zrok_share "$SLUG"; then
1250
+ echo "[tunnel] WARNING: Continuing in degraded mode without a public tunnel."
1251
+ echo "[tunnel] WARNING: The local gateway remains up and background tunnel retries will continue."
1252
+ rm -f "$FASED_CONFIG_DIR/.zrok-pid" 2>/dev/null || true
1253
+ fi
1254
+ if [[ -f "$ZROK_MONITOR_PID_FILE" ]]; then
1255
+ OLD_MONITOR_PID=$(cat "$ZROK_MONITOR_PID_FILE" 2>/dev/null || true)
1256
+ if [[ -n "$OLD_MONITOR_PID" ]] && kill -0 "$OLD_MONITOR_PID" 2>/dev/null; then
1257
+ kill "$OLD_MONITOR_PID" 2>/dev/null || true
1258
+ fi
1259
+ fi
1260
+ start_health_monitor "$SLUG" "$RES_TOKEN" &
1261
+ ZROK_MONITOR_PID=$!
1262
+ echo "$ZROK_MONITOR_PID" > "$ZROK_MONITOR_PID_FILE"
1263
+ fi
1264
+
1265
+ fi
1266
+
1267
+ WALLET_JSON=""
1268
+ WALLET_HEALTHY="false"
1269
+ WALLET_PID="N/A"
1270
+ WALLET_MODE="n/a"
1271
+ WALLET_SERVICE="n/a"
1272
+ WALLET_CHAINS="n/a"
1273
+ WALLET_DIRECT_SIGNING="n/a"
1274
+ WALLET_TOOL_SCOPE="n/a"
1275
+ WALLET_KEYS_PATH="n/a"
1276
+ WALLET_STARTUP_MODE="healthy"
1277
+ WALLET_AUTH_STATE="unknown"
1278
+ WALLET_AUTH_MODE="unknown"
1279
+ WALLET_AUTH_SOURCE="unknown"
1280
+ WALLET_ERROR=""
1281
+
1282
+ echo "==> Enforcing wallet baseline (wallet service)..."
1283
+ if [[ "${WALLET_BASELINE_MODE,,}" == "skip" ]]; then
1284
+ WALLET_STARTUP_MODE="degraded"
1285
+ WALLET_ERROR="Wallet baseline skipped (FASED_WALLET_BASELINE_MODE=skip)."
1286
+ echo "[wallet] WARNING: Wallet baseline skipped; continuing startup immediately."
1287
+ else
1288
+ # Keep stderr visible to avoid hidden sudo/docker prompts while also logging.
1289
+ WALLET_SETUP_CMD=(
1290
+ env
1291
+ FASED_SKIP_BUILD=1
1292
+ FASED_WALLET_SETUP_ALLOW_DEGRADED=1
1293
+ "$NODE_BIN"
1294
+ "$RUN_NODE_SCRIPT"
1295
+ wallet
1296
+ setup
1297
+ --json
1298
+ )
1299
+ USE_TIMEOUT=0
1300
+ if command -v timeout >/dev/null 2>&1; then
1301
+ if [[ "$WALLET_BASELINE_TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] && [[ "$WALLET_BASELINE_TIMEOUT_SECONDS" -gt 0 ]]; then
1302
+ USE_TIMEOUT=1
1303
+ fi
1304
+ fi
1305
+ if [[ "$USE_TIMEOUT" == "1" ]]; then
1306
+ echo "[wallet] Baseline timeout: ${WALLET_BASELINE_TIMEOUT_SECONDS}s (override: FASED_WALLET_BASELINE_TIMEOUT_SECONDS, disable timeout with 0)."
1307
+ if WALLET_OUTPUT=$(timeout --signal=TERM "${WALLET_BASELINE_TIMEOUT_SECONDS}" "${WALLET_SETUP_CMD[@]}" 2> >(tee "$WALLET_SETUP_LOG" >&2)); then
1308
+ WALLET_SETUP_RC=0
1309
+ else
1310
+ WALLET_SETUP_RC=$?
1311
+ fi
1312
+ else
1313
+ if WALLET_OUTPUT=$("${WALLET_SETUP_CMD[@]}" 2> >(tee "$WALLET_SETUP_LOG" >&2)); then
1314
+ WALLET_SETUP_RC=0
1315
+ else
1316
+ WALLET_SETUP_RC=$?
1317
+ fi
1318
+ fi
1319
+
1320
+ if [[ "${WALLET_SETUP_RC:-0}" == "0" ]]; then
1321
+ WALLET_JSON=$(printf '%s\n' "$WALLET_OUTPUT" | sed -n '/^{/,$p')
1322
+ if [[ -z "$WALLET_JSON" ]]; then
1323
+ WALLET_STARTUP_MODE="degraded"
1324
+ WALLET_ERROR="Wallet setup returned no JSON payload."
1325
+ else
1326
+ WALLET_HEALTHY=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.service.healthy // false' 2>/dev/null || echo "false")
1327
+ WALLET_PID=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.service.pid // "N/A"' 2>/dev/null || echo "N/A")
1328
+ WALLET_MODE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.mode // "n/a"' 2>/dev/null || echo "n/a")
1329
+ WALLET_SERVICE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.service.host + ":" + ((.status.service.port // 0)|tostring)' 2>/dev/null || echo "n/a")
1330
+ WALLET_CHAINS=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.chains // [] | join(",")' 2>/dev/null || echo "n/a")
1331
+ WALLET_DIRECT_SIGNING=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.policy.directSigning // "n/a"' 2>/dev/null || echo "n/a")
1332
+ WALLET_TOOL_SCOPE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.policy.toolAccessMode // "n/a"' 2>/dev/null || echo "n/a")
1333
+ WALLET_KEYS_PATH=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.paths.keysPath // "n/a"' 2>/dev/null || echo "n/a")
1334
+ WALLET_STARTUP_MODE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.startupState // "healthy"' 2>/dev/null || echo "healthy")
1335
+ WALLET_AUTH_STATE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.authState // "unknown"' 2>/dev/null || echo "unknown")
1336
+ WALLET_AUTH_MODE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.authMode // "unknown"' 2>/dev/null || echo "unknown")
1337
+ WALLET_AUTH_SOURCE=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.authSource // "unknown"' 2>/dev/null || echo "unknown")
1338
+ WALLET_ERROR=$(printf '%s\n' "$WALLET_JSON" | jq -r '.status.error // empty' 2>/dev/null || true)
1339
+ if [[ "$WALLET_HEALTHY" != "true" && "$WALLET_STARTUP_MODE" == "healthy" ]]; then
1340
+ WALLET_STARTUP_MODE="degraded"
1341
+ fi
1342
+ fi
1343
+ else
1344
+ WALLET_STARTUP_MODE="degraded"
1345
+ if [[ "${WALLET_SETUP_RC:-1}" == "124" || "${WALLET_SETUP_RC:-1}" == "143" ]]; then
1346
+ WALLET_ERROR="Wallet baseline timed out after ${WALLET_BASELINE_TIMEOUT_SECONDS}s (startup continued)."
1347
+ echo "[wallet] WARNING: Wallet baseline timed out; continuing startup in degraded mode."
1348
+ else
1349
+ WALLET_ERROR=$(tail -n 1 "$WALLET_SETUP_LOG" 2>/dev/null || echo "wallet setup failed")
1350
+ echo "[wallet] WARNING: Wallet baseline failed; continuing startup in degraded mode."
1351
+ fi
1352
+ echo "[wallet] See: $WALLET_SETUP_LOG"
1353
+ fi
1354
+
1355
+ if [[ "$WALLET_HEALTHY" != "true" ]]; then
1356
+ WALLET_STARTUP_MODE="degraded"
1357
+ fi
1358
+ fi
1359
+
1360
+ TAILSCALE_DNS_NAME=""
1361
+ TAILSCALE_ADMIN_URL="N/A"
1362
+ TAILSCALE_SSH_CMD="N/A"
1363
+ TAILSCALE_SERVE_READY=0
1364
+ if command -v tailscale >/dev/null 2>&1; then
1365
+ if [[ "${FASED_TAILSCALE_AUTO_SERVE:-1}" == "1" ]]; then
1366
+ tailscale serve --bg "http://127.0.0.1:${FASED_GATEWAY_PORT}" >/dev/null 2>&1 || \
1367
+ sudo tailscale serve --bg "http://127.0.0.1:${FASED_GATEWAY_PORT}" >/dev/null 2>&1 || \
1368
+ tailscale serve https / "http://127.0.0.1:${FASED_GATEWAY_PORT}" >/dev/null 2>&1 || true
1369
+ fi
1370
+ if tailscale serve status 2>/dev/null | grep -q "127.0.0.1:${FASED_GATEWAY_PORT}" || \
1371
+ sudo tailscale serve status 2>/dev/null | grep -q "127.0.0.1:${FASED_GATEWAY_PORT}"; then
1372
+ TAILSCALE_SERVE_READY=1
1373
+ fi
1374
+ TAILSCALE_DNS_NAME=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // empty' | tr -d '\n' || true)
1375
+ TAILSCALE_DNS_NAME="${TAILSCALE_DNS_NAME%.}"
1376
+ if [[ -n "$TAILSCALE_DNS_NAME" ]]; then
1377
+ TAILSCALE_SSH_CMD="tailscale ssh app@${TAILSCALE_DNS_NAME}"
1378
+ if [[ "$TAILSCALE_SERVE_READY" == "1" ]]; then
1379
+ TAILSCALE_ADMIN_URL="https://${TAILSCALE_DNS_NAME}"
1380
+ fi
1381
+ fi
1382
+ fi
1383
+
1384
+ FED_HANDLE=$(jq -r '.handle // "N/A"' "$TOKEN_PATH" 2>/dev/null || echo "N/A")
1385
+ FED_TOKEN_ID=$(jq -r '.tokenId // "N/A"' "$TOKEN_PATH" 2>/dev/null || echo "N/A")
1386
+ FED_EXPIRES_AT=$(jq -r '.expiresAt // "N/A"' "$TOKEN_PATH" 2>/dev/null || echo "N/A")
1387
+ FED_AGENT_SLUG=$(jq -r '.agentSlug // "N/A"' "$TOKEN_PATH" 2>/dev/null || echo "N/A")
1388
+ FED_PUBLIC_URL=$(jq -r '.publicUrl // "N/A"' "$TOKEN_PATH" 2>/dev/null || echo "N/A")
1389
+ FED_ZROK_TOKEN=$(jq -r '.zrokToken // empty' "$TOKEN_PATH" 2>/dev/null || true)
1390
+ FED_ZROK_TOKEN_MASKED="N/A"
1391
+ if [[ -n "$FED_ZROK_TOKEN" ]]; then
1392
+ FED_ZROK_TOKEN_MASKED=$(mask_secret "$FED_ZROK_TOKEN")
1393
+ fi
1394
+ RES_TOKEN_MASKED="N/A"
1395
+ if [[ -n "${RES_TOKEN:-}" ]]; then
1396
+ RES_TOKEN_MASKED=$(mask_secret "$RES_TOKEN")
1397
+ fi
1398
+ GATEWAY_TOKEN_MASKED=$(mask_secret "$(tr -d '\n' < "$GW_TOKEN_PATH")")
1399
+
1400
+ echo "==> Agent Running."
1401
+ echo ""
1402
+ echo "================== FASED STARTUP SUMMARY =================="
1403
+ echo "Gateway"
1404
+ echo " PID: $AGENT_PID"
1405
+ echo " Port: $FASED_GATEWAY_PORT"
1406
+ echo " Boot log: $GATEWAY_BOOT_LOG"
1407
+ echo "Federation"
1408
+ echo " Handle: $FED_HANDLE"
1409
+ echo " Token ID: $FED_TOKEN_ID"
1410
+ echo " Expires At: $FED_EXPIRES_AT"
1411
+ echo " Agent Slug: $FED_AGENT_SLUG"
1412
+ echo " Public URL (token): $FED_PUBLIC_URL"
1413
+ echo " zrokToken: $FED_ZROK_TOKEN_MASKED"
1414
+ echo "Federation/A2A Tunnel (zrok)"
1415
+ echo " PID: ${ZROK_PID:-N/A}"
1416
+ echo " Slug: ${SLUG:-N/A}"
1417
+ echo " Reservation token: $RES_TOKEN_MASKED"
1418
+ echo " Public URL (A2A): $FINAL_URL"
1419
+ echo " Runtime log: $ZROK_RUNTIME_LOG"
1420
+ if [[ "$MANAGED_TUNNEL_DISABLED" == "1" ]]; then
1421
+ echo " Startup Mode: degraded"
1422
+ echo " Warning: ${TUNNEL_ERROR:-Fased Network tunnel unavailable; dashboard gateway kept online}"
1423
+ fi
1424
+ echo "Wallet"
1425
+ echo " Healthy: $WALLET_HEALTHY"
1426
+ echo " Startup Mode: $WALLET_STARTUP_MODE"
1427
+ echo " Auth State: $WALLET_AUTH_STATE"
1428
+ echo " Auth Mode: $WALLET_AUTH_MODE"
1429
+ echo " Auth Source: $WALLET_AUTH_SOURCE"
1430
+ echo " Mode: $WALLET_MODE"
1431
+ echo " PID: $WALLET_PID"
1432
+ echo " Service: $WALLET_SERVICE"
1433
+ echo " Chains: $WALLET_CHAINS"
1434
+ echo " Direct Signing: $WALLET_DIRECT_SIGNING"
1435
+ echo " Tool Scope: $WALLET_TOOL_SCOPE"
1436
+ echo " Keys path: $WALLET_KEYS_PATH"
1437
+ echo " Runtime log: $WALLET_SETUP_LOG"
1438
+ if [[ "$WALLET_STARTUP_MODE" == "degraded" ]]; then
1439
+ echo " Warning: wallet degraded; gateway/tunnel kept online"
1440
+ if [[ -n "$WALLET_ERROR" ]]; then
1441
+ echo " Last Error: $WALLET_ERROR"
1442
+ fi
1443
+ fi
1444
+ echo "Signer"
1445
+ echo " Startup Mode: $SIGNERD_STARTUP_MODE"
1446
+ if [[ -n "$SIGNERD_ERROR" ]]; then
1447
+ echo " Last Error: $SIGNERD_ERROR"
1448
+ fi
1449
+ echo "Admin Access (Tailscale)"
1450
+ echo " Admin URL: $TAILSCALE_ADMIN_URL"
1451
+ if [[ "$TAILSCALE_ADMIN_URL" == "N/A" ]]; then
1452
+ if [[ -n "$TAILSCALE_DNS_NAME" ]]; then
1453
+ echo " Status: DNS present but tailscale serve is not active for 127.0.0.1:${FASED_GATEWAY_PORT}"
1454
+ echo " Fix: tailscale serve --bg http://127.0.0.1:${FASED_GATEWAY_PORT}"
1455
+ else
1456
+ echo " Status: tailscale DNS unavailable; run 'tailscale up --ssh' and verify with 'tailscale status'"
1457
+ fi
1458
+ else
1459
+ echo " Status: tailnet-only URL (not public internet)"
1460
+ fi
1461
+ echo " SSH command: $TAILSCALE_SSH_CMD"
1462
+ echo " Auth: Gateway token/password still required by Fased UI/API"
1463
+ echo " Public login link: disabled (no one-time public dashboard links)"
1464
+ echo "Secrets"
1465
+ echo " Gateway token file: $GW_TOKEN_PATH"
1466
+ echo " Gateway token: $GATEWAY_TOKEN_MASKED"
1467
+ echo " Federation token: $TOKEN_PATH"
1468
+ echo "==========================================================="
1469
+
1470
+ echo ""
1471
+ echo "Stream gateway logs: tail -f $GATEWAY_BOOT_LOG"
1472
+ echo "Stream zrok logs: tail -f $ZROK_RUNTIME_LOG"
1473
+
1474
+ cleanup_managed_runtime() {
1475
+ local current_zrok_pid=""
1476
+ local current_monitor_pid=""
1477
+ if [[ -f "$FASED_CONFIG_DIR/.zrok-pid" ]]; then
1478
+ current_zrok_pid="$(cat "$FASED_CONFIG_DIR/.zrok-pid" 2>/dev/null || true)"
1479
+ fi
1480
+ if [[ -f "$ZROK_MONITOR_PID_FILE" ]]; then
1481
+ current_monitor_pid="$(cat "$ZROK_MONITOR_PID_FILE" 2>/dev/null || true)"
1482
+ fi
1483
+ if [[ -n "${AGENT_PID:-}" ]] && kill -0 "$AGENT_PID" 2>/dev/null; then
1484
+ kill "$AGENT_PID" 2>/dev/null || true
1485
+ fi
1486
+ if [[ -n "$current_zrok_pid" ]] && kill -0 "$current_zrok_pid" 2>/dev/null; then
1487
+ kill "$current_zrok_pid" 2>/dev/null || true
1488
+ fi
1489
+ if [[ -n "${ZROK_PID:-}" ]] && kill -0 "$ZROK_PID" 2>/dev/null; then
1490
+ kill "$ZROK_PID" 2>/dev/null || true
1491
+ fi
1492
+ if [[ -n "$current_monitor_pid" ]] && kill -0 "$current_monitor_pid" 2>/dev/null; then
1493
+ kill "$current_monitor_pid" 2>/dev/null || true
1494
+ fi
1495
+ if [[ -n "${ZROK_MONITOR_PID:-}" ]] && kill -0 "$ZROK_MONITOR_PID" 2>/dev/null; then
1496
+ kill "$ZROK_MONITOR_PID" 2>/dev/null || true
1497
+ fi
1498
+ rm -f "$FASED_CONFIG_DIR/.zrok-pid" "$ZROK_MONITOR_PID_FILE" 2>/dev/null || true
1499
+ stop_existing_signerd >/dev/null 2>&1 || true
1500
+ force_stop_local_gateway
1501
+ }
1502
+
1503
+ MANAGED_RUNTIME_SHUTTING_DOWN=0
1504
+
1505
+ handle_managed_runtime_signal() {
1506
+ MANAGED_RUNTIME_SHUTTING_DOWN=1
1507
+ cleanup_managed_runtime
1508
+ exit 0
1509
+ }
1510
+
1511
+ trap cleanup_managed_runtime EXIT
1512
+ trap handle_managed_runtime_signal INT TERM
1513
+ if kill -0 "$AGENT_PID" 2>/dev/null; then
1514
+ wait "$AGENT_PID" || true
1515
+ fi
1516
+
1517
+ if [[ "$MANAGED_RUNTIME_SHUTTING_DOWN" == "1" ]]; then
1518
+ exit 0
1519
+ fi
1520
+
1521
+ echo "[gateway] supervising listener on port ${FASED_GATEWAY_PORT}."
1522
+ while true; do
1523
+ if ! is_gateway_listener_ready; then
1524
+ echo "[gateway] ERROR: Gateway listener stopped on port ${FASED_GATEWAY_PORT}."
1525
+ exit 1
1526
+ fi
1527
+ sleep 5
1528
+ done