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,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install the Tokenless plugin for Codex.
|
|
3
|
+
#
|
|
4
|
+
# Two-phase installation:
|
|
5
|
+
# Phase 1 — Binary: verify tokenless CLI is reachable (RPM or dev build).
|
|
6
|
+
# Phase 2 — Plugin: create a local codex marketplace, register it, and
|
|
7
|
+
# install the tokenless plugin via `codex plugin add`.
|
|
8
|
+
#
|
|
9
|
+
# Codex marketplace layout:
|
|
10
|
+
#
|
|
11
|
+
# codex-marketplace/ ← marketplace root
|
|
12
|
+
# ├── .agents/
|
|
13
|
+
# │ └── plugins/
|
|
14
|
+
# │ └── marketplace.json ← manifest
|
|
15
|
+
# └── tokenless/ ← symlink to the actual plugin dir
|
|
16
|
+
# ├── plugin.json
|
|
17
|
+
# ├── hooks/
|
|
18
|
+
# └── scripts/
|
|
19
|
+
#
|
|
20
|
+
# The plugin path in marketplace.json ("./tokenless") is resolved relative
|
|
21
|
+
# to the marketplace root, not the manifest file.
|
|
22
|
+
#
|
|
23
|
+
# Usage:
|
|
24
|
+
# ./install.sh # Interactive
|
|
25
|
+
# ./install.sh --non-interactive # CI / automated
|
|
26
|
+
#
|
|
27
|
+
# Environment variables:
|
|
28
|
+
# TOKENLESS_INSTALL_PREFIX Installation prefix (default: ~/.local)
|
|
29
|
+
# TOKENLESS_SOURCE_DIR Path to anolisa/src/tokenless (dev-only override)
|
|
30
|
+
# TOKENLESS_SKIP_BUILD Set to "1" to skip cargo build
|
|
31
|
+
# CODEX_BIN Path to codex CLI (default: codex)
|
|
32
|
+
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
PREFIX="${TOKENLESS_INSTALL_PREFIX:-$HOME/.local}"
|
|
36
|
+
BINDIR="$PREFIX/bin"
|
|
37
|
+
CODEX_BIN="${CODEX_BIN:-}"
|
|
38
|
+
MARKETPLACE_NAME="anolisa-tokenless"
|
|
39
|
+
|
|
40
|
+
# Resolve codex binary via shared helper (see _common.sh).
|
|
41
|
+
|
|
42
|
+
# Resolve the codex plugin source directory.
|
|
43
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
44
|
+
# shellcheck source=./_common.sh
|
|
45
|
+
source "$SCRIPT_DIR/_common.sh"
|
|
46
|
+
PLUGIN_SRC="$(cd "$SCRIPT_DIR/.." && pwd)" # codex/ directory
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Phase 1 — Ensure tokenless binary is available
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
TOKENLESS_BIN=""
|
|
53
|
+
if command -v tokenless >/dev/null 2>&1; then
|
|
54
|
+
TOKENLESS_BIN="$(command -v tokenless)"
|
|
55
|
+
elif [[ -x "$BINDIR/tokenless" ]]; then
|
|
56
|
+
TOKENLESS_BIN="$BINDIR/tokenless"
|
|
57
|
+
elif [[ -x /usr/local/bin/tokenless ]]; then
|
|
58
|
+
TOKENLESS_BIN="/usr/local/bin/tokenless"
|
|
59
|
+
elif [[ -x /usr/bin/tokenless ]]; then
|
|
60
|
+
TOKENLESS_BIN="/usr/bin/tokenless"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [[ -z "$TOKENLESS_BIN" ]]; then
|
|
64
|
+
# No binary — try building from source (dev scenario)
|
|
65
|
+
if [[ -n "${TOKENLESS_SOURCE_DIR:-}" ]]; then
|
|
66
|
+
SRCDIR="$TOKENLESS_SOURCE_DIR"
|
|
67
|
+
else
|
|
68
|
+
SRCDIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [[ ! -f "$SRCDIR/Cargo.toml" ]]; then
|
|
72
|
+
echo "[tokenless] ERROR: tokenless binary not found and no source tree at $SRCDIR" >&2
|
|
73
|
+
echo "[tokenless] Install the tokenless RPM package first:" >&2
|
|
74
|
+
echo "[tokenless] rpm -Uvh tokenless-*.rpm" >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "[tokenless] Building tokenless from source: $SRCDIR"
|
|
79
|
+
cd "$SRCDIR"
|
|
80
|
+
cargo build --release -p tokenless-cli 2>&1 | tail -3
|
|
81
|
+
mkdir -p "$BINDIR"
|
|
82
|
+
cp "$SRCDIR/target/release/tokenless" "$BINDIR/tokenless"
|
|
83
|
+
chmod 755 "$BINDIR/tokenless"
|
|
84
|
+
echo "[tokenless] Built and installed to $BINDIR/tokenless"
|
|
85
|
+
else
|
|
86
|
+
echo "[tokenless] Binary: $TOKENLESS_BIN ($("$TOKENLESS_BIN" --version))"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Phase 2 — Register plugin with codex
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
# 2a. Resolve codex and check availability
|
|
94
|
+
CODEX_BIN="$(resolve_codex)"
|
|
95
|
+
if [[ -z "$CODEX_BIN" ]]; then
|
|
96
|
+
echo "[tokenless] codex CLI not found — skipping plugin registration."
|
|
97
|
+
echo "[tokenless] Install codex first, then run: codex plugin add tokenless@${MARKETPLACE_NAME}"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# 2a. Create marketplace directory layout in a user-local location so
|
|
102
|
+
# non-root users can run this script even when the plugin source tree
|
|
103
|
+
# is installed under a root-owned prefix (e.g. /usr/share/anolisa).
|
|
104
|
+
MARKETPLACE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/anolisa/codex-marketplace"
|
|
105
|
+
AGENTS_DIR="$MARKETPLACE_DIR/.agents/plugins"
|
|
106
|
+
mkdir -p "$AGENTS_DIR"
|
|
107
|
+
|
|
108
|
+
# Symlink plugin at marketplace root (codex resolves plugin paths relative to
|
|
109
|
+
# the marketplace root, not the manifest file).
|
|
110
|
+
PLUGIN_LINK="$MARKETPLACE_DIR/tokenless"
|
|
111
|
+
if [[ ! -L "$PLUGIN_LINK" ]] || [[ "$(readlink "$PLUGIN_LINK")" != "$PLUGIN_SRC" ]]; then
|
|
112
|
+
rm -f "$PLUGIN_LINK"
|
|
113
|
+
ln -sfn "$PLUGIN_SRC" "$PLUGIN_LINK"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# 2b. Write marketplace.json at .agents/plugins/marketplace.json
|
|
117
|
+
cat > "$AGENTS_DIR/marketplace.json" <<JSON
|
|
118
|
+
{
|
|
119
|
+
"name": "${MARKETPLACE_NAME}",
|
|
120
|
+
"interface": {
|
|
121
|
+
"displayName": "ANOLISA Tokenless"
|
|
122
|
+
},
|
|
123
|
+
"plugins": [
|
|
124
|
+
{
|
|
125
|
+
"name": "tokenless",
|
|
126
|
+
"source": {
|
|
127
|
+
"source": "local",
|
|
128
|
+
"path": "./tokenless"
|
|
129
|
+
},
|
|
130
|
+
"policy": {
|
|
131
|
+
"installation": "AVAILABLE"
|
|
132
|
+
},
|
|
133
|
+
"category": "developer-tools"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
JSON
|
|
138
|
+
|
|
139
|
+
# 2c. Register marketplace (idempotent — remove first if exists)
|
|
140
|
+
if "$CODEX_BIN" plugin marketplace list 2>/dev/null | grep -q "^${MARKETPLACE_NAME}[[:space:]]"; then
|
|
141
|
+
"$CODEX_BIN" plugin marketplace remove "$MARKETPLACE_NAME" 2>/dev/null || true
|
|
142
|
+
fi
|
|
143
|
+
echo "[tokenless] Registering marketplace '${MARKETPLACE_NAME}'..."
|
|
144
|
+
"$CODEX_BIN" plugin marketplace add "$MARKETPLACE_DIR" 2>&1
|
|
145
|
+
|
|
146
|
+
# 2d. Install the plugin from marketplace
|
|
147
|
+
echo "[tokenless] Installing plugin 'tokenless@${MARKETPLACE_NAME}'..."
|
|
148
|
+
"$CODEX_BIN" plugin add "tokenless@${MARKETPLACE_NAME}" 2>&1
|
|
149
|
+
|
|
150
|
+
echo "[tokenless] Codex plugin installation complete."
|
|
151
|
+
echo "[tokenless] Verify: codex plugin list"
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Codex PreToolUse RTK command rewriting hook.
|
|
3
|
+
|
|
4
|
+
Reads a PreToolUse hook request from stdin, extracts the shell command,
|
|
5
|
+
invokes ``rtk rewrite`` for token optimization, and returns the rewritten
|
|
6
|
+
command via ``updatedInput``.
|
|
7
|
+
|
|
8
|
+
Protocol:
|
|
9
|
+
stdin ← codex PreToolUse JSON (tool_name, tool_input.command, ...)
|
|
10
|
+
stdout → {"hookSpecificOutput": {"hookEventName": "PreToolUse",
|
|
11
|
+
"permissionDecision": "allow",
|
|
12
|
+
"updatedInput": {...}}}
|
|
13
|
+
|
|
14
|
+
rtk exit code protocol:
|
|
15
|
+
0 = rewrite available → apply
|
|
16
|
+
1 = no RTK equivalent → skip (passthrough)
|
|
17
|
+
2 = deny rule matched → skip
|
|
18
|
+
3 = Ask/Default → apply (non-interactive context)
|
|
19
|
+
|
|
20
|
+
Fail-open: all error paths exit 0 with empty stdout (passthrough).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
# Resolve shared hook utilities (common/hooks/) with FHS fallback paths.
|
|
31
|
+
# Primary: relative path (works for source-tree and FHS/RPM install).
|
|
32
|
+
# Fallbacks: system and user FHS paths (works when Codex caches only the
|
|
33
|
+
# plugin subdirectory and the relative path resolves outside the cache).
|
|
34
|
+
#
|
|
35
|
+
# Trust model (aligned with bash is_trusted_file / Rust is_trusted_path):
|
|
36
|
+
# - System FHS paths (/usr/share, /usr/local/share) are unconditional.
|
|
37
|
+
# - User-relative paths use passwd DB home (NOT $HOME — env-controllable),
|
|
38
|
+
# with ownership and parent-dir permission checks.
|
|
39
|
+
# - Relative ../../common/hooks is trusted only if the resolved target
|
|
40
|
+
# exists and is under a system prefix or a passwd-validated home path.
|
|
41
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_trusted_dir(path: str) -> bool:
|
|
45
|
+
"""Check whether a directory is trusted for Python module imports.
|
|
46
|
+
|
|
47
|
+
Mirrors the trust criteria in bash tool_ready_hook.sh (is_trusted_file)
|
|
48
|
+
and Rust env_check.rs (is_trusted_path): system paths unconditional,
|
|
49
|
+
user paths validated via passwd + ownership + parent permission checks.
|
|
50
|
+
|
|
51
|
+
NOTE: Callers should pass realpath-resolved paths (not just abspath)
|
|
52
|
+
so that symlink targets are validated before the trust check — a
|
|
53
|
+
symlinked /usr/share/anolisa → /tmp/ would bypass the prefix check
|
|
54
|
+
if only abspath is used.
|
|
55
|
+
"""
|
|
56
|
+
# System FHS prefixes are always trusted
|
|
57
|
+
for prefix in ("/usr/share/", "/usr/local/share/", "/usr/libexec/", "/usr/lib/anolisa/"):
|
|
58
|
+
if path.startswith(prefix):
|
|
59
|
+
return True
|
|
60
|
+
# For paths outside system prefixes (e.g. source-tree cloned to
|
|
61
|
+
# /opt/anolisa/), trust them if owned by current uid or root AND
|
|
62
|
+
# parent directory is not world-writable. This covers developer
|
|
63
|
+
# worktrees without requiring the path to be under passwd home.
|
|
64
|
+
try:
|
|
65
|
+
st = os.stat(path)
|
|
66
|
+
except OSError:
|
|
67
|
+
return False
|
|
68
|
+
if st.st_uid != os.getuid() and st.st_uid != 0:
|
|
69
|
+
# Not owned by us or root — refuse. Home paths must be owned by
|
|
70
|
+
# us or root; source-tree paths outside system prefixes are
|
|
71
|
+
# already covered by the ownership check above (current uid/root).
|
|
72
|
+
return False
|
|
73
|
+
# Ownership OK — check parent directory is not world-writable
|
|
74
|
+
parent = os.path.dirname(path)
|
|
75
|
+
try:
|
|
76
|
+
pst = os.stat(parent)
|
|
77
|
+
except OSError:
|
|
78
|
+
return False
|
|
79
|
+
if pst.st_uid != os.getuid() and pst.st_uid != 0:
|
|
80
|
+
return False
|
|
81
|
+
if pst.st_mode & 0o002: # world-writable
|
|
82
|
+
return False
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Resolve real home from passwd DB for user-install fallback path.
|
|
87
|
+
# Falls back to "" (empty) when unavailable — candidate is then skipped.
|
|
88
|
+
try:
|
|
89
|
+
import pwd as _pwd
|
|
90
|
+
_REAL_HOME = _pwd.getpwuid(os.getuid()).pw_dir
|
|
91
|
+
except (ImportError, KeyError):
|
|
92
|
+
_REAL_HOME = ""
|
|
93
|
+
if not os.path.isabs(_REAL_HOME):
|
|
94
|
+
_REAL_HOME = ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _user_path(*parts: str) -> str:
|
|
98
|
+
return os.path.join(_REAL_HOME, *parts) if _REAL_HOME else ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_HOOK_UTILS_CANDIDATES = [
|
|
102
|
+
os.path.join(_HERE, "..", "..", "common", "hooks"), # source-tree / FHS
|
|
103
|
+
"/usr/share/anolisa/adapters/tokenless/common/hooks", # RPM system
|
|
104
|
+
"/usr/local/share/anolisa/adapters/tokenless/common/hooks", # manual system
|
|
105
|
+
os.path.join(_REAL_HOME, ".local", "share",
|
|
106
|
+
"anolisa", "adapters", "tokenless", "common", "hooks") if _REAL_HOME else "",
|
|
107
|
+
]
|
|
108
|
+
_HOOK_UTILS_RESOLVED = ""
|
|
109
|
+
for _C in _HOOK_UTILS_CANDIDATES:
|
|
110
|
+
if _C and _is_trusted_dir(os.path.realpath(_C)) and os.path.isdir(_C):
|
|
111
|
+
_HOOK_UTILS_RESOLVED = os.path.realpath(_C)
|
|
112
|
+
sys.path.insert(0, _C)
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if _HOOK_UTILS_RESOLVED:
|
|
116
|
+
from hook_utils import parse_version as _parse_version, skip_silent as _skip, write_context as _write_context # noqa: E402
|
|
117
|
+
else:
|
|
118
|
+
# Fail-open: no trusted hook_utils found → provide inline fallbacks
|
|
119
|
+
# so the hook script exits 0 (passthrough) rather than ImportError → exit 1.
|
|
120
|
+
import re as _re
|
|
121
|
+
|
|
122
|
+
def _skip() -> None: # type: ignore[assignment,misc]
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
def _parse_version(version_str: str) -> tuple | None: # type: ignore[assignment,misc]
|
|
126
|
+
m = _re.search(r"(\d+)\.(\d+)\.(\d+)", version_str)
|
|
127
|
+
if m:
|
|
128
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
_WRITE_CONTEXT_WARNED = False
|
|
132
|
+
|
|
133
|
+
def _write_context(agent_id: str, session_id: str, tool_use_id: str) -> None: # type: ignore[assignment,misc]
|
|
134
|
+
global _WRITE_CONTEXT_WARNED
|
|
135
|
+
if not _WRITE_CONTEXT_WARNED:
|
|
136
|
+
print("[tokenless] WARNING: rtk session tracking unavailable (hook_utils not loaded, rewrite still works)", file=sys.stderr)
|
|
137
|
+
_WRITE_CONTEXT_WARNED = True
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# constants
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
AGENT_ID = os.environ.get("TOKENLESS_AGENT_ID", "codex")
|
|
144
|
+
_MIN_RTK_VERSION = (0, 35, 0)
|
|
145
|
+
|
|
146
|
+
# RTK binary search paths. Codex can cache this script without common/hooks;
|
|
147
|
+
# keep this standalone list aligned with hook_utils.py::_known_binary_paths.
|
|
148
|
+
_RTK_FALLBACK = "/usr/libexec/anolisa/tokenless/rtk"
|
|
149
|
+
_RTK_FALLBACK_DEBIAN = "/usr/lib/anolisa/tokenless/rtk"
|
|
150
|
+
_RTK_SYSTEM_BIN = "/usr/local/bin/rtk"
|
|
151
|
+
_RTK_SYSTEM_LIBEXEC = "/usr/local/libexec/anolisa/tokenless/rtk"
|
|
152
|
+
_RTK_RPM_BIN = "/usr/bin/rtk"
|
|
153
|
+
_RTK_LOCAL_BIN = _user_path(".local", "bin", "rtk")
|
|
154
|
+
_RTK_ANOLISA_USER = _user_path(
|
|
155
|
+
".local",
|
|
156
|
+
"lib",
|
|
157
|
+
"anolisa",
|
|
158
|
+
"libexec",
|
|
159
|
+
"tokenless",
|
|
160
|
+
"rtk",
|
|
161
|
+
)
|
|
162
|
+
_RTK_MAKE_USER = _user_path(".local", "libexec", "anolisa", "tokenless", "rtk")
|
|
163
|
+
_RTK_LOCAL_SHARE = _user_path(".local", "share", "anolisa", "tokenless", "rtk")
|
|
164
|
+
_RTK_LOCAL_LIB = _user_path(".local", "lib", "anolisa", "tokenless", "rtk")
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# helpers
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _find_rtk() -> Optional[str]:
|
|
172
|
+
"""Locate the rtk binary."""
|
|
173
|
+
path = shutil.which("rtk")
|
|
174
|
+
if path:
|
|
175
|
+
return path
|
|
176
|
+
for fp in (
|
|
177
|
+
_RTK_LOCAL_BIN,
|
|
178
|
+
_RTK_ANOLISA_USER,
|
|
179
|
+
_RTK_MAKE_USER,
|
|
180
|
+
_RTK_SYSTEM_BIN,
|
|
181
|
+
_RTK_SYSTEM_LIBEXEC,
|
|
182
|
+
_RTK_RPM_BIN,
|
|
183
|
+
_RTK_FALLBACK,
|
|
184
|
+
_RTK_FALLBACK_DEBIAN,
|
|
185
|
+
_RTK_LOCAL_SHARE,
|
|
186
|
+
_RTK_LOCAL_LIB,
|
|
187
|
+
):
|
|
188
|
+
if os.path.isfile(fp) and os.access(fp, os.X_OK):
|
|
189
|
+
return fp
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# main
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def main() -> None:
|
|
199
|
+
# 1. Resolve rtk binary
|
|
200
|
+
rtk_bin = _find_rtk()
|
|
201
|
+
if not rtk_bin:
|
|
202
|
+
_skip()
|
|
203
|
+
|
|
204
|
+
# 2. Version guard
|
|
205
|
+
try:
|
|
206
|
+
result = subprocess.run(
|
|
207
|
+
[rtk_bin, "--version"],
|
|
208
|
+
capture_output=True, text=True, timeout=3,
|
|
209
|
+
)
|
|
210
|
+
ver = _parse_version(result.stdout)
|
|
211
|
+
if ver and ver < _MIN_RTK_VERSION:
|
|
212
|
+
_skip()
|
|
213
|
+
except Exception:
|
|
214
|
+
_skip()
|
|
215
|
+
|
|
216
|
+
# 3. Read stdin JSON
|
|
217
|
+
try:
|
|
218
|
+
raw = sys.stdin.read()
|
|
219
|
+
if not raw.strip():
|
|
220
|
+
_skip()
|
|
221
|
+
input_data = json.loads(raw)
|
|
222
|
+
except (json.JSONDecodeError, EOFError, ValueError):
|
|
223
|
+
_skip()
|
|
224
|
+
|
|
225
|
+
# 4. Extract command from tool_input
|
|
226
|
+
tool_name = input_data.get("tool_name", "")
|
|
227
|
+
tool_input = input_data.get("tool_input", {})
|
|
228
|
+
if isinstance(tool_input, str):
|
|
229
|
+
try:
|
|
230
|
+
tool_input = json.loads(tool_input)
|
|
231
|
+
except (json.JSONDecodeError, ValueError):
|
|
232
|
+
_skip()
|
|
233
|
+
cmd = tool_input.get("command", "")
|
|
234
|
+
if not cmd:
|
|
235
|
+
_skip()
|
|
236
|
+
|
|
237
|
+
# 5. Set up RTK env context for stats
|
|
238
|
+
session_id = input_data.get("session_id", "")
|
|
239
|
+
tool_use_id = input_data.get("tool_use_id", "")
|
|
240
|
+
|
|
241
|
+
env = os.environ.copy()
|
|
242
|
+
env["TOKENLESS_AGENT_ID"] = AGENT_ID
|
|
243
|
+
if session_id:
|
|
244
|
+
env["TOKENLESS_SESSION_ID"] = session_id
|
|
245
|
+
if tool_use_id:
|
|
246
|
+
env["TOKENLESS_TOOL_USE_ID"] = tool_use_id
|
|
247
|
+
|
|
248
|
+
_write_context(AGENT_ID, session_id, tool_use_id)
|
|
249
|
+
|
|
250
|
+
# 6. Rewrite via rtk
|
|
251
|
+
try:
|
|
252
|
+
proc = subprocess.run(
|
|
253
|
+
[rtk_bin, "rewrite", cmd],
|
|
254
|
+
capture_output=True, text=True, timeout=5, env=env,
|
|
255
|
+
)
|
|
256
|
+
except Exception:
|
|
257
|
+
_skip()
|
|
258
|
+
|
|
259
|
+
# 7. Interpret rtk exit code
|
|
260
|
+
if proc.returncode in (1, 2):
|
|
261
|
+
_skip()
|
|
262
|
+
|
|
263
|
+
rewritten = proc.stdout.strip()
|
|
264
|
+
if not rewritten or rewritten == cmd:
|
|
265
|
+
_skip()
|
|
266
|
+
|
|
267
|
+
# 8. Build codex PreToolUse response with updatedInput
|
|
268
|
+
updated_input = dict(tool_input)
|
|
269
|
+
updated_input["command"] = rewritten
|
|
270
|
+
|
|
271
|
+
response = {
|
|
272
|
+
"hookSpecificOutput": {
|
|
273
|
+
"hookEventName": "PreToolUse",
|
|
274
|
+
"permissionDecision": "allow",
|
|
275
|
+
"updatedInput": updated_input,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
print(json.dumps(response, ensure_ascii=False))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
main()
|