@turmind/halo 1.6.5 → 1.6.7
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/README.md +1 -0
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/989-cb287787a4aca288.js +1 -0
- package/admin-out/_next/static/chunks/app/{page-812bcbb46db628b6.js → page-3584adfe2f9a0913.js} +1 -1
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +4 -4
- package/bundled-docs/design/a2a.md +1 -1
- package/bundled-docs/design/canvas-extensions.md +8 -0
- package/bundled-docs/design/cron.md +1 -1
- package/bundled-docs/design/feishu.md +2 -0
- package/bundled-docs/design/halo-city.md +4 -0
- package/bundled-docs/design/slack.md +2 -2
- package/bundled-docs/design/wecom.md +1 -1
- package/bundled-docs/dev/add-model-provider.md +1 -1
- package/bundled-docs/dev/api.md +6 -5
- package/bundled-docs/guide/secrets-and-credentials.md +17 -5
- package/bundled-docs/requirements/settings.md +3 -0
- package/dist/index.js +199 -73
- package/package.json +1 -1
- package/templates/models/mimo-token-plan-china.yaml +3 -55
- package/templates/skills/halo/SKILL.md +2 -0
- package/admin-out/_next/static/chunks/989-066a874961bd519f.js +0 -1
- /package/admin-out/_next/static/{8v1hCXbUqJhyMuK7LVeet → x5jl6YGvzL0Ys4lxsHVg1}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{8v1hCXbUqJhyMuK7LVeet → x5jl6YGvzL0Ys4lxsHVg1}/_ssgManifest.js +0 -0
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--x5jl6YGvzL0Ys4lxsHVg1--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-cb287787a4aca288.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/87-6a2716c98a819c4e.js" async=""></script><script src="/_next/static/chunks/app/page-3584adfe2f9a0913.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[3124,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"87\",\"static/chunks/87-6a2716c98a819c4e.js\",\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"974\",\"static/chunks/app/page-3584adfe2f9a0913.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"x5jl6YGvzL0Ys4lxsHVg1\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
|
package/admin-out/index.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
1:"$Sreact.fragment"
|
|
2
|
-
2:I[58321,["989","static/chunks/989-
|
|
3
|
-
3:I[24554,["989","static/chunks/989-
|
|
2
|
+
2:I[58321,["989","static/chunks/989-cb287787a4aca288.js","177","static/chunks/app/layout-ea161287d934ba96.js"],"ThemeProvider"]
|
|
3
|
+
3:I[24554,["989","static/chunks/989-cb287787a4aca288.js","177","static/chunks/app/layout-ea161287d934ba96.js"],"AppI18nProvider"]
|
|
4
4
|
4:I[63693,[],""]
|
|
5
5
|
5:I[74713,[],""]
|
|
6
6
|
6:I[5444,[],"ClientPageRoot"]
|
|
7
|
-
7:I[3124,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","87","static/chunks/87-6a2716c98a819c4e.js","989","static/chunks/989-
|
|
7
|
+
7:I[3124,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","87","static/chunks/87-6a2716c98a819c4e.js","989","static/chunks/989-cb287787a4aca288.js","974","static/chunks/app/page-3584adfe2f9a0913.js"],"default"]
|
|
8
8
|
a:I[23632,[],"OutletBoundary"]
|
|
9
9
|
c:I[86889,[],"AsyncMetadataOutlet"]
|
|
10
10
|
e:I[23632,[],"ViewportBoundary"]
|
|
@@ -15,7 +15,7 @@ e:I[23632,[],"ViewportBoundary"]
|
|
|
15
15
|
:HL["/_next/static/css/7b736202f0b1dbfd.css","style"]
|
|
16
16
|
:HL["/_next/static/css/e890aa0d13e94290.css","style"]
|
|
17
17
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
18
|
-
0:{"P":null,"b":"
|
|
18
|
+
0:{"P":null,"b":"x5jl6YGvzL0Ys4lxsHVg1","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/e890aa0d13e94290.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_d8ebd1 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
|
|
19
19
|
8:{}
|
|
20
20
|
9:"$0:f:0:1:2:children:1:props:children:0:props:params"
|
|
21
21
|
f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
|
@@ -42,7 +42,7 @@ The URL is the workspace path relative to the base. `/home/u/A/B` becomes:
|
|
|
42
42
|
- `defaultInputModes` / `defaultOutputModes`: `text/plain` + the four image types.
|
|
43
43
|
- `version`: the user's, else `HALO_VERSION`.
|
|
44
44
|
|
|
45
|
-
Served with `Cache-Control: private, max-age=300` and an ETag (304 on match). An invalid or incomplete card file → 500 to the authenticated caller and `[A2A] invalid agent-card.json …` in the log.
|
|
45
|
+
Served with `Cache-Control: private, max-age=300` and an ETag (304 on match). The ETag is weak (`W/"…"`) because the gzipped and plain bodies share it; a 200 (gzipped or not) and its 304 carry the same tag, and If-None-Match is compared weakly (RFC 9110). An invalid or incomplete card file → 500 to the authenticated caller and `[A2A] invalid agent-card.json …` in the log.
|
|
46
46
|
|
|
47
47
|
**Interface URL:** `general.a2a.public_url` (env `HALO_A2A_PUBLIC_URL` overrides) + `/a2a/<rel>/`. Unset → derived from the request (`Host`, plus `X-Forwarded-Proto` when `general.server.trust_proxy` is on).
|
|
48
48
|
|
|
@@ -279,6 +279,14 @@ An extension can ship agent skills next to its viewer: `~/.halo/global/extension
|
|
|
279
279
|
- **Read from disk**: the server, CLI / TUI and cron children all scan the extensions root on every read, never the in-memory registry, so every process sees the same set. In the admin, the `extension:changed` frame also bumps the skill bus, so the Skills sidebar and the agent form's skill picker refetch. Uninstalling removes the skills at once (next read finds no folder, no restart). The watcher is non-recursive, so editing files inside an installed extension's `skills/` pushes nothing. Reinstalling the extension is the way to publish a change.
|
|
280
280
|
- **Sandbox**: `~/.halo/global/extensions/**` is outside the sandbox hidden lists, so workspace- and readonly-access sessions can read the skill's resources, and a workspace-access `shell_exec` can run its scripts. `activate_skill` names the skill's absolute directory in its resources header, so the agent knows where they are.
|
|
281
281
|
|
|
282
|
+
### Extension docs
|
|
283
|
+
|
|
284
|
+
An extension documents itself; nothing it ships is injected into prompts or INDEX (per-turn tokens, and INDEX has a 12K-character budget).
|
|
285
|
+
|
|
286
|
+
- **Agent-facing instructions** → an extension skill (above). Opt-in per agent like any skill; an install never enables one by itself.
|
|
287
|
+
- **Human-facing docs** → `README.md` at the extension root (every halo-hub extension has one), plus `DESIGN.md` for internals. They ship in the zip and sit at `~/.halo/global/extensions/<id>/` once installed.
|
|
288
|
+
- **How agents find them** → the built-in `halo` skill (Platform Documentation, since `TEMPLATE_VERSION` 104) tells the agent to `file_read` that README when asked about an installed extension. The core repo's docs cover only what the core implements (protocol, capabilities, server proxies), not a hub extension's behavior.
|
|
289
|
+
|
|
282
290
|
## `extension` skill and halo-hub
|
|
283
291
|
|
|
284
292
|
Built-in skill `templates/skills/extension/` (in `BUILTIN_SKILL_IDS`; `TEMPLATE_VERSION` bumped to 69 in the same change): object command `/extension install|list|remove|models` (`models` = model provider configs, see the end of this section), `requiresAccess: full`. The built-in `default` agent lists it in `skills:` (since `TEMPLATE_VERSION` 81), so `/extension` shows in the command popup wherever the default agent is the built-in one. The payload reaches the body as `$ARGUMENTS`; when it names a source the agent installs right away without confirming, and it mentions the hub only when asked what exists or when an install fails. Everything goes through `templates/ext.sh` (curl + unzip + node, + git for tag-based hubs; no `gh`). `ext.sh install <id|zip|url>`: a zip path / URL is used as given, an id is fetched from the hub (below); then the same pre-check + six-step atomic replace as the server. It never talks to the server — the watcher picks the rename up within ~300 ms. The `halo` skill has a one-line pointer.
|
|
@@ -114,7 +114,7 @@ interface CronChannelDispatcher {
|
|
|
114
114
|
- no `supportsMedia` (telegram, feishu, wecom) → the **original text with `MEDIA:` lines intact** — the attachment degrades to a visible path instead of silently vanishing.
|
|
115
115
|
- `CronMedia.workspacePath` is the **job's** workspace (where the run produced its files), not the target account's — it's the sandbox root for `isMediaPathAllowed` (paths outside workspace/OS-tmp are rejected with a failed result row). Cron passes no access level, so the `full`-account relaxation the chat handlers get does not apply here — the job workspace + OS tmp rule stays.
|
|
116
116
|
- Wiring telegram/feishu up for real requires refactoring first, not copying: telegram's file send is an inline `sendPhoto/…/sendDocument` switch in `handler.ts`'s responder; feishu's `sendFeishuMedia` is anchored to an inbound `messageId` a cron run doesn't have. Extract the send-to-chat half in the channel module, then flip `supportsMedia: true` (details in each `cron-dispatcher.ts` header).
|
|
117
|
-
- **Per-channel size limits are the dispatcher's job**, not the runner's: wechat splits the text at `WECHAT_TEXT_LIMIT` (3500 chars, shared `splitText` from `channels/shared/chunk.ts`) and sends the chunks in order — the ilink gateway rejects any single `sendmessage` over 16 KB with `ret=-2`. A failure on chunk *i* of *n* is recorded as `chunk i/n: <error>` in the target's `DispatchResult`, so the admin run row shows how much of the report already landed (a `failed` row no longer means "nothing arrived").
|
|
117
|
+
- **Per-channel size limits are the dispatcher's job**, not the runner's: wechat splits the text at `WECHAT_TEXT_LIMIT` (3500 chars, shared `splitText` from `channels/shared/chunk.ts`) and sends the chunks in order — the ilink gateway rejects any single `sendmessage` over 16 KB with `ret=-2`. A failure on chunk *i* of *n* is recorded as `chunk i/n: <error>` in the target's `DispatchResult`, so the admin run row shows how much of the report already landed (a `failed` row no longer means "nothing arrived"). All five chunked dispatchers cut at their channel's chat-responder limit, exported from that channel's `event-adapter.ts` so cron and chat replies stay aligned: telegram `TELEGRAM_TEXT_LIMIT` (4000 chars — the Bot API caps a message at 4096), slack `SLACK_TEXT_LIMIT` (35 000 — Slack caps a body at ~40k), feishu `FEISHU_TEXT_LIMIT` (4500), wecom `WECOM_TEXT_LIMIT` (5000 chars — under the 20480-byte cap at 4 bytes/char), with the same sequential send and `chunk i/n: <error>` failure row (later chunks are not sent). Slack and feishu also run each chunk through the responder's formatter, in the responder's order (cut the raw text first, then format each chunk): `formatForSlack` (CommonMark → mrkdwn) and `formatForFeishu` (strip markup — the text msg type shows `**` literally), both in `channels/shared/markdown.ts`. Telegram, wechat and wecom send the chunks as-is. On slack a failed text chunk still lets the `MEDIA:` uploads run.
|
|
118
118
|
- **`chatId` semantics**: when a job is created from inside a chat, the target pins to that conversation (`chatId` set). When unset, the channel's own default-recipient logic decides: WeChat falls back to the QR-bind owner, then the last active chat; Telegram/Slack/Feishu/WeCom require an explicit `chatId` and throw without one. WeCom is the one dispatcher with **no HTTP send path**: it borrows the handler's live long-connect `WSClient` (`liveClients`), so a stopped / kicked / auth-exhausted account fails the target loudly (`wecom long-connect not active`) instead of queueing.
|
|
119
119
|
- `listTargets()` aggregates across all dispatchers (`listAllCronTargets`) to feed the admin create-form dropdown without the routes knowing any channel.
|
|
120
120
|
|
|
@@ -62,6 +62,8 @@ The feishu cron-dispatcher (`channels/feishu/cron-dispatcher.ts`) requires an ex
|
|
|
62
62
|
1. **Explicit `chatId` on the target** — set when the cron was created from inside a Feishu chat (e.g. cron created from a group thread auto-pins to that thread). Sends only to that chat (top-level message, not nested in the thread — Feishu's open API v1 doesn't support thread-targeting in sendMessage).
|
|
63
63
|
2. **Admin-UI cron without explicit target** — runs silently if no chatId is provided.
|
|
64
64
|
|
|
65
|
+
The report goes through the same steps as a chat reply. It is split with `splitText(text, FEISHU_TEXT_LIMIT)` (4500 chars, exported from `event-adapter.ts`), each chunk runs through `formatForFeishu`, and the chunks are sent in order as `text` messages. A failed chunk stops the send and is recorded as `chunk i/n: <error>`; the chunks before it were already delivered. Same contract as the other channels (see [cron.md](cron.md#dispatch-model)).
|
|
66
|
+
|
|
65
67
|
## Modules
|
|
66
68
|
|
|
67
69
|
Files: `packages/server/src/channels/feishu/`
|
|
@@ -19,6 +19,10 @@ Halo 服务器运行态的**街景式**像素可视化:每个 **workspace 是一
|
|
|
19
19
|
hub [README.md](https://github.com/turmind/halo-hub/blob/main/extensions/halo-city/README.md#和-halo-同源)。
|
|
20
20
|
- `/api/show/state` — 无活跃 UIState 的会话(idle/stopped/重启后)token
|
|
21
21
|
从会话文件头读取(按会话 `updatedAt` 缓存,未变化时不随轮询重读),不显示 0;含 `messageCount` 字段
|
|
22
|
+
- `/api/show/state` 带弱 `ETag` + `Cache-Control: private, no-cache`:哈希不含
|
|
23
|
+
`serverTime`,`uptime` 按分钟计入,所以服务器空闲时 7 秒一次的轮询绝大多数回
|
|
24
|
+
空 body 的 304,每分钟最多一次 200。浏览器 `fetch` 默认缓存模式自动带
|
|
25
|
+
`If-None-Match`,扩展不用改;鉴权仍在最前,不同 scope 的调用方 ETag 不同
|
|
22
26
|
|
|
23
27
|
> **`messageCount` 与 admin 的 `exchangeCount` 是两个语义,不要互相对齐**:
|
|
24
28
|
> 这里读的是会话文件里的 `messageCount`(活跃 UI 日志的长度),城市只把它
|
|
@@ -161,8 +161,8 @@ Slack cron-dispatcher plugs into the unified cron/dispatcher.ts registry. At fir
|
|
|
161
161
|
|
|
162
162
|
1. Retrieve the Slack account (must be enabled)
|
|
163
163
|
2. Parse the target's `chatId` (format: `D…`, `C…`, or `C…:thread_ts`)
|
|
164
|
-
3.
|
|
165
|
-
4. Return dispatch result (ok / error)
|
|
164
|
+
3. Split the text with `splitText(text, SLACK_TEXT_LIMIT)` (35 000 chars, exported from `event-adapter.ts`, the same limit as chat replies), run each chunk through `formatForSlack`, and call `postMessage()` for each in order with the chatId (and threadTs if present). A failed chunk stops the text send.
|
|
165
|
+
4. Return dispatch result: one row for the text (`ok`, or `chunk i/n: <error>`), then one row per `MEDIA:` upload. Uploads still run after a text failure.
|
|
166
166
|
|
|
167
167
|
If no explicit chatId is provided, dispatch fails with a clear message. This prevents silent failures in a shared-bot scenario where the "last messager" might be unrelated to whoever set up the cron.
|
|
168
168
|
|
|
@@ -65,7 +65,7 @@ The wecom cron-dispatcher (`channels/wecom/cron-dispatcher.ts`) requires an expl
|
|
|
65
65
|
1. **Explicit `chatId` on the target** — set when the cron was created from inside a WeCom chat (auto-pinned via `CommandContext.channel.chatId`). Shape: `<userid>` for a single chat, `<chatid>` for a group — exactly what `pickConversation` cached as `chatKey`. `sendMessage(chatId, { msgtype: 'markdown', … })` omits `chat_type`; the server's compat mode (`chat_type = 0`) resolves single vs group from the id.
|
|
66
66
|
2. **Admin-UI cron without explicit target** — throws `wecom cron target requires an explicit chatId …` and the run fails visibly. There is no `/search` route (WeCom exposes no directory API on this channel), so the target must be typed as `wecom:<accountId>:<chatId>`.
|
|
67
67
|
|
|
68
|
-
Because the push rides the handler's socket, the dispatcher borrows it through the exported `liveClients: Map<accountId, WSClient>` (set on `authenticated`, cleared on stop / kick / auth exhaustion). No live client → `{ ok: false, error: 'wecom long-connect not active' }` rather than queueing. WeCom additionally refuses pushes to a user / group that has never messaged the bot, and rate-limits each conversation to 30 msgs/min · 1000/hour (replies and pushes combined).
|
|
68
|
+
Because the push rides the handler's socket, the dispatcher borrows it through the exported `liveClients: Map<accountId, WSClient>` (set on `authenticated`, cleared on stop / kick / auth exhaustion). No live client → `{ ok: false, error: 'wecom long-connect not active' }` rather than queueing. The report is split with `splitText(text, WECOM_TEXT_LIMIT)` (5000 chars, exported from `event-adapter.ts`, the same limit as chat replies) and the chunks are sent in order as `markdown` messages, with no formatter. A failed chunk stops the send and is recorded as `chunk i/n: <error>`. WeCom additionally refuses pushes to a user / group that has never messaged the bot, and rate-limits each conversation to 30 msgs/min · 1000/hour (replies and pushes combined).
|
|
69
69
|
|
|
70
70
|
**`MEDIA:` is not implemented for cron** — the dispatcher declares no `supportsMedia`, so `dispatchToTargets` hands it the original text with `MEDIA:` lines intact (the path degrades to visible text, same caveat as telegram / feishu in [cron.md](cron.md#dispatch-model)). The realtime uploader (`sendWecomMedia` in `handler.ts`) already falls back to `sendMediaMessage(chatid, …)` for a route without a `req_id` (restored at startup); wiring cron up means calling it with `{ reqId: '', chatId }` and flipping `supportsMedia: true`.
|
|
71
71
|
|
|
@@ -33,7 +33,7 @@ Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`,
|
|
|
33
33
|
| `gemini` | `gemini` | `GeminiAgent` | `gemini-agent.ts` | Google Gemini 3.x on the Gemini API's OpenAI-compatible surface (`generativelanguage.googleapis.com/v1beta/openai`, Bearer key), streaming via `fetchChatCompletionStream`. **Thought signatures**: the first tool call of a reply carries `extra_content.google.thought_signature`, and a follow-up that replays a current-turn call without it 400s. The runtime takes the signatures from the raw frames (the `onChunk` hook) and re-attaches them by tool-call id. A call it never saw (rebuilt instance) gets Google's placeholder `skip_thought_signature_validator`. Tool-call replies end with `finish_reason: "stop"`, so they're folded as `tool_calls`. Thinking can't be turned off on Gemini 3. The level goes as `extra_body.google.thinking_config.thinking_level` with `include_thoughts` (this 400s next to `reasoning_effort`). The summary streams inline as `<thought>…</thought>` at the start of `content` and is split out into thinking. Levels per model: 3.8-flash low / medium / high, 3.5-flash-lite minimal…high, xhigh / max invalid everywhere. Usage: `completion_tokens` excludes thought tokens, so output = `total_tokens − prompt_tokens`. Implicit caching via `prompt_tokens_details.cached_tokens` (included in `prompt_tokens`, subtracted). Vision via `image_url` data URLs (probed 2026-10-12) |
|
|
34
34
|
| `openai` | `openai-chat` | `OpenAIAgent` | `openai-agent.ts` | **Generic OpenAI-compatible**. Use for native OpenAI or any third-party OpenAI gateway (incl. Ollama / llama.cpp). Ships a short Bedrock Mantle list callable with a Bedrock API key (`openai.gpt-5.5` default, `openai.gpt-5.4`; `defaultEndpoint` Mantle us-east-1 `/openai/v1`, presets us-east-1 / us-west-2 / api.openai.com — GPT-6.x / 5.6 reject `max_tokens`, so they stay on the Responses-API providers) plus the legacy `gpt-4o` placeholder (official API only); any other endpoint + model id can be typed, and capabilities for unlisted ids are openly editable in the form. Streaming via `fetchChatCompletionStream` — always sends `stream: true` + `stream_options: { include_usage: true }` (OpenAI's own field; Ollama ≥ 0.3 / vLLM / LM Studio accept it too). A gateway that rejects unknown fields would 400 — accepted trade-off, no non-streaming fallback. Forgiving about cache field names (`cachedPromptTokens`: first value > 0 of `prompt_tokens_details.cached_tokens`, top-level `cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens`) and reasoning field names (`reasoning_content` for OpenAI o-series/DeepSeek, `reasoning` for Ollama/llama.cpp). |
|
|
35
35
|
| `anthropic` | `anthropic-messages` | `AnthropicAgent` | `anthropic-agent.ts` | **Generic Anthropic Messages**. Use for api.anthropic.com or any Anthropic-compatible gateway. Ships a short Bedrock Mantle list callable with a Bedrock API key as `x-api-key` (`anthropic.claude-opus-5-5` default, `anthropic.claude-haiku-4-5`; `defaultEndpoint` Mantle us-east-1 `/anthropic`, presets us-east-1 / us-west-2 / api.anthropic.com) plus the legacy `claude-sonnet-4-6` placeholder (official API only); any other endpoint + model id can be typed. Streaming via `fetchAnthropicStream` (the gateway must support `stream: true` SSE — the reference Anthropic behaviour). Standard `cache_control: ephemeral` with optional `ttl: '1h'`. |
|
|
36
|
-
| `mimo-token-plan-china` | `anthropic-messages` | `AnthropicAgent` | (reuses `anthropic-agent.ts`) | Xiaomi MiMo (UI name "Xiaomi MiMo"; the id keeps its historical name so existing configs don't break) — Anthropic-compatible gateway, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); no dedicated class, just `runtime: anthropic-messages` in its yaml (`defaultEndpoint` `token-plan-cn.xiaomimimo.com/anthropic`). The key type picks the endpoint: Token Plan `tp-`/`ttp-` keys only work on `token-plan-{cn,sgp,ams}.xiaomimimo.com/anthropic`, pay-as-you-go `sk-` keys only on `api.xiaomimimo.com/anthropic` — all four ship as `endpointPresets` (pay-as-you-go live-probed 2026-09-24; sgp / ams from the docs only). V2.6 (`mimo-v2.6-pro` / `-flash`, both vision, 1M / 131072) is
|
|
36
|
+
| `mimo-token-plan-china` | `anthropic-messages` | `AnthropicAgent` | (reuses `anthropic-agent.ts`) | Xiaomi MiMo (UI name "Xiaomi MiMo"; the id keeps its historical name so existing configs don't break) — Anthropic-compatible gateway, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); no dedicated class, just `runtime: anthropic-messages` in its yaml (`defaultEndpoint` `token-plan-cn.xiaomimimo.com/anthropic`). The key type picks the endpoint: Token Plan `tp-`/`ttp-` keys only work on `token-plan-{cn,sgp,ams}.xiaomimimo.com/anthropic`, pay-as-you-go `sk-` keys only on `api.xiaomimimo.com/anthropic` — all four ship as `endpointPresets` (pay-as-you-go live-probed 2026-09-24; sgp / ams from the docs only). V2.6 (`mimo-v2.6-pro` / `-flash`, both vision, 1M / 131072) is the only generation listed; `mimo-v2.5(-pro)` (officially retired 2026-10-21) were dropped from the template in revision 2026101401 |
|
|
37
37
|
| `aws-bedrock-mantle` | `bedrock-mantle` | `MantleAgent` | `mantle-agent.ts` | OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna) on Amazon Bedrock via the **OpenAI Responses API** (not Completions/Converse/InvokeModel). Vision, reasoning via `reasoning.effort` (low/med/high/xhigh/max, no budget_tokens; on 5.6 `max` is accepted live but absent from the model's own validation list — measured ~6% more reasoning_tokens than xhigh; on Astra it's on the official list), `text.verbosity` (low/medium/high, same on both generations). **Region split** (live-probed 2026-09-24): us-east-1 serves GPT-6.1 Sol + GPT-6 Sol / Luna + every 5.6 model (404s Astra; GPT-6.1 Sol probed 2026-09-30, us-east-1 only); us-east-2 serves only the 5.6 trio; us-west-2 serves Astra + 5.6 Terra / Luna. Template ships all three as `endpointPresets` (us-east-1 / us-west-2 / us-east-2, default us-east-1, `openai.gpt-6-sol` default model) and pins each model's regions with per-model `endpoints:` (Astra us-west-2; GPT-6.1 Sol / GPT-6 Sol / Luna us-east-1; GPT-5.6 Sol us-east-1 + us-east-2; Terra / Luna none = all presets), so the form only offers — and on a model switch moves to — a region that serves the model; `extractRegionFromHost` falls back to us-east-1. GPT-6 Astra / Sol / Luna: 1,050,000 context / 128,000 max output; GPT-6.1 Sol 1M / 131,072 (card; Mantle accepts any `max_output_tokens` ≥ 16, so the cap isn't enforced server-side); same effort (none/low/medium/high/xhigh/max) + verbosity surface. Input beyond 272K tokens bills at 2x, so every entry ships `contextWindow: 272000` (what a new agent gets) with the 1M-class figure as `maxContextWindow`; raise `context.maxTokens` past 272K only on purpose. Never surfaces reasoning summaries — `output[].reasoning.summary` stays empty even with `reasoning.summary=detailed` while `reasoning_tokens` counts up (verified live), so no thinking text in the UI is upstream behavior, not a parsing gap. Streams (`stream: true`, Responses API SSE folded by `readResponsesStream` in the same file): `response.output_text.delta` frames go out live, the terminal `response.completed` / `response.incomplete` frame carries the full final response object; this host sends **no** `data: [DONE]` (live-probed 2026-09-29). |
|
|
38
38
|
| `aws-bedrock-openai` | `bedrock-mantle` | `MantleAgent` | (reuses `mantle-agent.ts`) | Frontier models on Bedrock via the **bedrock-runtime** host's OpenAI Responses surface (`https://bedrock-runtime.<region>.amazonaws.com/openai/v1` — AWS's recommended endpoint for new apps; Mantle is the compatibility one). Same wire format, so its yaml just says `runtime: bedrock-mantle` + `extractRegionFromHost` accepts both hostnames (SigV4 signs with the host's region — a mismatched region 401s "Credential should be scoped to a valid region"). Rules that differ from Mantle: model ids must be cross-Region inference profiles (`global.*` / `us.*`; bare ids 400 "on-demand throughput isn't supported"); **no region split** — live-probed 2026-09-24, all nine `global.*` ids answer 200 from us-east-1 / us-west-2 / eu-west-1 / ap-northeast-1 alike; **no server-side tools** — `tools:[{type:"web_search"}]` 400s "web search is not supported for this request", which is why the web-search skill's deep gear stays on Mantle. Ships the same seven **GPT-6.1 / 6 / 5.6** entries as the Mantle template (`global.openai.gpt-6.1-sol`, `global.openai.gpt-6-{astra,sol,luna}`, `global.openai.gpt-5.6-{sol,terra,luna}`; identical effort none/low/medium/high/xhigh/max + verbosity surface per each model's own validation error — except Astra and GPT-6.1 Sol 400 on `none`, which the runtime never sends), plus **Grok 4.6** (500K ctx, 2x input price past 200K, max output undisclosed → 64000 carry-over, effort **only** low/medium/high/xhigh — `none`/`max` 400, so no Max preset; vision OK; $2/$6) and **Kimi K3** (1M ctx / 131072 out, effort none…max all accepted, ladder advisory on short prompts; vision OK; $3/$15). All: implicit prompt caching verified (run 1 `cache_write_tokens`, runs 2+ `cached_tokens` ≈ prefix), no reasoning summaries. Streams like Mantle (same event vocabulary) but this host **does** append a trailing `data: [DONE]`, which `readSseJson` skips; grok-4.6 emits a `reasoning` output item (no summary text deltas) before the `message` item. |
|
|
39
39
|
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -6,7 +6,7 @@ Auth: most `/api/*` routes require a valid JWT cookie (`halo_token`). Exceptions
|
|
|
6
6
|
|
|
7
7
|
**Trust model.** The admin cookie is a single-tenant, all-or-nothing credential: there is one password (`~/.halo/secrets/config.yaml`), one JWT secret, and no roles. A valid `halo_token` equals full control of the server process — every workspace's files (`/api/files/*` project-scoped, `/api/fs/browse` unrestricted), shell via the WS terminal, channel credentials, git credentials, cron jobs, and the ability to mint web-channel tokens at any access level. Routes under the cookie therefore don't re-check "who" beyond the cookie itself; per-user isolation is the job of channel accounts (`x-token` with `readonly` / `workspace` / `full`), not of the admin. Consequences worth knowing when adding routes: (1) never put a cookie-gated route in `PUBLIC_PATHS` to "make it easier for a client" — that's the entire boundary; (2) a route that accepts a path or id from the client still needs traversal / shape checks (`isSafeIdSegment`, `assertPathAllowed`), because the *web-channel* surface reuses the same helpers and is multi-user; (3) if multi-user admin ever lands, the first change is a role claim in the JWT plus per-route checks, and this paragraph becomes the checklist.
|
|
8
8
|
|
|
9
|
-
Compression: every response negotiates gzip/deflate via `hono/compress
|
|
9
|
+
Compression: every response negotiates gzip/deflate via `compressResponses` (`middleware/compress.ts`, wrapping `hono/compress`; `threshold: 1024` — bodies under 1 KiB pass through uncompressed). The threshold holds for every compressible body: one without `Content-Length` (`c.json` / `c.text`) is read up to 1 KiB first and, if it ends there, sent with a `Content-Length`. A 304 carries `Vary: Accept-Encoding`, merged into any existing `Vary`. Mounted after CORS so it covers both `/api/*` JSON and the served admin static assets. `text/event-stream` (SSE) is excluded, so `/api/web/chat`'s streamed reply is unaffected; WS upgrades never enter the Hono fetch pipeline either.
|
|
10
10
|
|
|
11
11
|
## Health
|
|
12
12
|
|
|
@@ -334,7 +334,7 @@ Files: `packages/server/src/a2a/routes.ts` (JSON-RPC handler), `exposure.ts` (pa
|
|
|
334
334
|
| Method | Path | Purpose |
|
|
335
335
|
|---|---|---|
|
|
336
336
|
| POST | `/a2a/<rel>` (trailing `/` ok) | JSON-RPC 2.0 for the workspace `<A2A base>/<rel>` |
|
|
337
|
-
| GET | `/a2a/<rel>/.well-known/agent-card.json` | The agent card. `Cache-Control: private, max-age=300` + `ETag` (304 on `If-None-Match`); invalid `.halo/agent-card.json` → 500 |
|
|
337
|
+
| GET | `/a2a/<rel>/.well-known/agent-card.json` | The agent card. `Cache-Control: private, max-age=300` + weak `ETag` (`W/"…"`, the same on a 200 gzipped or not and on its 304; 304 on a weak `If-None-Match` match); invalid `.halo/agent-card.json` → 500 |
|
|
338
338
|
| POST | `/a2a-push/:pushId` | Webhook receiver for remotes this server dispatched to (`a2a_send`) |
|
|
339
339
|
|
|
340
340
|
**Exposure**: `<rel>` is the workspace path relative to the base dir (`HALO_A2A_ROOT`, default the user's home), each segment percent-encoded. Only a workspace containing `.halo/agent-card.json` is served; empty / `.` / `..` segments, separators inside a segment, and symlinks leaving the base are refused. The card's interface URL is `general.a2a.public_url` (env `HALO_A2A_PUBLIC_URL`) + `/a2a/<rel>/`, else derived from the request host.
|
|
@@ -393,7 +393,7 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
|
|
|
393
393
|
|
|
394
394
|
| Method | Path | Purpose |
|
|
395
395
|
|---|---|---|
|
|
396
|
-
| GET | `/api/show/state` | Admin cookie, `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
|
|
396
|
+
| GET | `/api/show/state` | Admin cookie, `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }`. Sends a weak `ETag` (over `accessLevel` / `skills` / `workspaces` plus `uptime` by the minute — never `serverTime`) and `Cache-Control: private, no-cache`; a weakly-matching `If-None-Match` (list or `*`) gets an empty 304 after auth, so an idle server answers 200 at most once a minute. On a 304 the cached copy's `serverTime` / `uptime` are those of the last 200. |
|
|
397
397
|
| GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `totalMessages` / `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Tokens other than `full`/`observer` may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. The admin cookie reads any workspace. |
|
|
398
398
|
|
|
399
399
|
`observer` is more than an aggregate-counts role: past `/show/state`, it can also call `/show/session` to read **any workspace's** session transcript (last 40 messages, content truncated to 600 chars, tool input to 200 chars). Mint it knowing it grants cross-workspace transcript read access, not just dashboard counters.
|
|
@@ -830,7 +830,8 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/confi
|
|
|
830
830
|
"value": "AK****ST", // already masked when secret:true; null = unset
|
|
831
831
|
"hasValue": true, // any layer has a non-empty value
|
|
832
832
|
"source": "global", // 'workspace' | 'global' | 'unset'
|
|
833
|
-
"inheritedFromGlobal": false
|
|
833
|
+
"inheritedFromGlobal": false, // true when scope=workspace + value came from global
|
|
834
|
+
"envMissing": [] // names of <<X>> placeholders in the effective value whose env var is unset in the server process; names only, never values
|
|
834
835
|
}
|
|
835
836
|
]
|
|
836
837
|
}
|
|
@@ -841,7 +842,7 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/confi
|
|
|
841
842
|
}
|
|
842
843
|
```
|
|
843
844
|
|
|
844
|
-
`<<ENV_NAME>>` references in stored values are returned **as literals** (the browser never sees the resolved env var). Secret values are masked (`AK****ST`); env-var refs pass through unmasked since they're not real secrets.
|
|
845
|
+
`<<ENV_NAME>>` references in stored values are returned **as literals** (the browser never sees the resolved env var). Secret values are masked (`AK****ST`); env-var refs pass through unmasked since they're not real secrets. `envMissing` lists the referenced variables that are unset in the server's `process.env` (empty when all are set or the value has no placeholder); the Settings page shows a warning for them.
|
|
845
846
|
|
|
846
847
|
### PATCH `/api/settings`
|
|
847
848
|
|
|
@@ -105,12 +105,24 @@ env vars (process.env.X) > workspace settings.yaml > global settings.yaml > sche
|
|
|
105
105
|
|
|
106
106
|
## Missing env var
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
- The agent sees the literal `<<EXAMPLE_KEY>>` in the rendered skill body
|
|
110
|
-
- The server logs `[MdVars] Env var "EXAMPLE_KEY" not set — keeping <<EXAMPLE_KEY>> literal`
|
|
111
|
-
- API calls the agent makes will fail with a clear error (usually HTTP 401) and the error message will reveal the literal, so the user can tell what to fix
|
|
108
|
+
An unset variable is never replaced with an empty string. The `<<NAME>>` literal is kept, and Halo reports it where it can:
|
|
112
109
|
|
|
113
|
-
|
|
110
|
+
- **Model provider credentials** (`<provider>.secrets.api_key`, `access_key_id` / `secret_access_key`): the request is never sent. Each turn ends at once with one error in the chat (and on channels), with no retries. The error names the provider, the field and the variable, e.g. `minimax api_key uses <<MINIMAX_API_KEY>>, but MINIMAX_API_KEY is not set in the Halo server's environment. …`, and says how to fix it. You can still open the session, list it and read its history. Only calling the model fails.
|
|
111
|
+
- **Settings page**: a field whose value names an unset variable shows an amber warning under the input naming the variable. The check runs against the server process's environment, not your shell's.
|
|
112
|
+
- **Skill / AGENT.md placeholders** (`{{caption-image.params.api_key}}` → `<<EXAMPLE_KEY>>`): the agent sees the literal `<<EXAMPLE_KEY>>` in the rendered body, and the server logs `[MdVars] Env var "EXAMPLE_KEY" not set — keeping <<EXAMPLE_KEY>> literal`. Any request the agent makes with it fails on the far side, and don't count on that error naming the variable. A provider usually just answers 401 (MiniMax: `login fail: Please carry the API secret key…`).
|
|
113
|
+
|
|
114
|
+
## Where the server reads environment variables
|
|
115
|
+
|
|
116
|
+
`<<NAME>>` resolves against the **Halo server process's** environment, not your login shell's. An `export` in `~/.bashrc` / `~/.zshrc` only reaches a server started from a shell that read it:
|
|
117
|
+
|
|
118
|
+
| How the server runs | Where its environment comes from | After adding a variable |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| Foreground `halo server start` | The terminal you started it in | Restart it from a terminal that has the variable |
|
|
121
|
+
| `halo server start -d` on Linux (systemd user unit) | `~/.halo/secrets/server.env`, a snapshot of your shell's environment written by `start -d` / `halo server restart`. Run inside the server (an agent's shell, the admin terminal), `restart` keeps the existing snapshot | Open a new terminal (so it has the variable) and run `halo server restart`. Or edit `server.env` and restart |
|
|
122
|
+
| System unit (`/etc/systemd/system/*.service`) | Only the unit's `Environment=` / `EnvironmentFile=` lines; `~/.bashrc` is never read | Add the variable there, `sudo systemctl daemon-reload`, then restart the unit |
|
|
123
|
+
| Desktop app (macOS) opened from the Dock / Finder | launchd's environment; `~/.zshrc` is never read | Enter the key in Settings, or start the app binary from a terminal that has the variable (`/Applications/Halo.app/Contents/MacOS/Halo`) |
|
|
124
|
+
|
|
125
|
+
If in doubt, put the key itself in Settings: it works the same under every launcher.
|
|
114
126
|
|
|
115
127
|
## Placeholder rules
|
|
116
128
|
|
|
@@ -167,6 +167,9 @@ Values can carry `<<ENV_NAME>>` placeholders. They're expanded:
|
|
|
167
167
|
|
|
168
168
|
Env var unset → the `<<ENV_NAME>>` literal stays verbatim, plus `[MdVars] Env var "X" not set — keeping <<X>> literal` in the server log. The Settings UI returns the literal too — the browser never sees the real env value.
|
|
169
169
|
|
|
170
|
+
- **Settings page hint**: each field in the `GET /api/settings` response carries `envMissing`, the names of `<<X>>` placeholders in its effective value whose variable is unset in the server process (names only, never values). The page shows an amber warning under that field's input naming the variables, and explains that background servers and the desktop app don't read `~/.bashrc` / `~/.zshrc`. The check runs against the server's `process.env`, not the user's shell.
|
|
171
|
+
- **Model provider credentials** (`api_key`, `access_key_id` / `secret_access_key`): an unset placeholder is never sent as the key. Every model call in that session fails before the request with one `account`-class error naming the provider, field and variable, with no retries; opening and reading the session still work. A variable added while the session is in memory takes effect once the session's agent is rebuilt. See [guide/secrets-and-credentials.md → Missing env var](../guide/secrets-and-credentials.md#missing-env-var).
|
|
172
|
+
|
|
170
173
|
## Agent visibility
|
|
171
174
|
|
|
172
175
|
Placeholder syntax in MD bodies (SKILL.md / AGENT.md):
|