anolisa-tokenless 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +770 -0
- package/adapters/tokenless/claude-code/.claude-plugin/marketplace.json +15 -0
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +9 -0
- package/adapters/tokenless/claude-code/hooks/hooks.json +38 -0
- package/adapters/tokenless/claude-code/scripts/detect.sh +193 -0
- package/adapters/tokenless/claude-code/scripts/install.sh +92 -0
- package/adapters/tokenless/claude-code/scripts/uninstall.sh +50 -0
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +20 -0
- package/adapters/tokenless/codex/README.md +160 -0
- package/adapters/tokenless/codex/hooks/hooks.json +52 -0
- package/adapters/tokenless/codex/scripts/_common.sh +25 -0
- package/adapters/tokenless/codex/scripts/check-tokenless +94 -0
- package/adapters/tokenless/codex/scripts/compress-response +441 -0
- package/adapters/tokenless/codex/scripts/detect.sh +61 -0
- package/adapters/tokenless/codex/scripts/install.sh +151 -0
- package/adapters/tokenless/codex/scripts/rewrite-hook +282 -0
- package/adapters/tokenless/codex/scripts/tool-ready +303 -0
- package/adapters/tokenless/codex/scripts/uninstall.sh +78 -0
- package/adapters/tokenless/common/commands/tokenless-stats.toml +2 -0
- package/adapters/tokenless/common/cosh-extension.json +65 -0
- package/adapters/tokenless/common/hooks/compress_response_hook.py +487 -0
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +144 -0
- package/adapters/tokenless/common/hooks/compress_toon_hook.py +160 -0
- package/adapters/tokenless/common/hooks/hook_utils.py +584 -0
- package/adapters/tokenless/common/hooks/rewrite_hook.py +223 -0
- package/adapters/tokenless/common/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/common/hooks/tool_categories.json +99 -0
- package/adapters/tokenless/common/hooks/tool_ready_hook.sh +571 -0
- package/adapters/tokenless/common/tokenless-env-fix.sh +730 -0
- package/adapters/tokenless/common/tool-ready-spec.json +113 -0
- package/adapters/tokenless/dsh/cordis.patch.yml +8 -0
- package/adapters/tokenless/dsh/dist/index.js +399 -0
- package/adapters/tokenless/dsh/package.json +26 -0
- package/adapters/tokenless/hermes/__init__.py +572 -0
- package/adapters/tokenless/hermes/plugin.yaml +9 -0
- package/adapters/tokenless/hermes/scripts/detect.sh +80 -0
- package/adapters/tokenless/hermes/scripts/install.sh +60 -0
- package/adapters/tokenless/hermes/scripts/uninstall.sh +45 -0
- package/adapters/tokenless/manifest.json +147 -0
- package/adapters/tokenless/openclaw/dist/index.d.ts +27 -0
- package/adapters/tokenless/openclaw/dist/index.js +598 -0
- package/adapters/tokenless/openclaw/dist/tool_categories.json +99 -0
- package/adapters/tokenless/openclaw/index.ts +720 -0
- package/adapters/tokenless/openclaw/openclaw.plugin.json +40 -0
- package/adapters/tokenless/openclaw/package.json +24 -0
- package/adapters/tokenless/openclaw/scripts/detect.sh +102 -0
- package/adapters/tokenless/openclaw/scripts/install.sh +75 -0
- package/adapters/tokenless/openclaw/scripts/uninstall.sh +46 -0
- package/adapters/tokenless/openclaw/tsconfig.json +13 -0
- package/adapters/tokenless/opencode/plugin.js +246 -0
- package/adapters/tokenless/opencode/scripts/detect.sh +38 -0
- package/adapters/tokenless/opencode/scripts/install.sh +56 -0
- package/adapters/tokenless/opencode/scripts/uninstall.sh +29 -0
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +11 -0
- package/adapters/tokenless/qoder/commands/tokenless-stats.md +8 -0
- package/adapters/tokenless/qoder/hooks/hooks.json +36 -0
- package/adapters/tokenless/qoder/hooks/run-hook.sh +51 -0
- package/adapters/tokenless/qoder/scripts/detect.sh +35 -0
- package/adapters/tokenless/qoder/scripts/install.sh +136 -0
- package/adapters/tokenless/qoder/scripts/uninstall.sh +106 -0
- package/adapters/tokenless/qwencode/qwen-extension.json +69 -0
- package/adapters/tokenless/qwencode/scripts/detect.sh +125 -0
- package/adapters/tokenless/qwencode/scripts/install.sh +128 -0
- package/adapters/tokenless/qwencode/scripts/uninstall.sh +62 -0
- package/bin/rtk +6 -0
- package/bin/tokenless +6 -0
- package/bin/toon +6 -0
- package/package.json +57 -0
- package/scripts/postinstall.js +208 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tokenless-hook-version: 10
|
|
3
|
+
# Token-Less legacy Tool Ready environment pre-check (hard-disabled).
|
|
4
|
+
#
|
|
5
|
+
# Hook event: PreToolUse (matcher: "" — matches all tools)
|
|
6
|
+
# Requires: none while the hard bypass below is active
|
|
7
|
+
#
|
|
8
|
+
# Design: fail-open. If jq is missing or any phase fails, exit 0 silently.
|
|
9
|
+
#
|
|
10
|
+
# Dormant legacy four-phase flow (not reached while the hard bypass is active):
|
|
11
|
+
# Phase 1 — LOOKUP: Find tool in config dictionary. Not found → skip.
|
|
12
|
+
# Phase 2 — CHECK: Scan system readiness. All ready → continue silently.
|
|
13
|
+
# Phase 3 — FIX: Auto-install missing deps. Success → continue silently.
|
|
14
|
+
# Phase 4 — FEEDBACK: Fix failed. Inject additionalContext → "Skip retry".
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
# HARD BYPASS: the legacy readiness rules can incorrectly block valid agent
|
|
19
|
+
# work. Exit before reading input, loading the spec, running jq, attempting a
|
|
20
|
+
# repair, or emitting a block decision. Emit a valid pass-through response for
|
|
21
|
+
# hook runtimes that require JSON on successful execution. Re-enable only
|
|
22
|
+
# through a source change accompanied by a redesign of the readiness model.
|
|
23
|
+
printf '%s\n' '{}'
|
|
24
|
+
exit 0
|
|
25
|
+
|
|
26
|
+
VERBOSE="${TOKENLESS_VERBOSE:-}"
|
|
27
|
+
log_v() { [ -n "$VERBOSE" ] && echo "[tokenless:ready] $1" >&2 || true; }
|
|
28
|
+
|
|
29
|
+
USER_HOME="${HOME:-}"
|
|
30
|
+
case "$USER_HOME" in
|
|
31
|
+
/*) ;;
|
|
32
|
+
*) USER_HOME="" ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
# --- Dependency check (fail-open) ---
|
|
36
|
+
if ! command -v jq &>/dev/null; then log_v "jq not found, skipping"; exit 0; fi
|
|
37
|
+
|
|
38
|
+
# --- File trust validation ---
|
|
39
|
+
# User-writable paths must be owned by current user and not world-writable.
|
|
40
|
+
#
|
|
41
|
+
# KEEP IN SYNC with the Rust equivalent in
|
|
42
|
+
# `crates/tokenless-cli/src/env_check/mod.rs` (`is_trusted_path`).
|
|
43
|
+
# Changes to trust criteria must be applied to both implementations.
|
|
44
|
+
is_trusted_file() {
|
|
45
|
+
local f="$1"
|
|
46
|
+
[ -f "$f" ] || return 1
|
|
47
|
+
# System paths are always trusted
|
|
48
|
+
case "$f" in /usr/share/*|/usr/libexec/*|/usr/lib/anolisa/*|/usr/local/share/*) return 0 ;; esac
|
|
49
|
+
# Resolve symlink target before owner/perm checks
|
|
50
|
+
local check_path="$f"
|
|
51
|
+
local symlink_parent=""
|
|
52
|
+
if [ -L "$f" ]; then
|
|
53
|
+
local target
|
|
54
|
+
target=$(readlink -f "$f" 2>/dev/null || realpath "$f" 2>/dev/null || echo "")
|
|
55
|
+
# System targets are always trusted
|
|
56
|
+
case "$target" in /usr/share/*|/usr/libexec/*|/usr/lib/anolisa/*|/usr/local/share/*) return 0 ;; esac
|
|
57
|
+
[ -z "$target" ] && return 1
|
|
58
|
+
check_path="$target"
|
|
59
|
+
# Also check the symlink's own parent directory — if the symlink sits
|
|
60
|
+
# in a world-writable directory, an attacker can replace the symlink
|
|
61
|
+
# to point at a malicious file, bypassing the target-level checks below.
|
|
62
|
+
symlink_parent=$(dirname "$f")
|
|
63
|
+
fi
|
|
64
|
+
# Check the parent directory of the resolved target (or the file itself
|
|
65
|
+
# when it's not a symlink) — a world-writable directory allows an
|
|
66
|
+
# attacker to unlink and replace the file (TOCTOU), even if the file
|
|
67
|
+
# itself has correct ownership and permissions. Mirrors the Rust
|
|
68
|
+
# implementation in env_check.rs (`is_trusted_path`).
|
|
69
|
+
_check_parent() {
|
|
70
|
+
local pd="$1"
|
|
71
|
+
local po
|
|
72
|
+
po=$(stat -c '%u' "$pd" 2>/dev/null || stat -f '%u' "$pd" 2>/dev/null || echo "-1")
|
|
73
|
+
if [ "$po" != "$(id -u)" ] && [ "$po" != "0" ]; then
|
|
74
|
+
log_v "BLOCKED: parent dir of $f owned by uid $po (expected $(id -u) or 0)"
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
local pp
|
|
78
|
+
pp=$(stat -c '%a' "$pd" 2>/dev/null || stat -f '%Lp' "$pd" 2>/dev/null || echo "777")
|
|
79
|
+
local ppo="${pp: -1}"
|
|
80
|
+
if (( ppo & 2 )); then
|
|
81
|
+
log_v "BLOCKED: parent dir of $f is world-writable (perms=$pp)"
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
local parent_dir
|
|
87
|
+
parent_dir=$(dirname "$check_path")
|
|
88
|
+
_check_parent "$parent_dir" || return 1
|
|
89
|
+
# When the path is a symlink, also check the symlink's own parent.
|
|
90
|
+
if [ -n "$symlink_parent" ] && [ "$symlink_parent" != "$parent_dir" ]; then
|
|
91
|
+
_check_parent "$symlink_parent" || return 1
|
|
92
|
+
fi
|
|
93
|
+
local file_owner
|
|
94
|
+
file_owner=$(stat -c '%u' "$check_path" 2>/dev/null || stat -f '%u' "$check_path" 2>/dev/null || echo "-1")
|
|
95
|
+
if [ "$file_owner" != "$(id -u)" ] && [ "$file_owner" != "0" ]; then
|
|
96
|
+
log_v "BLOCKED: $f owned by uid $file_owner (expected $(id -u) or 0)"
|
|
97
|
+
return 1
|
|
98
|
+
fi
|
|
99
|
+
local file_perms
|
|
100
|
+
file_perms=$(stat -c '%a' "$check_path" 2>/dev/null || stat -f '%Lp' "$check_path" 2>/dev/null || echo "777")
|
|
101
|
+
local other_perms="${file_perms: -1}"
|
|
102
|
+
if (( other_perms & 2 )); then
|
|
103
|
+
log_v "BLOCKED: $f is world-writable (perms=$file_perms)"
|
|
104
|
+
return 1
|
|
105
|
+
fi
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# --- Resolve paths ---
|
|
110
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
111
|
+
|
|
112
|
+
SPEC_FILE=""
|
|
113
|
+
for candidate in \
|
|
114
|
+
"${TOKENLESS_TOOL_READY_SPEC:-}" \
|
|
115
|
+
"${ANOLISA_ADAPTER_DIR:+$ANOLISA_ADAPTER_DIR/common/tool-ready-spec.json}" \
|
|
116
|
+
"${USER_HOME:+$USER_HOME/.local/share/anolisa/adapters/tokenless/common/tool-ready-spec.json}" \
|
|
117
|
+
"/usr/local/share/anolisa/adapters/tokenless/common/tool-ready-spec.json" \
|
|
118
|
+
"/usr/share/anolisa/adapters/tokenless/common/tool-ready-spec.json" \
|
|
119
|
+
"${USER_HOME:+$USER_HOME/.tokenless/tool-ready-spec.json}" \
|
|
120
|
+
"${SCRIPT_DIR}/../tool-ready-spec.json"; do
|
|
121
|
+
if [ -n "$candidate" ] && is_trusted_file "$candidate"; then
|
|
122
|
+
SPEC_FILE="$candidate"
|
|
123
|
+
break
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
FIX_SCRIPT=""
|
|
128
|
+
# Fixers are installed as non-executable resources and always invoked through
|
|
129
|
+
# bash, so trust and readability—not an execute bit—define usability.
|
|
130
|
+
for candidate in \
|
|
131
|
+
"${TOKENLESS_ENV_FIX_SCRIPT:-}" \
|
|
132
|
+
"${ANOLISA_ADAPTER_DIR:+$ANOLISA_ADAPTER_DIR/common/tokenless-env-fix.sh}" \
|
|
133
|
+
"${USER_HOME:+$USER_HOME/.local/share/anolisa/adapters/tokenless/common/tokenless-env-fix.sh}" \
|
|
134
|
+
"/usr/local/share/anolisa/adapters/tokenless/common/tokenless-env-fix.sh" \
|
|
135
|
+
"/usr/share/anolisa/adapters/tokenless/common/tokenless-env-fix.sh" \
|
|
136
|
+
"${USER_HOME:+$USER_HOME/.tokenless/tokenless-env-fix.sh}" \
|
|
137
|
+
"${SCRIPT_DIR}/../tokenless-env-fix.sh"; do
|
|
138
|
+
if [ -n "$candidate" ] && [ -r "$candidate" ] && is_trusted_file "$candidate"; then
|
|
139
|
+
FIX_SCRIPT="$candidate"
|
|
140
|
+
break
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
# --- Read input (fail-open) ---
|
|
145
|
+
INPUT=$(cat || { exit 0; })
|
|
146
|
+
|
|
147
|
+
# ============================================================================
|
|
148
|
+
# Phase 1: LOOKUP — Find tool in config dictionary
|
|
149
|
+
# ============================================================================
|
|
150
|
+
|
|
151
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo '')
|
|
152
|
+
log_v "Phase 1 LOOKUP: tool_name=$TOOL_NAME"
|
|
153
|
+
if [ -z "$TOOL_NAME" ]; then exit 0; fi
|
|
154
|
+
|
|
155
|
+
if [ ! -f "$SPEC_FILE" ]; then log_v "spec file not found, skipping"; exit 0; fi
|
|
156
|
+
|
|
157
|
+
# Resolve: aliases reverse lookup → exact key → case-insensitive fallback
|
|
158
|
+
# Each spec entry has an "aliases" array listing tool names from all agent
|
|
159
|
+
# frameworks (cosh, openclaw, hermes). We reverse-lookup from the input
|
|
160
|
+
# tool_name to find the matching spec key.
|
|
161
|
+
SPEC_KEY=$(jq -r --arg name "$TOOL_NAME" '
|
|
162
|
+
to_entries[] | select(.key != "_meta") |
|
|
163
|
+
.key as $spec_key |
|
|
164
|
+
(.value.aliases // [])[] |
|
|
165
|
+
select(. == $name) |
|
|
166
|
+
$spec_key
|
|
167
|
+
' "$SPEC_FILE" 2>/dev/null | head -1)
|
|
168
|
+
|
|
169
|
+
# Fallback: exact spec key match
|
|
170
|
+
if [ -z "$SPEC_KEY" ]; then
|
|
171
|
+
SPEC_KEY=$(jq -r --arg name "$TOOL_NAME" '
|
|
172
|
+
to_entries[] | select(.key != "_meta") |
|
|
173
|
+
select(.key == $name) | .key
|
|
174
|
+
' "$SPEC_FILE" 2>/dev/null | head -1)
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Fallback: case-insensitive spec key match
|
|
178
|
+
if [ -z "$SPEC_KEY" ]; then
|
|
179
|
+
SPEC_KEY=$(jq -r --arg name "$TOOL_NAME" '
|
|
180
|
+
to_entries[] | select(.key != "_meta") |
|
|
181
|
+
select(.key | ascii_downcase == ($name | ascii_downcase)) | .key
|
|
182
|
+
' "$SPEC_FILE" 2>/dev/null | head -1)
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [ -z "$SPEC_KEY" ]; then
|
|
186
|
+
log_v "Phase 1: $TOOL_NAME not in spec dict → skip"
|
|
187
|
+
exit 0
|
|
188
|
+
fi
|
|
189
|
+
log_v "Phase 1: $TOOL_NAME → $SPEC_KEY found in spec dict"
|
|
190
|
+
|
|
191
|
+
# ============================================================================
|
|
192
|
+
# Phase 2: CHECK — Scan system readiness
|
|
193
|
+
# ============================================================================
|
|
194
|
+
|
|
195
|
+
# --- Normalize deps to object format ---
|
|
196
|
+
# Supports both string ("jq") and object ({binary:"jq",...}) formats.
|
|
197
|
+
# String defaults: manager="auto" (fix script auto-detects yum/dnf/apt/apk).
|
|
198
|
+
# Handles version constraints: "rtk>=0.35" → {binary:"rtk", version:">=0.35", ...}
|
|
199
|
+
|
|
200
|
+
normalize_deps() {
|
|
201
|
+
local array="$1"
|
|
202
|
+
echo "$array" | jq -c '[.[] | if type == "string" then
|
|
203
|
+
(if (test(">=") or test("[^<]<[^=]") or test("=")) then
|
|
204
|
+
{binary: (capture("^(?<b>[^>=<]+)") | .b), version: (match("[>=<]+[0-9.]+").string), package: (capture("^(?<b>[^>=<]+)") | .b), manager: "auto"}
|
|
205
|
+
else
|
|
206
|
+
{binary: ., package: ., manager: "auto"}
|
|
207
|
+
end)
|
|
208
|
+
else . end]' 2>/dev/null || echo '[]'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
REQUIRED=$(normalize_deps "$(jq -c --arg key "$SPEC_KEY" '.[$key].required // []' "$SPEC_FILE")")
|
|
212
|
+
RECOMMENDED=$(normalize_deps "$(jq -c --arg key "$SPEC_KEY" '.[$key].recommended // []' "$SPEC_FILE")")
|
|
213
|
+
PERMISSIONS=$(jq -r --arg key "$SPEC_KEY" '.[$key].permissions[] // empty' "$SPEC_FILE" 2>/dev/null || echo '')
|
|
214
|
+
|
|
215
|
+
SHELL_COMMAND=$(echo "$INPUT" | jq -r '
|
|
216
|
+
.tool_input.command //
|
|
217
|
+
.tool_input.cmd //
|
|
218
|
+
.input.command //
|
|
219
|
+
.parameters.command //
|
|
220
|
+
.arguments.command //
|
|
221
|
+
empty
|
|
222
|
+
' 2>/dev/null || echo '')
|
|
223
|
+
|
|
224
|
+
command_uses_binary() {
|
|
225
|
+
local command_text="$1"
|
|
226
|
+
local binary="$2"
|
|
227
|
+
[ -n "$command_text" ] || return 1
|
|
228
|
+
echo "$command_text" | grep -Eq "(^|[[:space:];|&()<>])(/[^[:space:];|&()<>]*/)?${binary}([[:space:];|&()<>]|$)"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if [ "$SPEC_KEY" = "Shell" ] && [ -n "$SHELL_COMMAND" ]; then
|
|
232
|
+
FILTERED_RECOMMENDED="[]"
|
|
233
|
+
rec_filter_count=$(echo "$RECOMMENDED" | jq 'length')
|
|
234
|
+
if [ "$rec_filter_count" -gt 0 ]; then
|
|
235
|
+
for i in $(seq 0 $((rec_filter_count - 1))); do
|
|
236
|
+
dep_json=$(echo "$RECOMMENDED" | jq -c ".[$i]")
|
|
237
|
+
binary=$(echo "$dep_json" | jq -r '.binary')
|
|
238
|
+
if command_uses_binary "$SHELL_COMMAND" "$binary"; then
|
|
239
|
+
FILTERED_RECOMMENDED=$(echo "$FILTERED_RECOMMENDED" | jq -c ". + [$dep_json]")
|
|
240
|
+
fi
|
|
241
|
+
done
|
|
242
|
+
fi
|
|
243
|
+
RECOMMENDED="$FILTERED_RECOMMENDED"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# --- Version comparison helper ---
|
|
247
|
+
# Handles prefixed versions (v22.1.0), build suffixes (1.2.3-rc1), and
|
|
248
|
+
# arbitrary segment counts (1.2, 1.2.3, 1.2.3.4, etc.).
|
|
249
|
+
# Missing segments are treated as 0.
|
|
250
|
+
version_ge() {
|
|
251
|
+
local installed="$1" required="$2"
|
|
252
|
+
# Strip common prefixes (v, V)
|
|
253
|
+
installed="${installed#v}"; installed="${installed#V}"
|
|
254
|
+
required="${required#v}"; required="${required#V}"
|
|
255
|
+
|
|
256
|
+
# Split both versions into segments, stripping build suffixes per segment
|
|
257
|
+
local i_segments r_segments
|
|
258
|
+
IFS='.' read -r -a i_segments <<< "$installed"
|
|
259
|
+
IFS='.' read -r -a r_segments <<< "$required"
|
|
260
|
+
|
|
261
|
+
# Find the longer segment count for comparison
|
|
262
|
+
local max_len=${#i_segments[@]}
|
|
263
|
+
[ "${#r_segments[@]}" -gt "$max_len" ] && max_len=${#r_segments[@]}
|
|
264
|
+
|
|
265
|
+
local i=0 iv rv
|
|
266
|
+
while [ "$i" -lt "$max_len" ]; do
|
|
267
|
+
# Extract digits from current segment (strip -rc1, +build, etc.)
|
|
268
|
+
iv=$(echo "${i_segments[$i]:-0}" | grep -oE '^[0-9]+' | head -1 || echo 0)
|
|
269
|
+
rv=$(echo "${r_segments[$i]:-0}" | grep -oE '^[0-9]+' | head -1 || echo 0)
|
|
270
|
+
iv=${iv:-0}; rv=${rv:-0}
|
|
271
|
+
[ "$iv" -gt "$rv" ] && return 0
|
|
272
|
+
[ "$iv" -lt "$rv" ] && return 1
|
|
273
|
+
i=$((i + 1))
|
|
274
|
+
done
|
|
275
|
+
return 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# --- Resolve binary path ---
|
|
279
|
+
# Tries command -v first, then known install paths.
|
|
280
|
+
# KEEP IN SYNC with hook_utils.py::_known_binary_paths,
|
|
281
|
+
# env_check.rs::binary_fallback_paths, OpenClaw's fallback constants, and the
|
|
282
|
+
# Codex standalone scripts. Makefile and the Anolisa component manifest define
|
|
283
|
+
# the supported layouts; the canonical order is user, /usr/local, /usr, legacy.
|
|
284
|
+
resolve_binary() {
|
|
285
|
+
local name="$1"
|
|
286
|
+
case "$name" in
|
|
287
|
+
""|*/*|"."|"..") return 1 ;;
|
|
288
|
+
esac
|
|
289
|
+
local found
|
|
290
|
+
found=$(command -v "$name" 2>/dev/null || true)
|
|
291
|
+
if [ -n "$found" ]; then echo "$found"; return 0; fi
|
|
292
|
+
|
|
293
|
+
local candidates=()
|
|
294
|
+
if [ -n "$USER_HOME" ]; then
|
|
295
|
+
candidates+=("$USER_HOME/.local/bin/$name")
|
|
296
|
+
fi
|
|
297
|
+
case "$name" in
|
|
298
|
+
rtk|toon)
|
|
299
|
+
if [ -n "$USER_HOME" ]; then
|
|
300
|
+
candidates+=(
|
|
301
|
+
"$USER_HOME/.local/lib/anolisa/libexec/tokenless/$name"
|
|
302
|
+
"$USER_HOME/.local/libexec/anolisa/tokenless/$name"
|
|
303
|
+
)
|
|
304
|
+
fi
|
|
305
|
+
;;
|
|
306
|
+
esac
|
|
307
|
+
candidates+=("/usr/local/bin/$name")
|
|
308
|
+
case "$name" in
|
|
309
|
+
rtk|toon)
|
|
310
|
+
candidates+=("/usr/local/libexec/anolisa/tokenless/$name")
|
|
311
|
+
;;
|
|
312
|
+
esac
|
|
313
|
+
candidates+=("/usr/bin/$name")
|
|
314
|
+
case "$name" in
|
|
315
|
+
rtk|toon)
|
|
316
|
+
candidates+=(
|
|
317
|
+
"/usr/libexec/anolisa/tokenless/$name"
|
|
318
|
+
"/usr/lib/anolisa/tokenless/$name"
|
|
319
|
+
)
|
|
320
|
+
if [ -n "$USER_HOME" ]; then
|
|
321
|
+
candidates+=(
|
|
322
|
+
"$USER_HOME/.local/share/anolisa/tokenless/$name"
|
|
323
|
+
"$USER_HOME/.local/lib/anolisa/tokenless/$name"
|
|
324
|
+
)
|
|
325
|
+
fi
|
|
326
|
+
;;
|
|
327
|
+
esac
|
|
328
|
+
|
|
329
|
+
local candidate
|
|
330
|
+
for candidate in "${candidates[@]}"; do
|
|
331
|
+
if [ -f "$candidate" ] && [ -x "$candidate" ]; then
|
|
332
|
+
echo "$candidate"
|
|
333
|
+
return 0
|
|
334
|
+
fi
|
|
335
|
+
done
|
|
336
|
+
return 1
|
|
337
|
+
}
|
|
338
|
+
# Output: "available", "missing", "version_low:<installed>:<required>"
|
|
339
|
+
check_dep() {
|
|
340
|
+
local dep_json="$1"
|
|
341
|
+
local binary version
|
|
342
|
+
binary=$(echo "$dep_json" | jq -r '.binary')
|
|
343
|
+
version=$(echo "$dep_json" | jq -r '.version // empty')
|
|
344
|
+
|
|
345
|
+
local resolved
|
|
346
|
+
if ! resolved=$(resolve_binary "$binary"); then
|
|
347
|
+
echo "missing"
|
|
348
|
+
return
|
|
349
|
+
fi
|
|
350
|
+
binary="$resolved"
|
|
351
|
+
|
|
352
|
+
if [ -z "$version" ]; then
|
|
353
|
+
echo "available"
|
|
354
|
+
return
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
local constraint_ver installed_version
|
|
358
|
+
constraint_ver=$(echo "$version" | sed 's/[>=<]//g')
|
|
359
|
+
installed_version=$("$binary" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "0.0.0")
|
|
360
|
+
[ -z "$installed_version" ] && installed_version="0.0.0"
|
|
361
|
+
|
|
362
|
+
if version_ge "$installed_version" "$constraint_ver"; then
|
|
363
|
+
echo "available"
|
|
364
|
+
else
|
|
365
|
+
echo "version_low:${installed_version}:${constraint_ver}"
|
|
366
|
+
fi
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# --- Check permissions ---
|
|
370
|
+
check_permissions() {
|
|
371
|
+
local perm_missing=""
|
|
372
|
+
for perm in $PERMISSIONS; do
|
|
373
|
+
case "$perm" in
|
|
374
|
+
file_read) [ ! -r / ] && perm_missing="${perm_missing} file_read" ;;
|
|
375
|
+
file_write) touch "${TMPDIR:-/tmp}/.tokenless-ready-test" 2>/dev/null; rc=$?; rm -f "${TMPDIR:-/tmp}/.tokenless-ready-test" 2>/dev/null; [ $rc -ne 0 ] && perm_missing="${perm_missing} file_write" ;;
|
|
376
|
+
exec_shell) ! resolve_binary bash >/dev/null 2>&1 && perm_missing="${perm_missing} exec_shell" ;;
|
|
377
|
+
docker_socket) [ ! -S /var/run/docker.sock ] && [ ! -S /run/docker.sock ] && perm_missing="${perm_missing} docker_socket" ;;
|
|
378
|
+
esac
|
|
379
|
+
done
|
|
380
|
+
echo "$perm_missing"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# --- Scan all deps ---
|
|
384
|
+
MISSING_DEP_JSONS="[]"
|
|
385
|
+
HAS_REQUIRED_MISSING=false
|
|
386
|
+
HAS_VERSION_LOW=false
|
|
387
|
+
PERM_MISSING=$(check_permissions)
|
|
388
|
+
|
|
389
|
+
# Check required deps
|
|
390
|
+
req_count=$(echo "$REQUIRED" | jq 'length')
|
|
391
|
+
if [ "$req_count" -gt 0 ]; then
|
|
392
|
+
for i in $(seq 0 $((req_count - 1))); do
|
|
393
|
+
dep_json=$(echo "$REQUIRED" | jq -c ".[$i]")
|
|
394
|
+
status=$(check_dep "$dep_json")
|
|
395
|
+
case "$status" in
|
|
396
|
+
missing)
|
|
397
|
+
HAS_REQUIRED_MISSING=true
|
|
398
|
+
MISSING_DEP_JSONS=$(echo "$MISSING_DEP_JSONS" | jq -c ". + [$dep_json]")
|
|
399
|
+
;;
|
|
400
|
+
version_low:*)
|
|
401
|
+
HAS_VERSION_LOW=true
|
|
402
|
+
;;
|
|
403
|
+
esac
|
|
404
|
+
done
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
REQUIRED_MISSING_COUNT=$(echo "$MISSING_DEP_JSONS" | jq 'length' 2>/dev/null || echo 0)
|
|
408
|
+
|
|
409
|
+
# Check recommended deps
|
|
410
|
+
rec_count=$(echo "$RECOMMENDED" | jq 'length')
|
|
411
|
+
missing_count_rec=0
|
|
412
|
+
RECOMMENDED_MISSING_LIST=""
|
|
413
|
+
if [ "$rec_count" -gt 0 ]; then
|
|
414
|
+
for i in $(seq 0 $((rec_count - 1))); do
|
|
415
|
+
dep_json=$(echo "$RECOMMENDED" | jq -c ".[$i]")
|
|
416
|
+
status=$(check_dep "$dep_json")
|
|
417
|
+
case "$status" in
|
|
418
|
+
missing)
|
|
419
|
+
binary=$(echo "$dep_json" | jq -r '.binary')
|
|
420
|
+
RECOMMENDED_MISSING_LIST="${RECOMMENDED_MISSING_LIST} ${binary}"
|
|
421
|
+
missing_count_rec=$((missing_count_rec + 1))
|
|
422
|
+
;;
|
|
423
|
+
esac
|
|
424
|
+
done
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
# --- Determine readiness ---
|
|
428
|
+
IS_READY=true
|
|
429
|
+
IS_PARTIAL=false
|
|
430
|
+
$HAS_REQUIRED_MISSING && IS_READY=false
|
|
431
|
+
$HAS_VERSION_LOW && IS_READY=false
|
|
432
|
+
[ -n "$PERM_MISSING" ] && IS_READY=false
|
|
433
|
+
|
|
434
|
+
# Recommended deps missing → PARTIAL (not blocking, but trigger auto-fix)
|
|
435
|
+
if $IS_READY && [ "$missing_count_rec" -gt 0 ]; then
|
|
436
|
+
IS_PARTIAL=true
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
if $IS_READY && ! $IS_PARTIAL; then
|
|
440
|
+
log_v "Phase 2 CHECK: $TOOL_NAME → READY, silent pass"
|
|
441
|
+
exit 0
|
|
442
|
+
fi
|
|
443
|
+
if $IS_PARTIAL; then
|
|
444
|
+
log_v "Phase 2 CHECK: $TOOL_NAME → PARTIAL (recommended missing: ${RECOMMENDED_MISSING_LIST})"
|
|
445
|
+
else
|
|
446
|
+
log_v "Phase 2 CHECK: $TOOL_NAME → NOT_READY (missing=$HAS_REQUIRED_MISSING version_low=$HAS_VERSION_LOW perm=$PERM_MISSING)"
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
# ============================================================================
|
|
450
|
+
# Phase 3: FIX — Auto-install missing dependencies
|
|
451
|
+
# ============================================================================
|
|
452
|
+
|
|
453
|
+
missing_count=$(echo "$MISSING_DEP_JSONS" | jq 'length' 2>/dev/null || echo 0)
|
|
454
|
+
|
|
455
|
+
log_v "Phase 3 FIX: $missing_count missing deps, fix_script=$FIX_SCRIPT"
|
|
456
|
+
|
|
457
|
+
if [ "$missing_count" -gt 0 ] \
|
|
458
|
+
&& [ -n "$FIX_SCRIPT" ] \
|
|
459
|
+
&& [ -r "$FIX_SCRIPT" ] \
|
|
460
|
+
&& is_trusted_file "$FIX_SCRIPT"; then
|
|
461
|
+
echo "$MISSING_DEP_JSONS" | bash "$FIX_SCRIPT" fix-all 2>/dev/null || true
|
|
462
|
+
hash -r 2>/dev/null || true
|
|
463
|
+
|
|
464
|
+
# Re-scan to check if fix succeeded
|
|
465
|
+
STILL_MISSING=""
|
|
466
|
+
HAS_REQUIRED_MISSING=false
|
|
467
|
+
for i in $(seq 0 $((missing_count - 1))); do
|
|
468
|
+
binary=$(echo "$MISSING_DEP_JSONS" | jq -r ".[$i].binary")
|
|
469
|
+
if ! resolve_binary "$binary" >/dev/null 2>&1; then
|
|
470
|
+
STILL_MISSING="${STILL_MISSING} ${binary}"
|
|
471
|
+
if [ "$i" -lt "$REQUIRED_MISSING_COUNT" ]; then
|
|
472
|
+
HAS_REQUIRED_MISSING=true
|
|
473
|
+
fi
|
|
474
|
+
fi
|
|
475
|
+
done
|
|
476
|
+
|
|
477
|
+
# Re-check version_low entries after fix
|
|
478
|
+
HAS_VERSION_LOW=false
|
|
479
|
+
if [ "$req_count" -gt 0 ]; then
|
|
480
|
+
for i in $(seq 0 $((req_count - 1))); do
|
|
481
|
+
dep_json=$(echo "$REQUIRED" | jq -c ".[$i]")
|
|
482
|
+
status=$(check_dep "$dep_json")
|
|
483
|
+
case "$status" in
|
|
484
|
+
version_low:*)
|
|
485
|
+
HAS_VERSION_LOW=true
|
|
486
|
+
;;
|
|
487
|
+
esac
|
|
488
|
+
done
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
# Re-scan recommended deps after fix
|
|
492
|
+
RECOMMENDED_MISSING_LIST=""
|
|
493
|
+
missing_count_rec=0
|
|
494
|
+
if [ "$rec_count" -gt 0 ]; then
|
|
495
|
+
for i in $(seq 0 $((rec_count - 1))); do
|
|
496
|
+
dep_json=$(echo "$RECOMMENDED" | jq -c ".[$i]")
|
|
497
|
+
status=$(check_dep "$dep_json")
|
|
498
|
+
case "$status" in
|
|
499
|
+
missing)
|
|
500
|
+
binary=$(echo "$dep_json" | jq -r '.binary')
|
|
501
|
+
RECOMMENDED_MISSING_LIST="${RECOMMENDED_MISSING_LIST} ${binary}"
|
|
502
|
+
missing_count_rec=$((missing_count_rec + 1))
|
|
503
|
+
;;
|
|
504
|
+
esac
|
|
505
|
+
done
|
|
506
|
+
fi
|
|
507
|
+
|
|
508
|
+
if [ -z "$STILL_MISSING" ] && ! $HAS_VERSION_LOW && [ -z "$PERM_MISSING" ]; then
|
|
509
|
+
exit 0
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
# After fix, re-check readiness
|
|
513
|
+
# If only recommended still missing but required OK → PARTIAL, don't block
|
|
514
|
+
if ! $HAS_REQUIRED_MISSING && ! $HAS_VERSION_LOW && [ -z "$PERM_MISSING" ]; then
|
|
515
|
+
log_v "Phase 3 FIX: recommended deps partially installed, remaining: ${RECOMMENDED_MISSING_LIST}"
|
|
516
|
+
DIAG_MSG="[tokenless:ready] ${TOOL_NAME}: PARTIAL — recommended deps not installed:${RECOMMENDED_MISSING_LIST}. Core tool is functional."
|
|
517
|
+
jq -n --arg context "$DIAG_MSG" --arg msg "$DIAG_MSG" '{
|
|
518
|
+
"systemMessage": $msg,
|
|
519
|
+
"hookSpecificOutput": {
|
|
520
|
+
"hookEventName": "PreToolUse",
|
|
521
|
+
"additionalContext": $context
|
|
522
|
+
}
|
|
523
|
+
}' || exit 0
|
|
524
|
+
exit 0
|
|
525
|
+
fi
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
# ============================================================================
|
|
529
|
+
# Phase 4: FEEDBACK — Tool not available, inform the Agent
|
|
530
|
+
# ============================================================================
|
|
531
|
+
|
|
532
|
+
# PARTIAL (no fix script available): inform Agent but don't block
|
|
533
|
+
if $IS_PARTIAL && ! $HAS_REQUIRED_MISSING && ! $HAS_VERSION_LOW && [ -z "$PERM_MISSING" ]; then
|
|
534
|
+
DIAG_MSG="[tokenless:ready] ${TOOL_NAME}: PARTIAL — recommended deps missing:${RECOMMENDED_MISSING_LIST}. Core tool is functional, extended deps may be unavailable."
|
|
535
|
+
log_v "Phase 4 FEEDBACK: $TOOL_NAME → PARTIAL → injecting additionalContext (non-blocking)"
|
|
536
|
+
jq -n --arg context "$DIAG_MSG" --arg msg "$DIAG_MSG" '{
|
|
537
|
+
"systemMessage": $msg,
|
|
538
|
+
"hookSpecificOutput": {
|
|
539
|
+
"hookEventName": "PreToolUse",
|
|
540
|
+
"additionalContext": $context
|
|
541
|
+
}
|
|
542
|
+
}' || exit 0
|
|
543
|
+
exit 0
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
# NOT_READY: required deps or permissions missing → block with "Skip retry"
|
|
547
|
+
MISSING_LIST=""
|
|
548
|
+
if [ "$missing_count" -gt 0 ]; then
|
|
549
|
+
for i in $(seq 0 $((missing_count - 1))); do
|
|
550
|
+
binary=$(echo "$MISSING_DEP_JSONS" | jq -r ".[$i].binary")
|
|
551
|
+
MISSING_LIST="${MISSING_LIST} ${binary}"
|
|
552
|
+
done
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
DIAG_PARTS=""
|
|
556
|
+
[ -n "$MISSING_LIST" ] && DIAG_PARTS="${DIAG_PARTS} missing:${MISSING_LIST};"
|
|
557
|
+
$HAS_VERSION_LOW && DIAG_PARTS="${DIAG_PARTS} version too low;"
|
|
558
|
+
[ -n "$PERM_MISSING" ] && DIAG_PARTS="${DIAG_PARTS} permission missing:${PERM_MISSING};"
|
|
559
|
+
|
|
560
|
+
DIAG_MSG="[tokenless:ready] ${TOOL_NAME}: NOT_READY (${DIAG_PARTS}) Skip retry."
|
|
561
|
+
|
|
562
|
+
log_v "Phase 4 FEEDBACK: $TOOL_NAME → NOT_READY → blocking with decision:block"
|
|
563
|
+
|
|
564
|
+
jq -n --arg context "$DIAG_MSG" --arg reason "$DIAG_MSG" '{
|
|
565
|
+
"decision": "block",
|
|
566
|
+
"reason": $reason,
|
|
567
|
+
"hookSpecificOutput": {
|
|
568
|
+
"hookEventName": "PreToolUse",
|
|
569
|
+
"additionalContext": $context
|
|
570
|
+
}
|
|
571
|
+
}' || exit 0
|