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,51 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Native Tokenless plugin for DeepSeek Harness (dsh).
|
|
3
3
|
*
|
|
4
|
-
* This entry intentionally has no dsh runtime imports.
|
|
4
|
+
* This entry intentionally has no dsh runtime imports. DSH supplies the
|
|
5
5
|
* Cordis event types at runtime, while the only process boundary is the
|
|
6
|
-
* installed Tokenless CLI.
|
|
6
|
+
* installed Tokenless CLI. Keeping the entry dependency-free lets ANOLISA
|
|
7
7
|
* install one self-contained bundle without running npm in $DSH_HOME.
|
|
8
8
|
*/
|
|
9
9
|
import { execFile } from 'node:child_process'
|
|
10
10
|
import { randomUUID } from 'node:crypto'
|
|
11
|
+
import {
|
|
12
|
+
accessSync,
|
|
13
|
+
appendFileSync,
|
|
14
|
+
constants,
|
|
15
|
+
lstatSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
statSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from 'node:fs'
|
|
21
|
+
import { delimiter, isAbsolute, join, resolve } from 'node:path'
|
|
11
22
|
|
|
12
23
|
const PLUGIN_NAME = 'anolisa-tokenless'
|
|
13
24
|
const DEFAULT_AGENT_ID = 'dsh'
|
|
14
25
|
const DEFAULT_TIMEOUT_MS = 3000
|
|
15
26
|
const DEFAULT_MAX_BUFFER = 2 * 1024 * 1024
|
|
27
|
+
const DSH_TOKENLESS_DATA_DIR = 'DSH_TOKENLESS_DATA_DIR'
|
|
28
|
+
const DSH_TOKENLESS_STATS_DB = 'DSH_TOKENLESS_STATS_DB'
|
|
29
|
+
const DSH_TOKENLESS_STASH_DB = 'DSH_TOKENLESS_STASH_DB'
|
|
30
|
+
const TOKENLESS_RETRIEVE_COMMAND_RE = /^[ \t]*(?:"tokenless"|'tokenless'|tokenless)[ \t]+retrieve[ \t]+(?:"(?:[0-9a-f]{24}|<<tokenless:[0-9a-f]{24}>>)"|'(?:[0-9a-f]{24}|<<tokenless:[0-9a-f]{24}>>)'|[0-9a-f]{24})[ \t]*$/i
|
|
16
31
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
const DEFAULT_SKIP_TOOLS = new Set([
|
|
21
|
-
'Read',
|
|
32
|
+
// DSH does not expose content origin, so map its built-in tool names at the
|
|
33
|
+
// host boundary. Core owns every compression decision after this translation.
|
|
34
|
+
const FILE_TOOLS = new Set([
|
|
22
35
|
'read',
|
|
23
36
|
'read_file',
|
|
24
37
|
'read_many_files',
|
|
25
|
-
'Glob',
|
|
26
38
|
'glob',
|
|
27
39
|
'search_file',
|
|
28
40
|
'list_directory',
|
|
29
41
|
'list_dir',
|
|
30
|
-
'Grep',
|
|
31
42
|
'grep',
|
|
32
43
|
'grep_code',
|
|
33
44
|
'grep_search',
|
|
34
45
|
'search_files',
|
|
35
|
-
'Lsp',
|
|
36
46
|
'lsp',
|
|
37
|
-
'NotebookRead',
|
|
38
|
-
'notebook_read',
|
|
39
47
|
'notebookread',
|
|
48
|
+
'notebook_read',
|
|
40
49
|
])
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
// dsh profile can override them in its plugin config; the Tokenless CLI still
|
|
44
|
-
// owns the actual compression algorithm and stats recording.
|
|
45
|
-
const DEFAULT_SHELL_TOOLS = new Set([
|
|
46
|
-
'Bash',
|
|
51
|
+
const COMMAND_TOOLS = new Set([
|
|
47
52
|
'bash',
|
|
48
|
-
'
|
|
53
|
+
'pwsh',
|
|
49
54
|
'shell',
|
|
50
55
|
'exec',
|
|
51
56
|
'terminal',
|
|
@@ -56,84 +61,7 @@ const DEFAULT_SHELL_TOOLS = new Set([
|
|
|
56
61
|
'process',
|
|
57
62
|
])
|
|
58
63
|
|
|
59
|
-
const
|
|
60
|
-
shell: { strings: 65536, arrays: 128, depth: 8 },
|
|
61
|
-
api: { strings: 1048576, arrays: 65536, depth: 32 },
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Mirror common/hooks/hook_utils.py ENV_PATTERNS exactly. The dsh bundle is
|
|
65
|
-
// independently publishable, so changes to the canonical table must update
|
|
66
|
-
// this dependency-free runtime copy and its tests together.
|
|
67
|
-
const ENV_PATTERNS = [
|
|
68
|
-
[
|
|
69
|
-
[
|
|
70
|
-
/command not found/i,
|
|
71
|
-
/not installed/i,
|
|
72
|
-
/which:\s+no/i,
|
|
73
|
-
/no command\s/i,
|
|
74
|
-
/cannot execute/i,
|
|
75
|
-
/is not recognized/i,
|
|
76
|
-
/could not find/i,
|
|
77
|
-
/unable to locate/i,
|
|
78
|
-
/package not found/i,
|
|
79
|
-
/\/bin\/sh:.*: not found/i,
|
|
80
|
-
/command not found:/i,
|
|
81
|
-
],
|
|
82
|
-
'ENV_DEPENDENCY_MISSING',
|
|
83
|
-
'Missing dependency detected. Install it or ask the user for guidance.',
|
|
84
|
-
],
|
|
85
|
-
[
|
|
86
|
-
[
|
|
87
|
-
/permission denied/i,
|
|
88
|
-
/operation not permitted/i,
|
|
89
|
-
/eacces/i,
|
|
90
|
-
/access denied/i,
|
|
91
|
-
/cannot open .* for writing/i,
|
|
92
|
-
],
|
|
93
|
-
'ENV_PERMISSION',
|
|
94
|
-
'Permission denied. Check file/directory permissions or run with appropriate access.',
|
|
95
|
-
],
|
|
96
|
-
[
|
|
97
|
-
[
|
|
98
|
-
/no such file or directory/i,
|
|
99
|
-
/enoent/i,
|
|
100
|
-
/cannot find/i,
|
|
101
|
-
/file not found/i,
|
|
102
|
-
/does not exist/i,
|
|
103
|
-
],
|
|
104
|
-
'ENV_FILE_MISSING',
|
|
105
|
-
'Required file or directory not found. Verify the path or create it.',
|
|
106
|
-
],
|
|
107
|
-
[
|
|
108
|
-
[
|
|
109
|
-
/connection refused/i,
|
|
110
|
-
/could not resolve host/i,
|
|
111
|
-
/network is unreachable/i,
|
|
112
|
-
/curl: \(7\)/i,
|
|
113
|
-
/curl: \(6\)/i,
|
|
114
|
-
/failed to connect/i,
|
|
115
|
-
/name or service not known/i,
|
|
116
|
-
/couldn't resolve host/i,
|
|
117
|
-
/temporary failure in name resolution/i,
|
|
118
|
-
/econnrefused/i,
|
|
119
|
-
/etimedout/i,
|
|
120
|
-
/connection timed out/i,
|
|
121
|
-
],
|
|
122
|
-
'ENV_NETWORK',
|
|
123
|
-
'Network connectivity issue. Check DNS, proxy, and firewall settings.',
|
|
124
|
-
],
|
|
125
|
-
[
|
|
126
|
-
[
|
|
127
|
-
/modulenotfounderror/i,
|
|
128
|
-
/importerror/i,
|
|
129
|
-
/no module named/i,
|
|
130
|
-
/cannot import name/i,
|
|
131
|
-
/npm err! 404/i,
|
|
132
|
-
],
|
|
133
|
-
'ENV_PACKAGE_MISSING',
|
|
134
|
-
'Required package or module is missing. Install the needed dependency.',
|
|
135
|
-
],
|
|
136
|
-
]
|
|
64
|
+
const INTERRUPTED_CODES = new Set(['ABORTED', 'ABORTED_BEFORE_DISPATCH'])
|
|
137
65
|
|
|
138
66
|
/** Return a plain config value or the supplied fallback. */
|
|
139
67
|
function valueOr(config, key, fallback) {
|
|
@@ -142,80 +70,187 @@ function valueOr(config, key, fallback) {
|
|
|
142
70
|
: fallback
|
|
143
71
|
}
|
|
144
72
|
|
|
145
|
-
/**
|
|
146
|
-
function toolSet(value, fallback) {
|
|
147
|
-
if (!Array.isArray(value)) return fallback
|
|
148
|
-
return new Set(value.filter((name) => typeof name === 'string' && name.length > 0))
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Resolve the executable without ever installing or mutating a dsh profile. */
|
|
73
|
+
/** Resolve the executable without installing or mutating a DSH profile. */
|
|
152
74
|
function tokenlessBinary(config) {
|
|
153
75
|
const configured = valueOr(config, 'tokenlessBin', undefined)
|
|
154
76
|
if (typeof configured === 'string' && configured.length > 0) return configured
|
|
155
77
|
return process.env.TOKENLESS_BIN || 'tokenless'
|
|
156
78
|
}
|
|
157
79
|
|
|
158
|
-
/**
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
|
|
80
|
+
/** Keep Core and DSH's sandboxed shell on the same writable state directory. */
|
|
81
|
+
function tokenlessDataDir(exec) {
|
|
82
|
+
const configured = process.env.TOKENLESS_DATA_DIR
|
|
83
|
+
if (typeof configured === 'string' && configured.length > 0) return configured
|
|
84
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
85
|
+
const workspace = typeof sessionCwd === 'string' && isAbsolute(sessionCwd)
|
|
86
|
+
? sessionCwd
|
|
87
|
+
: process.cwd()
|
|
88
|
+
return join(workspace, '.tokenless')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Prevent default workspace state from becoming a source-control candidate. */
|
|
92
|
+
function prepareTokenlessDataDir(exec) {
|
|
93
|
+
const dataDir = tokenlessDataDir(exec)
|
|
94
|
+
if (typeof process.env.TOKENLESS_DATA_DIR === 'string'
|
|
95
|
+
&& process.env.TOKENLESS_DATA_DIR.length > 0) return dataDir
|
|
96
|
+
|
|
97
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 })
|
|
98
|
+
if (!lstatSync(dataDir).isDirectory()) {
|
|
99
|
+
throw new Error(`Tokenless state path is not a directory: ${dataDir}`)
|
|
100
|
+
}
|
|
101
|
+
const ignorePath = join(dataDir, '.gitignore')
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(ignorePath, '*\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 })
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error?.code !== 'EEXIST') throw error
|
|
106
|
+
const metadata = lstatSync(ignorePath)
|
|
107
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
108
|
+
throw new Error(`Tokenless state ignore file is unsafe: ${ignorePath}`)
|
|
109
|
+
}
|
|
110
|
+
const contents = readFileSync(ignorePath, 'utf8')
|
|
111
|
+
if (!contents.split(/\r?\n/).includes('*')) {
|
|
112
|
+
appendFileSync(
|
|
113
|
+
ignorePath,
|
|
114
|
+
`${contents.endsWith('\n') || contents.length === 0 ? '' : '\n'}*\n`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return dataDir
|
|
167
119
|
}
|
|
168
120
|
|
|
169
|
-
/**
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
121
|
+
/** Publish the state overrides that DSH strips from model shell commands. */
|
|
122
|
+
function tokenlessShellEnvironment(exec) {
|
|
123
|
+
const environment = {
|
|
124
|
+
[DSH_TOKENLESS_DATA_DIR]: tokenlessDataDir(exec),
|
|
125
|
+
}
|
|
126
|
+
for (const [source, target] of [
|
|
127
|
+
['TOKENLESS_STATS_DB', DSH_TOKENLESS_STATS_DB],
|
|
128
|
+
['TOKENLESS_STASH_DB', DSH_TOKENLESS_STASH_DB],
|
|
129
|
+
]) {
|
|
130
|
+
const value = process.env[source]
|
|
131
|
+
if (typeof value === 'string' && value.length > 0) environment[target] = value
|
|
132
|
+
}
|
|
133
|
+
return environment
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Resolve one bare executable using the path inherited by DSH shell commands. */
|
|
137
|
+
function executableOnPath(name) {
|
|
138
|
+
if (typeof process.env.PATH !== 'string') return undefined
|
|
139
|
+
for (const directory of process.env.PATH.split(delimiter)) {
|
|
140
|
+
// A future Marker call may use a different shell workdir, so a
|
|
141
|
+
// cwd-relative entry cannot establish a stable executable identity.
|
|
142
|
+
if (!isAbsolute(directory)) return undefined
|
|
143
|
+
const candidate = join(directory, name)
|
|
144
|
+
try {
|
|
145
|
+
accessSync(candidate, constants.X_OK)
|
|
146
|
+
if (statSync(candidate).isFile()) return candidate
|
|
147
|
+
} catch {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
175
150
|
}
|
|
176
151
|
return undefined
|
|
177
152
|
}
|
|
178
153
|
|
|
179
|
-
/**
|
|
180
|
-
function
|
|
181
|
-
|
|
154
|
+
/** Require compression and Marker recovery to invoke the same CLI file. */
|
|
155
|
+
function tokenlessRetrieveCommandAvailable(config) {
|
|
156
|
+
const markerBinary = executableOnPath('tokenless')
|
|
157
|
+
if (markerBinary === undefined) return false
|
|
158
|
+
const selected = tokenlessBinary(config)
|
|
159
|
+
const selectedBinary = isAbsolute(selected)
|
|
160
|
+
? selected
|
|
161
|
+
: (selected.includes('/') ? resolve(selected) : executableOnPath(selected))
|
|
162
|
+
if (selectedBinary === undefined) return false
|
|
163
|
+
try {
|
|
164
|
+
accessSync(selectedBinary, constants.X_OK)
|
|
165
|
+
const markerStat = statSync(markerBinary)
|
|
166
|
+
const selectedStat = statSync(selectedBinary)
|
|
167
|
+
return selectedStat.isFile()
|
|
168
|
+
&& markerStat.dev === selectedStat.dev
|
|
169
|
+
&& markerStat.ino === selectedStat.ino
|
|
170
|
+
} catch {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Convert one process limit to a positive finite integer. */
|
|
176
|
+
function positiveInteger(value, fallback) {
|
|
177
|
+
return Number.isInteger(value) && value > 0 ? value : fallback
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Return the only DSH content shape that can be replaced without schema loss. */
|
|
181
|
+
function singleText(content) {
|
|
182
|
+
if (!Array.isArray(content) || content.length !== 1) return undefined
|
|
183
|
+
const [block] = content
|
|
184
|
+
if (!block || block.type !== 'text' || typeof block.text !== 'string') return undefined
|
|
185
|
+
return block.text
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Treat shell-shaped values as execution status only for known command tools. */
|
|
189
|
+
function commandFailed(value) {
|
|
190
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
182
191
|
const exitCode = value.exit_code ?? value.exitCode
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
const numericExit = typeof exitCode === 'number'
|
|
193
|
+
? exitCode
|
|
194
|
+
: (typeof exitCode === 'string' && /^-?\d+$/.test(exitCode.trim())
|
|
195
|
+
? Number(exitCode)
|
|
196
|
+
: 0)
|
|
197
|
+
return numericExit !== 0
|
|
198
|
+
|| (typeof value.signal === 'string' && value.signal.length > 0)
|
|
199
|
+
|| value.timed_out === true
|
|
200
|
+
|| value.timedOut === true
|
|
189
201
|
|| value.isError === true
|
|
190
202
|
|| value.success === false
|
|
191
203
|
|| value.ok === false
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Extract the failure payload while leaving diagnostic classification to Core. */
|
|
207
|
+
function failureText(result, value) {
|
|
208
|
+
const parts = []
|
|
209
|
+
if (result?.isError === true && typeof result.error?.message === 'string') {
|
|
210
|
+
parts.push(result.error.message)
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(result?.content)) {
|
|
213
|
+
parts.push(...result.content
|
|
214
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
215
|
+
.map((block) => block.text))
|
|
216
|
+
}
|
|
217
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
218
|
+
const stderr = value.stderr
|
|
219
|
+
if (typeof stderr === 'string') {
|
|
220
|
+
parts.push(stderr)
|
|
221
|
+
} else if (stderr && typeof stderr.text === 'string') {
|
|
222
|
+
parts.push(stderr.text)
|
|
223
|
+
}
|
|
224
|
+
if (typeof value.error === 'string') {
|
|
225
|
+
parts.push(value.error)
|
|
226
|
+
} else if (value.error && typeof value.error.message === 'string') {
|
|
227
|
+
parts.push(value.error.message)
|
|
228
|
+
} else if (value.error && typeof value.error === 'object') {
|
|
229
|
+
let serialized
|
|
199
230
|
try {
|
|
200
|
-
|
|
231
|
+
serialized = JSON.stringify(value.error)
|
|
201
232
|
} catch {
|
|
202
|
-
|
|
233
|
+
serialized = String(value.error)
|
|
234
|
+
}
|
|
235
|
+
if (typeof serialized === 'string') parts.push(serialized)
|
|
236
|
+
}
|
|
237
|
+
if (typeof value.signal === 'string' && value.signal.length > 0) {
|
|
238
|
+
parts.push(`terminated by signal: ${value.signal}`)
|
|
239
|
+
}
|
|
240
|
+
if (!parts.some((part) => part.length > 0)) {
|
|
241
|
+
const stdout = value.stdout
|
|
242
|
+
if (typeof stdout === 'string') {
|
|
243
|
+
parts.push(stdout)
|
|
244
|
+
} else if (stdout && typeof stdout.text === 'string') {
|
|
245
|
+
parts.push(stdout.text)
|
|
203
246
|
}
|
|
204
247
|
}
|
|
205
248
|
}
|
|
206
|
-
|
|
207
|
-
if (typeof stream === 'string') return stream
|
|
208
|
-
if (stream && typeof stream === 'object' && typeof stream.text === 'string') return stream.text
|
|
209
|
-
return undefined
|
|
210
|
-
}
|
|
211
|
-
const text = [streamText(value.stderr), errorValue]
|
|
212
|
-
.filter((part) => typeof part === 'string')
|
|
213
|
-
.join('\n')
|
|
214
|
-
return classifyEnvironmentError(text)
|
|
249
|
+
return parts.filter((part) => part.length > 0).join('\n')
|
|
215
250
|
}
|
|
216
251
|
|
|
217
|
-
/** Construct a valid plugin-owned user message without importing
|
|
218
|
-
function
|
|
252
|
+
/** Construct a valid plugin-owned user message without importing DSH modules. */
|
|
253
|
+
function diagnosticContext(text) {
|
|
219
254
|
return {
|
|
220
255
|
id: randomUUID(),
|
|
221
256
|
role: 'user',
|
|
@@ -229,171 +264,225 @@ function attributionContext(text) {
|
|
|
229
264
|
}
|
|
230
265
|
}
|
|
231
266
|
|
|
232
|
-
/** Add
|
|
233
|
-
function
|
|
234
|
-
if (
|
|
267
|
+
/** Add Core's diagnostic to a decision while preserving waterfall output. */
|
|
268
|
+
function withDiagnostic(decision, diagnostic) {
|
|
269
|
+
if (typeof diagnostic !== 'string' || diagnostic.length === 0) return decision
|
|
235
270
|
return {
|
|
236
271
|
...decision,
|
|
237
272
|
additionalContexts: [
|
|
238
273
|
...(Array.isArray(decision.additionalContexts) ? decision.additionalContexts : []),
|
|
239
|
-
|
|
274
|
+
diagnosticContext(diagnostic),
|
|
240
275
|
],
|
|
241
276
|
}
|
|
242
277
|
}
|
|
243
278
|
|
|
244
|
-
/**
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return block.text
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Convert one config threshold to a positive finite integer. */
|
|
254
|
-
function positiveInteger(value, fallback) {
|
|
255
|
-
return Number.isInteger(value) && value > 0 ? value : fallback
|
|
279
|
+
/** Map a DSH tool name to the explicit PostTool content origin. */
|
|
280
|
+
function contentOrigin(toolName) {
|
|
281
|
+
const normalized = toolName.toLowerCase()
|
|
282
|
+
if (FILE_TOOLS.has(normalized)) return 'file_content'
|
|
283
|
+
if (COMMAND_TOOLS.has(normalized)) return 'command_output'
|
|
284
|
+
return 'api_response'
|
|
256
285
|
}
|
|
257
286
|
|
|
258
|
-
/**
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const args = [
|
|
265
|
-
'compress-response',
|
|
266
|
-
'--agent-id',
|
|
267
|
-
String(valueOr(config, 'agentId', DEFAULT_AGENT_ID)),
|
|
268
|
-
'--truncate-strings-at',
|
|
269
|
-
String(strings),
|
|
270
|
-
'--truncate-arrays-at',
|
|
271
|
-
String(arrays),
|
|
272
|
-
'--max-depth',
|
|
273
|
-
String(depth),
|
|
274
|
-
]
|
|
275
|
-
const sessionId = exec.agent?.id
|
|
276
|
-
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
277
|
-
args.push('--session-id', sessionId)
|
|
278
|
-
}
|
|
279
|
-
if (typeof exec.callId === 'string' && exec.callId.length > 0) {
|
|
280
|
-
args.push('--tool-use-id', exec.callId)
|
|
281
|
-
}
|
|
282
|
-
if (valueOr(config, 'noStash', false) === true) args.push('--no-stash')
|
|
283
|
-
return args
|
|
287
|
+
/** Recognize the exact local recovery command emitted by Tokenless markers. */
|
|
288
|
+
function isTokenlessRetrieveCommand(toolName, args) {
|
|
289
|
+
if (!COMMAND_TOOLS.has(toolName.toLowerCase())) return false
|
|
290
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) return false
|
|
291
|
+
if (typeof args.command !== 'string') return false
|
|
292
|
+
return TOKENLESS_RETRIEVE_COMMAND_RE.test(args.command)
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
/** Execute a child process with bounded output and explicit stdin. */
|
|
287
|
-
function runTokenless(binary,
|
|
296
|
+
function runTokenless(binary, request, options) {
|
|
288
297
|
return new Promise((resolve, reject) => {
|
|
289
298
|
let child
|
|
290
299
|
try {
|
|
291
|
-
child = execFile(binary,
|
|
300
|
+
child = execFile(binary, ['compress'], options, (error, stdout) => {
|
|
292
301
|
if (error) {
|
|
293
|
-
error.stderr = stderr
|
|
294
302
|
reject(error)
|
|
295
303
|
return
|
|
296
304
|
}
|
|
297
|
-
resolve(
|
|
305
|
+
resolve(stdout)
|
|
298
306
|
})
|
|
299
307
|
} catch (error) {
|
|
300
308
|
reject(error)
|
|
301
309
|
return
|
|
302
310
|
}
|
|
303
311
|
child.stdin?.on('error', () => {})
|
|
304
|
-
|
|
305
|
-
child.stdin?.end(input)
|
|
306
|
-
} catch (error) {
|
|
307
|
-
reject(error)
|
|
308
|
-
}
|
|
312
|
+
child.stdin?.end(JSON.stringify(request))
|
|
309
313
|
})
|
|
310
314
|
}
|
|
311
315
|
|
|
312
|
-
/** Run
|
|
313
|
-
async function
|
|
314
|
-
if (exec.signal?.aborted) return undefined
|
|
315
|
-
let parsed
|
|
316
|
-
try {
|
|
317
|
-
parsed = JSON.parse(text)
|
|
318
|
-
} catch {
|
|
319
|
-
return undefined
|
|
320
|
-
}
|
|
321
|
-
if (parsed === null || (typeof parsed !== 'object' && !Array.isArray(parsed))) return undefined
|
|
322
|
-
const binary = tokenlessBinary(config)
|
|
316
|
+
/** Run one PostTool operation and reject malformed transport responses. */
|
|
317
|
+
async function runPostTool(request, exec, config) {
|
|
323
318
|
try {
|
|
324
|
-
const
|
|
319
|
+
const stdout = await runTokenless(tokenlessBinary(config), request, {
|
|
325
320
|
timeout: positiveInteger(valueOr(config, 'timeoutMs', undefined), DEFAULT_TIMEOUT_MS),
|
|
326
321
|
maxBuffer: positiveInteger(valueOr(config, 'maxBuffer', undefined), DEFAULT_MAX_BUFFER),
|
|
327
322
|
encoding: 'utf8',
|
|
328
323
|
windowsHide: true,
|
|
329
324
|
signal: exec.signal,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return
|
|
325
|
+
env: {
|
|
326
|
+
...process.env,
|
|
327
|
+
TOKENLESS_DATA_DIR: prepareTokenlessDataDir(exec),
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
const response = JSON.parse(stdout)
|
|
331
|
+
if (!response || typeof response !== 'object' || Array.isArray(response)) return undefined
|
|
332
|
+
if (response.protocol_version !== 2 || response.operation !== 'post_tool') return undefined
|
|
333
|
+
const result = response.result
|
|
334
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) return undefined
|
|
335
|
+
return result
|
|
338
336
|
} catch {
|
|
339
337
|
return undefined
|
|
340
338
|
}
|
|
341
339
|
}
|
|
342
340
|
|
|
343
|
-
/**
|
|
341
|
+
/** Build one operation request from the DSH execution boundary. */
|
|
342
|
+
function postToolRequest(
|
|
343
|
+
exec,
|
|
344
|
+
config,
|
|
345
|
+
content,
|
|
346
|
+
status,
|
|
347
|
+
origin,
|
|
348
|
+
replaceOutput,
|
|
349
|
+
resultKind,
|
|
350
|
+
retrievalAvailable,
|
|
351
|
+
) {
|
|
352
|
+
const attribution = {
|
|
353
|
+
agent_id: String(valueOr(config, 'agentId', DEFAULT_AGENT_ID)),
|
|
354
|
+
}
|
|
355
|
+
if (typeof exec.agent?.id === 'string' && exec.agent.id.length > 0) {
|
|
356
|
+
attribution.session_id = exec.agent.id
|
|
357
|
+
}
|
|
358
|
+
if (typeof exec.callId === 'string' && exec.callId.length > 0) {
|
|
359
|
+
attribution.tool_use_id = exec.callId
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
protocol_version: 2,
|
|
363
|
+
operation: 'post_tool',
|
|
364
|
+
attribution,
|
|
365
|
+
input: {
|
|
366
|
+
result_kind: resultKind,
|
|
367
|
+
tool_name: exec.name,
|
|
368
|
+
content,
|
|
369
|
+
status,
|
|
370
|
+
content_origin: origin,
|
|
371
|
+
output_optimization: 'none',
|
|
372
|
+
capabilities: {
|
|
373
|
+
replace_output: replaceOutput,
|
|
374
|
+
recovery: { kind: retrievalAvailable ? "shell" : "none" },
|
|
375
|
+
replace_with_text: replaceOutput,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Register DSH's PostTool seam as a thin Tokenless lifecycle adapter. */
|
|
344
382
|
export function apply(ctx, config = {}) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
383
|
+
const compressionEnabled = valueOr(config, 'responseCompressionEnabled', true) !== false
|
|
384
|
+
|
|
385
|
+
ctx.shellEnv.register({
|
|
386
|
+
name: PLUGIN_NAME,
|
|
387
|
+
variables: {
|
|
388
|
+
[DSH_TOKENLESS_DATA_DIR]: {
|
|
389
|
+
description: 'Workspace-local Tokenless state used by Marker recovery.',
|
|
390
|
+
},
|
|
391
|
+
[DSH_TOKENLESS_STATS_DB]: {
|
|
392
|
+
description: 'Tokenless statistics database selected by the DSH host.',
|
|
393
|
+
},
|
|
394
|
+
[DSH_TOKENLESS_STASH_DB]: {
|
|
395
|
+
description: 'Tokenless Stash database selected by the DSH host.',
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
resolve: tokenlessShellEnvironment,
|
|
399
|
+
})
|
|
358
400
|
|
|
359
|
-
|
|
360
|
-
//
|
|
401
|
+
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
402
|
+
// DSH post-execute is a waterfall; later policies own the final projection.
|
|
361
403
|
const decision = await next()
|
|
404
|
+
if (decision.kind !== 'accept' || exec.signal?.aborted) return decision
|
|
405
|
+
|
|
406
|
+
const toolName = exec.name.toLowerCase()
|
|
407
|
+
const isCommand = COMMAND_TOOLS.has(toolName)
|
|
362
408
|
const replacesValue = Object.prototype.hasOwnProperty.call(decision, 'value')
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const attribution = replacesValue
|
|
366
|
-
? (shellTools.has(exec.name)
|
|
367
|
-
? classifyStructuredEnvironmentError(decision.value)
|
|
368
|
-
: undefined)
|
|
369
|
-
: originalAttribution
|
|
370
|
-
const responseContext = attribution
|
|
371
|
-
? `[tokenless:env] ${exec.name} failed: ${attribution.category} (${attribution.hint}). Skip retry.`
|
|
372
|
-
: undefined
|
|
373
|
-
const canCompress = enabled
|
|
374
|
-
&& !skipTools.has(exec.name)
|
|
375
|
-
&& exec.parent === undefined
|
|
376
|
-
&& decision.kind === 'accept'
|
|
377
|
-
&& !replacesValue
|
|
378
|
-
if (!canCompress) return withAttribution(decision, responseContext)
|
|
379
|
-
|
|
380
|
-
// Only a single text block is safe to replace. Images, tool-call blocks,
|
|
381
|
-
// nested tool results, and mixed content remain untouched by design.
|
|
382
|
-
const contentResult = decision.content === undefined
|
|
383
|
-
? result
|
|
384
|
-
: { ...result, content: decision.content }
|
|
385
|
-
const original = singleTextContent(contentResult)
|
|
386
|
-
if (original === undefined) return withAttribution(decision, responseContext)
|
|
409
|
+
const effectiveValue = replacesValue ? decision.value : result.value
|
|
410
|
+
const structuredFailure = isCommand && commandFailed(effectiveValue)
|
|
387
411
|
|
|
388
|
-
|
|
389
|
-
|
|
412
|
+
if (replacesValue) {
|
|
413
|
+
if (!structuredFailure) return decision
|
|
414
|
+
const content = failureText(undefined, effectiveValue)
|
|
415
|
+
const request = postToolRequest(
|
|
416
|
+
exec,
|
|
417
|
+
config,
|
|
418
|
+
content,
|
|
419
|
+
'error',
|
|
420
|
+
'command_output',
|
|
421
|
+
false,
|
|
422
|
+
'tool',
|
|
423
|
+
false,
|
|
424
|
+
)
|
|
425
|
+
const response = await runPostTool(request, exec, config)
|
|
426
|
+
const diagnostic = response?.disposition === 'tool_error'
|
|
427
|
+
? response.additional_context
|
|
428
|
+
: undefined
|
|
429
|
+
return withDiagnostic(decision, diagnostic)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const abortCode = result?.isError === true ? result.error?.info?.code : undefined
|
|
433
|
+
const interrupted = INTERRUPTED_CODES.has(abortCode)
|
|
434
|
+
const failed = result?.isError === true || structuredFailure
|
|
435
|
+
const status = interrupted ? 'interrupted' : (failed ? 'error' : 'success')
|
|
390
436
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
437
|
+
if (status === 'success') {
|
|
438
|
+
if (!compressionEnabled || exec.parent !== undefined) return decision
|
|
439
|
+
const effectiveContent = decision.content ?? result.content
|
|
440
|
+
const content = singleText(effectiveContent)
|
|
441
|
+
if (content === undefined) return decision
|
|
442
|
+
const retrieveResult = isTokenlessRetrieveCommand(exec.name, exec.arguments)
|
|
443
|
+
const request = postToolRequest(
|
|
444
|
+
exec,
|
|
445
|
+
config,
|
|
446
|
+
content,
|
|
447
|
+
status,
|
|
448
|
+
contentOrigin(exec.name),
|
|
449
|
+
true,
|
|
450
|
+
retrieveResult ? 'retrieve' : 'tool',
|
|
451
|
+
!retrieveResult && tokenlessRetrieveCommandAvailable(config),
|
|
452
|
+
)
|
|
453
|
+
const response = await runPostTool(request, exec, config)
|
|
454
|
+
if (response?.disposition !== 'applied' || typeof response.output !== 'string') {
|
|
455
|
+
return decision
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
...decision,
|
|
459
|
+
content: [{ type: 'text', text: response.output }],
|
|
460
|
+
}
|
|
394
461
|
}
|
|
462
|
+
|
|
463
|
+
const effectiveResult = decision.content === undefined
|
|
464
|
+
? result
|
|
465
|
+
: { ...result, content: decision.content }
|
|
466
|
+
const content = structuredFailure
|
|
467
|
+
? failureText(undefined, effectiveValue)
|
|
468
|
+
: failureText(effectiveResult)
|
|
469
|
+
const request = postToolRequest(
|
|
470
|
+
exec,
|
|
471
|
+
config,
|
|
472
|
+
content,
|
|
473
|
+
status,
|
|
474
|
+
contentOrigin(exec.name),
|
|
475
|
+
false,
|
|
476
|
+
'tool',
|
|
477
|
+
false,
|
|
478
|
+
)
|
|
479
|
+
const response = await runPostTool(request, exec, config)
|
|
480
|
+
const diagnostic = response?.disposition === 'tool_error'
|
|
481
|
+
? response.additional_context
|
|
482
|
+
: undefined
|
|
483
|
+
return withDiagnostic(decision, diagnostic)
|
|
395
484
|
})
|
|
396
485
|
}
|
|
397
486
|
|
|
398
487
|
export const name = PLUGIN_NAME
|
|
399
|
-
export const inject = ['tools']
|
|
488
|
+
export const inject = ['tools', 'shellEnv']
|