@namzu/sdk 44.2.0 → 45.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/authorization/command-line.d.ts +66 -19
  3. package/dist/authorization/command-line.d.ts.map +1 -1
  4. package/dist/authorization/command-line.js +130 -270
  5. package/dist/authorization/command-line.js.map +1 -1
  6. package/dist/authorization/gate.d.ts +7 -0
  7. package/dist/authorization/gate.d.ts.map +1 -1
  8. package/dist/authorization/gate.js +1 -1
  9. package/dist/authorization/gate.js.map +1 -1
  10. package/dist/authorization/rules.d.ts +10 -1
  11. package/dist/authorization/rules.d.ts.map +1 -1
  12. package/dist/authorization/rules.js +21 -5
  13. package/dist/authorization/rules.js.map +1 -1
  14. package/dist/authorization/shell-lexer.d.ts +138 -0
  15. package/dist/authorization/shell-lexer.d.ts.map +1 -0
  16. package/dist/authorization/shell-lexer.js +2143 -0
  17. package/dist/authorization/shell-lexer.js.map +1 -0
  18. package/dist/authorization/skill-grant.d.ts +182 -0
  19. package/dist/authorization/skill-grant.d.ts.map +1 -0
  20. package/dist/authorization/skill-grant.js +314 -0
  21. package/dist/authorization/skill-grant.js.map +1 -0
  22. package/dist/persona/assembler.d.ts.map +1 -1
  23. package/dist/persona/assembler.js +5 -2
  24. package/dist/persona/assembler.js.map +1 -1
  25. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  26. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  27. package/dist/prompt/coding-agent-doctrine.js +1 -1
  28. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  29. package/dist/public-runtime.d.ts +1 -0
  30. package/dist/public-runtime.d.ts.map +1 -1
  31. package/dist/public-runtime.js +4 -0
  32. package/dist/public-runtime.js.map +1 -1
  33. package/dist/public-tools.d.ts.map +1 -1
  34. package/dist/public-tools.js +2 -1
  35. package/dist/public-tools.js.map +1 -1
  36. package/dist/public-types.d.ts +3 -1
  37. package/dist/public-types.d.ts.map +1 -1
  38. package/dist/runtime/jobs/registry.d.ts +2 -2
  39. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  40. package/dist/runtime/jobs/registry.js +6 -2
  41. package/dist/runtime/jobs/registry.js.map +1 -1
  42. package/dist/runtime/query/executor.d.ts +34 -40
  43. package/dist/runtime/query/executor.d.ts.map +1 -1
  44. package/dist/runtime/query/executor.js +81 -51
  45. package/dist/runtime/query/executor.js.map +1 -1
  46. package/dist/runtime/query/index.d.ts.map +1 -1
  47. package/dist/runtime/query/index.js +7 -0
  48. package/dist/runtime/query/index.js.map +1 -1
  49. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  50. package/dist/runtime/query/iteration/index.js +6 -0
  51. package/dist/runtime/query/iteration/index.js.map +1 -1
  52. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  53. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  54. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  55. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  56. package/dist/runtime/query/iteration/phases/tool-review.js +48 -0
  57. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  58. package/dist/runtime/query/review-policy.d.ts +11 -0
  59. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  60. package/dist/runtime/query/review-policy.js +32 -0
  61. package/dist/runtime/query/review-policy.js.map +1 -1
  62. package/dist/runtime/query/tooling.d.ts +3 -0
  63. package/dist/runtime/query/tooling.d.ts.map +1 -1
  64. package/dist/runtime/query/tooling.js +1 -0
  65. package/dist/runtime/query/tooling.js.map +1 -1
  66. package/dist/skills/loader.d.ts +8 -0
  67. package/dist/skills/loader.d.ts.map +1 -1
  68. package/dist/skills/loader.js +7 -1
  69. package/dist/skills/loader.js.map +1 -1
  70. package/dist/tools/builtins/bash.d.ts.map +1 -1
  71. package/dist/tools/builtins/bash.js +18 -6
  72. package/dist/tools/builtins/bash.js.map +1 -1
  73. package/dist/tools/builtins/skill.d.ts +2 -9
  74. package/dist/tools/builtins/skill.d.ts.map +1 -1
  75. package/dist/tools/builtins/skill.js +59 -51
  76. package/dist/tools/builtins/skill.js.map +1 -1
  77. package/dist/tools/command-shell.d.ts +90 -0
  78. package/dist/tools/command-shell.d.ts.map +1 -0
  79. package/dist/tools/command-shell.js +129 -0
  80. package/dist/tools/command-shell.js.map +1 -0
  81. package/dist/tools/defineTool.d.ts +11 -0
  82. package/dist/tools/defineTool.d.ts.map +1 -1
  83. package/dist/tools/defineTool.js +29 -1
  84. package/dist/tools/defineTool.js.map +1 -1
  85. package/dist/types/hitl/index.d.ts +23 -0
  86. package/dist/types/hitl/index.d.ts.map +1 -1
  87. package/dist/types/hitl/index.js.map +1 -1
  88. package/dist/types/provider/stream.d.ts +10 -0
  89. package/dist/types/provider/stream.d.ts.map +1 -1
  90. package/dist/types/tool/index.d.ts +67 -5
  91. package/dist/types/tool/index.d.ts.map +1 -1
  92. package/dist/types/tool/index.js.map +1 -1
  93. package/dist/utils/frontmatter.d.ts +17 -1
  94. package/dist/utils/frontmatter.d.ts.map +1 -1
  95. package/dist/utils/frontmatter.js +32 -2
  96. package/dist/utils/frontmatter.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/authorization/command-line.ts +148 -293
  99. package/src/authorization/gate.ts +8 -0
  100. package/src/authorization/rules.ts +33 -4
  101. package/src/authorization/shell-lexer.ts +2319 -0
  102. package/src/authorization/skill-grant.ts +400 -0
  103. package/src/persona/assembler.ts +5 -2
  104. package/src/prompt/coding-agent-doctrine.ts +1 -1
  105. package/src/public-runtime.ts +9 -0
  106. package/src/public-tools.ts +2 -1
  107. package/src/public-types.ts +7 -0
  108. package/src/runtime/jobs/registry.ts +19 -11
  109. package/src/runtime/query/executor.ts +99 -55
  110. package/src/runtime/query/index.ts +7 -0
  111. package/src/runtime/query/iteration/index.ts +5 -0
  112. package/src/runtime/query/iteration/phases/context.ts +7 -0
  113. package/src/runtime/query/iteration/phases/tool-review.ts +45 -0
  114. package/src/runtime/query/review-policy.ts +53 -0
  115. package/src/runtime/query/tooling.ts +4 -0
  116. package/src/skills/loader.ts +8 -1
  117. package/src/tools/builtins/bash.ts +24 -6
  118. package/src/tools/builtins/skill.ts +74 -53
  119. package/src/tools/command-shell.ts +166 -0
  120. package/src/tools/defineTool.ts +33 -1
  121. package/src/types/hitl/index.ts +21 -0
  122. package/src/types/provider/stream.ts +10 -0
  123. package/src/types/tool/index.ts +67 -5
  124. package/src/utils/frontmatter.ts +52 -2
@@ -0,0 +1,400 @@
1
+ /**
2
+ * A skill's `allowed-tools`, as what it is everywhere else: a pre-approval.
3
+ *
4
+ * The field comes from the Agent Skills format, and the runtime that made it
5
+ * popular documents it plainly: the listed tools may be used without asking
6
+ * for the rest of the turn that loaded the skill, and "it does not restrict
7
+ * which tools are available: every tool remains callable, and your permission
8
+ * settings still govern tools that are not listed."
9
+ *
10
+ * This kernel read it the other way round, as a RESTRICTION. A skill written
11
+ * for that ecosystem says `allowed-tools: Read Grep` to mean "these two are
12
+ * fine without a prompt", and a turn that loaded it here lost `bash` on its
13
+ * next batch and was told to "restrict yourself to" two tools — so the model
14
+ * stopped doing the work and tried to do everything through whatever the
15
+ * skill described. The field was never meant to take anything away.
16
+ *
17
+ * What it grants, and what it does not:
18
+ *
19
+ * - **Grants**: a call that matches an entry skips the approval PROMPT for the
20
+ * rest of the turn, on the same footing as a person answering "allow" for
21
+ * the turn. The turn ends, the grant ends; loading the skill again in a
22
+ * later turn grants again.
23
+ * - **Never overrides**: an operator `deny` rule, an operator `ask` rule, plan
24
+ * mode, `strict` mode, an escalation (a path argument outside the working
25
+ * directory, a sandbox escape), or a call the tool itself declares
26
+ * destructive. Those are all statements by the operator or the tool; a
27
+ * skill is repository content and cannot outrank either. `bash` has no path
28
+ * argument, so a pattern entry refuses output redirection itself (see
29
+ * `coveringSkill`); a whole-tool `Bash` entry grants what any line does.
30
+ * - **Ends early**: when the operator sends another message into the running
31
+ * turn, the iteration clears the set.
32
+ * - **Never adds a tool**: an entry names a tool this turn already has, or it
33
+ * is ignored and the model is told so. An unknown name widens nothing.
34
+ *
35
+ * Granting is equivalent to the operator trusting that skill's commands,
36
+ * which is why only a skill the host chose to load can grant anything; see
37
+ * `docs/` for the CLI's folder-trust story.
38
+ */
39
+
40
+ import { MAX_CUSTOM_PATTERN_LENGTH } from '../constants/authorization/index.js'
41
+ import type { ToolDefinition } from '../types/tool/index.js'
42
+ import { writesThroughRedirection } from './command-line.js'
43
+ import { evaluateRule } from './rules.js'
44
+ import type { ShellDialect } from './shell-lexer.js'
45
+
46
+ /**
47
+ * Split an `allowed-tools` value into entries.
48
+ *
49
+ * Accepts every spelling the format uses: space-separated (`Read Grep Bash`),
50
+ * comma-separated (`Read, Grep`), and — once the frontmatter reader has turned
51
+ * a YAML list into one line — the items of a list. Separators inside
52
+ * parentheses belong to the entry, so `Bash(git add *)` stays one entry.
53
+ *
54
+ * `undefined` means the skill declared nothing. An empty array means it
55
+ * declared the field empty. Neither grants anything, and neither restricts
56
+ * anything, so the distinction survives only for a host that wants to show
57
+ * what the author wrote.
58
+ */
59
+ export function parseAllowedTools(declared: string | undefined): readonly string[] | undefined {
60
+ if (declared === undefined) return undefined
61
+ const entries: string[] = []
62
+ let current = ''
63
+ let depth = 0
64
+ const flush = () => {
65
+ const entry = current.trim()
66
+ if (entry.length > 0) entries.push(entry)
67
+ current = ''
68
+ }
69
+ for (const char of declared) {
70
+ if (char === '(') depth += 1
71
+ if (char === ')' && depth > 0) depth -= 1
72
+ if (depth === 0 && (char === ',' || /\s/.test(char))) {
73
+ flush()
74
+ continue
75
+ }
76
+ current += char
77
+ }
78
+ flush()
79
+ return entries
80
+ }
81
+
82
+ /**
83
+ * The tool names the Agent Skills ecosystem writes, and what they are here.
84
+ *
85
+ * Keyed lower-case: names are matched without regard to case, because a skill
86
+ * author writes `Read` and this kernel's tool is `read`, and a grant lost to
87
+ * capitalisation is a prompt nobody can explain.
88
+ */
89
+ export const SKILL_TOOL_NAME_ALIASES: Readonly<Record<string, string>> = Object.freeze({
90
+ read: 'read',
91
+ write: 'write',
92
+ edit: 'edit',
93
+ multiedit: 'edit',
94
+ bash: 'bash',
95
+ grep: 'grep',
96
+ glob: 'glob',
97
+ ls: 'ls',
98
+ webfetch: 'web_fetch',
99
+ websearch: 'web_search',
100
+ skill: 'skill',
101
+ askuserquestion: 'ask_user_question',
102
+ task: 'create_task',
103
+ agent: 'create_task',
104
+ lsp: 'lsp',
105
+ toolsearch: 'search_tools',
106
+ // A background shell's output and its termination are one tool here,
107
+ // `job`, whatever the ecosystem's release called them. Granting `job`
108
+ // for `BashOutput` does not grant a kill without review: `job` declares
109
+ // `kill` destructive, and a destructive call is never skill-approved.
110
+ bashoutput: 'job',
111
+ taskoutput: 'job',
112
+ killshell: 'job',
113
+ killbash: 'job',
114
+ taskstop: 'job',
115
+ taskcreate: 'task_create',
116
+ taskupdate: 'task_update',
117
+ tasklist: 'task_list',
118
+ })
119
+
120
+ /**
121
+ * How a grant compiler learns what a name means in this turn: the registered
122
+ * tool a name refers to, or `undefined` for one this turn does not have.
123
+ * `commandArgument` is what lets a `Bash(<pattern>)` entry be matched against
124
+ * the command line rather than the serialised input.
125
+ */
126
+ export type SkillGrantToolResolver = (name: string) =>
127
+ | {
128
+ readonly name: string
129
+ readonly commandArgument?: string
130
+ /**
131
+ * Every call of this tool is destructive, whatever its input (the
132
+ * shipped `write` and `run_code`). A destructive call is always
133
+ * reviewed, so an entry naming such a tool can grant nothing and is
134
+ * reported as ignored rather than listed as pre-approved.
135
+ */
136
+ readonly alwaysDestructive?: boolean
137
+ /**
138
+ * The tool is registered but withheld from this turn or step by
139
+ * `allowedTools`. The model cannot call it, so an entry naming it is
140
+ * reported as ignored rather than listed as pre-approved.
141
+ */
142
+ readonly unavailable?: boolean
143
+ }
144
+ | undefined
145
+
146
+ /** One compiled entry: a whole tool, or one tool's command line matching a pattern. */
147
+ export interface SkillGrantEntry {
148
+ /** The registered tool name. */
149
+ readonly tool: string
150
+ /** Present for a pattern entry: the argument holding the command line. */
151
+ readonly argument?: string
152
+ /** Present for a pattern entry: an anchored regular-expression source. */
153
+ readonly pattern?: string
154
+ /** The entry as the author wrote it, for messages. */
155
+ readonly declared: string
156
+ }
157
+
158
+ export interface CompiledSkillGrant {
159
+ readonly entries: readonly SkillGrantEntry[]
160
+ /** Entries that granted nothing, with the reason, for the model and the log. */
161
+ readonly ignored: readonly {
162
+ readonly entry: string
163
+ readonly reason: string
164
+ }[]
165
+ }
166
+
167
+ /** The two placeholders a pattern may use for the skill's own directory. */
168
+ const SKILL_DIR_PLACEHOLDERS = ['${CLAUDE_SKILL_DIR}', '${NAMZU_SKILL_DIR}'] as const
169
+
170
+ const ENTRY_SHAPE = /^([A-Za-z0-9_.\-]+)(?:\(([\s\S]*)\))?$/
171
+
172
+ /**
173
+ * Turn a permission glob into an anchored regular-expression source.
174
+ *
175
+ * The one glob dialect for command permissions, shared with the CLI's
176
+ * `[permissions]` table so that `Bash(git status *)` in a skill and
177
+ * `bash = { "git status *" = "allow" }` in config mean the same commands:
178
+ *
179
+ * - `*` matches any run of characters, `?` exactly one, everything else is
180
+ * literal;
181
+ * - backslashes become forward slashes, so one pattern works on every platform;
182
+ * - a pattern ending in `<space>*` also matches the bare command, so
183
+ * `git status *` covers `git status` as well as `git status -s`, and does
184
+ * not cover `git statusx`.
185
+ */
186
+ export function permissionPatternToRegExpSource(pattern: string): string {
187
+ const normalized = pattern.replaceAll('\\', '/')
188
+ const escaped = normalized
189
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
190
+ .replace(/\*/g, '.*')
191
+ .replace(/\?/g, '.')
192
+ const trailingSpaceStar = escaped.endsWith(' .*') ? `${escaped.slice(0, -3)}( .*)?` : escaped
193
+ return `^${trailingSpaceStar}$`
194
+ }
195
+
196
+ /**
197
+ * Compile a skill's parsed `allowed-tools` into grant entries.
198
+ *
199
+ * Pure: the resolver supplies everything the turn knows. An entry that cannot
200
+ * be honoured exactly is ignored and reported, never approximated — the only
201
+ * safe approximation of a permission is a narrower one, and "ignored" is the
202
+ * narrowest.
203
+ */
204
+ export function compileSkillGrant(
205
+ declared: readonly string[],
206
+ options: {
207
+ readonly resolveTool: SkillGrantToolResolver
208
+ /** The skill's directory, for `${CLAUDE_SKILL_DIR}` / `${NAMZU_SKILL_DIR}`. */
209
+ readonly skillDirectory?: string
210
+ },
211
+ ): CompiledSkillGrant {
212
+ const entries: SkillGrantEntry[] = []
213
+ const ignored: { entry: string; reason: string }[] = []
214
+
215
+ for (const entry of declared) {
216
+ const shape = ENTRY_SHAPE.exec(entry)
217
+ if (!shape) {
218
+ ignored.push({ entry, reason: 'not a tool name or `Tool(pattern)`' })
219
+ continue
220
+ }
221
+ const written = shape[1] ?? ''
222
+ const specifier = shape[2]?.trim()
223
+ const alias = SKILL_TOOL_NAME_ALIASES[written.toLowerCase()]
224
+ const tool = options.resolveTool(alias ?? written)
225
+ if (!tool) {
226
+ ignored.push({ entry, reason: 'this turn has no tool by that name' })
227
+ continue
228
+ }
229
+ if (tool.unavailable) {
230
+ ignored.push({
231
+ entry,
232
+ reason: `\`${tool.name}\` is not available in this turn, so nothing was granted for it`,
233
+ })
234
+ continue
235
+ }
236
+ if (tool.alwaysDestructive) {
237
+ ignored.push({
238
+ entry,
239
+ reason: `every \`${tool.name}\` call is destructive and is always reviewed, so nothing was granted for it`,
240
+ })
241
+ continue
242
+ }
243
+
244
+ if (specifier === undefined || specifier === '' || specifier.replaceAll('*', '') === '') {
245
+ entries.push({ tool: tool.name, declared: entry })
246
+ continue
247
+ }
248
+
249
+ if (tool.commandArgument === undefined) {
250
+ ignored.push({
251
+ entry,
252
+ reason: `a pattern is honoured only for a tool that takes a command line, and \`${tool.name}\` does not; nothing was granted for it`,
253
+ })
254
+ continue
255
+ }
256
+
257
+ let pattern = specifier
258
+ // The legacy prefix spelling `npm run test:*` means `npm run test *`.
259
+ if (pattern.endsWith(':*')) pattern = `${pattern.slice(0, -2)} *`
260
+ if (SKILL_DIR_PLACEHOLDERS.some((placeholder) => pattern.includes(placeholder))) {
261
+ if (!options.skillDirectory) {
262
+ ignored.push({
263
+ entry,
264
+ reason: "the skill's directory is not known in this turn",
265
+ })
266
+ continue
267
+ }
268
+ for (const placeholder of SKILL_DIR_PLACEHOLDERS) {
269
+ pattern = pattern.replaceAll(placeholder, options.skillDirectory)
270
+ }
271
+ }
272
+ const source = permissionPatternToRegExpSource(pattern)
273
+ if (source.length > MAX_CUSTOM_PATTERN_LENGTH) {
274
+ ignored.push({ entry, reason: 'the pattern is too long to evaluate' })
275
+ continue
276
+ }
277
+ entries.push({
278
+ tool: tool.name,
279
+ argument: tool.commandArgument,
280
+ pattern: source,
281
+ declared: entry,
282
+ })
283
+ }
284
+
285
+ return { entries, ignored }
286
+ }
287
+
288
+ interface HeldGrant {
289
+ readonly skill: string
290
+ readonly entry: SkillGrantEntry
291
+ readonly compiled?: RegExp
292
+ readonly names: Set<string>
293
+ }
294
+
295
+ /**
296
+ * The skill grants in force for one turn.
297
+ *
298
+ * Turn-scoped for the reason `ToolGrantSet` is: a grant is a statement about
299
+ * this turn's work. It is created with the turn and dropped with it, so it
300
+ * never reaches the next message, and a delegated child — which runs its own
301
+ * turn — starts with an empty one of its own rather than its parent's.
302
+ */
303
+ export class SkillGrantSet {
304
+ private readonly held: HeldGrant[] = []
305
+
306
+ /** Record a skill's compiled entries. Loading the same skill twice is harmless. */
307
+ grant(skill: string, compiled: CompiledSkillGrant): void {
308
+ for (const entry of compiled.entries) {
309
+ const duplicate = this.held.some(
310
+ (held) =>
311
+ held.skill === skill &&
312
+ held.entry.tool === entry.tool &&
313
+ held.entry.argument === entry.argument &&
314
+ held.entry.pattern === entry.pattern,
315
+ )
316
+ if (duplicate) continue
317
+ this.held.push({
318
+ skill,
319
+ entry,
320
+ ...(entry.pattern !== undefined ? { compiled: new RegExp(entry.pattern) } : {}),
321
+ names: new Set([entry.tool]),
322
+ })
323
+ }
324
+ }
325
+
326
+ /**
327
+ * The skill whose grant covers this call, or `undefined`.
328
+ *
329
+ * A pattern entry is matched the way an operator's `allow` pattern is: per
330
+ * command, every command in the line must match, and a line the reader
331
+ * cannot see through (a substitution, `eval`, a line that does not parse)
332
+ * matches nothing. So
333
+ * `Bash(git status *)` covers `git status -s` and not
334
+ * `git status && git push`.
335
+ *
336
+ * A pattern entry also never covers a line that redirects output into a
337
+ * file (`git status > ~/.bashrc`). The pattern names a command; where its
338
+ * output lands is not part of that command, and the shell's redirection
339
+ * writes wherever the line says without the tool reporting a path.
340
+ * `/dev/null` and descriptor duplication (`2>&1`) are not writes and stay
341
+ * covered. A whole-tool entry (`Bash`) grants the tool as it is, which
342
+ * includes redirection.
343
+ */
344
+ coveringSkill(
345
+ call: { readonly name: string; readonly input: unknown },
346
+ toolDef?: ToolDefinition,
347
+ options: { readonly commandDialect?: ShellDialect } = {},
348
+ ): string | undefined {
349
+ // Read the line for the shell that will run it; unknown is `sh`, the
350
+ // reading that holds for any POSIX shell.
351
+ const dialect = options.commandDialect ?? 'sh'
352
+ for (const held of this.held) {
353
+ if (held.entry.tool !== call.name) continue
354
+ if (held.entry.pattern === undefined || held.entry.argument === undefined) return held.skill
355
+ const line =
356
+ call.input !== null && typeof call.input === 'object'
357
+ ? (call.input as Record<string, unknown>)[held.entry.argument]
358
+ : undefined
359
+ if (typeof line !== 'string' || writesThroughRedirection(line, dialect)) continue
360
+ const decision = evaluateRule(
361
+ {
362
+ type: 'argument_pattern',
363
+ toolNames: [held.entry.tool],
364
+ argument: held.entry.argument,
365
+ pattern: held.entry.pattern,
366
+ decision: 'allow',
367
+ },
368
+ call.name,
369
+ call.input,
370
+ toolDef,
371
+ held.compiled,
372
+ held.names,
373
+ { commandDialect: dialect },
374
+ )
375
+ if (decision === 'allow') return held.skill
376
+ }
377
+ return undefined
378
+ }
379
+
380
+ /**
381
+ * Drop every grant. Called when the operator speaks again inside a running
382
+ * turn: the grant belonged to the request that loaded the skill, and a new
383
+ * message is a new request, even when it reaches the same `query()`.
384
+ */
385
+ clear(): void {
386
+ this.held.length = 0
387
+ }
388
+
389
+ get size(): number {
390
+ return this.held.length
391
+ }
392
+
393
+ /** What is granted, by skill, as the authors wrote it. */
394
+ list(): { readonly skill: string; readonly entry: string }[] {
395
+ return this.held.map((held) => ({
396
+ skill: held.skill,
397
+ entry: held.entry.declared,
398
+ }))
399
+ }
400
+ }
@@ -67,8 +67,11 @@ export function renderSkillsSection(skills?: Skill[]): string | null {
67
67
  if (s.metadata.license) {
68
68
  lines.push(`<license>${s.metadata.license}</license>`)
69
69
  }
70
+ // Named for what it does. Rendered as `<allowed_tools>` it read as a
71
+ // whitelist, and a model that took it that way stopped reaching for
72
+ // tools the skill did not list — the reverse of the field's meaning.
70
73
  if (s.metadata.allowedTools) {
71
- lines.push(`<allowed_tools>${s.metadata.allowedTools}</allowed_tools>`)
74
+ lines.push(`<pre_approved_tools>${s.metadata.allowedTools}</pre_approved_tools>`)
72
75
  }
73
76
  lines.push('</skill>')
74
77
  return lines.join('\n')
@@ -81,7 +84,7 @@ export function renderSkillsSection(skills?: Skill[]): string | null {
81
84
  // instructions and no listing that would let it reason about them.
82
85
  const loadedSkills = forModel.filter((s) => s.body)
83
86
  const sections = [
84
- `## Available Skills\nThe following block is a manifest, not the full skill content. Skill metadata is always visible; SKILL.md bodies are loaded only when the user's task matches the skill description.\n\n<available_skills>\n${available}\n</available_skills>\n\nSkill usage protocol:\n- Plain questions do not require a skill.\n- When a matching skill is already listed under Loaded Skills, apply its loaded instructions.\n- When a matching skill is not loaded and the runtime exposes filesystem or skill-loading tools, read the SKILL.md at its <location> before writing code, creating files, running shell commands for that workflow, or calling mutation tools guided by the skill.\n- Do not claim to have read a SKILL.md until its content is actually present in the prompt or returned by a tool.\n- Tool schemas and runtime permissions remain authoritative; skills provide guidance, not hidden tools.`,
87
+ `## Available Skills\nThe following block is a manifest, not the full skill content. Skill metadata is always visible; SKILL.md bodies are loaded only when the user's task matches the skill description.\n\n<available_skills>\n${available}\n</available_skills>\n\nSkill usage protocol:\n- Plain questions do not require a skill.\n- When a matching skill is already listed under Loaded Skills, apply its loaded instructions.\n- When a matching skill is not loaded and the runtime exposes filesystem or skill-loading tools, read the SKILL.md at its <location> before writing code, creating files, running shell commands for that workflow, or calling mutation tools guided by the skill.\n- Do not claim to have read a SKILL.md until its content is actually present in the prompt or returned by a tool.\n- Tool schemas and runtime permissions remain authoritative; skills provide guidance, not hidden tools.\n- <pre_approved_tools> lists tools a skill lets run without an approval prompt once it is loaded. It never limits which tools you may use: every tool stays available.`,
85
88
  ]
86
89
 
87
90
  if (loadedSkills.length > 0) {
@@ -103,7 +103,7 @@ When you have understood the task, reply with the plan: what you would change, i
103
103
  * doctrine did not already name.
104
104
  */
105
105
  export const CODING_AGENT_ORCHESTRATE_DOCTRINE = `### Orchestrate mode
106
- This session has orchestrate mode on: treat delegation through \`Agent\` as the default for substantive work, not the exception. Before doing multi-step work yourself, ask whether an independent piece of it — a lookup, a draft, a check — could run as its own delegation, and prefer delegating it when the answer is yes. This changes only how eagerly you reach for \`Agent\` on work you would otherwise do inline; it does not mount a roster or start any delegation by itself.`
106
+ This session has orchestrate mode on: treat delegation through \`Agent\` as the default for substantive work, not the exception. Before doing multi-step work yourself, ask whether an independent piece of it — a lookup, a draft, a check — could run as its own delegation, and prefer delegating it when the answer is yes. This changes only how eagerly you reach for \`Agent\` on work you would otherwise do inline; it does not mount a roster or start any delegation by itself. Agents in the same phase are launched in the same response, each with \`run_in_background: true\` when you mean to wait for them together; a phase is never started one agent at a time.`
107
107
 
108
108
  export const CODING_AGENT_DOCTRINE_CONTRIBUTION_ID = 'namzu.coding-agent-doctrine'
109
109
 
@@ -318,6 +318,15 @@ export { deriveTurnStatus } from './types/session/derive-status.js'
318
318
  // wide their yes is, instead of choosing between 'this one call' and
319
319
  // 'everything for the session'.
320
320
  export { ToolGrantSet, toolGrantKeys } from './runtime/query/tool-grants.js'
321
+ // A skill's `allowed-tools` as a turn-scoped pre-approval: the parser, the
322
+ // compiler a host can run against its own registry, the per-turn set, and
323
+ // the one permission-glob dialect the CLI's `[permissions]` table shares.
324
+ export {
325
+ SKILL_TOOL_NAME_ALIASES,
326
+ SkillGrantSet,
327
+ compileSkillGrant,
328
+ permissionPatternToRegExpSource,
329
+ } from './authorization/skill-grant.js'
321
330
  export type { ToolGrantKeys } from './runtime/query/tool-grants.js'
322
331
  // `toWireTurnStatus` comes through `./contracts/session/index.js`.
323
332
  // Durable turn state: the snapshot a different process picks a turn up from.
@@ -61,7 +61,8 @@ export { JobTool } from './tools/builtins/job.js'
61
61
  // the same reason `job` does: a job with nothing that can block on it is
62
62
  // the same unbacked suggestion `job` itself exists to fix.
63
63
  export { WaitForJobTool } from './tools/builtins/wait-for-job.js'
64
- // Loads a skill's instructions, and adopts the tool scope it declares.
64
+ // Loads a skill's instructions, and pre-approves what its `allowed-tools`
65
+ // names for the rest of the turn (never narrowing the tool set).
65
66
  // NOT in the default builtin set: a turn with no skills has nothing for it
66
67
  // to do, and offering a tool that can only refuse is worse than not
67
68
  // offering it. Hosts register it alongside a skills registry.
@@ -113,9 +113,15 @@ export type { CacheRates, ModelPricing } from './utils/cost.js'
113
113
  export type { VendorRates } from './pricing/index.js'
114
114
  export type { PricingSubject } from './manager/session/turn-recorder.js'
115
115
  export type {
116
+ FrontmatterOptions,
116
117
  FrontmatterValue,
117
118
  ParsedFrontmatter,
118
119
  } from './utils/frontmatter.js'
120
+ export type {
121
+ CompiledSkillGrant,
122
+ SkillGrantEntry,
123
+ SkillGrantToolResolver,
124
+ } from './authorization/skill-grant.js'
119
125
  export type { Logger } from './utils/logger.js'
120
126
  export type {
121
127
  LevelFilter,
@@ -255,6 +261,7 @@ export type { AgentBusConfig } from './bus/index.js'
255
261
 
256
262
  export type { ToolCallContext } from './authorization/index.js'
257
263
  export type { PermissionPreset } from './authorization/index.js'
264
+ export type { EvaluateRuleOptions } from './authorization/rules.js'
258
265
 
259
266
  export type {
260
267
  DiskSessionStoreConfig,
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process'
2
2
 
3
3
  import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
4
4
  import { killTree } from '../../process/kill-tree.js'
5
+ import { hostShellSpawn } from '../../tools/command-shell.js'
5
6
  import { scrubInheritedEnv } from '../../tools/env-scrub.js'
6
7
  import { awaitWithAbort } from '../../utils/await-with-abort.js'
7
8
 
@@ -72,8 +73,8 @@ export interface StartJobParams {
72
73
  readonly env?: Readonly<Record<string, string>>
73
74
  /**
74
75
  * Start the process yourself — a sandbox does, so the job runs inside
75
- * its boundary. Absent, the registry runs `/bin/sh -c command` on the
76
- * host. The process must be the leader of its own group and must not
76
+ * its boundary. Absent, the registry runs it on the
77
+ * host in the `bash` tool's shell (`tools/command-shell.ts`). The process must be the leader of its own group and must not
77
78
  * expect stdin.
78
79
  */
79
80
  readonly spawn?: () => JobProcess
@@ -205,18 +206,25 @@ export class BackgroundJobRegistry {
205
206
  // started it, which makes the leak longer-lived, not smaller.
206
207
  const inherited = scrubInheritedEnv()
207
208
 
209
+ // The same shell as a foreground `bash` call, so the permission
210
+ // rules' reading of the line holds for the job too.
211
+ const shell = hostShellSpawn(params.command, { ...inherited.env, ...params.env })
208
212
  const started = params.spawn
209
213
  ? params.spawn()
210
214
  : {
211
- child: spawn('/bin/sh', ['-c', params.command], {
212
- cwd: params.workingDirectory,
213
- env: { ...inherited.env, ...params.env },
214
- // Leader of its own process group, which is what `killTree` needs
215
- // to reach the command and everything it forks rather than only the
216
- // wrapping shell. See `process/kill-tree.ts`.
217
- detached: process.platform !== 'win32',
218
- stdio: ['ignore', 'pipe', 'pipe'],
219
- }),
215
+ child: spawn(
216
+ shell.file ?? '/bin/sh',
217
+ shell.file === undefined ? ['-c', params.command] : [...shell.args],
218
+ {
219
+ cwd: params.workingDirectory,
220
+ env: shell.env,
221
+ // Leader of its own process group, which is what `killTree` needs
222
+ // to reach the command and everything it forks rather than only the
223
+ // wrapping shell. See `process/kill-tree.ts`.
224
+ detached: process.platform !== 'win32',
225
+ stdio: ['ignore', 'pipe', 'pipe'],
226
+ },
227
+ ),
220
228
  }
221
229
  const child = started.child
222
230
  // Started, not adopted: this process stays the parent for the job's