@turmind/halo 2.0.5 → 2.0.6
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/app/page-2e03b23bcc38e934.js +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +2 -2
- package/bundled-docs/design/canvas-extensions.md +1 -1
- package/bundled-docs/dev/api.md +3 -3
- package/bundled-docs/requirements/chat.md +1 -1
- package/bundled-docs/requirements/editor.md +1 -1
- package/dist/index.js +39 -32
- package/package.json +1 -1
- package/admin-out/_next/static/chunks/app/page-cb05cd98d8f9042a.js +0 -1
- /package/admin-out/_next/static/{F7kFetjUGIXr5T6aTQAtP → l5P7Pjp-jJ_CFv3zMF4cK}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{F7kFetjUGIXr5T6aTQAtP → l5P7Pjp-jJ_CFv3zMF4cK}/_ssgManifest.js +0 -0
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--l5P7Pjp_jJ_CFv3zMF4cK--><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/2e6b00182ac44ae1.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-5c90f88e8111b360.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/241-75bac3bb8e7aca1b.js" async=""></script><script src="/_next/static/chunks/app/page-2e03b23bcc38e934.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-5c90f88e8111b360.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-5c90f88e8111b360.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[48891,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"241\",\"static/chunks/241-75bac3bb8e7aca1b.js\",\"989\",\"static/chunks/989-5c90f88e8111b360.js\",\"974\",\"static/chunks/app/page-2e03b23bcc38e934.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/2e6b00182ac44ae1.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"l5P7Pjp-jJ_CFv3zMF4cK\",\"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/2e6b00182ac44ae1.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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
4:I[63693,[],""]
|
|
5
5
|
5:I[74713,[],""]
|
|
6
6
|
6:I[5444,[],"ClientPageRoot"]
|
|
7
|
-
7:I[48891,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","241","static/chunks/241-75bac3bb8e7aca1b.js","989","static/chunks/989-5c90f88e8111b360.js","974","static/chunks/app/page-
|
|
7
|
+
7:I[48891,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","241","static/chunks/241-75bac3bb8e7aca1b.js","989","static/chunks/989-5c90f88e8111b360.js","974","static/chunks/app/page-2e03b23bcc38e934.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/2e6b00182ac44ae1.css","style"]
|
|
17
17
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
18
|
-
0:{"P":null,"b":"
|
|
18
|
+
0:{"P":null,"b":"l5P7Pjp-jJ_CFv3zMF4cK","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/2e6b00182ac44ae1.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"}]]
|
|
@@ -210,7 +210,7 @@ A viewer extension (emulator, model viewer) otherwise sees only its own file or
|
|
|
210
210
|
|
|
211
211
|
- **`fs` with `scope`** (`extension-host-logic.ts` `onFsFrame`): `workspace` → the same `/files/*` read routes with a workspace-relative path; `system` → admin-cookie routes by absolute path: `read` → `GET /fs/raw` (streamed bytes), `stat` → `GET /fs/stat`, `list` → `GET /fs/browse?files=1` (name + type per entry). Validation happens before any request: `isSystemPath` (POSIX `/…` or `C:/…`, forward slashes, no NUL, no empty / `.` / `..` segment); unknown scope or op, a missing capability, or `write` / `append` → `fs-result{denied}`; a bad path → `invalid-path`.
|
|
212
212
|
- **`pick`**: opens `FilePicker` (`shared/components/file-picker.tsx`) — a whole-machine browser with Workspace and Home shortcuts, a path box, a breadcrumb and the `accept` filter (directories always show; keyboard: arrows / Enter / Backspace for up / Esc cancels, the capture listener swallowing Esc before maximize sees it). One picker at a time, tied to the iframe attempt that asked; a reply for a since-remounted iframe is dropped. If the viewer is in its own fullscreen the host first drops back one level, so the picker isn't covered. On pick, the host `stat`s both the file and the workspace and compares **realpaths**, so a symlinked workspace or ROM dir lands on the right side: inside → workspace-relative `path`, else the absolute one. Closing it answers `cancelled`.
|
|
213
|
-
- **Hidden-path guard** (`routes/files.ts`): `/fs/stat`, `/fs/raw
|
|
213
|
+
- **Hidden-path guard** (`routes/files.ts`): `/fs/stat`, `/fs/raw`, `/fs/browse?files=1` and `/files/download` given an absolute path all refuse the sandbox hidden lists (`~/.halo/secrets`, `~/.ssh`, `.git-credentials`, the global dbs …) via `isHiddenHostPath` (exported from `tools/sandbox.ts`; no second list), judged on the **realpath** so a symlink can't reach them. A refused path answers with the same 404 and message as a missing file, and a listing silently omits hidden entries. `/fs/browse?files=1` lists the same noise-filtered tree as `/files/tree` but with dot-entries shown and symlinks counted as what they point at; `sizes=1` adds file sizes. The default folder-picker response is unchanged. See [dev/api.md → Filesystem browse](../dev/api.md#filesystem-browse-not-project-scoped--for-the-workspace-picker).
|
|
214
214
|
- Settings → Extensions shows a "Reads files on this machine" tag for an extension that declares it.
|
|
215
215
|
|
|
216
216
|
### Platforms
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -35,7 +35,7 @@ File: `packages/server/src/routes/files.ts`
|
|
|
35
35
|
| GET | `/api/files/search?projectId=&q=[&limit=][&dirsOnly=1]` | Recursive name search. Default limit 200, max 1000. Scans up to 50000 entries then truncates; returns `{matches, truncated}`. `dirsOnly=1` matches directories instead of files (powers the chat `@scope` directory completion). |
|
|
36
36
|
| GET | `/api/files?path=&projectId=` | Read file content (max 10 MB) |
|
|
37
37
|
| GET | `/api/files/stat?path=&projectId=` | Lightweight mtime + size |
|
|
38
|
-
| GET | `/api/files/download?path=&projectId=&inline=` | Download or inline-preview — streams the file (no full read into memory), supports `Range` (206 Partial Content) so `<video>`/`<audio>` can seek + partial-load; aborts the read if the client disconnects. Missing file → 404 `{error:'File not found'}
|
|
38
|
+
| GET | `/api/files/download?path=&projectId=&inline=` | Download or inline-preview — streams the file (no full read into memory), supports `Range` (206 Partial Content) so `<video>`/`<audio>` can seek + partial-load; aborts the read if the client disconnects. A **relative** `path` is confined to the workspace (`..` out of it → 403). An **absolute** `path` may be any file on the host, under `/api/fs/raw`'s rules: no `..` segment (→ 400), judged by realpath, the sandbox hidden lists answer like a missing file; admin cookie only (this is how chat `MEDIA:` chips load files from other workspaces / temp dirs). `projectId` is still required. Missing or hidden file → 404 `{error:'File not found'}`; a directory → 400 |
|
|
39
39
|
| GET | `/api/files/download-folder?path=&projectId=[&include=excluded][&estimate=1]` | Folder → one zip, built by yazl while it streams (zip64, UTF-8 names; already-compressed extensions stored, the rest deflated; one file open at a time, paced by backpressure; never staged on disk or in memory). `path` must be a directory inside the project, `''` = workspace root; it may itself be a folder symlink inside the workspace (resolved, then checked like any target); no `/tmp` exception. `Content-Disposition: attachment` with RFC 5987 `filename*` = `<folder name>.zip` (root → workspace dir name); no `Content-Length`, no Range. **Excluded by default**: any *folder* whose name is in `IGNORED_SEGMENTS` (`ws/file-watcher.ts`); `include=excluded` brings them back. **Always excluded**: `isHiddenHostPath` / `isHiddenWorkspacePath` (also against a nested `.halo/`), plus common credential stores (~/.kube, cloud CLI configs, shell histories — a zip-only list in `folder-zip.ts`, matched relative to the home dir), `.halo/settings.yaml`, `.env` / `.env.*`. Names a zip can't hold — any containing `\`, or a top-level `X:` prefix — are skipped as `unreadable`. Directory symlinks never followed; file symlinks only when the realpath stays inside the workspace and passes those checks. `estimate=1` → `{files, bytes, excluded: {<reason>: n}, ignoredNames: {<folder>: n}}` with reasons `ignored` / `blocked` / `symlink` / `unreadable`, same rules, no content read. More than 200,000 files or a walk over 10 s → **413** `{error}` (both modes, before any header). A blocked target → 403. Anything left out → `_halo-excluded.txt` at the zip root (grouped by reason, a pruned dir listed once, 1000 paths per reason + a count). Mid-stream (status already sent): a file that fails to open is skipped, a read error partway through truncates that entry — both are listed there. A client disconnect stops the walk and destroys the zip stream |
|
|
40
40
|
| PUT | `/api/files` | Save file (body: `{path, content, projectId}`) |
|
|
41
41
|
| PUT | `/api/files/raw?path=&projectId=[&expectMtime=][&create=1][&append=1][&root=]` | Replace an **existing** file's bytes with the raw request body (binary-safe; `PUT /api/files` is utf-8 text only). Used by canvas preview extensions' save path. 404 if the file doesn't exist (save never creates) unless `create=1`; 400 for a directory. `expectMtime` is the `modifiedAt` the client loaded from `/api/files/stat` (float ms — compared after `Math.round`): when the on-disk mtime differs → **409** `{error:'conflict', mtime, size}` so the client can offer overwrite (retry without / with the new `expectMtime`). `create=1` creates a missing file (parent dirs `mkdir -p`); `append=1` appends instead of overwriting (`expectMtime` ignored; with `create=1` a missing file is created first) — the bundle extensions' `fs` write / append. `root=<dir>` (workspace-relative; the bundle host always sends its bundle path): `<dir>` must be an existing directory and `path` strictly inside it, else **404** `{error:'Bundle root not found'}` with nothing created. Success → `{ok, path, mtime, size}` |
|
|
@@ -47,7 +47,7 @@ File: `packages/server/src/routes/files.ts`
|
|
|
47
47
|
|
|
48
48
|
**Inline Content-Type** for images comes from core's shared extension→MIME table (`imageMimeFromExt`, `packages/core/src/media/mime.ts`) on both `/api/files/download?inline=1` and `/api/web/file`; each route keeps only its own non-image entries (pdf / html / video / audio) and falls back to `application/octet-stream`. That table is why `.bmp` now serves as `image/bmp` on both — the two routes used to carry their own copies and `/api/web/file` returned `application/octet-stream` for it. `.svg` / `.ico` / `.avif` get a real image Content-Type when served, but are deliberately not "photos" (they're outside `IMAGE_EXTS`) so they never reach a vision model or a channel's sendPhoto.
|
|
49
49
|
|
|
50
|
-
All file operations validate the path stays inside the project root (prevents path traversal). The check (`validatePath` in `routes/workspace-path.ts`, shared with `data-preview.ts` and `git.ts`) realpaths both sides — longest existing ancestor plus the re-appended tail — so a symlink inside the workspace pointing outside it is rejected too, not just lexical `../`. One exception: `GET /api/files/download` (not `download-folder`) also accepts absolute
|
|
50
|
+
All file operations validate the path stays inside the project root (prevents path traversal). The check (`validatePath` in `routes/workspace-path.ts`, shared with `data-preview.ts` and `git.ts`) realpaths both sides — longest existing ancestor plus the re-appended tail — so a symlink inside the workspace pointing outside it is rejected too, not just lexical `../`. One exception: `GET /api/files/download` (not `download-folder`) also accepts an absolute path anywhere on the host, with the same rules and trust boundary as `/api/fs/raw` (below), so chat media chips can preview / download agent files outside the active workspace.
|
|
51
51
|
|
|
52
52
|
### Filesystem browse (not project-scoped — for the workspace picker)
|
|
53
53
|
|
|
@@ -60,7 +60,7 @@ All file operations validate the path stays inside the project root (prevents pa
|
|
|
60
60
|
| GET | `/api/fs/raw?path=/abs` | `fs-read` `scope: 'system'` read: the file's bytes, streamed (`application/octet-stream` + `Content-Length`; the stream is destroyed if the client aborts). Same path rules and error shapes as `/api/fs/stat`; 400 `not a file` for a directory. |
|
|
61
61
|
| POST | `/api/fs/workspace/resolve` | Resolve `{path}` to an absolute path and run `ensureWorkspaceHalo()`. Returns `{id, path}`. Used by the workspace-picker on switch. 404 `path not found` when it doesn't exist; 400 `not a directory` for a file; 400 `filesystem root cannot be a workspace` for `/` (or a drive root) — `ensureWorkspaceHalo()` writes a whole `.halo/` scaffold, so those two shapes are refused before the write. Deeper system trees (`/usr`, `/etc`) are deliberately **not** banned. |
|
|
62
62
|
|
|
63
|
-
Absolute paths only. Purpose: Explorer's workspace picker and switching validation. `/api/fs/browse` is intentionally unrestricted (it's a read-only picker over the server's filesystem — same trust boundary as the admin cookie). Reading/writing files still goes through `/api/files/*` and remains project-sandboxed
|
|
63
|
+
Absolute paths only. Purpose: Explorer's workspace picker and switching validation. `/api/fs/browse` is intentionally unrestricted (it's a read-only picker over the server's filesystem — same trust boundary as the admin cookie). Reading/writing files still goes through `/api/files/*` and remains project-sandboxed, except `GET /api/files/download` with an absolute path, which follows the `/api/fs/raw` rules below. The read-only exceptions are that and the `/api/fs/stat` + `/api/fs/raw` + `/api/fs/browse?files=1` trio behind the `fs-read` extension capability: they take absolute paths (no `..` segment) and refuse the sandbox hidden lists (`~/.halo/secrets`, `~/.ssh`, `.git-credentials`, the global dbs … — `isHiddenHostPath` in `tools/sandbox.ts`, the same lists the agent sandbox masks) by **realpath**, so a symlink can't reach them. They are admin-cookie routes, never in `PUBLIC_PATHS`; `fs-read` itself is host-side UX gating (the extension iframe is same-origin and could call them directly), which is why the guard lives on the routes.
|
|
64
64
|
|
|
65
65
|
## Data Preview
|
|
66
66
|
|
|
@@ -120,7 +120,7 @@ A Scissors button in the toolbar's input group (upload · **screenshot** · scre
|
|
|
120
120
|
- Shown only when a frame source exists and the selected agent's model accepts images — same gate as live capture (no button on mobile browsers).
|
|
121
121
|
|
|
122
122
|
### Inline media chips
|
|
123
|
-
Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). The modal has a Download button (top-right, next to close) for image/video/audio; the media URL carries a per-open cache-buster (`&t=<timestamp>`) so overwritten files (same path, new bytes) always show current content.
|
|
123
|
+
Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). The modal has a Download button (top-right, next to close) for image/video/audio; the media URL carries a per-open cache-buster (`&t=<timestamp>`) so overwritten files (same path, new bytes) always show current content. Any absolute path is previewable and downloadable — including files in another workspace or the OS temp dir; sensitive dirs (`~/.halo/secrets`, `~/.ssh`, …) and missing files show the preview's failed state. Only a relative path, or no active workspace, degrades to a non-clickable chip.
|
|
124
124
|
|
|
125
125
|
Images in chat (the modal's full-size image, a user message's screenshot strip and its zoom, markdown images in replies) never show the browser's broken-image icon: a themed pulsing box while loading, the image once loaded, and an `ImageOff` icon + "Image unavailable" / 「图片无法加载」 box if it fails (`shared/components/safe-image.tsx`).
|
|
126
126
|
|
|
@@ -87,7 +87,7 @@ File types without a built-in plugin can be handled by an **installed extension*
|
|
|
87
87
|
### Renderable text formats (Markdown / HTML)
|
|
88
88
|
Markdown and HTML open as text in Monaco *and* have a rendered view — Canvas defaults to the **rendered** view since the primary audience is an AI generating reports / pages for humans to read.
|
|
89
89
|
- Each pane's tab bar shows an **Edit / Preview** toggle for the active Markdown / HTML tab; the state is per pane, shared by both formats
|
|
90
|
-
- **Markdown** → `MarkdownPreview` (react-markdown + GFM; relative image `src` rewritten to the download endpoint so local images work)
|
|
90
|
+
- **Markdown** → `MarkdownPreview` (react-markdown + GFM; relative image `src` rewritten to the download endpoint so local images work; an absolute `src` may point anywhere on the server except Halo's hidden secrets)
|
|
91
91
|
- **HTML** → sandboxed iframe (`sandbox="allow-scripts allow-same-origin"`: the page's own JS runs, so a self-contained page renders live; top-navigation, forms and pop-ups stay blocked. Trusted like opening the file in Monaco — it comes from the user's own workspace)
|
|
92
92
|
- Toggle to Edit → Monaco source, Cmd+S saves as usual
|
|
93
93
|
|
package/dist/index.js
CHANGED
|
@@ -15857,7 +15857,7 @@ async function initObservability(haloProcess = "server") {
|
|
|
15857
15857
|
return true;
|
|
15858
15858
|
}
|
|
15859
15859
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
15860
|
-
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.0.
|
|
15860
|
+
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.0.6", haloProcess);
|
|
15861
15861
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, halo.process=${haloProcess}, capture_content=${config.observability.captureContent})`);
|
|
15862
15862
|
return true;
|
|
15863
15863
|
}
|
|
@@ -23626,6 +23626,24 @@ function createFileRoutes(opts = {}) {
|
|
|
23626
23626
|
m4a: "audio/mp4",
|
|
23627
23627
|
wasm: "application/wasm"
|
|
23628
23628
|
};
|
|
23629
|
+
function systemPath(target) {
|
|
23630
|
+
if (!target || !path60.isAbsolute(target) || target.split(/[\\/]/).includes(".."))
|
|
23631
|
+
return null;
|
|
23632
|
+
return path60.resolve(target);
|
|
23633
|
+
}
|
|
23634
|
+
const fsErrorStatus = (err) => {
|
|
23635
|
+
const code = err.code;
|
|
23636
|
+
return code === "ENOENT" || code === "ENOTDIR" ? 404 : code === "EACCES" || code === "EPERM" ? 403 : 500;
|
|
23637
|
+
};
|
|
23638
|
+
function hiddenAsMissing(syscall, target) {
|
|
23639
|
+
return Object.assign(new Error(`ENOENT: no such file or directory, ${syscall} '${target}'`), { code: "ENOENT" });
|
|
23640
|
+
}
|
|
23641
|
+
async function readableRealPath(target) {
|
|
23642
|
+
const real = await fs51.realpath(target);
|
|
23643
|
+
if (isHiddenHostPath(real))
|
|
23644
|
+
throw hiddenAsMissing("realpath", target);
|
|
23645
|
+
return real;
|
|
23646
|
+
}
|
|
23629
23647
|
app2.get("/files/download", async (c) => {
|
|
23630
23648
|
try {
|
|
23631
23649
|
const filePath = c.req.query("path");
|
|
@@ -23638,16 +23656,23 @@ function createFileRoutes(opts = {}) {
|
|
|
23638
23656
|
if (!projectPath) {
|
|
23639
23657
|
return c.json({ error: "Project not found" }, 404);
|
|
23640
23658
|
}
|
|
23641
|
-
|
|
23642
|
-
if (
|
|
23643
|
-
|
|
23659
|
+
let absolutePath;
|
|
23660
|
+
if (path60.isAbsolute(filePath)) {
|
|
23661
|
+
const target = systemPath(filePath);
|
|
23662
|
+
if (!target)
|
|
23663
|
+
return c.json({ error: "absolute path without .. required" }, 400);
|
|
23664
|
+
absolutePath = await readableRealPath(target);
|
|
23665
|
+
} else {
|
|
23666
|
+
if (!validatePath(filePath, projectPath)) {
|
|
23667
|
+
return c.json({ error: "Path traversal not allowed" }, 403);
|
|
23668
|
+
}
|
|
23669
|
+
absolutePath = path60.resolve(projectPath, filePath);
|
|
23644
23670
|
}
|
|
23645
|
-
const absolutePath = isTmp ? path60.resolve(filePath) : path60.resolve(projectPath, filePath);
|
|
23646
23671
|
const stat = await fs51.stat(absolutePath);
|
|
23647
23672
|
if (stat.isDirectory()) {
|
|
23648
23673
|
return c.json({ error: "Cannot download a directory" }, 400);
|
|
23649
23674
|
}
|
|
23650
|
-
const fileName = path60.basename(
|
|
23675
|
+
const fileName = path60.basename(absolutePath);
|
|
23651
23676
|
const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
|
|
23652
23677
|
const mime = inline ? imageMimeFromExt(ext) ?? MIME_MAP[ext] ?? "application/octet-stream" : "application/octet-stream";
|
|
23653
23678
|
const disposition = inline ? `inline; filename="${encodeURIComponent(fileName)}"` : `attachment; filename="${encodeURIComponent(fileName)}"`;
|
|
@@ -23690,11 +23715,12 @@ function createFileRoutes(opts = {}) {
|
|
|
23690
23715
|
}
|
|
23691
23716
|
return new Response(webStream, { status, headers });
|
|
23692
23717
|
} catch (err) {
|
|
23693
|
-
|
|
23718
|
+
const status = fsErrorStatus(err);
|
|
23719
|
+
if (status === 404)
|
|
23694
23720
|
return c.json({ error: "File not found" }, 404);
|
|
23695
23721
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
23696
23722
|
console.log(`[Files] Error downloading: ${errorMessage}`);
|
|
23697
|
-
return c.json({ error: errorMessage },
|
|
23723
|
+
return c.json({ error: errorMessage }, status);
|
|
23698
23724
|
}
|
|
23699
23725
|
});
|
|
23700
23726
|
app2.get("/files/download-folder", async (c) => {
|
|
@@ -23874,24 +23900,6 @@ function createFileRoutes(opts = {}) {
|
|
|
23874
23900
|
return c.json({ error: err.message }, 404);
|
|
23875
23901
|
}
|
|
23876
23902
|
});
|
|
23877
|
-
function systemPath(target) {
|
|
23878
|
-
if (!target || !path60.isAbsolute(target) || target.split(/[\\/]/).includes(".."))
|
|
23879
|
-
return null;
|
|
23880
|
-
return path60.resolve(target);
|
|
23881
|
-
}
|
|
23882
|
-
const fsErrorStatus = (err) => {
|
|
23883
|
-
const code = err.code;
|
|
23884
|
-
return code === "ENOENT" || code === "ENOTDIR" ? 404 : code === "EACCES" || code === "EPERM" ? 403 : 500;
|
|
23885
|
-
};
|
|
23886
|
-
function hiddenAsMissing(syscall, target) {
|
|
23887
|
-
return Object.assign(new Error(`ENOENT: no such file or directory, ${syscall} '${target}'`), { code: "ENOENT" });
|
|
23888
|
-
}
|
|
23889
|
-
async function readableRealPath(target) {
|
|
23890
|
-
const real = await fs51.realpath(target);
|
|
23891
|
-
if (isHiddenHostPath(real))
|
|
23892
|
-
throw hiddenAsMissing("realpath", target);
|
|
23893
|
-
return real;
|
|
23894
|
-
}
|
|
23895
23903
|
app2.get("/fs/stat", async (c) => {
|
|
23896
23904
|
const target = systemPath(c.req.query("path"));
|
|
23897
23905
|
if (!target)
|
|
@@ -23928,7 +23936,6 @@ var init_files2 = __esm({
|
|
|
23928
23936
|
"../server/dist/routes/files.js"() {
|
|
23929
23937
|
"use strict";
|
|
23930
23938
|
init_dist();
|
|
23931
|
-
init_media();
|
|
23932
23939
|
init_workspace_path();
|
|
23933
23940
|
init_sandbox();
|
|
23934
23941
|
init_folder_zip();
|
|
@@ -31001,7 +31008,7 @@ function buildCard(workspace, interfaceUrl, caps) {
|
|
|
31001
31008
|
return {
|
|
31002
31009
|
name: raw.name,
|
|
31003
31010
|
description: raw.description,
|
|
31004
|
-
version: typeof raw.version === "string" ? raw.version : "2.0.
|
|
31011
|
+
version: typeof raw.version === "string" ? raw.version : "2.0.6",
|
|
31005
31012
|
supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
|
|
31006
31013
|
capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
|
|
31007
31014
|
...caps.tokenAuth === false ? {} : {
|
|
@@ -33760,7 +33767,7 @@ function createCoreChecker(opts) {
|
|
|
33760
33767
|
}
|
|
33761
33768
|
function getCoreStatus(force) {
|
|
33762
33769
|
checker ??= createCoreChecker({
|
|
33763
|
-
haloVersion: "2.0.
|
|
33770
|
+
haloVersion: "2.0.6",
|
|
33764
33771
|
fetchLatest: fetchNpmLatest,
|
|
33765
33772
|
onChange: (status) => broadcast({ type: "updates:changed", scope: "core", status })
|
|
33766
33773
|
});
|
|
@@ -33928,7 +33935,7 @@ function hubBase() {
|
|
|
33928
33935
|
}
|
|
33929
33936
|
function serverChecker() {
|
|
33930
33937
|
checker2 ??= createExtensionsChecker({
|
|
33931
|
-
haloVersion: "2.0.
|
|
33938
|
+
haloVersion: "2.0.6",
|
|
33932
33939
|
installed: () => {
|
|
33933
33940
|
const s = getSnapshot();
|
|
33934
33941
|
return [...s.extensions, ...s.disabled];
|
|
@@ -40213,7 +40220,7 @@ var init_dist2 = __esm({
|
|
|
40213
40220
|
init_git_ssh();
|
|
40214
40221
|
__dirname3 = path90.dirname(fileURLToPath3(import.meta.url));
|
|
40215
40222
|
PORT = config.server.port;
|
|
40216
|
-
HALO_VERSION = "2.0.
|
|
40223
|
+
HALO_VERSION = "2.0.6";
|
|
40217
40224
|
PROJECT_ROOT = findProjectRoot();
|
|
40218
40225
|
HALO_HOME4 = path90.join(homedir25(), ".halo");
|
|
40219
40226
|
GIT_SHA = readGitSha();
|
|
@@ -42391,7 +42398,7 @@ function installUserUnit(opts, run = runCommand) {
|
|
|
42391
42398
|
}
|
|
42392
42399
|
|
|
42393
42400
|
// src/index.ts
|
|
42394
|
-
var VERSION = "2.0.
|
|
42401
|
+
var VERSION = "2.0.6";
|
|
42395
42402
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
42396
42403
|
|
|
42397
42404
|
Commands:
|