@namzu/sdk 52.4.0 → 52.5.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 +27 -0
- package/dist/authorization/gate.d.ts +11 -0
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +35 -0
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/removal.js +11 -6
- package/dist/authorization/removal.js.map +1 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +30 -2
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-paths.d.ts +20 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -1
- package/dist/authorization/shell-paths.js +254 -52
- package/dist/authorization/shell-paths.js.map +1 -1
- package/dist/bridge/acp/server.d.ts +8 -0
- package/dist/bridge/acp/server.d.ts.map +1 -1
- package/dist/bridge/acp/server.js +7 -0
- package/dist/bridge/acp/server.js.map +1 -1
- package/dist/manager/resident/outbox.d.ts +4 -4
- package/dist/public-runtime.d.ts +2 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/classifier.d.ts +130 -0
- package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/classifier.js +244 -0
- package/dist/runtime/query/auto-review/classifier.js.map +1 -0
- package/dist/runtime/query/auto-review/index.d.ts +7 -0
- package/dist/runtime/query/auto-review/index.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/index.js +4 -0
- package/dist/runtime/query/auto-review/index.js.map +1 -0
- package/dist/runtime/query/auto-review/prompt.d.ts +20 -0
- package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/prompt.js +89 -0
- package/dist/runtime/query/auto-review/prompt.js.map +1 -0
- package/dist/runtime/query/auto-review/rules.d.ts +49 -0
- package/dist/runtime/query/auto-review/rules.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/rules.js +81 -0
- package/dist/runtime/query/auto-review/rules.js.map +1 -0
- package/dist/runtime/query/auto-review/transcript.d.ts +59 -0
- package/dist/runtime/query/auto-review/transcript.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/transcript.js +116 -0
- package/dist/runtime/query/auto-review/transcript.js.map +1 -0
- package/dist/runtime/query/auto-review/verdict.d.ts +21 -0
- package/dist/runtime/query/auto-review/verdict.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/verdict.js +45 -0
- package/dist/runtime/query/auto-review/verdict.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +7 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +26 -2
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.js +2 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +0 -9
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +11 -2
- package/dist/runtime/query/iteration/phases/context.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 +39 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +43 -10
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +14 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +88 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +504 -11
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +2 -0
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/classifier-input.d.ts +9 -0
- package/dist/tools/builtins/classifier-input.d.ts.map +1 -0
- package/dist/tools/builtins/classifier-input.js +20 -0
- package/dist/tools/builtins/classifier-input.js.map +1 -0
- package/dist/tools/builtins/code-analysis.d.ts +2 -0
- package/dist/tools/builtins/code-analysis.d.ts.map +1 -1
- package/dist/tools/builtins/code-analysis.js +196 -119
- package/dist/tools/builtins/code-analysis.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +10 -10
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +12 -0
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/monitor.d.ts.map +1 -1
- package/dist/tools/builtins/monitor.js +3 -0
- package/dist/tools/builtins/monitor.js.map +1 -1
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +3 -0
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +6 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +1 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/types/acp/index.d.ts +7 -0
- package/dist/types/acp/index.d.ts.map +1 -1
- package/dist/types/code-analysis/index.d.ts +54 -3
- package/dist/types/code-analysis/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +45 -1
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +16 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +1 -1
- package/src/authorization/gate.ts +35 -0
- package/src/authorization/removal.ts +12 -6
- package/src/authorization/rules.ts +27 -2
- package/src/authorization/shell-paths.ts +264 -51
- package/src/bridge/acp/server.ts +20 -0
- package/src/public-runtime.ts +18 -0
- package/src/public-types.ts +11 -0
- package/src/runtime/query/auto-review/classifier.ts +404 -0
- package/src/runtime/query/auto-review/index.ts +23 -0
- package/src/runtime/query/auto-review/prompt.ts +97 -0
- package/src/runtime/query/auto-review/rules.ts +106 -0
- package/src/runtime/query/auto-review/transcript.ts +159 -0
- package/src/runtime/query/auto-review/verdict.ts +50 -0
- package/src/runtime/query/executor.ts +29 -2
- package/src/runtime/query/index.ts +2 -2
- package/src/runtime/query/iteration/phases/context.ts +17 -3
- package/src/runtime/query/iteration/phases/tool-review.ts +41 -0
- package/src/runtime/query/path-escalation.ts +44 -9
- package/src/runtime/query/resume-pending.ts +14 -0
- package/src/runtime/query/review-policy.ts +591 -14
- package/src/tools/builtins/bash.ts +3 -0
- package/src/tools/builtins/classifier-input.ts +19 -0
- package/src/tools/builtins/code-analysis.ts +211 -119
- package/src/tools/builtins/edit.ts +13 -0
- package/src/tools/builtins/monitor.ts +4 -0
- package/src/tools/builtins/web.ts +3 -0
- package/src/tools/builtins/write-file.ts +6 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/types/acp/index.ts +7 -0
- package/src/types/code-analysis/index.ts +51 -3
- package/src/types/hitl/index.ts +49 -1
- package/src/types/tool/index.ts +17 -0
|
@@ -21,10 +21,18 @@
|
|
|
21
21
|
* person (`prompt`).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { unknownProgramInLine } from '../../authorization/program.js'
|
|
25
|
+
import type { ShellWord } from '../../authorization/shell-lexer.js'
|
|
26
|
+
import {
|
|
27
|
+
type ShellInvocation,
|
|
28
|
+
readShellLine,
|
|
29
|
+
shellPathAccess,
|
|
30
|
+
} from '../../authorization/shell-paths.js'
|
|
24
31
|
import { isTrustedReadOnly } from '../../tools/trusted-read-only.js'
|
|
25
32
|
import type { ToolManager } from '../../toolsets/manager.js'
|
|
26
33
|
import type {
|
|
27
34
|
HITLResumeDecision,
|
|
35
|
+
ResumeContext,
|
|
28
36
|
ResumeHandler,
|
|
29
37
|
ToolCallDeclined,
|
|
30
38
|
ToolCallSummary,
|
|
@@ -32,7 +40,9 @@ import type {
|
|
|
32
40
|
import type { ApprovalPolicy } from '../../types/hitl/policy.js'
|
|
33
41
|
import type { SessionId, TurnId } from '../../types/ids/index.js'
|
|
34
42
|
import { PLAN_MODE_REFUSAL } from '../../types/permission/index.js'
|
|
43
|
+
import { type AutoReviewClassifier, autoReviewBlockedFeedback } from './auto-review/classifier.js'
|
|
35
44
|
import { DECLINED_TOOL_CALL_FEEDBACK } from './declined.js'
|
|
45
|
+
import { shellEscalations } from './path-escalation.js'
|
|
36
46
|
|
|
37
47
|
export type ReviewMode =
|
|
38
48
|
/** Ask a person. The default when a `prompt` is supplied. */
|
|
@@ -68,6 +78,32 @@ export type ReviewMode =
|
|
|
68
78
|
* an unattended turn can actually be reasoned about.
|
|
69
79
|
*/
|
|
70
80
|
| 'strict'
|
|
81
|
+
/**
|
|
82
|
+
* Run everything except what no mode may approve in advance.
|
|
83
|
+
*
|
|
84
|
+
* Where `auto` approves what remains after the questions a person must
|
|
85
|
+
* answer (a path outside the roots, a program nobody can name, a protected
|
|
86
|
+
* write), `bypass` also approves those. It still asks, or refuses when
|
|
87
|
+
* nobody can be asked: a `deny` rule (the gate refuses before this is
|
|
88
|
+
* reached), an `ask` rule or a hook that asked for a person, a catastrophic
|
|
89
|
+
* removal, a tool's own `requiresApproval`, a sandbox escape, and a call
|
|
90
|
+
* whose safety checks did not finish. The dangerous-pattern floor refuses in
|
|
91
|
+
* the gate, unchanged. With nobody to ask, whatever would have been asked is
|
|
92
|
+
* refused.
|
|
93
|
+
*/
|
|
94
|
+
| 'bypass'
|
|
95
|
+
/**
|
|
96
|
+
* Let a classifier model decide what a person would otherwise be asked.
|
|
97
|
+
*
|
|
98
|
+
* Needs a {@link ReviewPolicyOptions.classifier}. A call that `prompt` would
|
|
99
|
+
* ask about goes to it instead, and it allows or blocks; the questions that
|
|
100
|
+
* are a person's to answer (the same set `bypass` keeps) still go to one,
|
|
101
|
+
* or are refused when nobody can be asked. Calls that would not be asked
|
|
102
|
+
* about in `prompt`, calls `accept-edits` would approve, and the tools on
|
|
103
|
+
* {@link CLASSIFIER_SAFE_TOOLS} never reach it. See
|
|
104
|
+
* `runtime/query/auto-review/`.
|
|
105
|
+
*/
|
|
106
|
+
| 'classify'
|
|
71
107
|
|
|
72
108
|
export const REVIEW_MODES: readonly ReviewMode[] = [
|
|
73
109
|
'prompt',
|
|
@@ -75,6 +111,8 @@ export const REVIEW_MODES: readonly ReviewMode[] = [
|
|
|
75
111
|
'auto',
|
|
76
112
|
'strict',
|
|
77
113
|
'plan',
|
|
114
|
+
'bypass',
|
|
115
|
+
'classify',
|
|
78
116
|
]
|
|
79
117
|
|
|
80
118
|
export function isReviewMode(value: unknown): value is ReviewMode {
|
|
@@ -248,6 +286,219 @@ export const PROTECTED_PATH_UNATTENDED_REFUSAL =
|
|
|
248
286
|
export const DANGEROUS_REMOVAL_UNATTENDED_REFUSAL =
|
|
249
287
|
'Refused: a call in this batch removes something this check treats as catastrophic (the file system root, a top-level or home directory, the working directory or a parent of it, a glob across directories, or a target a variable or substitution may leave empty), which needs a person to approve it each time, and nobody can be asked in this session. Nothing in this batch ran. Name the exact directory to remove with a literal path.'
|
|
250
288
|
|
|
289
|
+
/**
|
|
290
|
+
* What the model is told when a batch carries a call a rule or a hook asked a
|
|
291
|
+
* person to approve and nobody can be asked.
|
|
292
|
+
*/
|
|
293
|
+
export const ASK_RULE_UNATTENDED_REFUSAL =
|
|
294
|
+
'Refused: a call in this batch is one a rule or a hook says a person must approve, and nobody can be asked in this session. Nothing in this batch ran. Run it in a session where a person can be asked, or have the operator change the rule.'
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* What the model is told when the checks that decide whether a call reaches
|
|
298
|
+
* past its boundary did not finish and nobody can be asked.
|
|
299
|
+
*/
|
|
300
|
+
export const UNFINISHED_CHECK_UNATTENDED_REFUSAL =
|
|
301
|
+
'Refused: the safety checks on a call in this batch did not finish, so it cannot be shown to be safe, and a person has to approve it and nobody can be asked in this session. Nothing in this batch ran. Try a simpler form of the call.'
|
|
302
|
+
|
|
303
|
+
/** What the model is told when the conversation is too long for the classifier to read. */
|
|
304
|
+
export const CLASSIFIER_TRANSCRIPT_TOO_LONG_FEEDBACK =
|
|
305
|
+
'Blocked by auto mode: the conversation has become too long for the safety check to read, so this action was not run. The conversation is being shortened; try the action again.'
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Tools the classifier is never asked about: they read, plan or keep the
|
|
309
|
+
* agent's own notes, and cannot change anything outside the session.
|
|
310
|
+
*/
|
|
311
|
+
export const CLASSIFIER_SAFE_TOOLS: ReadonlySet<string> = new Set([
|
|
312
|
+
'read',
|
|
313
|
+
'grep',
|
|
314
|
+
'glob',
|
|
315
|
+
'ls',
|
|
316
|
+
'lsp',
|
|
317
|
+
'code_analysis',
|
|
318
|
+
'search_tools',
|
|
319
|
+
'view_image',
|
|
320
|
+
'task_create',
|
|
321
|
+
'task_update',
|
|
322
|
+
'task_list',
|
|
323
|
+
'agent_task_list',
|
|
324
|
+
'get_goal',
|
|
325
|
+
'update_goal',
|
|
326
|
+
'exit_plan_mode',
|
|
327
|
+
])
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Programs an allow rule must not stand in for the classifier on: each runs
|
|
331
|
+
* whatever it is handed, so a rule that let `python` through would approve any
|
|
332
|
+
* script without the classifier reading it. Matched on the program's base name
|
|
333
|
+
* with any `.exe` removed, so `/usr/bin/python3` and `Python3.11` count.
|
|
334
|
+
*/
|
|
335
|
+
const CLASSIFIER_LAUNCHERS: ReadonlySet<string> = new Set([
|
|
336
|
+
'node',
|
|
337
|
+
'nodejs',
|
|
338
|
+
'deno',
|
|
339
|
+
'tsx',
|
|
340
|
+
'ts-node',
|
|
341
|
+
'ruby',
|
|
342
|
+
'perl',
|
|
343
|
+
'php',
|
|
344
|
+
'lua',
|
|
345
|
+
'rscript',
|
|
346
|
+
'julia',
|
|
347
|
+
'osascript',
|
|
348
|
+
'npx',
|
|
349
|
+
'bunx',
|
|
350
|
+
'uvx',
|
|
351
|
+
'pipx',
|
|
352
|
+
'bash',
|
|
353
|
+
'sh',
|
|
354
|
+
'dash',
|
|
355
|
+
'ash',
|
|
356
|
+
'ksh',
|
|
357
|
+
'zsh',
|
|
358
|
+
'fish',
|
|
359
|
+
'csh',
|
|
360
|
+
'tcsh',
|
|
361
|
+
'pwsh',
|
|
362
|
+
'powershell',
|
|
363
|
+
'cmd',
|
|
364
|
+
'busybox',
|
|
365
|
+
'ssh',
|
|
366
|
+
'eval',
|
|
367
|
+
'exec',
|
|
368
|
+
'env',
|
|
369
|
+
'xargs',
|
|
370
|
+
'sudo',
|
|
371
|
+
'doas',
|
|
372
|
+
'make',
|
|
373
|
+
'gmake',
|
|
374
|
+
'awk',
|
|
375
|
+
'gawk',
|
|
376
|
+
'mawk',
|
|
377
|
+
'nawk',
|
|
378
|
+
])
|
|
379
|
+
|
|
380
|
+
/** Interpreters named by a pattern rather than a list: `python`, `python3`, `python3.11`. */
|
|
381
|
+
const CLASSIFIER_LAUNCHER_PATTERNS: readonly RegExp[] = [/^python[0-9.]*$/, /^pypy[0-9.]*$/]
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Package managers and what they run. `npm` starts a script only through
|
|
385
|
+
* these verbs; the others treat any verb that is not one of their own commands
|
|
386
|
+
* as a script name (`pnpm build`, `yarn test`, `bun app.ts`).
|
|
387
|
+
*/
|
|
388
|
+
const NPM_SCRIPT_VERBS: ReadonlySet<string> = new Set([
|
|
389
|
+
'run',
|
|
390
|
+
'run-script',
|
|
391
|
+
'rum',
|
|
392
|
+
'urn',
|
|
393
|
+
'test',
|
|
394
|
+
't',
|
|
395
|
+
'tst',
|
|
396
|
+
'start',
|
|
397
|
+
'stop',
|
|
398
|
+
'restart',
|
|
399
|
+
'exec',
|
|
400
|
+
'x',
|
|
401
|
+
'init',
|
|
402
|
+
'create',
|
|
403
|
+
'innit',
|
|
404
|
+
'explore',
|
|
405
|
+
])
|
|
406
|
+
const SCRIPT_RUNNERS_WITH_OWN_VERBS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|
407
|
+
[
|
|
408
|
+
'pnpm',
|
|
409
|
+
new Set(['install', 'i', 'add', 'remove', 'rm', 'uninstall', 'un', 'update', 'up', 'upgrade']),
|
|
410
|
+
],
|
|
411
|
+
['yarn', new Set(['install', 'add', 'remove', 'upgrade', 'up', 'info', 'why', 'list', 'config'])],
|
|
412
|
+
[
|
|
413
|
+
'bun',
|
|
414
|
+
new Set(['install', 'i', 'add', 'a', 'remove', 'rm', 'update', 'pm', 'outdated', 'info']),
|
|
415
|
+
],
|
|
416
|
+
])
|
|
417
|
+
/** Package-manager and tool flags whose value is the next word, so the verb is not that word. */
|
|
418
|
+
const FLAGS_WITH_A_VALUE: ReadonlySet<string> = new Set([
|
|
419
|
+
'-C',
|
|
420
|
+
'--dir',
|
|
421
|
+
'--cwd',
|
|
422
|
+
'--prefix',
|
|
423
|
+
'--filter',
|
|
424
|
+
'-F',
|
|
425
|
+
'-w',
|
|
426
|
+
'--workspace',
|
|
427
|
+
'--registry',
|
|
428
|
+
'--userconfig',
|
|
429
|
+
'--cache',
|
|
430
|
+
'--loglevel',
|
|
431
|
+
'--config',
|
|
432
|
+
'--cd',
|
|
433
|
+
])
|
|
434
|
+
|
|
435
|
+
/** The first word of `args` that is not an option or an option's value. */
|
|
436
|
+
function verbOf(args: readonly ShellWord[]): string | undefined {
|
|
437
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
438
|
+
const value = (args[i] as ShellWord).value
|
|
439
|
+
if (value === '--') return args[i + 1]?.value
|
|
440
|
+
if (!value.startsWith('-')) return value
|
|
441
|
+
if (!value.includes('=') && FLAGS_WITH_A_VALUE.has(value)) i += 1
|
|
442
|
+
}
|
|
443
|
+
return undefined
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Whether a program in a line runs code it is handed, so that an allow rule cannot stand in for a reviewer. */
|
|
447
|
+
function launchesCode(invocation: ShellInvocation): boolean {
|
|
448
|
+
const program = (invocation.program.split(/[\\/]/).pop() ?? invocation.program)
|
|
449
|
+
.toLowerCase()
|
|
450
|
+
.replace(/\.exe$/, '')
|
|
451
|
+
if (CLASSIFIER_LAUNCHERS.has(program)) return true
|
|
452
|
+
if (CLASSIFIER_LAUNCHER_PATTERNS.some((pattern) => pattern.test(program))) return true
|
|
453
|
+
const verb = verbOf(invocation.args)
|
|
454
|
+
switch (program) {
|
|
455
|
+
case 'npm':
|
|
456
|
+
return verb !== undefined && NPM_SCRIPT_VERBS.has(verb)
|
|
457
|
+
case 'pnpm':
|
|
458
|
+
case 'yarn':
|
|
459
|
+
case 'bun': {
|
|
460
|
+
// A bare `yarn` runs the install; anything else that is not the manager's
|
|
461
|
+
// own command is a script, or for bun a file.
|
|
462
|
+
if (verb === undefined) return false
|
|
463
|
+
return !(SCRIPT_RUNNERS_WITH_OWN_VERBS.get(program) as ReadonlySet<string>).has(verb)
|
|
464
|
+
}
|
|
465
|
+
case 'uv':
|
|
466
|
+
return verb === 'run' || verb === 'tool' || verb === 'x'
|
|
467
|
+
case 'poetry':
|
|
468
|
+
case 'pipenv':
|
|
469
|
+
case 'cargo':
|
|
470
|
+
case 'go':
|
|
471
|
+
return verb === 'run'
|
|
472
|
+
case 'git':
|
|
473
|
+
// `-c key=value` can name a program git then runs (core.pager, core.sshCommand).
|
|
474
|
+
return invocation.args.some((arg) => /^(?:-c|--config-env)/.test(arg.value))
|
|
475
|
+
case 'find':
|
|
476
|
+
return invocation.args.some((arg) =>
|
|
477
|
+
/^-(?:exec|execdir|ok|okdir|fprint|fprintf|fls)/.test(arg.value),
|
|
478
|
+
)
|
|
479
|
+
default:
|
|
480
|
+
return false
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Tools that start other agents; an allow rule for one is not consulted under `classify`. */
|
|
485
|
+
const CLASSIFIER_DELEGATION_TOOLS: ReadonlySet<string> = new Set([
|
|
486
|
+
'agent',
|
|
487
|
+
'create_task',
|
|
488
|
+
'continue_task',
|
|
489
|
+
])
|
|
490
|
+
|
|
491
|
+
/** The shell commands `accept-edits` also approves, when every path they name is inside the roots. */
|
|
492
|
+
export const ACCEPT_EDITS_SHELL_COMMANDS: ReadonlySet<string> = new Set([
|
|
493
|
+
'mkdir',
|
|
494
|
+
'touch',
|
|
495
|
+
'rm',
|
|
496
|
+
'rmdir',
|
|
497
|
+
'mv',
|
|
498
|
+
'cp',
|
|
499
|
+
'sed',
|
|
500
|
+
])
|
|
501
|
+
|
|
251
502
|
/**
|
|
252
503
|
* What the model is told when a batch would show it the operator's screen for
|
|
253
504
|
* the first time in a session and nobody can be asked.
|
|
@@ -270,19 +521,132 @@ export interface ScreenConsentRecord {
|
|
|
270
521
|
readonly sessions: Set<string>
|
|
271
522
|
}
|
|
272
523
|
|
|
273
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* Whether the call writes a protected path, removes something catastrophic, or
|
|
526
|
+
* could not be checked.
|
|
527
|
+
*/
|
|
274
528
|
function reachesAFloor(tc: ToolCallSummary): boolean {
|
|
275
529
|
return (
|
|
276
530
|
(tc.escalation?.protectedPaths?.length ?? 0) > 0 ||
|
|
277
|
-
tc.escalation?.dangerousRemoval !== undefined
|
|
531
|
+
tc.escalation?.dangerousRemoval !== undefined ||
|
|
532
|
+
tc.escalation?.unfinishedCheck !== undefined
|
|
278
533
|
)
|
|
279
534
|
}
|
|
280
535
|
|
|
281
536
|
/** The refusal for a batch that reaches a floor and cannot be asked about it. */
|
|
282
537
|
function floorRefusal(toolCalls: readonly ToolCallSummary[]): string {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
538
|
+
if (toolCalls.some((tc) => (tc.escalation?.protectedPaths?.length ?? 0) > 0)) {
|
|
539
|
+
return PROTECTED_PATH_UNATTENDED_REFUSAL
|
|
540
|
+
}
|
|
541
|
+
return toolCalls.some((tc) => tc.escalation?.dangerousRemoval !== undefined)
|
|
542
|
+
? DANGEROUS_REMOVAL_UNATTENDED_REFUSAL
|
|
543
|
+
: UNFINISHED_CHECK_UNATTENDED_REFUSAL
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Whether a mode needs a batch a rule already allowed to come to the policy.
|
|
548
|
+
*
|
|
549
|
+
* `plan` must refuse a change a rule would have let through, and `classify`
|
|
550
|
+
* must look at a call whose allow rule is too wide to vouch for it. A host
|
|
551
|
+
* answers its `reviewAllowedCalls` with this for the mode it is in.
|
|
552
|
+
*/
|
|
553
|
+
export function modeReviewsAllowedCalls(mode: ReviewMode): boolean {
|
|
554
|
+
return mode === 'plan' || mode === 'classify'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Whether the mode keeps only the questions a person must answer, and leaves the rest to itself. */
|
|
558
|
+
function isPermissive(mode: ReviewMode): boolean {
|
|
559
|
+
return mode === 'bypass' || mode === 'classify'
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Whether the call is one of the few no mode may settle in advance: a rule or
|
|
564
|
+
* hook asked for a person, a sandbox escape, a catastrophic removal, a check
|
|
565
|
+
* that did not finish, or a tool that always needs approval. The dangerous
|
|
566
|
+
* pattern floor and `deny` rules are not here: the gate refuses those first.
|
|
567
|
+
*/
|
|
568
|
+
function isBypassImmune(tc: ToolCallSummary): boolean {
|
|
569
|
+
return (
|
|
570
|
+
tc.authorization?.askRule === true ||
|
|
571
|
+
tc.escalation?.sandboxEscape === true ||
|
|
572
|
+
tc.escalation?.dangerousRemoval !== undefined ||
|
|
573
|
+
tc.escalation?.unfinishedCheck !== undefined ||
|
|
574
|
+
tc.requiresApproval === true
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** The refusal for the first immune thing in a batch nobody can be asked about, or undefined. */
|
|
579
|
+
function immuneRefusal(
|
|
580
|
+
toolCalls: readonly ToolCallSummary[],
|
|
581
|
+
escapeConfirmed: boolean,
|
|
582
|
+
): string | undefined {
|
|
583
|
+
if (!escapeConfirmed && toolCalls.some((tc) => tc.escalation?.sandboxEscape === true)) {
|
|
584
|
+
return SANDBOX_ESCAPE_UNATTENDED_REFUSAL
|
|
585
|
+
}
|
|
586
|
+
if (toolCalls.some((tc) => tc.escalation?.dangerousRemoval !== undefined)) {
|
|
587
|
+
return DANGEROUS_REMOVAL_UNATTENDED_REFUSAL
|
|
588
|
+
}
|
|
589
|
+
if (toolCalls.some((tc) => tc.escalation?.unfinishedCheck !== undefined)) {
|
|
590
|
+
return UNFINISHED_CHECK_UNATTENDED_REFUSAL
|
|
591
|
+
}
|
|
592
|
+
if (toolCalls.some((tc) => tc.requiresApproval === true)) {
|
|
593
|
+
return REQUIRES_APPROVAL_UNATTENDED_REFUSAL
|
|
594
|
+
}
|
|
595
|
+
if (toolCalls.some((tc) => tc.authorization?.askRule === true)) return ASK_RULE_UNATTENDED_REFUSAL
|
|
596
|
+
return undefined
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** One sed command a script may hold: a substitution with plain flags, or a delete, print or line number. */
|
|
600
|
+
const SAFE_SED_COMMAND =
|
|
601
|
+
/^\s*(?:\d+(?:,\d+)?|\$|\/[^/]*\/)?\s*(?:s(.)(?:(?!\1)[^\\]|\\.)*\1(?:(?!\1)[^\\]|\\.)*\1[gpiI0-9]*|[dpP=])\s*$/
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Whether a `sed` invocation can only edit text: its script is made of
|
|
605
|
+
* substitutions and simple line commands. A script with the `e`, `w` or `r`
|
|
606
|
+
* commands or flags can run a program or reach another file, and a script from
|
|
607
|
+
* a file cannot be read here, so those are not edits this vouches for.
|
|
608
|
+
*/
|
|
609
|
+
function sedOnlyEdits(
|
|
610
|
+
args: readonly { readonly value: string; readonly expands: boolean }[],
|
|
611
|
+
): boolean {
|
|
612
|
+
const scripts: string[] = []
|
|
613
|
+
let positional = 0
|
|
614
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
615
|
+
const arg = args[i] as { value: string; expands: boolean }
|
|
616
|
+
if (arg.expands) return false
|
|
617
|
+
const text = arg.value
|
|
618
|
+
if (text === '-e') {
|
|
619
|
+
const next = args[i + 1]
|
|
620
|
+
if (next === undefined || next.expands) return false
|
|
621
|
+
scripts.push(next.value)
|
|
622
|
+
i += 1
|
|
623
|
+
continue
|
|
624
|
+
}
|
|
625
|
+
if (text.startsWith('--expression=')) {
|
|
626
|
+
scripts.push(text.slice('--expression='.length))
|
|
627
|
+
continue
|
|
628
|
+
}
|
|
629
|
+
if (text === '-f' || text.startsWith('--file') || text === '--follow-symlinks') return false
|
|
630
|
+
if (text.startsWith('-')) {
|
|
631
|
+
if (
|
|
632
|
+
!/^-(?:[nErszi]+|i\S*|-in-place(?:=\S*)?|-quiet|-silent|-regexp-extended|-separate|-null-data)$/.test(
|
|
633
|
+
text,
|
|
634
|
+
)
|
|
635
|
+
) {
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
continue
|
|
639
|
+
}
|
|
640
|
+
positional += 1
|
|
641
|
+
if (positional === 1 && scripts.length === 0) scripts.push(text)
|
|
642
|
+
}
|
|
643
|
+
if (scripts.length === 0) return false
|
|
644
|
+
return scripts.every((script) =>
|
|
645
|
+
script
|
|
646
|
+
.split(/;|\n/)
|
|
647
|
+
.filter((part) => part.trim() !== '')
|
|
648
|
+
.every((part) => SAFE_SED_COMMAND.test(part)),
|
|
649
|
+
)
|
|
286
650
|
}
|
|
287
651
|
|
|
288
652
|
/** The batch a person is asked about. */
|
|
@@ -384,6 +748,39 @@ export interface ReviewPolicyOptions {
|
|
|
384
748
|
* other mode.
|
|
385
749
|
*/
|
|
386
750
|
readonly planModeWrite?: (call: ToolCallSummary) => boolean
|
|
751
|
+
/**
|
|
752
|
+
* The classifier `classify` mode asks. Required for that mode (building a
|
|
753
|
+
* `classify` policy without one throws) and ignored in every other.
|
|
754
|
+
*/
|
|
755
|
+
readonly classifier?: AutoReviewClassifier
|
|
756
|
+
/** Tool names added to {@link CLASSIFIER_SAFE_TOOLS} for `classify`: never sent to the classifier. */
|
|
757
|
+
readonly safeTools?: readonly string[]
|
|
758
|
+
/**
|
|
759
|
+
* The roots the turn works in, the working directory first and then the
|
|
760
|
+
* added directories (or a function that reads them now). `accept-edits`,
|
|
761
|
+
* and `classify` through it, approve `mkdir`, `touch`, `rm`, `rmdir`, `mv`,
|
|
762
|
+
* `cp` and `sed` only when every path the shell line names is inside them,
|
|
763
|
+
* and only when the host says what they are: without this those commands
|
|
764
|
+
* are asked about as before.
|
|
765
|
+
*/
|
|
766
|
+
readonly roots?: readonly string[] | (() => readonly string[])
|
|
767
|
+
/**
|
|
768
|
+
* Called when the classifier has blocked enough calls (see
|
|
769
|
+
* `AutoReviewLimits`) that the host should stop relying on it. The policy
|
|
770
|
+
* then asks the person about the call that tripped the limit, or ends the
|
|
771
|
+
* turn when there is nobody to ask. It does not change the mode: the host,
|
|
772
|
+
* which owns the mode and how it is shown, does that here.
|
|
773
|
+
*/
|
|
774
|
+
readonly onClassifierLimit?: (event: {
|
|
775
|
+
readonly limit: 'consecutive' | 'total'
|
|
776
|
+
readonly reason: string
|
|
777
|
+
}) => void
|
|
778
|
+
/**
|
|
779
|
+
* Called when the conversation was too long for the classifier to read, so
|
|
780
|
+
* a host can start a compaction. The call is blocked once with a note telling
|
|
781
|
+
* the model to try again.
|
|
782
|
+
*/
|
|
783
|
+
readonly onClassifierTranscriptTooLong?: () => void
|
|
387
784
|
}
|
|
388
785
|
|
|
389
786
|
/**
|
|
@@ -424,6 +821,17 @@ function isHookGranted(tc: ToolCallSummary): boolean {
|
|
|
424
821
|
export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHandler {
|
|
425
822
|
const { prompt, registry } = options
|
|
426
823
|
const mode: ReviewMode = options.mode ?? (prompt ? 'prompt' : 'auto')
|
|
824
|
+
if (mode === 'classify' && options.classifier === undefined) {
|
|
825
|
+
throw new TypeError(
|
|
826
|
+
'The classify review mode needs a classifier (ReviewPolicyOptions.classifier).',
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
const safeTools = new Set([...CLASSIFIER_SAFE_TOOLS, ...(options.safeTools ?? [])])
|
|
830
|
+
/** Set once the classifier has said the conversation is too long, until it judges something. */
|
|
831
|
+
let transcriptTooLong = false
|
|
832
|
+
const toolOf = (name: string) => registry?.get(name) ?? registry?.get(name.toLowerCase())
|
|
833
|
+
const rootsNow = (): readonly string[] =>
|
|
834
|
+
typeof options.roots === 'function' ? options.roots() : (options.roots ?? [])
|
|
427
835
|
const exempt: ReviewExemption =
|
|
428
836
|
options.exempt ??
|
|
429
837
|
(registry ? (name, input) => isReviewExempt(registry, name, input) : () => false)
|
|
@@ -436,7 +844,103 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
436
844
|
return tool?.capturesScreen?.(input) === true
|
|
437
845
|
}
|
|
438
846
|
: () => false)
|
|
439
|
-
|
|
847
|
+
/**
|
|
848
|
+
* Whether a shell call only makes, touches, removes, moves, copies or edits
|
|
849
|
+
* files, and every path it names is inside the roots: what `accept-edits`
|
|
850
|
+
* approves beyond `edit` and `write`. A catastrophic removal, a protected
|
|
851
|
+
* path, a path in a variable, a program nobody can name or a `sed` script
|
|
852
|
+
* that can run something is not that.
|
|
853
|
+
*/
|
|
854
|
+
const shellFileCommandsInRoots = async (tc: ToolCallSummary): Promise<boolean> => {
|
|
855
|
+
const rootList = rootsNow()
|
|
856
|
+
if (rootList.length === 0) return false
|
|
857
|
+
if (
|
|
858
|
+
tc.escalation !== undefined ||
|
|
859
|
+
tc.isDestructive ||
|
|
860
|
+
tc.requiresApproval === true ||
|
|
861
|
+
tc.authorization?.explicitReview ||
|
|
862
|
+
tc.authorization?.askRule
|
|
863
|
+
) {
|
|
864
|
+
return false
|
|
865
|
+
}
|
|
866
|
+
const tool = toolOf(tc.name)
|
|
867
|
+
const argument = tool?.commandArgument
|
|
868
|
+
if (tool === undefined || argument === undefined) return false
|
|
869
|
+
const line = (tc.input as Record<string, unknown> | null)?.[argument]
|
|
870
|
+
if (typeof line !== 'string') return false
|
|
871
|
+
const dialect = tool.commandDialect?.({ sandboxed: false }) ?? 'sh'
|
|
872
|
+
if (unknownProgramInLine(line, dialect) !== undefined) return false
|
|
873
|
+
const reading = readShellLine(line, dialect)
|
|
874
|
+
if (!reading.complete || reading.invocations.length === 0) return false
|
|
875
|
+
for (const invocation of reading.invocations) {
|
|
876
|
+
if (invocation.program === 'cd') continue
|
|
877
|
+
if (!ACCEPT_EDITS_SHELL_COMMANDS.has(invocation.program)) return false
|
|
878
|
+
if (invocation.program === 'sed' && !sedOnlyEdits(invocation.args)) return false
|
|
879
|
+
}
|
|
880
|
+
if (shellPathAccess(reading).uses.some((use) => use.unresolved)) return false
|
|
881
|
+
const found = await shellEscalations(line, dialect, { roots: rootList, reviewOutside: true })
|
|
882
|
+
return (
|
|
883
|
+
found.outsidePaths.length === 0 &&
|
|
884
|
+
found.protectedPaths.length === 0 &&
|
|
885
|
+
found.dangerousRemoval === undefined
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** What `accept-edits` approves without asking: edits, exempt reads and file commands inside the roots. */
|
|
890
|
+
const acceptEditsAllows = async (tc: ToolCallSummary): Promise<boolean> =>
|
|
891
|
+
!tc.authorization?.explicitReview &&
|
|
892
|
+
tc.escalation === undefined &&
|
|
893
|
+
!tc.isDestructive &&
|
|
894
|
+
tc.requiresApproval !== true &&
|
|
895
|
+
(ACCEPT_EDITS_TOOLS.has(tc.name) ||
|
|
896
|
+
exempt(tc.name, tc.input) ||
|
|
897
|
+
(await shellFileCommandsInRoots(tc)))
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Whether a rule's `allow` is one the classifier should not defer to: the
|
|
901
|
+
* rule covers a whole shell tool, or the line starts an interpreter or a
|
|
902
|
+
* launcher that runs whatever it is given, or the tool starts other agents.
|
|
903
|
+
* Judged on the call as well as the rule because a rule that names a
|
|
904
|
+
* program ("python") is a rule about a program that can run anything.
|
|
905
|
+
*/
|
|
906
|
+
const allowRuleTooWide = (tc: ToolCallSummary): boolean => {
|
|
907
|
+
if (CLASSIFIER_DELEGATION_TOOLS.has(tc.name.toLowerCase())) return true
|
|
908
|
+
const tool = toolOf(tc.name)
|
|
909
|
+
const argument = tool?.commandArgument
|
|
910
|
+
if (tool === undefined || argument === undefined) return false
|
|
911
|
+
if (tc.authorization?.allowScope === 'tool') return true
|
|
912
|
+
const line = (tc.input as Record<string, unknown> | null)?.[argument]
|
|
913
|
+
if (typeof line !== 'string') return true
|
|
914
|
+
const reading = readShellLine(line, tool.commandDialect?.({ sandboxed: false }) ?? 'sh')
|
|
915
|
+
return !reading.complete || reading.invocations.some(launchesCode)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** Whether `classify` decides this call without the classifier: it would not be asked, or is plainly safe. */
|
|
919
|
+
const neverSent = async (tc: ToolCallSummary): Promise<boolean> => {
|
|
920
|
+
if (tc.authorization?.decision === 'allow' && !allowRuleTooWide(tc)) return true
|
|
921
|
+
// A name on the safe list is safe for what it does in the session; a read of
|
|
922
|
+
// /etc/shadow is still a read of /etc/shadow, and a person's own ask is
|
|
923
|
+
// still a person's ask. Anything that reaches past the boundary, or that a
|
|
924
|
+
// rule or hook put to a person, is not "plainly safe".
|
|
925
|
+
if (
|
|
926
|
+
safeTools.has(tc.name.toLowerCase()) &&
|
|
927
|
+
tc.escalation === undefined &&
|
|
928
|
+
!tc.authorization?.explicitReview
|
|
929
|
+
) {
|
|
930
|
+
return true
|
|
931
|
+
}
|
|
932
|
+
if (
|
|
933
|
+
tc.escalation === undefined &&
|
|
934
|
+
!tc.isDestructive &&
|
|
935
|
+
!tc.authorization?.explicitReview &&
|
|
936
|
+
exempt(tc.name, tc.input)
|
|
937
|
+
) {
|
|
938
|
+
return true
|
|
939
|
+
}
|
|
940
|
+
return acceptEditsAllows(tc)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return async (request, context?: ResumeContext): Promise<HITLResumeDecision> => {
|
|
440
944
|
if (request.type !== 'tool_review') {
|
|
441
945
|
return request.type === 'plan_approval' ? { action: 'approve_plan' } : { action: 'continue' }
|
|
442
946
|
}
|
|
@@ -503,14 +1007,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
503
1007
|
// through beside it would be approving something it did not show.
|
|
504
1008
|
if (
|
|
505
1009
|
mode === 'accept-edits' &&
|
|
506
|
-
request.toolCalls.every(
|
|
507
|
-
(tc) =>
|
|
508
|
-
!tc.authorization?.explicitReview &&
|
|
509
|
-
tc.escalation === undefined &&
|
|
510
|
-
!tc.isDestructive &&
|
|
511
|
-
tc.requiresApproval !== true &&
|
|
512
|
-
(ACCEPT_EDITS_TOOLS.has(tc.name) || exempt(tc.name, tc.input)),
|
|
513
|
-
)
|
|
1010
|
+
(await Promise.all(request.toolCalls.map(acceptEditsAllows))).every(Boolean)
|
|
514
1011
|
) {
|
|
515
1012
|
return { action: 'approve_tools' }
|
|
516
1013
|
}
|
|
@@ -531,6 +1028,86 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
531
1028
|
return { action: 'reject_tools', feedback: PLAN_MODE_REFUSAL }
|
|
532
1029
|
}
|
|
533
1030
|
if (mode === 'strict') return { action: 'reject_tools', feedback: STRICT_MODE_REFUSAL }
|
|
1031
|
+
if (isPermissive(mode)) {
|
|
1032
|
+
// A person's answer on the whole batch, as a decision. An escape is
|
|
1033
|
+
// confirmed by id only here, only for a person's yes.
|
|
1034
|
+
const askPerson = async (escapes: readonly string[]): Promise<HITLResumeDecision> => {
|
|
1035
|
+
const answer = await ask()
|
|
1036
|
+
if (answer.kind === 'reject') {
|
|
1037
|
+
return {
|
|
1038
|
+
action: 'reject_tools',
|
|
1039
|
+
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1040
|
+
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (answer.kind === 'approve-all') remembered.all = true
|
|
1044
|
+
return {
|
|
1045
|
+
action: 'approve_tools',
|
|
1046
|
+
...(escapes.length > 0 ? { confirmedEscalations: escapes } : {}),
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const live = request.toolCalls.filter((tc) => tc.authorization?.decision !== 'deny')
|
|
1050
|
+
const escapes = live.filter((tc) => tc.escalation?.sandboxEscape === true).map((tc) => tc.id)
|
|
1051
|
+
if (mode === 'classify') {
|
|
1052
|
+
const classifier = options.classifier as AutoReviewClassifier
|
|
1053
|
+
const transcript = context?.transcript() ?? []
|
|
1054
|
+
for (const tc of live) {
|
|
1055
|
+
// The questions a person must answer are asked of a person, never of a model.
|
|
1056
|
+
if (isBypassImmune(tc) || (await neverSent(tc))) continue
|
|
1057
|
+
const decision = await classifier.review({
|
|
1058
|
+
call: tc,
|
|
1059
|
+
transcript,
|
|
1060
|
+
...(registry ? { tools: registry } : {}),
|
|
1061
|
+
...(context?.signal ? { signal: context.signal } : {}),
|
|
1062
|
+
})
|
|
1063
|
+
if (decision.verdict === 'allow') {
|
|
1064
|
+
transcriptTooLong = false
|
|
1065
|
+
continue
|
|
1066
|
+
}
|
|
1067
|
+
if (decision.verdict === 'too_long') {
|
|
1068
|
+
if (!transcriptTooLong) {
|
|
1069
|
+
transcriptTooLong = true
|
|
1070
|
+
options.onClassifierTranscriptTooLong?.()
|
|
1071
|
+
return { action: 'reject_tools', feedback: CLASSIFIER_TRANSCRIPT_TOO_LONG_FEEDBACK }
|
|
1072
|
+
}
|
|
1073
|
+
// Still too long after the host was told: nobody can judge it
|
|
1074
|
+
// automatically. A person decides, or the turn ends.
|
|
1075
|
+
if (!prompt) {
|
|
1076
|
+
return {
|
|
1077
|
+
action: 'abort',
|
|
1078
|
+
reason: 'auto mode stopped: the conversation is too long for the safety check',
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return askPerson(escapes)
|
|
1082
|
+
}
|
|
1083
|
+
transcriptTooLong = false
|
|
1084
|
+
if (decision.limitReached !== undefined) {
|
|
1085
|
+
options.onClassifierLimit?.({ limit: decision.limitReached, reason: decision.reason })
|
|
1086
|
+
if (!prompt) {
|
|
1087
|
+
return {
|
|
1088
|
+
action: 'abort',
|
|
1089
|
+
reason: `auto mode stopped: too many actions were blocked (${decision.reason})`,
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return askPerson(escapes)
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
action: 'reject_tools',
|
|
1096
|
+
feedback: autoReviewBlockedFeedback(decision.reason),
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (!live.some(isBypassImmune)) return { action: 'approve_tools' }
|
|
1101
|
+
if (!prompt) {
|
|
1102
|
+
const refusal = immuneRefusal(live, options.unattendedSandboxEscape === 'allow')
|
|
1103
|
+
if (refusal !== undefined) return { action: 'reject_tools', feedback: refusal }
|
|
1104
|
+
return {
|
|
1105
|
+
action: 'approve_tools',
|
|
1106
|
+
...(escapes.length > 0 ? { confirmedEscalations: escapes } : {}),
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return askPerson(escapes)
|
|
1110
|
+
}
|
|
534
1111
|
// A sandbox escape is asked about every time, in every mode that got
|
|
535
1112
|
// this far — `auto` and a remembered "approve all" included, because
|
|
536
1113
|
// both are answers somebody gave BEFORE this command existed. The
|
|
@@ -478,6 +478,9 @@ const BashToolBase = defineTool({
|
|
|
478
478
|
// so a permission rule about it is a rule about several commands more often
|
|
479
479
|
// than not. Naming the argument is what lets the gate read it that way.
|
|
480
480
|
commandArgument: 'command',
|
|
481
|
+
// The classifier judges the command line, not the surrounding options.
|
|
482
|
+
classifierInput: (input: BashInput) =>
|
|
483
|
+
`${input.dangerously_disable_sandbox === true ? '[outside the sandbox] ' : ''}${input.command}`,
|
|
481
484
|
// The shell that runs it, so the rules read the line as that shell will.
|
|
482
485
|
commandDialect: bashToolDialect,
|
|
483
486
|
// The kernel reviews a call that sets it under a sandbox every time and
|