@namzu/sdk 63.1.0 → 64.0.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/CHANGELOG.md +53 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -1
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/compaction/hooks.d.ts +57 -0
- package/dist/compaction/hooks.d.ts.map +1 -0
- package/dist/compaction/hooks.js +44 -0
- package/dist/compaction/hooks.js.map +1 -0
- package/dist/compaction/manual.d.ts +9 -0
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +74 -5
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/model-summary.d.ts +6 -0
- package/dist/compaction/model-summary.d.ts.map +1 -1
- package/dist/compaction/model-summary.js +13 -3
- package/dist/compaction/model-summary.js.map +1 -1
- package/dist/compaction/verifier.d.ts +8 -2
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +23 -7
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/plugin/hook-context.d.ts +16 -0
- package/dist/plugin/hook-context.d.ts.map +1 -1
- package/dist/plugin/hook-context.js +40 -0
- package/dist/plugin/hook-context.js.map +1 -1
- package/dist/plugin/index.d.ts +4 -3
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +3 -2
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/shell-hook-command.d.ts +25 -0
- package/dist/plugin/shell-hook-command.d.ts.map +1 -0
- package/dist/plugin/shell-hook-command.js +218 -0
- package/dist/plugin/shell-hook-command.js.map +1 -0
- package/dist/plugin/shell-hook-tool-names.d.ts +20 -0
- package/dist/plugin/shell-hook-tool-names.d.ts.map +1 -0
- package/dist/plugin/shell-hook-tool-names.js +43 -0
- package/dist/plugin/shell-hook-tool-names.js.map +1 -0
- package/dist/plugin/shell-hook.d.ts +62 -20
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +372 -85
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/compact-session.d.ts +7 -1
- package/dist/runtime/query/compact-session.d.ts.map +1 -1
- package/dist/runtime/query/compact-session.js +8 -3
- package/dist/runtime/query/compact-session.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +4 -3
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +5 -3
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +9 -2
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +2 -1
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +24 -12
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +2 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.d.ts +13 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +29 -2
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/types/agent/query.d.ts +8 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/plugin/index.d.ts +55 -11
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +2 -1
- package/src/compaction/hooks.ts +104 -0
- package/src/compaction/manual.ts +117 -4
- package/src/compaction/model-summary.ts +19 -3
- package/src/compaction/verifier.ts +33 -3
- package/src/plugin/hook-context.ts +46 -0
- package/src/plugin/index.ts +14 -0
- package/src/plugin/shell-hook-command.ts +215 -0
- package/src/plugin/shell-hook-tool-names.ts +46 -0
- package/src/plugin/shell-hook.ts +405 -79
- package/src/public-runtime.ts +12 -0
- package/src/public-types.ts +2 -0
- package/src/runtime/query/compact-session.ts +16 -4
- package/src/runtime/query/executor/tool-call-admission.ts +10 -2
- package/src/runtime/query/executor.ts +5 -2
- package/src/runtime/query/finalize-turn.ts +9 -2
- package/src/runtime/query/iteration/index.ts +2 -1
- package/src/runtime/query/iteration/phases/compaction.ts +31 -12
- package/src/runtime/query/iteration/phases/tool-review.ts +2 -0
- package/src/runtime/query/plugin-hooks.ts +32 -2
- package/src/types/agent/query.ts +9 -0
- package/src/types/plugin/index.ts +66 -22
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a hook's `if` on a shell command sees beyond the line as typed.
|
|
3
|
+
*
|
|
4
|
+
* A filter such as `Bash(git push *)` is written about a command, and the
|
|
5
|
+
* same command runs behind a wrapper (`sudo git push`, `env A=1 git push`,
|
|
6
|
+
* `nohup git push &`, `xargs git push`, `find . -exec git push {} ;`), under
|
|
7
|
+
* its full path (`/usr/bin/git push`) and with options in front of its
|
|
8
|
+
* subcommand (`git -C repo push`). The hook is attached to stop things, so a
|
|
9
|
+
* filter that misses one of those spellings switches the guard off. Each
|
|
10
|
+
* command of the line is therefore also read with its wrappers peeled, its
|
|
11
|
+
* program named by its basename and git's global options dropped, and the
|
|
12
|
+
* filter matches if any of those readings does.
|
|
13
|
+
*
|
|
14
|
+
* A command whose name is not known until the line runs (`$(echo git) push`,
|
|
15
|
+
* `$CMD push`) cannot be compared with anything: `unknowable` says so, and
|
|
16
|
+
* the caller runs the hook.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { basename, lexShellCommandLine } from '../authorization/shell-lexer.js'
|
|
20
|
+
|
|
21
|
+
export interface HookCommandReading {
|
|
22
|
+
/** Every spelling of every command the line runs, as words joined by one space. */
|
|
23
|
+
readonly candidates: readonly string[]
|
|
24
|
+
/** The line runs something whose name cannot be read from it. */
|
|
25
|
+
readonly unknowable: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_VARIANTS = 48
|
|
29
|
+
|
|
30
|
+
/** Options of a wrapper that take the next word as their value. */
|
|
31
|
+
const WRAPPER_VALUE_OPTIONS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
32
|
+
env: new Set(['-u', '--unset', '-C', '--chdir', '-S', '--split-string']),
|
|
33
|
+
sudo: new Set([
|
|
34
|
+
'-u',
|
|
35
|
+
'--user',
|
|
36
|
+
'-g',
|
|
37
|
+
'--group',
|
|
38
|
+
'-h',
|
|
39
|
+
'--host',
|
|
40
|
+
'-p',
|
|
41
|
+
'--prompt',
|
|
42
|
+
'-C',
|
|
43
|
+
'-D',
|
|
44
|
+
'-R',
|
|
45
|
+
'-T',
|
|
46
|
+
'-U',
|
|
47
|
+
]),
|
|
48
|
+
doas: new Set(['-u', '-C']),
|
|
49
|
+
nice: new Set(['-n', '--adjustment']),
|
|
50
|
+
ionice: new Set(['-c', '-n', '-p', '-P', '-u']),
|
|
51
|
+
timeout: new Set(['-s', '--signal', '-k', '--kill-after']),
|
|
52
|
+
xargs: new Set([
|
|
53
|
+
'-I',
|
|
54
|
+
'-i',
|
|
55
|
+
'-n',
|
|
56
|
+
'-P',
|
|
57
|
+
'-d',
|
|
58
|
+
'-L',
|
|
59
|
+
'-l',
|
|
60
|
+
'-s',
|
|
61
|
+
'-E',
|
|
62
|
+
'-e',
|
|
63
|
+
'-a',
|
|
64
|
+
'--max-args',
|
|
65
|
+
'--max-procs',
|
|
66
|
+
'--delimiter',
|
|
67
|
+
'--max-lines',
|
|
68
|
+
'--max-chars',
|
|
69
|
+
'--arg-file',
|
|
70
|
+
]),
|
|
71
|
+
stdbuf: new Set(['-i', '-o', '-e']),
|
|
72
|
+
command: new Set(),
|
|
73
|
+
nohup: new Set(),
|
|
74
|
+
exec: new Set(['-a']),
|
|
75
|
+
builtin: new Set(),
|
|
76
|
+
time: new Set(['-f', '--format', '-o', '--output']),
|
|
77
|
+
setsid: new Set(),
|
|
78
|
+
chroot: new Set(),
|
|
79
|
+
busybox: new Set(),
|
|
80
|
+
watch: new Set(['-n', '--interval']),
|
|
81
|
+
flock: new Set(['-w', '--timeout', '-E', '--conflict-exit-code']),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Global options of git that stand between `git` and its subcommand. */
|
|
85
|
+
const GIT_VALUE_OPTIONS = new Set([
|
|
86
|
+
'-C',
|
|
87
|
+
'-c',
|
|
88
|
+
'--git-dir',
|
|
89
|
+
'--work-tree',
|
|
90
|
+
'--namespace',
|
|
91
|
+
'--exec-path',
|
|
92
|
+
'--super-prefix',
|
|
93
|
+
'--config-env',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
/** The words after the wrapper at `words[0]`, or `undefined` when it is not one. */
|
|
97
|
+
function peelWrapper(words: readonly string[]): readonly string[] | undefined {
|
|
98
|
+
const head = basename(words[0] ?? '')
|
|
99
|
+
const valueOptions = WRAPPER_VALUE_OPTIONS[head]
|
|
100
|
+
if (valueOptions === undefined) return undefined
|
|
101
|
+
let index = 1
|
|
102
|
+
while (index < words.length) {
|
|
103
|
+
const word = words[index] as string
|
|
104
|
+
if (word === '--') {
|
|
105
|
+
index += 1
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
if (head === 'env' && /^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) {
|
|
109
|
+
index += 1
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if (word.startsWith('-') && word.length > 1) {
|
|
113
|
+
index += valueOptions.has(word) ? 2 : 1
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
// `timeout 5 cmd`, `nice 10 cmd` (old form): a bare duration or number.
|
|
117
|
+
if ((head === 'timeout' || head === 'nice') && /^\d+(?:\.\d+)?[smhd]?$/.test(word)) {
|
|
118
|
+
index += 1
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
// `flock <lockfile> cmd`: the first word after the options is the lock, not the command.
|
|
124
|
+
if (head === 'flock') index += 1
|
|
125
|
+
const rest = words.slice(index)
|
|
126
|
+
return rest.length > 0 ? rest : undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The commands after each `-exec`, `-execdir` or `-ok` of a `find`. */
|
|
130
|
+
function findExecutions(words: readonly string[]): string[][] {
|
|
131
|
+
if (basename(words[0] ?? '') !== 'find') return []
|
|
132
|
+
const found: string[][] = []
|
|
133
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
134
|
+
const word = words[index]
|
|
135
|
+
if (word !== '-exec' && word !== '-execdir' && word !== '-ok' && word !== '-okdir') continue
|
|
136
|
+
const body: string[] = []
|
|
137
|
+
for (let next = index + 1; next < words.length; next += 1) {
|
|
138
|
+
const each = words[next] as string
|
|
139
|
+
if (each === ';' || each === ';' || each === '+') break
|
|
140
|
+
body.push(each)
|
|
141
|
+
}
|
|
142
|
+
if (body.length > 0) found.push(body)
|
|
143
|
+
}
|
|
144
|
+
return found
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `git -C repo push` as `git push`. */
|
|
148
|
+
function dropGitGlobals(words: readonly string[]): readonly string[] | undefined {
|
|
149
|
+
if (basename(words[0] ?? '') !== 'git') return undefined
|
|
150
|
+
const kept: string[] = ['git']
|
|
151
|
+
let index = 1
|
|
152
|
+
let dropped = false
|
|
153
|
+
while (index < words.length) {
|
|
154
|
+
const word = words[index] as string
|
|
155
|
+
if (!word.startsWith('-')) break
|
|
156
|
+
const name = word.split('=')[0] as string
|
|
157
|
+
if (GIT_VALUE_OPTIONS.has(name)) {
|
|
158
|
+
index += word.includes('=') ? 1 : 2
|
|
159
|
+
dropped = true
|
|
160
|
+
} else if (
|
|
161
|
+
word === '--no-pager' ||
|
|
162
|
+
word === '-p' ||
|
|
163
|
+
word === '-P' ||
|
|
164
|
+
word === '--paginate' ||
|
|
165
|
+
word === '--no-optional-locks' ||
|
|
166
|
+
word === '--bare' ||
|
|
167
|
+
word === '--no-replace-objects'
|
|
168
|
+
) {
|
|
169
|
+
index += 1
|
|
170
|
+
dropped = true
|
|
171
|
+
} else {
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!dropped) return undefined
|
|
176
|
+
return [...kept, ...words.slice(index)]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function variantsOf(start: readonly string[]): string[][] {
|
|
180
|
+
const seen = new Map<string, string[]>()
|
|
181
|
+
const queue: string[][] = [[...start]]
|
|
182
|
+
while (queue.length > 0 && seen.size < MAX_VARIANTS) {
|
|
183
|
+
const words = queue.shift() as string[]
|
|
184
|
+
const key = words.join('\u0000')
|
|
185
|
+
if (seen.has(key)) continue
|
|
186
|
+
seen.set(key, words)
|
|
187
|
+
const first = words[0]
|
|
188
|
+
if (first === undefined) continue
|
|
189
|
+
const peeled = peelWrapper(words)
|
|
190
|
+
if (peeled !== undefined) queue.push([...peeled])
|
|
191
|
+
for (const body of findExecutions(words)) queue.push(body)
|
|
192
|
+
const git = dropGitGlobals(words)
|
|
193
|
+
if (git !== undefined) queue.push([...git])
|
|
194
|
+
const base = basename(first)
|
|
195
|
+
if (base !== first) queue.push([base, ...words.slice(1)])
|
|
196
|
+
}
|
|
197
|
+
return [...seen.values()]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function readHookCommand(line: string): HookCommandReading {
|
|
201
|
+
const lexed = lexShellCommandLine(line, { dialect: 'sh' })
|
|
202
|
+
if (!lexed.complete) return { candidates: [], unknowable: true }
|
|
203
|
+
const candidates = new Set<string>()
|
|
204
|
+
let unknowable = false
|
|
205
|
+
for (const command of lexed.commands) {
|
|
206
|
+
const words = command.words.slice(command.assignments)
|
|
207
|
+
const head = words[0]
|
|
208
|
+
if (head === undefined) continue
|
|
209
|
+
if (head.expands && /[$`]/.test(head.text)) unknowable = true
|
|
210
|
+
for (const variant of variantsOf(words.map((word) => word.value))) {
|
|
211
|
+
candidates.add(variant.join(' '))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { candidates: [...candidates], unknowable }
|
|
215
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names a shell hook may be sent for a tool.
|
|
3
|
+
*
|
|
4
|
+
* A hook sees the tool's own name (`bash`, `web_fetch`) unless its entry asks
|
|
5
|
+
* for the written one (`toolNames: 'written'`), the capitalised name a person
|
|
6
|
+
* writes in a matcher for that tool (`Bash`, `WebFetch`). An entry read from
|
|
7
|
+
* the common hook contract asks for it by default.
|
|
8
|
+
*
|
|
9
|
+
* ONE table, from the name a tool is registered under to its written name.
|
|
10
|
+
* A tool with no common name is not in it and keeps its own; a tool-server
|
|
11
|
+
* tool (`mcp__<server>__<tool>`) is never in it. The table is documented in
|
|
12
|
+
* `docs/sdk/hooks.md`, and a test holds the two equal.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Which spelling of a tool's name an entry receives. */
|
|
16
|
+
export type ShellHookToolNames = 'namzu' | 'written'
|
|
17
|
+
|
|
18
|
+
/** Our tool name to the name written for it. Every key is lower case. */
|
|
19
|
+
export const SHELL_HOOK_WRITTEN_TOOL_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
20
|
+
Agent: 'Agent',
|
|
21
|
+
ask_user_question: 'AskUserQuestion',
|
|
22
|
+
bash: 'Bash',
|
|
23
|
+
edit: 'Edit',
|
|
24
|
+
glob: 'Glob',
|
|
25
|
+
grep: 'Grep',
|
|
26
|
+
ls: 'LS',
|
|
27
|
+
lsp: 'LSP',
|
|
28
|
+
monitor: 'Monitor',
|
|
29
|
+
read: 'Read',
|
|
30
|
+
search_tools: 'ToolSearch',
|
|
31
|
+
skill: 'Skill',
|
|
32
|
+
task_create: 'TaskCreate',
|
|
33
|
+
task_list: 'TaskList',
|
|
34
|
+
task_update: 'TaskUpdate',
|
|
35
|
+
web_fetch: 'WebFetch',
|
|
36
|
+
web_search: 'WebSearch',
|
|
37
|
+
write: 'Write',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/** The name a hook is sent for `toolName` under the entry's `toolNames`. */
|
|
41
|
+
export function shellHookToolName(toolName: string, mode: ShellHookToolNames | undefined): string {
|
|
42
|
+
if (mode !== 'written') return toolName
|
|
43
|
+
return Object.hasOwn(SHELL_HOOK_WRITTEN_TOOL_NAMES, toolName)
|
|
44
|
+
? (SHELL_HOOK_WRITTEN_TOOL_NAMES[toolName] as string)
|
|
45
|
+
: toolName
|
|
46
|
+
}
|