create-walle 0.9.21 → 0.9.23

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 (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. package/template/wall-e/eval/weekly-eval-loop.js +0 -241
@@ -19,6 +19,8 @@ const HOST_READY_TIMEOUT_MS = 45000;
19
19
  const WATCHDOG_CHECK_INTERVAL_MS = 30000;
20
20
  const WATCHDOG_WAKE_DRIFT_MS = 90000;
21
21
  const PUBLIC_PROBE_TIMEOUT_MS = 6000;
22
+ const DEVTUNNEL_MAX_EXPIRATION = '30d';
23
+ const DEVTUNNEL_RENEWAL_INTERVAL_MS = 25 * 24 * 60 * 60 * 1000;
22
24
  const HOST_HEADER_MODE_PASSTHROUGH = 'passthrough';
23
25
  const HOST_HEADER_MODE_DEFAULT = 'default';
24
26
 
@@ -68,6 +70,58 @@ function loginProviderLabel(provider) {
68
70
  return provider === 'github' ? 'GitHub' : 'Microsoft';
69
71
  }
70
72
 
73
+ // Host-event diagnostics: classify devtunnel host output so the Access page can
74
+ // show WHY the phone link dropped (credential expiry vs relay disconnect vs
75
+ // recovery) instead of a generic failure.
76
+ const HOST_EVENT_LIMIT = 30;
77
+ const HOST_EVENT_REPEAT_SUPPRESS_MS = 30 * 1000;
78
+
79
+ function classifyDevTunnelHostEventText(text) {
80
+ const t = String(text || '');
81
+ if (!t.trim()) return null;
82
+ if (/unauthoriz|forbidden|authentication failed|login\s+required|token[^\n]{0,60}(expired|invalid|refresh)/i.test(t)) {
83
+ return 'auth_failure';
84
+ }
85
+ if (/connection\s*lost|session closed/i.test(t)) return 'connection_lost';
86
+ if (/relay restored|reconnected/i.test(t)) return 'reconnected';
87
+ return null;
88
+ }
89
+
90
+ function appendManagedTunnelHostEvent(event, options = {}) {
91
+ if (!event || !event.type) return null;
92
+ try {
93
+ const state = loadManagedTunnelState(options);
94
+ if (!state || !state.tunnel_id) return null;
95
+ const events = Array.isArray(state.host_events) ? state.host_events : [];
96
+ const at = new Date().toISOString();
97
+ const last = events.length ? events[events.length - 1] : null;
98
+ const suppressMs = Number(options.hostEventRepeatSuppressMs || HOST_EVENT_REPEAT_SUPPRESS_MS);
99
+ if (last && last.type === event.type && (Date.parse(at) - (Date.parse(last.at) || 0)) < suppressMs) {
100
+ return null;
101
+ }
102
+ const entry = { at, type: String(event.type), detail: String(event.detail || '').slice(0, 240) };
103
+ state.host_events = [...events, entry].slice(-HOST_EVENT_LIMIT);
104
+ writeManagedTunnelState(state, options);
105
+ return entry;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // Only the CTM process that spawned the host sees its pipes; after a CTM restart
112
+ // the re-adopted host runs unobserved until its next (re)start.
113
+ function watchDevTunnelHostOutput(child, options = {}) {
114
+ let carry = '';
115
+ function scan(chunk) {
116
+ const text = carry + String(chunk || '');
117
+ carry = text.slice(-200);
118
+ const type = classifyDevTunnelHostEventText(text);
119
+ if (type) appendManagedTunnelHostEvent({ type, detail: text.trim().slice(-240) }, options);
120
+ }
121
+ if (child && child.stdout) child.stdout.on('data', scan);
122
+ if (child && child.stderr) child.stderr.on('data', scan);
123
+ }
124
+
71
125
  function setupDir(options = {}) {
72
126
  const configDir = path.resolve(options.configDir || path.join(process.env.HOME || process.cwd(), '.walle', 'data'));
73
127
  return path.join(configDir, 'microsoft-dev-tunnel');
@@ -231,10 +285,32 @@ function devTunnelAccessListArgs(tunnelId, options = {}) {
231
285
  return ['access', 'list', tunnelId, '-p', String(options.port || 3456), '-j'];
232
286
  }
233
287
 
288
+ function devTunnelUpdateArgs(tunnelId, options = {}) {
289
+ return ['update', tunnelId, '--expiration', String(options.tunnelExpiration || options.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION)];
290
+ }
291
+
234
292
  function devTunnelPrivateAccessResetArgs(tunnelId, options = {}) {
235
293
  return ['access', 'reset', tunnelId, '-p', String(options.port || 3456), '-j'];
236
294
  }
237
295
 
296
+ function devTunnelAccessDeleteArgs(tunnelId, index, options = {}) {
297
+ return ['access', 'delete', tunnelId, '-p', String(options.port || 3456), '-i', String(index), '-j'];
298
+ }
299
+
300
+ function devTunnelAnonymousConnectAccessArgs(tunnelId, options = {}) {
301
+ const expiration = String(options.anonymousAccessExpiration || options.anonymous_access_expiration || DEVTUNNEL_MAX_EXPIRATION);
302
+ return [
303
+ 'access',
304
+ 'create',
305
+ tunnelId,
306
+ '-p', String(options.port || 3456),
307
+ '--anonymous',
308
+ '--scopes', 'connect',
309
+ '-e', expiration,
310
+ '-j',
311
+ ];
312
+ }
313
+
238
314
  function caffeinateArgs(options = {}) {
239
315
  const parentPid = Number(options.parentPid || process.pid);
240
316
  const args = ['-i', '-m'];
@@ -243,6 +319,42 @@ function caffeinateArgs(options = {}) {
243
319
  return args;
244
320
  }
245
321
 
322
+ function isoAfter(timestamp, ms) {
323
+ const base = Date.parse(timestamp || '');
324
+ const at = Number.isFinite(base) ? base : Date.now();
325
+ return new Date(at + ms).toISOString();
326
+ }
327
+
328
+ function ageMs(timestamp) {
329
+ const at = Date.parse(timestamp || '');
330
+ if (!Number.isFinite(at)) return Infinity;
331
+ return Date.now() - at;
332
+ }
333
+
334
+ function renewalDueFrom(timestamp, options = {}) {
335
+ const intervalMs = Number(options.renewalIntervalMs || options.renewal_interval_ms || DEVTUNNEL_RENEWAL_INTERVAL_MS);
336
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) return false;
337
+ return ageMs(timestamp) >= intervalMs;
338
+ }
339
+
340
+ function microsoftTunnelTunnelRenewalDue(state = {}, options = {}) {
341
+ if (!state || typeof state !== 'object') return true;
342
+ return renewalDueFrom(state.last_tunnel_renewed_at || state.tunnel_renewed_at || state.started_at, options);
343
+ }
344
+
345
+ function microsoftTunnelAccessRenewalDue(state = {}, options = {}) {
346
+ const mode = normalizeMicrosoftTunnelAccessMode(state.access_mode || state.access?.mode || options.accessMode || options.access_mode);
347
+ if (mode !== 'ctm_authenticated') return false;
348
+ return renewalDueFrom(state.last_access_renewed_at || state.access?.renewed_at || state.access?.created_at, options);
349
+ }
350
+
351
+ function microsoftTunnelRenewalDue(state = {}, options = {}) {
352
+ return {
353
+ tunnel: microsoftTunnelTunnelRenewalDue(state, options),
354
+ access: microsoftTunnelAccessRenewalDue(state, options),
355
+ };
356
+ }
357
+
246
358
  async function inspectManagedTunnelProcess(state = {}, options = {}) {
247
359
  const pid = Number(state && state.pid);
248
360
  const pidRunning = pidIsRunning(pid);
@@ -482,31 +594,110 @@ async function annotateWatchdogState(partial, options = {}) {
482
594
 
483
595
  function managedTunnelAccessSummary(partial = {}, options = {}) {
484
596
  const port = Number(options.port || 3456);
597
+ const mode = normalizeMicrosoftTunnelAccessMode(partial.mode || options.accessMode || options.access_mode);
598
+ const anonymousConnect = Object.prototype.hasOwnProperty.call(partial, 'anonymous_connect')
599
+ ? !!partial.anonymous_connect
600
+ : mode === 'ctm_authenticated';
601
+ const staleAnonymous = !!partial.stale_anonymous_access
602
+ || !!partial.has_stale_anonymous_access
603
+ || (mode === 'private_microsoft' && anonymousConnect && !partial.reset);
604
+ const error = partial.error || '';
485
605
  return {
486
- mode: 'private_microsoft',
487
- anonymous_connect: false,
606
+ mode,
607
+ status: error ? 'error' : (staleAnonymous ? 'stale_anonymous' : mode),
608
+ anonymous_connect: anonymousConnect,
609
+ stale_anonymous_access: staleAnonymous,
610
+ needs_private_reset: staleAnonymous,
488
611
  port,
489
612
  checked_at: partial.checked_at || new Date().toISOString(),
490
613
  already_configured: !!partial.already_configured,
614
+ created: !!partial.created,
615
+ renewed: !!partial.renewed,
616
+ renewed_at: partial.renewed_at || '',
617
+ expiration: partial.expiration || '',
618
+ next_renewal_at: partial.next_renewal_at || '',
619
+ renewal_due: !!partial.renewal_due,
491
620
  reset: !!partial.reset,
492
621
  anonymous_removed: !!partial.anonymous_removed,
493
622
  list_error: partial.list_error || '',
494
- error: partial.error || '',
623
+ error,
495
624
  };
496
625
  }
497
626
 
627
+ function normalizeMicrosoftTunnelAccessMode(value) {
628
+ const raw = String(value || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
629
+ if (raw === 'private' || raw === 'microsoft' || raw === 'private_microsoft') return 'private_microsoft';
630
+ if (raw === 'anonymous' || raw === 'public' || raw === 'ctm_auth' || raw === 'ctm_authenticated') return 'ctm_authenticated';
631
+ // Default is app-gated (ctm_authenticated): reachable URL + CTM passkey/device
632
+ // auth. It is the only PWA/WebSocket-compatible mode; private is an opt-in.
633
+ return 'ctm_authenticated';
634
+ }
635
+
636
+ // SECURITY / REACHABILITY: the Dev Tunnel default is "ctm_authenticated" — the
637
+ // tunnel transport is reachable by URL, and CTM's own app-layer auth (per-origin
638
+ // passkey + revocable device token + step-up, behind an unguessable random tunnel
639
+ // URL) is the gate. This is the ONLY mode compatible with the phone client, which
640
+ // is a PWA that talks to CTM via fetch() + a WebSocket: Dev Tunnels "private" mode
641
+ // enforces auth by redirecting the *initial request* to an interactive
642
+ // Microsoft/GitHub login page, and programmatic XHR/fetch/WebSocket cannot follow
643
+ // that cross-origin redirect — so private silently bricks remote access.
644
+ //
645
+ // "private_microsoft" therefore is an explicit, deliberate opt-in only. It is
646
+ // honored when it comes from a fresh user selection (input.access_mode), the env
647
+ // (CTM_MS_TUNNEL_ACCESS_MODE), or options.allowPrivateAccess — but NOT when a
648
+ // private value arrives only from restored/persisted state. A persisted private
649
+ // value is treated as stale (it predates this policy) and migrated back to the
650
+ // app-gated default, so the phone keeps working across headless restarts.
651
+ function microsoftTunnelPrivateAccessForced(options = {}) {
652
+ if (options.allowPrivateAccess === true) return true;
653
+ const envMode = String(process.env.CTM_MS_TUNNEL_ACCESS_MODE || '').trim();
654
+ return envMode !== '' && normalizeMicrosoftTunnelAccessMode(envMode) === 'private_microsoft';
655
+ }
656
+
657
+ function resolveMicrosoftTunnelAccessMode(input = {}, options = {}) {
658
+ const fromInput = normalizeMicrosoftTunnelAccessMode(input.access_mode || input.accessMode);
659
+ const inputIsExplicit = !!(input.access_mode || input.accessMode);
660
+ const requested = inputIsExplicit ? fromInput : normalizeMicrosoftTunnelAccessMode(
661
+ options.accessMode
662
+ || options.access_mode
663
+ || process.env.CTM_MS_TUNNEL_ACCESS_MODE
664
+ );
665
+ if (requested === 'private_microsoft') {
666
+ const deliberate = (inputIsExplicit && fromInput === 'private_microsoft')
667
+ || microsoftTunnelPrivateAccessForced(options);
668
+ if (!deliberate) return 'ctm_authenticated';
669
+ }
670
+ return requested;
671
+ }
672
+
498
673
  function persistManagedTunnelAccess(tunnelId, access, options = {}) {
499
674
  const state = loadManagedTunnelState(options);
500
675
  if (!state || normalizeTunnelId(state.tunnel_id || '') !== normalizeTunnelId(tunnelId || '')) return;
676
+ const renewedAt = access.renewed_at || (access.created ? access.checked_at : '') || '';
501
677
  writeManagedTunnelState({
502
678
  ...state,
503
679
  access_mode: access.mode || 'private_microsoft',
504
680
  access,
505
681
  last_access_check_at: access.checked_at || new Date().toISOString(),
682
+ last_access_renewed_at: renewedAt || state.last_access_renewed_at || '',
683
+ next_access_renewal_at: access.next_renewal_at || (renewedAt ? isoAfter(renewedAt, DEVTUNNEL_RENEWAL_INTERVAL_MS) : state.next_access_renewal_at || ''),
684
+ access_expiration: access.expiration || state.access_expiration || '',
506
685
  access_error: access.error || '',
507
686
  }, options);
508
687
  }
509
688
 
689
+ function persistManagedTunnelRenewal(tunnelId, renewal, options = {}) {
690
+ const state = loadManagedTunnelState(options);
691
+ if (!state || normalizeTunnelId(state.tunnel_id || '') !== normalizeTunnelId(tunnelId || '')) return;
692
+ writeManagedTunnelState({
693
+ ...state,
694
+ last_tunnel_renewed_at: renewal.renewed_at || state.last_tunnel_renewed_at || '',
695
+ next_tunnel_renewal_at: renewal.next_renewal_at || state.next_tunnel_renewal_at || '',
696
+ tunnel_expiration: renewal.expiration || state.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION,
697
+ tunnel_renewal_error: renewal.error || '',
698
+ }, options);
699
+ }
700
+
510
701
  function parseDevTunnelAccessEntries(stdout) {
511
702
  const text = String(stdout || '').trim();
512
703
  if (!text) return [];
@@ -544,6 +735,47 @@ function hasAnonymousConnectAccess(stdout) {
544
735
  return parseDevTunnelAccessEntries(stdout).some(accessEntryAllowsAnonymousConnect);
545
736
  }
546
737
 
738
+ function anonymousConnectAccessIndices(stdout) {
739
+ return parseDevTunnelAccessEntries(stdout)
740
+ .map((entry, index) => (accessEntryAllowsAnonymousConnect(entry) ? index : -1))
741
+ .filter((index) => index >= 0);
742
+ }
743
+
744
+ async function inspectMicrosoftTunnelAccess(tunnelId, options = {}) {
745
+ const id = normalizeTunnelId(tunnelId);
746
+ const checkedAt = new Date().toISOString();
747
+ if (!id) {
748
+ return managedTunnelAccessSummary({
749
+ checked_at: checkedAt,
750
+ error: 'Microsoft tunnel id is missing.',
751
+ }, options);
752
+ }
753
+
754
+ try {
755
+ const list = await execDevTunnel(devTunnelAccessListArgs(id, options), options);
756
+ const body = list?.stdout || list?.stderr || '';
757
+ const hasAnonymous = hasAnonymousConnectAccess(body);
758
+ const mode = resolveMicrosoftTunnelAccessMode({}, options);
759
+ const access = managedTunnelAccessSummary({
760
+ mode,
761
+ anonymous_connect: hasAnonymous,
762
+ already_configured: mode === 'ctm_authenticated' ? hasAnonymous : !hasAnonymous,
763
+ checked_at: checkedAt,
764
+ }, options);
765
+ persistManagedTunnelAccess(id, access, options);
766
+ return access;
767
+ } catch (err) {
768
+ const message = String(err?.stderr || err?.message || err || 'Could not list Microsoft tunnel access.').slice(0, 800);
769
+ const access = managedTunnelAccessSummary({
770
+ checked_at: checkedAt,
771
+ list_error: message,
772
+ error: message,
773
+ }, options);
774
+ persistManagedTunnelAccess(id, access, options);
775
+ return access;
776
+ }
777
+ }
778
+
547
779
  function normalizeTunnelId(value) {
548
780
  const id = String(value || '').trim().toLowerCase();
549
781
  return /^[a-z0-9][a-z0-9-]{2,59}$/.test(id) ? id : '';
@@ -559,6 +791,14 @@ function parseDevTunnelUserShow(stdout, stderr) {
559
791
  if (/not\s+(logged|signed)\s+in|no\s+user|login\s+required/i.test(text)) {
560
792
  return { signed_in: false, account: null, raw: text.slice(0, 800) };
561
793
  }
794
+ if (/(?:github|microsoft|entra|oauth|auth(?:entication)?|token|credential)[^\n]*(?:refresh|expired|invalid|failed|unauthori[sz]ed)|(?:refresh|expired|invalid|failed|unauthori[sz]ed)[^\n]*(?:github|microsoft|entra|oauth|auth(?:entication)?|token|credential)|invalid_grant|interaction_required|AADSTS/i.test(text)) {
795
+ return {
796
+ signed_in: false,
797
+ account: null,
798
+ raw: text.slice(0, 800),
799
+ error: text.slice(0, 800),
800
+ };
801
+ }
562
802
  let parsed = null;
563
803
  try { parsed = JSON.parse(text); } catch {}
564
804
  if (parsed && typeof parsed === 'object') {
@@ -705,6 +945,60 @@ async function detectLoginStatus(options = {}) {
705
945
  }
706
946
  }
707
947
 
948
+ async function checkMicrosoftDevTunnelLoginStatus(options = {}) {
949
+ const previous = loadLoginProcessState(options) || {};
950
+ const checkedAt = new Date().toISOString();
951
+ const login = await detectLoginStatus(options);
952
+ const provider = previous.provider || normalizeLoginProvider(options);
953
+ const signedIn = !!login.signed_in;
954
+ const account = login.account || null;
955
+ const nextState = {
956
+ ...previous,
957
+ provider,
958
+ provider_label: previous.provider_label || loginProviderLabel(provider),
959
+ installed: login.installed !== false,
960
+ signed_in: signedIn,
961
+ account,
962
+ account_display: account && account.display ? String(account.display) : '',
963
+ login_checked_at: checkedAt,
964
+ login_check_error: signedIn ? '' : String(login.error || '').slice(0, 800),
965
+ };
966
+ if (signedIn) {
967
+ nextState.running = false;
968
+ nextState.finished_at = nextState.finished_at || checkedAt;
969
+ nextState.error = '';
970
+ nextState.error_code = '';
971
+ nextState.verified_at = checkedAt;
972
+ } else if (login.installed === false) {
973
+ nextState.running = false;
974
+ nextState.error_code = 'devtunnel_cli_missing';
975
+ nextState.error = login.error || 'devtunnel CLI is not installed or not on PATH.';
976
+ }
977
+ if (previous.signed_in === true && !signedIn && login.installed !== false) {
978
+ appendManagedTunnelHostEvent({
979
+ type: 'credential_expired',
980
+ detail: nextState.login_check_error || `${loginProviderLabel(provider)} sign-in is no longer valid.`,
981
+ }, options);
982
+ } else if (previous.signed_in === false && signedIn) {
983
+ appendManagedTunnelHostEvent({ type: 'signed_in', detail: nextState.account_display || '' }, options);
984
+ }
985
+ writeLoginProcessState(nextState, options);
986
+ const progress = getMicrosoftDevTunnelProgress(options);
987
+ const setup = options.includeSetup === false
988
+ ? null
989
+ : await detectMicrosoftDevTunnelSetup(options).catch(() => null);
990
+ return {
991
+ ok: signedIn,
992
+ signed_in: signedIn,
993
+ account,
994
+ login: progress.login,
995
+ setup,
996
+ microsoft_dev_tunnel: setup,
997
+ progress,
998
+ error: signedIn ? '' : (login.error || 'Dev Tunnels is not signed in yet. Finish the browser sign-in, then check again.'),
999
+ };
1000
+ }
1001
+
708
1002
  async function detectManagedMicrosoftDevTunnel(options = {}) {
709
1003
  const state = loadManagedTunnelState(options);
710
1004
  if (!state) return { configured: false, running: false };
@@ -743,7 +1037,15 @@ async function detectManagedMicrosoftDevTunnel(options = {}) {
743
1037
  access_mode: state.access_mode || (state.access && state.access.mode) || '',
744
1038
  access: state.access || null,
745
1039
  last_access_check_at: state.last_access_check_at || (state.access && state.access.checked_at) || '',
1040
+ last_access_renewed_at: state.last_access_renewed_at || (state.access && state.access.renewed_at) || '',
1041
+ next_access_renewal_at: state.next_access_renewal_at || (state.access && state.access.next_renewal_at) || '',
1042
+ access_expiration: state.access_expiration || (state.access && state.access.expiration) || '',
746
1043
  access_error: state.access_error || (state.access && state.access.error) || '',
1044
+ last_tunnel_renewed_at: state.last_tunnel_renewed_at || '',
1045
+ next_tunnel_renewal_at: state.next_tunnel_renewal_at || '',
1046
+ tunnel_expiration: state.tunnel_expiration || '',
1047
+ tunnel_renewal_error: state.tunnel_renewal_error || '',
1048
+ host_events: Array.isArray(state.host_events) ? state.host_events : [],
747
1049
  state_path: managedStatePath(options),
748
1050
  };
749
1051
  }
@@ -978,12 +1280,26 @@ async function detectMicrosoftDevTunnelSetup(options = {}) {
978
1280
  }
979
1281
 
980
1282
  const login = await detectLoginStatus(options);
981
- const managed = await detectManagedMicrosoftDevTunnel(options);
1283
+ let managed = await detectManagedMicrosoftDevTunnel(options);
1284
+ if (login.signed_in && managed.tunnel_id) {
1285
+ const access = await inspectMicrosoftTunnelAccess(managed.tunnel_id, {
1286
+ ...options,
1287
+ accessMode: managed.access_mode || managed.access?.mode || options.accessMode || options.access_mode,
1288
+ });
1289
+ managed = {
1290
+ ...managed,
1291
+ access,
1292
+ access_mode: access.mode,
1293
+ last_access_check_at: access.checked_at,
1294
+ access_error: access.error || '',
1295
+ };
1296
+ }
982
1297
  return {
983
1298
  installed: login.installed !== false,
984
1299
  available: login.installed !== false && !!login.signed_in,
985
1300
  signed_in: !!login.signed_in,
986
1301
  account: login.account || null,
1302
+ login_provider: loginStateProvider(loadLoginProcessState(options) || {}),
987
1303
  version,
988
1304
  origin: managed.origin || '',
989
1305
  mobile_url: managed.mobile_url || '',
@@ -1129,12 +1445,15 @@ function microsoftDevTunnelCommands(options = {}) {
1129
1445
  login: command('devtunnel', ['user', 'login', '-g']),
1130
1446
  device_login: command('devtunnel', ['user', 'login', '-g', '-d']),
1131
1447
  microsoft_device_login: command('devtunnel', ['user', 'login', '-e', '-d']),
1448
+ logout: command('devtunnel', ['user', 'logout']),
1132
1449
  access_private_reset: command('devtunnel', devTunnelPrivateAccessResetArgs('TUNNELID', { port })),
1450
+ access_ctm_authenticated: command('devtunnel', devTunnelAnonymousConnectAccessArgs('TUNNELID', { port })),
1451
+ renew_tunnel: command('devtunnel', devTunnelUpdateArgs('TUNNELID', { port })),
1133
1452
  host_temporary: command('devtunnel', [
1134
1453
  'host',
1135
1454
  '-p', String(port),
1136
1455
  '--protocol', 'http',
1137
- '--expiration', '30d',
1456
+ '--expiration', DEVTUNNEL_MAX_EXPIRATION,
1138
1457
  '--host-header', 'unchanged',
1139
1458
  '--origin-header', 'unchanged',
1140
1459
  ]),
@@ -1142,7 +1461,7 @@ function microsoftDevTunnelCommands(options = {}) {
1142
1461
  'host',
1143
1462
  '-p', String(port),
1144
1463
  '--protocol', 'http',
1145
- '--expiration', '30d',
1464
+ '--expiration', DEVTUNNEL_MAX_EXPIRATION,
1146
1465
  ]),
1147
1466
  };
1148
1467
  }
@@ -1204,12 +1523,215 @@ async function ensurePrivateConnectAccess(tunnelId, options = {}) {
1204
1523
  }
1205
1524
  }
1206
1525
 
1526
+ async function renewCtmAuthenticatedConnectAccess(tunnelId, accessListBody, options = {}) {
1527
+ const checkedAt = new Date().toISOString();
1528
+ const indices = anonymousConnectAccessIndices(accessListBody);
1529
+ // Delete descending so the CLI's zero-based indices do not shift underneath us.
1530
+ for (const index of indices.slice().sort((a, b) => b - a)) {
1531
+ await execDevTunnel(devTunnelAccessDeleteArgs(tunnelId, index, options), options);
1532
+ }
1533
+ await execDevTunnel(devTunnelAnonymousConnectAccessArgs(tunnelId, options), options);
1534
+ const access = managedTunnelAccessSummary({
1535
+ mode: 'ctm_authenticated',
1536
+ renewed: true,
1537
+ renewed_at: checkedAt,
1538
+ expiration: String(options.anonymousAccessExpiration || options.anonymous_access_expiration || DEVTUNNEL_MAX_EXPIRATION),
1539
+ next_renewal_at: isoAfter(checkedAt, DEVTUNNEL_RENEWAL_INTERVAL_MS),
1540
+ checked_at: checkedAt,
1541
+ }, options);
1542
+ persistManagedTunnelAccess(tunnelId, access, options);
1543
+ return access;
1544
+ }
1545
+
1546
+ async function ensureCtmAuthenticatedConnectAccess(tunnelId, options = {}) {
1547
+ const checkedAt = new Date().toISOString();
1548
+ const listArgs = devTunnelAccessListArgs(tunnelId, options);
1549
+ let listError = '';
1550
+ let body = '';
1551
+ try {
1552
+ const list = await execDevTunnel(listArgs, options);
1553
+ body = list?.stdout || list?.stderr || '';
1554
+ } catch (err) {
1555
+ listError = String(err?.stderr || err?.message || err || 'Could not list Microsoft tunnel access.').slice(0, 500);
1556
+ // Listing can fail on older CLI builds or transient service errors. Try to
1557
+ // establish the narrow connect-only rule; CTM still owns application auth.
1558
+ }
1559
+ const hasAnonymous = hasAnonymousConnectAccess(body);
1560
+ if (hasAnonymous) {
1561
+ const state = loadManagedTunnelState(options) || {};
1562
+ const renewalDue = options.renewAccess === true || options.renew_access === true || microsoftTunnelAccessRenewalDue(state, options);
1563
+ if (renewalDue) {
1564
+ return await renewCtmAuthenticatedConnectAccess(tunnelId, body, options);
1565
+ }
1566
+ const access = managedTunnelAccessSummary({
1567
+ mode: 'ctm_authenticated',
1568
+ already_configured: true,
1569
+ renewal_due: false,
1570
+ checked_at: checkedAt,
1571
+ }, options);
1572
+ persistManagedTunnelAccess(tunnelId, access, options);
1573
+ return access;
1574
+ }
1575
+
1576
+ try {
1577
+ await execDevTunnel(devTunnelAnonymousConnectAccessArgs(tunnelId, options), options);
1578
+ const access = managedTunnelAccessSummary({
1579
+ mode: 'ctm_authenticated',
1580
+ created: true,
1581
+ renewed_at: checkedAt,
1582
+ expiration: String(options.anonymousAccessExpiration || options.anonymous_access_expiration || DEVTUNNEL_MAX_EXPIRATION),
1583
+ next_renewal_at: isoAfter(checkedAt, DEVTUNNEL_RENEWAL_INTERVAL_MS),
1584
+ list_error: listError,
1585
+ checked_at: checkedAt,
1586
+ }, options);
1587
+ persistManagedTunnelAccess(tunnelId, access, options);
1588
+ return access;
1589
+ } catch (err) {
1590
+ if (ignoreAlreadyExists(err)) {
1591
+ const access = managedTunnelAccessSummary({
1592
+ mode: 'ctm_authenticated',
1593
+ already_configured: true,
1594
+ renewal_due: false,
1595
+ list_error: listError,
1596
+ checked_at: checkedAt,
1597
+ }, options);
1598
+ persistManagedTunnelAccess(tunnelId, access, options);
1599
+ return access;
1600
+ }
1601
+ const message = String(err?.stderr || err?.message || err || 'Could not enable CTM-authenticated Microsoft tunnel access.').slice(0, 800);
1602
+ const access = managedTunnelAccessSummary({
1603
+ mode: 'ctm_authenticated',
1604
+ checked_at: checkedAt,
1605
+ list_error: listError,
1606
+ error: message,
1607
+ }, options);
1608
+ persistManagedTunnelAccess(tunnelId, access, options);
1609
+ throw err;
1610
+ }
1611
+ }
1612
+
1613
+ async function ensureMicrosoftTunnelConnectAccess(tunnelId, options = {}) {
1614
+ const accessMode = resolveMicrosoftTunnelAccessMode(options.input || {}, options);
1615
+ // Default is the app-gated (ctm_authenticated) anonymous-connect ACE so the
1616
+ // phone PWA + WebSocket can reach CTM; CTM's own passkey/device-token auth is
1617
+ // the gate. Only take the private path when private was a deliberate opt-in
1618
+ // (resolveMicrosoftTunnelAccessMode already migrated stale persisted private
1619
+ // back to ctm_authenticated).
1620
+ if (accessMode === 'private_microsoft') {
1621
+ return ensurePrivateConnectAccess(tunnelId, { ...options, accessMode: 'private_microsoft' });
1622
+ }
1623
+ return ensureCtmAuthenticatedConnectAccess(tunnelId, { ...options, accessMode: 'ctm_authenticated' });
1624
+ }
1625
+
1626
+ async function resetMicrosoftDevTunnelPrivateAccess(input = {}, options = {}) {
1627
+ // This is the explicit, deliberate "switch to private" action, so it carries the
1628
+ // private opt-in flag — otherwise the follow-up detect/inspect would resolve the
1629
+ // mode without it and migrate the just-set private back to the app-gated default.
1630
+ const mergedOptions = {
1631
+ ...options,
1632
+ accessMode: 'private_microsoft',
1633
+ allowPrivateAccess: true,
1634
+ input: { ...(options.input || {}), ...(input || {}), access_mode: 'private_microsoft' },
1635
+ };
1636
+ const state = loadManagedTunnelState(mergedOptions) || {};
1637
+ const tunnelId = normalizeTunnelId(input.tunnel_id || input.tunnelId || state.tunnel_id || '');
1638
+ if (!tunnelId) {
1639
+ return {
1640
+ ok: false,
1641
+ error_code: 'microsoft_tunnel_missing',
1642
+ error: 'No managed Microsoft Dev Tunnel exists yet.',
1643
+ setup: await detectMicrosoftDevTunnelSetup(mergedOptions).catch(() => null),
1644
+ };
1645
+ }
1646
+ const login = await detectLoginStatus(mergedOptions);
1647
+ if (login.installed === false || !login.signed_in) {
1648
+ return {
1649
+ ok: false,
1650
+ error_code: login.installed === false ? 'devtunnel_cli_missing' : 'microsoft_tunnel_sign_in_required',
1651
+ error: login.installed === false
1652
+ ? 'Microsoft devtunnel CLI is not installed or not on PATH.'
1653
+ : 'Sign in to Microsoft Dev Tunnels on this Mac before resetting tunnel access.',
1654
+ setup: await detectMicrosoftDevTunnelSetup(mergedOptions).catch(() => null),
1655
+ };
1656
+ }
1657
+ try {
1658
+ const access = await ensurePrivateConnectAccess(tunnelId, mergedOptions);
1659
+ const managed = await detectManagedMicrosoftDevTunnel(mergedOptions);
1660
+ return {
1661
+ ok: true,
1662
+ tunnel_id: tunnelId,
1663
+ access,
1664
+ managed_tunnel: { ...managed, access },
1665
+ setup: await detectMicrosoftDevTunnelSetup(mergedOptions),
1666
+ };
1667
+ } catch (err) {
1668
+ return {
1669
+ ok: false,
1670
+ error_code: 'microsoft_tunnel_private_reset_failed',
1671
+ error: String(err?.stderr || err?.message || err || 'Could not reset Microsoft tunnel access to private.').slice(0, 800),
1672
+ setup: await detectMicrosoftDevTunnelSetup(mergedOptions).catch(() => null),
1673
+ };
1674
+ }
1675
+ }
1676
+
1677
+ async function renewPersistentTunnelExpiration(tunnelId, options = {}) {
1678
+ const checkedAt = new Date().toISOString();
1679
+ try {
1680
+ await execDevTunnel(devTunnelUpdateArgs(tunnelId, options), options);
1681
+ const renewal = {
1682
+ ok: true,
1683
+ renewed: true,
1684
+ renewed_at: checkedAt,
1685
+ expiration: String(options.tunnelExpiration || options.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION),
1686
+ next_renewal_at: isoAfter(checkedAt, DEVTUNNEL_RENEWAL_INTERVAL_MS),
1687
+ error: '',
1688
+ };
1689
+ persistManagedTunnelRenewal(tunnelId, renewal, options);
1690
+ return renewal;
1691
+ } catch (err) {
1692
+ const renewal = {
1693
+ ok: false,
1694
+ renewed: false,
1695
+ checked_at: checkedAt,
1696
+ expiration: String(options.tunnelExpiration || options.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION),
1697
+ error: String(err?.stderr || err?.message || err || 'Could not renew Microsoft tunnel expiration.').slice(0, 800),
1698
+ };
1699
+ persistManagedTunnelRenewal(tunnelId, renewal, options);
1700
+ return renewal;
1701
+ }
1702
+ }
1703
+
1704
+ function createdTunnelRenewal(options = {}) {
1705
+ const checkedAt = new Date().toISOString();
1706
+ return {
1707
+ ok: true,
1708
+ created: true,
1709
+ renewed: true,
1710
+ renewed_at: checkedAt,
1711
+ expiration: String(options.tunnelExpiration || options.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION),
1712
+ next_renewal_at: isoAfter(checkedAt, DEVTUNNEL_RENEWAL_INTERVAL_MS),
1713
+ error: '',
1714
+ };
1715
+ }
1716
+
1207
1717
  async function ensurePersistentTunnel(tunnelId, options = {}) {
1718
+ const previousState = loadManagedTunnelState(options) || {};
1719
+ let created = false;
1208
1720
  try {
1209
- await execDevTunnel(['create', tunnelId, '--expiration', '30d'], options);
1721
+ await execDevTunnel(['create', tunnelId, '--expiration', String(options.tunnelExpiration || options.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION)], options);
1722
+ created = true;
1210
1723
  } catch (err) {
1211
1724
  if (!ignoreAlreadyExists(err)) throw err;
1212
1725
  }
1726
+ const renewalDue = options.renewTunnel === true || options.renew_tunnel === true || microsoftTunnelTunnelRenewalDue(previousState, options);
1727
+ const tunnelRenewal = created ? createdTunnelRenewal(options) : (renewalDue ? await renewPersistentTunnelExpiration(tunnelId, options) : {
1728
+ ok: true,
1729
+ renewed: false,
1730
+ expiration: previousState.tunnel_expiration || String(options.tunnelExpiration || options.tunnel_expiration || DEVTUNNEL_MAX_EXPIRATION),
1731
+ renewed_at: previousState.last_tunnel_renewed_at || '',
1732
+ next_renewal_at: previousState.next_tunnel_renewal_at || '',
1733
+ error: previousState.tunnel_renewal_error || '',
1734
+ });
1213
1735
  try {
1214
1736
  await execDevTunnel(['port', 'create', tunnelId, '-p', String(options.port || 3456), '--protocol', 'http'], options);
1215
1737
  } catch (err) {
@@ -1221,8 +1743,22 @@ async function ensurePersistentTunnel(tunnelId, options = {}) {
1221
1743
  }
1222
1744
  }
1223
1745
  }
1224
- const access = await ensurePrivateConnectAccess(tunnelId, options);
1225
- return { access };
1746
+ // Fault-isolate access configuration from tunnel exposure. A transient
1747
+ // `devtunnel access` failure must NOT abort hosting the tunnel — CTM's app-layer
1748
+ // auth still gates every request, and a missing ACE at worst costs a one-time
1749
+ // sign-in, never a full outage. Surface the error in the access summary instead.
1750
+ let access;
1751
+ try {
1752
+ const accessRenewalDue = options.renewAccess === true || options.renew_access === true || microsoftTunnelAccessRenewalDue(previousState, options);
1753
+ access = await ensureMicrosoftTunnelConnectAccess(tunnelId, { ...options, renewAccess: accessRenewalDue });
1754
+ } catch (err) {
1755
+ access = managedTunnelAccessSummary({
1756
+ mode: resolveMicrosoftTunnelAccessMode(options.input || {}, options),
1757
+ checked_at: new Date().toISOString(),
1758
+ error: String(err?.stderr || err?.message || err || 'tunnel access configuration failed').slice(0, 800),
1759
+ }, options);
1760
+ }
1761
+ return { access, tunnel_renewal: tunnelRenewal };
1226
1762
  }
1227
1763
 
1228
1764
  function waitForHostReady(child, logStreams, options = {}) {
@@ -1309,6 +1845,16 @@ async function startMicrosoftDevTunnelHostAttempt(tunnelId, options = {}) {
1309
1845
  throw hostErr;
1310
1846
  }
1311
1847
  const runCommand = command('devtunnel', args).display;
1848
+ const tunnelRenewal = options.tunnelRenewal || options.tunnel_renewal || {};
1849
+ const lastTunnelRenewedAt = tunnelRenewal.renewed_at || '';
1850
+ const accessRenewedAt = options.access && (options.access.renewed_at || (options.access.created ? options.access.checked_at : '')) || '';
1851
+ const previousEvents = (() => {
1852
+ try {
1853
+ const prev = loadManagedTunnelState(options);
1854
+ return prev && Array.isArray(prev.host_events) ? prev.host_events : [];
1855
+ } catch { return []; }
1856
+ })();
1857
+ const startedAtIso = new Date().toISOString();
1312
1858
  const state = {
1313
1859
  pid: child && child.pid ? child.pid : null,
1314
1860
  tunnel_id: tunnelId,
@@ -1323,16 +1869,25 @@ async function startMicrosoftDevTunnelHostAttempt(tunnelId, options = {}) {
1323
1869
  header_passthrough: hostHeaderMode === HOST_HEADER_MODE_PASSTHROUGH,
1324
1870
  header_passthrough_fallback: !!options.headerPassthroughFallback,
1325
1871
  header_passthrough_fallback_reason: options.headerPassthroughFallbackReason || '',
1326
- started_at: new Date().toISOString(),
1872
+ started_at: startedAtIso,
1327
1873
  stopped_at: '',
1328
1874
  last_restore_failed_at: '',
1329
1875
  restore_error: '',
1876
+ host_events: [...previousEvents, { at: startedAtIso, type: 'host_started', detail: urls.origin || '' }].slice(-HOST_EVENT_LIMIT),
1330
1877
  access_mode: options.access && options.access.mode || '',
1331
1878
  access: options.access || null,
1332
1879
  last_access_check_at: options.access && options.access.checked_at || '',
1880
+ last_access_renewed_at: accessRenewedAt,
1881
+ next_access_renewal_at: options.access && options.access.next_renewal_at || '',
1882
+ access_expiration: options.access && options.access.expiration || '',
1333
1883
  access_error: options.access && options.access.error || '',
1884
+ last_tunnel_renewed_at: lastTunnelRenewedAt,
1885
+ next_tunnel_renewal_at: tunnelRenewal.next_renewal_at || '',
1886
+ tunnel_expiration: tunnelRenewal.expiration || '',
1887
+ tunnel_renewal_error: tunnelRenewal.error || '',
1334
1888
  };
1335
1889
  writeManagedTunnelState(state, options);
1890
+ watchDevTunnelHostOutput(child, options);
1336
1891
  return { configured: true, running: !!state.pid, started: true, ...state, state_path: managedStatePath(options) };
1337
1892
  }
1338
1893
 
@@ -1366,9 +1921,9 @@ async function applyMicrosoftDevTunnelSetup(input = {}, options = {}) {
1366
1921
  const previous = loadManagedTunnelState(options) || {};
1367
1922
  const previousProcess = previous.pid ? await inspectManagedTunnelProcess(previous, options) : null;
1368
1923
  if (previousProcess && managedTunnelProcessUsable(previousProcess, previous) && previous.origin) {
1369
- let access = null;
1924
+ let persistent = null;
1370
1925
  try {
1371
- access = await ensurePrivateConnectAccess(previous.tunnel_id, options);
1926
+ persistent = await ensurePersistentTunnel(previous.tunnel_id, { ...options, input });
1372
1927
  } catch (err) {
1373
1928
  return {
1374
1929
  ok: false,
@@ -1377,6 +1932,7 @@ async function applyMicrosoftDevTunnelSetup(input = {}, options = {}) {
1377
1932
  setup: detected,
1378
1933
  };
1379
1934
  }
1935
+ const access = persistent && persistent.access;
1380
1936
  const managed = await detectManagedMicrosoftDevTunnel(options);
1381
1937
  return {
1382
1938
  ok: true,
@@ -1385,7 +1941,14 @@ async function applyMicrosoftDevTunnelSetup(input = {}, options = {}) {
1385
1941
  origin: managed.origin,
1386
1942
  mobile_url: managed.mobile_url,
1387
1943
  inspect_url: managed.inspect_url,
1388
- managed_tunnel: { ...managed, access, access_mode: access.mode, last_access_check_at: access.checked_at, access_error: access.error || '' },
1944
+ managed_tunnel: {
1945
+ ...managed,
1946
+ access,
1947
+ access_mode: access.mode,
1948
+ last_access_check_at: access.checked_at,
1949
+ access_error: access.error || '',
1950
+ tunnel_renewal: persistent.tunnel_renewal,
1951
+ },
1389
1952
  traffic: getTrafficInspection(),
1390
1953
  };
1391
1954
  }
@@ -1396,8 +1959,12 @@ async function applyMicrosoftDevTunnelSetup(input = {}, options = {}) {
1396
1959
  const requestedTunnelId = normalizeTunnelId(input.tunnel_id || input.tunnelId || previous.tunnel_id || '');
1397
1960
  const tunnelId = requestedTunnelId || createTunnelId();
1398
1961
  try {
1399
- const persistent = await ensurePersistentTunnel(tunnelId, options);
1400
- const managed = await startMicrosoftDevTunnelHost(tunnelId, { ...options, access: persistent.access });
1962
+ const persistent = await ensurePersistentTunnel(tunnelId, { ...options, input });
1963
+ const managed = await startMicrosoftDevTunnelHost(tunnelId, {
1964
+ ...options,
1965
+ access: persistent.access,
1966
+ tunnelRenewal: persistent.tunnel_renewal,
1967
+ });
1401
1968
  return {
1402
1969
  ok: true,
1403
1970
  reused: false,
@@ -1428,16 +1995,31 @@ async function restoreMicrosoftDevTunnelHost(options = {}) {
1428
1995
  if (managed.running && options.forceRestart === true) {
1429
1996
  killProcessTree(state.pid, options);
1430
1997
  } else if (managedTunnelProcessUsable(managed, state)) {
1431
- let access = managed.access || null;
1432
- if (options.repairAccess === true) {
1433
- access = await ensurePrivateConnectAccess(state.tunnel_id, options);
1998
+ const due = microsoftTunnelRenewalDue(state, options);
1999
+ let persistent = null;
2000
+ if (options.repairAccess === true || options.renewTunnel === true || options.renew_tunnel === true || options.renewAccess === true || options.renew_access === true || due.tunnel || due.access) {
2001
+ persistent = await ensurePersistentTunnel(state.tunnel_id, {
2002
+ ...options,
2003
+ accessMode: state.access_mode || state.access?.mode || options.accessMode || options.access_mode,
2004
+ renewTunnel: options.renewTunnel === true || options.renew_tunnel === true || due.tunnel,
2005
+ renewAccess: options.renewAccess === true || options.renew_access === true || due.access,
2006
+ });
1434
2007
  }
1435
2008
  const latest = await detectManagedMicrosoftDevTunnel(options);
1436
2009
  return {
1437
2010
  ok: true,
1438
2011
  restored: false,
1439
2012
  already_running: true,
1440
- managed_tunnel: access ? { ...latest, access, access_mode: access.mode, last_access_check_at: access.checked_at, access_error: access.error || '' } : latest,
2013
+ managed_tunnel: persistent && persistent.access
2014
+ ? {
2015
+ ...latest,
2016
+ access: persistent.access,
2017
+ access_mode: persistent.access.mode,
2018
+ last_access_check_at: persistent.access.checked_at,
2019
+ access_error: persistent.access.error || '',
2020
+ tunnel_renewal: persistent.tunnel_renewal,
2021
+ }
2022
+ : latest,
1441
2023
  };
1442
2024
  }
1443
2025
  if (managed.running && !managedTunnelProcessUsable(managed, state)) {
@@ -1475,8 +2057,15 @@ async function restoreMicrosoftDevTunnelHost(options = {}) {
1475
2057
  }
1476
2058
 
1477
2059
  try {
1478
- const persistent = await ensurePersistentTunnel(tunnelId, options);
1479
- const restored = await startMicrosoftDevTunnelHost(tunnelId, { ...options, access: persistent.access });
2060
+ const persistent = await ensurePersistentTunnel(tunnelId, {
2061
+ ...options,
2062
+ accessMode: state.access_mode || state.access?.mode || options.accessMode || options.access_mode,
2063
+ });
2064
+ const restored = await startMicrosoftDevTunnelHost(tunnelId, {
2065
+ ...options,
2066
+ access: persistent.access,
2067
+ tunnelRenewal: persistent.tunnel_renewal,
2068
+ });
1480
2069
  return { ok: true, restored: true, managed_tunnel: restored };
1481
2070
  } catch (err) {
1482
2071
  const nextState = {
@@ -1521,11 +2110,25 @@ async function checkMicrosoftDevTunnelAvailability(options = {}) {
1521
2110
  await annotateWatchdogState({ last_watchdog_check_at: checkedAt, watchdog_error: '' }, options);
1522
2111
  if (managedTunnelProcessUsable(before, state)) {
1523
2112
  let managed = before;
1524
- if (options.repairAccess === true) {
2113
+ const due = microsoftTunnelRenewalDue(state, options);
2114
+ if (options.repairAccess === true || options.renewTunnel === true || options.renew_tunnel === true || options.renewAccess === true || options.renew_access === true || due.tunnel || due.access) {
1525
2115
  try {
1526
- const access = await ensurePrivateConnectAccess(state.tunnel_id, options);
2116
+ const persistent = await ensurePersistentTunnel(state.tunnel_id, {
2117
+ ...options,
2118
+ accessMode: state.access_mode || state.access?.mode || options.accessMode || options.access_mode,
2119
+ renewTunnel: options.renewTunnel === true || options.renew_tunnel === true || due.tunnel,
2120
+ renewAccess: options.renewAccess === true || options.renew_access === true || due.access,
2121
+ });
1527
2122
  const latest = await detectManagedMicrosoftDevTunnel(options);
1528
- managed = { ...latest, access, access_mode: access.mode, last_access_check_at: access.checked_at, access_error: access.error || '' };
2123
+ const access = persistent.access;
2124
+ managed = {
2125
+ ...latest,
2126
+ access,
2127
+ access_mode: access.mode,
2128
+ last_access_check_at: access.checked_at,
2129
+ access_error: access.error || '',
2130
+ tunnel_renewal: persistent.tunnel_renewal,
2131
+ };
1529
2132
  } catch (err) {
1530
2133
  const error = String(err?.stderr || err?.message || err || 'Microsoft tunnel access repair failed.').slice(0, 800);
1531
2134
  await annotateWatchdogState({ last_watchdog_check_at: checkedAt, watchdog_error: error, access_error: error }, options);
@@ -1557,6 +2160,20 @@ async function checkMicrosoftDevTunnelAvailability(options = {}) {
1557
2160
  };
1558
2161
  }
1559
2162
  if (probe.blocked_by_tunnel_auth) {
2163
+ if (managed.access?.mode === 'ctm_authenticated') {
2164
+ const error = probe.message || 'Microsoft Dev Tunnels is still requiring provider sign-in even though CTM-authenticated tunnel access is configured.';
2165
+ await annotateWatchdogState({ last_watchdog_check_at: checkedAt, watchdog_error: error }, options);
2166
+ return {
2167
+ ok: false,
2168
+ checked: true,
2169
+ recovered: false,
2170
+ reason: 'unexpected_tunnel_auth_gate',
2171
+ error,
2172
+ public_probe: probe,
2173
+ keep_awake: await getMicrosoftDevTunnelKeepAwakeStatus(options),
2174
+ managed_tunnel: managed,
2175
+ };
2176
+ }
1560
2177
  await annotateWatchdogState({ last_watchdog_check_at: checkedAt, watchdog_error: '' }, options);
1561
2178
  return {
1562
2179
  ok: true,
@@ -1704,6 +2321,90 @@ function loginStateIsReusable(state, provider, options = {}) {
1704
2321
  return now - started < Number(options.loginReuseMs || LOGIN_REUSE_MS);
1705
2322
  }
1706
2323
 
2324
+ async function logoutMicrosoftDevTunnelUser(options = {}) {
2325
+ const previous = loadLoginProcessState(options);
2326
+ if (previous && previous.running && pidIsRunning(previous.pid)) {
2327
+ stopLoginProcessState(previous, options);
2328
+ }
2329
+
2330
+ const execFile = options.execFile || defaultExecFile;
2331
+ const args = ['user', 'logout'];
2332
+ // Keep the provider the user last signed in with so the next sign-in (and the
2333
+ // one-click Reconnect action) defaults to the same account type.
2334
+ const previousProvider = loginStateProvider(previous || {});
2335
+ const baseState = {
2336
+ ...(previous || {}),
2337
+ running: false,
2338
+ pid: null,
2339
+ provider: previousProvider,
2340
+ provider_label: loginProviderLabel(previousProvider),
2341
+ command: command('devtunnel', args).display,
2342
+ login_url: '',
2343
+ device_code: '',
2344
+ error: '',
2345
+ error_code: '',
2346
+ signed_out_at: new Date().toISOString(),
2347
+ };
2348
+ try {
2349
+ const result = await execFile('devtunnel', args, {
2350
+ encoding: 'utf8',
2351
+ timeout: options.commandTimeoutMs || 15000,
2352
+ maxBuffer: 512 * 1024,
2353
+ });
2354
+ const stoppedTunnel = options.stopTunnel === false ? null : await stopMicrosoftDevTunnel(options).catch((err) => ({
2355
+ ok: false,
2356
+ error: String(err?.message || err || 'Could not stop existing Microsoft tunnel').slice(0, 800),
2357
+ }));
2358
+ const nextState = {
2359
+ ...baseState,
2360
+ stdout: String(result?.stdout || '').trim().slice(0, 800),
2361
+ stderr: String(result?.stderr || '').trim().slice(0, 800),
2362
+ };
2363
+ writeLoginProcessState(nextState, options);
2364
+ return {
2365
+ ok: true,
2366
+ already_signed_out: false,
2367
+ stopped_tunnel: stoppedTunnel,
2368
+ logout: { ...nextState, state_path: loginStatePath(options) },
2369
+ setup: await detectMicrosoftDevTunnelSetup(options),
2370
+ progress: getMicrosoftDevTunnelProgress(options),
2371
+ };
2372
+ } catch (err) {
2373
+ const text = String(`${err?.stdout || ''}\n${err?.stderr || ''}\n${err?.message || ''}`);
2374
+ const alreadySignedOut = /not\s+(?:logged|signed)\s+in|no\s+user|login\s+required/i.test(text);
2375
+ const nextState = {
2376
+ ...baseState,
2377
+ already_signed_out: alreadySignedOut,
2378
+ error_code: alreadySignedOut ? '' : (err?.code === 'ENOENT' ? 'devtunnel_cli_missing' : 'devtunnel_logout_failed'),
2379
+ error: alreadySignedOut ? '' : text.trim().slice(0, 800),
2380
+ };
2381
+ writeLoginProcessState(nextState, options);
2382
+ if (alreadySignedOut) {
2383
+ const stoppedTunnel = options.stopTunnel === false ? null : await stopMicrosoftDevTunnel(options).catch((stopErr) => ({
2384
+ ok: false,
2385
+ error: String(stopErr?.message || stopErr || 'Could not stop existing Microsoft tunnel').slice(0, 800),
2386
+ }));
2387
+ return {
2388
+ ok: true,
2389
+ already_signed_out: true,
2390
+ stopped_tunnel: stoppedTunnel,
2391
+ logout: { ...nextState, state_path: loginStatePath(options) },
2392
+ setup: await detectMicrosoftDevTunnelSetup(options),
2393
+ progress: getMicrosoftDevTunnelProgress(options),
2394
+ };
2395
+ }
2396
+ return {
2397
+ ok: false,
2398
+ already_signed_out: false,
2399
+ error_code: nextState.error_code,
2400
+ error: nextState.error || 'Microsoft Dev Tunnels sign-out failed.',
2401
+ logout: { ...nextState, state_path: loginStatePath(options) },
2402
+ setup: await detectMicrosoftDevTunnelSetup(options).catch(() => null),
2403
+ progress: getMicrosoftDevTunnelProgress(options),
2404
+ };
2405
+ }
2406
+ }
2407
+
1707
2408
  function startMicrosoftDevTunnelLogin(input = {}, options = {}) {
1708
2409
  const fsImpl = options.fs || fs;
1709
2410
  fsImpl.mkdirSync(setupDir(options), { recursive: true, mode: 0o700 });
@@ -1711,9 +2412,15 @@ function startMicrosoftDevTunnelLogin(input = {}, options = {}) {
1711
2412
  fsImpl.mkdirSync(logDir, { recursive: true, mode: 0o700 });
1712
2413
  const logPath = path.join(logDir, 'devtunnel-login.log');
1713
2414
  const errorLogPath = path.join(logDir, 'devtunnel-login.err');
1714
- const provider = normalizeLoginProvider(input);
2415
+ // No explicit provider (e.g. the one-click Reconnect health action) → stick to
2416
+ // the last-used provider instead of silently switching account types.
2417
+ const requestedProvider = String(input.provider || '').trim() || (input.github === true ? 'github' : '');
2418
+ const provider = requestedProvider
2419
+ ? normalizeLoginProvider({ provider: requestedProvider })
2420
+ : loginStateProvider(loadLoginProcessState(options) || {});
2421
+ const forceNew = input.force_new === true || input.forceNew === true || input.regenerate === true;
1715
2422
  const previous = loadLoginProcessState(options);
1716
- if (loginStateIsReusable(previous, provider, options)) {
2423
+ if (!forceNew && loginStateIsReusable(previous, provider, options)) {
1717
2424
  const login = processProgressFromState(previous, options);
1718
2425
  return {
1719
2426
  ok: true,
@@ -1777,6 +2484,11 @@ function startMicrosoftDevTunnelLogin(input = {}, options = {}) {
1777
2484
  update({ running: false, exit_code: code, signal: signal || '', finished_at: new Date().toISOString() });
1778
2485
  try { if (out) out.end(); } catch {}
1779
2486
  try { if (err) err.end(); } catch {}
2487
+ if (code === 0) {
2488
+ checkMicrosoftDevTunnelLoginStatus({ ...options, includeSetup: false }).catch((loginErr) => {
2489
+ update({ login_check_error: String(loginErr?.message || loginErr || 'Could not verify Dev Tunnels sign-in').slice(0, 800) });
2490
+ });
2491
+ }
1780
2492
  });
1781
2493
  return {
1782
2494
  ok: true,
@@ -1786,8 +2498,11 @@ function startMicrosoftDevTunnelLogin(input = {}, options = {}) {
1786
2498
  }
1787
2499
 
1788
2500
  module.exports = {
2501
+ appendManagedTunnelHostEvent,
1789
2502
  applyMicrosoftDevTunnelSetup,
1790
2503
  checkMicrosoftDevTunnelAvailability,
2504
+ checkMicrosoftDevTunnelLoginStatus,
2505
+ classifyDevTunnelHostEventText,
1791
2506
  clearMicrosoftDevTunnelTraffic,
1792
2507
  command,
1793
2508
  detectManagedMicrosoftDevTunnel,
@@ -1798,15 +2513,23 @@ module.exports = {
1798
2513
  getMicrosoftDevTunnelProgress,
1799
2514
  getTrafficInspection,
1800
2515
  installMicrosoftDevTunnelCli,
2516
+ inspectMicrosoftTunnelAccess,
1801
2517
  inspectKeepAwakeProcess,
1802
2518
  startMicrosoftDevTunnelInstall,
1803
2519
  inspectManagedTunnelProcess,
1804
2520
  loadKeepAwakeState,
1805
2521
  loadManagedTunnelState,
1806
2522
  loadLoginProcessState,
2523
+ loginProviderArgs,
2524
+ loginProviderLabel,
2525
+ loginStateProvider,
2526
+ logoutMicrosoftDevTunnelUser,
1807
2527
  managedTunnelDesiredRunning,
1808
2528
  microsoftDevTunnelCommands,
2529
+ microsoftTunnelPrivateAccessForced,
2530
+ normalizeMicrosoftTunnelAccessMode,
1809
2531
  normalizeTunnelId,
2532
+ resolveMicrosoftTunnelAccessMode,
1810
2533
  parseDevTunnelAccessEntries,
1811
2534
  parseDevTunnelUrls,
1812
2535
  parseDevTunnelLoginHints,
@@ -1815,6 +2538,7 @@ module.exports = {
1815
2538
  pidIsRunning,
1816
2539
  probeMicrosoftDevTunnelPublicAccess,
1817
2540
  recordMicrosoftDevTunnelTraffic,
2541
+ resetMicrosoftDevTunnelPrivateAccess,
1818
2542
  restoreMicrosoftDevTunnelHost,
1819
2543
  sanitizeRequestPath,
1820
2544
  setMicrosoftDevTunnelKeepAwake,