@cocorograph/hub-agent 0.8.64 → 0.8.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/package.json +1 -1
- package/scripts/install.sh +354 -1
- package/src/claude-display-mask.mjs +89 -0
- package/src/claude-stream-bridge.mjs +36 -11
- package/src/codex-result-mask.mjs +187 -0
- package/src/main.mjs +5 -2
- package/src/permission-display-mask.mjs +142 -0
- package/src/tool-result-mask.mjs +201 -130
package/README.md
CHANGED
|
@@ -77,6 +77,23 @@ hub-agent plugins uninstall <name> # ~/.hub/plugins/<prio>-<name>/ を削除
|
|
|
77
77
|
|
|
78
78
|
## 構成
|
|
79
79
|
|
|
80
|
+
### チャット表示のマスク
|
|
81
|
+
|
|
82
|
+
Claude / Codex のチャットは、既定で表示・WebSocket 配信用コピーをマスクします。
|
|
83
|
+
`get_skill_prompt`、`skill/`・`エージェント/` の内部本文取得、デプロイ接続情報取得の
|
|
84
|
+
結果を非表示にし、通常の本文・引数・エラー・差分に含まれる既知の秘密値も伏せます。
|
|
85
|
+
一般のナレッジ取得や通常のツール結果は表示します。
|
|
86
|
+
|
|
87
|
+
- 分割された秘密値の漏出を防ぐため、文字単位の途中出力ではなく完成済み本文を表示します。
|
|
88
|
+
- コード実行ラッパーに対象ツール名が明記されている場合、その実行結果全体を非表示にします。
|
|
89
|
+
動的に組み立てたツール名や、任意の文章に埋め込まれた未知形式の秘密を完全検出する機能ではありません。
|
|
90
|
+
- 許可確認には伏せた入力を渡し、変更されなかった伏せ字を元の入力へ戻して実行します。
|
|
91
|
+
一意に復元できない編集は拒否します。マスク失敗時に生の本文へ戻すことはしません。
|
|
92
|
+
- Codex のモデル入力・再開用 rollout 原本は変更しません。Claude の既存の終了後 JSONL
|
|
93
|
+
マスクは継続します。ターミナル表示や過去に配信済みのデータを遡って消すものではありません。
|
|
94
|
+
- 緊急時の切り戻し設定 `HUB_AGENT_TOOL_RESULT_MASK=0` は、秘密値を含め無加工で配信します。
|
|
95
|
+
通常運用では無効化しないでください。
|
|
96
|
+
|
|
80
97
|
```
|
|
81
98
|
hub-agent/
|
|
82
99
|
├── bin/
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -45,6 +45,13 @@ NODE_DEFAULT_BREW_FORMULA="node@24"
|
|
|
45
45
|
PACKAGE_NAME="@cocorograph/hub-agent"
|
|
46
46
|
CLAUDE_CODE_PACKAGE="@anthropic-ai/claude-code"
|
|
47
47
|
CODEX_PACKAGE="@openai/codex"
|
|
48
|
+
UV_VERSION="0.12.8"
|
|
49
|
+
HUB_PYTHON_MINOR="3.13"
|
|
50
|
+
HUB_VENV_DIR="$HOME/.local/share/hub-venv"
|
|
51
|
+
# Hub Python の依存バージョンを更新するときは、この配列を変更する。
|
|
52
|
+
HUB_PYTHON_PACKAGES=("PyYAML==6.0.3" "Pillow==12.3.0" "openpyxl==3.1.5")
|
|
53
|
+
# HUB_PYTHON_PACKAGES と同じ 3 件を指す。依存を増やすときは両方を更新する。
|
|
54
|
+
HUB_PYTHON_IMPORTS=("yaml" "PIL" "openpyxl")
|
|
48
55
|
|
|
49
56
|
color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
|
|
50
57
|
color_ok() { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
|
|
@@ -598,6 +605,7 @@ ensure_build_tools() {
|
|
|
598
605
|
ensure_python3() {
|
|
599
606
|
if python3 --version >/dev/null 2>&1; then
|
|
600
607
|
color_ok "$(python3 --version 2>&1)"
|
|
608
|
+
_check_python3_version
|
|
601
609
|
return 0
|
|
602
610
|
fi
|
|
603
611
|
|
|
@@ -615,12 +623,355 @@ ensure_python3() {
|
|
|
615
623
|
|
|
616
624
|
if python3 --version >/dev/null 2>&1; then
|
|
617
625
|
color_ok "$(python3 --version 2>&1)"
|
|
626
|
+
_check_python3_version
|
|
618
627
|
else
|
|
619
628
|
color_err "python3 の導入後も実行できません"
|
|
620
629
|
exit 1
|
|
621
630
|
fi
|
|
622
631
|
}
|
|
623
632
|
|
|
633
|
+
_check_python3_version() {
|
|
634
|
+
local version
|
|
635
|
+
version="$(python3 -c 'import platform; print(platform.python_version())')" || {
|
|
636
|
+
color_err "python3 のバージョンを確認できません"
|
|
637
|
+
exit 1
|
|
638
|
+
}
|
|
639
|
+
if python3 -c 'import sys; raise SystemExit(0 if sys.version_info < (3, 9) else 1)'; then
|
|
640
|
+
color_err "hub の配布物は Python 3.9 以上が必要です(現在: $version)"
|
|
641
|
+
exit 1
|
|
642
|
+
fi
|
|
643
|
+
if python3 -c 'import sys; raise SystemExit(0 if sys.version_info < (3, 11) else 1)'; then
|
|
644
|
+
color_warn "Python 3.11 未満では一部機能が無効になります(Codex の MCP 承認チェック)。現在: $version"
|
|
645
|
+
fi
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_hub_uv_triple() {
|
|
649
|
+
local os arch libc="gnu"
|
|
650
|
+
os="$(uname -s)"
|
|
651
|
+
arch="$(uname -m)"
|
|
652
|
+
if [[ "$os" == "Linux" ]] && ldd --version 2>&1 | grep -qi musl; then
|
|
653
|
+
libc="musl"
|
|
654
|
+
fi
|
|
655
|
+
case "$os:$arch" in
|
|
656
|
+
Darwin:arm64) printf '%s\n' "aarch64-apple-darwin" ;;
|
|
657
|
+
Darwin:x86_64) printf '%s\n' "x86_64-apple-darwin" ;;
|
|
658
|
+
Linux:x86_64) printf '%s\n' "x86_64-unknown-linux-$libc" ;;
|
|
659
|
+
Linux:aarch64|Linux:arm64) printf '%s\n' "aarch64-unknown-linux-$libc" ;;
|
|
660
|
+
*) color_err "uv が対応していないプラットフォームです: $os $arch"; return 1 ;;
|
|
661
|
+
esac
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
_install_hub_uv() (
|
|
665
|
+
set -e
|
|
666
|
+
local triple url tmp expected actual uv_src local_bin destination staged=""
|
|
667
|
+
triple="$(_hub_uv_triple)" || exit 1
|
|
668
|
+
local_bin="$HOME/.local/bin"
|
|
669
|
+
destination="$local_bin/uv"
|
|
670
|
+
mkdir -p "$local_bin"
|
|
671
|
+
if [[ -e "$destination" || -L "$destination" ]]; then
|
|
672
|
+
color_err "$destination は uv と名前衝突しています。既存ファイルを退避するか PATH を修正してください"
|
|
673
|
+
exit 1
|
|
674
|
+
fi
|
|
675
|
+
url="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${triple}.tar.gz"
|
|
676
|
+
tmp="$(mktemp -d)"
|
|
677
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
678
|
+
staged="$(mktemp "$local_bin/.uv.tmp.XXXXXX")"
|
|
679
|
+
trap 'rm -rf "$tmp"; if [[ -n "$staged" ]]; then rm -f "$staged"; fi' EXIT
|
|
680
|
+
trap 'exit 130' INT
|
|
681
|
+
trap 'exit 143' TERM
|
|
682
|
+
retry 3 curl -fsSL "$url" -o "$tmp/uv.tar.gz" || exit 1
|
|
683
|
+
retry 3 curl -fsSL "$url.sha256" -o "$tmp/uv.tar.gz.sha256" || exit 1
|
|
684
|
+
expected="$(head -1 "$tmp/uv.tar.gz.sha256")"
|
|
685
|
+
expected="${expected%%[[:space:]]*}"
|
|
686
|
+
if present shasum; then
|
|
687
|
+
actual="$(shasum -a 256 "$tmp/uv.tar.gz")"
|
|
688
|
+
elif present sha256sum; then
|
|
689
|
+
actual="$(sha256sum "$tmp/uv.tar.gz")"
|
|
690
|
+
else
|
|
691
|
+
color_err "sha256 検算コマンド (shasum / sha256sum) がありません"
|
|
692
|
+
exit 1
|
|
693
|
+
fi
|
|
694
|
+
actual="${actual%%[[:space:]]*}"
|
|
695
|
+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
|
|
696
|
+
color_err "uv tarball の sha256 が一致しません"
|
|
697
|
+
exit 1
|
|
698
|
+
fi
|
|
699
|
+
tar -xzf "$tmp/uv.tar.gz" -C "$tmp"
|
|
700
|
+
uv_src="$(find "$tmp" -type f -name uv | head -1)"
|
|
701
|
+
[[ -n "$uv_src" ]] || { color_err "uv tarball に uv がありません"; exit 1; }
|
|
702
|
+
mv "$uv_src" "$staged"
|
|
703
|
+
chmod +x "$staged"
|
|
704
|
+
"$staged" --version >/dev/null 2>&1 || { color_err "取得した uv を実行できません"; exit 1; }
|
|
705
|
+
if [[ -e "$destination" || -L "$destination" ]]; then
|
|
706
|
+
color_err "$destination は uv と名前衝突しています。既存ファイルを退避するか PATH を修正してください"
|
|
707
|
+
exit 1
|
|
708
|
+
fi
|
|
709
|
+
mv "$staged" "$destination"
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
_hub_venv_usable() {
|
|
713
|
+
local venv_dir="${1:-$HUB_VENV_DIR}" python base_prefix version package spec distribution expected
|
|
714
|
+
python="$venv_dir/bin/python3"
|
|
715
|
+
[[ -x "$python" ]] || return 1
|
|
716
|
+
base_prefix="$($python -c 'import sys; print(sys.base_prefix)' 2>/dev/null)" || return 1
|
|
717
|
+
[[ -e "$base_prefix/bin/python3" ]] || return 1
|
|
718
|
+
version="$($python -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")' 2>/dev/null)" || return 1
|
|
719
|
+
[[ "$version" == "$HUB_PYTHON_MINOR" ]] || return 1
|
|
720
|
+
for package in "${HUB_PYTHON_PACKAGES[@]}"; do
|
|
721
|
+
distribution="${package%%==*}"
|
|
722
|
+
expected="${package#*==}"
|
|
723
|
+
"$python" -c 'import importlib.metadata, sys; raise SystemExit(0 if importlib.metadata.version(sys.argv[1]) == sys.argv[2] else 1)' \
|
|
724
|
+
"$distribution" "$expected" >/dev/null 2>&1 || return 1
|
|
725
|
+
done
|
|
726
|
+
for spec in "${HUB_PYTHON_IMPORTS[@]}"; do
|
|
727
|
+
"$python" -c 'import importlib, sys; importlib.import_module(sys.argv[1])' "$spec" >/dev/null 2>&1 || return 1
|
|
728
|
+
done
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
_build_hub_venv() (
|
|
732
|
+
set -e
|
|
733
|
+
local new_dir="$1" built=0
|
|
734
|
+
trap '(( built == 1 )) || rm -rf "$new_dir"' EXIT
|
|
735
|
+
trap 'exit 130' INT
|
|
736
|
+
trap 'exit 143' TERM
|
|
737
|
+
rm -rf "$new_dir"
|
|
738
|
+
uv venv --python "$HUB_PYTHON_MINOR" "$new_dir" || exit 1
|
|
739
|
+
uv pip install --python "$new_dir/bin/python3" --only-binary :all: "${HUB_PYTHON_PACKAGES[@]}" || exit 1
|
|
740
|
+
_hub_venv_usable "$new_dir" || { color_err "構築した Hub Python venv の検証に失敗しました"; exit 1; }
|
|
741
|
+
built=1
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
_acquire_hub_python_lock() {
|
|
745
|
+
local lock_dir="$1" attempts=0 owner_file token owner_pid
|
|
746
|
+
owner_file="$lock_dir/owner"
|
|
747
|
+
# ⚠️ ここで時刻を採らない。`printf -v t '%(%s)T'` は bash 4.2 以降の機能で、macOS 標準の
|
|
748
|
+
# bash 3.2 では「invalid format character」で失敗する。`set -euo pipefail` の下なので
|
|
749
|
+
# インストーラ自体がここで止まる(実測。テストの bash は Homebrew の 5.x なので緑になってしまう)。
|
|
750
|
+
# mkdir lock では「観測 → 回収 → 再取得」の間に別プロセスが取り直した lock を、遅れて来た
|
|
751
|
+
# 回収者が持ち去れる。回収の rename が保証するのは、同時に回収する複数の勝者が 1 つになる
|
|
752
|
+
# ことまでで、破壊的操作の直前に所有を再確認しても TOCTOU の窓は消えない。
|
|
753
|
+
# そのため自動回収は行わず、異常終了後は表示されたディレクトリを人が削除して再実行する。
|
|
754
|
+
token="$$-${RANDOM:-0}-${RANDOM:-0}"
|
|
755
|
+
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
756
|
+
attempts=$((attempts + 1))
|
|
757
|
+
if (( attempts >= 50 )); then
|
|
758
|
+
owner_pid=""
|
|
759
|
+
if [[ -f "$owner_file" ]]; then
|
|
760
|
+
{
|
|
761
|
+
read -r _ || true
|
|
762
|
+
read -r owner_pid || true
|
|
763
|
+
} < "$owner_file"
|
|
764
|
+
fi
|
|
765
|
+
if [[ "$owner_pid" =~ ^[0-9]+$ ]]; then
|
|
766
|
+
color_err "Hub Python lock を取得できませんでした: $lock_dir。保持しているプロセス (PID $owner_pid) が動いているか ps -p $owner_pid で確かめ、動いていなければこのディレクトリを削除して再実行してください"
|
|
767
|
+
else
|
|
768
|
+
color_err "Hub Python lock を取得できませんでした: $lock_dir。インストーラが走っていないならこのディレクトリを削除して再実行してください"
|
|
769
|
+
fi
|
|
770
|
+
return 1
|
|
771
|
+
fi
|
|
772
|
+
sleep 0.1
|
|
773
|
+
done
|
|
774
|
+
if ! printf '%s\n%s\n' "$token" "$$" > "$owner_file"; then
|
|
775
|
+
rm -rf "$lock_dir" 2>/dev/null || true
|
|
776
|
+
return 1
|
|
777
|
+
fi
|
|
778
|
+
HUB_PYTHON_LOCK_TOKEN="$token"
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
_replace_hub_venv() (
|
|
782
|
+
local new_dir="$1" old_dir="$2" moved_old=0 installed=0
|
|
783
|
+
cleanup_replacement() {
|
|
784
|
+
if (( moved_old == 1 && installed == 0 )) && [[ -e "$old_dir" ]] && [[ ! -e "$HUB_VENV_DIR" ]]; then
|
|
785
|
+
mv "$old_dir" "$HUB_VENV_DIR"
|
|
786
|
+
fi
|
|
787
|
+
}
|
|
788
|
+
trap cleanup_replacement EXIT
|
|
789
|
+
if [[ -e "$HUB_VENV_DIR" || -L "$HUB_VENV_DIR" ]]; then
|
|
790
|
+
mv "$HUB_VENV_DIR" "$old_dir" || return 1
|
|
791
|
+
moved_old=1
|
|
792
|
+
fi
|
|
793
|
+
mv "$new_dir" "$HUB_VENV_DIR" || return 1
|
|
794
|
+
installed=1
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
_publish_hub_python_wrapper() (
|
|
798
|
+
local local_bin="$HOME/.local/bin" wrapper="$HOME/.local/bin/hub-python3"
|
|
799
|
+
local staged content wrapper_content
|
|
800
|
+
local marker='# hub-agent managed: hub-python3 (hub-agent のインストーラが生成・更新します。手で編集しないでください)'
|
|
801
|
+
local managed_line="exec \"$HUB_VENV_DIR/bin/python3\" \"\$@\""
|
|
802
|
+
mkdir -p "$local_bin"
|
|
803
|
+
if [[ -L "$wrapper" ]]; then
|
|
804
|
+
color_err "$wrapper は名前衝突しています。既存 symlink を退避するか PATH を修正してください"
|
|
805
|
+
return 1
|
|
806
|
+
fi
|
|
807
|
+
staged="$(mktemp "$local_bin/.hub-python3.tmp.XXXXXX")" || return 1
|
|
808
|
+
trap 'rm -f "$staged"' EXIT
|
|
809
|
+
printf '%s\n' '#!/usr/bin/env sh' "$marker" "$managed_line" > "$staged" || return 1
|
|
810
|
+
chmod +x "$staged" || return 1
|
|
811
|
+
# ⚠️ `$(<file)` は末尾の改行を落とすので、この比較は「末尾改行の数の違い」と「最終行の改行の
|
|
812
|
+
# 有無」を区別しない。内容のある行が 1 行でも違えば不一致になり、末尾改行だけが違う wrapper を
|
|
813
|
+
# 上書きしても失われる情報は無いので許容する。サンドボックスに cat / stat / wc / cmp が無く、
|
|
814
|
+
# バイト単位の比較には別の機構が要るため、そのために機構を足すことはしない。
|
|
815
|
+
# 以前付けていた sentinel の "x" は末尾改行の差を捉えているように見えて実際は効いていなかった
|
|
816
|
+
# (実測: `$(<a)x` は "a\n" と "a\n\n\n" と "a" のすべてで同じ値になる)ので外した。
|
|
817
|
+
content="$(<"$staged")"
|
|
818
|
+
[[ "$content" == $'#!/usr/bin/env sh\n'"$marker"$'\n'"$managed_line" ]] || return 1
|
|
819
|
+
_hub_python_wrapper_is_managed() {
|
|
820
|
+
[[ -f "$wrapper" && ! -L "$wrapper" ]] || return 1
|
|
821
|
+
wrapper_content="$(<"$wrapper")"
|
|
822
|
+
[[ "$wrapper_content" == "$content" ]] || return 1
|
|
823
|
+
# 未公開ブランチにしか存在しなかった marker なしの旧形は受け入れない。将来内容を変える版では
|
|
824
|
+
# marker で識別し、既知の旧内容を完全一致で列挙する。パスのワイルドカード許可には戻さない。
|
|
825
|
+
# marker をコピーした同一ユーザーの別 wrapper も管理物とみなす。脅威モデルは攻撃者ではなく、
|
|
826
|
+
# 同一ユーザーによる並行実行と中断であるため、このなりすまし耐性は担保しない。
|
|
827
|
+
}
|
|
828
|
+
if [[ -e "$wrapper" ]] && ! _hub_python_wrapper_is_managed; then
|
|
829
|
+
color_err "$wrapper は名前衝突しています。既存ファイルを退避するか PATH を修正してください"
|
|
830
|
+
return 1
|
|
831
|
+
fi
|
|
832
|
+
if [[ -L "$wrapper" ]] || { [[ -e "$wrapper" ]] && ! _hub_python_wrapper_is_managed; }; then
|
|
833
|
+
color_err "$wrapper は公開直前に名前衝突しました。既存ファイルを退避するか PATH を修正してください"
|
|
834
|
+
return 1
|
|
835
|
+
fi
|
|
836
|
+
mv "$staged" "$wrapper" || return 1
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# lock がまだ自分のものかを確かめる。これは相互排除ではなく best-effort の検出であり、確認と
|
|
840
|
+
# 操作の間の TOCTOU の窓は残る。自動回収の削除後に自分の lock を失うのは、人が lock
|
|
841
|
+
# ディレクトリを削除して別のインストーラを起動した場合だけなので、そのとき黙って並行実行しない
|
|
842
|
+
# ために破壊的操作の直前で検出する。
|
|
843
|
+
_hub_python_lock_is_mine() {
|
|
844
|
+
local lock_dir="$1" want="$2" owner_token=""
|
|
845
|
+
[[ -n "$want" && -f "$lock_dir/owner" ]] || return 1
|
|
846
|
+
read -r owner_token < "$lock_dir/owner" || true
|
|
847
|
+
[[ "$owner_token" == "$want" ]]
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
_hub_python_wrapper_verified() {
|
|
851
|
+
# ⚠️ realpath(sys.executable) で比較しないこと。venv の bin/python3 は土台 interpreter への
|
|
852
|
+
# symlink なので、realpath は venv でも素の土台でも同じ値に解決される(実測)。それでは
|
|
853
|
+
# 「site-packages を持たない素の uv interpreter を指す wrapper」を成功扱いにしてしまう。
|
|
854
|
+
# 識別に使えるのは sys.prefix(venv 自身のパス)。
|
|
855
|
+
local expected_prefix actual_prefix spec
|
|
856
|
+
expected_prefix="$(cd "$HUB_VENV_DIR" 2>/dev/null && pwd -P)" || expected_prefix=""
|
|
857
|
+
actual_prefix="$(hub-python3 -c 'import os, sys; print(os.path.realpath(sys.prefix))' 2>/dev/null)" || actual_prefix=""
|
|
858
|
+
if [[ -z "$expected_prefix" || "$actual_prefix" != "$expected_prefix" ]]; then
|
|
859
|
+
color_err "hub-python3 が Hub Python venv を指していません (expected prefix: ${expected_prefix:-unknown}, actual: ${actual_prefix:-unknown})"
|
|
860
|
+
return 1
|
|
861
|
+
fi
|
|
862
|
+
# 版一致だけでは wheel の破損を見逃すため、wrapper 経由で実際に import できることまで確認する。
|
|
863
|
+
# ⚠️ この import 検査は `_hub_venv_usable` の import 検査と同じ interpreter を通るため重複しており、
|
|
864
|
+
# 独立した回帰テストは無い(wrapper 側だけを失敗させる状態を作れない)。担保は上の prefix 比較で、
|
|
865
|
+
# ここは公開後の wrapper 経路を一度通す念のための確認だと理解して読むこと。
|
|
866
|
+
for spec in "${HUB_PYTHON_IMPORTS[@]}"; do
|
|
867
|
+
if ! hub-python3 -c 'import importlib, sys; importlib.import_module(sys.argv[1])' "$spec" >/dev/null 2>&1; then
|
|
868
|
+
color_err "hub-python3 経由で $spec を import できません"
|
|
869
|
+
return 1
|
|
870
|
+
fi
|
|
871
|
+
done
|
|
872
|
+
color_ok "hub-python3 が Hub Python venv を指すことを確認"
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
_finalize_hub_python() (
|
|
876
|
+
# 退避先は lock token ごとの実行固有名にし、他実行の退避物と削除対象を分離する。
|
|
877
|
+
local new_dir="$1" old_dir="" leftover="" found_leftover=0
|
|
878
|
+
local lock_dir="${HUB_VENV_DIR}.lock" lock_token="" owner_token=""
|
|
879
|
+
trap 'rm -rf "$new_dir"; owner_token=""; if [[ -f "$lock_dir/owner" ]]; then read -r owner_token < "$lock_dir/owner" || true; fi; if [[ -n "$lock_token" && "$owner_token" == "$lock_token" ]]; then rm -rf "$lock_dir"; fi' EXIT
|
|
880
|
+
trap 'exit 130' INT
|
|
881
|
+
trap 'exit 143' TERM
|
|
882
|
+
_acquire_hub_python_lock "$lock_dir" || exit 1
|
|
883
|
+
lock_token="$HUB_PYTHON_LOCK_TOKEN"
|
|
884
|
+
old_dir="${HUB_VENV_DIR}.old.${lock_token}"
|
|
885
|
+
if _hub_venv_usable; then
|
|
886
|
+
color_ok "Hub Python venv は利用可能 ($HUB_VENV_DIR)"
|
|
887
|
+
elif [[ -n "$new_dir" ]]; then
|
|
888
|
+
if ! _hub_python_lock_is_mine "$lock_dir" "$lock_token"; then
|
|
889
|
+
color_err "Hub Python lock を他のインストーラに引き継がれたため差し替えを中止しました: $lock_dir"
|
|
890
|
+
exit 1
|
|
891
|
+
fi
|
|
892
|
+
# 残骸を別名で溜めず、stale lock と同様に手動復旧へ統一する。
|
|
893
|
+
# ⚠️ ここへ来るのは公開先が使えないと判定した場合なので、残骸は「直前まで使えていた venv」で
|
|
894
|
+
# ある可能性が高い。削除だけを案内せず、戻す選択肢を先に示す。
|
|
895
|
+
for leftover in "${HUB_VENV_DIR}".old*; do
|
|
896
|
+
if [[ -e "$leftover" || -L "$leftover" ]]; then
|
|
897
|
+
color_err "退避先が既に存在します: $leftover"
|
|
898
|
+
found_leftover=1
|
|
899
|
+
fi
|
|
900
|
+
done
|
|
901
|
+
if (( found_leftover == 1 )); then
|
|
902
|
+
color_err "前回の中断が残っている可能性があります。直前まで使えていた venv かもしれません。中身を確認して $HUB_VENV_DIR へ手で入れ替えるか、不要なら削除してから再実行してください"
|
|
903
|
+
exit 1
|
|
904
|
+
fi
|
|
905
|
+
_replace_hub_venv "$new_dir" "$old_dir" || exit 1
|
|
906
|
+
color_ok "Hub Python venv を構築 ($HUB_VENV_DIR)"
|
|
907
|
+
else
|
|
908
|
+
color_err "Hub Python venv の再構築結果がありません"
|
|
909
|
+
exit 1
|
|
910
|
+
fi
|
|
911
|
+
if ! _hub_python_lock_is_mine "$lock_dir" "$lock_token"; then
|
|
912
|
+
color_err "Hub Python lock を他のインストーラに引き継がれたため wrapper の公開を中止しました: $lock_dir"
|
|
913
|
+
exit 1
|
|
914
|
+
fi
|
|
915
|
+
_publish_hub_python_wrapper || exit 1
|
|
916
|
+
if ! _hub_python_wrapper_verified; then
|
|
917
|
+
if [[ -e "$old_dir" || -L "$old_dir" ]]; then
|
|
918
|
+
color_err "公開後の検証に失敗しました。公開先 $HUB_VENV_DIR は変更していません。直前まで使えていた venv は $old_dir にあります。中身を確認して手で入れ替えるか、$old_dir を削除してから再実行してください"
|
|
919
|
+
else
|
|
920
|
+
color_err "公開後の検証に失敗しました。公開先 $HUB_VENV_DIR は変更していません。手で確認してください"
|
|
921
|
+
fi
|
|
922
|
+
exit 1
|
|
923
|
+
fi
|
|
924
|
+
# 公開後の最終検証に成功してから、lock を保持したまま実行固有名の退避物を削除する。
|
|
925
|
+
# 差し替えを行わなかった実行ではこのパスは存在せず、他実行が残した退避物は削除しない。
|
|
926
|
+
if ! rm -rf "$old_dir" 2>/dev/null; then
|
|
927
|
+
color_warn "元の venv を削除できませんでした: $old_dir。中身を確認して手動で削除してください"
|
|
928
|
+
fi
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
ensure_hub_python() {
|
|
932
|
+
local local_bin="$HOME/.local/bin" uv_path
|
|
933
|
+
export PATH="$PATH:$local_bin"
|
|
934
|
+
hash -r 2>/dev/null || true
|
|
935
|
+
if present uv; then
|
|
936
|
+
uv_path="$(command -v uv)"
|
|
937
|
+
uv_path="$(cd "$(dirname "$uv_path")" && pwd -P)/$(basename "$uv_path")"
|
|
938
|
+
if ! uv --version >/dev/null 2>&1 || ! uv venv --help >/dev/null 2>&1 \
|
|
939
|
+
|| ! uv pip --help >/dev/null 2>&1 || ! uv python --help >/dev/null 2>&1; then
|
|
940
|
+
color_err "発見した uv を利用できません: $uv_path(上書きせず停止します)"
|
|
941
|
+
exit 1
|
|
942
|
+
fi
|
|
943
|
+
color_ok "uv already installed ($uv_path)"
|
|
944
|
+
else
|
|
945
|
+
_install_hub_uv || exit 1
|
|
946
|
+
color_ok "uv $UV_VERSION を $local_bin に導入"
|
|
947
|
+
fi
|
|
948
|
+
persist_profile_snippet 'export PATH="$PATH:$HOME/.local/bin"' "~/.local/bin PATH" \
|
|
949
|
+
"# >>> hub-agent: local-bin PATH (Hub Python) >>>" "# <<< hub-agent: local-bin PATH <<<" \
|
|
950
|
+
"$HOME/.zprofile" "$(bash_login_profile)" || exit 1
|
|
951
|
+
|
|
952
|
+
retry 3 uv python install "$HUB_PYTHON_MINOR" || exit 1
|
|
953
|
+
if _hub_venv_usable; then
|
|
954
|
+
_finalize_hub_python "" || exit 1
|
|
955
|
+
else
|
|
956
|
+
# ⚠️ 構築成功から finalize の trap 設置までの中断で未公開の .new が残らないよう、
|
|
957
|
+
# 構築と finalize を同じ cleanup スコープに入れる。差し替え成功後は .new のパスが
|
|
958
|
+
# 消えているので、この trap の rm は no-op になる。
|
|
959
|
+
(
|
|
960
|
+
new_dir="${HUB_VENV_DIR}.new.$$"
|
|
961
|
+
trap 'rm -rf "$new_dir"' EXIT
|
|
962
|
+
trap 'exit 130' INT
|
|
963
|
+
trap 'exit 143' TERM
|
|
964
|
+
_build_hub_venv "$new_dir" || exit 1
|
|
965
|
+
_finalize_hub_python "$new_dir" || exit 1
|
|
966
|
+
) || exit 1
|
|
967
|
+
fi
|
|
968
|
+
# 公開後の検証成功後にだけ、Hub Python を指す profile を更新する。
|
|
969
|
+
export HUB_AI_PYTHON="$HUB_VENV_DIR/bin/python3"
|
|
970
|
+
persist_profile_snippet "export HUB_AI_PYTHON=\"$HUB_VENV_DIR/bin/python3\"" "HUB_AI_PYTHON" \
|
|
971
|
+
"# >>> hub-agent: HUB_AI_PYTHON >>>" "# <<< hub-agent: HUB_AI_PYTHON <<<" \
|
|
972
|
+
"$HOME/.zprofile" "$(bash_login_profile)" || exit 1
|
|
973
|
+
}
|
|
974
|
+
|
|
624
975
|
# timeout を保証する(補助コマンド。AI セッションの Bash やスクリプトが使うが、
|
|
625
976
|
# hub-agent の常駐動作には必須ではないため、導入失敗は警告して継続する。
|
|
626
977
|
# 必須ランタイムの python3 とは契約を分けている)。
|
|
@@ -1042,7 +1393,7 @@ EOF
|
|
|
1042
1393
|
|
|
1043
1394
|
main() {
|
|
1044
1395
|
color_step "hub-agent ワンライナーセットアップを開始"
|
|
1045
|
-
STEP_TOTAL=
|
|
1396
|
+
STEP_TOTAL=16
|
|
1046
1397
|
STEP_NUM=0
|
|
1047
1398
|
|
|
1048
1399
|
step_header "Homebrew"
|
|
@@ -1055,6 +1406,8 @@ main() {
|
|
|
1055
1406
|
ensure_build_tools
|
|
1056
1407
|
step_header "Python 3"
|
|
1057
1408
|
ensure_python3
|
|
1409
|
+
step_header "Hub Python (uv / shared venv)"
|
|
1410
|
+
ensure_hub_python
|
|
1058
1411
|
step_header "GNU coreutils (timeout)"
|
|
1059
1412
|
ensure_timeout
|
|
1060
1413
|
step_header "npm global prefix"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createEventMasker, MASK_ENABLED, redactSecretsValue } from "./tool-result-mask.mjs"
|
|
2
|
+
|
|
3
|
+
/** Also cover errors, ready metadata and permission copies outside SDK _emit.
|
|
4
|
+
* Recheck the complete envelope deliberately: ready/history can combine already
|
|
5
|
+
* masked events with newly added metadata. A type-based bypass would trust those
|
|
6
|
+
* unrelated fields too. Do not trade this boundary for a size-based raw fallback. */
|
|
7
|
+
export function installClaudeDisplayMask(client) {
|
|
8
|
+
const send = client.send.bind(client)
|
|
9
|
+
client.send = (payload, ...args) => {
|
|
10
|
+
if (!MASK_ENABLED || !payload?.type?.startsWith("claude.")) return send(payload, ...args)
|
|
11
|
+
let masked
|
|
12
|
+
try {
|
|
13
|
+
masked = redactSecretsValue(payload)
|
|
14
|
+
} catch {
|
|
15
|
+
masked = {
|
|
16
|
+
type: "claude.error",
|
|
17
|
+
stream_id: payload.stream_id,
|
|
18
|
+
session_id: payload.session_id,
|
|
19
|
+
error: "[本文を安全にマスクできないため表示を省略しました]",
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return send(masked, ...args)
|
|
23
|
+
}
|
|
24
|
+
return client
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A partial string cannot be redacted independently: a key or a quoted value may
|
|
29
|
+
* straddle notifications. Keep lifecycle notifications, but publish text,
|
|
30
|
+
* thinking and tool arguments only with the complete assistant message. The
|
|
31
|
+
* SDK/model input and the original event are never modified here.
|
|
32
|
+
*/
|
|
33
|
+
export function createClaudeDisplayMasker(initialToolNames) {
|
|
34
|
+
const mask = createEventMasker(initialToolNames)
|
|
35
|
+
return (event) => {
|
|
36
|
+
if (!MASK_ENABLED) return event
|
|
37
|
+
if (event?.type === "stream_event") {
|
|
38
|
+
const partial = event.event
|
|
39
|
+
if (partial?.type === "content_block_delta") {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
if (partial?.type === "content_block_start") {
|
|
43
|
+
const block = partial.content_block
|
|
44
|
+
return mask({
|
|
45
|
+
...event,
|
|
46
|
+
event: {
|
|
47
|
+
type: partial.type,
|
|
48
|
+
index: partial.index,
|
|
49
|
+
content_block: {
|
|
50
|
+
type: block?.type,
|
|
51
|
+
...(block?.id ? { id: block.id } : {}),
|
|
52
|
+
...(block?.name ? { name: block.name } : {}),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return mask(event)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Failure must not disclose either the source payload or an exception message. */
|
|
63
|
+
export function claudeMaskFailure(event) {
|
|
64
|
+
const notice = "[本文を安全にマスクできないため表示を省略しました]"
|
|
65
|
+
if (event?.type === "result") {
|
|
66
|
+
return {
|
|
67
|
+
type: "result",
|
|
68
|
+
subtype: event.subtype,
|
|
69
|
+
is_error: event.is_error,
|
|
70
|
+
session_id: event.session_id,
|
|
71
|
+
result: notice,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (event?.type === "system" && event.subtype === "session_state_changed") {
|
|
75
|
+
return { type: "system", subtype: event.subtype, state: event.state }
|
|
76
|
+
}
|
|
77
|
+
if (event?.type === "assistant" || event?.type === "user") {
|
|
78
|
+
// Keep a renderable completed message and numeric accounting only. Do not
|
|
79
|
+
// copy arbitrary usage/provider metadata from the failed source envelope.
|
|
80
|
+
const usage = Object.fromEntries([
|
|
81
|
+
"input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens",
|
|
82
|
+
].filter(key => typeof event.message?.usage?.[key] === "number" && Number.isFinite(event.message.usage[key]))
|
|
83
|
+
.map(key => [key, event.message.usage[key]]))
|
|
84
|
+
return { type: event.type, message: { role: event.type,
|
|
85
|
+
content: [{ type: "text", text: notice }], ...(Object.keys(usage).length ? { usage } : {}),
|
|
86
|
+
} }
|
|
87
|
+
}
|
|
88
|
+
return { type: "system", subtype: "mask_error", message: notice }
|
|
89
|
+
}
|
|
@@ -27,9 +27,10 @@ import { jsonlPath } from "./claude-history.mjs"
|
|
|
27
27
|
import { watchSessionFile } from "./claude-history-watch.mjs"
|
|
28
28
|
import {
|
|
29
29
|
MASK_ENABLED,
|
|
30
|
-
createEventMasker,
|
|
31
30
|
maskJsonlFileAtRest,
|
|
32
31
|
} from "./tool-result-mask.mjs"
|
|
32
|
+
import { createClaudeDisplayMasker, claudeMaskFailure } from "./claude-display-mask.mjs"
|
|
33
|
+
import { copyPermissionInput, preparePermissionInput, restorePermissionInput, PERMISSION_MASK_DENIAL } from "./permission-display-mask.mjs"
|
|
33
34
|
import { getOrCreateSiblingToken } from "./sibling-token.mjs"
|
|
34
35
|
import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
35
36
|
import { readPositiveIntEnv, sanitizePositiveInt } from "./positive-int-env.mjs"
|
|
@@ -693,7 +694,7 @@ export class ClaudeStreamSession {
|
|
|
693
694
|
/** マスク (2026-06-23): ライブ stream / watch 用のステートフル masker。tool_use_id→name を
|
|
694
695
|
* 蓄積し、tool_result の秘匿情報 / 非公開スキルプロンプト本文を表示・配信前に伏せる。
|
|
695
696
|
* モデル消費後のコピーに作用するためスキル実行は壊れない。 */
|
|
696
|
-
this._eventMasker =
|
|
697
|
+
this._eventMasker = createClaudeDisplayMasker()
|
|
697
698
|
/** オンディスク jsonl をマスク済みか (close 時の二重書き換えを防ぐ冪等ガード)。 */
|
|
698
699
|
this._jsonlMasked = false
|
|
699
700
|
|
|
@@ -702,7 +703,7 @@ export class ClaudeStreamSession {
|
|
|
702
703
|
}
|
|
703
704
|
|
|
704
705
|
/** マスク済みイベントを onEvent へ流す共通経路。秘匿情報 / 非公開スキルプロンプト本文を
|
|
705
|
-
* 表示・WS
|
|
706
|
+
* 表示・WS 配信前に伏せる。マスク失敗時も元の本文へ戻さない。 */
|
|
706
707
|
/** 連携カード種別 (Phase 2): 注入本文と source を FIFO に控える。sendToCwd/sendToStream が
|
|
707
708
|
* session.sendMessage() を呼ぶ直前に記録し、**drain 昇格時に _emitQueueState が started 本文
|
|
708
709
|
* 一致で消費**して startedSources として frontend へ載せる。
|
|
@@ -846,15 +847,15 @@ export class ClaudeStreamSession {
|
|
|
846
847
|
if (MASK_ENABLED) {
|
|
847
848
|
try {
|
|
848
849
|
out = this._eventMasker(event)
|
|
849
|
-
} catch
|
|
850
|
+
} catch {
|
|
850
851
|
this.logger?.warn(
|
|
851
|
-
{
|
|
852
|
+
{ stream_id: this.stream_id },
|
|
852
853
|
"event mask failed",
|
|
853
854
|
)
|
|
854
|
-
out = event
|
|
855
|
+
out = claudeMaskFailure(event)
|
|
855
856
|
}
|
|
856
857
|
}
|
|
857
|
-
this.onEvent(out)
|
|
858
|
+
if (out != null) this.onEvent(out)
|
|
858
859
|
}
|
|
859
860
|
|
|
860
861
|
/** セッション終了時にオンディスク jsonl をマスク書き換えする (冪等)。Claude CLI が当該
|
|
@@ -947,18 +948,25 @@ export class ClaudeStreamSession {
|
|
|
947
948
|
/** SDK options.canUseTool: tool 起動を許可するかを browser に問い合わせる。 */
|
|
948
949
|
async _canUseTool(toolName, input) {
|
|
949
950
|
if (!this.onPermission) return { behavior: "allow", updatedInput: input }
|
|
951
|
+
let displayState
|
|
952
|
+
try {
|
|
953
|
+
displayState = MASK_ENABLED ? preparePermissionInput(input, { toolName }) : null
|
|
954
|
+
} catch {
|
|
955
|
+
return { behavior: "deny", message: PERMISSION_MASK_DENIAL }
|
|
956
|
+
}
|
|
950
957
|
const request_id = randomUUID()
|
|
951
958
|
return await new Promise((resolve) => {
|
|
952
959
|
this._permissionResolvers.set(request_id, {
|
|
953
960
|
resolve,
|
|
954
961
|
tool_name: toolName,
|
|
955
962
|
input,
|
|
963
|
+
displayState,
|
|
956
964
|
})
|
|
957
965
|
try {
|
|
958
|
-
this.onPermission({ tool_name: toolName, input, request_id })
|
|
966
|
+
this.onPermission({ tool_name: toolName, input: displayState ? copyPermissionInput(displayState.displayInput) : input, request_id })
|
|
959
967
|
} catch (err) {
|
|
960
968
|
this.logger?.warn(
|
|
961
|
-
{
|
|
969
|
+
{ stream_id: this.stream_id },
|
|
962
970
|
"onPermission callback threw",
|
|
963
971
|
)
|
|
964
972
|
this._permissionResolvers.delete(request_id)
|
|
@@ -1014,6 +1022,16 @@ export class ClaudeStreamSession {
|
|
|
1014
1022
|
this._permissionResolvers.delete(request_id)
|
|
1015
1023
|
// B11: 現 primary 端末からの permission 応答を活性として記録する。
|
|
1016
1024
|
this.touch(this.stream_id)
|
|
1025
|
+
if (!["allow", "deny"].includes(decision?.behavior)) {
|
|
1026
|
+
decision = { behavior: "deny", message: PERMISSION_MASK_DENIAL }
|
|
1027
|
+
}
|
|
1028
|
+
if (MASK_ENABLED && decision?.behavior === "allow" && r.displayState) {
|
|
1029
|
+
try {
|
|
1030
|
+
decision = { ...decision, updatedInput: restorePermissionInput(r.displayState.originalInput, r.displayState.displayInput, decision.updatedInput, { toolName: r.tool_name }) }
|
|
1031
|
+
} catch {
|
|
1032
|
+
decision = { behavior: "deny", message: PERMISSION_MASK_DENIAL }
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1017
1035
|
r.resolve(decision)
|
|
1018
1036
|
return true
|
|
1019
1037
|
}
|
|
@@ -1062,12 +1080,19 @@ export class ClaudeStreamSession {
|
|
|
1062
1080
|
if (!this.onPermission || this._permissionResolvers.size === 0) return
|
|
1063
1081
|
for (const [request_id, r] of this._permissionResolvers) {
|
|
1064
1082
|
try {
|
|
1065
|
-
this.onPermission({ tool_name: r.tool_name, input: r.input, request_id })
|
|
1083
|
+
this.onPermission({ tool_name: r.tool_name, input: r.displayState ? copyPermissionInput(r.displayState.displayInput) : r.input, request_id })
|
|
1066
1084
|
} catch (err) {
|
|
1067
1085
|
this.logger?.warn(
|
|
1068
|
-
{
|
|
1086
|
+
{ stream_id: this.stream_id, request_id },
|
|
1069
1087
|
"replay permission failed",
|
|
1070
1088
|
)
|
|
1089
|
+
this._permissionResolvers.delete(request_id)
|
|
1090
|
+
r.resolve({ behavior: "deny", message: PERMISSION_MASK_DENIAL })
|
|
1091
|
+
try {
|
|
1092
|
+
this.onPermissionCancel?.({ request_ids: [request_id], reason: "replay failed" })
|
|
1093
|
+
} catch {
|
|
1094
|
+
this.logger?.warn({ stream_id: this.stream_id, request_id }, "permission cancel callback failed")
|
|
1095
|
+
}
|
|
1071
1096
|
}
|
|
1072
1097
|
}
|
|
1073
1098
|
}
|