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.
Files changed (70) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +770 -0
  3. package/adapters/tokenless/claude-code/.claude-plugin/marketplace.json +15 -0
  4. package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +9 -0
  5. package/adapters/tokenless/claude-code/hooks/hooks.json +38 -0
  6. package/adapters/tokenless/claude-code/scripts/detect.sh +193 -0
  7. package/adapters/tokenless/claude-code/scripts/install.sh +92 -0
  8. package/adapters/tokenless/claude-code/scripts/uninstall.sh +50 -0
  9. package/adapters/tokenless/codex/.codex-plugin/plugin.json +20 -0
  10. package/adapters/tokenless/codex/README.md +160 -0
  11. package/adapters/tokenless/codex/hooks/hooks.json +52 -0
  12. package/adapters/tokenless/codex/scripts/_common.sh +25 -0
  13. package/adapters/tokenless/codex/scripts/check-tokenless +94 -0
  14. package/adapters/tokenless/codex/scripts/compress-response +441 -0
  15. package/adapters/tokenless/codex/scripts/detect.sh +61 -0
  16. package/adapters/tokenless/codex/scripts/install.sh +151 -0
  17. package/adapters/tokenless/codex/scripts/rewrite-hook +282 -0
  18. package/adapters/tokenless/codex/scripts/tool-ready +303 -0
  19. package/adapters/tokenless/codex/scripts/uninstall.sh +78 -0
  20. package/adapters/tokenless/common/commands/tokenless-stats.toml +2 -0
  21. package/adapters/tokenless/common/cosh-extension.json +65 -0
  22. package/adapters/tokenless/common/hooks/compress_response_hook.py +487 -0
  23. package/adapters/tokenless/common/hooks/compress_schema_hook.py +144 -0
  24. package/adapters/tokenless/common/hooks/compress_toon_hook.py +160 -0
  25. package/adapters/tokenless/common/hooks/hook_utils.py +584 -0
  26. package/adapters/tokenless/common/hooks/rewrite_hook.py +223 -0
  27. package/adapters/tokenless/common/hooks/run-hook.sh +62 -0
  28. package/adapters/tokenless/common/hooks/tool_categories.json +99 -0
  29. package/adapters/tokenless/common/hooks/tool_ready_hook.sh +571 -0
  30. package/adapters/tokenless/common/tokenless-env-fix.sh +730 -0
  31. package/adapters/tokenless/common/tool-ready-spec.json +113 -0
  32. package/adapters/tokenless/dsh/cordis.patch.yml +8 -0
  33. package/adapters/tokenless/dsh/dist/index.js +399 -0
  34. package/adapters/tokenless/dsh/package.json +26 -0
  35. package/adapters/tokenless/hermes/__init__.py +572 -0
  36. package/adapters/tokenless/hermes/plugin.yaml +9 -0
  37. package/adapters/tokenless/hermes/scripts/detect.sh +80 -0
  38. package/adapters/tokenless/hermes/scripts/install.sh +60 -0
  39. package/adapters/tokenless/hermes/scripts/uninstall.sh +45 -0
  40. package/adapters/tokenless/manifest.json +147 -0
  41. package/adapters/tokenless/openclaw/dist/index.d.ts +27 -0
  42. package/adapters/tokenless/openclaw/dist/index.js +598 -0
  43. package/adapters/tokenless/openclaw/dist/tool_categories.json +99 -0
  44. package/adapters/tokenless/openclaw/index.ts +720 -0
  45. package/adapters/tokenless/openclaw/openclaw.plugin.json +40 -0
  46. package/adapters/tokenless/openclaw/package.json +24 -0
  47. package/adapters/tokenless/openclaw/scripts/detect.sh +102 -0
  48. package/adapters/tokenless/openclaw/scripts/install.sh +75 -0
  49. package/adapters/tokenless/openclaw/scripts/uninstall.sh +46 -0
  50. package/adapters/tokenless/openclaw/tsconfig.json +13 -0
  51. package/adapters/tokenless/opencode/plugin.js +246 -0
  52. package/adapters/tokenless/opencode/scripts/detect.sh +38 -0
  53. package/adapters/tokenless/opencode/scripts/install.sh +56 -0
  54. package/adapters/tokenless/opencode/scripts/uninstall.sh +29 -0
  55. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +11 -0
  56. package/adapters/tokenless/qoder/commands/tokenless-stats.md +8 -0
  57. package/adapters/tokenless/qoder/hooks/hooks.json +36 -0
  58. package/adapters/tokenless/qoder/hooks/run-hook.sh +51 -0
  59. package/adapters/tokenless/qoder/scripts/detect.sh +35 -0
  60. package/adapters/tokenless/qoder/scripts/install.sh +136 -0
  61. package/adapters/tokenless/qoder/scripts/uninstall.sh +106 -0
  62. package/adapters/tokenless/qwencode/qwen-extension.json +69 -0
  63. package/adapters/tokenless/qwencode/scripts/detect.sh +125 -0
  64. package/adapters/tokenless/qwencode/scripts/install.sh +128 -0
  65. package/adapters/tokenless/qwencode/scripts/uninstall.sh +62 -0
  66. package/bin/rtk +6 -0
  67. package/bin/tokenless +6 -0
  68. package/bin/toon +6 -0
  69. package/package.json +57 -0
  70. 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