@turmind/halo 0.1.9
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/LICENSE +21 -0
- package/README.md +143 -0
- package/admin-out/404.html +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
- package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
- package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
- package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
- package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
- package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
- package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
- package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
- package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
- package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
- package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
- package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
- package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
- package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
- package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
- package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
- package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
- package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
- package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
- package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
- package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
- package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
- package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
- package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
- package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
- package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
- package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
- package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
- package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/admin-out/favicon.ico +0 -0
- package/admin-out/icon.svg +18 -0
- package/admin-out/index.html +1 -0
- package/admin-out/index.txt +23 -0
- package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
- package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
- package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
- package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
- package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
- package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
- package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
- package/admin-out/monaco/vs/editor/editor.main.css +1 -0
- package/admin-out/monaco/vs/editor/editor.main.js +5 -0
- package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
- package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
- package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
- package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
- package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
- package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
- package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
- package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
- package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
- package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
- package/admin-out/monaco/vs/loader.js +1368 -0
- package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
- package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
- package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
- package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.js.js +10 -0
- package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
- package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
- package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
- package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
- package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
- package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
- package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
- package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
- package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
- package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
- package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
- package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
- package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
- package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
- package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
- package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
- package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
- package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
- package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
- package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
- package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
- package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
- package/bin/halo.js +9 -0
- package/bundled-docs/design/agent.md +145 -0
- package/bundled-docs/design/architecture.md +270 -0
- package/bundled-docs/design/background-dispatch.md +151 -0
- package/bundled-docs/design/command.md +58 -0
- package/bundled-docs/design/cron.md +88 -0
- package/bundled-docs/design/evolution.md +383 -0
- package/bundled-docs/design/express-self.md +145 -0
- package/bundled-docs/design/feishu.md +224 -0
- package/bundled-docs/design/halo-city.md +112 -0
- package/bundled-docs/design/prompt-system.md +411 -0
- package/bundled-docs/design/session.md +321 -0
- package/bundled-docs/design/slack.md +212 -0
- package/bundled-docs/design/storage.md +578 -0
- package/bundled-docs/design/telegram.md +161 -0
- package/bundled-docs/design/web.md +222 -0
- package/bundled-docs/design/wechat.md +146 -0
- package/bundled-docs/design/ws.md +144 -0
- package/bundled-docs/dev/acp-adapter.md +337 -0
- package/bundled-docs/dev/add-channel.md +376 -0
- package/bundled-docs/dev/add-model-provider.md +386 -0
- package/bundled-docs/dev/api.md +686 -0
- package/bundled-docs/dev/deploy.md +198 -0
- package/bundled-docs/dev/desktop-packaging.md +327 -0
- package/bundled-docs/dev/env.md +158 -0
- package/bundled-docs/dev/logging.md +85 -0
- package/bundled-docs/dev/previews.md +225 -0
- package/bundled-docs/dev/tools.md +331 -0
- package/bundled-docs/guide/README.md +29 -0
- package/bundled-docs/guide/agents.md +264 -0
- package/bundled-docs/guide/channels/README.md +62 -0
- package/bundled-docs/guide/channels/acp.md +150 -0
- package/bundled-docs/guide/channels/feishu.md +154 -0
- package/bundled-docs/guide/channels/slack.md +156 -0
- package/bundled-docs/guide/channels/telegram.md +112 -0
- package/bundled-docs/guide/channels/web.md +149 -0
- package/bundled-docs/guide/channels/wechat.md +110 -0
- package/bundled-docs/guide/chat.md +112 -0
- package/bundled-docs/guide/cli.md +197 -0
- package/bundled-docs/guide/getting-started.md +93 -0
- package/bundled-docs/guide/secrets-and-credentials.md +161 -0
- package/bundled-docs/guide/sessions.md +104 -0
- package/bundled-docs/guide/skills.md +302 -0
- package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
- package/bundled-docs/guide/workspace.md +131 -0
- package/bundled-docs/plans/cron-tasks.md +362 -0
- package/bundled-docs/plans/design-suggestions.md +203 -0
- package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
- package/bundled-docs/plans/launch-plan.md +111 -0
- package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
- package/bundled-docs/plans/remote-mac-worker.md +196 -0
- package/bundled-docs/plans/roadmap.md +40 -0
- package/bundled-docs/plans/self-evolution.md +1379 -0
- package/bundled-docs/plans/todo.md +32 -0
- package/bundled-docs/plans/weixin-channel.md +255 -0
- package/bundled-docs/requirements/agent.md +92 -0
- package/bundled-docs/requirements/chat.md +81 -0
- package/bundled-docs/requirements/command.md +130 -0
- package/bundled-docs/requirements/editor.md +82 -0
- package/bundled-docs/requirements/explorer.md +89 -0
- package/bundled-docs/requirements/overview.md +62 -0
- package/bundled-docs/requirements/session.md +73 -0
- package/bundled-docs/requirements/settings.md +224 -0
- package/bundled-docs/requirements/skill.md +125 -0
- package/bundled-docs/requirements/source-control.md +82 -0
- package/bundled-docs/requirements/terminal.md +118 -0
- package/bundled-docs/requirements/workspace.md +80 -0
- package/bundled-docs/test/docs-seeding.md +101 -0
- package/bundled-docs/test/session.md +374 -0
- package/dist/index.js +26750 -0
- package/package.json +67 -0
- package/templates/INSTRUCTIONS.md +25 -0
- package/templates/agents/__apply_agent__/AGENT.md +229 -0
- package/templates/agents/__apply_agent__/agent.yaml +30 -0
- package/templates/agents/__evo_agent__/AGENT.md +323 -0
- package/templates/agents/__evo_agent__/agent.yaml +29 -0
- package/templates/agents/__score__/AGENT.md +177 -0
- package/templates/agents/__score__/agent.yaml +29 -0
- package/templates/agents/deep-executor/AGENT.md +60 -0
- package/templates/agents/deep-executor/agent.yaml +25 -0
- package/templates/agents/default/AGENT.md +38 -0
- package/templates/agents/default/agent.yaml +51 -0
- package/templates/agents/executor/AGENT.md +38 -0
- package/templates/agents/executor/agent.yaml +25 -0
- package/templates/aliases.yaml +26 -0
- package/templates/canvas/self.html +660 -0
- package/templates/config.yaml +69 -0
- package/templates/models/anthropic.yaml +47 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
- package/templates/models/aws-bedrock-mantle.yaml +89 -0
- package/templates/models/deepseek.yaml +62 -0
- package/templates/models/doubao.yaml +137 -0
- package/templates/models/hunyuan.yaml +56 -0
- package/templates/models/kimi.yaml +41 -0
- package/templates/models/mimo-token-plan-china.yaml +61 -0
- package/templates/models/minimax.yaml +80 -0
- package/templates/models/openai.yaml +51 -0
- package/templates/models/qwen.yaml +78 -0
- package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
- package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
- package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
- package/templates/prompts/all/TOOL_SHELL.md +4 -0
- package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
- package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
- package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
- package/templates/schema.sql +30 -0
- package/templates/skills/acp/SKILL.md +236 -0
- package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
- package/templates/skills/acp/templates/ask.py +176 -0
- package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
- package/templates/skills/agent/SKILL.md +128 -0
- package/templates/skills/aws-knowledge/SKILL.md +82 -0
- package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
- package/templates/skills/cron/SKILL.md +217 -0
- package/templates/skills/cron/manage-cron.py +434 -0
- package/templates/skills/halo/SKILL.md +98 -0
- package/templates/skills/nova-web-search/SKILL.md +65 -0
- package/templates/skills/self/SKILL.md +139 -0
- package/templates/skills/send-file/SKILL.md +28 -0
- package/templates/skills/send-file/feishu.md +16 -0
- package/templates/skills/send-file/slack.md +7 -0
- package/templates/skills/send-file/telegram.md +9 -0
- package/templates/skills/send-file/web.md +6 -0
- package/templates/skills/send-file/wechat.md +8 -0
- package/templates/skills/skill/SKILL.md +131 -0
- package/templates/skills/workspace/SKILL.md +28 -0
- package/templates/skills/workspace/setup-tidy.md +265 -0
- package/templates/skills/workspace/share.md +187 -0
- package/templates/skills/workspace/stage.py +487 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Share Workspace
|
|
2
|
+
|
|
3
|
+
Stage the current workspace's `.halo/` into a self-contained bundle, let the user review what
|
|
4
|
+
made the cut, then zip it up. The bundle is designed to drop into any other project and yield
|
|
5
|
+
the same Halo behavior — agents, skills, prompts, instructions — minus anything personal.
|
|
6
|
+
|
|
7
|
+
What's included: **all** workspace-scope agents/skills, plus any **global**-scope ones an
|
|
8
|
+
included agent references that the workspace doesn't already have (workspace wins on id clash).
|
|
9
|
+
Platform built-ins (default / executor / send-file / create-skill / …) are **not** bundled —
|
|
10
|
+
the receiver's own server seeds those on startup, so shipping them would be redundant and could
|
|
11
|
+
leak local edits. So the receiver gets: your customizations + needed global extensions + their
|
|
12
|
+
own fresh built-ins = a workspace that runs as-is.
|
|
13
|
+
|
|
14
|
+
A Python helper (`stage.py`, sibling of this file) does the heavy lifting:
|
|
15
|
+
file selection, secret redaction, manifest. You orchestrate it and confirm with the user.
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
### 1. Locate the helper
|
|
20
|
+
|
|
21
|
+
The script lives at one of these paths (workspace wins if both exist):
|
|
22
|
+
- \`<workspace-root>/.halo/skills/workspace/stage.py\`
|
|
23
|
+
- \`~/.halo/global/skills/workspace/stage.py\`
|
|
24
|
+
|
|
25
|
+
The two paths above are deterministic, so \`glob\` / \`find\` searches add latency
|
|
26
|
+
without finding anything new. \`file_list\` the workspace skill dir first; if
|
|
27
|
+
no \`stage.py\` there, fall back to the global path.
|
|
28
|
+
|
|
29
|
+
### 2. Run the staging script
|
|
30
|
+
|
|
31
|
+
The argument is the **workspace root** (the directory containing \`.halo/\`). This is the
|
|
32
|
+
project the user is currently in — same path you'd see in \`/workspace\`. Passing \`/home/<user>\`
|
|
33
|
+
or any parent traverses far more than the workspace and pulls in noise.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
shell_exec: python3 <stage.py-path> <workspace-root>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Output is a JSON object on stdout summarizing what was staged. The full file contents go to
|
|
40
|
+
`<workspace>/.halo/tmp/share/staged/`.
|
|
41
|
+
|
|
42
|
+
### 3. Read the manifest and summarize for the user
|
|
43
|
+
|
|
44
|
+
The script writes `<staged>/share-manifest.json`. Read it and present a digest. Keep the
|
|
45
|
+
output focused on what's *in* the bundle — agents, skills, redactions, suspicious flags.
|
|
46
|
+
You don't need to echo the staged path or generation timestamp back to the user.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Agents (2):
|
|
50
|
+
- code-reviewer (workspace)
|
|
51
|
+
- researcher (global)
|
|
52
|
+
|
|
53
|
+
Skills (3):
|
|
54
|
+
- repo-conventions (workspace)
|
|
55
|
+
- aws-knowledge (global)
|
|
56
|
+
- nova-web-search (global)
|
|
57
|
+
|
|
58
|
+
Instructions: .halo/INSTRUCTIONS.md (global fallback)
|
|
59
|
+
INDEX.md → 2 docs followed: docs/architecture/auth.md, docs/dev/api.md
|
|
60
|
+
|
|
61
|
+
Prompts: all=global-fallback, root=global-fallback
|
|
62
|
+
|
|
63
|
+
Redactions:
|
|
64
|
+
• 1 secret field auto-replaced with {{...params.api_key}} placeholder
|
|
65
|
+
• 0 unambiguous leaks redacted in markdown
|
|
66
|
+
• 2 suspicious strings flagged for review (see below)
|
|
67
|
+
|
|
68
|
+
Excluded:
|
|
69
|
+
• USER.md (3 files — personal profile)
|
|
70
|
+
• memory/ (12 files)
|
|
71
|
+
• assets/ (84 files)
|
|
72
|
+
• sessions/, logs/, tmp/, *.db, settings.yaml — always
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Lead with the most important callouts: anything in the **redactions** section, anything in
|
|
76
|
+
**missing_skills**, and anything that looks wrong (unexpected agent included, expected one
|
|
77
|
+
missing).
|
|
78
|
+
|
|
79
|
+
### 4. Walk through suspicious markdown findings
|
|
80
|
+
|
|
81
|
+
For each entry in `redactions.markdown_suspicious`, read the actual line from the staged file
|
|
82
|
+
and show the user. Use the **full word** ("keep" / "redact" / "edit") in the prompt — single
|
|
83
|
+
letters confuse the model when the user replies with just "k". Stay in the share
|
|
84
|
+
flow until all suspicious entries are resolved:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
⚠️ Suspicious match — needs your call:
|
|
88
|
+
|
|
89
|
+
File: .halo/agents/default/AGENT.md, line 42
|
|
90
|
+
Pattern: email
|
|
91
|
+
Line: "contact alice@example.com for the staging creds"
|
|
92
|
+
|
|
93
|
+
This looks like {your interpretation: e.g. "a real email" or "a doc placeholder"}.
|
|
94
|
+
|
|
95
|
+
How should I handle it?
|
|
96
|
+
- Reply **keep** — leave it as-is (it's a doc example / not sensitive)
|
|
97
|
+
- Reply **redact** — replace with [REDACTED:email]
|
|
98
|
+
- Reply **edit X** — change the snippet to X
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When the user replies with "keep" / "k" → record decision and move to the next finding.
|
|
102
|
+
When they reply with "redact" / "r" → `file_edit` the *staged* file (not the workspace
|
|
103
|
+
file), substituting the snippet with `[REDACTED:<pattern>]`.
|
|
104
|
+
When they reply with "edit ..." → apply their replacement via `file_edit`.
|
|
105
|
+
|
|
106
|
+
After every suspicious entry is resolved, summarize the decisions and move to step 5. **Do not
|
|
107
|
+
treat short responses (`k`, `r`, `keep`, etc.) as off-topic chat** — you are still inside the
|
|
108
|
+
flow until the user explicitly cancels or you finish step 7.
|
|
109
|
+
|
|
110
|
+
### 5. Hand control to the user
|
|
111
|
+
|
|
112
|
+
> "Bundle staged at `.halo/tmp/share/staged/`. Have a look — open files directly, edit anything
|
|
113
|
+
> sensitive, remove anything you don't want shared. When you're happy, tell me to zip it."
|
|
114
|
+
|
|
115
|
+
Wait for explicit confirmation before zipping. Auto-zipping at this step
|
|
116
|
+
skips the user's last review window for sensitive content.
|
|
117
|
+
|
|
118
|
+
### 6. Zip on confirmation
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd <workspace>/.halo/tmp/share/staged
|
|
122
|
+
zip -r ../workspace-$(date +%Y-%m-%d).zip .halo README.md
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Notes:
|
|
126
|
+
- `share-manifest.json` is intentionally **not** zipped — it's a review aid, not part of the bundle.
|
|
127
|
+
- `README.md` goes in the zip so the receiver knows what to do.
|
|
128
|
+
|
|
129
|
+
### 7. Report
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Bundle ready: .halo/tmp/share/workspace-2026-05-16.zip (147 KB)
|
|
133
|
+
|
|
134
|
+
To share: send the zip. Receiver unzips into their project root, then fills in any
|
|
135
|
+
{{...params.<key>}} placeholders (grep the unzipped tree to find them).
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## What gets included / excluded (rules the helper enforces)
|
|
139
|
+
|
|
140
|
+
**Included:**
|
|
141
|
+
- `<ws>/.halo/INSTRUCTIONS.md` chain (root + any sub-dir `.halo/INSTRUCTIONS.md`).
|
|
142
|
+
If the workspace has no root `INSTRUCTIONS.md`, falls back to `~/.halo/global/INSTRUCTIONS.md`.
|
|
143
|
+
- `<ws>/.halo/INDEX.md` plus only the `docs/...` paths it links to (other paths ignored).
|
|
144
|
+
- All workspace agents (`<ws>/.halo/agents/<id>/`), minus `USER.md`.
|
|
145
|
+
- Global agents that **are not in the workspace's `disabled_items`** AND **not overridden by a
|
|
146
|
+
workspace agent of the same id**. Those get copied into the bundle's `agents/` so the receiver
|
|
147
|
+
doesn't depend on having identical globals.
|
|
148
|
+
- Skills that are **referenced by an included agent's `skills:` list**. Workspace skill wins;
|
|
149
|
+
otherwise global skill (if not disabled).
|
|
150
|
+
- `prompts/{bootstrap,all,root}/` — workspace dir if it exists, else global fallback.
|
|
151
|
+
|
|
152
|
+
**Excluded (always):**
|
|
153
|
+
- Every `USER.md` (workspace, agent dirs, anywhere) — personal profile, never share.
|
|
154
|
+
- `memory/`, `assets/` — workspace-specific knowledge / data.
|
|
155
|
+
- `sessions/`, `logs/`, `tmp/`, `*.db`, `*.db-shm`, `*.db-wal` — runtime state.
|
|
156
|
+
- `~/.halo/secrets/settings.yaml` — secrets are redacted into agent.yaml/SKILL.md as
|
|
157
|
+
`{{<id>.params.<key>}}` placeholders; the receiver fills them in their own `settings.yaml`.
|
|
158
|
+
- Agents with `internal: true` in their `agent.yaml` (`__evo_agent__`, `__score__`,
|
|
159
|
+
`__apply_agent__`, etc.) — these are platform-internal, force-overwritten by the
|
|
160
|
+
receiver's own server on startup, so bundling them is redundant and can leak
|
|
161
|
+
experimental local edits.
|
|
162
|
+
|
|
163
|
+
## Sanitization
|
|
164
|
+
|
|
165
|
+
- **YAML field redaction (auto):** Inside `agent.yaml` and SKILL.md frontmatter, fields named
|
|
166
|
+
`api_key`, `secret`, `token`, `password`, `access_key`, `aws_secret_access_key`, `bot_token`,
|
|
167
|
+
`client_secret` (case-insensitive) get their values replaced with `{{<id>.params.<field>}}`
|
|
168
|
+
if the value is a literal (not already a `{{...}}` or `<<ENV>>` placeholder).
|
|
169
|
+
- **Markdown auto-redact:** AWS access key formats (`AKIA…`, `ASIA…`) are replaced with
|
|
170
|
+
`[REDACTED:<type>]`.
|
|
171
|
+
- **Markdown flag-only:** Emails, bearer-style headers, and long base64-ish tokens are
|
|
172
|
+
flagged in the manifest but **not auto-replaced** — too many false positives. You walk
|
|
173
|
+
the user through these in step 4.
|
|
174
|
+
|
|
175
|
+
## Patterns that go sideways
|
|
176
|
+
|
|
177
|
+
- Zipping before user confirmation — they lose the chance to catch
|
|
178
|
+
sensitive data the auto-redactions missed; explicit confirmation keeps
|
|
179
|
+
the call with them.
|
|
180
|
+
- Including `share-manifest.json` in the zip — clutters the receiver's
|
|
181
|
+
unpacked tree with a review aid that's only useful during staging.
|
|
182
|
+
- Including business data (anything outside `.halo/`) in the zip —
|
|
183
|
+
violates the sharing intent and raises security risk; the user expects
|
|
184
|
+
configuration only.
|
|
185
|
+
- Modifying the user's actual workspace files instead of the staged copy
|
|
186
|
+
— the original changes are irreversible, the staged copy is where
|
|
187
|
+
sanitization belongs.
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Stage a Halo workspace's .halo/ contents into a shareable bundle.
|
|
4
|
+
|
|
5
|
+
Run from the workspace skill's share verb via shell_exec:
|
|
6
|
+
python3 stage.py <workspace>
|
|
7
|
+
|
|
8
|
+
Output:
|
|
9
|
+
<workspace>/.halo/tmp/share/staged/
|
|
10
|
+
├── .halo/ ← mirrors target workspace's .halo/ (zip this)
|
|
11
|
+
├── share-manifest.json ← review aid (NOT part of the final zip)
|
|
12
|
+
├── REQUIRED_PARAMS.md ← receiver fills these in (in zip)
|
|
13
|
+
└── README.md ← receiver instructions (in zip)
|
|
14
|
+
|
|
15
|
+
The agent runs this, reads the manifest, summarizes for the user, awaits
|
|
16
|
+
confirmation, then zips `<staged>/.halo/`, REQUIRED_PARAMS.md, and README.md.
|
|
17
|
+
"""
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import sqlite3
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import yaml
|
|
30
|
+
except ImportError:
|
|
31
|
+
sys.exit("Error: PyYAML required. Install with `pip install pyyaml` (or `pip3 install pyyaml`).")
|
|
32
|
+
|
|
33
|
+
# --- patterns --------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
SECRET_FIELD = re.compile(
|
|
36
|
+
r"^(api[-_]?key|secret|secret[-_]?key|token|password|access[-_]?key|"
|
|
37
|
+
r"aws[-_]?secret[-_]?access[-_]?key|bot[-_]?token|client[-_]?secret)$",
|
|
38
|
+
re.IGNORECASE,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
PLACEHOLDER = re.compile(r"^\s*(\{\{[^}]+\}\}|<<[^>]+>>)\s*$")
|
|
42
|
+
|
|
43
|
+
# (label, pattern, action) — action is "redact" | "flag"
|
|
44
|
+
LEAK_PATTERNS = [
|
|
45
|
+
("aws-access-key", re.compile(r"AKIA[0-9A-Z]{16}"), "redact"),
|
|
46
|
+
("aws-session-key", re.compile(r"ASIA[0-9A-Z]{16}"), "redact"),
|
|
47
|
+
("bearer-header", re.compile(r"(?i)\b(bearer|authorization)\s*[:\s]\s*[^\s]{8,}"), "flag"),
|
|
48
|
+
("email", re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"), "flag"),
|
|
49
|
+
# Long base64-ish token. Constrained:
|
|
50
|
+
# - 40+ chars from the strict base64 alphabet (no `_` or `-`, those are URL-safe variant
|
|
51
|
+
# and produce too many path-like false positives)
|
|
52
|
+
# - Contains at least one digit AND at least one upper- and one lower-case letter
|
|
53
|
+
# (filters out repeated-character padding in markdown rules / dividers)
|
|
54
|
+
# - Optional `=` padding
|
|
55
|
+
# This still flags real tokens (JWTs, AWS session tokens, OAuth) without snagging
|
|
56
|
+
# filesystem paths (which contain `/` segments and word-shaped chunks).
|
|
57
|
+
("long-base64", re.compile(
|
|
58
|
+
r"(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])(?=[A-Za-z0-9+/]*\d)"
|
|
59
|
+
r"\b[A-Za-z0-9+/]{40,}={0,2}\b"
|
|
60
|
+
), "flag"),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
ALWAYS_EXCLUDED = ["sessions/", "logs/", "tmp/", "*.db", "*.db-shm", "*.db-wal", "settings.yaml", "USER.md"]
|
|
64
|
+
|
|
65
|
+
# Platform built-ins. The receiver's own server force-seeds these on every
|
|
66
|
+
# startup (see packages/server/src/init.ts BUILTIN_AGENT_IDS / BUILTIN_SKILL_IDS),
|
|
67
|
+
# so a *global*-scope copy never needs to ride along in the bundle — it'd be
|
|
68
|
+
# redundant and would leak any local edits the sharer made to them. Keep this
|
|
69
|
+
# in sync with init.ts (small, rarely changes). Workspace-scope overrides of
|
|
70
|
+
# these ids ARE still shared — those are real per-workspace customizations.
|
|
71
|
+
BUILTIN_AGENT_IDS = {
|
|
72
|
+
"__apply_agent__", "__evo_agent__", "__score__",
|
|
73
|
+
"default", "executor", "deep-executor",
|
|
74
|
+
}
|
|
75
|
+
BUILTIN_SKILL_IDS = {
|
|
76
|
+
"agent", "skill", "workspace", "cron", "send-file", "acp",
|
|
77
|
+
"aws-knowledge", "nova-web-search", "self",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# --- helpers ---------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def load_disabled(db_path: Path):
|
|
83
|
+
"""Return disabled items from the workspace DB, split by scope:
|
|
84
|
+
(global_agents, global_skills, ws_agents, ws_skills).
|
|
85
|
+
|
|
86
|
+
A disabled item never ships in the bundle (share == "don't take it
|
|
87
|
+
along"). We track global and workspace scopes separately because the two
|
|
88
|
+
take different code paths below — workspace agents are staged from one
|
|
89
|
+
loop, un-overridden globals from another."""
|
|
90
|
+
if not db_path.exists():
|
|
91
|
+
return set(), set(), set(), set()
|
|
92
|
+
try:
|
|
93
|
+
conn = sqlite3.connect(db_path)
|
|
94
|
+
cur = conn.execute("SELECT item_type, item_id, scope FROM disabled_items")
|
|
95
|
+
g_agents, g_skills, w_agents, w_skills = set(), set(), set(), set()
|
|
96
|
+
for item_type, item_id, scope in cur:
|
|
97
|
+
if scope == "global":
|
|
98
|
+
(g_agents if item_type == "agent" else g_skills).add(item_id)
|
|
99
|
+
else:
|
|
100
|
+
(w_agents if item_type == "agent" else w_skills).add(item_id)
|
|
101
|
+
conn.close()
|
|
102
|
+
return g_agents, g_skills, w_agents, w_skills
|
|
103
|
+
except sqlite3.DatabaseError:
|
|
104
|
+
return set(), set(), set(), set()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_yaml(path: Path):
|
|
108
|
+
try:
|
|
109
|
+
with path.open("r", encoding="utf-8") as f:
|
|
110
|
+
return yaml.safe_load(f) or {}
|
|
111
|
+
except (OSError, yaml.YAMLError):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sanitize_yaml(data, namespace: str, file_label: str, findings: list) -> bool:
|
|
116
|
+
"""Replace leaf string values under SECRET_FIELD keys with `{{ns.params.key}}`."""
|
|
117
|
+
modified = [False]
|
|
118
|
+
|
|
119
|
+
def walk(node, key_chain):
|
|
120
|
+
if isinstance(node, dict):
|
|
121
|
+
for k, v in list(node.items()):
|
|
122
|
+
key_str = str(k)
|
|
123
|
+
if isinstance(v, (dict, list)):
|
|
124
|
+
walk(v, key_chain + [key_str])
|
|
125
|
+
elif isinstance(v, str) and SECRET_FIELD.match(key_str) and v.strip() and not PLACEHOLDER.match(v):
|
|
126
|
+
placeholder = f"{{{{{namespace}.params.{key_str}}}}}"
|
|
127
|
+
node[k] = placeholder
|
|
128
|
+
modified[0] = True
|
|
129
|
+
findings.append({
|
|
130
|
+
"file": file_label,
|
|
131
|
+
"field": ".".join(key_chain + [key_str]),
|
|
132
|
+
"placeholder": placeholder,
|
|
133
|
+
})
|
|
134
|
+
elif isinstance(node, list):
|
|
135
|
+
for item in node:
|
|
136
|
+
walk(item, key_chain)
|
|
137
|
+
|
|
138
|
+
walk(data, [])
|
|
139
|
+
return modified[0]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def sanitize_markdown(text: str, file_label: str):
|
|
143
|
+
"""Auto-redact high-confidence leaks; collect lower-confidence ones for review."""
|
|
144
|
+
auto, suspicious = [], []
|
|
145
|
+
lines = text.splitlines(keepends=True)
|
|
146
|
+
for i, line in enumerate(lines, 1):
|
|
147
|
+
new_line = line
|
|
148
|
+
for label, pattern, action in LEAK_PATTERNS:
|
|
149
|
+
for m in pattern.finditer(new_line):
|
|
150
|
+
excerpt = m.group(0)
|
|
151
|
+
if action == "redact":
|
|
152
|
+
new_line = new_line.replace(excerpt, f"[REDACTED:{label}]")
|
|
153
|
+
auto.append({"file": file_label, "line": i, "pattern": label, "before": excerpt[:60]})
|
|
154
|
+
else:
|
|
155
|
+
suspicious.append({"file": file_label, "line": i, "pattern": label, "snippet": excerpt[:80]})
|
|
156
|
+
lines[i - 1] = new_line
|
|
157
|
+
return "".join(lines), auto, suspicious
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def sanitize_md_file(src: Path, dst: Path, label: str, manifest: dict):
|
|
161
|
+
"""Read src, sanitize, write to dst, accumulate redactions into manifest."""
|
|
162
|
+
text = src.read_text(encoding="utf-8")
|
|
163
|
+
text, auto, sus = sanitize_markdown(text, label)
|
|
164
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
dst.write_text(text, encoding="utf-8")
|
|
166
|
+
manifest["redactions"]["markdown_auto"].extend(auto)
|
|
167
|
+
manifest["redactions"]["markdown_suspicious"].extend(sus)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def index_doc_links(index_path: Path) -> list:
|
|
171
|
+
"""[text](docs/...) markdown links from INDEX.md. Only follow docs/-prefixed paths."""
|
|
172
|
+
if not index_path.exists():
|
|
173
|
+
return []
|
|
174
|
+
text = index_path.read_text(encoding="utf-8")
|
|
175
|
+
return [m.group(1).strip() for m in re.finditer(r"\[[^\]]+\]\(([^)]+)\)", text)
|
|
176
|
+
if m.group(1).strip().startswith("docs/")]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# --- main ------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
ap = argparse.ArgumentParser()
|
|
183
|
+
ap.add_argument("workspace", help="Workspace root (containing .halo/)")
|
|
184
|
+
args = ap.parse_args()
|
|
185
|
+
|
|
186
|
+
ws = Path(args.workspace).resolve()
|
|
187
|
+
ws_halo = ws / ".halo"
|
|
188
|
+
if not ws_halo.is_dir():
|
|
189
|
+
sys.exit(f"Error: {ws_halo} not found. Is this a Halo workspace?")
|
|
190
|
+
|
|
191
|
+
home_global = Path.home() / ".halo" / "global"
|
|
192
|
+
|
|
193
|
+
stage_root = ws_halo / "tmp" / "share" / "staged"
|
|
194
|
+
if stage_root.exists():
|
|
195
|
+
shutil.rmtree(stage_root)
|
|
196
|
+
stage_root.mkdir(parents=True)
|
|
197
|
+
stage_halo = stage_root / ".halo"
|
|
198
|
+
stage_halo.mkdir()
|
|
199
|
+
|
|
200
|
+
# All paths in the manifest/stdout are relative to the workspace root,
|
|
201
|
+
# so the bundle and review aid never leak the host's absolute layout.
|
|
202
|
+
manifest = {
|
|
203
|
+
"staged_at": str(stage_root.relative_to(ws)),
|
|
204
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
205
|
+
"included": {
|
|
206
|
+
"instructions": [],
|
|
207
|
+
"index": None,
|
|
208
|
+
"docs": [],
|
|
209
|
+
"agents": [],
|
|
210
|
+
"skills": [],
|
|
211
|
+
"prompts": {"all": None, "root": None},
|
|
212
|
+
},
|
|
213
|
+
"excluded": {
|
|
214
|
+
"user_md": [],
|
|
215
|
+
"memory_count": 0,
|
|
216
|
+
"assets_count": 0,
|
|
217
|
+
"missing_skills": [],
|
|
218
|
+
"always_excluded": ALWAYS_EXCLUDED,
|
|
219
|
+
},
|
|
220
|
+
"redactions": {"yaml_params": [], "markdown_auto": [], "markdown_suspicious": []},
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
disabled_agents, disabled_skills, ws_disabled_agents, ws_disabled_skills = load_disabled(ws_halo / "halo.db")
|
|
224
|
+
|
|
225
|
+
# --- INSTRUCTIONS.md (root + sub-dir chain), with global fallback if no root ws version
|
|
226
|
+
ws_root_instr = ws_halo / "INSTRUCTIONS.md"
|
|
227
|
+
if ws_root_instr.exists():
|
|
228
|
+
sanitize_md_file(ws_root_instr, stage_halo / "INSTRUCTIONS.md", ".halo/INSTRUCTIONS.md", manifest)
|
|
229
|
+
manifest["included"]["instructions"].append(".halo/INSTRUCTIONS.md")
|
|
230
|
+
else:
|
|
231
|
+
global_instr = home_global / "INSTRUCTIONS.md"
|
|
232
|
+
if global_instr.exists():
|
|
233
|
+
sanitize_md_file(global_instr, stage_halo / "INSTRUCTIONS.md", ".halo/INSTRUCTIONS.md (from global)", manifest)
|
|
234
|
+
manifest["included"]["instructions"].append(".halo/INSTRUCTIONS.md (global fallback)")
|
|
235
|
+
|
|
236
|
+
for sub_instr in ws.rglob(".halo/INSTRUCTIONS.md"):
|
|
237
|
+
if sub_instr == ws_root_instr:
|
|
238
|
+
continue
|
|
239
|
+
if "/.halo/tmp/" in str(sub_instr):
|
|
240
|
+
continue
|
|
241
|
+
rel = sub_instr.relative_to(ws)
|
|
242
|
+
sanitize_md_file(sub_instr, stage_root / rel, str(rel), manifest)
|
|
243
|
+
manifest["included"]["instructions"].append(str(rel))
|
|
244
|
+
|
|
245
|
+
# --- INDEX.md + docs/* it explicitly links to
|
|
246
|
+
ws_index = ws_halo / "INDEX.md"
|
|
247
|
+
if ws_index.exists():
|
|
248
|
+
sanitize_md_file(ws_index, stage_halo / "INDEX.md", ".halo/INDEX.md", manifest)
|
|
249
|
+
manifest["included"]["index"] = ".halo/INDEX.md"
|
|
250
|
+
for link in index_doc_links(ws_index):
|
|
251
|
+
src = ws_halo / link
|
|
252
|
+
if src.is_file():
|
|
253
|
+
sanitize_md_file(src, stage_halo / link, f".halo/{link}", manifest)
|
|
254
|
+
manifest["included"]["docs"].append(link)
|
|
255
|
+
elif src.is_dir():
|
|
256
|
+
for f in src.rglob("*.md"):
|
|
257
|
+
rel = f.relative_to(ws_halo)
|
|
258
|
+
sanitize_md_file(f, stage_halo / rel, f".halo/{rel}", manifest)
|
|
259
|
+
manifest["included"]["docs"].append(str(rel))
|
|
260
|
+
|
|
261
|
+
# --- agents (workspace + un-disabled-and-not-overridden global)
|
|
262
|
+
# A workspace agent disabled in this workspace is dropped from ws_agent_ids
|
|
263
|
+
# entirely. That mirrors runtime (loadUsableAgents skips the disabled
|
|
264
|
+
# record): it isn't staged, AND it no longer shadows a same-id global — so
|
|
265
|
+
# if the workspace disabled its own `foo`, an un-disabled global `foo` still
|
|
266
|
+
# ships, exactly as the running agent would fall back to it.
|
|
267
|
+
ws_agents_dir = ws_halo / "agents"
|
|
268
|
+
global_agents_dir = home_global / "agents"
|
|
269
|
+
ws_agent_ids = {d.name for d in ws_agents_dir.iterdir()
|
|
270
|
+
if ws_agents_dir.is_dir() and d.is_dir() and (d / "agent.yaml").exists()
|
|
271
|
+
and d.name not in ws_disabled_agents} \
|
|
272
|
+
if ws_agents_dir.is_dir() else set()
|
|
273
|
+
|
|
274
|
+
referenced_skills = set()
|
|
275
|
+
|
|
276
|
+
def stage_agent(agent_dir: Path, scope: str):
|
|
277
|
+
agent_yaml_src = agent_dir / "agent.yaml"
|
|
278
|
+
config = load_yaml(agent_yaml_src)
|
|
279
|
+
if config is None:
|
|
280
|
+
return
|
|
281
|
+
# Skip platform-internal agents (evolution, score, apply, etc.).
|
|
282
|
+
# They're shipped force-overwritten by the receiver's own server on
|
|
283
|
+
# startup, so bundling them is redundant — and shipping the workspace
|
|
284
|
+
# copy can leak experimental edits the user made locally.
|
|
285
|
+
if config.get("internal") is True:
|
|
286
|
+
manifest["excluded"].setdefault("internal_agents", []).append(agent_dir.name)
|
|
287
|
+
return
|
|
288
|
+
target_dir = stage_halo / "agents" / agent_dir.name
|
|
289
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
# agent.yaml — sanitize and write
|
|
291
|
+
yaml_findings = []
|
|
292
|
+
modified = sanitize_yaml(config, agent_dir.name, f".halo/agents/{agent_dir.name}/agent.yaml", yaml_findings)
|
|
293
|
+
if modified:
|
|
294
|
+
(target_dir / "agent.yaml").write_text(
|
|
295
|
+
yaml.safe_dump(config, sort_keys=False, allow_unicode=True),
|
|
296
|
+
encoding="utf-8",
|
|
297
|
+
)
|
|
298
|
+
manifest["redactions"]["yaml_params"].extend(yaml_findings)
|
|
299
|
+
else:
|
|
300
|
+
shutil.copy2(agent_yaml_src, target_dir / "agent.yaml")
|
|
301
|
+
# siblings (AGENT.md, etc.); skip USER.md
|
|
302
|
+
for f in agent_dir.iterdir():
|
|
303
|
+
if f.name in {"agent.yaml", "USER.md"}:
|
|
304
|
+
if f.name == "USER.md" and f.exists():
|
|
305
|
+
manifest["excluded"]["user_md"].append(f".halo/agents/{agent_dir.name}/USER.md")
|
|
306
|
+
continue
|
|
307
|
+
if f.is_file():
|
|
308
|
+
if f.suffix == ".md":
|
|
309
|
+
sanitize_md_file(f, target_dir / f.name, f".halo/agents/{agent_dir.name}/{f.name}", manifest)
|
|
310
|
+
else:
|
|
311
|
+
shutil.copy2(f, target_dir / f.name)
|
|
312
|
+
elif f.is_dir():
|
|
313
|
+
shutil.copytree(f, target_dir / f.name, dirs_exist_ok=True)
|
|
314
|
+
# collect skill refs
|
|
315
|
+
for s in (config.get("skills") or []):
|
|
316
|
+
if isinstance(s, str):
|
|
317
|
+
referenced_skills.add(s)
|
|
318
|
+
manifest["included"]["agents"].append({"id": agent_dir.name, "scope": scope})
|
|
319
|
+
|
|
320
|
+
for aid in sorted(ws_agent_ids):
|
|
321
|
+
stage_agent(ws_agents_dir / aid, "workspace")
|
|
322
|
+
|
|
323
|
+
if global_agents_dir.is_dir():
|
|
324
|
+
for d in sorted(global_agents_dir.iterdir()):
|
|
325
|
+
if not d.is_dir() or not (d / "agent.yaml").exists():
|
|
326
|
+
continue
|
|
327
|
+
if d.name in ws_agent_ids:
|
|
328
|
+
continue # workspace overrides
|
|
329
|
+
if d.name in disabled_agents:
|
|
330
|
+
continue # explicitly disabled in this workspace
|
|
331
|
+
if d.name in BUILTIN_AGENT_IDS:
|
|
332
|
+
continue # receiver's server seeds these itself
|
|
333
|
+
stage_agent(d, "global")
|
|
334
|
+
|
|
335
|
+
# --- skills
|
|
336
|
+
# Ship every workspace-scope skill (it's a per-workspace customization, so
|
|
337
|
+
# it belongs in a self-contained share — even if no agent.yaml lists it,
|
|
338
|
+
# e.g. slash-command-only skills). For skills an included agent references
|
|
339
|
+
# but the workspace doesn't have, pull the global copy — except platform
|
|
340
|
+
# built-ins, which the receiver's server seeds itself.
|
|
341
|
+
ws_skills_dir = ws_halo / "skills"
|
|
342
|
+
global_skills_dir = home_global / "skills"
|
|
343
|
+
# Same rule as agents: a workspace skill disabled in this workspace doesn't
|
|
344
|
+
# ship, and no longer shadows a same-id global (so an agent that references
|
|
345
|
+
# it still resolves to the global copy in the bundle, mirroring runtime).
|
|
346
|
+
ws_skill_ids = {d.name for d in ws_skills_dir.iterdir()
|
|
347
|
+
if ws_skills_dir.is_dir() and d.is_dir() and (d / "SKILL.md").exists()
|
|
348
|
+
and d.name not in ws_disabled_skills} \
|
|
349
|
+
if ws_skills_dir.is_dir() else set()
|
|
350
|
+
|
|
351
|
+
def stage_skill(skill_dir: Path, scope: str):
|
|
352
|
+
target_dir = stage_halo / "skills" / skill_dir.name
|
|
353
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
for f in skill_dir.iterdir():
|
|
355
|
+
if f.is_file():
|
|
356
|
+
if f.name == "SKILL.md":
|
|
357
|
+
text = f.read_text(encoding="utf-8")
|
|
358
|
+
fm_match = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
|
|
359
|
+
if fm_match:
|
|
360
|
+
fm = yaml.safe_load(fm_match.group(1)) or {}
|
|
361
|
+
body = text[fm_match.end():]
|
|
362
|
+
yaml_findings = []
|
|
363
|
+
modified = sanitize_yaml(fm, skill_dir.name,
|
|
364
|
+
f".halo/skills/{skill_dir.name}/SKILL.md (frontmatter)",
|
|
365
|
+
yaml_findings)
|
|
366
|
+
body, auto, sus = sanitize_markdown(body, f".halo/skills/{skill_dir.name}/SKILL.md")
|
|
367
|
+
if modified:
|
|
368
|
+
new_fm = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True).rstrip()
|
|
369
|
+
text = f"---\n{new_fm}\n---\n{body}"
|
|
370
|
+
manifest["redactions"]["yaml_params"].extend(yaml_findings)
|
|
371
|
+
else:
|
|
372
|
+
text = f"{fm_match.group(0)}{body}"
|
|
373
|
+
manifest["redactions"]["markdown_auto"].extend(auto)
|
|
374
|
+
manifest["redactions"]["markdown_suspicious"].extend(sus)
|
|
375
|
+
else:
|
|
376
|
+
text, auto, sus = sanitize_markdown(text, f".halo/skills/{skill_dir.name}/SKILL.md")
|
|
377
|
+
manifest["redactions"]["markdown_auto"].extend(auto)
|
|
378
|
+
manifest["redactions"]["markdown_suspicious"].extend(sus)
|
|
379
|
+
(target_dir / "SKILL.md").write_text(text, encoding="utf-8")
|
|
380
|
+
elif f.suffix == ".md":
|
|
381
|
+
sanitize_md_file(f, target_dir / f.name, f".halo/skills/{skill_dir.name}/{f.name}", manifest)
|
|
382
|
+
else:
|
|
383
|
+
shutil.copy2(f, target_dir / f.name)
|
|
384
|
+
elif f.is_dir():
|
|
385
|
+
shutil.copytree(f, target_dir / f.name, dirs_exist_ok=True)
|
|
386
|
+
manifest["included"]["skills"].append({"id": skill_dir.name, "scope": scope})
|
|
387
|
+
|
|
388
|
+
# 1. all workspace-scope skills
|
|
389
|
+
for sid in sorted(ws_skill_ids):
|
|
390
|
+
stage_skill(ws_skills_dir / sid, "workspace")
|
|
391
|
+
|
|
392
|
+
# 2. agent-referenced skills missing from the workspace → global fallback
|
|
393
|
+
for sid in sorted(referenced_skills - ws_skill_ids):
|
|
394
|
+
if sid in BUILTIN_SKILL_IDS:
|
|
395
|
+
continue # receiver's server seeds these itself
|
|
396
|
+
if (global_skills_dir / sid / "SKILL.md").exists() and sid not in disabled_skills:
|
|
397
|
+
stage_skill(global_skills_dir / sid, "global")
|
|
398
|
+
else:
|
|
399
|
+
manifest["excluded"]["missing_skills"].append(sid)
|
|
400
|
+
|
|
401
|
+
# --- prompts (workspace per-scope, else global fallback for missing scopes)
|
|
402
|
+
ws_prompts = ws_halo / "prompts"
|
|
403
|
+
global_prompts = home_global / "prompts"
|
|
404
|
+
# `bootstrap` is intentionally excluded — it only fires for first-run
|
|
405
|
+
# users who don't have a USER.md yet, which a shared bundle never targets.
|
|
406
|
+
# `builtin/` (PLATFORM_KNOWLEDGE.md, etc.) is server-owned and version-tied;
|
|
407
|
+
# it lives outside `prompts/` and is never shared — receiver's own server
|
|
408
|
+
# seeds it on startup.
|
|
409
|
+
for scope in ["all", "root"]:
|
|
410
|
+
ws_scope = ws_prompts / scope
|
|
411
|
+
global_scope = global_prompts / scope
|
|
412
|
+
if ws_scope.is_dir():
|
|
413
|
+
shutil.copytree(ws_scope, stage_halo / "prompts" / scope, dirs_exist_ok=True)
|
|
414
|
+
manifest["included"]["prompts"][scope] = "workspace"
|
|
415
|
+
elif global_scope.is_dir():
|
|
416
|
+
shutil.copytree(global_scope, stage_halo / "prompts" / scope, dirs_exist_ok=True)
|
|
417
|
+
manifest["included"]["prompts"][scope] = "global-fallback"
|
|
418
|
+
|
|
419
|
+
# --- excluded counts
|
|
420
|
+
if (ws_halo / "memory").is_dir():
|
|
421
|
+
manifest["excluded"]["memory_count"] = sum(1 for _ in (ws_halo / "memory").rglob("*") if _.is_file())
|
|
422
|
+
if (ws / "assets").is_dir():
|
|
423
|
+
manifest["excluded"]["assets_count"] = sum(1 for _ in (ws / "assets").rglob("*") if _.is_file())
|
|
424
|
+
if (ws_halo / "USER.md").exists():
|
|
425
|
+
manifest["excluded"]["user_md"].append(".halo/USER.md")
|
|
426
|
+
|
|
427
|
+
# --- write manifest
|
|
428
|
+
(stage_root / "share-manifest.json").write_text(
|
|
429
|
+
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# --- README.md
|
|
433
|
+
# Secret-redacted fields appear inline in agent.yaml / SKILL.md as
|
|
434
|
+
# `{{<id>.params.<key>}}`. The receiver greps for those after unpacking;
|
|
435
|
+
# no separate declaration file needed.
|
|
436
|
+
readme = (
|
|
437
|
+
"# Shared Halo Workspace\n\n"
|
|
438
|
+
"This bundle contains a Halo workspace's `.halo/` configuration: agents, skills,\n"
|
|
439
|
+
"prompts, and instructions. Drop it into any project to replicate the setup.\n\n"
|
|
440
|
+
"## To use\n\n"
|
|
441
|
+
"1. Unzip into your project root. This places `.halo/` at the project root.\n"
|
|
442
|
+
"2. If any agent.yaml or SKILL.md contains `{{<id>.params.<key>}}` placeholders,\n"
|
|
443
|
+
" add the matching values to `~/.halo/secrets/settings.yaml`. Search the bundle\n"
|
|
444
|
+
" with `grep -rn '{{' .halo/` to find every placeholder.\n"
|
|
445
|
+
"3. Open the workspace in Halo — agents and skills will be ready.\n\n"
|
|
446
|
+
"## Inside\n\n"
|
|
447
|
+
f"- {len(manifest['included']['agents'])} agent(s)\n"
|
|
448
|
+
f"- {len(manifest['included']['skills'])} skill(s)\n"
|
|
449
|
+
f"- Prompts: {', '.join(f'{k}={v}' for k, v in manifest['included']['prompts'].items() if v) or '(none)'}\n"
|
|
450
|
+
f"- {len(manifest['included']['docs'])} doc(s) referenced from INDEX.md\n\n"
|
|
451
|
+
"## Excluded (privacy / portability)\n\n"
|
|
452
|
+
"- `USER.md` (personal profile, all scopes)\n"
|
|
453
|
+
"- `memory/`, `assets/` (workspace-specific)\n"
|
|
454
|
+
"- `sessions/`, `logs/`, `tmp/`, `*.db*`, `settings.yaml`\n\n"
|
|
455
|
+
"## Auto-redactions\n\n"
|
|
456
|
+
f"Markdown files were scanned. {len(manifest['redactions']['markdown_auto'])} unambiguous "
|
|
457
|
+
f"leak(s) were auto-redacted to `[REDACTED:<type>]`. "
|
|
458
|
+
f"{len(manifest['redactions']['markdown_suspicious'])} suspicious string(s) were flagged "
|
|
459
|
+
"for the maintainer to review before sharing.\n"
|
|
460
|
+
)
|
|
461
|
+
(stage_root / "README.md").write_text(readme, encoding="utf-8")
|
|
462
|
+
|
|
463
|
+
# --- summary to stdout (for the agent). Paths are relative to the
|
|
464
|
+
# workspace so the agent's reply doesn't leak the host's absolute layout.
|
|
465
|
+
print(json.dumps({
|
|
466
|
+
"ok": True,
|
|
467
|
+
"staged_at": str(stage_root.relative_to(ws)),
|
|
468
|
+
"manifest": str((stage_root / "share-manifest.json").relative_to(ws)),
|
|
469
|
+
"summary": {
|
|
470
|
+
"agents": [a["id"] + f" ({a['scope']})" for a in manifest["included"]["agents"]],
|
|
471
|
+
"skills": [s["id"] + f" ({s['scope']})" for s in manifest["included"]["skills"]],
|
|
472
|
+
"instructions": manifest["included"]["instructions"],
|
|
473
|
+
"docs_count": len(manifest["included"]["docs"]),
|
|
474
|
+
"prompts": manifest["included"]["prompts"],
|
|
475
|
+
"yaml_redactions": len(manifest["redactions"]["yaml_params"]),
|
|
476
|
+
"markdown_auto_redactions": len(manifest["redactions"]["markdown_auto"]),
|
|
477
|
+
"markdown_suspicious": len(manifest["redactions"]["markdown_suspicious"]),
|
|
478
|
+
"excluded_user_md": len(manifest["excluded"]["user_md"]),
|
|
479
|
+
"excluded_memory_files": manifest["excluded"]["memory_count"],
|
|
480
|
+
"excluded_asset_files": manifest["excluded"]["assets_count"],
|
|
481
|
+
"missing_skills": manifest["excluded"]["missing_skills"],
|
|
482
|
+
},
|
|
483
|
+
}, indent=2, ensure_ascii=False))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
main()
|