@turmind/halo 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -198,6 +198,10 @@ function generalSection() {
198
198
  { key: "theme", type: "enum", options: ["dark", "light", "midnight", "warm"], optionLabels: ["Dark", "Light", "Midnight", "Warm"], globalOnly: true, description: "Admin UI color theme.", description_zh: "Admin \u754C\u9762\u914D\u8272\u4E3B\u9898\u3002", default: "dark" },
199
199
  // agent scaffold
200
200
  { key: "agent.default_provider", type: "enum", options: providerIds, description: "Provider used when scaffolding a new agent. Model id, endpoint, prompt-caching TTL, and thinking defaults are read from that provider's YAML.", description_zh: "\u65B0\u5EFA agent \u65F6\u4F7F\u7528\u7684\u4F9B\u5E94\u5546\u3002\u6A21\u578B id\u3001endpoint\u3001\u63D0\u793A\u7F13\u5B58 TTL\u3001Thinking \u9ED8\u8BA4\u503C\u90FD\u4ECE\u8BE5\u4F9B\u5E94\u5546\u7684 YAML \u8BFB\u53D6\u3002", default: defaultProvider2 },
201
+ { key: "agent.max_retries", type: "int", description: "Max attempts per model call on transient errors (rate limit, 5xx, network). Backoff grows between attempts.", description_zh: "\u5355\u6B21\u6A21\u578B\u8C03\u7528\u9047\u5230\u77AC\u6001\u9519\u8BEF\uFF08\u9650\u6D41\u30015xx\u3001\u7F51\u7EDC\uFF09\u65F6\u7684\u6700\u5927\u5C1D\u8BD5\u6B21\u6570\uFF0C\u91CD\u8BD5\u95F4\u9694\u9012\u589E\u3002", default: "5" },
202
+ // server — network-facing knobs; global-only since they alter how the
203
+ // server itself authenticates clients.
204
+ { key: "server.trust_proxy", type: "boolean", globalOnly: true, description: "Trust the x-forwarded-for header for client IP resolution (brute-force rate limiting). Enable ONLY when a reverse proxy you control sits in front and rewrites the header \u2014 otherwise clients can forge it to bypass lockouts.", description_zh: "\u4FE1\u4EFB x-forwarded-for \u8BF7\u6C42\u5934\u89E3\u6790\u5BA2\u6237\u7AEF IP\uFF08\u66B4\u529B\u7834\u89E3\u9650\u901F\u7528\uFF09\u3002\u4EC5\u5F53\u524D\u9762\u6709\u4F60\u63A7\u5236\u7684\u53CD\u5411\u4EE3\u7406\u5E76\u4F1A\u91CD\u5199\u8BE5\u5934\u65F6\u624D\u5F00\u542F\u2014\u2014\u5426\u5219\u5BA2\u6237\u7AEF\u53EF\u4F2A\u9020\u7ED5\u8FC7\u9501\u5B9A\u3002", default: "false" },
201
205
  // session
202
206
  { key: "session.max_queue_size", type: "int", description: "Maximum queued messages per session", description_zh: "\u6BCF\u4E2A\u4F1A\u8BDD\u6700\u5927\u6392\u961F\u6D88\u606F\u6570", default: "256" },
203
207
  { key: "session.max_nesting_depth", type: "int", description: "Maximum session nesting depth for agent delegation", description_zh: "Agent \u59D4\u6D3E\u7684\u6700\u5927\u4F1A\u8BDD\u5D4C\u5957\u6DF1\u5EA6", default: "16" },
@@ -207,22 +211,24 @@ function generalSection() {
207
211
  { key: "compact.max_summary_input", type: "int", description: "Max chars fed into local truncation fallback", description_zh: "\u672C\u5730\u622A\u65AD\u515C\u5E95\u65F6\u7684\u603B\u8F93\u5165\u5B57\u7B26\u4E0A\u9650", default: "15000" },
208
212
  { key: "compact.max_message_slice", type: "int", description: "Max chars kept per old message during local truncation", description_zh: "\u672C\u5730\u622A\u65AD\u515C\u5E95\u65F6\u6BCF\u6761\u65E7\u6D88\u606F\u4FDD\u7559\u7684\u6700\u5927\u5B57\u7B26\u6570", default: "800" },
209
213
  { key: "compact.summarize_timeout_sec", type: "int", description: "LLM summarization timeout (seconds)", description_zh: "LLM \u6458\u8981\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09", default: "300" },
210
- // sandbox (Linux bwrap only)
211
- { key: "sandbox.hidden_dirs", description: "Comma-separated dirs hidden by bwrap (Linux only)", description_zh: "\u901A\u8FC7 bwrap tmpfs \u9690\u85CF\u7684\u76EE\u5F55\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u4EC5 Linux\uFF09", default: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker" },
212
- { key: "sandbox.writable_dirs", description: "Comma-separated dirs bind-mounted read-write inside the bwrap sandbox (Linux only) \u2014 for external CLIs that keep local state, e.g. ~/.kiro,~/.local/share/kiro-cli. Not applied to readonly sessions.", description_zh: "\u5728 bwrap \u6C99\u7BB1\u5185\u4EE5\u53EF\u5199\u65B9\u5F0F\u6302\u8F7D\u7684\u76EE\u5F55\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u4EC5 Linux\uFF09\u2014\u2014\u7ED9\u9700\u8981\u672C\u5730\u72B6\u6001\u7684\u5916\u90E8 CLI \u7528\uFF0C\u5982 ~/.kiro,~/.local/share/kiro-cli\u3002readonly \u4F1A\u8BDD\u4E0D\u751F\u6548\u3002", default: "" },
213
- { key: "sandbox.hidden_files", description: "Comma-separated files hidden by bwrap (Linux only)", description_zh: "\u901A\u8FC7 /dev/null bind \u9690\u85CF\u7684\u6587\u4EF6\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u4EC5 Linux\uFF09", default: "~/.npmrc,~/.bash_history,~/.gitconfig" },
214
+ // sandbox (Linux bwrap only) — global-only: these define the security
215
+ // boundary agents run inside; a workspace overriding them could lift
216
+ // its own sandbox constraints.
217
+ { key: "sandbox.hidden_dirs", globalOnly: true, description: "Comma-separated dirs hidden by bwrap (Linux only)", description_zh: "\u901A\u8FC7 bwrap tmpfs \u9690\u85CF\u7684\u76EE\u5F55\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u4EC5 Linux\uFF09", default: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs" },
218
+ { key: "sandbox.writable_dirs", globalOnly: true, description: "Comma-separated dirs bind-mounted read-write inside the bwrap sandbox (Linux only) \u2014 for external CLIs that keep local state, e.g. ~/.kiro,~/.local/share/kiro-cli. Not applied to readonly sessions.", description_zh: "\u5728 bwrap \u6C99\u7BB1\u5185\u4EE5\u53EF\u5199\u65B9\u5F0F\u6302\u8F7D\u7684\u76EE\u5F55\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u4EC5 Linux\uFF09\u2014\u2014\u7ED9\u9700\u8981\u672C\u5730\u72B6\u6001\u7684\u5916\u90E8 CLI \u7528\uFF0C\u5982 ~/.kiro,~/.local/share/kiro-cli\u3002readonly \u4F1A\u8BDD\u4E0D\u751F\u6548\u3002", default: "" },
219
+ { key: "sandbox.hidden_files", globalOnly: true, description: "Comma-separated files hidden by bwrap (Linux only)", description_zh: "\u901A\u8FC7 /dev/null bind \u9690\u85CF\u7684\u6587\u4EF6\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u4EC5 Linux\uFF09", default: "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/evo.db,~/.halo/global/evo.db-wal,~/.halo/global/evo.db-shm,~/.halo/global/cron.db,~/.halo/global/cron.db-wal,~/.halo/global/cron.db-shm" },
214
220
  // logging
215
221
  { key: "logging.level", type: "enum", options: ["debug", "info", "warn", "error"], description: "Log level", description_zh: "\u65E5\u5FD7\u7EA7\u522B", default: "warn" },
216
222
  // self-evolution (see plans/self-evolution.md). All evo settings are
217
223
  // global-only — the worker / ticker live in the server process and
218
224
  // would have to reload mid-flight if a workspace could override them.
219
- { key: "evolution.level", type: "enum", options: ["L0", "L1"], globalOnly: true, description: "Self-evolution capability ladder. L0: manual only \u2014 drafts via /note, manual approve/reject. L1: L0 plus automatic drafting on pre-compact. (Queued runs always execute regardless of level; the level only gates how they get enqueued.)", description_zh: "\u81EA\u6211\u8FDB\u5316\u7B49\u7EA7\u3002L0\uFF1A\u4EC5\u624B\u52A8\u2014\u2014\u901A\u8FC7 /note \u8D77\u8349\uFF0C\u624B\u52A8 approve/reject\u3002L1\uFF1A\u5728 L0 \u57FA\u7840\u4E0A\uFF0C\u538B\u7F29\u524D\uFF08pre-compact\uFF09\u81EA\u52A8\u8D77\u8349\u3002\uFF08\u5DF2\u6392\u961F\u7684 run \u59CB\u7EC8\u6267\u884C\uFF0Clevel \u53EA\u51B3\u5B9A\u5982\u4F55\u5165\u961F\u3002\uFF09", default: "L0" },
225
+ { key: "evolution.level", type: "enum", options: ["L0", "L1"], globalOnly: true, description: "Self-evolution capability ladder. L0: manual only \u2014 drafts via /evo, manual approve/reject. L1: L0 plus automatic drafting on pre-compact. (Queued runs always execute regardless of level; the level only gates how they get enqueued.)", description_zh: "\u81EA\u6211\u8FDB\u5316\u7B49\u7EA7\u3002L0\uFF1A\u4EC5\u624B\u52A8\u2014\u2014\u901A\u8FC7 /evo \u8D77\u8349\uFF0C\u624B\u52A8 approve/reject\u3002L1\uFF1A\u5728 L0 \u57FA\u7840\u4E0A\uFF0C\u538B\u7F29\u524D\uFF08pre-compact\uFF09\u81EA\u52A8\u8D77\u8349\u3002\uFF08\u5DF2\u6392\u961F\u7684 run \u59CB\u7EC8\u6267\u884C\uFF0Clevel \u53EA\u51B3\u5B9A\u5982\u4F55\u5165\u961F\u3002\uFF09", default: "L0" },
220
226
  { key: "evolution.max_concurrent_run", type: "int", globalOnly: true, description: "Max concurrent evo run wrappers (drafting patches).", description_zh: "\u540C\u65F6\u8FD0\u884C\u7684 evo run wrapper \u4E0A\u9650\uFF08\u8D77\u8349 patch\uFF09\u3002", default: "1" },
221
227
  { key: "evolution.max_concurrent_apply", type: "int", globalOnly: true, description: "Max concurrent evo apply wrappers (merging patches).", description_zh: "\u540C\u65F6\u8FD0\u884C\u7684 evo apply wrapper \u4E0A\u9650\uFF08\u5408\u5E76 patch\uFF09\u3002", default: "1" },
222
- { key: "evolution.run_timeout_minutes", type: "int", globalOnly: true, description: "Heartbeat timeout for run wrappers; older than this with status=running is marked timeout. Must exceed the wrapper per-phase cap (10min) so a healthy slow phase is never wrongly requeued.", description_zh: "run wrapper \u5FC3\u8DF3\u8D85\u65F6\uFF08\u5206\u949F\uFF09\uFF1B\u8D85\u8FC7\u4ECD\u6807 running \u7684\u8BB0\u5F55\u4F1A\u88AB\u6807 timeout\u3002\u987B\u5927\u4E8E wrapper \u5355\u9636\u6BB5\u4E0A\u9650\uFF0810 \u5206\u949F\uFF09\uFF0C\u5426\u5219\u6B63\u5E38\u7684\u6162\u9636\u6BB5\u4F1A\u88AB\u8BEF\u5224\u91CD\u6392\u3002", default: "12" },
228
+ { key: "evolution.run_timeout_minutes", type: "int", globalOnly: true, description: "Heartbeat timeout for run wrappers. The wrapper writes a heartbeat every 60s for as long as it lives, so exceeding this means the wrapper process died \u2014 the row is requeued (or marked timeout past max_attempts). This is dead-wrapper detection latency, NOT a budget for slow models; the per-phase model budget is the wrapper-internal 30min cap.", description_zh: "run wrapper \u5FC3\u8DF3\u8D85\u65F6\uFF08\u5206\u949F\uFF09\u3002wrapper \u5B58\u6D3B\u671F\u95F4\u6BCF 60 \u79D2\u5199\u4E00\u6B21\u5FC3\u8DF3\uFF0C\u8D85\u65F6\u5373\u89C6\u4E3A wrapper \u8FDB\u7A0B\u5DF2\u6B7B\uFF0C\u4EFB\u52A1\u91CD\u6392\uFF08\u8D85\u8FC7 max_attempts \u5219\u6807 timeout\uFF09\u3002\u8FD9\u662F\u6B7B wrapper \u7684\u68C0\u6D4B\u5EF6\u8FDF\uFF0C\u4E0D\u662F\u6162\u6A21\u578B\u7684\u8FD0\u884C\u9884\u7B97\u2014\u2014\u5355\u9636\u6BB5\u6A21\u578B\u9884\u7B97\u662F wrapper \u5185\u90E8\u7684 30 \u5206\u949F\u4E0A\u9650\u3002", default: "12" },
223
229
  { key: "evolution.apply_timeout_minutes", type: "int", globalOnly: true, description: "Heartbeat timeout for apply wrappers.", description_zh: "apply wrapper \u5FC3\u8DF3\u8D85\u65F6\uFF08\u5206\u949F\uFF09\u3002", default: "5" },
224
230
  { key: "evolution.max_attempts", type: "int", globalOnly: true, description: "How many times the ticker may try to spawn a wrapper for a single row before giving up. Stops infinite retry loops on permanently broken tasks.", description_zh: "ticker \u4E3A\u5355\u6761\u4EFB\u52A1\u5C1D\u8BD5\u62C9\u8D77 wrapper \u7684\u6700\u5927\u6B21\u6570\uFF0C\u8D85\u8FC7\u540E\u653E\u5F03\u3002\u907F\u514D\u574F\u4EFB\u52A1\u65E0\u9650\u91CD\u8BD5\u3002", default: "3" },
225
- { key: "evolution.triggers.pre_compact", type: "boolean", globalOnly: true, description: "Snapshot the session right before compaction so evo can analyze it. /note is always available regardless of this setting.", description_zh: "\u5728 compact \u524D\u5FEB\u7167\u4F1A\u8BDD\u4F9B evo \u5206\u6790\u3002/note \u603B\u662F\u53EF\u7528\uFF0C\u4E0E\u6B64\u5F00\u5173\u65E0\u5173\u3002", default: "true" },
231
+ { key: "evolution.triggers.pre_compact", type: "boolean", globalOnly: true, description: "Snapshot the session right before compaction so evo can analyze it. /evo is always available regardless of this setting.", description_zh: "\u5728 compact \u524D\u5FEB\u7167\u4F1A\u8BDD\u4F9B evo \u5206\u6790\u3002/evo \u603B\u662F\u53EF\u7528\uFF0C\u4E0E\u6B64\u5F00\u5173\u65E0\u5173\u3002", default: "true" },
226
232
  // limits — runtime caps that protect against runaway tool output / UI
227
233
  // freezes. All read live from settings.yaml so changes take effect on
228
234
  // the next tool call without a server restart.
@@ -231,6 +237,7 @@ function generalSection() {
231
237
  { key: "limits.grep_default_matches", type: "int", globalOnly: true, description: "Default match cap for the grep tool when the call site does not pass an explicit `max`.", description_zh: "grep \u5DE5\u5177\u672A\u663E\u5F0F\u4F20 `max` \u53C2\u6570\u65F6\u7684\u9ED8\u8BA4\u5339\u914D\u4E0A\u9650\u3002", default: "50" },
232
238
  { key: "limits.tool_result_render_chars", type: "int", globalOnly: true, description: 'Per-tool-result cap on the content fed back to the LLM. Larger results are truncated (with a "[Content truncated]" marker telling the model to re-run with narrower scope) to protect the context window and prompt cache. The UI/admin chat panel shows a far larger slice \u2014 see tool_result_ui_chars.', description_zh: "\u6BCF\u6761 tool result \u56DE\u4F20\u7ED9 LLM \u7684\u5185\u5BB9\u957F\u5EA6\u4E0A\u9650\u3002\u8D85\u51FA\u90E8\u5206\u4F1A\u88AB\u622A\u65AD\uFF08\u5E76\u9644 [Content truncated] \u6807\u8BB0\u63D0\u793A\u6A21\u578B\u7F29\u5C0F\u8303\u56F4\u91CD\u67E5\uFF09\uFF0C\u4EE5\u4FDD\u62A4\u4E0A\u4E0B\u6587\u7A97\u53E3\u548C\u63D0\u793A\u8BCD\u7F13\u5B58\u3002UI/admin \u804A\u5929\u9762\u677F\u5C55\u793A\u7684\u5185\u5BB9\u4E0A\u9650\u8981\u5927\u5F97\u591A\uFF0C\u89C1 tool_result_ui_chars\u3002", default: "8000" },
233
239
  { key: "limits.tool_result_ui_chars", type: "int", globalOnly: true, description: "Per-tool-result cap on the content stored for UI display (admin/web chat panel). Far larger than the LLM cap so the full output of a normal command stays visible, but bounded so a multi-MB `cat` does not bloat the session file, the WS payload, or the browser render. Output past this is truncated with a marker pointing at file_read for the complete content.", description_zh: "\u6BCF\u6761 tool result \u5B58\u7ED9 UI \u5C55\u793A\uFF08admin/web \u804A\u5929\u9762\u677F\uFF09\u7684\u5185\u5BB9\u957F\u5EA6\u4E0A\u9650\u3002\u8FDC\u5927\u4E8E LLM \u4E0A\u9650\uFF0C\u4FDD\u8BC1\u666E\u901A\u547D\u4EE4\u7684\u5B8C\u6574\u8F93\u51FA\u90FD\u80FD\u770B\u5230\uFF1B\u4F46\u4ECD\u6709\u754C\uFF0C\u907F\u514D cat \u4E2A\u51E0 MB \u7684\u6587\u4EF6\u6491\u7206\u4F1A\u8BDD\u6587\u4EF6\u3001WS \u4F20\u8F93\u548C\u6D4F\u89C8\u5668\u6E32\u67D3\u3002\u8D85\u51FA\u90E8\u5206\u622A\u65AD\u5E76\u9644\u6807\u8BB0\uFF0C\u63D0\u793A\u7528 file_read \u770B\u5B8C\u6574\u5185\u5BB9\u3002", default: String(64 * 1024) },
240
+ { key: "limits.auto_report_chars", type: "int", globalOnly: true, description: "Cap on the auto-report a finished sub-agent delivers to its parent. Longer reports are truncated with a pointer to get_session_output for the full text.", description_zh: "\u5B50\u4F1A\u8BDD\u5B8C\u6210\u540E\u81EA\u52A8\u4E0A\u62A5\u7ED9\u7236\u4F1A\u8BDD\u7684\u5185\u5BB9\u957F\u5EA6\u4E0A\u9650\u3002\u8D85\u51FA\u622A\u65AD\uFF0C\u5E76\u63D0\u793A\u7528 get_session_output \u53D6\u5168\u6587\u3002", default: "8192" },
234
241
  { key: "limits.ws_event_buffer", type: "int", globalOnly: true, description: "Events buffered per detached WebSocket session before the oldest are dropped on reattach.", description_zh: "WebSocket \u4F1A\u8BDD\u5728\u65AD\u5F00\u671F\u95F4\u6700\u591A\u7F13\u5B58\u591A\u5C11\u4E8B\u4EF6\uFF0C\u8D85\u51FA\u540E\u4ECE\u6700\u65E9\u7684\u5F00\u59CB\u4E22\u3002", default: "5000" },
235
242
  { key: "limits.terminal_scrollback_bytes", type: "int", globalOnly: true, description: "Off-screen scrollback bytes retained per persistent terminal while detached.", description_zh: "\u7EC8\u7AEF\u65AD\u5F00\u671F\u95F4\u4FDD\u7559\u7684\u56DE\u6EDA\u7F13\u51B2\u5B57\u8282\u6570\u3002", default: "50000" }
236
243
  ].map((f) => ({ ...f, kind: "param" }))
@@ -716,7 +723,21 @@ var init_config = __esm({
716
723
  // Random base64 JWT signing key, set by `halo setup`. Empty = not set up.
717
724
  jwtSecret: systemString("HALO_JWT_SECRET", "server.jwt_secret", null),
718
725
  // Empty allowlist means "reflect any origin" (see CORS setup in index.ts).
719
- corsOrigins: systemStringArray("HALO_CORS_ORIGINS", "server.cors_origins", [])
726
+ corsOrigins: systemStringArray("HALO_CORS_ORIGINS", "server.cors_origins", []),
727
+ /** Trust `x-forwarded-for` for client-IP resolution (brute-force
728
+ * buckets). Off by default — the header is client-forgeable unless a
729
+ * reverse proxy the operator controls rewrites it. */
730
+ get trustProxy() {
731
+ return settingsBool("general.server.trust_proxy", false);
732
+ },
733
+ /** `HALO_RUNTIME_MODE=agentcore` runs the server as an Amazon Bedrock
734
+ * AgentCore Runtime container: auth is terminated upstream by AgentCore,
735
+ * so password/JWT checks, channels, cron/evolution and the instance lock
736
+ * are skipped, and the /ping + /invocations + WS /ws adapter is mounted
737
+ * (see routes/agentcore.ts). Unset / any other value → normal server. */
738
+ runtimeMode: process.env.HALO_RUNTIME_MODE ?? null,
739
+ /** Workspace for agentcore mode. Env `HALO_WORKSPACE`, fallback cwd. */
740
+ agentcoreWorkspace: process.env.HALO_WORKSPACE ?? process.cwd()
720
741
  },
721
742
  // `model.compressAt` is intentionally a getter — it reads from
722
743
  // settings.yaml (`general.compact.compress_at`) on every access so a
@@ -820,8 +841,8 @@ var init_config = __esm({
820
841
  maxNestingDepth: settingsInt("general.session.max_nesting_depth", 16)
821
842
  },
822
843
  sandbox: {
823
- hiddenDirs: settingsStr("general.sandbox.hidden_dirs", "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker").split(",").map((s) => s.trim()).filter(Boolean),
824
- hiddenFiles: settingsStr("general.sandbox.hidden_files", "~/.npmrc,~/.bash_history,~/.gitconfig").split(",").map((s) => s.trim()).filter(Boolean),
844
+ hiddenDirs: settingsStr("general.sandbox.hidden_dirs", "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs").split(",").map((s) => s.trim()).filter(Boolean),
845
+ hiddenFiles: settingsStr("general.sandbox.hidden_files", "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/evo.db,~/.halo/global/evo.db-wal,~/.halo/global/evo.db-shm,~/.halo/global/cron.db,~/.halo/global/cron.db-wal,~/.halo/global/cron.db-shm").split(",").map((s) => s.trim()).filter(Boolean),
825
846
  writableDirs: settingsStr("general.sandbox.writable_dirs", "").split(",").map((s) => s.trim()).filter(Boolean)
826
847
  },
827
848
  logging: {
@@ -870,8 +891,8 @@ var init_config = __esm({
870
891
  }
871
892
  }
872
893
  };
873
- HIDDEN_DIRS_DEFAULT = "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker";
874
- HIDDEN_FILES_DEFAULT = "~/.npmrc,~/.bash_history,~/.gitconfig";
894
+ HIDDEN_DIRS_DEFAULT = "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs";
895
+ HIDDEN_FILES_DEFAULT = "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/evo.db,~/.halo/global/evo.db-wal,~/.halo/global/evo.db-shm,~/.halo/global/cron.db,~/.halo/global/cron.db-wal,~/.halo/global/cron.db-shm";
875
896
  }
876
897
  });
877
898
 
@@ -1848,11 +1869,11 @@ function copyTemplate(srcAbs, dstAbs) {
1848
1869
  const content = fs6.readFileSync(srcAbs, "utf-8");
1849
1870
  fs6.writeFileSync(dstAbs, content, "utf-8");
1850
1871
  }
1851
- function writeIfMissing(filePath, content) {
1872
+ function writeIfMissing(filePath, content, mode) {
1852
1873
  if (fs6.existsSync(filePath))
1853
1874
  return;
1854
1875
  fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
1855
- fs6.writeFileSync(filePath, content, "utf-8");
1876
+ fs6.writeFileSync(filePath, content, mode === void 0 ? { encoding: "utf-8" } : { encoding: "utf-8", mode });
1856
1877
  console.log(`[Init] Created ${filePath}`);
1857
1878
  }
1858
1879
  function mergeAgentYaml(srcAbs, dstAbs) {
@@ -1969,7 +1990,7 @@ function forceCopyDir(srcDir, dstDir) {
1969
1990
  function migrateSecrets(haloHome2) {
1970
1991
  const globalDir2 = path6.join(haloHome2, "global");
1971
1992
  const secretsDir = path6.join(haloHome2, "secrets");
1972
- fs6.mkdirSync(secretsDir, { recursive: true });
1993
+ fs6.mkdirSync(secretsDir, { recursive: true, mode: 448 });
1973
1994
  const fileMoves = [
1974
1995
  [path6.join(globalDir2, "settings.yaml"), path6.join(secretsDir, "settings.yaml")],
1975
1996
  [path6.join(globalDir2, "config.yaml"), path6.join(secretsDir, "config.yaml")]
@@ -2045,7 +2066,11 @@ function ensureHaloHome(haloHome2) {
2045
2066
  const globalDir2 = path6.join(haloHome2, "global");
2046
2067
  const secretsDir = path6.join(haloHome2, "secrets");
2047
2068
  fs6.mkdirSync(globalDir2, { recursive: true });
2048
- fs6.mkdirSync(secretsDir, { recursive: true });
2069
+ fs6.mkdirSync(secretsDir, { recursive: true, mode: 448 });
2070
+ try {
2071
+ fs6.chmodSync(secretsDir, 448);
2072
+ } catch {
2073
+ }
2049
2074
  migrateSecrets(haloHome2);
2050
2075
  const versionPath = path6.join(globalDir2, VERSION_FILE);
2051
2076
  let existingVersion = 0;
@@ -2098,7 +2123,7 @@ function ensureHaloHome(haloHome2) {
2098
2123
  }
2099
2124
  }
2100
2125
  mergeConfigYaml(path6.join(TEMPLATES_DIR, "config.yaml"), path6.join(secretsDir, "config.yaml"));
2101
- writeIfMissing(path6.join(secretsDir, "settings.yaml"), "");
2126
+ writeIfMissing(path6.join(secretsDir, "settings.yaml"), "", 384);
2102
2127
  const aliasesSrc = path6.join(TEMPLATES_DIR, "aliases.yaml");
2103
2128
  if (fs6.existsSync(aliasesSrc)) {
2104
2129
  writeIfMissing(path6.join(globalDir2, "aliases.yaml"), fs6.readFileSync(aliasesSrc, "utf-8"));
@@ -2511,7 +2536,8 @@ function buildSessionTools(sm, sessionId) {
2511
2536
  agent_id: { type: "string", description: "The agent ID \u2014 must be one of the agents listed in your system prompt. Do not invent ids." },
2512
2537
  message: { type: "string", description: "Task description / initial message for the agent" },
2513
2538
  system_prompt_context: { type: "string", description: "Optional background material prepended to the opening message BEFORE your task text \u2014 despite the name, it is NOT injected into the system prompt. Put here what frames the whole task: relevant file paths, prior findings, user-stated constraints, role framing. Keep one-shot task instructions in `message` itself so it stays a clean task statement. Note the cost: this text lives in the child's conversation history and is re-sent on every one of its model calls until compaction \u2014 include what the child genuinely needs throughout, not bulk material it could read from files instead." },
2514
- working_dir: { type: "string", description: "Optional focus directory for the sub-agent (workspace-relative or absolute; must be inside the workspace). The platform bakes the directory-scoped INSTRUCTIONS.md found along the path from the workspace root down to this directory into the sub-agent's system prompt (present every turn), and tags its prompt with this focus. Does NOT change where tools run (shell/file tools still operate from the project root). Omit for project-root scope." }
2539
+ working_dir: { type: "string", description: "Optional focus directory for the sub-agent (workspace-relative or absolute; must be inside the workspace). The platform bakes the directory-scoped INSTRUCTIONS.md found along the path from the workspace root down to this directory into the sub-agent's system prompt (present every turn), and tags its prompt with this focus. Does NOT change where tools run (shell/file tools still operate from the project root). Omit for project-root scope." },
2540
+ title: { type: "string", description: "Optional session title shown in the admin sidebar. When omitted, auto-generated from the task message." }
2515
2541
  },
2516
2542
  required: ["agent_id", "message"]
2517
2543
  },
@@ -2548,7 +2574,7 @@ function buildSessionTools(sm, sessionId) {
2548
2574
  const displayName = agentYaml.name ?? params.agent_id;
2549
2575
  const parentSession = sm.sessions.get(sessionId);
2550
2576
  const inheritedAccessLevel = parentSession?.accessLevel ?? null;
2551
- const childSessionId = await sm.createSession(params.agent_id, sessionId, params.message.slice(0, 200), displayName, void 0, resolvedWorkingDir, inheritedAccessLevel);
2577
+ const childSessionId = await sm.createSession(params.agent_id, sessionId, params.message.slice(0, 200), displayName, void 0, resolvedWorkingDir, inheritedAccessLevel, params.title);
2552
2578
  let initialMessage = `[Session ${childSessionId}]
2553
2579
 
2554
2580
  `;
@@ -6330,11 +6356,23 @@ function assertPathAllowed(filePath, opts, write = false) {
6330
6356
  }
6331
6357
  return resolved;
6332
6358
  }
6333
- if (!write && (resolved === GLOBAL_DIR || resolved.startsWith(GLOBAL_DIR + "/"))) {
6359
+ if (!write && (resolved === GLOBAL_DIR || resolved.startsWith(GLOBAL_DIR + "/")) && !isHiddenGlobalPath(resolved)) {
6334
6360
  return resolved;
6335
6361
  }
6336
6362
  throw new Error(`Access denied: "${filePath}" is outside the allowed sandbox paths`);
6337
6363
  }
6364
+ function isHiddenGlobalPath(resolved) {
6365
+ for (const raw of _hiddenFiles) {
6366
+ if (resolved === expandTilde(raw))
6367
+ return true;
6368
+ }
6369
+ for (const raw of _hiddenDirs) {
6370
+ const dir = expandTilde(raw);
6371
+ if (resolved === dir || resolved.startsWith(dir + "/"))
6372
+ return true;
6373
+ }
6374
+ return false;
6375
+ }
6338
6376
  function buildExecScriptArgs(workspaceRoot, command) {
6339
6377
  return ["bash", "-c", `cd "$1" && shift && ${command}`, "bash", workspaceRoot];
6340
6378
  }
@@ -6554,12 +6592,32 @@ var init_sandbox = __esm({
6554
6592
  "~/.aws",
6555
6593
  "~/.ssh",
6556
6594
  "~/.gnupg",
6557
- "~/.docker"
6595
+ "~/.docker",
6596
+ "~/.config/gh",
6597
+ // Cross-workspace state under ~/.halo/global — internal-agent session
6598
+ // transcripts and server/cron logs would leak other workspaces' activity
6599
+ // to a workspace/readonly session (global/ is readable by design for
6600
+ // skills/agents/prompts, so the sensitive subtrees are hidden explicitly).
6601
+ "~/.halo/global/internal-sessions",
6602
+ "~/.halo/global/logs"
6558
6603
  ];
6559
6604
  DEFAULT_HIDDEN_FILES = [
6560
6605
  "~/.npmrc",
6561
6606
  "~/.bash_history",
6562
- "~/.gitconfig"
6607
+ "~/.gitconfig",
6608
+ // Halo's own git-credential store (git-credentials.ts writes tokens here
6609
+ // in plaintext) — must be hidden like ~/.aws & co.
6610
+ "~/.git-credentials",
6611
+ "~/.netrc",
6612
+ // Global evolution / cron databases (plus sqlite WAL/SHM sidecars) carry
6613
+ // cross-workspace prompts, run history and channel targets — hidden from
6614
+ // workspace/readonly sessions like the dirs above.
6615
+ "~/.halo/global/evo.db",
6616
+ "~/.halo/global/evo.db-wal",
6617
+ "~/.halo/global/evo.db-shm",
6618
+ "~/.halo/global/cron.db",
6619
+ "~/.halo/global/cron.db-wal",
6620
+ "~/.halo/global/cron.db-shm"
6563
6621
  ];
6564
6622
  _hiddenDirs = DEFAULT_HIDDEN_DIRS;
6565
6623
  _hiddenFiles = DEFAULT_HIDDEN_FILES;
@@ -6762,6 +6820,13 @@ async function substituteSecrets(cmd, workspaceRoot, allowedNamespaces) {
6762
6820
  return { cmd, secrets: [] };
6763
6821
  const secrets = [];
6764
6822
  const settings = await loadMergedSettings(workspaceRoot);
6823
+ const declaredSecretParams = /* @__PURE__ */ new Set();
6824
+ for (const section of loadSettingsSchema(workspaceRoot)) {
6825
+ for (const f of section.fields) {
6826
+ if (f.kind === "param" && f.secret === true)
6827
+ declaredSecretParams.add(`${section.namespaceId}.${f.key}`);
6828
+ }
6829
+ }
6765
6830
  const result = cmd.replace(PARAMS_PATTERN, (_match, namespace, dotted) => {
6766
6831
  if (allowedNamespaces && !allowedNamespaces.has(namespace)) {
6767
6832
  console.log(`[workspace-tools] {{${namespace}.params.${dotted}}} rejected \u2014 namespace not in allowed list`);
@@ -6795,7 +6860,7 @@ async function substituteSecrets(cmd, workspaceRoot, allowedNamespaces) {
6795
6860
  envInjected = true;
6796
6861
  return val;
6797
6862
  });
6798
- if (envInjected && expanded !== "")
6863
+ if ((envInjected || declaredSecretParams.has(`${namespace}.${dotted}`)) && expanded !== "")
6799
6864
  secrets.push(expanded);
6800
6865
  return expanded;
6801
6866
  });
@@ -7344,6 +7409,7 @@ var init_workspace_tools = __esm({
7344
7409
  init_config();
7345
7410
  init_sandbox();
7346
7411
  init_md_vars();
7412
+ init_settings_schema();
7347
7413
  init_media_store();
7348
7414
  init_agent_loop();
7349
7415
  SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist", ".halo"]);
@@ -8810,7 +8876,7 @@ var init_session_manager = __esm({
8810
8876
  * Create a new session. Inserts metadata into SQLite, builds agent, stores in-memory.
8811
8877
  * @param explicitId - Optional explicit session ID (for unifying frontend sessionId with agent sessionId)
8812
8878
  */
8813
- async createSession(agentId, parentId, description, agentName, explicitId, workingDir, accessLevel = null) {
8879
+ async createSession(agentId, parentId, description, agentName, explicitId, workingDir, accessLevel = null, title) {
8814
8880
  const segment = generateSessionId();
8815
8881
  const sessionId = explicitId ?? (parentId ? `${parentId}>${segment}` : segment);
8816
8882
  const now = Date.now();
@@ -8827,6 +8893,15 @@ var init_session_manager = __esm({
8827
8893
  createdAt: now,
8828
8894
  updatedAt: now
8829
8895
  }).run();
8896
+ if (title) {
8897
+ try {
8898
+ const dir = this.sessionDir(agentId);
8899
+ fsSync6.mkdirSync(dir, { recursive: true });
8900
+ atomicWriteSessionFile(path20.join(dir, `${fileSegment(sessionId)}.json`), JSON.stringify({ id: sessionId, agentId, title, createdAt: new Date(now).toISOString() }, null, 2));
8901
+ } catch (err) {
8902
+ console.warn(`[SessionManager] Failed to pre-seed title for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
8903
+ }
8904
+ }
8830
8905
  const createdImageOverride = createdYaml?.model?.image;
8831
8906
  const session = this.createAgentSession(sessionId, parentId, agentId, resolvedAgentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort, workingDir ?? null, accessLevel, meta, createdImageOverride, draftReset);
8832
8907
  this.sessions.set(sessionId, session);
@@ -10415,22 +10490,22 @@ function expandAlias(command, arg) {
10415
10490
  }
10416
10491
  return { command: cmd, arg: a };
10417
10492
  }
10418
- function findActiveSessionId(sm, userId, sessionPrefix2, activeOverrides, accessLevel) {
10419
- const override = activeOverrides.get(userId);
10493
+ function findActiveSessionId(sm, userId, sessionPrefix2, activeOverrides2, accessLevel) {
10494
+ const override = activeOverrides2.get(userId);
10420
10495
  if (override && accessLevel === "full") {
10421
10496
  if (sm.getSessionById(override))
10422
10497
  return override;
10423
- activeOverrides.delete(userId);
10498
+ activeOverrides2.delete(userId);
10424
10499
  return null;
10425
10500
  }
10426
10501
  if (override && override.startsWith(sessionPrefix2)) {
10427
10502
  if (sm.getSessionById(override))
10428
10503
  return override;
10429
- activeOverrides.delete(userId);
10504
+ activeOverrides2.delete(userId);
10430
10505
  }
10431
10506
  const latest = sm.findLatestByPrefix(sessionPrefix2);
10432
10507
  if (!latest) {
10433
- activeOverrides.delete(userId);
10508
+ activeOverrides2.delete(userId);
10434
10509
  return null;
10435
10510
  }
10436
10511
  return latest.id;
@@ -10542,7 +10617,7 @@ async function execNew(ctx) {
10542
10617
  }
10543
10618
  function visibleSessions(ctx) {
10544
10619
  const { sessions: sessions2 } = ctx.sm.listSessions(ctx.accessLevel === "full" ? { rootOnly: true, limit: CHANNEL_LIST_LIMIT } : { rootOnly: true, prefix: ctx.sessionPrefix, limit: CHANNEL_LIST_LIMIT });
10545
- return sessions2;
10620
+ return sessions2.sort((a, b) => b.createdAt - a.createdAt);
10546
10621
  }
10547
10622
  function execList(ctx) {
10548
10623
  const sessions2 = visibleSessions(ctx);
@@ -11118,7 +11193,7 @@ async function createHarness(opts) {
11118
11193
  const lang = opts.lang ?? "en";
11119
11194
  const agentId = opts.agentId ?? "default";
11120
11195
  const accessLevel = !opts.accessLevel || opts.accessLevel === "full" ? null : opts.accessLevel;
11121
- const activeOverrides = /* @__PURE__ */ new Map();
11196
+ const activeOverrides2 = /* @__PURE__ */ new Map();
11122
11197
  async function bindWorkspace(workspace, requestedSessionId, newSession = false) {
11123
11198
  const sm = registry2.getOrCreate(workspace);
11124
11199
  let sessionId;
@@ -11132,7 +11207,7 @@ async function createHarness(opts) {
11132
11207
  await sm.createSession(agentId, null, "CLI", agentId, newId, void 0, accessLevel);
11133
11208
  sessionId = newId;
11134
11209
  } else {
11135
- const existing = findActiveSessionId(sm, USER_ID, SESSION_PREFIX, activeOverrides, opts.accessLevel ?? "full");
11210
+ const existing = findActiveSessionId(sm, USER_ID, SESSION_PREFIX, activeOverrides2, opts.accessLevel ?? "full");
11136
11211
  if (existing) {
11137
11212
  sessionId = existing;
11138
11213
  } else {
@@ -11141,7 +11216,7 @@ async function createHarness(opts) {
11141
11216
  sessionId = newId;
11142
11217
  }
11143
11218
  }
11144
- activeOverrides.set(USER_ID, sessionId);
11219
+ activeOverrides2.set(USER_ID, sessionId);
11145
11220
  const disabledSet = getDisabledSet(sm.getDb(), "agent");
11146
11221
  const agents = await scanAvailableAgents(workspace, disabledSet);
11147
11222
  const agentEntry = agents.find((a) => a.id === agentId);
@@ -11169,7 +11244,7 @@ async function createHarness(opts) {
11169
11244
  sessionPrefix: SESSION_PREFIX,
11170
11245
  accessLevel: opts.accessLevel ?? "full",
11171
11246
  channelLabel: "CLI",
11172
- activeOverrides,
11247
+ activeOverrides: activeOverrides2,
11173
11248
  workspacePath: state.workspace,
11174
11249
  lang
11175
11250
  };
@@ -11211,7 +11286,7 @@ async function createHarness(opts) {
11211
11286
  async function command(cmd, arg) {
11212
11287
  const result = await dispatchCommand(buildCommandContext(), cmd, arg, { channelName: "cli" });
11213
11288
  if (result?.switchTo) {
11214
- activeOverrides.set(USER_ID, result.switchTo);
11289
+ activeOverrides2.set(USER_ID, result.switchTo);
11215
11290
  state.unsubPersistent();
11216
11291
  state = {
11217
11292
  ...state,
@@ -11287,7 +11362,7 @@ async function createHarness(opts) {
11287
11362
  throw new Error(`Workspace does not exist: ${resolved}`);
11288
11363
  }
11289
11364
  state.unsubPersistent();
11290
- activeOverrides.delete(USER_ID);
11365
+ activeOverrides2.delete(USER_ID);
11291
11366
  state = await bindWorkspace(resolved);
11292
11367
  }
11293
11368
  return {
@@ -11372,8 +11447,14 @@ async function verifyPassword(plain, stored) {
11372
11447
  return false;
11373
11448
  const salt = Buffer.from(saltB64, "base64");
11374
11449
  const expected = Buffer.from(hashB64, "base64");
11375
- const actual = await scryptAsync(plain, salt, expected.length, { N, r, p });
11376
- return crypto2.timingSafeEqual(actual, expected);
11450
+ if (expected.length !== KEY_LEN)
11451
+ return false;
11452
+ try {
11453
+ const actual = await scryptAsync(plain, salt, expected.length, { N, r, p });
11454
+ return crypto2.timingSafeEqual(actual, expected);
11455
+ } catch {
11456
+ return false;
11457
+ }
11377
11458
  }
11378
11459
  function generateJwtSecret() {
11379
11460
  return crypto2.randomBytes(32).toString("base64");
@@ -11523,12 +11604,17 @@ var init_setup_settings = __esm({
11523
11604
  // packages/server/dist/setup-providers.js
11524
11605
  var setup_providers_exports = {};
11525
11606
  __export(setup_providers_exports, {
11607
+ AWS_CREDENTIAL_CHAIN_PROVIDER: () => AWS_CREDENTIAL_CHAIN_PROVIDER,
11608
+ bindBuiltinAgentsToProvider: () => bindBuiltinAgentsToProvider,
11609
+ listConfiguredSwitchableProviders: () => listConfiguredSwitchableProviders,
11526
11610
  listModelProviders: () => listModelProviders,
11527
11611
  listOptionalSkills: () => listOptionalSkills,
11528
- listRequiredSkillsWithSecrets: () => listRequiredSkillsWithSecrets
11612
+ listRequiredSkillsWithSecrets: () => listRequiredSkillsWithSecrets,
11613
+ readDefaultAgentProvider: () => readDefaultAgentProvider
11529
11614
  });
11530
11615
  import fs22 from "node:fs";
11531
11616
  import path26 from "node:path";
11617
+ import { homedir as homedir14 } from "node:os";
11532
11618
  import YAML9 from "yaml";
11533
11619
  function readYamlFile2(filePath) {
11534
11620
  if (!fs22.existsSync(filePath))
@@ -11617,6 +11703,69 @@ function listOptionalSkills() {
11617
11703
  }
11618
11704
  return out.sort((a, b) => a.id.localeCompare(b.id));
11619
11705
  }
11706
+ function listConfiguredSwitchableProviders() {
11707
+ return listModelProviders().filter((p) => {
11708
+ if (p.id === AWS_CREDENTIAL_CHAIN_PROVIDER)
11709
+ return false;
11710
+ return p.fields.some((f) => {
11711
+ const v = readSetting(`${p.id}.secrets.${f.key}`);
11712
+ return v != null && v.length > 0;
11713
+ });
11714
+ });
11715
+ }
11716
+ function readDefaultAgentProvider() {
11717
+ const data = readYamlFile2(path26.join(GLOBAL_AGENTS_DIR3, "default", "agent.yaml"));
11718
+ const provider = data?.model && typeof data.model === "object" ? data.model.provider : void 0;
11719
+ return typeof provider === "string" && provider.length > 0 ? provider : void 0;
11720
+ }
11721
+ function buildTemplateModelBlock(providerId) {
11722
+ const data = readYamlFile2(path26.join(TEMPLATES_DIR, "models", `${providerId}.yaml`));
11723
+ if (!data || typeof data !== "object")
11724
+ return null;
11725
+ const models = Array.isArray(data.models) ? data.models : [];
11726
+ const modelId = (typeof data.defaultModelId === "string" ? data.defaultModelId : void 0) ?? (models[0] ? models[0].id : void 0);
11727
+ const endpoint = typeof data.defaultEndpoint === "string" ? data.defaultEndpoint : void 0;
11728
+ const model = models.find((m) => m.id === modelId);
11729
+ const caps = model?.capabilities ?? {};
11730
+ const promptCaching = caps.promptCaching?.default;
11731
+ const thinkingCap = caps.thinking;
11732
+ const block = { provider: providerId };
11733
+ if (modelId)
11734
+ block.id = modelId;
11735
+ if (endpoint)
11736
+ block.endpoint = endpoint;
11737
+ if (promptCaching)
11738
+ block.promptCaching = promptCaching;
11739
+ if (thinkingCap?.defaultEnabled) {
11740
+ const thinking = { enabled: true };
11741
+ if (thinkingCap.default)
11742
+ thinking.effort = thinkingCap.default;
11743
+ if (thinkingCap.defaultBudgetTokens != null)
11744
+ thinking.budget_tokens = thinkingCap.defaultBudgetTokens;
11745
+ block.thinking = thinking;
11746
+ }
11747
+ return block;
11748
+ }
11749
+ function bindBuiltinAgentsToProvider(providerId) {
11750
+ const block = buildTemplateModelBlock(providerId);
11751
+ if (!block)
11752
+ return null;
11753
+ const agents = [];
11754
+ for (const id of REBIND_BUILTIN_AGENT_IDS) {
11755
+ const yamlPath = path26.join(GLOBAL_AGENTS_DIR3, id, "agent.yaml");
11756
+ if (!fs22.existsSync(yamlPath))
11757
+ continue;
11758
+ try {
11759
+ const doc = YAML9.parseDocument(fs22.readFileSync(yamlPath, "utf-8"));
11760
+ doc.set("model", block);
11761
+ fs22.writeFileSync(yamlPath, doc.toString(), "utf-8");
11762
+ agents.push(id);
11763
+ } catch {
11764
+ }
11765
+ }
11766
+ writeSetting("general.agent.default_provider", providerId);
11767
+ return { modelId: typeof block.id === "string" ? block.id : void 0, agents };
11768
+ }
11620
11769
  function listRequiredSkillsWithSecrets() {
11621
11770
  const dir = path26.join(TEMPLATES_DIR, "skills");
11622
11771
  if (!fs22.existsSync(dir))
@@ -11656,10 +11805,62 @@ function listRequiredSkillsWithSecrets() {
11656
11805
  }
11657
11806
  return out.sort((a, b) => a.id.localeCompare(b.id));
11658
11807
  }
11808
+ var AWS_CREDENTIAL_CHAIN_PROVIDER, REBIND_BUILTIN_AGENT_IDS, GLOBAL_AGENTS_DIR3;
11659
11809
  var init_setup_providers = __esm({
11660
11810
  "packages/server/dist/setup-providers.js"() {
11661
11811
  "use strict";
11662
11812
  init_init();
11813
+ init_setup_settings();
11814
+ AWS_CREDENTIAL_CHAIN_PROVIDER = "aws-bedrock-claude-invoke";
11815
+ REBIND_BUILTIN_AGENT_IDS = ["default", "executor", "deep-executor"];
11816
+ GLOBAL_AGENTS_DIR3 = path26.join(homedir14(), ".halo", "global", "agents");
11817
+ }
11818
+ });
11819
+
11820
+ // packages/cli/src/setup-bind-provider.ts
11821
+ var setup_bind_provider_exports = {};
11822
+ __export(setup_bind_provider_exports, {
11823
+ AWS_CHAIN_PROVIDER: () => AWS_CHAIN_PROVIDER,
11824
+ KEEP_CURRENT: () => KEEP_CURRENT,
11825
+ buildBindProviderPlan: () => buildBindProviderPlan
11826
+ });
11827
+ function buildBindProviderPlan(configured, currentProvider, currentProviderHasKeys) {
11828
+ if (configured.length === 0) return null;
11829
+ const current = currentProvider ?? AWS_CHAIN_PROVIDER;
11830
+ if (configured.some((p) => p.id === current)) return null;
11831
+ const currentIsAwsChain = current === AWS_CHAIN_PROVIDER;
11832
+ const intro = [
11833
+ `Default agent model provider: ${current}${currentIsAwsChain ? " (works with zero keys via the AWS credential chain: env / ~/.aws / EC2-ECS machine role)" : ""}`
11834
+ ];
11835
+ if (currentIsAwsChain) {
11836
+ intro.push('If you rely on AWS credentials / a machine role for Bedrock access, choose "Keep" to stay on Bedrock.');
11837
+ }
11838
+ const options = [
11839
+ ...configured.map((p) => ({
11840
+ value: p.id,
11841
+ label: `Switch default agent to ${p.displayName}`,
11842
+ hint: `(${p.id})`
11843
+ })),
11844
+ {
11845
+ value: KEEP_CURRENT,
11846
+ label: `Keep ${current}`,
11847
+ hint: currentIsAwsChain ? "(AWS credential chain / machine role)" : ""
11848
+ }
11849
+ ];
11850
+ const initialIndex = configured.length === 1 && !currentProviderHasKeys ? 0 : options.length - 1;
11851
+ return {
11852
+ intro,
11853
+ question: "Bind the default agent to a provider you just configured?",
11854
+ options,
11855
+ initialIndex
11856
+ };
11857
+ }
11858
+ var AWS_CHAIN_PROVIDER, KEEP_CURRENT;
11859
+ var init_setup_bind_provider = __esm({
11860
+ "packages/cli/src/setup-bind-provider.ts"() {
11861
+ "use strict";
11862
+ AWS_CHAIN_PROVIDER = "aws-bedrock-claude-invoke";
11863
+ KEEP_CURRENT = "__keep__";
11663
11864
  }
11664
11865
  });
11665
11866
 
@@ -12472,7 +12673,7 @@ function scanCandidates(partial, workspace, kind) {
12472
12673
  function applyPathPick(value, ref, pick) {
12473
12674
  let markerStart = ref.pathStart - 1;
12474
12675
  if (value[markerStart - 1] === '"') markerStart -= 1;
12475
- const explicitMatch = value.slice(0, markerStart + 1).match(/@(file|image|scope)\s+$/);
12676
+ const explicitMatch = value.slice(0, markerStart + 1).match(/@(file|image|scope)\s+"?$/);
12476
12677
  if (explicitMatch) markerStart = explicitMatch.index ?? markerStart;
12477
12678
  else {
12478
12679
  markerStart = ref.pathStart - 1 - (value[ref.pathStart - 1] === '"' ? 1 : 0);
@@ -12480,8 +12681,8 @@ function applyPathPick(value, ref, pick) {
12480
12681
  const before = value.slice(0, markerStart);
12481
12682
  const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
12482
12683
  const needsQuote = pick.insertPath.includes(" ");
12483
- const path62 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
12484
- return `${before}@${kind} ${path62}`;
12684
+ const path63 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
12685
+ return `${before}@${kind} ${path63}`;
12485
12686
  }
12486
12687
  var IMAGE_EXTS2;
12487
12688
  var init_path_suggest_engine = __esm({
@@ -13961,7 +14162,8 @@ var init_cli = __esm({
13961
14162
 
13962
14163
  // packages/core/dist/workspace/workspace.js
13963
14164
  import { readFile, writeFile, mkdir, readdir, access } from "node:fs/promises";
13964
- import { resolve, relative, join, isAbsolute } from "node:path";
14165
+ import { realpathSync as realpathSync2 } from "node:fs";
14166
+ import { resolve, relative, join, isAbsolute, sep } from "node:path";
13965
14167
  var WorkspaceError, SKIP_DIRS2, Workspace;
13966
14168
  var init_workspace = __esm({
13967
14169
  "packages/core/dist/workspace/workspace.js"() {
@@ -14000,12 +14202,12 @@ None yet.
14000
14202
  }
14001
14203
  console.log(`[Workspace] Initialized workspace: ${this.projectRoot}`);
14002
14204
  }
14003
- async readFile(path62) {
14004
- const fullPath = this.validatePath(path62);
14205
+ async readFile(path63) {
14206
+ const fullPath = this.validatePath(path63);
14005
14207
  return readFile(fullPath, "utf-8");
14006
14208
  }
14007
- async writeFile(path62, content) {
14008
- const fullPath = this.validatePath(path62);
14209
+ async writeFile(path63, content) {
14210
+ const fullPath = this.validatePath(path63);
14009
14211
  const dir = resolve(fullPath, "..");
14010
14212
  await mkdir(dir, { recursive: true });
14011
14213
  await writeFile(fullPath, content, "utf-8");
@@ -14025,19 +14227,30 @@ None yet.
14025
14227
  }
14026
14228
  return results;
14027
14229
  }
14028
- async fileExists(path62) {
14230
+ async fileExists(path63) {
14029
14231
  try {
14030
- const fullPath = this.validatePath(path62);
14232
+ const fullPath = this.validatePath(path63);
14031
14233
  await access(fullPath);
14032
14234
  return true;
14033
14235
  } catch {
14034
14236
  return false;
14035
14237
  }
14036
14238
  }
14037
- validatePath(path62) {
14038
- const fullPath = isAbsolute(path62) ? resolve(path62) : resolve(this.projectRoot, path62);
14039
- if (!fullPath.startsWith(this.projectRoot)) {
14040
- throw new WorkspaceError(`Path "${path62}" is outside the workspace directory "${this.projectRoot}"`);
14239
+ validatePath(path63) {
14240
+ const fullPath = isAbsolute(path63) ? resolve(path63) : resolve(this.projectRoot, path63);
14241
+ const root = resolve(this.projectRoot);
14242
+ if (fullPath !== root && !fullPath.startsWith(root + sep)) {
14243
+ throw new WorkspaceError(`Path "${path63}" is outside the workspace directory "${this.projectRoot}"`);
14244
+ }
14245
+ try {
14246
+ const real = realpathSync2(fullPath);
14247
+ const realRoot = realpathSync2(root);
14248
+ if (real !== realRoot && !real.startsWith(realRoot + sep)) {
14249
+ throw new WorkspaceError(`Path "${path63}" is outside the workspace directory "${this.projectRoot}"`);
14250
+ }
14251
+ } catch (err) {
14252
+ if (err instanceof WorkspaceError)
14253
+ throw err;
14041
14254
  }
14042
14255
  return fullPath;
14043
14256
  }
@@ -14146,10 +14359,10 @@ var init_git_manager = __esm({
14146
14359
  throw new GitManagerError(`Failed to commit: ${message2}`);
14147
14360
  }
14148
14361
  }
14149
- async getDiff(path62) {
14362
+ async getDiff(path63) {
14150
14363
  try {
14151
- if (path62) {
14152
- return await this.git.diff(["HEAD", "--", path62]);
14364
+ if (path63) {
14365
+ return await this.git.diff(["HEAD", "--", path63]);
14153
14366
  }
14154
14367
  return await this.git.diff(["HEAD"]);
14155
14368
  } catch {
@@ -14225,6 +14438,7 @@ var init_git_manager = __esm({
14225
14438
  "--name-status",
14226
14439
  "-r",
14227
14440
  "-m",
14441
+ "-M",
14228
14442
  "--root",
14229
14443
  hash
14230
14444
  ]);
@@ -14237,12 +14451,12 @@ var init_git_manager = __esm({
14237
14451
  const code = parts[0];
14238
14452
  const status = code[0];
14239
14453
  const isRenameOrCopy = status === "R" || status === "C";
14240
- const path62 = isRenameOrCopy ? parts[2] : parts[1];
14241
- if (!path62 || seen.has(path62))
14454
+ const path63 = isRenameOrCopy ? parts[2] : parts[1];
14455
+ if (!path63 || seen.has(path63))
14242
14456
  continue;
14243
- seen.add(path62);
14457
+ seen.add(path63);
14244
14458
  files.push({
14245
- path: path62,
14459
+ path: path63,
14246
14460
  status,
14247
14461
  from: isRenameOrCopy ? parts[1] : void 0
14248
14462
  });
@@ -14390,9 +14604,9 @@ var init_git_manager = __esm({
14390
14604
  * empty for an added file or the root commit) and `modified` is the version
14391
14605
  * at `commit` (empty for a deletion). The working-tree/staged path above is
14392
14606
  * left entirely untouched (commit === undefined preserves existing behavior). */
14393
- async getFileDiff(path62, staged, from, commit) {
14607
+ async getFileDiff(path63, staged, from, commit) {
14394
14608
  if (commit) {
14395
- const parentPath = from || path62;
14609
+ const parentPath = from || path63;
14396
14610
  let original2 = "";
14397
14611
  try {
14398
14612
  original2 = await this.git.show([`${commit}^:${parentPath}`]);
@@ -14401,13 +14615,13 @@ var init_git_manager = __esm({
14401
14615
  }
14402
14616
  let modified2 = "";
14403
14617
  try {
14404
- modified2 = await this.git.show([`${commit}:${path62}`]);
14618
+ modified2 = await this.git.show([`${commit}:${path63}`]);
14405
14619
  } catch {
14406
14620
  modified2 = "";
14407
14621
  }
14408
14622
  return { original: original2, modified: modified2 };
14409
14623
  }
14410
- const headPath = from || path62;
14624
+ const headPath = from || path63;
14411
14625
  let original = "";
14412
14626
  try {
14413
14627
  original = await this.git.show([`HEAD:${headPath}`]);
@@ -14417,13 +14631,13 @@ var init_git_manager = __esm({
14417
14631
  let modified = "";
14418
14632
  if (staged) {
14419
14633
  try {
14420
- modified = await this.git.show([`:${path62}`]);
14634
+ modified = await this.git.show([`:${path63}`]);
14421
14635
  } catch {
14422
14636
  modified = "";
14423
14637
  }
14424
14638
  } else {
14425
14639
  try {
14426
- modified = await this.workspace.readFile(path62);
14640
+ modified = await this.workspace.readFile(path63);
14427
14641
  } catch {
14428
14642
  modified = "";
14429
14643
  }
@@ -14480,7 +14694,7 @@ import fs27 from "node:fs/promises";
14480
14694
  import { createReadStream } from "node:fs";
14481
14695
  import { Readable } from "node:stream";
14482
14696
  import path32 from "node:path";
14483
- import { homedir as homedir14 } from "node:os";
14697
+ import { homedir as homedir15 } from "node:os";
14484
14698
  function createFileRoutes() {
14485
14699
  const app2 = new Hono();
14486
14700
  async function resolveProjectPath(projectId) {
@@ -15025,7 +15239,7 @@ function createFileRoutes() {
15025
15239
  }
15026
15240
  });
15027
15241
  app2.get("/fs/home", (c) => {
15028
- return c.json({ home: homedir14() });
15242
+ return c.json({ home: homedir15() });
15029
15243
  });
15030
15244
  app2.post("/fs/workspace/resolve", async (c) => {
15031
15245
  const body = await c.req.json();
@@ -15051,7 +15265,7 @@ function createFileRoutes() {
15051
15265
  }
15052
15266
  });
15053
15267
  app2.get("/fs/browse", async (c) => {
15054
- const target = c.req.query("path") ?? homedir14();
15268
+ const target = c.req.query("path") ?? homedir15();
15055
15269
  if (!path32.isAbsolute(target))
15056
15270
  return c.json({ error: "absolute path required" }, 400);
15057
15271
  try {
@@ -15076,7 +15290,7 @@ var init_files = __esm({
15076
15290
  // packages/server/dist/git-credentials.js
15077
15291
  import fs28 from "node:fs";
15078
15292
  import path33 from "node:path";
15079
- import { homedir as homedir15 } from "node:os";
15293
+ import { homedir as homedir16 } from "node:os";
15080
15294
  import { spawnSync } from "node:child_process";
15081
15295
  function writeGitCredentialsFile(host, username, token) {
15082
15296
  const line = `https://${encodeURIComponent(username)}:${encodeURIComponent(token)}@${host}`;
@@ -15150,7 +15364,7 @@ var GIT_CREDENTIALS_PATH;
15150
15364
  var init_git_credentials = __esm({
15151
15365
  "packages/server/dist/git-credentials.js"() {
15152
15366
  "use strict";
15153
- GIT_CREDENTIALS_PATH = path33.join(homedir15(), ".git-credentials");
15367
+ GIT_CREDENTIALS_PATH = path33.join(homedir16(), ".git-credentials");
15154
15368
  }
15155
15369
  });
15156
15370
 
@@ -15711,7 +15925,7 @@ var init_git = __esm({
15711
15925
  import { Hono as Hono3 } from "hono";
15712
15926
  import fs31 from "node:fs/promises";
15713
15927
  import path36 from "node:path";
15714
- import { homedir as homedir16 } from "node:os";
15928
+ import { homedir as homedir17 } from "node:os";
15715
15929
  import YAML10 from "yaml";
15716
15930
  function getAvailableTools() {
15717
15931
  if (_cachedTools)
@@ -15872,9 +16086,9 @@ function createAgentConfigRoutes() {
15872
16086
  return c.json(getModelsRegistry());
15873
16087
  });
15874
16088
  app2.get("/agent-configs", async (c) => {
15875
- await ensureDir2(GLOBAL_AGENTS_DIR3);
15876
- await ensureDefaultAgent(GLOBAL_AGENTS_DIR3);
15877
- const globalAgents = await scanAgentsDir(GLOBAL_AGENTS_DIR3, "global");
16089
+ await ensureDir2(GLOBAL_AGENTS_DIR4);
16090
+ await ensureDefaultAgent(GLOBAL_AGENTS_DIR4);
16091
+ const globalAgents = await scanAgentsDir(GLOBAL_AGENTS_DIR4, "global");
15878
16092
  const projectId = c.req.query("projectId");
15879
16093
  let workspaceAgents = [];
15880
16094
  if (projectId) {
@@ -15912,7 +16126,7 @@ function createAgentConfigRoutes() {
15912
16126
  return c.json({ error: "projectId required for workspace agents" }, 400);
15913
16127
  baseDir = path36.join(body.projectId, ".halo", "agents");
15914
16128
  } else {
15915
- baseDir = GLOBAL_AGENTS_DIR3;
16129
+ baseDir = GLOBAL_AGENTS_DIR4;
15916
16130
  }
15917
16131
  const agentDir = path36.join(baseDir, id);
15918
16132
  try {
@@ -15923,7 +16137,7 @@ function createAgentConfigRoutes() {
15923
16137
  let conflictScope = null;
15924
16138
  if (scope === "workspace") {
15925
16139
  try {
15926
- await fs31.access(path36.join(GLOBAL_AGENTS_DIR3, id));
16140
+ await fs31.access(path36.join(GLOBAL_AGENTS_DIR4, id));
15927
16141
  conflictScope = "global";
15928
16142
  } catch {
15929
16143
  }
@@ -15952,7 +16166,7 @@ ${body.description ?? ""}
15952
16166
  if (scope === "workspace" && projectId) {
15953
16167
  agentDir = path36.join(projectId, ".halo", "agents", id);
15954
16168
  } else {
15955
- agentDir = path36.join(GLOBAL_AGENTS_DIR3, id);
16169
+ agentDir = path36.join(GLOBAL_AGENTS_DIR4, id);
15956
16170
  }
15957
16171
  const yamlPath = path36.join(agentDir, "agent.yaml");
15958
16172
  try {
@@ -15975,7 +16189,7 @@ ${body.description ?? ""}
15975
16189
  if (scope === "workspace" && body.projectId) {
15976
16190
  agentDir = path36.join(body.projectId, ".halo", "agents", id);
15977
16191
  } else {
15978
- agentDir = path36.join(GLOBAL_AGENTS_DIR3, id);
16192
+ agentDir = path36.join(GLOBAL_AGENTS_DIR4, id);
15979
16193
  }
15980
16194
  const yamlPath = path36.join(agentDir, "agent.yaml");
15981
16195
  try {
@@ -15995,7 +16209,7 @@ ${body.description ?? ""}
15995
16209
  if (scope === "workspace" && projectId) {
15996
16210
  agentDir = path36.join(projectId, ".halo", "agents", id);
15997
16211
  } else {
15998
- agentDir = path36.join(GLOBAL_AGENTS_DIR3, id);
16212
+ agentDir = path36.join(GLOBAL_AGENTS_DIR4, id);
15999
16213
  }
16000
16214
  try {
16001
16215
  await fs31.access(agentDir);
@@ -16003,7 +16217,7 @@ ${body.description ?? ""}
16003
16217
  return c.json({ error: "Agent not found" }, 404);
16004
16218
  }
16005
16219
  if (scope === "global") {
16006
- const globalAgents = await scanAgentsDir(GLOBAL_AGENTS_DIR3, "global");
16220
+ const globalAgents = await scanAgentsDir(GLOBAL_AGENTS_DIR4, "global");
16007
16221
  if (globalAgents.length <= 1) {
16008
16222
  return c.json({ error: "Cannot delete the last global agent. At least one global agent must remain." }, 400);
16009
16223
  }
@@ -16083,9 +16297,9 @@ ${body.description ?? ""}
16083
16297
  });
16084
16298
  function getSessionsDir(agentId, _source, projectId) {
16085
16299
  if (agentId.startsWith("__") && agentId.endsWith("__")) {
16086
- return path36.join(homedir16(), ".halo", "global", "internal-sessions", agentId);
16300
+ return path36.join(homedir17(), ".halo", "global", "internal-sessions", agentId);
16087
16301
  }
16088
- const base = projectId ? path36.join(projectId, ".halo") : path36.join(homedir16(), ".halo");
16302
+ const base = projectId ? path36.join(projectId, ".halo") : path36.join(homedir17(), ".halo");
16089
16303
  return path36.join(base, "sessions", agentId);
16090
16304
  }
16091
16305
  app2.get("/agent-configs/:id/sessions", async (c) => {
@@ -16198,7 +16412,7 @@ ${body.description ?? ""}
16198
16412
  });
16199
16413
  return app2;
16200
16414
  }
16201
- var _cachedTools, GLOBAL_AGENTS_DIR3, FALLBACK_SCAFFOLD_PROVIDER, _yamlCache;
16415
+ var _cachedTools, GLOBAL_AGENTS_DIR4, FALLBACK_SCAFFOLD_PROVIDER, _yamlCache;
16202
16416
  var init_agent_configs = __esm({
16203
16417
  "packages/server/dist/routes/agent-configs.js"() {
16204
16418
  "use strict";
@@ -16208,7 +16422,7 @@ var init_agent_configs = __esm({
16208
16422
  init_config();
16209
16423
  init_db();
16210
16424
  _cachedTools = null;
16211
- GLOBAL_AGENTS_DIR3 = path36.join(homedir16(), ".halo", "global", "agents");
16425
+ GLOBAL_AGENTS_DIR4 = path36.join(homedir17(), ".halo", "global", "agents");
16212
16426
  FALLBACK_SCAFFOLD_PROVIDER = "aws-bedrock-claude-invoke";
16213
16427
  _yamlCache = /* @__PURE__ */ new Map();
16214
16428
  }
@@ -16218,7 +16432,7 @@ var init_agent_configs = __esm({
16218
16432
  import { Hono as Hono4 } from "hono";
16219
16433
  import fs32 from "node:fs/promises";
16220
16434
  import path37 from "node:path";
16221
- import { homedir as homedir17 } from "node:os";
16435
+ import { homedir as homedir18 } from "node:os";
16222
16436
  import YAML11 from "yaml";
16223
16437
  function defaultSkillSettings() {
16224
16438
  return {
@@ -16416,7 +16630,7 @@ var init_skills = __esm({
16416
16630
  "use strict";
16417
16631
  init_agent_loader();
16418
16632
  init_db();
16419
- GLOBAL_SETTINGS_PATH = path37.join(homedir17(), ".halo", "secrets", "settings.yaml");
16633
+ GLOBAL_SETTINGS_PATH = path37.join(homedir18(), ".halo", "secrets", "settings.yaml");
16420
16634
  _skillMdCache = /* @__PURE__ */ new Map();
16421
16635
  }
16422
16636
  });
@@ -16425,7 +16639,7 @@ var init_skills = __esm({
16425
16639
  import { Hono as Hono5 } from "hono";
16426
16640
  import fs33 from "node:fs/promises";
16427
16641
  import path38 from "node:path";
16428
- import { homedir as homedir18 } from "node:os";
16642
+ import { homedir as homedir19 } from "node:os";
16429
16643
  import YAML12 from "yaml";
16430
16644
  async function readSettingsFile(filePath) {
16431
16645
  try {
@@ -16437,7 +16651,11 @@ async function readSettingsFile(filePath) {
16437
16651
  }
16438
16652
  async function writeSettingsFile(filePath, data) {
16439
16653
  await fs33.mkdir(path38.dirname(filePath), { recursive: true });
16440
- await fs33.writeFile(filePath, YAML12.stringify(data, { lineWidth: 120 }), "utf-8");
16654
+ await fs33.writeFile(filePath, YAML12.stringify(data, { lineWidth: 120 }), { encoding: "utf-8", mode: 384 });
16655
+ try {
16656
+ await fs33.chmod(filePath, 384);
16657
+ } catch {
16658
+ }
16441
16659
  }
16442
16660
  function deepMerge2(base, override) {
16443
16661
  const result = { ...base };
@@ -16546,8 +16764,8 @@ function detectOrphans(tree, declared) {
16546
16764
  if (!block || typeof block !== "object")
16547
16765
  continue;
16548
16766
  for (const k of Object.keys(block)) {
16549
- const path62 = `${namespaceId}.${kindWord}.${k}`;
16550
- if (!declaredMap.has(path62)) {
16767
+ const path63 = `${namespaceId}.${kindWord}.${k}`;
16768
+ if (!declaredMap.has(path63)) {
16551
16769
  out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
16552
16770
  }
16553
16771
  }
@@ -16673,16 +16891,16 @@ var init_settings = __esm({
16673
16891
  "packages/server/dist/routes/settings.js"() {
16674
16892
  "use strict";
16675
16893
  init_settings_schema();
16676
- GLOBAL_SETTINGS_PATH2 = path38.join(homedir18(), ".halo", "secrets", "settings.yaml");
16894
+ GLOBAL_SETTINGS_PATH2 = path38.join(homedir19(), ".halo", "secrets", "settings.yaml");
16677
16895
  _changeListeners = [];
16678
16896
  }
16679
16897
  });
16680
16898
 
16681
16899
  // packages/server/dist/paths.js
16682
16900
  import path39 from "node:path";
16683
- import { homedir as homedir19 } from "node:os";
16901
+ import { homedir as homedir20 } from "node:os";
16684
16902
  function haloHome() {
16685
- return path39.join(homedir19(), ".halo");
16903
+ return path39.join(homedir20(), ".halo");
16686
16904
  }
16687
16905
  function globalDir() {
16688
16906
  return path39.join(haloHome(), "global");
@@ -16876,7 +17094,7 @@ import { Hono as Hono6 } from "hono";
16876
17094
  import { eq as eq7, desc as desc2, asc, inArray as inArray2, lt as lt3, and as and5, or, sql } from "drizzle-orm";
16877
17095
  import fs35 from "node:fs/promises";
16878
17096
  import path41 from "node:path";
16879
- import { homedir as homedir20 } from "node:os";
17097
+ import { homedir as homedir21 } from "node:os";
16880
17098
  function runDirOf(workspacePath, runId) {
16881
17099
  return path41.join(workspacePath, ".halo", "evo", "runs", runId);
16882
17100
  }
@@ -16955,7 +17173,7 @@ function createEvolutionRoutes() {
16955
17173
  }
16956
17174
  let wrapperLog = null;
16957
17175
  try {
16958
- wrapperLog = await fs35.readFile(path41.join(homedir20(), ".halo", "global", "logs", "evo", `run-${id}.log`), "utf-8");
17176
+ wrapperLog = await fs35.readFile(path41.join(homedir21(), ".halo", "global", "logs", "evo", `run-${id}.log`), "utf-8");
16959
17177
  } catch {
16960
17178
  }
16961
17179
  let subCliLog = null;
@@ -17644,6 +17862,7 @@ var init_accounts2 = __esm({
17644
17862
  });
17645
17863
 
17646
17864
  // packages/server/dist/middleware/brute-force.js
17865
+ import { getConnInfo } from "@hono/node-server/conninfo";
17647
17866
  function bucket(name) {
17648
17867
  let b = buckets.get(name);
17649
17868
  if (!b) {
@@ -17653,9 +17872,17 @@ function bucket(name) {
17653
17872
  return b;
17654
17873
  }
17655
17874
  function getClientIp(c) {
17656
- const forwarded = c.req.header("x-forwarded-for");
17657
- if (forwarded)
17658
- return forwarded.split(",")[0].trim();
17875
+ if (config.server.trustProxy) {
17876
+ const forwarded = c.req.header("x-forwarded-for");
17877
+ if (forwarded)
17878
+ return forwarded.split(",")[0].trim();
17879
+ }
17880
+ try {
17881
+ const addr = getConnInfo(c).remote.address;
17882
+ if (addr)
17883
+ return addr;
17884
+ } catch {
17885
+ }
17659
17886
  return "unknown";
17660
17887
  }
17661
17888
  function isLockedOut(bucketName, ip) {
@@ -17687,6 +17914,7 @@ var MAX_ATTEMPTS, LOCKOUT_MS, buckets;
17687
17914
  var init_brute_force = __esm({
17688
17915
  "packages/server/dist/middleware/brute-force.js"() {
17689
17916
  "use strict";
17917
+ init_config();
17690
17918
  MAX_ATTEMPTS = 5;
17691
17919
  LOCKOUT_MS = 15 * 60 * 1e3;
17692
17920
  buckets = /* @__PURE__ */ new Map();
@@ -17912,6 +18140,29 @@ function snapshotWorkspaceReadonly(wsPath, label) {
17912
18140
  skills: scanSkills2(path43.join(wsPath, ".halo", "skills"))
17913
18141
  };
17914
18142
  }
18143
+ function readonlySessionCounts(wsPath, limit) {
18144
+ const counts = { running: 0, idle: 0, stopped: 0, total: 0 };
18145
+ const reader = roReader(wsPath);
18146
+ if (!reader)
18147
+ return counts;
18148
+ try {
18149
+ const rows = reader.listPage.all(limit);
18150
+ const liveParents = new Set(reader.liveParents.all().map((r) => r.parent_id));
18151
+ counts.total = rows.length;
18152
+ for (const r of rows) {
18153
+ if (r.stopped_at)
18154
+ counts.stopped++;
18155
+ else if (liveParents.has(r.id))
18156
+ counts.running++;
18157
+ else
18158
+ counts.idle++;
18159
+ }
18160
+ } catch (err) {
18161
+ console.log(`[Show] read-only query failed ${wsPath}: ${err instanceof Error ? err.message : String(err)}`);
18162
+ dropRoReader(wsPath);
18163
+ }
18164
+ return counts;
18165
+ }
17915
18166
  function trimSessionMessages(messages2) {
17916
18167
  return messages2.slice(-SESSION_LOG_MAX_MSGS).map((m) => ({
17917
18168
  id: m.id,
@@ -18141,9 +18392,18 @@ function createMetricsRoutes(registry2) {
18141
18392
  let workspaces = 0;
18142
18393
  for (const wsPath of discoverWorkspaces2(registry2)) {
18143
18394
  try {
18144
- const sm = registry2.getOrCreate(wsPath);
18145
- const { sessions: sessions2 } = sm.listSessions({ includeArchived: false, limit: SESSIONS_PER_WS2 });
18395
+ const sm = registry2.peek(wsPath);
18146
18396
  workspaces++;
18397
+ if (!sm) {
18398
+ const counts = readonlySessionCounts(wsPath, SESSIONS_PER_WS2);
18399
+ running += counts.running;
18400
+ idle += counts.idle;
18401
+ stopped += counts.stopped;
18402
+ total += counts.total;
18403
+ continue;
18404
+ }
18405
+ dropRoReader(wsPath);
18406
+ const { sessions: sessions2 } = sm.listSessions({ includeArchived: false, limit: SESSIONS_PER_WS2 });
18147
18407
  for (const r of sessions2) {
18148
18408
  total++;
18149
18409
  if (r.status === "running")
@@ -18186,6 +18446,7 @@ var init_metrics = __esm({
18186
18446
  init_channel_db();
18187
18447
  init_accounts2();
18188
18448
  init_brute_force();
18449
+ init_show();
18189
18450
  TOKEN_BUCKET2 = "web-token";
18190
18451
  SESSIONS_PER_WS2 = 500;
18191
18452
  }
@@ -18221,16 +18482,300 @@ var init_commands3 = __esm({
18221
18482
  }
18222
18483
  });
18223
18484
 
18224
- // packages/server/dist/ws/file-watcher.js
18225
- import fs40 from "node:fs";
18485
+ // packages/server/dist/sessions/session-types.js
18486
+ function inferMessageType(msg) {
18487
+ if (msg.type)
18488
+ return msg.type;
18489
+ if (msg.role === "user")
18490
+ return "user";
18491
+ if (msg.role === "assistant")
18492
+ return "assistant";
18493
+ if (msg.toolName)
18494
+ return "tool_call";
18495
+ if (msg.toolOutput !== void 0 && !msg.toolName)
18496
+ return "tool_result";
18497
+ if (msg.usage)
18498
+ return "usage";
18499
+ if (msg.systemPrompt)
18500
+ return "context";
18501
+ return "notification";
18502
+ }
18503
+ var init_session_types = __esm({
18504
+ "packages/server/dist/sessions/session-types.js"() {
18505
+ "use strict";
18506
+ }
18507
+ });
18508
+
18509
+ // packages/server/dist/routes/agentcore.js
18510
+ import { Hono as Hono11 } from "hono";
18226
18511
  import path44 from "node:path";
18512
+ import fs40 from "node:fs";
18513
+ function haloSessionId(runtimeSessionId) {
18514
+ return `agentcore_${runtimeSessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 80)}`;
18515
+ }
18516
+ function userWorkspace(base, runtimeSessionId) {
18517
+ const dir = path44.join(base, "users", runtimeSessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 80));
18518
+ fs40.mkdirSync(dir, { recursive: true });
18519
+ return dir;
18520
+ }
18521
+ async function ensureSession(sm, workspace, sessionId) {
18522
+ if (sm.getSessionById(sessionId))
18523
+ return;
18524
+ const agentId = await resolveDefaultAgentId(sm, workspace);
18525
+ await sm.createSession(agentId, null, "AgentCore Runtime", void 0, sessionId);
18526
+ }
18527
+ function runTurnCollect(sm, sessionId, prompt) {
18528
+ return new Promise((resolve2, reject) => {
18529
+ let text5 = "";
18530
+ const unsubscribe = sm.registerEventListener(sessionId, (event) => {
18531
+ if (event.taskId)
18532
+ return;
18533
+ if (event.type === "stream") {
18534
+ text5 += event.text ?? "";
18535
+ } else if (event.type === "complete" && !event.batchBoundary) {
18536
+ unsubscribe();
18537
+ resolve2(text5);
18538
+ } else if (event.type === "error") {
18539
+ unsubscribe();
18540
+ resolve2(text5 ? `${text5}
18541
+
18542
+ [error] ${event.error}` : `[error] ${event.error ?? "unknown error"}`);
18543
+ }
18544
+ });
18545
+ sm.appendUserMessage(sessionId, prompt);
18546
+ sm.sendUserMessage(sessionId, `[channel: agentcore]
18547
+
18548
+ ${prompt}`).then((result) => {
18549
+ if (result === "queued") {
18550
+ unsubscribe();
18551
+ resolve2("(agent busy \u2014 message queued and will be processed with the current turn)");
18552
+ }
18553
+ }).catch((err) => {
18554
+ unsubscribe();
18555
+ reject(err);
18556
+ });
18557
+ });
18558
+ }
18559
+ function createAgentCoreRoutes(deps) {
18560
+ const { registry: registry2, workspace } = deps;
18561
+ const app2 = new Hono11();
18562
+ app2.get("/ping", (c) => {
18563
+ const busy = registry2.list().some(({ sm }) => sm.hasRunningSessions());
18564
+ return c.json({ status: busy ? "HealthyBusy" : "Healthy" });
18565
+ });
18566
+ app2.post("/invocations", async (c) => {
18567
+ const body = await c.req.json().catch(() => ({}));
18568
+ const prompt = typeof body.input?.prompt === "string" ? body.input.prompt.trim() : "";
18569
+ if (!prompt)
18570
+ return c.json({ error: "input.prompt required" }, 400);
18571
+ const runtimeSessionId = c.req.header(SESSION_HEADER) || `inv-${Date.now().toString(36)}`;
18572
+ const sessionId = haloSessionId(runtimeSessionId);
18573
+ const userWs = userWorkspace(workspace, runtimeSessionId);
18574
+ const sm = registry2.getOrCreate(userWs);
18575
+ await ensureSession(sm, userWs, sessionId);
18576
+ const text5 = await runTurnCollect(sm, sessionId, prompt);
18577
+ return c.json({ output: { message: { role: "assistant", content: [{ text: text5 }] } } });
18578
+ });
18579
+ return app2;
18580
+ }
18581
+ function buildHistory(sm, sessionId) {
18582
+ const state = sm.getUIState(sessionId);
18583
+ if (!state)
18584
+ return { messages: [], running: false };
18585
+ const messages2 = [];
18586
+ for (const m of createSaveSnapshot(state)) {
18587
+ if (m.taskId)
18588
+ continue;
18589
+ const type = inferMessageType(m);
18590
+ if (type !== "user" && type !== "assistant")
18591
+ continue;
18592
+ const blocks = [];
18593
+ for (const b of m.contentBlocks ?? []) {
18594
+ if (b.type === "thinking") {
18595
+ blocks.push({ type: "thinking", text: b.text });
18596
+ } else if (b.type === "tool_call") {
18597
+ blocks.push({ type: "tool_call", toolCall: { name: b.toolCall.name, input: b.toolCall.input, output: b.toolCall.output?.slice(0, 500) } });
18598
+ }
18599
+ }
18600
+ messages2.push({
18601
+ role: m.role,
18602
+ content: m.content,
18603
+ ts: m.timestamp,
18604
+ ...blocks.length > 0 ? { contentBlocks: blocks } : {}
18605
+ });
18606
+ }
18607
+ return { messages: messages2, running: sm.isSessionRunning(sessionId) };
18608
+ }
18609
+ function setupAgentCoreWebSocket(deps) {
18610
+ const { wss: wss2, registry: registry2, workspace } = deps;
18611
+ wss2.on("connection", (ws, req) => {
18612
+ const url = new URL(req.url ?? "/ws", "http://localhost");
18613
+ const runtimeSessionId = req.headers[SESSION_HEADER] || url.searchParams.get("sessionId") || `ws-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
18614
+ let sessionId = haloSessionId(runtimeSessionId);
18615
+ const userWs = userWorkspace(workspace, runtimeSessionId);
18616
+ const sm = registry2.getOrCreate(userWs);
18617
+ const send = (obj) => {
18618
+ if (ws.readyState !== ws.OPEN)
18619
+ return;
18620
+ try {
18621
+ ws.send(JSON.stringify(obj));
18622
+ } catch {
18623
+ }
18624
+ };
18625
+ let unsubscribe = null;
18626
+ const attachListener = () => {
18627
+ unsubscribe ??= sm.registerEventListener(sessionId, (event) => {
18628
+ if (event.taskId)
18629
+ return;
18630
+ switch (event.type) {
18631
+ case "stream":
18632
+ if (event.text)
18633
+ send({ type: "stream", text: event.text });
18634
+ break;
18635
+ case "thinking":
18636
+ send({ type: "thinking", text: event.text ?? "" });
18637
+ break;
18638
+ case "tool_call":
18639
+ send({ type: "tool_call", toolName: event.toolName, toolInput: event.toolInput });
18640
+ break;
18641
+ case "tool_result":
18642
+ send({ type: "tool_result", toolName: event.toolName, result: event.toolResult?.slice(0, 500) });
18643
+ break;
18644
+ case "complete":
18645
+ if (!event.batchBoundary)
18646
+ send({ type: "complete" });
18647
+ break;
18648
+ case "error":
18649
+ send({ type: "error", error: event.error ?? "unknown error" });
18650
+ break;
18651
+ }
18652
+ });
18653
+ };
18654
+ let ready = null;
18655
+ const ensureOnce = () => {
18656
+ ready ??= ensureSession(sm, userWs, sessionId).then(() => attachListener());
18657
+ return ready;
18658
+ };
18659
+ ws.on("message", (raw) => {
18660
+ let msg;
18661
+ try {
18662
+ msg = JSON.parse(String(raw));
18663
+ } catch {
18664
+ send({ type: "error", error: "invalid JSON" });
18665
+ return;
18666
+ }
18667
+ if (msg.type === "init" || typeof msg.inputText === "string" && msg.inputText.trim() === "/init") {
18668
+ if (sm.getSessionById(sessionId)) {
18669
+ attachListener();
18670
+ send({ type: "history", ...buildHistory(sm, sessionId) });
18671
+ } else {
18672
+ send({ type: "history", messages: [], running: false });
18673
+ }
18674
+ return;
18675
+ }
18676
+ if (msg.type === "stop") {
18677
+ if (!sm.getSessionById(sessionId))
18678
+ return;
18679
+ sm.stopSession(sessionId).catch((err) => {
18680
+ send({ type: "error", error: err instanceof Error ? err.message : String(err) });
18681
+ });
18682
+ return;
18683
+ }
18684
+ const text5 = typeof msg.inputText === "string" ? msg.inputText.trim() : "";
18685
+ if (!text5) {
18686
+ return;
18687
+ }
18688
+ if (text5.startsWith("/")) {
18689
+ void (async () => {
18690
+ try {
18691
+ await ensureOnce();
18692
+ const spaceIdx = text5.indexOf(" ");
18693
+ const command = spaceIdx === -1 ? text5 : text5.slice(0, spaceIdx);
18694
+ const arg = spaceIdx === -1 ? "" : text5.slice(spaceIdx + 1).trim();
18695
+ const ctx = {
18696
+ sm,
18697
+ userId: runtimeSessionId,
18698
+ sessionPrefix: "agentcore_",
18699
+ accessLevel: "full",
18700
+ channelLabel: "AgentCore Runtime",
18701
+ activeOverrides,
18702
+ workspacePath: userWs,
18703
+ lang: "en"
18704
+ };
18705
+ const cmdResult = await dispatchCommand(ctx, command, arg, { channelName: "agentcore" });
18706
+ if (cmdResult) {
18707
+ send({ type: "stream", text: cmdResult.text });
18708
+ if (cmdResult.switchTo) {
18709
+ unsubscribe?.();
18710
+ unsubscribe = null;
18711
+ sessionId = cmdResult.switchTo;
18712
+ attachListener();
18713
+ send({ type: "switch" });
18714
+ send({ type: "history", ...buildHistory(sm, sessionId) });
18715
+ }
18716
+ if (cmdResult.startedTurn && cmdResult.sessionId) {
18717
+ if (cmdResult.sessionId !== sessionId) {
18718
+ unsubscribe?.();
18719
+ unsubscribe = null;
18720
+ sessionId = cmdResult.sessionId;
18721
+ attachListener();
18722
+ }
18723
+ } else {
18724
+ send({ type: "complete" });
18725
+ }
18726
+ } else {
18727
+ send({ type: "stream", text: `Unknown command: ${command}` });
18728
+ send({ type: "complete" });
18729
+ }
18730
+ } catch (err) {
18731
+ send({ type: "error", error: err instanceof Error ? err.message : String(err) });
18732
+ }
18733
+ })();
18734
+ return;
18735
+ }
18736
+ void (async () => {
18737
+ try {
18738
+ await ensureOnce();
18739
+ sm.appendUserMessage(sessionId, text5);
18740
+ const result = await sm.sendUserMessage(sessionId, `[channel: agentcore]
18741
+
18742
+ ${text5}`);
18743
+ if (result === "queued")
18744
+ send({ type: "queued" });
18745
+ } catch (err) {
18746
+ send({ type: "error", error: err instanceof Error ? err.message : String(err) });
18747
+ }
18748
+ })();
18749
+ });
18750
+ ws.on("close", () => {
18751
+ unsubscribe?.();
18752
+ unsubscribe = null;
18753
+ });
18754
+ console.log(`[AgentCore] ws connected \u2014 session ${sessionId}`);
18755
+ });
18756
+ }
18757
+ var SESSION_HEADER, activeOverrides;
18758
+ var init_agentcore = __esm({
18759
+ "packages/server/dist/routes/agentcore.js"() {
18760
+ "use strict";
18761
+ init_commands2();
18762
+ init_ui_log_builder();
18763
+ init_session_types();
18764
+ SESSION_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id";
18765
+ activeOverrides = /* @__PURE__ */ new Map();
18766
+ }
18767
+ });
18768
+
18769
+ // packages/server/dist/ws/file-watcher.js
18770
+ import fs41 from "node:fs";
18771
+ import path45 from "node:path";
18227
18772
  function isUnwatchablePath(absPath) {
18228
- const norm = path44.resolve(absPath);
18229
- if (norm === "/" || norm === path44.parse(norm).root)
18773
+ const norm = path45.resolve(absPath);
18774
+ if (norm === "/" || norm === path45.parse(norm).root)
18230
18775
  return "filesystem root";
18231
18776
  const banned = ["/System", "/Library", "/private", "/usr", "/bin", "/sbin", "/etc"];
18232
18777
  for (const b of banned) {
18233
- if (norm === b || norm.startsWith(b + path44.sep))
18778
+ if (norm === b || norm.startsWith(b + path45.sep))
18234
18779
  return `system path ${b}`;
18235
18780
  }
18236
18781
  return null;
@@ -18345,10 +18890,10 @@ var init_file_watcher = __esm({
18345
18890
  const root = this.workspaceRoot;
18346
18891
  if (!root)
18347
18892
  return;
18348
- const raw = path44.relative(root, absPath);
18893
+ const raw = path45.relative(root, absPath);
18349
18894
  if (!raw || raw.startsWith(".."))
18350
18895
  return;
18351
- const rel = raw.split(path44.sep).join("/");
18896
+ const rel = raw.split(path45.sep).join("/");
18352
18897
  let action;
18353
18898
  if (type === "delete") {
18354
18899
  action = "unlink";
@@ -18357,7 +18902,7 @@ var init_file_watcher = __esm({
18357
18902
  } else {
18358
18903
  let isDir = false;
18359
18904
  try {
18360
- isDir = fs40.statSync(absPath).isDirectory();
18905
+ isDir = fs41.statSync(absPath).isDirectory();
18361
18906
  } catch {
18362
18907
  }
18363
18908
  action = isDir ? "addDir" : "add";
@@ -18427,8 +18972,8 @@ var init_file_watcher = __esm({
18427
18972
  });
18428
18973
 
18429
18974
  // packages/server/dist/ws/git-dir-watcher.js
18430
- import fs41 from "node:fs";
18431
- import path45 from "node:path";
18975
+ import fs42 from "node:fs";
18976
+ import path46 from "node:path";
18432
18977
  var GitDirWatcher;
18433
18978
  var init_git_dir_watcher = __esm({
18434
18979
  "packages/server/dist/ws/git-dir-watcher.js"() {
@@ -18452,8 +18997,8 @@ var init_git_dir_watcher = __esm({
18452
18997
  return;
18453
18998
  this.stop();
18454
18999
  this.workspaceRoot = workspaceRoot;
18455
- const gitDir = path45.join(workspaceRoot, ".git");
18456
- if (fs41.existsSync(gitDir)) {
19000
+ const gitDir = path46.join(workspaceRoot, ".git");
19001
+ if (fs42.existsSync(gitDir)) {
18457
19002
  this.watchGitDir(gitDir);
18458
19003
  } else {
18459
19004
  this.watchParentForGit(workspaceRoot);
@@ -18462,7 +19007,7 @@ var init_git_dir_watcher = __esm({
18462
19007
  /** Watch the `.git` dir for index/HEAD changes (steady state). */
18463
19008
  watchGitDir(gitDir) {
18464
19009
  try {
18465
- this.watcher = fs41.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
19010
+ this.watcher = fs42.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
18466
19011
  this.handleGitEvent(filename);
18467
19012
  });
18468
19013
  this.mode = "git";
@@ -18476,7 +19021,7 @@ var init_git_dir_watcher = __esm({
18476
19021
  * Exists solely during the non-repo window; upgraded away on first `.git`. */
18477
19022
  watchParentForGit(workspaceRoot) {
18478
19023
  try {
18479
- this.watcher = fs41.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
19024
+ this.watcher = fs42.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
18480
19025
  this.handleParentEvent(filename);
18481
19026
  });
18482
19027
  this.mode = "parent";
@@ -18507,8 +19052,8 @@ var init_git_dir_watcher = __esm({
18507
19052
  const root = this.workspaceRoot;
18508
19053
  if (!root)
18509
19054
  return;
18510
- const gitDir = path45.join(root, ".git");
18511
- if (!fs41.existsSync(gitDir))
19055
+ const gitDir = path46.join(root, ".git");
19056
+ if (!fs42.existsSync(gitDir))
18512
19057
  return;
18513
19058
  this.closeWatcher();
18514
19059
  this.watchGitDir(gitDir);
@@ -18646,7 +19191,7 @@ var init_event_processor = __esm({
18646
19191
  });
18647
19192
 
18648
19193
  // packages/server/dist/ws/terminal-manager.js
18649
- import { homedir as homedir21 } from "node:os";
19194
+ import { homedir as homedir22 } from "node:os";
18650
19195
  import { basename } from "node:path";
18651
19196
  import { createRequire } from "node:module";
18652
19197
  function getPty() {
@@ -18702,7 +19247,7 @@ var init_terminal_manager = __esm({
18702
19247
  clearTimeout(existing.graceTimer);
18703
19248
  terminals.delete(terminalId);
18704
19249
  }
18705
- const cwd = opts.cwd === "~" || !opts.cwd ? homedir21() : opts.cwd;
19250
+ const cwd = opts.cwd === "~" || !opts.cwd ? homedir22() : opts.cwd;
18706
19251
  const cols = opts.cols ?? 80;
18707
19252
  const rows = opts.rows ?? 24;
18708
19253
  const shell = process.env.SHELL ?? (process.platform === "win32" ? process.env.ComSpec || "powershell.exe" : "/bin/bash");
@@ -18810,12 +19355,12 @@ var init_terminal_manager = __esm({
18810
19355
  });
18811
19356
 
18812
19357
  // packages/server/dist/ws/handler.js
18813
- import path46 from "node:path";
19358
+ import path47 from "node:path";
18814
19359
  function setupWebSocketHandler(deps) {
18815
19360
  const { wss: wss2, registry: registry2 } = deps;
18816
19361
  const clients = /* @__PURE__ */ new Set();
18817
19362
  function resolveProjectPath(projectId) {
18818
- if (path46.isAbsolute(projectId))
19363
+ if (path47.isAbsolute(projectId))
18819
19364
  return projectId;
18820
19365
  return null;
18821
19366
  }
@@ -19337,11 +19882,11 @@ var init_handler = __esm({
19337
19882
  import Database5 from "better-sqlite3";
19338
19883
  import { drizzle as drizzle4 } from "drizzle-orm/better-sqlite3";
19339
19884
  import { sqliteTable as sqliteTable4, text as text4, integer as integer4 } from "drizzle-orm/sqlite-core";
19340
- import path47 from "node:path";
19341
- import fs42 from "node:fs";
19885
+ import path48 from "node:path";
19886
+ import fs43 from "node:fs";
19342
19887
  function createCronDb(globalDir2) {
19343
- fs42.mkdirSync(globalDir2, { recursive: true });
19344
- const dbPath = path47.join(globalDir2, "cron.db");
19888
+ fs43.mkdirSync(globalDir2, { recursive: true });
19889
+ const dbPath = path48.join(globalDir2, "cron.db");
19345
19890
  const sqlite = new Database5(dbPath);
19346
19891
  sqlite.pragma("journal_mode = WAL");
19347
19892
  sqlite.exec(CREATE_SQL3);
@@ -19510,11 +20055,24 @@ var init_dispatcher = __esm({
19510
20055
  }
19511
20056
  });
19512
20057
 
20058
+ // packages/server/dist/child-env.js
20059
+ function cleanChildEnv() {
20060
+ const env = { ...process.env };
20061
+ delete env.HALO_PASSWORD;
20062
+ delete env.HALO_JWT_SECRET;
20063
+ return env;
20064
+ }
20065
+ var init_child_env = __esm({
20066
+ "packages/server/dist/child-env.js"() {
20067
+ "use strict";
20068
+ }
20069
+ });
20070
+
19513
20071
  // packages/server/dist/cron/runner.js
19514
20072
  import { spawn as spawn2 } from "node:child_process";
19515
- import fs43 from "node:fs";
19516
- import path48 from "node:path";
19517
- import { homedir as homedir22 } from "node:os";
20073
+ import fs44 from "node:fs";
20074
+ import path49 from "node:path";
20075
+ import { homedir as homedir23 } from "node:os";
19518
20076
  import { Cron } from "croner";
19519
20077
  import { eq as eq10, lt as lt4 } from "drizzle-orm";
19520
20078
  function resolveHaloCli() {
@@ -19523,16 +20081,16 @@ function resolveHaloCli() {
19523
20081
  return process.platform === "win32" ? "halo.cmd" : "halo";
19524
20082
  }
19525
20083
  function logsDir() {
19526
- return path48.join(homedir22(), ".halo", "global", "logs", "cron");
20084
+ return path49.join(homedir23(), ".halo", "global", "logs", "cron");
19527
20085
  }
19528
20086
  function logPathFor(runId) {
19529
- return path48.join(logsDir(), `${runId}.log`);
20087
+ return path49.join(logsDir(), `${runId}.log`);
19530
20088
  }
19531
20089
  function newRunId() {
19532
20090
  return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`;
19533
20091
  }
19534
20092
  function startCronDaemon() {
19535
- fs43.mkdirSync(logsDir(), { recursive: true });
20093
+ fs44.mkdirSync(logsDir(), { recursive: true });
19536
20094
  reloadAll();
19537
20095
  setInterval(pruneOldLogs, DAY_MS2);
19538
20096
  setInterval(reconcileFromDb, 1e4);
@@ -19677,7 +20235,7 @@ async function runJob(jobId, triggerKind) {
19677
20235
  const runId = newRunId();
19678
20236
  const startedAt = Date.now();
19679
20237
  const logPath = logPathFor(runId);
19680
- fs43.mkdirSync(logsDir(), { recursive: true });
20238
+ fs44.mkdirSync(logsDir(), { recursive: true });
19681
20239
  if (_inflight.has(jobId)) {
19682
20240
  db.insert(cronRuns).values({
19683
20241
  id: runId,
@@ -19712,7 +20270,7 @@ async function runJob(jobId, triggerKind) {
19712
20270
  }).run();
19713
20271
  broadcast({ type: "cron:run_changed", jobId, runId, status: "running" });
19714
20272
  const targets = parseTargets(job.targets);
19715
- if (!fs43.existsSync(job.workspacePath)) {
20273
+ if (!fs44.existsSync(job.workspacePath)) {
19716
20274
  finalize(runId, jobId, {
19717
20275
  status: "failed",
19718
20276
  output: null,
@@ -19726,7 +20284,7 @@ async function runJob(jobId, triggerKind) {
19726
20284
  const sessionId = `cron-${job.id}`;
19727
20285
  const args = ["cli", "-a", job.agentId, "-s", sessionId, "-w", job.workspacePath];
19728
20286
  const [spawnBin, spawnArgs] = process.platform === "win32" && cli.endsWith(".cmd") ? ["cmd.exe", ["/c", cli, ...args]] : [cli, args];
19729
- const teeStream = fs43.createWriteStream(logPath, { flags: "a" });
20287
+ const teeStream = fs44.createWriteStream(logPath, { flags: "a" });
19730
20288
  teeStream.write(`=== ${(/* @__PURE__ */ new Date()).toISOString()} cron run ${runId} job=${jobId} trigger=${triggerKind} ===
19731
20289
  `);
19732
20290
  teeStream.write(`agent=${job.agentId} workspace=${job.workspacePath} schedule=${job.schedule}
@@ -19739,7 +20297,7 @@ async function runJob(jobId, triggerKind) {
19739
20297
  const result = await new Promise((resolve2) => {
19740
20298
  const child = spawn2(spawnBin, spawnArgs, {
19741
20299
  stdio: ["pipe", "pipe", "pipe"],
19742
- env: process.env,
20300
+ env: cleanChildEnv(),
19743
20301
  // Suppress the console window the `cmd.exe /c` wrapper would otherwise
19744
20302
  // pop up on Windows (CREATE_NO_WINDOW). No-op on macOS/Linux.
19745
20303
  windowsHide: true
@@ -19879,15 +20437,15 @@ function pruneOldRuns(jobId) {
19879
20437
  }
19880
20438
  function pruneOldLogs() {
19881
20439
  const dir = logsDir();
19882
- if (!fs43.existsSync(dir))
20440
+ if (!fs44.existsSync(dir))
19883
20441
  return;
19884
20442
  const cutoff = Date.now() - LOG_RETENTION_DAYS * DAY_MS2;
19885
- for (const name of fs43.readdirSync(dir)) {
19886
- const p = path48.join(dir, name);
20443
+ for (const name of fs44.readdirSync(dir)) {
20444
+ const p = path49.join(dir, name);
19887
20445
  try {
19888
- const stat = fs43.statSync(p);
20446
+ const stat = fs44.statSync(p);
19889
20447
  if (stat.mtimeMs < cutoff)
19890
- fs43.rmSync(p, { force: true });
20448
+ fs44.rmSync(p, { force: true });
19891
20449
  } catch {
19892
20450
  }
19893
20451
  }
@@ -19902,6 +20460,7 @@ var init_runner = __esm({
19902
20460
  init_cron_db();
19903
20461
  init_dispatcher();
19904
20462
  init_broadcast();
20463
+ init_child_env();
19905
20464
  DAY_MS2 = 24 * 60 * 60 * 1e3;
19906
20465
  CLI_TIMEOUT_SEC = 3600;
19907
20466
  RUNS_PER_JOB_KEEP = 100;
@@ -19914,9 +20473,9 @@ var init_runner = __esm({
19914
20473
  });
19915
20474
 
19916
20475
  // packages/server/dist/routes/cron.js
19917
- import { Hono as Hono11 } from "hono";
20476
+ import { Hono as Hono12 } from "hono";
19918
20477
  import { eq as eq11, desc as desc4, lt as lt5, and as and7 } from "drizzle-orm";
19919
- import fs44 from "node:fs/promises";
20478
+ import fs45 from "node:fs/promises";
19920
20479
  import { Cron as Cron2 } from "croner";
19921
20480
  function newJobId() {
19922
20481
  return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -19930,7 +20489,7 @@ function validateSchedule(expr) {
19930
20489
  }
19931
20490
  }
19932
20491
  function createCronRoutes() {
19933
- const router = new Hono11();
20492
+ const router = new Hono12();
19934
20493
  router.get("/cron/jobs", async (c) => {
19935
20494
  const db = getCronDb();
19936
20495
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "20", 10) || 20, 1), 100);
@@ -20093,7 +20652,7 @@ function createCronRoutes() {
20093
20652
  return c.json({ log: null });
20094
20653
  let log = null;
20095
20654
  try {
20096
- log = await fs44.readFile(row.logPath, "utf-8");
20655
+ log = await fs45.readFile(row.logPath, "utf-8");
20097
20656
  } catch {
20098
20657
  }
20099
20658
  return c.json({ log });
@@ -20389,20 +20948,21 @@ var init_ticker = __esm({
20389
20948
  });
20390
20949
 
20391
20950
  // packages/server/dist/evolution/spawn.js
20392
- import path49 from "node:path";
20951
+ import path50 from "node:path";
20393
20952
  import { spawn as spawn3 } from "node:child_process";
20394
20953
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20395
20954
  var __dirname2, WRAPPER_PATH, realEvoSpawner;
20396
20955
  var init_spawn = __esm({
20397
20956
  "packages/server/dist/evolution/spawn.js"() {
20398
20957
  "use strict";
20399
- __dirname2 = path49.dirname(fileURLToPath2(import.meta.url));
20400
- WRAPPER_PATH = path49.resolve(__dirname2, "evo-wrapper.js");
20958
+ init_child_env();
20959
+ __dirname2 = path50.dirname(fileURLToPath2(import.meta.url));
20960
+ WRAPPER_PATH = path50.resolve(__dirname2, "evo-wrapper.js");
20401
20961
  realEvoSpawner = (mode, id) => {
20402
20962
  const child = spawn3(process.execPath, [WRAPPER_PATH, `--mode=${mode}`, `--id=${id}`], {
20403
20963
  detached: true,
20404
20964
  stdio: "ignore",
20405
- env: process.env,
20965
+ env: cleanChildEnv(),
20406
20966
  // Without this, a detached console process on Windows gets a freshly
20407
20967
  // allocated console window (a black box pops up for every evo/score/apply
20408
20968
  // run). cron doesn't hit this — it spawns the cli inline, not via a
@@ -20643,7 +21203,7 @@ var init_session_prefix = __esm({
20643
21203
 
20644
21204
  // packages/server/dist/channels/telegram/handler.js
20645
21205
  import { Bot, InputFile } from "grammy";
20646
- import path50 from "node:path";
21206
+ import path51 from "node:path";
20647
21207
  function inferMediaKind(filePath) {
20648
21208
  const cls = classifyMedia(filePath);
20649
21209
  if (cls === "image")
@@ -20694,7 +21254,7 @@ function startTelegramChannel(deps) {
20694
21254
  }
20695
21255
  const abort = new AbortController();
20696
21256
  const unsubscribers = /* @__PURE__ */ new Map();
20697
- const activeOverrides = /* @__PURE__ */ new Map();
21257
+ const activeOverrides2 = /* @__PURE__ */ new Map();
20698
21258
  const restartSelf = () => {
20699
21259
  queueMicrotask(() => {
20700
21260
  void stopAccount(accountId).then(() => {
@@ -20703,8 +21263,8 @@ function startTelegramChannel(deps) {
20703
21263
  });
20704
21264
  };
20705
21265
  const bot = new Bot(account.botToken);
20706
- const promise = runBot({ registry: registry2, db, account, bot, abort, unsubscribers, activeOverrides, restartSelf }).catch((err) => console.log(`[telegram] account ${accountId} bot crashed: ${String(err)}`));
20707
- runners.set(accountId, { accountId, bot, abort, promise, unsubscribers, activeOverrides });
21266
+ const promise = runBot({ registry: registry2, db, account, bot, abort, unsubscribers, activeOverrides: activeOverrides2, restartSelf }).catch((err) => console.log(`[telegram] account ${accountId} bot crashed: ${String(err)}`));
21267
+ runners.set(accountId, { accountId, bot, abort, promise, unsubscribers, activeOverrides: activeOverrides2 });
20708
21268
  console.log(`[telegram] account ${accountId} started (@${account.botUsername}, workspace=${account.workspacePath})`);
20709
21269
  }
20710
21270
  async function stopAccount(accountId) {
@@ -20729,7 +21289,7 @@ function startTelegramChannel(deps) {
20729
21289
  return { startAccount, stopAccount, stopAll };
20730
21290
  }
20731
21291
  async function runBot(args) {
20732
- const { registry: registry2, db, account, bot, unsubscribers, activeOverrides, restartSelf } = args;
21292
+ const { registry: registry2, db, account, bot, unsubscribers, activeOverrides: activeOverrides2, restartSelf } = args;
20733
21293
  bot.catch((err) => {
20734
21294
  console.log(`[telegram] ${account.accountId} bot error: ${String(err)}`);
20735
21295
  });
@@ -20744,7 +21304,7 @@ async function runBot(args) {
20744
21304
  sessionPrefix: buildTgSessionPrefix(userId),
20745
21305
  accessLevel: account.accessLevel,
20746
21306
  channelLabel: `Telegram: ${userId}`,
20747
- activeOverrides,
21307
+ activeOverrides: activeOverrides2,
20748
21308
  workspacePath: account.workspacePath,
20749
21309
  lang,
20750
21310
  channel: {
@@ -20805,7 +21365,7 @@ async function runBot(args) {
20805
21365
  }
20806
21366
  }
20807
21367
  }
20808
- await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, lang, unsubscribers, activeOverrides });
21368
+ await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, lang, unsubscribers, activeOverrides: activeOverrides2 });
20809
21369
  });
20810
21370
  bot.on("message:photo", async (ctx) => {
20811
21371
  const userId = ctx.from.id;
@@ -20841,7 +21401,7 @@ async function runBot(args) {
20841
21401
  }
20842
21402
  const text5 = caption ? `${caption}
20843
21403
  ${imageNote}` : imageNote;
20844
- await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, images, lang, unsubscribers, activeOverrides });
21404
+ await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, images, lang, unsubscribers, activeOverrides: activeOverrides2 });
20845
21405
  });
20846
21406
  bot.on("message:document", async (ctx) => {
20847
21407
  const userId = ctx.from.id;
@@ -20865,7 +21425,7 @@ ${imageNote}` : imageNote;
20865
21425
  }
20866
21426
  const text5 = caption ? `${caption}
20867
21427
  ${fileNote}` : fileNote;
20868
- await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, lang, unsubscribers, activeOverrides });
21428
+ await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, lang, unsubscribers, activeOverrides: activeOverrides2 });
20869
21429
  });
20870
21430
  bot.on("message:voice", async (ctx) => {
20871
21431
  const userId = ctx.from.id;
@@ -20885,7 +21445,7 @@ ${fileNote}` : fileNote;
20885
21445
  }
20886
21446
  } catch {
20887
21447
  }
20888
- await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: voiceNote, lang, unsubscribers, activeOverrides });
21448
+ await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: voiceNote, lang, unsubscribers, activeOverrides: activeOverrides2 });
20889
21449
  });
20890
21450
  bot.on("message:video_note", async (ctx) => {
20891
21451
  const userId = ctx.from.id;
@@ -20904,7 +21464,7 @@ ${fileNote}` : fileNote;
20904
21464
  }
20905
21465
  } catch {
20906
21466
  }
20907
- await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: vnNote, lang, unsubscribers, activeOverrides });
21467
+ await handleUserMessage({ registry: registry2, db, account, bot, ctx, userId, workspace, text: vnNote, lang, unsubscribers, activeOverrides: activeOverrides2 });
20908
21468
  });
20909
21469
  console.log(`[telegram] ${account.accountId} starting long-poll\u2026`);
20910
21470
  await bot.start({
@@ -20913,12 +21473,12 @@ ${fileNote}` : fileNote;
20913
21473
  });
20914
21474
  }
20915
21475
  async function handleUserMessage(args) {
20916
- const { registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, images, lang, unsubscribers, activeOverrides } = args;
21476
+ const { registry: registry2, db, account, bot, ctx, userId, workspace, text: text5, images, lang, unsubscribers, activeOverrides: activeOverrides2 } = args;
20917
21477
  const chatId = ctx.chat.id;
20918
21478
  rememberLastActiveChat(db, account.accountId, String(chatId));
20919
21479
  const sm = registry2.getOrCreate(workspace);
20920
21480
  const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
20921
- const sessionId = await getOrCreateActiveSession(sm, userId, activeOverrides, accessLevel, workspace);
21481
+ const sessionId = await getOrCreateActiveSession(sm, userId, activeOverrides2, accessLevel, workspace);
20922
21482
  if (sm.isSessionCompacting(sessionId)) {
20923
21483
  await ctx.reply(t("handler.compacting", lang));
20924
21484
  return;
@@ -20932,9 +21492,9 @@ async function handleUserMessage(args) {
20932
21492
  await bot.api.sendMessage(chatId, chunk, { parse_mode: void 0 });
20933
21493
  },
20934
21494
  sendMedia: async (filePath) => {
20935
- const resolved = path50.resolve(filePath);
20936
- const ws = path50.resolve(workspace);
20937
- const inWorkspace = resolved === ws || resolved.startsWith(ws + path50.sep);
21495
+ const resolved = path51.resolve(filePath);
21496
+ const ws = path51.resolve(workspace);
21497
+ const inWorkspace = resolved === ws || resolved.startsWith(ws + path51.sep);
20938
21498
  if (!inWorkspace && !isInTempDir(resolved)) {
20939
21499
  console.log(`[telegram] sendMedia blocked: ${filePath} not under workspace`);
20940
21500
  return;
@@ -20971,11 +21531,11 @@ ${text5}`;
20971
21531
  console.log(`[telegram] sendUserMessage ${sessionId}: ${String(err)}`);
20972
21532
  });
20973
21533
  }
20974
- function findActiveTgSession(sm, userId, activeOverrides, accessLevel) {
20975
- return findActiveSessionId(sm, String(userId), buildTgSessionPrefix(userId), activeOverrides, accessLevel);
21534
+ function findActiveTgSession(sm, userId, activeOverrides2, accessLevel) {
21535
+ return findActiveSessionId(sm, String(userId), buildTgSessionPrefix(userId), activeOverrides2, accessLevel);
20976
21536
  }
20977
- async function getOrCreateActiveSession(sm, userId, activeOverrides, accessLevel, workspacePath) {
20978
- const existing = findActiveTgSession(sm, userId, activeOverrides, accessLevel === null ? "full" : accessLevel);
21537
+ async function getOrCreateActiveSession(sm, userId, activeOverrides2, accessLevel, workspacePath) {
21538
+ const existing = findActiveTgSession(sm, userId, activeOverrides2, accessLevel === null ? "full" : accessLevel);
20979
21539
  if (existing)
20980
21540
  return existing;
20981
21541
  const newId = `${buildTgSessionPrefix(userId)}${Date.now().toString(36)}`;
@@ -20999,20 +21559,20 @@ var init_handler2 = __esm({
20999
21559
  });
21000
21560
 
21001
21561
  // packages/server/dist/routes/telegram.js
21002
- import { Hono as Hono12 } from "hono";
21003
- import path51 from "node:path";
21562
+ import { Hono as Hono13 } from "hono";
21563
+ import path52 from "node:path";
21004
21564
  import { Bot as Bot2 } from "grammy";
21005
- import fs45 from "node:fs";
21565
+ import fs46 from "node:fs";
21006
21566
  function createTelegramRoutes(deps) {
21007
21567
  const { db, channel } = deps;
21008
- const app2 = new Hono12();
21568
+ const app2 = new Hono13();
21009
21569
  app2.get("/telegram/accounts", (c) => {
21010
21570
  const accounts = listAccounts3(db).map((acc) => {
21011
21571
  return {
21012
21572
  accountId: acc.accountId,
21013
21573
  botUsername: acc.botUsername,
21014
21574
  workspacePath: acc.workspacePath,
21015
- workspaceMissing: !fs45.existsSync(acc.workspacePath),
21575
+ workspaceMissing: !fs46.existsSync(acc.workspacePath),
21016
21576
  label: acc.label,
21017
21577
  enabled: acc.enabled,
21018
21578
  accessLevel: acc.accessLevel,
@@ -21030,9 +21590,9 @@ function createTelegramRoutes(deps) {
21030
21590
  return c.json({ error: "botToken required" }, 400);
21031
21591
  if (!body.workspacePath)
21032
21592
  return c.json({ error: "workspacePath required" }, 400);
21033
- if (!path51.isAbsolute(body.workspacePath))
21593
+ if (!path52.isAbsolute(body.workspacePath))
21034
21594
  return c.json({ error: "workspacePath must be absolute" }, 400);
21035
- if (!fs45.existsSync(body.workspacePath))
21595
+ if (!fs46.existsSync(body.workspacePath))
21036
21596
  return c.json({ error: "workspace path not found" }, 400);
21037
21597
  ensureWorkspaceHalo(body.workspacePath);
21038
21598
  let botUsername;
@@ -21091,7 +21651,7 @@ function createTelegramRoutes(deps) {
21091
21651
  if (body.enabled !== void 0)
21092
21652
  patch.enabled = body.enabled ? 1 : 0;
21093
21653
  if (body.workspacePath) {
21094
- if (!fs45.existsSync(body.workspacePath))
21654
+ if (!fs46.existsSync(body.workspacePath))
21095
21655
  return c.json({ error: "workspace path not found" }, 400);
21096
21656
  ensureWorkspaceHalo(body.workspacePath);
21097
21657
  patch.workspacePath = body.workspacePath;
@@ -21737,8 +22297,8 @@ var init_cdn = __esm({
21737
22297
  });
21738
22298
 
21739
22299
  // packages/server/dist/channels/wechat/send-media.js
21740
- import fs46 from "node:fs/promises";
21741
- import path52 from "node:path";
22300
+ import fs47 from "node:fs/promises";
22301
+ import path53 from "node:path";
21742
22302
  function inferKind(filePath) {
21743
22303
  const cls = classifyMedia(filePath);
21744
22304
  if (cls === "image")
@@ -21750,7 +22310,7 @@ function inferKind(filePath) {
21750
22310
  async function sendMediaFile(params) {
21751
22311
  const { baseUrl, token, toUserId, contextToken, filePath } = params;
21752
22312
  const kind = params.kind ?? inferKind(filePath);
21753
- const plaintext = await fs46.readFile(filePath);
22313
+ const plaintext = await fs47.readFile(filePath);
21754
22314
  const prep = prepareUpload(plaintext);
21755
22315
  console.log(`[weixin] sendMediaFile: path=${filePath} kind=${kind} rawsize=${prep.rawsize} filesize=${prep.filesize}`);
21756
22316
  const mediaType = kind === "image" ? UploadMediaType.IMAGE : kind === "video" ? UploadMediaType.VIDEO : UploadMediaType.FILE;
@@ -21788,7 +22348,7 @@ async function sendMediaFile(params) {
21788
22348
  aesKeyBase64,
21789
22349
  fileSizeCiphertext: prep.filesize,
21790
22350
  fileSize: prep.rawsize,
21791
- fileName: path52.basename(filePath)
22351
+ fileName: path53.basename(filePath)
21792
22352
  });
21793
22353
  const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
21794
22354
  const body = {
@@ -21951,12 +22511,12 @@ var init_login = __esm({
21951
22511
  });
21952
22512
 
21953
22513
  // packages/server/dist/channels/wechat/handler.js
21954
- import fs47 from "node:fs";
21955
- import path53 from "node:path";
22514
+ import fs48 from "node:fs";
22515
+ import path54 from "node:path";
21956
22516
  import QRCode from "qrcode";
21957
22517
  function isPathAllowed(filePath, workspacePath) {
21958
- const resolved = path53.resolve(filePath);
21959
- return resolved.startsWith(path53.resolve(workspacePath)) || isInTempDir(resolved);
22518
+ const resolved = path54.resolve(filePath);
22519
+ return resolved.startsWith(path54.resolve(workspacePath)) || isInTempDir(resolved);
21960
22520
  }
21961
22521
  function startWeixinChannel(deps) {
21962
22522
  const { registry: registry2, db } = deps;
@@ -21977,7 +22537,7 @@ function startWeixinChannel(deps) {
21977
22537
  }
21978
22538
  const abort = new AbortController();
21979
22539
  const unsubscribers = /* @__PURE__ */ new Map();
21980
- const activeOverrides = /* @__PURE__ */ new Map();
22540
+ const activeOverrides2 = /* @__PURE__ */ new Map();
21981
22541
  const sessionContextTokens = /* @__PURE__ */ new Map();
21982
22542
  const restartSelf = () => {
21983
22543
  queueMicrotask(() => {
@@ -21986,8 +22546,8 @@ function startWeixinChannel(deps) {
21986
22546
  });
21987
22547
  });
21988
22548
  };
21989
- const promise = runAccountLoop({ registry: registry2, db, account, abort: abort.signal, unsubscribers, activeOverrides, sessionContextTokens, restartSelf, startNewAccount: startAccount }).catch((err) => console.log(`[weixin] account ${accountId} loop crashed: ${String(err)}`));
21990
- runners.set(accountId, { accountId, abort, promise, unsubscribers, activeOverrides });
22549
+ const promise = runAccountLoop({ registry: registry2, db, account, abort: abort.signal, unsubscribers, activeOverrides: activeOverrides2, sessionContextTokens, restartSelf, startNewAccount: startAccount }).catch((err) => console.log(`[weixin] account ${accountId} loop crashed: ${String(err)}`));
22550
+ runners.set(accountId, { accountId, abort, promise, unsubscribers, activeOverrides: activeOverrides2 });
21991
22551
  console.log(`[weixin] account ${accountId} started (workspace=${account.workspacePath})`);
21992
22552
  }
21993
22553
  async function stopAccount(accountId) {
@@ -22019,7 +22579,7 @@ function startWeixinChannel(deps) {
22019
22579
  return { startAccount, stopAccount, stopAll };
22020
22580
  }
22021
22581
  async function runAccountLoop(args) {
22022
- const { registry: registry2, db, account, abort, unsubscribers, activeOverrides, sessionContextTokens, restartSelf, startNewAccount } = args;
22582
+ const { registry: registry2, db, account, abort, unsubscribers, activeOverrides: activeOverrides2, sessionContextTokens, restartSelf, startNewAccount } = args;
22023
22583
  try {
22024
22584
  await notifyStart({ baseUrl: account.baseUrl, token: account.botToken });
22025
22585
  } catch (err) {
@@ -22058,7 +22618,7 @@ async function runAccountLoop(args) {
22058
22618
  saveSyncBuf(db, account.accountId, getUpdatesBuf);
22059
22619
  }
22060
22620
  for (const msg of resp.msgs ?? []) {
22061
- await handleInbound({ registry: registry2, db, account, msg, unsubscribers, activeOverrides, sessionContextTokens, restartSelf, startNewAccount });
22621
+ await handleInbound({ registry: registry2, db, account, msg, unsubscribers, activeOverrides: activeOverrides2, sessionContextTokens, restartSelf, startNewAccount });
22062
22622
  }
22063
22623
  } catch (err) {
22064
22624
  if (abort.aborted)
@@ -22181,11 +22741,11 @@ async function processItems(args) {
22181
22741
  function buildWxSessionPrefix(fromUserId) {
22182
22742
  return sessionPrefix("wx", normalizeAccountId(fromUserId));
22183
22743
  }
22184
- function findActiveWxSession(sm, fromUserId, activeOverrides, accessLevel) {
22185
- return findActiveSessionId(sm, fromUserId, buildWxSessionPrefix(fromUserId), activeOverrides, accessLevel);
22744
+ function findActiveWxSession(sm, fromUserId, activeOverrides2, accessLevel) {
22745
+ return findActiveSessionId(sm, fromUserId, buildWxSessionPrefix(fromUserId), activeOverrides2, accessLevel);
22186
22746
  }
22187
- async function getOrCreateActiveSession2(sm, fromUserId, activeOverrides, accessLevel, workspacePath) {
22188
- const existing = findActiveWxSession(sm, fromUserId, activeOverrides, accessLevel === null ? "full" : accessLevel);
22747
+ async function getOrCreateActiveSession2(sm, fromUserId, activeOverrides2, accessLevel, workspacePath) {
22748
+ const existing = findActiveWxSession(sm, fromUserId, activeOverrides2, accessLevel === null ? "full" : accessLevel);
22189
22749
  if (existing)
22190
22750
  return existing;
22191
22751
  const newId = `${buildWxSessionPrefix(fromUserId)}${Date.now().toString(36)}`;
@@ -22194,7 +22754,7 @@ async function getOrCreateActiveSession2(sm, fromUserId, activeOverrides, access
22194
22754
  return newId;
22195
22755
  }
22196
22756
  async function handleInbound(args) {
22197
- const { registry: registry2, db, account: storedAccount, msg, unsubscribers, activeOverrides, sessionContextTokens, restartSelf, startNewAccount } = args;
22757
+ const { registry: registry2, db, account: storedAccount, msg, unsubscribers, activeOverrides: activeOverrides2, sessionContextTokens, restartSelf, startNewAccount } = args;
22198
22758
  const fromUserId = msg.from_user_id ?? "";
22199
22759
  if (!fromUserId)
22200
22760
  return;
@@ -22228,7 +22788,7 @@ async function handleInbound(args) {
22228
22788
  restartSelf,
22229
22789
  startNewAccount,
22230
22790
  registry: registry2,
22231
- activeOverrides,
22791
+ activeOverrides: activeOverrides2,
22232
22792
  unsubscribers,
22233
22793
  sessionContextTokens,
22234
22794
  lang
@@ -22240,7 +22800,7 @@ async function handleInbound(args) {
22240
22800
  rememberLastActiveChat(db, account.accountId, fromUserId);
22241
22801
  const sm = registry2.getOrCreate(account.workspacePath);
22242
22802
  const sessionAccessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
22243
- const sessionId = await getOrCreateActiveSession2(sm, fromUserId, activeOverrides, sessionAccessLevel, account.workspacePath);
22803
+ const sessionId = await getOrCreateActiveSession2(sm, fromUserId, activeOverrides2, sessionAccessLevel, account.workspacePath);
22244
22804
  if (sm.isSessionCompacting(sessionId)) {
22245
22805
  await sendToUser({
22246
22806
  account,
@@ -22298,7 +22858,7 @@ ${userText}`;
22298
22858
  });
22299
22859
  }
22300
22860
  async function handleSlashCommand(args) {
22301
- const { text: text5, account, db, fromUserId, contextToken, restartSelf, startNewAccount, registry: registry2, activeOverrides, unsubscribers, sessionContextTokens, lang } = args;
22861
+ const { text: text5, account, db, fromUserId, contextToken, restartSelf, startNewAccount, registry: registry2, activeOverrides: activeOverrides2, unsubscribers, sessionContextTokens, lang } = args;
22302
22862
  const reply = (msg) => sendToUser({ account, toUserId: fromUserId, text: msg, contextToken });
22303
22863
  function ensureListener(sm2, sessionId) {
22304
22864
  if (unsubscribers.has(sessionId))
@@ -22340,7 +22900,7 @@ async function handleSlashCommand(args) {
22340
22900
  sessionPrefix: buildWxSessionPrefix(fromUserId),
22341
22901
  accessLevel: account.accessLevel,
22342
22902
  channelLabel: `WeChat: ${fromUserId}`,
22343
- activeOverrides,
22903
+ activeOverrides: activeOverrides2,
22344
22904
  workspacePath: account.workspacePath,
22345
22905
  lang,
22346
22906
  // WeChat's "chat id" for cron purposes is the openId of the user
@@ -22366,7 +22926,7 @@ async function handleSlashCommand(args) {
22366
22926
  restartSelf();
22367
22927
  } else {
22368
22928
  if (result.switchTo) {
22369
- const oldSid = findActiveSessionId(sm, fromUserId, ctx.sessionPrefix, activeOverrides, ctx.accessLevel);
22929
+ const oldSid = findActiveSessionId(sm, fromUserId, ctx.sessionPrefix, activeOverrides2, ctx.accessLevel);
22370
22930
  if (oldSid && oldSid !== result.switchTo) {
22371
22931
  unsubscribers.get(oldSid)?.();
22372
22932
  unsubscribers.delete(oldSid);
@@ -22393,7 +22953,7 @@ async function handleSlashCommand(args) {
22393
22953
  await reply(login.message);
22394
22954
  return true;
22395
22955
  }
22396
- const tmpPath = path53.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
22956
+ const tmpPath = path54.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
22397
22957
  await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
22398
22958
  await sendMediaFile({
22399
22959
  baseUrl: account.baseUrl,
@@ -22402,7 +22962,7 @@ async function handleSlashCommand(args) {
22402
22962
  contextToken,
22403
22963
  filePath: tmpPath
22404
22964
  });
22405
- await fs47.promises.unlink(tmpPath).catch(() => {
22965
+ await fs48.promises.unlink(tmpPath).catch(() => {
22406
22966
  });
22407
22967
  await reply(t("wechat.qr_sent", lang, { level, path: account.workspacePath }));
22408
22968
  waitLogin({ sessionKey: login.sessionKey }).then(async (result2) => {
@@ -22480,18 +23040,18 @@ var init_handler3 = __esm({
22480
23040
  });
22481
23041
 
22482
23042
  // packages/server/dist/routes/weixin.js
22483
- import { Hono as Hono13 } from "hono";
22484
- import path54 from "node:path";
22485
- import fs48 from "node:fs";
23043
+ import { Hono as Hono14 } from "hono";
23044
+ import path55 from "node:path";
23045
+ import fs49 from "node:fs";
22486
23046
  function createWeixinRoutes(deps) {
22487
23047
  const { db, channel } = deps;
22488
- const app2 = new Hono13();
23048
+ const app2 = new Hono14();
22489
23049
  app2.get("/weixin/accounts", (c) => {
22490
23050
  const accounts = listAccounts4(db).map((acc) => {
22491
23051
  return {
22492
23052
  ...stripSecrets(acc),
22493
23053
  workspacePath: acc.workspacePath,
22494
- workspaceMissing: !fs48.existsSync(acc.workspacePath)
23054
+ workspaceMissing: !fs49.existsSync(acc.workspacePath)
22495
23055
  };
22496
23056
  });
22497
23057
  return c.json({ accounts });
@@ -22507,14 +23067,14 @@ function createWeixinRoutes(deps) {
22507
23067
  return c.json({ error: "sessionKey required" }, 400);
22508
23068
  if (!body.workspacePath)
22509
23069
  return c.json({ error: "workspacePath required" }, 400);
22510
- if (!path54.isAbsolute(body.workspacePath))
23070
+ if (!path55.isAbsolute(body.workspacePath))
22511
23071
  return c.json({ error: "workspacePath must be absolute" }, 400);
22512
23072
  const accessLevel = body.accessLevel === "full" ? "full" : body.accessLevel === "workspace" ? "workspace" : "readonly";
22513
23073
  const result = await waitLogin({ sessionKey: body.sessionKey, timeoutMs: body.timeoutMs });
22514
23074
  if (!result.connected || !result.accountId || !result.botToken || !result.baseUrl) {
22515
23075
  return c.json({ connected: false, message: result.message }, 200);
22516
23076
  }
22517
- if (!fs48.existsSync(body.workspacePath))
23077
+ if (!fs49.existsSync(body.workspacePath))
22518
23078
  return c.json({ error: "workspace path not found" }, 400);
22519
23079
  ensureWorkspaceHalo(body.workspacePath);
22520
23080
  const normalized = normalizeAccountId(result.accountId);
@@ -22552,7 +23112,7 @@ function createWeixinRoutes(deps) {
22552
23112
  if (!existing)
22553
23113
  return c.json({ error: "not found" }, 404);
22554
23114
  const body = await c.req.json().catch(() => ({}));
22555
- if (body.workspacePath !== void 0 && !path54.isAbsolute(body.workspacePath)) {
23115
+ if (body.workspacePath !== void 0 && !path55.isAbsolute(body.workspacePath)) {
22556
23116
  return c.json({ error: "workspacePath must be absolute" }, 400);
22557
23117
  }
22558
23118
  if (body.accessLevel !== void 0 && body.accessLevel !== "full" && body.accessLevel !== "workspace" && body.accessLevel !== "readonly") {
@@ -22560,7 +23120,7 @@ function createWeixinRoutes(deps) {
22560
23120
  }
22561
23121
  const patch = { ...body };
22562
23122
  if (body.workspacePath !== void 0) {
22563
- if (!fs48.existsSync(body.workspacePath))
23123
+ if (!fs49.existsSync(body.workspacePath))
22564
23124
  return c.json({ error: "workspace path not found" }, 400);
22565
23125
  ensureWorkspaceHalo(body.workspacePath);
22566
23126
  patch.workspacePath = body.workspacePath;
@@ -22679,20 +23239,20 @@ function buildWebSessionPrefix(accountId) {
22679
23239
  }
22680
23240
  function createWebChannel(deps) {
22681
23241
  const { registry: registry2, db } = deps;
22682
- const activeOverrides = /* @__PURE__ */ new Map();
23242
+ const activeOverrides2 = /* @__PURE__ */ new Map();
22683
23243
  function getActiveSessionId(sm, accountId) {
22684
23244
  const prefix = buildWebSessionPrefix(accountId);
22685
- return findActiveSessionId(sm, accountId, prefix, activeOverrides, "full") ?? void 0;
23245
+ return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
22686
23246
  }
22687
23247
  function resolveWorkspace(account, override) {
22688
- let path62 = account.workspacePath;
23248
+ let path63 = account.workspacePath;
22689
23249
  if (override && override !== account.workspacePath) {
22690
23250
  if (account.accessLevel !== "full") {
22691
23251
  return { ok: false, error: "workspace override requires a full-access token" };
22692
23252
  }
22693
- path62 = override;
23253
+ path63 = override;
22694
23254
  }
22695
- const resolved = resolveAccountWorkspace({ ...account, workspacePath: path62 });
23255
+ const resolved = resolveAccountWorkspace({ ...account, workspacePath: path63 });
22696
23256
  if (!resolved)
22697
23257
  return { ok: false, error: "workspace not found" };
22698
23258
  return { ok: true, path: resolved };
@@ -22704,7 +23264,7 @@ function createWebChannel(deps) {
22704
23264
  sessionPrefix: buildWebSessionPrefix(account.accountId),
22705
23265
  accessLevel: account.accessLevel,
22706
23266
  channelLabel: `Web: ${account.label || account.accountId}`,
22707
- activeOverrides,
23267
+ activeOverrides: activeOverrides2,
22708
23268
  workspacePath: account.workspacePath,
22709
23269
  lang: getLang(account),
22710
23270
  // Web is SSE-only, no per-conversation chat id — but the channel
@@ -22805,7 +23365,7 @@ function createWebChannel(deps) {
22805
23365
  const agentId = opts?.agentId || await resolveDefaultAgentId(sm, workspace);
22806
23366
  await sm.createSession(agentId, null, `Web: ${account.label || account.accountId}`, void 0, sessionId, void 0, accessLevel);
22807
23367
  if (!opts?.sessionId)
22808
- activeOverrides.set(account.accountId, sessionId);
23368
+ activeOverrides2.set(account.accountId, sessionId);
22809
23369
  }
22810
23370
  yield sseData({ type: "session", sessionId });
22811
23371
  const queue = [];
@@ -23075,21 +23635,21 @@ var init_handler4 = __esm({
23075
23635
  });
23076
23636
 
23077
23637
  // packages/server/dist/routes/web.js
23078
- import { Hono as Hono14 } from "hono";
23638
+ import { Hono as Hono15 } from "hono";
23079
23639
  import { streamSSE } from "hono/streaming";
23080
23640
  import crypto4 from "node:crypto";
23081
- import path55 from "node:path";
23082
- import fs49 from "node:fs";
23641
+ import path56 from "node:path";
23642
+ import fs50 from "node:fs";
23083
23643
  function createWebRoutes(deps) {
23084
23644
  const { db, channel } = deps;
23085
- const app2 = new Hono14();
23645
+ const app2 = new Hono15();
23086
23646
  app2.get("/web/accounts", (c) => {
23087
23647
  const accounts = listAccounts2(db).map((acc) => {
23088
23648
  return {
23089
23649
  accountId: acc.accountId,
23090
23650
  token: acc.token,
23091
23651
  workspacePath: acc.workspacePath,
23092
- workspaceMissing: !fs49.existsSync(acc.workspacePath),
23652
+ workspaceMissing: !fs50.existsSync(acc.workspacePath),
23093
23653
  label: acc.label,
23094
23654
  enabled: acc.enabled,
23095
23655
  accessLevel: acc.accessLevel,
@@ -23104,9 +23664,9 @@ function createWebRoutes(deps) {
23104
23664
  const body = await c.req.json().catch(() => ({}));
23105
23665
  if (!body.workspacePath)
23106
23666
  return c.json({ error: "workspacePath required" }, 400);
23107
- if (!path55.isAbsolute(body.workspacePath))
23667
+ if (!path56.isAbsolute(body.workspacePath))
23108
23668
  return c.json({ error: "workspacePath must be absolute" }, 400);
23109
- if (!fs49.existsSync(body.workspacePath))
23669
+ if (!fs50.existsSync(body.workspacePath))
23110
23670
  return c.json({ error: "workspace path not found" }, 400);
23111
23671
  ensureWorkspaceHalo(body.workspacePath);
23112
23672
  const accountId = crypto4.randomUUID().slice(0, 8);
@@ -23137,7 +23697,7 @@ function createWebRoutes(deps) {
23137
23697
  if (body.enabled !== void 0)
23138
23698
  patch.enabled = body.enabled ? 1 : 0;
23139
23699
  if (body.workspacePath) {
23140
- if (!fs49.existsSync(body.workspacePath))
23700
+ if (!fs50.existsSync(body.workspacePath))
23141
23701
  return c.json({ error: "workspace path not found" }, 400);
23142
23702
  ensureWorkspaceHalo(body.workspacePath);
23143
23703
  patch.workspacePath = body.workspacePath;
@@ -23242,18 +23802,27 @@ function createWebRoutes(deps) {
23242
23802
  const filePath = c.req.query("path");
23243
23803
  if (!filePath)
23244
23804
  return c.json({ error: "path required" }, 400);
23245
- const path62 = await import("node:path");
23246
- const resolved = path62.default.resolve(account.workspacePath, filePath);
23247
- const root = path62.default.resolve(account.workspacePath);
23248
- if (resolved !== root && !resolved.startsWith(root + path62.default.sep)) {
23805
+ const path63 = await import("node:path");
23806
+ const resolved = path63.default.resolve(account.workspacePath, filePath);
23807
+ const root = path63.default.resolve(account.workspacePath);
23808
+ if (resolved !== root && !resolved.startsWith(root + path63.default.sep)) {
23249
23809
  return c.json({ error: "path traversal not allowed" }, 403);
23250
23810
  }
23251
- if (!fs49.existsSync(resolved))
23811
+ let real;
23812
+ let realRoot;
23813
+ try {
23814
+ real = fs50.realpathSync(resolved);
23815
+ realRoot = fs50.realpathSync(root);
23816
+ } catch {
23252
23817
  return c.json({ error: "file not found" }, 404);
23253
- const stat = fs49.statSync(resolved);
23818
+ }
23819
+ if (real !== realRoot && !real.startsWith(realRoot + path63.default.sep)) {
23820
+ return c.json({ error: "path traversal not allowed" }, 403);
23821
+ }
23822
+ const stat = fs50.statSync(real);
23254
23823
  if (stat.isDirectory())
23255
23824
  return c.json({ error: "cannot serve directory" }, 400);
23256
- const ext = path62.default.extname(resolved).toLowerCase();
23825
+ const ext = path63.default.extname(real).toLowerCase();
23257
23826
  const mimeMap = {
23258
23827
  ".png": "image/png",
23259
23828
  ".jpg": "image/jpeg",
@@ -23267,8 +23836,8 @@ function createWebRoutes(deps) {
23267
23836
  ".txt": "text/plain"
23268
23837
  };
23269
23838
  const contentType = mimeMap[ext] || "application/octet-stream";
23270
- const fileName = path62.default.basename(resolved);
23271
- const data = fs49.readFileSync(resolved);
23839
+ const fileName = path63.default.basename(resolved);
23840
+ const data = fs50.readFileSync(real);
23272
23841
  c.header("content-type", contentType);
23273
23842
  c.header("content-disposition", `inline; filename="${fileName}"`);
23274
23843
  return c.body(data);
@@ -23429,12 +23998,12 @@ function splitInlineCode(line) {
23429
23998
  return segs.length > 0 ? segs : [{ text: line, code: false }];
23430
23999
  }
23431
24000
  function transformProse(text5) {
23432
- text5 = text5.replace(/^(#{1,6})\s+(.*)$/, (_m, _h, body) => `*${body}*`);
23433
24001
  text5 = text5.replace(/(?<!!)\[([^\]]+)\]\(([^)\s]+)\)/g, "<$2|$1>");
23434
- text5 = text5.replace(/\*\*([^*\n]+?)\*\*/g, "*$1*");
23435
- text5 = text5.replace(/__([^_\n]+?)__/g, "*$1*");
23436
24002
  text5 = text5.replace(/(?<![*_])\*(?!\*)([^*\n]+?)(?<!\*)\*(?!\*)/g, "_$1_");
23437
24003
  text5 = text5.replace(/(?<![_*])_(?!_)([^_\n]+?)(?<!_)_(?!_)/g, "_$1_");
24004
+ text5 = text5.replace(/\*\*([^*\n]+?)\*\*/g, "*$1*");
24005
+ text5 = text5.replace(/__([^_\n]+?)__/g, "*$1*");
24006
+ text5 = text5.replace(/^(#{1,6})\s+(.*)$/, (_m, _h, body) => `*${body}*`);
23438
24007
  return text5;
23439
24008
  }
23440
24009
  function formatForFeishu(input) {
@@ -23598,10 +24167,10 @@ async function authTest(botToken) {
23598
24167
  return slackPost("auth.test", botToken, {});
23599
24168
  }
23600
24169
  async function uploadFile(args) {
23601
- const fs54 = await import("node:fs");
23602
- const path62 = await import("node:path");
23603
- const filename = args.filename ?? path62.basename(args.filePath);
23604
- const stat = fs54.statSync(args.filePath);
24170
+ const fs55 = await import("node:fs");
24171
+ const path63 = await import("node:path");
24172
+ const filename = args.filename ?? path63.basename(args.filePath);
24173
+ const stat = fs55.statSync(args.filePath);
23605
24174
  if (!stat.isFile())
23606
24175
  throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
23607
24176
  const length = stat.size;
@@ -23619,7 +24188,7 @@ async function uploadFile(args) {
23619
24188
  if (step1Parsed.ok === false || !step1Parsed.upload_url || !step1Parsed.file_id) {
23620
24189
  throw new Error(`[slack:files.getUploadURLExternal] ${step1Parsed.error ?? "unknown"}`);
23621
24190
  }
23622
- const buf = fs54.readFileSync(args.filePath);
24191
+ const buf = fs55.readFileSync(args.filePath);
23623
24192
  const step2 = await fetch(step1Parsed.upload_url, {
23624
24193
  method: "POST",
23625
24194
  body: buf
@@ -23764,7 +24333,7 @@ var init_api2 = __esm({
23764
24333
  });
23765
24334
 
23766
24335
  // packages/server/dist/channels/slack/handler.js
23767
- import path56 from "node:path";
24336
+ import path57 from "node:path";
23768
24337
  import { WebSocket } from "ws";
23769
24338
  function slashToBang(text5, commandNames) {
23770
24339
  if (commandNames.length === 0)
@@ -23774,9 +24343,9 @@ function slashToBang(text5, commandNames) {
23774
24343
  return text5.replace(re, "$1!$2");
23775
24344
  }
23776
24345
  function isPathAllowed2(filePath, workspacePath) {
23777
- const resolved = path56.resolve(filePath);
23778
- const wsResolved = path56.resolve(workspacePath);
23779
- return resolved.startsWith(wsResolved + path56.sep) || resolved === wsResolved || isInTempDir(resolved);
24346
+ const resolved = path57.resolve(filePath);
24347
+ const wsResolved = path57.resolve(workspacePath);
24348
+ return resolved.startsWith(wsResolved + path57.sep) || resolved === wsResolved || isInTempDir(resolved);
23780
24349
  }
23781
24350
  function rememberEventId(id) {
23782
24351
  if (SEEN_EVENT_IDS.has(id))
@@ -24048,7 +24617,7 @@ async function handleInbound2(args) {
24048
24617
  await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: chunk });
24049
24618
  },
24050
24619
  sendMedia: async (filePath) => {
24051
- const resolved = path56.resolve(filePath);
24620
+ const resolved = path57.resolve(filePath);
24052
24621
  if (!isPathAllowed2(resolved, workspace)) {
24053
24622
  console.log(`[slack] sendMedia blocked: ${filePath} not under workspace`);
24054
24623
  return;
@@ -24066,7 +24635,7 @@ async function handleInbound2(args) {
24066
24635
  botToken: account.botToken,
24067
24636
  channel: channelId,
24068
24637
  threadTs: replyTs,
24069
- text: `\u26A0\uFE0F Failed to upload \`${path56.basename(filePath)}\`: ${err instanceof Error ? err.message : String(err)}`
24638
+ text: `\u26A0\uFE0F Failed to upload \`${path57.basename(filePath)}\`: ${err instanceof Error ? err.message : String(err)}`
24070
24639
  }).catch(() => {
24071
24640
  });
24072
24641
  }
@@ -24142,12 +24711,12 @@ var init_handler5 = __esm({
24142
24711
  });
24143
24712
 
24144
24713
  // packages/server/dist/routes/slack.js
24145
- import { Hono as Hono15 } from "hono";
24146
- import fs50 from "node:fs";
24147
- import path57 from "node:path";
24714
+ import { Hono as Hono16 } from "hono";
24715
+ import fs51 from "node:fs";
24716
+ import path58 from "node:path";
24148
24717
  function createSlackRoutes(deps) {
24149
24718
  const { db, channel } = deps;
24150
- const app2 = new Hono15();
24719
+ const app2 = new Hono16();
24151
24720
  app2.get("/slack/accounts/:id/search", async (c) => {
24152
24721
  const id = c.req.param("id");
24153
24722
  const q = c.req.query("q") ?? "";
@@ -24167,7 +24736,7 @@ function createSlackRoutes(deps) {
24167
24736
  botUserId: a.botUserId,
24168
24737
  teamId: a.teamId,
24169
24738
  workspacePath: a.workspacePath,
24170
- workspaceMissing: !fs50.existsSync(a.workspacePath),
24739
+ workspaceMissing: !fs51.existsSync(a.workspacePath),
24171
24740
  label: a.label,
24172
24741
  enabled: a.enabled,
24173
24742
  accessLevel: a.accessLevel,
@@ -24187,9 +24756,9 @@ function createSlackRoutes(deps) {
24187
24756
  return c.json({ error: "appToken must start with xapp-" }, 400);
24188
24757
  if (!body.workspacePath)
24189
24758
  return c.json({ error: "workspacePath required" }, 400);
24190
- if (!path57.isAbsolute(body.workspacePath))
24759
+ if (!path58.isAbsolute(body.workspacePath))
24191
24760
  return c.json({ error: "workspacePath must be absolute" }, 400);
24192
- if (!fs50.existsSync(body.workspacePath))
24761
+ if (!fs51.existsSync(body.workspacePath))
24193
24762
  return c.json({ error: "workspace path not found" }, 400);
24194
24763
  ensureWorkspaceHalo(body.workspacePath);
24195
24764
  let identity;
@@ -24249,7 +24818,7 @@ function createSlackRoutes(deps) {
24249
24818
  if (body.enabled !== void 0)
24250
24819
  patch.enabled = body.enabled ? 1 : 0;
24251
24820
  if (body.workspacePath) {
24252
- if (!fs50.existsSync(body.workspacePath))
24821
+ if (!fs51.existsSync(body.workspacePath))
24253
24822
  return c.json({ error: "workspace path not found" }, 400);
24254
24823
  ensureWorkspaceHalo(body.workspacePath);
24255
24824
  patch.workspacePath = body.workspacePath;
@@ -24663,10 +25232,10 @@ async function replyMessage(args) {
24663
25232
  return { message_id: parsed.data?.message_id ?? "" };
24664
25233
  }
24665
25234
  async function uploadImage(args) {
24666
- const fs54 = await import("node:fs");
24667
- const path62 = await import("node:path");
24668
- const buf = fs54.readFileSync(args.filePath);
24669
- const filename = path62.basename(args.filePath);
25235
+ const fs55 = await import("node:fs");
25236
+ const path63 = await import("node:path");
25237
+ const buf = fs55.readFileSync(args.filePath);
25238
+ const filename = path63.basename(args.filePath);
24670
25239
  const send = async (token2) => {
24671
25240
  const form = new FormData();
24672
25241
  form.append("image_type", "message");
@@ -24697,10 +25266,10 @@ async function uploadImage(args) {
24697
25266
  return { imageKey: parsed.data.image_key };
24698
25267
  }
24699
25268
  async function uploadFile2(args) {
24700
- const fs54 = await import("node:fs");
24701
- const path62 = await import("node:path");
24702
- const buf = fs54.readFileSync(args.filePath);
24703
- const filename = args.filename ?? path62.basename(args.filePath);
25269
+ const fs55 = await import("node:fs");
25270
+ const path63 = await import("node:path");
25271
+ const buf = fs55.readFileSync(args.filePath);
25272
+ const filename = args.filename ?? path63.basename(args.filePath);
24704
25273
  const send = async (token2) => {
24705
25274
  const form = new FormData();
24706
25275
  form.append("file_type", args.fileType);
@@ -24806,12 +25375,12 @@ var init_api3 = __esm({
24806
25375
  });
24807
25376
 
24808
25377
  // packages/server/dist/channels/feishu/handler.js
24809
- import path58 from "node:path";
25378
+ import path59 from "node:path";
24810
25379
  import * as Lark from "@larksuiteoapi/node-sdk";
24811
25380
  function isPathAllowed3(filePath, workspacePath) {
24812
- const resolved = path58.resolve(filePath);
24813
- const wsResolved = path58.resolve(workspacePath);
24814
- return resolved.startsWith(wsResolved + path58.sep) || resolved === wsResolved || isInTempDir(resolved);
25381
+ const resolved = path59.resolve(filePath);
25382
+ const wsResolved = path59.resolve(workspacePath);
25383
+ return resolved.startsWith(wsResolved + path59.sep) || resolved === wsResolved || isInTempDir(resolved);
24815
25384
  }
24816
25385
  function newAccountState2() {
24817
25386
  return {
@@ -25053,7 +25622,7 @@ async function handleInbound3(args) {
25053
25622
  await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: chunk });
25054
25623
  },
25055
25624
  sendMedia: async (filePath) => {
25056
- const resolved = path58.resolve(filePath);
25625
+ const resolved = path59.resolve(filePath);
25057
25626
  if (!isPathAllowed3(resolved, workspace)) {
25058
25627
  console.log(`[feishu] sendMedia blocked: ${filePath} not under workspace`);
25059
25628
  return;
@@ -25073,7 +25642,7 @@ async function handleInbound3(args) {
25073
25642
  inboundMessageId,
25074
25643
  isP2P,
25075
25644
  chatId,
25076
- text: `\u26A0\uFE0F \u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF1A${path58.basename(filePath)} \u2014 ${err instanceof Error ? err.message : String(err)}`
25645
+ text: `\u26A0\uFE0F \u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF1A${path59.basename(filePath)} \u2014 ${err instanceof Error ? err.message : String(err)}`
25077
25646
  }).catch(() => {
25078
25647
  });
25079
25648
  }
@@ -25103,7 +25672,7 @@ async function replyToInbound(args) {
25103
25672
  async function sendFeishuMedia(args) {
25104
25673
  const { account, inboundMessageId, isP2P, chatId, filePath } = args;
25105
25674
  const cls = classifyMedia(filePath);
25106
- const ext = path58.extname(filePath).toLowerCase();
25675
+ const ext = path59.extname(filePath).toLowerCase();
25107
25676
  if (cls === "image") {
25108
25677
  const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
25109
25678
  await replyToInboundRaw({
@@ -25241,12 +25810,12 @@ var init_handler6 = __esm({
25241
25810
  });
25242
25811
 
25243
25812
  // packages/server/dist/routes/feishu.js
25244
- import { Hono as Hono16 } from "hono";
25245
- import fs51 from "node:fs";
25246
- import path59 from "node:path";
25813
+ import { Hono as Hono17 } from "hono";
25814
+ import fs52 from "node:fs";
25815
+ import path60 from "node:path";
25247
25816
  function createFeishuRoutes(deps) {
25248
25817
  const { db, channel } = deps;
25249
- const app2 = new Hono16();
25818
+ const app2 = new Hono17();
25250
25819
  app2.get("/feishu/accounts/:id/search", async (c) => {
25251
25820
  const id = c.req.param("id");
25252
25821
  const q = c.req.query("q") ?? "";
@@ -25267,7 +25836,7 @@ function createFeishuRoutes(deps) {
25267
25836
  botOpenId: a.botOpenId,
25268
25837
  hasEncryptKey: !!a.encryptKey,
25269
25838
  workspacePath: a.workspacePath,
25270
- workspaceMissing: !fs51.existsSync(a.workspacePath),
25839
+ workspaceMissing: !fs52.existsSync(a.workspacePath),
25271
25840
  label: a.label,
25272
25841
  enabled: a.enabled,
25273
25842
  accessLevel: a.accessLevel,
@@ -25285,9 +25854,9 @@ function createFeishuRoutes(deps) {
25285
25854
  return c.json({ error: "appSecret required" }, 400);
25286
25855
  if (!body.workspacePath)
25287
25856
  return c.json({ error: "workspacePath required" }, 400);
25288
- if (!path59.isAbsolute(body.workspacePath))
25857
+ if (!path60.isAbsolute(body.workspacePath))
25289
25858
  return c.json({ error: "workspacePath must be absolute" }, 400);
25290
- if (!fs51.existsSync(body.workspacePath))
25859
+ if (!fs52.existsSync(body.workspacePath))
25291
25860
  return c.json({ error: "workspace path not found" }, 400);
25292
25861
  ensureWorkspaceHalo(body.workspacePath);
25293
25862
  let botOpenId;
@@ -25351,7 +25920,7 @@ function createFeishuRoutes(deps) {
25351
25920
  if (body.enabled !== void 0)
25352
25921
  patch.enabled = body.enabled ? 1 : 0;
25353
25922
  if (body.workspacePath) {
25354
- if (!fs51.existsSync(body.workspacePath))
25923
+ if (!fs52.existsSync(body.workspacePath))
25355
25924
  return c.json({ error: "workspace path not found" }, 400);
25356
25925
  ensureWorkspaceHalo(body.workspacePath);
25357
25926
  patch.workspacePath = body.workspacePath;
@@ -25480,7 +26049,7 @@ var init_descriptors = __esm({
25480
26049
  });
25481
26050
 
25482
26051
  // packages/server/dist/middleware/auth.js
25483
- import { Hono as Hono17 } from "hono";
26052
+ import { Hono as Hono18 } from "hono";
25484
26053
  import { getCookie, setCookie } from "hono/cookie";
25485
26054
  import crypto5 from "node:crypto";
25486
26055
  function passwordHash() {
@@ -25581,7 +26150,7 @@ function setTokenCookie(c, token) {
25581
26150
  });
25582
26151
  }
25583
26152
  function createAuthRoutes() {
25584
- const app2 = new Hono17();
26153
+ const app2 = new Hono18();
25585
26154
  app2.post("/auth/login", async (c) => {
25586
26155
  const ip = getClientIp(c);
25587
26156
  if (isLockedOut2(ip)) {
@@ -25623,14 +26192,14 @@ function createAuthRoutes() {
25623
26192
  }
25624
26193
  function authMiddleware() {
25625
26194
  return async (c, next) => {
25626
- const path62 = c.req.path;
25627
- if (PUBLIC_PATHS.some((p) => path62 === p || path62.startsWith(p + "?"))) {
26195
+ const path63 = c.req.path;
26196
+ if (PUBLIC_PATHS.some((p) => path63 === p || path63.startsWith(p + "?"))) {
25628
26197
  return next();
25629
26198
  }
25630
26199
  const token = getCookie(c, COOKIE_NAME);
25631
26200
  const payload = validateToken(token);
25632
26201
  if (!payload) {
25633
- if (!path62.startsWith("/api/")) {
26202
+ if (!path63.startsWith("/api/")) {
25634
26203
  return next();
25635
26204
  }
25636
26205
  return c.json({ error: "Unauthorized" }, 401);
@@ -25662,22 +26231,22 @@ var init_auth = __esm({
25662
26231
 
25663
26232
  // packages/server/dist/index.js
25664
26233
  var dist_exports = {};
25665
- import { Hono as Hono18 } from "hono";
26234
+ import { Hono as Hono19 } from "hono";
25666
26235
  import { cors } from "hono/cors";
25667
26236
  import { serve } from "@hono/node-server";
25668
26237
  import { serveStatic } from "@hono/node-server/serve-static";
25669
26238
  import { WebSocketServer } from "ws";
25670
- import path60 from "node:path";
25671
- import fs52 from "node:fs";
26239
+ import path61 from "node:path";
26240
+ import fs53 from "node:fs";
25672
26241
  import { spawnSync as spawnSync4 } from "node:child_process";
25673
- import { homedir as homedir23 } from "node:os";
26242
+ import { homedir as homedir24 } from "node:os";
25674
26243
  import { fileURLToPath as fileURLToPath3 } from "node:url";
25675
26244
  function findProjectRoot() {
25676
26245
  let dir = process.cwd();
25677
26246
  for (let i = 0; i < 32; i++) {
25678
- if (fs52.existsSync(path60.join(dir, "pnpm-workspace.yaml")))
26247
+ if (fs53.existsSync(path61.join(dir, "pnpm-workspace.yaml")))
25679
26248
  return dir;
25680
- const parent = path60.dirname(dir);
26249
+ const parent = path61.dirname(dir);
25681
26250
  if (parent === dir)
25682
26251
  break;
25683
26252
  dir = parent;
@@ -25686,15 +26255,15 @@ function findProjectRoot() {
25686
26255
  return process.cwd();
25687
26256
  }
25688
26257
  function acquireSingleInstanceLock(lockFile) {
25689
- fs52.mkdirSync(path60.dirname(lockFile), { recursive: true });
25690
- const fd = fs52.openSync(lockFile, fs52.constants.O_RDWR | fs52.constants.O_CREAT, 420);
26258
+ fs53.mkdirSync(path61.dirname(lockFile), { recursive: true });
26259
+ const fd = fs53.openSync(lockFile, fs53.constants.O_RDWR | fs53.constants.O_CREAT, 420);
25691
26260
  const result = spawnSync4("flock", ["-n", "3"], {
25692
26261
  stdio: ["ignore", "ignore", "ignore", fd]
25693
26262
  });
25694
26263
  if (result.error) {
25695
26264
  const content = readLockPid(fd);
25696
26265
  if (content > 0 && isProcessAlive(content)) {
25697
- fs52.closeSync(fd);
26266
+ fs53.closeSync(fd);
25698
26267
  console.error(`[Server] another halo server is already running (pid ${content}). kill it first.`);
25699
26268
  process.exit(1);
25700
26269
  }
@@ -25703,19 +26272,19 @@ function acquireSingleInstanceLock(lockFile) {
25703
26272
  if (content > 0 && !isProcessAlive(content)) {
25704
26273
  console.warn(`[Server] removing stale server.lock (pid ${content} not running)`);
25705
26274
  } else {
25706
- fs52.closeSync(fd);
26275
+ fs53.closeSync(fd);
25707
26276
  console.error(`[Server] another halo server is already running${content > 0 ? ` (pid ${content})` : ""}. kill it first.`);
25708
26277
  process.exit(1);
25709
26278
  }
25710
26279
  }
25711
- fs52.ftruncateSync(fd);
25712
- fs52.writeSync(fd, String(process.pid));
25713
- fs52.fsyncSync(fd);
26280
+ fs53.ftruncateSync(fd);
26281
+ fs53.writeSync(fd, String(process.pid));
26282
+ fs53.fsyncSync(fd);
25714
26283
  }
25715
26284
  function readLockPid(fd) {
25716
26285
  try {
25717
26286
  const buf = Buffer.alloc(32);
25718
- const n = fs52.readSync(fd, buf, 0, 32, 0);
26287
+ const n = fs53.readSync(fd, buf, 0, 32, 0);
25719
26288
  return parseInt(buf.subarray(0, n).toString("utf-8").trim(), 10) || 0;
25720
26289
  } catch {
25721
26290
  return 0;
@@ -25744,14 +26313,14 @@ function isProcessAlive(pid) {
25744
26313
  }
25745
26314
  function resolveFrontendDir() {
25746
26315
  if (process.env.HALO_FRONTEND_DIR)
25747
- return path60.resolve(process.env.HALO_FRONTEND_DIR);
25748
- const bundled = path60.resolve(__dirname3, "..", "admin-out");
25749
- if (fs52.existsSync(path60.join(bundled, "index.html")))
26316
+ return path61.resolve(process.env.HALO_FRONTEND_DIR);
26317
+ const bundled = path61.resolve(__dirname3, "..", "admin-out");
26318
+ if (fs53.existsSync(path61.join(bundled, "index.html")))
25750
26319
  return bundled;
25751
- const sibling = path60.resolve(__dirname3, "..", "..", "admin", "out");
25752
- if (fs52.existsSync(path60.join(sibling, "index.html")))
26320
+ const sibling = path61.resolve(__dirname3, "..", "..", "admin", "out");
26321
+ if (fs53.existsSync(path61.join(sibling, "index.html")))
25753
26322
  return sibling;
25754
- return path60.resolve(PROJECT_ROOT, "packages", "admin", "out");
26323
+ return path61.resolve(PROJECT_ROOT, "packages", "admin", "out");
25755
26324
  }
25756
26325
  async function gracefulShutdown(signal) {
25757
26326
  console.log(`[Server] Received ${signal}, shutting down gracefully...`);
@@ -25768,7 +26337,7 @@ async function gracefulShutdown(signal) {
25768
26337
  console.log("[Server] Shutdown complete");
25769
26338
  process.exit(0);
25770
26339
  }
25771
- var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, app, allowlist, authRoutes, fileRoutes, gitRoutes, agentConfigRoutes, skillRoutes, settingsRoutes, evolutionRoutes, cronRoutes, channelDb, registry, sessionRoutes, showRoutes, metricsRoutes, commandRoutes, FRONTEND_DIR, server, wss;
26340
+ var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, AGENTCORE, app, allowlist, authRoutes, fileRoutes, gitRoutes, agentConfigRoutes, skillRoutes, settingsRoutes, evolutionRoutes, cronRoutes, channelDb, registry, sessionRoutes, showRoutes, metricsRoutes, commandRoutes, FRONTEND_DIR, server, wss;
25772
26341
  var init_dist2 = __esm({
25773
26342
  async "packages/server/dist/index.js"() {
25774
26343
  "use strict";
@@ -25782,6 +26351,7 @@ var init_dist2 = __esm({
25782
26351
  init_show();
25783
26352
  init_metrics();
25784
26353
  init_commands3();
26354
+ init_agentcore();
25785
26355
  init_commands();
25786
26356
  init_commands2();
25787
26357
  init_handler();
@@ -25803,12 +26373,12 @@ var init_dist2 = __esm({
25803
26373
  init_sandbox();
25804
26374
  init_init();
25805
26375
  init_git_ssh();
25806
- __dirname3 = path60.dirname(fileURLToPath3(import.meta.url));
26376
+ __dirname3 = path61.dirname(fileURLToPath3(import.meta.url));
25807
26377
  PORT = config.server.port;
25808
- HALO_VERSION = "0.2.1";
26378
+ HALO_VERSION = "0.2.3";
25809
26379
  PROJECT_ROOT = findProjectRoot();
25810
- HALO_HOME4 = path60.join(homedir23(), ".halo");
25811
- if (!fs52.existsSync(path60.join(HALO_HOME4, "global", ".template-version"))) {
26380
+ HALO_HOME4 = path61.join(homedir24(), ".halo");
26381
+ if (!fs53.existsSync(path61.join(HALO_HOME4, "global", ".template-version"))) {
25812
26382
  process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
25813
26383
  process.exit(1);
25814
26384
  }
@@ -25823,11 +26393,13 @@ var init_dist2 = __esm({
25823
26393
  }
25824
26394
  }
25825
26395
  }
25826
- if (!config.server.password && !config.server.passwordEnvPlaintext || !config.server.jwtSecret) {
26396
+ AGENTCORE = config.server.runtimeMode === "agentcore";
26397
+ if (!AGENTCORE && (!config.server.password && !config.server.passwordEnvPlaintext || !config.server.jwtSecret)) {
25827
26398
  process.stderr.write("\x1B[31m[Server] admin password not configured. Run `halo setup` to set one, or set the HALO_PASSWORD env.\x1B[0m\n");
25828
26399
  process.exit(1);
25829
26400
  }
25830
- acquireSingleInstanceLock(path60.join(HALO_HOME4, "global", "server.lock"));
26401
+ if (!AGENTCORE)
26402
+ acquireSingleInstanceLock(path61.join(HALO_HOME4, "global", "server.lock"));
25831
26403
  {
25832
26404
  const declared = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
25833
26405
  const dispatched = new Set(DISPATCH_COMMANDS);
@@ -25852,7 +26424,7 @@ var init_dist2 = __esm({
25852
26424
  ensureSshAgent();
25853
26425
  console.log(`[Server] Halo home: ${HALO_HOME4}`);
25854
26426
  console.log("[Server] Services initialized (ModelRuntime)");
25855
- app = new Hono18();
26427
+ app = new Hono19();
25856
26428
  allowlist = config.server.corsOrigins;
25857
26429
  app.use("/*", cors({
25858
26430
  origin: allowlist.length > 0 ? (origin) => origin && allowlist.includes(origin) ? origin : null : (origin) => origin ?? "*",
@@ -25892,13 +26464,15 @@ var init_dist2 = __esm({
25892
26464
  app.route("/api", evolutionRoutes);
25893
26465
  cronRoutes = createCronRoutes();
25894
26466
  app.route("/api", cronRoutes);
25895
- channelDb = createChannelDb(path60.join(HALO_HOME4, "secrets"));
26467
+ channelDb = createChannelDb(path61.join(HALO_HOME4, "secrets"));
25896
26468
  setChannelDb(channelDb);
25897
- setEvoDb(createEvoDb(path60.join(HALO_HOME4, "global")));
25898
- setCronDb(createCronDb(path60.join(HALO_HOME4, "global")));
26469
+ setEvoDb(createEvoDb(path61.join(HALO_HOME4, "global")));
26470
+ setCronDb(createCronDb(path61.join(HALO_HOME4, "global")));
25899
26471
  setEvoSpawner(realEvoSpawner);
25900
- startEvoTicker();
25901
- startArchiveDaemon();
26472
+ if (!AGENTCORE) {
26473
+ startEvoTicker();
26474
+ startArchiveDaemon();
26475
+ }
25902
26476
  registry = new SessionManagerRegistry({ reconcileOrphansOnBoot: true });
25903
26477
  sessionRoutes = createSessionRoutes(registry);
25904
26478
  app.route("/api", sessionRoutes);
@@ -25908,14 +26482,20 @@ var init_dist2 = __esm({
25908
26482
  app.route("/api", metricsRoutes);
25909
26483
  commandRoutes = createCommandRoutes(commandRegistry, registry);
25910
26484
  app.route("/api", commandRoutes);
25911
- bootChannels(app, defaultChannelDescriptors, { registry, db: channelDb });
25912
- startCronDaemon();
26485
+ if (!AGENTCORE) {
26486
+ bootChannels(app, defaultChannelDescriptors, { registry, db: channelDb });
26487
+ startCronDaemon();
26488
+ } else {
26489
+ const agentcoreRoutes = createAgentCoreRoutes({ registry, workspace: config.server.agentcoreWorkspace });
26490
+ app.route("/", agentcoreRoutes);
26491
+ console.log(`[Server] AgentCore runtime mode \u2014 workspace: ${config.server.agentcoreWorkspace}`);
26492
+ }
25913
26493
  FRONTEND_DIR = resolveFrontendDir();
25914
- app.use("/*", serveStatic({ root: path60.relative(process.cwd(), FRONTEND_DIR) }));
26494
+ app.use("/*", serveStatic({ root: path61.relative(process.cwd(), FRONTEND_DIR) }));
25915
26495
  app.get("/*", (c) => {
25916
- const indexPath = path60.join(FRONTEND_DIR, "index.html");
26496
+ const indexPath = path61.join(FRONTEND_DIR, "index.html");
25917
26497
  try {
25918
- const html = fs52.readFileSync(indexPath, "utf-8");
26498
+ const html = fs53.readFileSync(indexPath, "utf-8");
25919
26499
  return c.html(html);
25920
26500
  } catch {
25921
26501
  return c.text("Frontend not built. Run: cd packages/admin && npx next build", 503);
@@ -25934,7 +26514,9 @@ var init_dist2 = __esm({
25934
26514
  wss = new WebSocketServer({
25935
26515
  server,
25936
26516
  path: "/ws",
25937
- verifyClient: (info, callback) => {
26517
+ // AgentCore terminates auth upstream (SigV4/OAuth) before the connection
26518
+ // reaches the container, so its /ws is open; normal mode keeps cookie auth.
26519
+ verifyClient: AGENTCORE ? void 0 : (info, callback) => {
25938
26520
  const token = getTokenFromCookieHeader(info.req.headers.cookie);
25939
26521
  if (isAuthenticated(token)) {
25940
26522
  callback(true);
@@ -25943,8 +26525,12 @@ var init_dist2 = __esm({
25943
26525
  }
25944
26526
  }
25945
26527
  });
25946
- setupWebSocketHandler({ wss, registry });
25947
- setBroadcastWss(wss);
26528
+ if (AGENTCORE) {
26529
+ setupAgentCoreWebSocket({ wss, registry, workspace: config.server.agentcoreWorkspace });
26530
+ } else {
26531
+ setupWebSocketHandler({ wss, registry });
26532
+ setBroadcastWss(wss);
26533
+ }
25948
26534
  console.log(`[Server] WebSocket server ready on ws://localhost:${PORT}/ws`);
25949
26535
  process.on("SIGINT", () => gracefulShutdown("SIGINT"));
25950
26536
  process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
@@ -26678,11 +27264,11 @@ process.emitWarning = (warning, ...args) => {
26678
27264
  };
26679
27265
 
26680
27266
  // packages/cli/src/index.ts
26681
- import path61 from "node:path";
26682
- import fs53 from "node:fs";
26683
- import { homedir as homedir24 } from "node:os";
27267
+ import path62 from "node:path";
27268
+ import fs54 from "node:fs";
27269
+ import { homedir as homedir25 } from "node:os";
26684
27270
  import { parseArgs as parseArgs2 } from "node:util";
26685
- var VERSION = "0.2.1";
27271
+ var VERSION = "0.2.3";
26686
27272
  var HELP_TOP = `Usage: halo <command> [options]
26687
27273
 
26688
27274
  Commands:
@@ -26918,11 +27504,19 @@ async function cmdSetup(argv = []) {
26918
27504
  const { hashPassword: hashPassword2, generateJwtSecret: generateJwtSecret2 } = await Promise.resolve().then(() => (init_password_hash(), password_hash_exports));
26919
27505
  const { updateConfigLeaves: updateConfigLeaves2, configLeafSet: configLeafSet2, readConfigLeaf: readConfigLeaf2 } = await Promise.resolve().then(() => (init_setup_config(), setup_config_exports));
26920
27506
  const { readSetting: readSetting2, writeSetting: writeSetting2, maskSecret: maskSecret2 } = await Promise.resolve().then(() => (init_setup_settings(), setup_settings_exports));
26921
- const { listModelProviders: listModelProviders2, listOptionalSkills: listOptionalSkills2, listRequiredSkillsWithSecrets: listRequiredSkillsWithSecrets2 } = await Promise.resolve().then(() => (init_setup_providers(), setup_providers_exports));
27507
+ const {
27508
+ listModelProviders: listModelProviders2,
27509
+ listOptionalSkills: listOptionalSkills2,
27510
+ listRequiredSkillsWithSecrets: listRequiredSkillsWithSecrets2,
27511
+ listConfiguredSwitchableProviders: listConfiguredSwitchableProviders2,
27512
+ readDefaultAgentProvider: readDefaultAgentProvider2,
27513
+ bindBuiltinAgentsToProvider: bindBuiltinAgentsToProvider2
27514
+ } = await Promise.resolve().then(() => (init_setup_providers(), setup_providers_exports));
27515
+ const { buildBindProviderPlan: buildBindProviderPlan2, KEEP_CURRENT: KEEP_CURRENT2, AWS_CHAIN_PROVIDER: AWS_CHAIN_PROVIDER2 } = await Promise.resolve().then(() => (init_setup_bind_provider(), setup_bind_provider_exports));
26922
27516
  const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
26923
27517
  const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
26924
- const haloHome2 = path61.join(homedir24(), ".halo");
26925
- const globalDir2 = path61.join(haloHome2, "global");
27518
+ const haloHome2 = path62.join(homedir25(), ".halo");
27519
+ const globalDir2 = path62.join(haloHome2, "global");
26926
27520
  ensureHaloHome2(haloHome2);
26927
27521
  process.stderr.write(`[setup] ${haloHome2}/ ready
26928
27522
  `);
@@ -26935,6 +27529,17 @@ async function cmdSetup(argv = []) {
26935
27529
  if (!process.env.HALO_PASSWORD || process.env.HALO_PASSWORD.length === 0) {
26936
27530
  if (!configLeafSet2("server.password")) {
26937
27531
  process.stderr.write(`[setup] WARNING: no password set. Either set HALO_PASSWORD env, or re-run \`halo setup\` interactively.
27532
+ `);
27533
+ }
27534
+ }
27535
+ const envProvider = process.env.HALO_DEFAULT_PROVIDER;
27536
+ if (envProvider && envProvider.length > 0) {
27537
+ const bound = bindBuiltinAgentsToProvider2(envProvider);
27538
+ if (bound) {
27539
+ process.stderr.write(`[setup] built-in agents (${bound.agents.join(", ")}) bound to ${envProvider}${bound.modelId ? ` / ${bound.modelId}` : ""} (HALO_DEFAULT_PROVIDER)
27540
+ `);
27541
+ } else {
27542
+ process.stderr.write(`[setup] WARNING: HALO_DEFAULT_PROVIDER=${envProvider} is not a known provider \u2014 agents unchanged
26938
27543
  `);
26939
27544
  }
26940
27545
  }
@@ -26946,12 +27551,22 @@ async function cmdSetup(argv = []) {
26946
27551
  await stepPassword();
26947
27552
  await stepPort();
26948
27553
  await stepModelProviders();
27554
+ await stepBindDefaultAgent();
26949
27555
  await stepOptionalSkills();
26950
27556
  await stepRequiredSkillSecrets();
26951
27557
  if (!configLeafSet2("server.jwt_secret")) {
26952
27558
  updateConfigLeaves2({ "server.jwt_secret": generateJwtSecret2() });
26953
27559
  }
26954
27560
  process.stderr.write("\n[setup] done\n");
27561
+ {
27562
+ const portLeaf = readConfigLeaf2("server.port");
27563
+ const port = typeof portLeaf === "number" ? portLeaf : 9527;
27564
+ process.stderr.write("\nNext steps:\n");
27565
+ process.stderr.write(" 1. halo server start\n");
27566
+ process.stderr.write(` 2. Open http://localhost:${port} and log in with the password you just set
27567
+ `);
27568
+ process.stderr.write(" 3. Agents, model providers, and channels (Telegram / Slack / \u2026) are all configurable in the admin UI\n");
27569
+ }
26955
27570
  async function stepPassword() {
26956
27571
  const passwordSet = configLeafSet2("server.password");
26957
27572
  const envSet = process.env.HALO_PASSWORD ? process.env.HALO_PASSWORD.length > 0 : false;
@@ -27050,6 +27665,36 @@ async function cmdSetup(argv = []) {
27050
27665
  }
27051
27666
  process.stderr.write("\n");
27052
27667
  }
27668
+ async function stepBindDefaultAgent() {
27669
+ const configured = listConfiguredSwitchableProviders2();
27670
+ const current = readDefaultAgentProvider2();
27671
+ const currentHasKeys = current != null && listModelProviders2().some((p) => p.id === current && p.fields.some((f) => {
27672
+ const v = readSetting2(`${p.id}.secrets.${f.key}`);
27673
+ return v != null && v.length > 0;
27674
+ }));
27675
+ const plan = buildBindProviderPlan2(configured, current, currentHasKeys);
27676
+ if (!plan) return;
27677
+ for (const line of plan.intro) process.stderr.write(`${line}
27678
+ `);
27679
+ const pick = await promptSelect2(plan.question, plan.options, plan.initialIndex);
27680
+ if (pick == null) abort();
27681
+ if (pick === KEEP_CURRENT2) {
27682
+ process.stderr.write(`[setup] default agent unchanged (${current ?? AWS_CHAIN_PROVIDER2})
27683
+
27684
+ `);
27685
+ return;
27686
+ }
27687
+ const bound = bindBuiltinAgentsToProvider2(pick);
27688
+ if (bound) {
27689
+ process.stderr.write(`[setup] built-in agents (${bound.agents.join(", ")}) bound to ${pick}${bound.modelId ? ` / ${bound.modelId}` : ""}
27690
+
27691
+ `);
27692
+ } else {
27693
+ process.stderr.write(`[setup] could not bind agents to ${pick} \u2014 provider template missing; agents unchanged
27694
+
27695
+ `);
27696
+ }
27697
+ }
27053
27698
  async function stepOptionalSkills() {
27054
27699
  const skills = listOptionalSkills2();
27055
27700
  if (skills.length === 0) return;
@@ -27215,7 +27860,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
27215
27860
  async function cmdAgents(flags) {
27216
27861
  const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
27217
27862
  await initRuntime2();
27218
- const workspace = path61.resolve(flags.workspace ?? process.cwd());
27863
+ const workspace = path62.resolve(flags.workspace ?? process.cwd());
27219
27864
  const agents = await listAgents2(workspace);
27220
27865
  if (agents.length === 0) {
27221
27866
  process.stderr.write("No agents found.\n");
@@ -27231,7 +27876,7 @@ async function cmdAgents(flags) {
27231
27876
  async function cmdSessions(flags) {
27232
27877
  const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
27233
27878
  await initRuntime2();
27234
- const workspace = path61.resolve(flags.workspace ?? process.cwd());
27879
+ const workspace = path62.resolve(flags.workspace ?? process.cwd());
27235
27880
  const sessions2 = listSessions2(workspace);
27236
27881
  if (sessions2.length === 0) {
27237
27882
  process.stderr.write("No sessions found.\n");
@@ -27333,11 +27978,11 @@ ${stdinContent}` : stdinContent;
27333
27978
  harness.destroy();
27334
27979
  }
27335
27980
  }
27336
- var SERVER_LOCK_PATH = path61.join(homedir24(), ".halo", "global", "server.lock");
27337
- var SERVER_LOG_PATH = path61.join(homedir24(), ".halo", "logs", "server.log");
27981
+ var SERVER_LOCK_PATH = path62.join(homedir25(), ".halo", "global", "server.lock");
27982
+ var SERVER_LOG_PATH = path62.join(homedir25(), ".halo", "logs", "server.log");
27338
27983
  function readServerPid() {
27339
27984
  try {
27340
- const txt = fs53.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
27985
+ const txt = fs54.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
27341
27986
  const n = parseInt(txt, 10);
27342
27987
  return Number.isFinite(n) && n > 0 ? n : null;
27343
27988
  } catch {
@@ -27419,8 +28064,8 @@ async function cmdServerStart(argv, _unused) {
27419
28064
  return;
27420
28065
  }
27421
28066
  const { spawn: spawn4 } = await import("node:child_process");
27422
- fs53.mkdirSync(path61.dirname(SERVER_LOG_PATH), { recursive: true });
27423
- const out = fs53.openSync(SERVER_LOG_PATH, "a");
28067
+ fs54.mkdirSync(path62.dirname(SERVER_LOG_PATH), { recursive: true });
28068
+ const out = fs54.openSync(SERVER_LOG_PATH, "a");
27424
28069
  const child = spawn4(process.argv[0], [process.argv[1], "server", "start"], {
27425
28070
  detached: true,
27426
28071
  stdio: ["ignore", out, out],
@@ -27520,7 +28165,7 @@ async function cmdServerLogs(argv) {
27520
28165
  lines: { type: "string", short: "n" }
27521
28166
  }
27522
28167
  });
27523
- if (!fs53.existsSync(SERVER_LOG_PATH)) {
28168
+ if (!fs54.existsSync(SERVER_LOG_PATH)) {
27524
28169
  process.stderr.write(`No log file at ${SERVER_LOG_PATH}
27525
28170
  `);
27526
28171
  return;
@@ -27538,7 +28183,7 @@ function exitMissingSetup() {
27538
28183
  process.exit(1);
27539
28184
  }
27540
28185
  function isHaloHomeReady() {
27541
- return fs53.existsSync(path61.join(homedir24(), ".halo", "global", ".template-version"));
28186
+ return fs54.existsSync(path62.join(homedir25(), ".halo", "global", ".template-version"));
27542
28187
  }
27543
28188
  async function main2() {
27544
28189
  const argv = process.argv.slice(2);