anolisa-tokenless 0.7.13 → 0.8.0
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 +219 -87
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
- package/adapters/tokenless/claude-code/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +5 -4
- package/adapters/tokenless/codex/README.md +22 -32
- package/adapters/tokenless/codex/hooks/hooks.json +3 -3
- package/adapters/tokenless/codex/scripts/response-diagnostics +170 -0
- package/adapters/tokenless/common/cosh-extension.json +4 -4
- package/adapters/tokenless/common/hooks/compress_response_hook.py +250 -307
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +34 -42
- package/adapters/tokenless/common/hooks/hook_utils.py +281 -44
- package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
- package/adapters/tokenless/dsh/dist/index.js +353 -264
- package/adapters/tokenless/dsh/package.json +2 -2
- package/adapters/tokenless/hermes/__init__.py +191 -355
- package/adapters/tokenless/hermes/plugin.yaml +2 -2
- package/adapters/tokenless/manifest.json +18 -3
- package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
- package/adapters/tokenless/openclaw/dist/index.js +291 -507
- package/adapters/tokenless/openclaw/index.ts +408 -628
- package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
- package/adapters/tokenless/openclaw/package.json +6 -4
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
- package/adapters/tokenless/qwenpaw/plugin.json +17 -0
- package/adapters/tokenless/qwenpaw/plugin.py +390 -0
- package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
- package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
- package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
- package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
- package/package.json +5 -5
- package/adapters/tokenless/codex/scripts/compress-response +0 -445
- package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -171
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Tokenless schema compression hook.
|
|
3
3
|
|
|
4
|
-
Reads a BeforeModel JSON from stdin, extracts the tools array,
|
|
5
|
-
|
|
4
|
+
Reads a BeforeModel JSON from stdin, extracts the tools array, forwards it
|
|
5
|
+
to the unified ``tokenless compress`` Protocol v2 BeforeModel operation and
|
|
6
6
|
writes a HookOutput JSON to stdout.
|
|
7
|
+
The entry point returns the original array on no-savings, which this hook
|
|
8
|
+
wraps exactly like a compressed one — the historical behavior of the
|
|
9
|
+
``compress-schema`` flow it replaces.
|
|
7
10
|
|
|
8
11
|
Hook point: **BeforeModel**
|
|
9
12
|
|
|
@@ -16,7 +19,6 @@ from __future__ import annotations
|
|
|
16
19
|
import contextlib
|
|
17
20
|
import json
|
|
18
21
|
import os
|
|
19
|
-
import subprocess
|
|
20
22
|
import sys
|
|
21
23
|
|
|
22
24
|
try: # POSIX hosts (cosh / Cosh-NG) — the platforms these hooks target.
|
|
@@ -30,9 +32,11 @@ from hook_utils import (
|
|
|
30
32
|
_TOKENLESS_FALLBACK,
|
|
31
33
|
_TOKENLESS_LOCAL_LIB,
|
|
32
34
|
_TOKENLESS_LOCAL_SHARE,
|
|
35
|
+
build_before_model_request,
|
|
33
36
|
resolve_agent_id,
|
|
34
37
|
resolve_binary,
|
|
35
38
|
resolve_tool_call_id,
|
|
39
|
+
run_compress,
|
|
36
40
|
secure_write_text,
|
|
37
41
|
skip,
|
|
38
42
|
warn,
|
|
@@ -42,6 +46,11 @@ from hook_utils import (
|
|
|
42
46
|
|
|
43
47
|
_AGENT_ID = resolve_agent_id()
|
|
44
48
|
|
|
49
|
+
# Below the extension manifests' 10 s host wrapper, so a pathological batch
|
|
50
|
+
# is killed here (fail-open skip) instead of racing the host's kill of the
|
|
51
|
+
# whole hook (the old subprocess timeout was 10 s against a 10 s wrapper).
|
|
52
|
+
_COMPRESS_TIMEOUT = 8
|
|
53
|
+
|
|
45
54
|
# One marker file holds the session keys that already emitted the "no tool
|
|
46
55
|
# declarations" warning — one key per line, most recent last — so the warning
|
|
47
56
|
# repeats at most once per session even though BeforeModel fires on every
|
|
@@ -103,14 +112,6 @@ def _marker_lock():
|
|
|
103
112
|
# -- helpers -----------------------------------------------------------------
|
|
104
113
|
|
|
105
114
|
|
|
106
|
-
def _is_json_array(data: str) -> bool:
|
|
107
|
-
try:
|
|
108
|
-
obj = json.loads(data)
|
|
109
|
-
return isinstance(obj, list)
|
|
110
|
-
except (json.JSONDecodeError, ValueError):
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
|
|
114
115
|
def _session_warn_key(session_id: str) -> str:
|
|
115
116
|
"""Normalize the session ID into the dedup key for the no-tools warning.
|
|
116
117
|
|
|
@@ -266,42 +267,33 @@ def main() -> None:
|
|
|
266
267
|
)
|
|
267
268
|
skip()
|
|
268
269
|
|
|
269
|
-
tools_json = json.dumps(tools, separators=(",", ":"))
|
|
270
|
-
|
|
271
270
|
# 4. Extract caller context
|
|
272
271
|
session_id = input_data.get("session_id", "")
|
|
273
272
|
tool_use_id = resolve_tool_call_id(_AGENT_ID, input_data)
|
|
274
273
|
|
|
275
|
-
# 5. Compress schemas via
|
|
276
|
-
|
|
277
|
-
if
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
274
|
+
# 5. Compress schemas via the unified entry point (one subprocess).
|
|
275
|
+
llm_request = input_data.get("llm_request", {})
|
|
276
|
+
visible_context = dict(llm_request) if isinstance(llm_request, dict) else {}
|
|
277
|
+
visible_context.pop("tools", None)
|
|
278
|
+
config = visible_context.get("config")
|
|
279
|
+
if isinstance(config, dict):
|
|
280
|
+
visible_context["config"] = dict(config)
|
|
281
|
+
visible_context["config"].pop("tools", None)
|
|
282
|
+
request = build_before_model_request(
|
|
283
|
+
tools,
|
|
284
|
+
visible_context,
|
|
285
|
+
_AGENT_ID,
|
|
286
|
+
session_id=session_id,
|
|
287
|
+
tool_use_id=tool_use_id,
|
|
288
|
+
)
|
|
289
|
+
response = run_compress(
|
|
290
|
+
tokenless_bin, request, _COMPRESS_TIMEOUT, "before_model"
|
|
291
|
+
)
|
|
292
|
+
if response is None:
|
|
291
293
|
warn("Schema compression subprocess failed. Passing through unchanged.")
|
|
292
294
|
skip()
|
|
293
|
-
|
|
294
|
-
if
|
|
295
|
-
detail = (proc.stderr or "").strip()[:200]
|
|
296
|
-
warn(
|
|
297
|
-
f"Schema compression failed with exit code {proc.returncode}: {detail}"
|
|
298
|
-
if detail
|
|
299
|
-
else f"Schema compression failed with exit code {proc.returncode}. Passing through unchanged."
|
|
300
|
-
)
|
|
301
|
-
skip()
|
|
302
|
-
|
|
303
|
-
compressed = proc.stdout.strip()
|
|
304
|
-
if not compressed or not _is_json_array(compressed):
|
|
295
|
+
compressed = response.get("tools")
|
|
296
|
+
if not isinstance(compressed, list):
|
|
305
297
|
warn(
|
|
306
298
|
"Schema compression returned invalid JSON. Passing through unchanged."
|
|
307
299
|
)
|
|
@@ -313,7 +305,7 @@ def main() -> None:
|
|
|
313
305
|
"hookEventName": "BeforeModel",
|
|
314
306
|
"llm_request": {
|
|
315
307
|
"config": {
|
|
316
|
-
"tools":
|
|
308
|
+
"tools": compressed,
|
|
317
309
|
},
|
|
318
310
|
},
|
|
319
311
|
},
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
9
|
import shutil
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
12
|
+
import time
|
|
11
13
|
|
|
12
14
|
# -- Binary fallback paths ----------------------------------------------------
|
|
13
15
|
#
|
|
@@ -35,16 +37,10 @@ def _user_path(*parts: str) -> str:
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
_TOKENLESS_FALLBACK = "/usr/bin/tokenless"
|
|
38
|
-
_TOKENLESS_LOCAL_SHARE = _user_path(
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
_TOKENLESS_LOCAL_LIB = _user_path(
|
|
42
|
-
".local", "lib", "anolisa", "tokenless", "tokenless"
|
|
43
|
-
)
|
|
40
|
+
_TOKENLESS_LOCAL_SHARE = _user_path(".local", "share", "anolisa", "tokenless", "tokenless")
|
|
41
|
+
_TOKENLESS_LOCAL_LIB = _user_path(".local", "lib", "anolisa", "tokenless", "tokenless")
|
|
44
42
|
_RTK_FALLBACK = "/usr/libexec/anolisa/tokenless/rtk"
|
|
45
|
-
_RTK_LOCAL_SHARE = _user_path(
|
|
46
|
-
".local", "share", "anolisa", "tokenless", "rtk"
|
|
47
|
-
)
|
|
43
|
+
_RTK_LOCAL_SHARE = _user_path(".local", "share", "anolisa", "tokenless", "rtk")
|
|
48
44
|
_RTK_LOCAL_LIB = _user_path(".local", "lib", "anolisa", "tokenless", "rtk")
|
|
49
45
|
|
|
50
46
|
_TOKENLESS_HELPER_BINARIES = frozenset({"rtk"})
|
|
@@ -87,9 +83,7 @@ def _known_binary_paths(name: str, home: str | None = None) -> tuple[str, ...]:
|
|
|
87
83
|
paths.append(os.path.join("/usr/local/bin", name))
|
|
88
84
|
if name in _TOKENLESS_HELPER_BINARIES:
|
|
89
85
|
# Anolisa CLI system mode.
|
|
90
|
-
paths.append(
|
|
91
|
-
os.path.join("/usr/local/libexec/anolisa/tokenless", name)
|
|
92
|
-
)
|
|
86
|
+
paths.append(os.path.join("/usr/local/libexec/anolisa/tokenless", name))
|
|
93
87
|
paths.append(os.path.join("/usr/bin", name))
|
|
94
88
|
if name in _TOKENLESS_HELPER_BINARIES:
|
|
95
89
|
paths.extend(
|
|
@@ -103,16 +97,13 @@ def _known_binary_paths(name: str, home: str | None = None) -> tuple[str, ...]:
|
|
|
103
97
|
if user_home:
|
|
104
98
|
paths.extend(
|
|
105
99
|
[
|
|
106
|
-
os.path.join(
|
|
107
|
-
|
|
108
|
-
),
|
|
109
|
-
os.path.join(
|
|
110
|
-
user_home, ".local", "lib", "anolisa", "tokenless", name
|
|
111
|
-
),
|
|
100
|
+
os.path.join(user_home, ".local", "share", "anolisa", "tokenless", name),
|
|
101
|
+
os.path.join(user_home, ".local", "lib", "anolisa", "tokenless", name),
|
|
112
102
|
]
|
|
113
103
|
)
|
|
114
104
|
return tuple(paths)
|
|
115
105
|
|
|
106
|
+
|
|
116
107
|
# -- Unified tool categorization ----------------------------------------------
|
|
117
108
|
|
|
118
109
|
# Tool categories are loaded from tool_categories.json, which serves as the
|
|
@@ -126,16 +117,38 @@ _TOOL_CATEGORIES_PATH = os.path.join(os.path.dirname(__file__), "tool_categories
|
|
|
126
117
|
# invalid. Matches the minimum safe classification from before the JSON was
|
|
127
118
|
# introduced, ensuring content-retrieval tools are never accidentally compressed.
|
|
128
119
|
_FALLBACK_SKIP_TOOLS = [
|
|
129
|
-
"Read",
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
120
|
+
"Read",
|
|
121
|
+
"read",
|
|
122
|
+
"read_file",
|
|
123
|
+
"read_many_files",
|
|
124
|
+
"Glob",
|
|
125
|
+
"glob",
|
|
126
|
+
"search_file",
|
|
127
|
+
"list_directory",
|
|
128
|
+
"list_dir",
|
|
129
|
+
"Grep",
|
|
130
|
+
"grep",
|
|
131
|
+
"grep_code",
|
|
132
|
+
"grep_search",
|
|
133
|
+
"search_files",
|
|
134
|
+
"Lsp",
|
|
135
|
+
"lsp",
|
|
136
|
+
"NotebookRead",
|
|
137
|
+
"notebook_read",
|
|
138
|
+
"notebookread",
|
|
134
139
|
]
|
|
135
140
|
_FALLBACK_SHELL_TOOLS = [
|
|
136
|
-
"Bash",
|
|
137
|
-
"
|
|
138
|
-
"
|
|
141
|
+
"Bash",
|
|
142
|
+
"bash",
|
|
143
|
+
"Shell",
|
|
144
|
+
"shell",
|
|
145
|
+
"exec",
|
|
146
|
+
"terminal",
|
|
147
|
+
"run_shell_command",
|
|
148
|
+
"run_in_terminal",
|
|
149
|
+
"get_terminal_output",
|
|
150
|
+
"execute_command",
|
|
151
|
+
"process",
|
|
139
152
|
]
|
|
140
153
|
|
|
141
154
|
|
|
@@ -187,6 +200,29 @@ SKIP_TOOLS: set[str] = set(_tool_categories.get("layer_1_skip", {}).get("tools",
|
|
|
187
200
|
# These tools produce text output that can be safely truncated if too long.
|
|
188
201
|
SHELL_TOOLS: set[str] = set(_tool_categories.get("layer_2_shell", {}).get("tools", []))
|
|
189
202
|
|
|
203
|
+
_TOKENLESS_RETRIEVE_COMMAND_RE = re.compile(
|
|
204
|
+
r"^[ \t]*(?:\"tokenless\"|'tokenless'|tokenless)[ \t]+retrieve[ \t]+"
|
|
205
|
+
r"(?:\"(?:[0-9a-f]{24}|<<tokenless:[0-9a-f]{24}>>)\"|"
|
|
206
|
+
r"'(?:[0-9a-f]{24}|<<tokenless:[0-9a-f]{24}>>)'|[0-9a-f]{24})[ \t]*$",
|
|
207
|
+
re.IGNORECASE,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def tokenless_retrieve_command_available() -> bool:
|
|
212
|
+
"""Return whether a Marker command can invoke bare ``tokenless``."""
|
|
213
|
+
return shutil.which("tokenless") is not None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_tokenless_retrieve_command(tool_name: str, arguments: object) -> bool:
|
|
217
|
+
"""Recognize the exact local recovery command emitted by Tokenless markers."""
|
|
218
|
+
if tool_name not in SHELL_TOOLS or not isinstance(arguments, dict):
|
|
219
|
+
return False
|
|
220
|
+
command = arguments.get("command")
|
|
221
|
+
if not isinstance(command, str):
|
|
222
|
+
return False
|
|
223
|
+
return _TOKENLESS_RETRIEVE_COMMAND_RE.fullmatch(command) is not None
|
|
224
|
+
|
|
225
|
+
|
|
190
226
|
# Layer 3: API tools (zero-truncation).
|
|
191
227
|
# These tools return structured data or API responses that should not be truncated.
|
|
192
228
|
# No explicit set needed; tools not in SKIP_TOOLS or SHELL_TOOLS are Layer 3.
|
|
@@ -196,7 +232,7 @@ SHELL_TOOLS: set[str] = set(_tool_categories.get("layer_2_shell", {}).get("tools
|
|
|
196
232
|
# is missing or the file failed to load.
|
|
197
233
|
|
|
198
234
|
# Layer 2 thresholds: moderate truncation for shell/exec output.
|
|
199
|
-
# Restores old
|
|
235
|
+
# Restores the old JSON compression defaults for shell commands (git log, ls,
|
|
200
236
|
# cat, etc.) where truncation is acceptable.
|
|
201
237
|
# 64K strings: 95% of real shell output (git diff ~63K, git log ~34K) preserved.
|
|
202
238
|
# 128 arrays: 95% of result sets (test results, audit reports) preserved.
|
|
@@ -211,11 +247,6 @@ _TRUNCATE_STRINGS_AT = _layer3_thr.get("truncate_strings_at", 1_048_576)
|
|
|
211
247
|
_TRUNCATE_ARRAYS_AT = _layer3_thr.get("truncate_arrays_at", 65_536)
|
|
212
248
|
_MAX_DEPTH = _layer3_thr.get("max_depth", 32)
|
|
213
249
|
|
|
214
|
-
# Backward-compatible alias — direct reference (not a copy) so consumers see
|
|
215
|
-
# the same set as SKIP_TOOLS. Used by compress_toon_hook.py for the standalone
|
|
216
|
-
# TOON-only path where "content retrieval" is the more descriptive name.
|
|
217
|
-
CONTENT_RETRIEVAL_TOOLS = SKIP_TOOLS
|
|
218
|
-
|
|
219
250
|
|
|
220
251
|
def get_thresholds(tool_name: str) -> tuple[int, int, int]:
|
|
221
252
|
"""Return (truncate_strings_at, truncate_arrays_at, max_depth) for a tool.
|
|
@@ -231,9 +262,9 @@ def get_thresholds(tool_name: str) -> tuple[int, int, int]:
|
|
|
231
262
|
|
|
232
263
|
# -- Shared environment error patterns ----------------------------------------
|
|
233
264
|
#
|
|
234
|
-
# Superset of patterns from both the
|
|
265
|
+
# Superset of patterns from both the Codex diagnostics and shared hook adapters. Uses regex
|
|
235
266
|
# matching (case-insensitive) so patterns like "/bin/sh:.*: not found" work
|
|
236
|
-
# correctly. Both codex/scripts/
|
|
267
|
+
# correctly. Both codex/scripts/response-diagnostics and compress_response_hook
|
|
237
268
|
# import this list and the classify_env_error() function below.
|
|
238
269
|
|
|
239
270
|
ENV_PATTERNS: list[tuple[list[str], str, str]] = [
|
|
@@ -315,7 +346,7 @@ def classify_env_error(tool_response) -> tuple[str | None, str | None]:
|
|
|
315
346
|
Accepts either a parsed dict (with stderr/error/exit_code fields) or a
|
|
316
347
|
plain string. Returns (category_tag, fix_hint) or (None, None).
|
|
317
348
|
|
|
318
|
-
Shared by codex/scripts/
|
|
349
|
+
Shared by codex/scripts/response-diagnostics and compress_response_hook.
|
|
319
350
|
"""
|
|
320
351
|
if isinstance(tool_response, dict):
|
|
321
352
|
text = str(tool_response.get("stderr", "")) + str(tool_response.get("error", ""))
|
|
@@ -346,6 +377,9 @@ def classify_env_error(tool_response) -> tuple[str | None, str | None]:
|
|
|
346
377
|
|
|
347
378
|
_CONTEXT_DIR = os.path.join(os.path.expanduser("~"), ".tokenless")
|
|
348
379
|
_CONTEXT_FILE = os.path.join(_CONTEXT_DIR, ".rewrite-context")
|
|
380
|
+
_OPTIMIZATION_STATE_DIR = os.path.join(_CONTEXT_DIR, "hook-state")
|
|
381
|
+
_OPTIMIZATION_STATE_TTL_SECONDS = 24 * 60 * 60
|
|
382
|
+
_OPTIMIZATION_STATE_MAX_FILES = 1024
|
|
349
383
|
|
|
350
384
|
# -- Binary resolution (cached) -----------------------------------------------
|
|
351
385
|
|
|
@@ -412,9 +446,7 @@ def unwrap_string_json(raw: str) -> str | None:
|
|
|
412
446
|
# characters (code points), not \uXXXX escape sequences, so
|
|
413
447
|
# string-wrapped payloads are measured the same way as the
|
|
414
448
|
# dict/list branch and the OpenClaw adapter.
|
|
415
|
-
return json.dumps(
|
|
416
|
-
inner_obj, separators=(",", ":"), ensure_ascii=False
|
|
417
|
-
)
|
|
449
|
+
return json.dumps(inner_obj, separators=(",", ":"), ensure_ascii=False)
|
|
418
450
|
return None
|
|
419
451
|
return raw
|
|
420
452
|
|
|
@@ -474,6 +506,77 @@ def write_context(agent_id: str, session_id: str, tool_use_id: str) -> None:
|
|
|
474
506
|
secure_write_text(_CONTEXT_FILE, f"{agent_id}\n{session_id}\n{tool_use_id}\n")
|
|
475
507
|
|
|
476
508
|
|
|
509
|
+
def _optimization_state_path(agent_id: str, session_id: str, tool_use_id: str) -> str:
|
|
510
|
+
identity = "\0".join((agent_id, session_id, tool_use_id)).encode()
|
|
511
|
+
digest = hashlib.sha256(identity).hexdigest()
|
|
512
|
+
return os.path.join(_OPTIMIZATION_STATE_DIR, digest)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _prune_optimization_states() -> None:
|
|
516
|
+
"""Bound abandoned per-call state when a host omits PostToolUse."""
|
|
517
|
+
try:
|
|
518
|
+
entries = os.scandir(_OPTIMIZATION_STATE_DIR)
|
|
519
|
+
except FileNotFoundError:
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
cutoff = time.time() - _OPTIMIZATION_STATE_TTL_SECONDS
|
|
523
|
+
live = []
|
|
524
|
+
with entries:
|
|
525
|
+
for entry in entries:
|
|
526
|
+
try:
|
|
527
|
+
if not entry.is_file(follow_symlinks=False):
|
|
528
|
+
continue
|
|
529
|
+
modified = entry.stat(follow_symlinks=False).st_mtime
|
|
530
|
+
if ".consuming." in entry.name and modified > cutoff:
|
|
531
|
+
continue
|
|
532
|
+
if modified <= cutoff:
|
|
533
|
+
os.unlink(entry.path)
|
|
534
|
+
else:
|
|
535
|
+
live.append((modified, entry.path))
|
|
536
|
+
except FileNotFoundError:
|
|
537
|
+
continue
|
|
538
|
+
|
|
539
|
+
excess = len(live) - _OPTIMIZATION_STATE_MAX_FILES + 1
|
|
540
|
+
if excess > 0:
|
|
541
|
+
for _, path in sorted(live)[:excess]:
|
|
542
|
+
try:
|
|
543
|
+
os.unlink(path)
|
|
544
|
+
except FileNotFoundError:
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def mark_rtk_optimized(agent_id: str, session_id: str, tool_use_id: str) -> None:
|
|
549
|
+
"""Persist RTK ownership for one tool call before applying its rewrite."""
|
|
550
|
+
_prune_optimization_states()
|
|
551
|
+
secure_write_text(_optimization_state_path(agent_id, session_id, tool_use_id), "rtk\n")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def consume_output_optimization(agent_id: str, session_id: str, tool_use_id: str) -> str:
|
|
555
|
+
"""Consume one tool call's optimization state for its final result."""
|
|
556
|
+
if not tool_use_id:
|
|
557
|
+
return "none"
|
|
558
|
+
path = _optimization_state_path(agent_id, session_id, tool_use_id)
|
|
559
|
+
consuming_path = f"{path}.consuming.{os.getpid()}"
|
|
560
|
+
try:
|
|
561
|
+
os.rename(path, consuming_path)
|
|
562
|
+
except FileNotFoundError:
|
|
563
|
+
return "none"
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
flags = os.O_RDONLY
|
|
567
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
568
|
+
flags |= os.O_NOFOLLOW
|
|
569
|
+
fd = os.open(consuming_path, flags)
|
|
570
|
+
with os.fdopen(fd) as state_file:
|
|
571
|
+
state = state_file.read()
|
|
572
|
+
finally:
|
|
573
|
+
try:
|
|
574
|
+
os.unlink(consuming_path)
|
|
575
|
+
except FileNotFoundError:
|
|
576
|
+
pass
|
|
577
|
+
return "rtk" if state == "rtk\n" else "none"
|
|
578
|
+
|
|
579
|
+
|
|
477
580
|
def forward_stderr(proc: subprocess.CompletedProcess) -> None:
|
|
478
581
|
"""Forward subprocess stderr on failure (non-zero exit) via warn()."""
|
|
479
582
|
if proc.returncode != 0 and proc.stderr:
|
|
@@ -496,6 +599,144 @@ def run(args: list[str], input_data: str, timeout: int = 3) -> subprocess.Comple
|
|
|
496
599
|
return None
|
|
497
600
|
|
|
498
601
|
|
|
602
|
+
def _attribution(
|
|
603
|
+
agent_id: str,
|
|
604
|
+
session_id: str = "",
|
|
605
|
+
tool_use_id: str = "",
|
|
606
|
+
) -> dict:
|
|
607
|
+
"""Build the shared Protocol v2 attribution object."""
|
|
608
|
+
value = {"agent_id": agent_id}
|
|
609
|
+
if session_id:
|
|
610
|
+
value["session_id"] = session_id
|
|
611
|
+
if tool_use_id:
|
|
612
|
+
value["tool_use_id"] = tool_use_id
|
|
613
|
+
return value
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def build_before_model_request(
|
|
617
|
+
tools: list,
|
|
618
|
+
visible_context: object,
|
|
619
|
+
agent_id: str,
|
|
620
|
+
session_id: str = "",
|
|
621
|
+
tool_use_id: str = "",
|
|
622
|
+
) -> dict:
|
|
623
|
+
"""Build a Protocol v2 BeforeModel transport request."""
|
|
624
|
+
return {
|
|
625
|
+
"protocol_version": 2,
|
|
626
|
+
"operation": "before_model",
|
|
627
|
+
"attribution": _attribution(agent_id, session_id, tool_use_id),
|
|
628
|
+
"input": {
|
|
629
|
+
"tools": tools,
|
|
630
|
+
"visible_context": visible_context,
|
|
631
|
+
"capabilities": {
|
|
632
|
+
"replace_tools": True,
|
|
633
|
+
# A local CLI is not marker-scoped Agent authorization.
|
|
634
|
+
"recovery": {"kind": "none"},
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def build_pre_tool_request(
|
|
641
|
+
arguments: dict,
|
|
642
|
+
agent_id: str,
|
|
643
|
+
tool_name: str,
|
|
644
|
+
command_field: str,
|
|
645
|
+
session_id: str = "",
|
|
646
|
+
tool_use_id: str = "",
|
|
647
|
+
replace_arguments: bool = True,
|
|
648
|
+
block_and_suggest: bool = False,
|
|
649
|
+
) -> dict:
|
|
650
|
+
"""Build a Protocol v2 PreTool transport request."""
|
|
651
|
+
return {
|
|
652
|
+
"protocol_version": 2,
|
|
653
|
+
"operation": "pre_tool",
|
|
654
|
+
"attribution": _attribution(agent_id, session_id, tool_use_id),
|
|
655
|
+
"input": {
|
|
656
|
+
"tool_name": tool_name,
|
|
657
|
+
"arguments": arguments,
|
|
658
|
+
"command_field": command_field,
|
|
659
|
+
"capabilities": {
|
|
660
|
+
"replace_arguments": replace_arguments,
|
|
661
|
+
"block_and_suggest": block_and_suggest,
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def build_post_tool_request(
|
|
668
|
+
content: str,
|
|
669
|
+
agent_id: str,
|
|
670
|
+
tool_name: str,
|
|
671
|
+
status: str,
|
|
672
|
+
content_origin: str,
|
|
673
|
+
output_optimization: str,
|
|
674
|
+
*,
|
|
675
|
+
result_kind: str,
|
|
676
|
+
recovery: dict[str, str],
|
|
677
|
+
session_id: str = "",
|
|
678
|
+
tool_use_id: str = "",
|
|
679
|
+
replace_output: bool = False,
|
|
680
|
+
replace_with_text: bool = False,
|
|
681
|
+
) -> dict:
|
|
682
|
+
"""Build a Protocol v2 PostTool transport request."""
|
|
683
|
+
return {
|
|
684
|
+
"protocol_version": 2,
|
|
685
|
+
"operation": "post_tool",
|
|
686
|
+
"attribution": _attribution(agent_id, session_id, tool_use_id),
|
|
687
|
+
"input": {
|
|
688
|
+
"result_kind": result_kind,
|
|
689
|
+
"tool_name": tool_name,
|
|
690
|
+
"content": content,
|
|
691
|
+
"status": status,
|
|
692
|
+
"content_origin": content_origin,
|
|
693
|
+
"output_optimization": output_optimization,
|
|
694
|
+
"capabilities": {
|
|
695
|
+
"replace_output": replace_output,
|
|
696
|
+
"recovery": recovery,
|
|
697
|
+
"replace_with_text": replace_with_text,
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def run_compress(
|
|
704
|
+
tokenless_bin: str, request: dict, timeout: int, expected_operation: str
|
|
705
|
+
) -> dict | None:
|
|
706
|
+
"""Run ``tokenless compress`` on one request; None on any failure.
|
|
707
|
+
|
|
708
|
+
The single Tokenless subprocess of a hook invocation (roadmap §5.6).
|
|
709
|
+
Fail-open: a dead binary, non-zero exit, or malformed stdout all
|
|
710
|
+
return None so the caller passes the original through.
|
|
711
|
+
"""
|
|
712
|
+
proc = run(
|
|
713
|
+
[tokenless_bin, "compress"],
|
|
714
|
+
json.dumps(request, ensure_ascii=False),
|
|
715
|
+
timeout=timeout,
|
|
716
|
+
)
|
|
717
|
+
if proc is None or proc.returncode != 0 or not proc.stdout.strip():
|
|
718
|
+
if proc is not None and proc.returncode != 0:
|
|
719
|
+
warn(f"tokenless compress exited {proc.returncode}")
|
|
720
|
+
return None
|
|
721
|
+
response = try_parse_json(proc.stdout.strip())
|
|
722
|
+
if not isinstance(response, dict):
|
|
723
|
+
warn("tokenless compress returned malformed output")
|
|
724
|
+
return None
|
|
725
|
+
if response.get("protocol_version") != 2:
|
|
726
|
+
# Version-skewed binary: never trust a response whose contract
|
|
727
|
+
# this adapter does not speak.
|
|
728
|
+
warn("tokenless compress returned an unsupported protocol version")
|
|
729
|
+
return None
|
|
730
|
+
if response.get("operation") != expected_operation:
|
|
731
|
+
warn("tokenless compress returned a mismatched operation")
|
|
732
|
+
return None
|
|
733
|
+
result = response.get("result")
|
|
734
|
+
if not isinstance(result, dict):
|
|
735
|
+
warn("tokenless compress returned a malformed operation result")
|
|
736
|
+
return None
|
|
737
|
+
return result
|
|
738
|
+
|
|
739
|
+
|
|
499
740
|
def detect_cosh_ng_runtime() -> tuple | None:
|
|
500
741
|
"""Detect if we are running under Cosh-NG and return its version.
|
|
501
742
|
|
|
@@ -547,7 +788,7 @@ def _agent_id_from_argv(argv: list[str] | None) -> str | None:
|
|
|
547
788
|
following = args[index + 1] if index + 1 < len(args) else ""
|
|
548
789
|
return following or None
|
|
549
790
|
if arg.startswith("--agent-id="):
|
|
550
|
-
return arg[len("--agent-id="):] or None
|
|
791
|
+
return arg[len("--agent-id=") :] or None
|
|
551
792
|
return None
|
|
552
793
|
|
|
553
794
|
|
|
@@ -575,11 +816,7 @@ def resolve_agent_id(default: str = "unknown", argv: list[str] | None = None) ->
|
|
|
575
816
|
"""
|
|
576
817
|
if detect_cosh_ng_runtime() is not None:
|
|
577
818
|
return "cosh-ng"
|
|
578
|
-
return (
|
|
579
|
-
_agent_id_from_argv(argv)
|
|
580
|
-
or os.environ.get("TOKENLESS_AGENT_ID")
|
|
581
|
-
or default
|
|
582
|
-
)
|
|
819
|
+
return _agent_id_from_argv(argv) or os.environ.get("TOKENLESS_AGENT_ID") or default
|
|
583
820
|
|
|
584
821
|
|
|
585
822
|
def parse_version(version_str: str) -> tuple | None:
|