@turmind/halo 1.5.4 → 1.5.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/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
- package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +1 -0
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
- package/admin-out/_next/static/css/b8c87b919758c52b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +10 -8
- package/bundled-docs/design/agentcore.md +13 -10
- package/bundled-docs/design/architecture.md +24 -14
- package/bundled-docs/design/canvas-extensions.md +19 -4
- package/bundled-docs/design/command.md +5 -5
- package/bundled-docs/design/cron.md +2 -2
- package/bundled-docs/design/evolution.md +34 -35
- package/bundled-docs/design/express-self.md +6 -4
- package/bundled-docs/design/feishu.md +18 -14
- package/bundled-docs/design/halo-city.md +5 -4
- package/bundled-docs/design/observability.md +9 -9
- package/bundled-docs/design/prompt-system.md +12 -12
- package/bundled-docs/design/session.md +14 -14
- package/bundled-docs/design/slack.md +22 -20
- package/bundled-docs/design/storage.md +30 -29
- package/bundled-docs/design/telegram.md +26 -8
- package/bundled-docs/design/web.md +3 -1
- package/bundled-docs/design/wechat.md +8 -6
- package/bundled-docs/design/wecom.md +23 -9
- package/bundled-docs/design/ws.md +6 -6
- package/bundled-docs/dev/acp-adapter.md +8 -8
- package/bundled-docs/dev/add-channel.md +12 -9
- package/bundled-docs/dev/add-model-provider.md +31 -20
- package/bundled-docs/dev/api.md +86 -57
- package/bundled-docs/dev/deploy.md +8 -6
- package/bundled-docs/dev/env.md +29 -21
- package/bundled-docs/dev/logging.md +15 -29
- package/bundled-docs/dev/previews.md +20 -11
- package/bundled-docs/dev/tools.md +26 -14
- package/bundled-docs/guide/README.md +1 -0
- package/bundled-docs/guide/agents.md +19 -53
- package/bundled-docs/guide/channels/README.md +7 -4
- package/bundled-docs/guide/channels/acp.md +3 -1
- package/bundled-docs/guide/channels/feishu.md +13 -11
- package/bundled-docs/guide/channels/slack.md +12 -10
- package/bundled-docs/guide/channels/telegram.md +21 -27
- package/bundled-docs/guide/channels/web.md +15 -11
- package/bundled-docs/guide/channels/wechat.md +15 -14
- package/bundled-docs/guide/channels/wecom.md +7 -2
- package/bundled-docs/guide/chat.md +10 -9
- package/bundled-docs/guide/cli.md +10 -8
- package/bundled-docs/guide/delegation-and-access.md +1 -1
- package/bundled-docs/guide/getting-started.md +3 -3
- package/bundled-docs/guide/secrets-and-credentials.md +3 -3
- package/bundled-docs/guide/sessions.md +3 -3
- package/bundled-docs/guide/skills.md +9 -7
- package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
- package/bundled-docs/guide/workspace.md +29 -17
- package/bundled-docs/requirements/agent.md +19 -18
- package/bundled-docs/requirements/chat.md +16 -17
- package/bundled-docs/requirements/command.md +10 -10
- package/bundled-docs/requirements/editor.md +12 -10
- package/bundled-docs/requirements/explorer.md +10 -7
- package/bundled-docs/requirements/session.md +16 -17
- package/bundled-docs/requirements/settings.md +30 -18
- package/bundled-docs/requirements/skill.md +4 -4
- package/bundled-docs/requirements/source-control.md +4 -3
- package/bundled-docs/requirements/terminal.md +13 -13
- package/bundled-docs/requirements/workspace.md +19 -13
- package/dist/index.js +10587 -12088
- package/package.json +1 -1
- package/templates/agents/default/agent.yaml +1 -0
- package/templates/config.yaml +6 -0
- package/templates/skills/extension/SKILL.md +25 -14
- package/templates/skills/extension/config.yaml +9 -0
- package/templates/skills/extension/templates/ext.sh +166 -34
- package/templates/skills/web-search/SKILL.md +20 -6
- package/templates/skills/web-search/config.yaml +13 -0
- package/templates/skills/web-search/search.py +27 -6
- package/templates/skills/workspace/share.md +11 -3
- package/templates/skills/workspace/stage.py +85 -5
- package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
- package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
- package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_ssgManifest.js +0 -0
package/package.json
CHANGED
package/templates/config.yaml
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# server.cors_origins → HALO_CORS_ORIGINS (comma-separated)
|
|
8
8
|
# timeout.shell_exec → HALO_SHELL_TIMEOUT
|
|
9
9
|
# timeout.web_fetch → HALO_WEB_FETCH_TIMEOUT
|
|
10
|
+
# timeout.model_request → HALO_MODEL_TIMEOUT
|
|
10
11
|
# timeout.session_grace → HALO_SESSION_GRACE
|
|
11
12
|
# timeout.terminal_grace → HALO_TERMINAL_GRACE
|
|
12
13
|
# logging.max_file_size → HALO_LOG_MAX_SIZE
|
|
@@ -45,6 +46,11 @@ timeout:
|
|
|
45
46
|
default: 10000
|
|
46
47
|
description: "web_fetch tool timeout (ms)."
|
|
47
48
|
description_zh: "web_fetch 工具的超时(毫秒)。"
|
|
49
|
+
model_request:
|
|
50
|
+
value: 600000
|
|
51
|
+
default: 600000
|
|
52
|
+
description: "Per model-call timeout (ms). Streaming providers reset it on any received data, so it acts as an idle timeout; on expiry the call aborts and the agent loop retries."
|
|
53
|
+
description_zh: "单次模型调用的超时(毫秒)。流式 provider 每收到数据就重新计时,相当于空闲超时;超时后中止本次调用,由 agent 循环重试。"
|
|
48
54
|
session_grace:
|
|
49
55
|
value: 300000
|
|
50
56
|
default: 300000
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extension
|
|
3
|
-
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`). `/extension install <id|zip|url>` pulls the latest `<id>-v*` release from turmind/halo-hub
|
|
3
|
+
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`). `/extension install <id|zip|url>` pulls the latest `<id>-v*` release from the configured hub (default turmind/halo-hub). Activate when the user wants to preview a file type the editor can't open, or asks to install / update / remove an extension.
|
|
4
4
|
command: /extension
|
|
5
5
|
requiresAccess: full
|
|
6
6
|
verbs:
|
|
7
|
-
- { name: install, desc: "Install or upgrade an extension from
|
|
7
|
+
- { name: install, desc: "Install or upgrade an extension from the hub (by id), a local zip path, or a zip URL" }
|
|
8
8
|
- { name: list, desc: List installed extensions with version and status }
|
|
9
9
|
- { name: remove, desc: Remove an installed extension by id }
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# extension
|
|
13
13
|
|
|
14
|
-
The requested action is **`$1`**;
|
|
15
|
-
natural language ("install the glb
|
|
14
|
+
The requested action is **`$1`**; everything after it is the payload
|
|
15
|
+
(`$ARGUMENTS` minus the first token). With natural language ("install the glb
|
|
16
|
+
viewer"), infer both.
|
|
16
17
|
|
|
17
18
|
Preview extensions are static bundles the admin editor loads in a sandboxed
|
|
18
19
|
iframe to preview file types it has no built-in viewer for (e.g. `.glb`).
|
|
@@ -30,23 +31,33 @@ Pick EXT = the first that exists, then:
|
|
|
30
31
|
|
|
31
32
|
## install
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
When the payload names a source, run the install right away — no confirmation,
|
|
35
|
+
no explanation first. Ask only when no source was given.
|
|
36
|
+
|
|
37
|
+
`HALO_HUB_REPO='{{params.hub_repo}}' bash EXT install <source>` — keep
|
|
38
|
+
`{{params.hub_repo}}` exactly as-is (the system substitutes it at runtime), where
|
|
39
|
+
`<source>` is one of:
|
|
40
|
+
- an extension id (e.g. `glb`) → the newest `glb-v*` release zip from the hub
|
|
41
|
+
(a GitHub / Gitea / Forgejo / GitLab repo), or the newest `glb-v<x.y.z>` tag
|
|
42
|
+
when the hub is any other git URL / local repo;
|
|
36
43
|
- a local `.zip` path;
|
|
37
44
|
- an `https://…zip` URL.
|
|
38
45
|
|
|
46
|
+
The hub is set in Settings → Skills → extension → `hub_repo`; empty means
|
|
47
|
+
https://github.com/turmind/halo-hub.
|
|
48
|
+
|
|
39
49
|
The script unpacks to a temp dir, pre-checks `halo-extension.json` (id matches,
|
|
40
50
|
entry exists), then atomically replaces `~/.halo/global/extensions/<id>/`.
|
|
41
51
|
Reinstalling the same id upgrades (or downgrades) it. Relay the script's last
|
|
42
52
|
line to the user (`installed glb 1.0.0` / the error). Never edit files inside an
|
|
43
53
|
installed extension — reinstall instead.
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
On failure relay the script's hint. "release API failed" → rate-limited
|
|
56
|
+
(GitHub: 60 anonymous requests/hour), private, or a wrong `hub_repo`; setting
|
|
57
|
+
`GITHUB_TOKEN` / `GITLAB_TOKEN` / `GITEA_TOKEN` in the server environment helps.
|
|
58
|
+
"no release for <id>" → the id isn't on that hub. "needs a build step" → that
|
|
59
|
+
extension can't be installed from git tags; get its release zip. Either way the
|
|
60
|
+
user can download the zip by hand and run `install <path>`.
|
|
50
61
|
|
|
51
62
|
## list
|
|
52
63
|
|
|
@@ -63,8 +74,8 @@ the user first if they didn't name the id explicitly.
|
|
|
63
74
|
|
|
64
75
|
- Extensions are global (all workspaces on this server). Say so if the user
|
|
65
76
|
asks "install it for this project".
|
|
66
|
-
-
|
|
67
|
-
|
|
77
|
+
- Mention the configured hub (its `extensions/` directory) only when the user
|
|
78
|
+
asks what extensions exist or an install fails; do not scrape it.
|
|
68
79
|
- The script only pre-checks the required fields; the server's scanner is the
|
|
69
80
|
authority. A package it rejects shows up in `list` (and the admin) as
|
|
70
81
|
`ERROR` with the reason.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
displayName_zh: 预览扩展
|
|
2
|
+
description: Install / list / remove admin preview extensions.
|
|
3
|
+
description_zh: 安装 / 列出 / 删除管理后台的预览扩展。
|
|
4
|
+
|
|
5
|
+
params:
|
|
6
|
+
- key: hub_repo
|
|
7
|
+
description: "Hub that `install <id>` pulls from: a GitHub / Gitea / Forgejo / GitLab repo URL (newest `<id>-v*` release zip), `owner/repo` for GitHub, or any git URL / local repo path (newest `<id>-v<x.y.z>` tag). Empty = https://github.com/turmind/halo-hub."
|
|
8
|
+
description_zh: "`install <id>` 的来源仓库:GitHub / Gitea / Forgejo / GitLab 仓库地址(取最新的 `<id>-v*` release zip),GitHub 可简写为 `owner/repo`;也可以是任意 git 地址或本地仓库路径(取最新的 `<id>-v<x.y.z>` 标签)。留空 = https://github.com/turmind/halo-hub。"
|
|
9
|
+
default: https://github.com/turmind/halo-hub
|
|
@@ -9,17 +9,18 @@
|
|
|
9
9
|
# ext.sh list
|
|
10
10
|
# ext.sh remove <id>
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# `install <id>` reads the hub in $HALO_HUB_REPO (the skill's hub_repo param;
|
|
13
|
+
# empty → https://github.com/turmind/halo-hub). GitHub / Gitea-Forgejo / GitLab
|
|
14
|
+
# hubs go through their public release API (GITHUB_TOKEN / GITEA_TOKEN /
|
|
15
|
+
# GITLAB_TOKEN, if set, are sent — lifts GitHub's 60 req/h anonymous limit);
|
|
16
|
+
# any other git URL or local repo installs from `<id>-v<semver>` tags.
|
|
17
|
+
# Needs curl + unzip + node (+ git for tag-based hubs). No `gh`.
|
|
14
18
|
set -euo pipefail
|
|
15
19
|
|
|
16
20
|
ROOT="$HOME/.halo/global/extensions"
|
|
17
|
-
|
|
21
|
+
DEFAULT_HUB="https://github.com/turmind/halo-hub"
|
|
18
22
|
ID_RE='^[a-z0-9][a-z0-9_-]{0,63}$' # keep in sync with server extensions/registry.ts ID_RE
|
|
19
23
|
|
|
20
|
-
cmd=${1:?usage: ext.sh install <id|zip|url> | list | remove <id>}
|
|
21
|
-
shift || true
|
|
22
|
-
|
|
23
24
|
# Print `<id> <version> <name>` for the extension dir $1, or fail with the
|
|
24
25
|
# reason. $2 (optional) = the id the directory / request claims.
|
|
25
26
|
precheck() {
|
|
@@ -38,44 +39,175 @@ precheck() {
|
|
|
38
39
|
' "$1" "${2:-}"
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
+
# ── Hub source ──────────────────────────────────────────────────────────────
|
|
43
|
+
# Self-contained hub_* functions (no globals besides DEFAULT_HUB) so a later
|
|
44
|
+
# `/skill install` / `/workspace import` can lift them out unchanged.
|
|
45
|
+
|
|
46
|
+
# Normalize a hub setting: empty / unsubstituted `{{…}}` → default,
|
|
47
|
+
# `owner/repo` → GitHub, an existing dir → absolute path, drop trailing `/`.
|
|
48
|
+
# A `.git` suffix stays (git mode clones the URL as given); the API helpers
|
|
49
|
+
# below drop it.
|
|
50
|
+
hub_url() {
|
|
51
|
+
local h=${1:-}
|
|
52
|
+
if [[ -z $h || $h == '{{'* ]]; then h=$DEFAULT_HUB; fi
|
|
53
|
+
if [[ $h =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9_.-]+$ ]]; then h="https://github.com/$h"
|
|
54
|
+
elif [[ -d $h ]]; then h=$(cd "$h" && pwd)
|
|
55
|
+
fi
|
|
56
|
+
echo "${h%/}"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# curl with the platform's optional token header.
|
|
60
|
+
hub_curl() {
|
|
61
|
+
local p=$1; shift
|
|
62
|
+
case $p in
|
|
63
|
+
github) curl -fsSL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} "$@" ;;
|
|
64
|
+
gitlab) curl -fsSL ${GITLAB_TOKEN:+-H "PRIVATE-TOKEN: $GITLAB_TOKEN"} "$@" ;;
|
|
65
|
+
gitea) curl -fsSL ${GITEA_TOKEN:+-H "Authorization: token $GITEA_TOKEN"} "$@" ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Repo endpoint of <platform>'s API for hub URL $2.
|
|
70
|
+
hub_repo_api() {
|
|
71
|
+
[[ $2 =~ ^(https?://[^/]+)/(.+)$ ]] || return 1
|
|
72
|
+
local base=${BASH_REMATCH[1]} path=${BASH_REMATCH[2]%.git}
|
|
73
|
+
case $1 in
|
|
74
|
+
github) echo "https://api.github.com/repos/$path" ;;
|
|
75
|
+
gitea) echo "$base/api/v1/repos/$path" ;;
|
|
76
|
+
gitlab) echo "$base/api/v4/projects/${path//\//%2F}" ;;
|
|
77
|
+
esac
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
hub_releases_api() {
|
|
81
|
+
local q='per_page=100'
|
|
82
|
+
if [[ $1 == gitea ]]; then q='limit=50'; fi
|
|
83
|
+
echo "$(hub_repo_api "$1" "$2")/releases?$q"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# 0 when the host answers <platform>'s repo endpoint for $2 with JSON.
|
|
87
|
+
hub_probe() {
|
|
88
|
+
local api ct
|
|
89
|
+
api=$(hub_repo_api "$1" "$2") || return 1
|
|
90
|
+
ct=$(hub_curl "$1" --max-time 10 -o /dev/null -w '%{content_type}' "$api" 2>/dev/null) || return 1
|
|
91
|
+
[[ $ct == application/json* ]]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# github | gitlab | gitea | git (= tag-based fallback) for a normalized hub URL.
|
|
95
|
+
hub_platform() {
|
|
96
|
+
if [[ ! $1 =~ ^https?://([^/]+)/[^/]+/.+$ ]]; then echo git; return; fi
|
|
97
|
+
case ${BASH_REMATCH[1]} in
|
|
98
|
+
github.com) echo github ;;
|
|
99
|
+
*gitlab*) echo gitlab ;;
|
|
100
|
+
codeberg.org|*gitea*|*forgejo*) echo gitea ;;
|
|
101
|
+
*) if hub_probe gitea "$1"; then echo gitea
|
|
102
|
+
elif hub_probe gitlab "$1"; then echo gitlab
|
|
103
|
+
else echo git; fi ;;
|
|
104
|
+
esac
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hub_releases_page() {
|
|
108
|
+
if [[ $1 == gitlab ]]; then echo "${2%.git}/-/releases"; else echo "${2%.git}/releases"; fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# stdin = <platform>'s release list (newest-first); print the first `.zip`
|
|
112
|
+
# asset URL of the newest non-draft / non-prerelease / non-upcoming release
|
|
113
|
+
# tagged `<id>-v*`, exit 2 if there is none.
|
|
114
|
+
hub_pick_release() {
|
|
115
|
+
node -e '
|
|
116
|
+
let s = ""; process.stdin.on("data", (d) => s += d).on("end", () => {
|
|
117
|
+
const [platform, id] = process.argv.slice(1)
|
|
118
|
+
// "<id>-v<digit>", so id glb never matches a glb-viewer-v… tag.
|
|
119
|
+
const r = JSON.parse(s).find((r) => new RegExp("^" + id + "-v\\d").test(r.tag_name) && !r.draft && !r.prerelease && !r.upcoming_release)
|
|
120
|
+
const assets = !r ? [] : platform === "gitlab"
|
|
121
|
+
? ((r.assets && r.assets.links) || []).map((l) => ({ name: l.name, url: l.direct_asset_url || l.url }))
|
|
122
|
+
: (r.assets || []).map((a) => ({ name: a.name, url: a.browser_download_url }))
|
|
123
|
+
const a = assets.find((a) => String(a.name).endsWith(".zip"))
|
|
124
|
+
if (!a) process.exit(2)
|
|
125
|
+
console.log(a.url)
|
|
126
|
+
})' "$1" "$2"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# stdin = `git ls-remote --tags --refs`; print the highest `<id>-vX.Y.Z` tag
|
|
130
|
+
# (prerelease `-…` suffixes skipped), nothing if there is none.
|
|
131
|
+
hub_pick_tag() {
|
|
132
|
+
local v
|
|
133
|
+
v=$(sed -n "s|.*refs/tags/$1-v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$|\1|p" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
|
|
134
|
+
if [[ -n $v ]]; then echo "$1-v$v"; fi
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Tag-based hub: shallow-clone the newest `<id>-v<semver>` tag and echo
|
|
138
|
+
# `extensions/<id>/` minus the build inputs scripts/pack.mjs leaves out of a
|
|
139
|
+
# release zip. A package with a build step is refused — its output isn't in git.
|
|
140
|
+
hub_git_package() {
|
|
141
|
+
local hub=$1 id=$2 tmp=$3 url=$1 refs tag dir
|
|
142
|
+
if [[ $url == /* ]]; then url="file://$url"; fi # a plain path makes git ignore --depth
|
|
143
|
+
refs=$(git ls-remote --tags --refs "$url") || { echo "cannot list tags of $hub (git ls-remote failed)" >&2; exit 1; }
|
|
144
|
+
tag=$(printf '%s\n' "$refs" | hub_pick_tag "$id")
|
|
145
|
+
[[ -n $tag ]] || { echo "no release for $id in $hub (no $id-v<x.y.z> tag)" >&2; exit 1; }
|
|
146
|
+
git -c advice.detachedHead=false clone -q --depth 1 --branch "$tag" "$url" "$tmp/repo" || { echo "git clone of $tag from $hub failed" >&2; exit 1; }
|
|
147
|
+
dir="$tmp/repo/extensions/$id"
|
|
148
|
+
[[ -d $dir ]] || { echo "$hub tag $tag has no extensions/$id/" >&2; exit 1; }
|
|
149
|
+
if [[ -e $dir/fetch-deps.sh || -e $dir/build.sh ]]; then
|
|
150
|
+
echo "$id needs a build step — install it from a release zip (install <path/to.zip> or <https://…zip>)" >&2; exit 1
|
|
151
|
+
fi
|
|
152
|
+
rm -rf "$dir/node_modules" "$dir/src" "$dir/package.json" "$dir/package-lock.json"
|
|
153
|
+
echo "$dir"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Fetch hub package <id> from hub URL $1 into $3: echo a zip path (release
|
|
157
|
+
# API) or the package dir (git tags). API errors never fall back to git — a
|
|
158
|
+
# git checkout of a build-step package would install broken.
|
|
159
|
+
hub_fetch() {
|
|
160
|
+
local hub=$1 id=$2 tmp=$3 platform api url token page
|
|
161
|
+
platform=$(hub_platform "$hub")
|
|
162
|
+
if [[ $platform == git ]]; then hub_git_package "$hub" "$id" "$tmp"; return; fi
|
|
163
|
+
api=$(hub_releases_api "$platform" "$hub")
|
|
164
|
+
page=$(hub_releases_page "$platform" "$hub")
|
|
165
|
+
case $platform in github) token=GITHUB_TOKEN ;; gitlab) token=GITLAB_TOKEN ;; *) token=GITEA_TOKEN ;; esac
|
|
166
|
+
hub_curl "$platform" "$api" -o "$tmp/releases.json" \
|
|
167
|
+
|| { echo "$platform release API failed for $hub — rate-limited, private or wrong repo? set $token, or download the zip from $page and run: install <path>" >&2; exit 1; }
|
|
168
|
+
url=$(hub_pick_release "$platform" "$id" <"$tmp/releases.json") \
|
|
169
|
+
|| { echo "no release for $id in $hub — check the id, or download the zip from $page and run: install <path>" >&2; exit 1; }
|
|
170
|
+
# No token here: asset links redirect to other hosts and curl -L would
|
|
171
|
+
# forward a custom header like PRIVATE-TOKEN there.
|
|
172
|
+
curl -fsSL "$url" -o "$tmp/pkg.zip" || { echo "download failed: $url" >&2; exit 1; }
|
|
173
|
+
echo "$tmp/pkg.zip"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Echo a local zip or an unpacked package dir for source $1 (path, https URL, or hub id).
|
|
42
177
|
resolve_source() {
|
|
43
|
-
local src=$1
|
|
178
|
+
local src=$1
|
|
44
179
|
if [[ -f $src ]]; then echo "$src"; return; fi
|
|
45
180
|
if [[ $src =~ ^https?:// ]]; then curl -fsSL "$src" -o "$TMP/pkg.zip"; echo "$TMP/pkg.zip"; return; fi
|
|
46
181
|
[[ $src =~ $ID_RE ]] || { echo "invalid id or missing file: $src" >&2; exit 1; }
|
|
47
|
-
|
|
48
|
-
url=$(curl -fsSL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} \
|
|
49
|
-
"https://api.github.com/repos/$REPO/releases?per_page=100" \
|
|
50
|
-
| node -e '
|
|
51
|
-
let s = ""; process.stdin.on("data", (d) => s += d).on("end", () => {
|
|
52
|
-
const id = process.argv[1]
|
|
53
|
-
const r = JSON.parse(s).find((r) => r.tag_name.startsWith(id + "-v") && !r.draft && !r.prerelease)
|
|
54
|
-
const a = r && r.assets.find((a) => a.name.endsWith(".zip"))
|
|
55
|
-
if (!a) process.exit(2)
|
|
56
|
-
console.log(a.browser_download_url)
|
|
57
|
-
})' "$src") \
|
|
58
|
-
|| { echo "no release for $src in $REPO — download the zip from https://github.com/$REPO/releases and run: install <path>" >&2; exit 1; }
|
|
59
|
-
curl -fsSL "$url" -o "$TMP/pkg.zip"
|
|
60
|
-
echo "$TMP/pkg.zip"
|
|
182
|
+
hub_fetch "$(hub_url "${HALO_HUB_REPO:-}")" "$src" "$TMP"
|
|
61
183
|
}
|
|
62
184
|
|
|
185
|
+
# Sourced (tests, later reuse of the hub_* functions): definitions only.
|
|
186
|
+
if [[ ${BASH_SOURCE[0]} != "$0" ]]; then return 0; fi
|
|
187
|
+
|
|
188
|
+
cmd=${1:?usage: ext.sh install <id|zip|url> | list | remove <id>}
|
|
189
|
+
shift || true
|
|
190
|
+
|
|
63
191
|
case $cmd in
|
|
64
192
|
install)
|
|
65
193
|
src=${1:?usage: ext.sh install <id|zip|url>}
|
|
66
194
|
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
195
|
+
pkg=$(resolve_source "$src")
|
|
196
|
+
if [[ -d $pkg ]]; then
|
|
197
|
+
dir=$pkg
|
|
198
|
+
else
|
|
199
|
+
# Same refusals as the server's zip installer: no `..`, no absolute paths,
|
|
200
|
+
# no symlinks (zipinfo mode column starts with `l`).
|
|
201
|
+
if unzip -Z1 "$pkg" | grep -Eq '(^|/)\.\.(/|$)|^/'; then echo "zip contains ../ or absolute entries" >&2; exit 1; fi
|
|
202
|
+
if unzip -Z "$pkg" | grep -q '^l'; then echo "zip contains symlinks" >&2; exit 1; fi
|
|
203
|
+
mkdir -p "$TMP/x"
|
|
204
|
+
unzip -q "$pkg" -d "$TMP/x"
|
|
205
|
+
# `zip -r glb.zip glb/` wraps everything in one directory — descend into it.
|
|
206
|
+
dir="$TMP/x"
|
|
207
|
+
if [[ ! -f $dir/halo-extension.json ]]; then
|
|
208
|
+
only=$(find "$dir" -mindepth 1 -maxdepth 1 | head -2)
|
|
209
|
+
if [[ $(printf '%s\n' "$only" | wc -l) -eq 1 && -f $only/halo-extension.json ]]; then dir=$only; fi
|
|
210
|
+
fi
|
|
79
211
|
fi
|
|
80
212
|
expect=""
|
|
81
213
|
if [[ $src =~ $ID_RE && ! -f $src ]]; then expect=$src; fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-search
|
|
3
|
-
description: Real-time web search with two gears. Fast (default, Amazon Nova grounding, ~3s, cheap) — current events, news, prices, any routine lookup. Deep (--deep, OpenAI GPT-5.6 web_search via Bedrock Mantle, ~20-40s, token-expensive) — multi-round retrieval with first-hand sources and per-claim citations, for verifying facts behind important decisions or double-checking weak/contradictory fast results. Default to fast; escalate to deep only when the question deserves depth. For AWS documentation, service announcements or regional availability, use the aws-knowledge skill instead.
|
|
3
|
+
description: Real-time web search on AWS Bedrock (needs AWS credentials with Bedrock access) with two gears. Fast (default, Amazon Nova grounding, ~3s, cheap) — current events, news, prices, any routine lookup. Deep (--deep, OpenAI GPT-5.6 web_search via Bedrock Mantle, ~20-40s, token-expensive) — multi-round retrieval with first-hand sources and per-claim citations, for verifying facts behind important decisions or double-checking weak/contradictory fast results. Default to fast; escalate to deep only when the question deserves depth. For AWS documentation, service announcements or regional availability, use the aws-knowledge skill instead.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
# Web Search
|
|
@@ -22,12 +22,18 @@ searches against official sites), leans toward authoritative first-hand sources
|
|
|
22
22
|
Use `<workspace>/.halo/skills/web-search/search.py` if that exists, else
|
|
23
23
|
`~/.halo/global/skills/web-search/search.py`.
|
|
24
24
|
|
|
25
|
+
Always prefix the call with the two region variables, keeping
|
|
26
|
+
`{{params.fast_region}}` / `{{params.deep_region}}` exactly as written (the
|
|
27
|
+
system substitutes them at runtime; unset = built-in defaults):
|
|
28
|
+
|
|
25
29
|
```bash
|
|
26
30
|
# fast (default) — everyday lookups
|
|
27
|
-
|
|
31
|
+
WEB_SEARCH_FAST_REGION='{{params.fast_region}}' WEB_SEARCH_DEEP_REGION='{{params.deep_region}}' \
|
|
32
|
+
python3 ~/.halo/global/skills/web-search/search.py "What are the latest news about the China A-share market today?"
|
|
28
33
|
|
|
29
34
|
# deep — verify facts that must be right
|
|
30
|
-
|
|
35
|
+
WEB_SEARCH_FAST_REGION='{{params.fast_region}}' WEB_SEARCH_DEEP_REGION='{{params.deep_region}}' \
|
|
36
|
+
python3 ~/.halo/global/skills/web-search/search.py --deep "What did the US Federal Reserve announce most recently about interest rates?"
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
- Output: answer text + a `Sources:` URL list (gear/model/token usage on stderr).
|
|
@@ -40,8 +46,16 @@ python3 ~/.halo/global/skills/web-search/search.py --deep "What did the US Feder
|
|
|
40
46
|
English works best; Chinese works too — deep gear rewrites queries bilingually.
|
|
41
47
|
- Search output is still a lead, not gospel: for critical figures, `web_fetch`
|
|
42
48
|
the cited source URL and confirm before acting on it.
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
- Both gears run on AWS Bedrock with the machine's AWS credentials (no API key):
|
|
50
|
+
fast = Amazon Nova 2 Lite + web grounding (`bedrock:InvokeModel` +
|
|
51
|
+
`bedrock:InvokeTool` on `amazon.nova_grounding`, US regions only); deep =
|
|
52
|
+
OpenAI GPT-5.6 on Bedrock Mantle (`bedrock-mantle:CreateInference` +
|
|
53
|
+
`bedrock-mantle:CallWithBearerToken`). Deep gear needs the
|
|
54
|
+
`aws-bedrock-token-generator` pip package (one-time:
|
|
55
|
+
`pip3 install --user aws-bedrock-token-generator`). An AccessDenied /
|
|
56
|
+
credentials error means this machine lacks that access — say so, don't retry.
|
|
57
|
+
- Regions are set in Settings → Skills → web-search (`fast_region` default
|
|
58
|
+
us-east-1; `deep_region` default us-west-2 for luna, us-east-1 for sol — sol
|
|
59
|
+
isn't offered in us-west-2).
|
|
46
60
|
- A deep call can take 40 s — be patient in shell_exec. One clear question per
|
|
47
61
|
call; don't fan out parallel calls.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
displayName_zh: 联网搜索
|
|
2
|
+
description: Real-time web search on AWS Bedrock — needs AWS credentials with Bedrock access.
|
|
3
|
+
description_zh: 基于 AWS Bedrock 的实时联网搜索,需要有 Bedrock 权限的 AWS 凭证。
|
|
4
|
+
|
|
5
|
+
params:
|
|
6
|
+
- key: fast_region
|
|
7
|
+
description: "AWS region for the fast gear (Amazon Nova 2 Lite + web grounding, US regions only: us-east-1 / us-east-2 / us-west-2). Empty = us-east-1."
|
|
8
|
+
description_zh: "快速档(Amazon Nova 2 Lite + Web Grounding)使用的 AWS 区域,仅限美国区域:us-east-1 / us-east-2 / us-west-2。留空 = us-east-1。"
|
|
9
|
+
default: us-east-1
|
|
10
|
+
- key: deep_region
|
|
11
|
+
description: "AWS region for the deep gear (OpenAI GPT-5.6 on Bedrock Mantle). Luna: us-east-1 / us-east-2 / us-west-2; Sol: us-east-1 / us-east-2. Empty = us-west-2 for Luna, us-east-1 for Sol."
|
|
12
|
+
description_zh: "深度档(Bedrock Mantle 上的 OpenAI GPT-5.6)使用的 AWS 区域。Luna:us-east-1 / us-east-2 / us-west-2;Sol:us-east-1 / us-east-2。留空 = Luna 用 us-west-2,Sol 用 us-east-1。"
|
|
13
|
+
default: us-west-2
|
|
@@ -12,27 +12,44 @@ Usage:
|
|
|
12
12
|
Auth is automatic in both gears (machine AWS credentials): fast uses boto3's
|
|
13
13
|
default credential chain; deep mints a 12-hour bearer token on every call via
|
|
14
14
|
aws-bedrock-token-generator — no API key to configure or rotate.
|
|
15
|
+
|
|
16
|
+
Regions: WEB_SEARCH_FAST_REGION / WEB_SEARCH_DEEP_REGION override the defaults
|
|
17
|
+
below (the skill passes its fast_region / deep_region params through them).
|
|
15
18
|
"""
|
|
16
19
|
import json
|
|
20
|
+
import os
|
|
17
21
|
import sys
|
|
18
22
|
import urllib.error
|
|
19
23
|
import urllib.request
|
|
20
24
|
|
|
21
|
-
MANTLE_URL = "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses"
|
|
22
25
|
MODELS = {"luna": "openai.gpt-5.6-luna", "sol": "openai.gpt-5.6-sol"}
|
|
26
|
+
# Nova web grounding is US-only; us-east-1 / us-east-2 / us-west-2 all verified.
|
|
27
|
+
FAST_REGION = "us-east-1"
|
|
28
|
+
# bedrock-mantle: Luna is in us-east-1 / us-east-2 / us-west-2, Sol only in
|
|
29
|
+
# us-east-1 / us-east-2. us-west-2 answered 3-8x faster than us-east-1 in
|
|
30
|
+
# 2026-10-04 tests, so Luna defaults there; Sol takes us-east-1.
|
|
31
|
+
DEEP_REGION = {"openai.gpt-5.6-luna": "us-west-2", "openai.gpt-5.6-sol": "us-east-1"}
|
|
23
32
|
# luna (default) runs effort=max to squeeze out its retrieval planning; sol's
|
|
24
33
|
# retrieval is already exhaustive at medium (measured 48 search queries on one
|
|
25
34
|
# question), so it gets xhigh to avoid pointless token doubling — max over
|
|
26
35
|
# xhigh measured only ~6% more reasoning tokens.
|
|
27
36
|
EFFORTS = {"openai.gpt-5.6-luna": "max", "openai.gpt-5.6-sol": "xhigh"}
|
|
37
|
+
SOL_REGIONS = ("us-east-1", "us-east-2")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def env_region(name: str) -> str:
|
|
41
|
+
"""Region from env; empty or an unsubstituted `{{…}}` skill-param placeholder = unset."""
|
|
42
|
+
v = os.environ.get(name, "").strip()
|
|
43
|
+
return "" if v.startswith("{{") else v
|
|
28
44
|
|
|
29
45
|
|
|
30
46
|
def fast_search(question: str) -> None:
|
|
31
47
|
import boto3
|
|
32
48
|
from botocore.config import Config
|
|
33
49
|
|
|
50
|
+
region = env_region("WEB_SEARCH_FAST_REGION") or FAST_REGION
|
|
34
51
|
bedrock = boto3.client(
|
|
35
|
-
"bedrock-runtime", region_name=
|
|
52
|
+
"bedrock-runtime", region_name=region, config=Config(read_timeout=60)
|
|
36
53
|
)
|
|
37
54
|
response = bedrock.converse(
|
|
38
55
|
modelId="us.amazon.nova-2-lite-v1:0",
|
|
@@ -55,13 +72,17 @@ def fast_search(question: str) -> None:
|
|
|
55
72
|
for u in sources:
|
|
56
73
|
print(" -", u)
|
|
57
74
|
usage = response.get("usage", {})
|
|
58
|
-
print(f"\n[gear=fast model=nova-2-lite total_tokens={usage.get('totalTokens')}]", file=sys.stderr)
|
|
75
|
+
print(f"\n[gear=fast model=nova-2-lite region={region} total_tokens={usage.get('totalTokens')}]", file=sys.stderr)
|
|
59
76
|
|
|
60
77
|
|
|
61
78
|
def deep_search(question: str, model: str) -> None:
|
|
62
79
|
from aws_bedrock_token_generator import provide_token
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
region = env_region("WEB_SEARCH_DEEP_REGION") or DEEP_REGION[model]
|
|
82
|
+
if model == MODELS["sol"] and region not in SOL_REGIONS:
|
|
83
|
+
print(f"[sol is not offered in {region}; using {DEEP_REGION[model]}]", file=sys.stderr)
|
|
84
|
+
region = DEEP_REGION[model]
|
|
85
|
+
token = provide_token(region=region)
|
|
65
86
|
body = json.dumps(
|
|
66
87
|
{
|
|
67
88
|
"model": model,
|
|
@@ -71,7 +92,7 @@ def deep_search(question: str, model: str) -> None:
|
|
|
71
92
|
}
|
|
72
93
|
).encode()
|
|
73
94
|
req = urllib.request.Request(
|
|
74
|
-
|
|
95
|
+
f"https://bedrock-mantle.{region}.api.aws/openai/v1/responses",
|
|
75
96
|
data=body,
|
|
76
97
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
|
77
98
|
method="POST",
|
|
@@ -101,7 +122,7 @@ def deep_search(question: str, model: str) -> None:
|
|
|
101
122
|
for u in sources:
|
|
102
123
|
print(" -", u)
|
|
103
124
|
usage = data.get("usage", {})
|
|
104
|
-
print(f"\n[gear=deep model={model} total_tokens={usage.get('total_tokens')}]", file=sys.stderr)
|
|
125
|
+
print(f"\n[gear=deep model={model} region={region} total_tokens={usage.get('total_tokens')}]", file=sys.stderr)
|
|
105
126
|
|
|
106
127
|
|
|
107
128
|
def main() -> None:
|
|
@@ -62,6 +62,7 @@ Prompts: all=global-fallback, root=global-fallback
|
|
|
62
62
|
|
|
63
63
|
Redactions:
|
|
64
64
|
• 1 secret field auto-replaced with {{...params.api_key}} placeholder
|
|
65
|
+
• settings.yaml: 2 secret values cleared
|
|
65
66
|
• 0 unambiguous leaks redacted in markdown
|
|
66
67
|
• 2 suspicious strings flagged for review (see below)
|
|
67
68
|
|
|
@@ -69,7 +70,7 @@ Excluded:
|
|
|
69
70
|
• USER.md (3 files — personal profile)
|
|
70
71
|
• memory/ (12 files)
|
|
71
72
|
• assets/ (84 files)
|
|
72
|
-
• sessions/, logs/, tmp/, *.db, settings.yaml — always
|
|
73
|
+
• sessions/, logs/, tmp/, *.db, ~/.halo/secrets/settings.yaml — always
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
Lead with the most important callouts: anything in the **redactions** section, anything in
|
|
@@ -148,13 +149,17 @@ To share: send the zip. Receiver unzips into their project root, then fills in a
|
|
|
148
149
|
- Skills that are **referenced by an included agent's `skills:` list**. Workspace skill wins;
|
|
149
150
|
otherwise global skill (if not disabled).
|
|
150
151
|
- `prompts/{bootstrap,all,root}/` — workspace dir if it exists, else global fallback.
|
|
152
|
+
- `<ws>/.halo/settings.yaml` (if present) — values under any `secrets` map or a secret-named
|
|
153
|
+
key (`api_key`, `token`, …) are cleared to `""`; `<<ENV>>` placeholders stay. The receiver
|
|
154
|
+
fills them in. Unparseable files are skipped (`excluded.settings_skipped`).
|
|
151
155
|
|
|
152
156
|
**Excluded (always):**
|
|
153
157
|
- Every `USER.md` (workspace, agent dirs, anywhere) — personal profile, never share.
|
|
154
158
|
- `memory/`, `assets/` — workspace-specific knowledge / data.
|
|
155
159
|
- `sessions/`, `logs/`, `tmp/`, `*.db`, `*.db-shm`, `*.db-wal` — runtime state.
|
|
156
|
-
- `~/.halo/secrets/settings.yaml` —
|
|
157
|
-
`{{<id>.params.<key>}}` placeholders; the receiver fills them in
|
|
160
|
+
- `~/.halo/secrets/settings.yaml` — global settings, never bundled. Secrets in agent.yaml /
|
|
161
|
+
SKILL.md are redacted to `{{<id>.params.<key>}}` placeholders; the receiver fills them in
|
|
162
|
+
their own `settings.yaml`.
|
|
158
163
|
- Agents with `internal: true` in their `agent.yaml` (`__evo_agent__`, `__score__`,
|
|
159
164
|
`__apply_agent__`, etc.) — these are platform-internal, force-overwritten by the
|
|
160
165
|
receiver's own server on startup, so bundling them is redundant and can leak
|
|
@@ -166,6 +171,9 @@ To share: send the zip. Receiver unzips into their project root, then fills in a
|
|
|
166
171
|
`api_key`, `secret`, `token`, `password`, `access_key`, `aws_secret_access_key`, `bot_token`,
|
|
167
172
|
`client_secret` (case-insensitive) get their values replaced with `{{<id>.params.<field>}}`
|
|
168
173
|
if the value is a literal (not already a `{{...}}` or `<<ENV>>` placeholder).
|
|
174
|
+
- **Workspace settings.yaml (auto):** secret values cleared to `""` (see Included); other
|
|
175
|
+
strings get the markdown leak scan below. Each cleared value is listed in
|
|
176
|
+
`redactions.settings`.
|
|
169
177
|
- **Markdown auto-redact:** AWS access key formats (`AKIA…`, `ASIA…`) are replaced with
|
|
170
178
|
`[REDACTED:<type>]`.
|
|
171
179
|
- **Markdown flag-only:** Emails, bearer-style headers, and long base64-ish tokens are
|