@namzu/sdk 44.3.0 → 45.1.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 +94 -0
- package/dist/authorization/command-line.d.ts +66 -19
- package/dist/authorization/command-line.d.ts.map +1 -1
- package/dist/authorization/command-line.js +130 -270
- package/dist/authorization/command-line.js.map +1 -1
- package/dist/authorization/gate.d.ts +7 -0
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +13 -3
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/rules.d.ts +10 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +55 -8
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-lexer.d.ts +152 -0
- package/dist/authorization/shell-lexer.d.ts.map +1 -0
- package/dist/authorization/shell-lexer.js +2156 -0
- package/dist/authorization/shell-lexer.js.map +1 -0
- package/dist/authorization/skill-grant.d.ts +182 -0
- package/dist/authorization/skill-grant.d.ts.map +1 -0
- package/dist/authorization/skill-grant.js +314 -0
- package/dist/authorization/skill-grant.js.map +1 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/directory/types.d.ts +2 -0
- package/dist/directory/types.d.ts.map +1 -1
- package/dist/directory/types.js.map +1 -1
- package/dist/manager/resident/outbox.d.ts +4 -4
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +5 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +1 -0
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +15 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +4 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +12 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +9 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +2 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +6 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/declined.d.ts +12 -0
- package/dist/runtime/query/declined.d.ts.map +1 -0
- package/dist/runtime/query/declined.js +12 -0
- package/dist/runtime/query/declined.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +36 -40
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +95 -53
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +8 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +13 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +13 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/handoff.d.ts +22 -0
- package/dist/runtime/query/iteration/phases/handoff.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/handoff.js +65 -0
- package/dist/runtime/query/iteration/phases/handoff.js.map +1 -0
- package/dist/runtime/query/iteration/phases/index.d.ts +1 -0
- package/dist/runtime/query/iteration/phases/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/index.js +1 -0
- package/dist/runtime/query/iteration/phases/index.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 +56 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +3 -2
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +11 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +36 -3
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +3 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/schedules/cron.d.ts +21 -0
- package/dist/schedules/cron.d.ts.map +1 -0
- package/dist/schedules/cron.js +167 -0
- package/dist/schedules/cron.js.map +1 -0
- package/dist/schedules/describe.d.ts +14 -0
- package/dist/schedules/describe.d.ts.map +1 -0
- package/dist/schedules/describe.js +133 -0
- package/dist/schedules/describe.js.map +1 -0
- package/dist/schedules/errors.d.ts +11 -0
- package/dist/schedules/errors.d.ts.map +1 -0
- package/dist/schedules/errors.js +15 -0
- package/dist/schedules/errors.js.map +1 -0
- package/dist/schedules/evaluate.d.ts +35 -0
- package/dist/schedules/evaluate.d.ts.map +1 -0
- package/dist/schedules/evaluate.js +158 -0
- package/dist/schedules/evaluate.js.map +1 -0
- package/dist/schedules/index.d.ts +11 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +8 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/schedules/next-fire.d.ts +50 -0
- package/dist/schedules/next-fire.d.ts.map +1 -0
- package/dist/schedules/next-fire.js +250 -0
- package/dist/schedules/next-fire.js.map +1 -0
- package/dist/schedules/spec.d.ts +30 -0
- package/dist/schedules/spec.d.ts.map +1 -0
- package/dist/schedules/spec.js +169 -0
- package/dist/schedules/spec.js.map +1 -0
- package/dist/schedules/types.d.ts +144 -0
- package/dist/schedules/types.d.ts.map +1 -0
- package/dist/schedules/types.js +11 -0
- package/dist/schedules/types.js.map +1 -0
- package/dist/schedules/tz.d.ts +44 -0
- package/dist/schedules/tz.d.ts.map +1 -0
- package/dist/schedules/tz.js +141 -0
- package/dist/schedules/tz.js.map +1 -0
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +21 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +51 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +18 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/browser-url.d.ts +83 -0
- package/dist/tools/builtins/browser-url.d.ts.map +1 -0
- package/dist/tools/builtins/browser-url.js +240 -0
- package/dist/tools/builtins/browser-url.js.map +1 -0
- package/dist/tools/builtins/browser.d.ts +367 -0
- package/dist/tools/builtins/browser.d.ts.map +1 -0
- package/dist/tools/builtins/browser.js +704 -0
- package/dist/tools/builtins/browser.js.map +1 -0
- package/dist/tools/builtins/skill.d.ts +2 -9
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +74 -51
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/command-shell.d.ts +90 -0
- package/dist/tools/command-shell.d.ts.map +1 -0
- package/dist/tools/command-shell.js +129 -0
- package/dist/tools/command-shell.js.map +1 -0
- package/dist/tools/defineTool.d.ts +13 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +30 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/schedules/index.d.ts +5 -0
- package/dist/tools/schedules/index.d.ts.map +1 -0
- package/dist/tools/schedules/index.js +4 -0
- package/dist/tools/schedules/index.js.map +1 -0
- package/dist/tools/schedules/loop-tool.d.ts +14 -0
- package/dist/tools/schedules/loop-tool.d.ts.map +1 -0
- package/dist/tools/schedules/loop-tool.js +81 -0
- package/dist/tools/schedules/loop-tool.js.map +1 -0
- package/dist/tools/schedules/present.d.ts +16 -0
- package/dist/tools/schedules/present.d.ts.map +1 -0
- package/dist/tools/schedules/present.js +69 -0
- package/dist/tools/schedules/present.js.map +1 -0
- package/dist/tools/schedules/prompt-scan.d.ts +17 -0
- package/dist/tools/schedules/prompt-scan.d.ts.map +1 -0
- package/dist/tools/schedules/prompt-scan.js +92 -0
- package/dist/tools/schedules/prompt-scan.js.map +1 -0
- package/dist/tools/schedules/schedule-tool.d.ts +16 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
- package/dist/tools/schedules/schedule-tool.js +327 -0
- package/dist/tools/schedules/schedule-tool.js.map +1 -0
- package/dist/tools/schedules/types.d.ts +184 -0
- package/dist/tools/schedules/types.d.ts.map +1 -0
- package/dist/tools/schedules/types.js +11 -0
- package/dist/tools/schedules/types.js.map +1 -0
- package/dist/types/authorization/index.d.ts +86 -9
- package/dist/types/authorization/index.d.ts.map +1 -1
- package/dist/types/authorization/index.js +11 -1
- package/dist/types/authorization/index.js.map +1 -1
- package/dist/types/browser/index.d.ts +280 -0
- package/dist/types/browser/index.d.ts.map +1 -0
- package/dist/types/browser/index.js +12 -0
- package/dist/types/browser/index.js.map +1 -0
- package/dist/types/hitl/index.d.ts +23 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/events.d.ts +6 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +23 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +9 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +108 -5
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +7 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +35 -3
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +45 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/id.d.ts +8 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +12 -0
- package/dist/utils/id.js.map +1 -1
- package/package.json +1 -1
- package/src/authorization/command-line.ts +148 -293
- package/src/authorization/gate.ts +22 -2
- package/src/authorization/rules.ts +67 -8
- package/src/authorization/shell-lexer.ts +2349 -0
- package/src/authorization/skill-grant.ts +400 -0
- package/src/bridge/a2a/mapper.ts +2 -0
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/directory/types.ts +2 -0
- package/src/persona/assembler.ts +5 -2
- package/src/prompt/coding-agent-doctrine.ts +1 -0
- package/src/public-runtime.ts +37 -0
- package/src/public-tools.ts +37 -1
- package/src/public-types.ts +57 -0
- package/src/runtime/jobs/registry.ts +19 -11
- package/src/runtime/query/declined.ts +12 -0
- package/src/runtime/query/executor.ts +116 -56
- package/src/runtime/query/index.ts +8 -0
- package/src/runtime/query/iteration/index.ts +13 -0
- package/src/runtime/query/iteration/phases/context.ts +13 -0
- package/src/runtime/query/iteration/phases/handoff.ts +74 -0
- package/src/runtime/query/iteration/phases/index.ts +1 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +55 -3
- package/src/runtime/query/resume-pending.ts +3 -2
- package/src/runtime/query/review-policy.ts +57 -3
- package/src/runtime/query/tooling.ts +4 -0
- package/src/schedules/cron.ts +202 -0
- package/src/schedules/describe.ts +138 -0
- package/src/schedules/errors.ts +15 -0
- package/src/schedules/evaluate.ts +178 -0
- package/src/schedules/index.ts +18 -0
- package/src/schedules/next-fire.ts +257 -0
- package/src/schedules/spec.ts +210 -0
- package/src/schedules/types.ts +163 -0
- package/src/schedules/tz.ts +155 -0
- package/src/skills/index.ts +1 -1
- package/src/skills/loader.ts +59 -1
- package/src/tools/builtins/bash.ts +24 -6
- package/src/tools/builtins/browser-url.ts +251 -0
- package/src/tools/builtins/browser.ts +817 -0
- package/src/tools/builtins/skill.ts +92 -53
- package/src/tools/command-shell.ts +166 -0
- package/src/tools/defineTool.ts +36 -1
- package/src/tools/schedules/index.ts +4 -0
- package/src/tools/schedules/loop-tool.ts +85 -0
- package/src/tools/schedules/present.ts +86 -0
- package/src/tools/schedules/prompt-scan.ts +96 -0
- package/src/tools/schedules/schedule-tool.ts +376 -0
- package/src/tools/schedules/types.ts +197 -0
- package/src/types/authorization/index.ts +60 -2
- package/src/types/browser/index.ts +341 -0
- package/src/types/hitl/index.ts +21 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +10 -0
- package/src/types/tool/index.ts +109 -5
- package/src/types/tool/presentation.ts +7 -0
- package/src/utils/frontmatter.ts +81 -5
- package/src/utils/id.ts +14 -0
|
@@ -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
|
|
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(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the model is told when a person declines its tool calls and gave no
|
|
3
|
+
* words of their own.
|
|
4
|
+
*
|
|
5
|
+
* "Declined" alone reads to a model as an obstacle to route around: in a live
|
|
6
|
+
* session an operator refused a browser navigation and the model fetched the
|
|
7
|
+
* same page through web search instead. The refusal is about the outcome, not
|
|
8
|
+
* the tool, so the text says so — for every tool, since any of them can be
|
|
9
|
+
* swapped for another that reaches the same place.
|
|
10
|
+
*/
|
|
11
|
+
export const DECLINED_TOOL_CALL_FEEDBACK =
|
|
12
|
+
'The user declined this. Do not get the same content or result another way (another tool, another site or address, or a web search) unless you ask the user first and they agree. Say what you wanted to do, or carry on without it.'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import type { Span } from '@opentelemetry/api'
|
|
3
3
|
import type { AuthorizationGate } from '../../authorization/gate.js'
|
|
4
|
+
import { type SkillGrantSet, compileSkillGrant } from '../../authorization/skill-grant.js'
|
|
4
5
|
import { extractFromToolCall, extractFromToolResult } from '../../compaction/extractor.js'
|
|
5
6
|
import type { WorkingStateManager } from '../../compaction/manager.js'
|
|
6
7
|
import { GENAI, NAMZU } from '../../constants/telemetry/index.js'
|
|
@@ -10,7 +11,8 @@ import { ProbeVetoError } from '../../probe/errors.js'
|
|
|
10
11
|
import { probe as defaultProbeRegistry } from '../../probe/registry.js'
|
|
11
12
|
import type { ProbeEnforcement } from '../../probe/registry.js'
|
|
12
13
|
import type { ActivityStore } from '../../store/activity/memory.js'
|
|
13
|
-
import {
|
|
14
|
+
import { sandboxShellSpawn, withoutBashStartup } from '../../tools/command-shell.js'
|
|
15
|
+
import { isAlwaysDestructive } from '../../tools/defineTool.js'
|
|
14
16
|
import { createFileReadTracker } from '../../tools/file-read-tracker.js'
|
|
15
17
|
import { pathOutsideRoots, toolRoots } from '../../tools/paths.js'
|
|
16
18
|
import type { ToolResultGuardrailSpec } from '../../types/guardrail/index.js'
|
|
@@ -33,9 +35,11 @@ import type {
|
|
|
33
35
|
FileReadTracker,
|
|
34
36
|
PreparedToolExecution,
|
|
35
37
|
RequestToolPause,
|
|
38
|
+
ShellDialect,
|
|
36
39
|
SkillRegistryRef,
|
|
37
40
|
ToolContext,
|
|
38
41
|
ToolDispatchOptions,
|
|
42
|
+
ToolHandoff,
|
|
39
43
|
ToolRegistryContract,
|
|
40
44
|
ToolResult,
|
|
41
45
|
} from '../../types/tool/index.js'
|
|
@@ -449,6 +453,12 @@ export interface ToolExecutorConfig {
|
|
|
449
453
|
authorizationGate?: AuthorizationGate
|
|
450
454
|
/** Durable refusal sink paired with {@link authorizationGate}. */
|
|
451
455
|
recordAudit?: (input: AuditEventInput) => Promise<unknown>
|
|
456
|
+
/**
|
|
457
|
+
* Where the `skill` tool's `allowed-tools` pre-approvals are recorded for
|
|
458
|
+
* this turn. The review phase reads the same set. Absent: a loaded skill
|
|
459
|
+
* grants nothing, and its tool says so.
|
|
460
|
+
*/
|
|
461
|
+
skillGrants?: SkillGrantSet
|
|
452
462
|
}
|
|
453
463
|
|
|
454
464
|
/**
|
|
@@ -480,6 +490,8 @@ export interface ToolCallOutcome {
|
|
|
480
490
|
/** Rich form for the model, when the tool supplied one. */
|
|
481
491
|
content?: ToolResultContent
|
|
482
492
|
isError?: boolean
|
|
493
|
+
/** The tool asked for a person; see `ToolResult.handoff`. */
|
|
494
|
+
handoff?: ToolHandoff
|
|
483
495
|
}
|
|
484
496
|
|
|
485
497
|
export interface ToolExecutionBatch {
|
|
@@ -692,58 +704,83 @@ export class ToolExecutor {
|
|
|
692
704
|
private batchMode?: PermissionMode
|
|
693
705
|
|
|
694
706
|
/**
|
|
695
|
-
* The
|
|
696
|
-
*
|
|
697
|
-
* `allowed-tools` was parsed, stored and rendered into the prompt, and
|
|
698
|
-
* read by nothing — advice phrased as a declaration. This is what makes
|
|
699
|
-
* it a restriction, on the same line that already enforces the step's
|
|
700
|
-
* list, because a narrowing the model can decline is not one.
|
|
701
|
-
*
|
|
702
|
-
* Two fields rather than one, and the second is the point: a skill
|
|
703
|
-
* loaded MID-batch must not retroactively refuse the calls the model
|
|
704
|
-
* issued alongside it. The model chose that batch under the old scope,
|
|
705
|
-
* and refusing half of it teaches nothing except that tools fail at
|
|
706
|
-
* random. `adoptedInBatch` is compared against the batch counter, so the
|
|
707
|
-
* scope takes effect from the next one.
|
|
707
|
+
* The step's list where it has one; the turn's is the default.
|
|
708
708
|
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
* computed before any of them could adopt anything — remove this
|
|
713
|
-
* comparison and no test changes, because the guarantee currently comes
|
|
714
|
-
* from where the context happens to be built rather than from here.
|
|
715
|
-
* Moving the context into the per-call spread is a plausible refactor,
|
|
716
|
-
* and it would silently produce a batch whose second half is refused for
|
|
717
|
-
* a scope its first half installed. That is precisely the incoherent
|
|
718
|
-
* batch this line exists to make impossible.
|
|
709
|
+
* A loaded skill no longer narrows this. Its `allowed-tools` used to be
|
|
710
|
+
* intersected in here from the next batch on, which read the field as a
|
|
711
|
+
* restriction when it is a pre-approval; see `grantSkillTools`.
|
|
719
712
|
*/
|
|
720
|
-
private
|
|
721
|
-
|
|
722
|
-
allowedTools: readonly string[]
|
|
723
|
-
adoptedInBatch: number
|
|
713
|
+
private effectiveAllowedTools(): readonly string[] | undefined {
|
|
714
|
+
return this.stepAllowedTools ?? this.config.allowedTools
|
|
724
715
|
}
|
|
725
|
-
private batchCounter = 0
|
|
726
716
|
|
|
727
717
|
/**
|
|
728
|
-
*
|
|
718
|
+
* Compile a skill's `allowed-tools` into pre-approvals for the rest of the
|
|
719
|
+
* turn, say what they would be, and record them only on `commit()`.
|
|
729
720
|
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
721
|
+
* Names resolve against THIS turn's registry, case-insensitively and
|
|
722
|
+
* through the Agent Skills aliases (`Read` is `read`, `WebFetch` is
|
|
723
|
+
* `web_fetch`), so a grant can only ever name a tool the turn already has.
|
|
724
|
+
* An entry that resolves to nothing, or to a tool every call of which is
|
|
725
|
+
* destructive (and therefore always reviewed), is reported and grants
|
|
726
|
+
* nothing.
|
|
736
727
|
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
* the
|
|
728
|
+
* Two steps because the `skill` tool can still fail after it knows what
|
|
729
|
+
* to say — its instructions may not fit the output budget — and a skill
|
|
730
|
+
* the model never received must not have approved anything.
|
|
740
731
|
*/
|
|
741
|
-
private
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
732
|
+
private grantSkillTools(grant: {
|
|
733
|
+
readonly skill: string
|
|
734
|
+
readonly allowedTools: readonly string[]
|
|
735
|
+
readonly skillDirectory?: string
|
|
736
|
+
}): {
|
|
737
|
+
readonly granted: readonly string[]
|
|
738
|
+
readonly ignored: readonly {
|
|
739
|
+
readonly entry: string
|
|
740
|
+
readonly reason: string
|
|
741
|
+
}[]
|
|
742
|
+
readonly commit: () => void
|
|
743
|
+
} {
|
|
744
|
+
const grants = this.config.skillGrants
|
|
745
|
+
if (!grants) return { granted: [], ignored: [], commit: () => {} }
|
|
746
|
+
const tools = this.config.tools
|
|
747
|
+
const byLowerName = new Map<string, string>()
|
|
748
|
+
for (const name of tools.listNames()) byLowerName.set(name.toLowerCase(), name)
|
|
749
|
+
// A grant can only ever name a tool this turn — or this step — can call.
|
|
750
|
+
// The registry holds more than that when `allowedTools` withholds some,
|
|
751
|
+
// and telling the model a withheld tool is pre-approved is a promise the
|
|
752
|
+
// executor will refuse to keep.
|
|
753
|
+
const allowed = this.effectiveAllowedTools()
|
|
754
|
+
const compiled = compileSkillGrant(grant.allowedTools, {
|
|
755
|
+
resolveTool: (name) => {
|
|
756
|
+
const registered = byLowerName.get(name.toLowerCase())
|
|
757
|
+
if (registered === undefined) return undefined
|
|
758
|
+
const definition = tools.get(registered)
|
|
759
|
+
const commandArgument = definition?.commandArgument
|
|
760
|
+
return {
|
|
761
|
+
name: registered,
|
|
762
|
+
...(allowed !== undefined && !allowed.includes(registered) ? { unavailable: true } : {}),
|
|
763
|
+
...(commandArgument === undefined ? {} : { commandArgument }),
|
|
764
|
+
...(definition && isAlwaysDestructive(definition) ? { alwaysDestructive: true } : {}),
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
...(grant.skillDirectory ? { skillDirectory: grant.skillDirectory } : {}),
|
|
768
|
+
})
|
|
769
|
+
return {
|
|
770
|
+
granted: compiled.entries.map((entry) =>
|
|
771
|
+
entry.pattern === undefined ? entry.tool : entry.declared,
|
|
772
|
+
),
|
|
773
|
+
ignored: compiled.ignored,
|
|
774
|
+
commit: () => {
|
|
775
|
+
grants.grant(grant.skill, compiled)
|
|
776
|
+
if (compiled.ignored.length > 0) {
|
|
777
|
+
this.log.warn('Skill allowed-tools entries were ignored', {
|
|
778
|
+
'namzu.skill.name': grant.skill,
|
|
779
|
+
'namzu.skill.ignored': compiled.ignored.map((item) => item.entry),
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
}
|
|
747
784
|
}
|
|
748
785
|
|
|
749
786
|
private resolvePermissionMode(): PermissionMode {
|
|
@@ -757,9 +794,20 @@ export class ToolExecutor {
|
|
|
757
794
|
toolName,
|
|
758
795
|
toolInput: input,
|
|
759
796
|
toolDef: this.config.tools.get(toolName),
|
|
797
|
+
commandDialect: this.commandDialect(toolName),
|
|
760
798
|
})
|
|
761
799
|
}
|
|
762
800
|
|
|
801
|
+
/**
|
|
802
|
+
* The shell a tool's command line will run in this turn, for the
|
|
803
|
+
* permission rules to read it in. A tool that does not say is `sh`, the
|
|
804
|
+
* reading that holds for any POSIX shell.
|
|
805
|
+
*/
|
|
806
|
+
commandDialect(toolName: string): ShellDialect {
|
|
807
|
+
const tool = this.config.tools.get(toolName)
|
|
808
|
+
return tool?.commandDialect?.({ sandboxed: this.config.sandbox !== undefined }) ?? 'sh'
|
|
809
|
+
}
|
|
810
|
+
|
|
763
811
|
/**
|
|
764
812
|
* Resolve repairs and pre-tool hooks, then decode each call exactly once.
|
|
765
813
|
* The returned projection is what policy and a human review; execution later
|
|
@@ -883,8 +931,6 @@ export class ToolExecutor {
|
|
|
883
931
|
}
|
|
884
932
|
assertUniqueToolCallIds(toolCalls)
|
|
885
933
|
|
|
886
|
-
this.batchCounter += 1
|
|
887
|
-
|
|
888
934
|
// Sampled here, once, and held for every call below. See the note on
|
|
889
935
|
// `permissionMode` in the config type.
|
|
890
936
|
this.batchMode = this.resolvePermissionMode()
|
|
@@ -1212,6 +1258,7 @@ export class ToolExecutor {
|
|
|
1212
1258
|
toolName: name,
|
|
1213
1259
|
toolInput: preparedInput,
|
|
1214
1260
|
toolDef: this.config.tools.get(name),
|
|
1261
|
+
commandDialect: this.commandDialect(name),
|
|
1215
1262
|
})
|
|
1216
1263
|
if (gateResult && gateResult.decision !== 'allow') {
|
|
1217
1264
|
const reason =
|
|
@@ -1402,9 +1449,16 @@ export class ToolExecutor {
|
|
|
1402
1449
|
}
|
|
1403
1450
|
|
|
1404
1451
|
private resultPresentation(name: string, input: unknown, result: ToolResult) {
|
|
1405
|
-
if (!result.success) return {}
|
|
1406
1452
|
try {
|
|
1407
1453
|
const view = this.config.tools.get(name)?.presentResult?.(input, result)
|
|
1454
|
+
// A failed call carries one view only: the person's No on the tool's
|
|
1455
|
+
// own screen, which a host draws as cancelled rather than failed and
|
|
1456
|
+
// cannot tell apart from the result text alone.
|
|
1457
|
+
if (!result.success) {
|
|
1458
|
+
return view?.kind === 'generic' && view.outcome === 'cancelled'
|
|
1459
|
+
? { presentation: { kind: 'generic', label: view.label, outcome: 'cancelled' } as const }
|
|
1460
|
+
: {}
|
|
1461
|
+
}
|
|
1408
1462
|
if (view?.kind !== 'diff') return {}
|
|
1409
1463
|
const serialized = JSON.stringify(view)
|
|
1410
1464
|
if (serialized.length > (this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS))
|
|
@@ -1442,11 +1496,11 @@ export class ToolExecutor {
|
|
|
1442
1496
|
// Same precedence the request already uses when it decides which
|
|
1443
1497
|
// schemas to send, so the menu and the kitchen agree.
|
|
1444
1498
|
allowedTools: this.effectiveAllowedTools(),
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1499
|
+
// A skill loaded during this batch pre-approves calls from the NEXT
|
|
1500
|
+
// review on; this batch was already reviewed. See `grantSkillTools`.
|
|
1501
|
+
...(this.config.skillGrants
|
|
1502
|
+
? { grantSkillTools: (grant) => this.grantSkillTools(grant) }
|
|
1503
|
+
: {}),
|
|
1450
1504
|
maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
1451
1505
|
// The turn's screens, defaulted HERE rather than on the registry: a
|
|
1452
1506
|
// host builds the registry and hands it over, so a registry-side
|
|
@@ -1498,11 +1552,11 @@ export class ToolExecutor {
|
|
|
1498
1552
|
readonly env?: Record<string, string>
|
|
1499
1553
|
}): JobProcess =>
|
|
1500
1554
|
(this.config.sandbox as Sandbox).spawnDetached?.(
|
|
1501
|
-
|
|
1502
|
-
|
|
1555
|
+
sandboxShellSpawn(job.command).file,
|
|
1556
|
+
sandboxShellSpawn(job.command).args,
|
|
1503
1557
|
{
|
|
1504
1558
|
cwd: job.workingDirectory,
|
|
1505
|
-
...(job.env ? { env: job.env } : {}),
|
|
1559
|
+
...(job.env ? { env: withoutBashStartup(job.env) } : {}),
|
|
1506
1560
|
},
|
|
1507
1561
|
) as JobProcess,
|
|
1508
1562
|
}
|
|
@@ -1960,6 +2014,12 @@ export class ToolExecutor {
|
|
|
1960
2014
|
// a result whose image is unaffected — and a hook that needs it gone
|
|
1961
2015
|
// says so with `content`, which wins over both.
|
|
1962
2016
|
...(modelContent !== undefined ? { content: modelContent } : {}),
|
|
2017
|
+
// Carried whatever a post-tool hook did to the text: the request is
|
|
2018
|
+
// the tool's statement about the world, not about its output, and a
|
|
2019
|
+
// hook that redacts a sign-in page has not signed anyone in.
|
|
2020
|
+
...(result.handoff !== undefined && !this.config.abortSignal.aborted
|
|
2021
|
+
? { handoff: result.handoff }
|
|
2022
|
+
: {}),
|
|
1963
2023
|
}
|
|
1964
2024
|
}
|
|
1965
2025
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
assertBudgetEnforceable,
|
|
7
7
|
} from '../../advisory/index.js'
|
|
8
8
|
import { AuthorizationGate } from '../../authorization/gate.js'
|
|
9
|
+
import { SkillGrantSet } from '../../authorization/skill-grant.js'
|
|
9
10
|
import { repairToolMessageHistory, toolHistoryRepairChanged } from '../../compaction/dangling.js'
|
|
10
11
|
import { extractFromUserMessage } from '../../compaction/extractor.js'
|
|
11
12
|
import { WorkingStateManager } from '../../compaction/manager.js'
|
|
@@ -1197,8 +1198,13 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1197
1198
|
: undefined
|
|
1198
1199
|
awaitedJobs?.attach()
|
|
1199
1200
|
|
|
1201
|
+
// Turn-scoped, like `toolGrants` below: what a skill loaded in this turn
|
|
1202
|
+
// pre-approves ends with the turn. Shared by the executor, where the
|
|
1203
|
+
// `skill` tool records a grant, and the review phase, which reads it.
|
|
1204
|
+
const skillGrants = new SkillGrantSet()
|
|
1200
1205
|
const toolExecutor = ToolingBootstrap.init(
|
|
1201
1206
|
{
|
|
1207
|
+
skillGrants,
|
|
1202
1208
|
tools: params.tools,
|
|
1203
1209
|
sessionId: ctx.sessionId,
|
|
1204
1210
|
turnId: ctx.turnId,
|
|
@@ -1456,6 +1462,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1456
1462
|
provider: resilientProvider,
|
|
1457
1463
|
providerCapabilities: capabilities,
|
|
1458
1464
|
strictCapabilities: params.strictCapabilities === true,
|
|
1465
|
+
...(params.parentSessionId !== undefined ? { delegated: true } : {}),
|
|
1459
1466
|
servingMember: () => serving.current,
|
|
1460
1467
|
turnConfig,
|
|
1461
1468
|
...(params.stopWhen ? { stopWhen: params.stopWhen } : {}),
|
|
@@ -1496,6 +1503,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1496
1503
|
// Turn-scoped. An approval is a statement about this turn's work;
|
|
1497
1504
|
// carrying one into a later turn would be reuse nobody agreed to.
|
|
1498
1505
|
toolGrants: new ToolGrantSet(),
|
|
1506
|
+
skillGrants,
|
|
1499
1507
|
...(params.reviewAllowedCalls ? { reviewAllowedCalls: params.reviewAllowedCalls } : {}),
|
|
1500
1508
|
// Turn-scoped for the same reason. A repeat count carried into a later
|
|
1501
1509
|
// run is a claim about work nobody repeated, and a module-level map
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
relieveOverflow,
|
|
72
72
|
runCompactionCheck,
|
|
73
73
|
} from './phases/compaction.js'
|
|
74
|
+
import { runHandoffPause } from './phases/handoff.js'
|
|
74
75
|
import type { IterationContext } from './phases/index.js'
|
|
75
76
|
import { runPlanGate } from './phases/plan.js'
|
|
76
77
|
import { runToolReview } from './phases/tool-review.js'
|
|
@@ -1421,6 +1422,13 @@ export class IterationOrchestrator {
|
|
|
1421
1422
|
return
|
|
1422
1423
|
}
|
|
1423
1424
|
|
|
1425
|
+
// A tool asked for a person. The whole batch is committed; the
|
|
1426
|
+
// turn parks here rather than asking the model what to do about
|
|
1427
|
+
// something only the operator can do.
|
|
1428
|
+
if ((yield* runHandoffPause(this.ctx, iterationNum, reviewOutcome.results)) === 'stop') {
|
|
1429
|
+
return
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1424
1432
|
if (reviewOutcome.decision === 'rejected') {
|
|
1425
1433
|
continue
|
|
1426
1434
|
}
|
|
@@ -1826,6 +1834,11 @@ export class IterationOrchestrator {
|
|
|
1826
1834
|
private rememberUserMessage(message: Message, arriving = true): void {
|
|
1827
1835
|
if (!isOperatorUserMessage(message)) return
|
|
1828
1836
|
this.latestUserMessage = message
|
|
1837
|
+
// A skill's `allowed-tools` pre-approves for the request that loaded
|
|
1838
|
+
// it. The operator speaking again — a queued message or steering
|
|
1839
|
+
// delivered into this same `query()` — is a new request, and it starts
|
|
1840
|
+
// with nothing pre-approved, exactly as a new turn would.
|
|
1841
|
+
if (arriving) this.ctx.skillGrants?.clear()
|
|
1829
1842
|
// The hook field alone does not reach the model. Preserve arrivals in
|
|
1830
1843
|
// the compaction state too, before their original message or attached
|
|
1831
1844
|
// tool result can be shed. Initial history was already extracted at seed.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdvisoryContext } from '../../../../advisory/context.js'
|
|
2
|
+
import type { SkillGrantSet } from '../../../../authorization/skill-grant.js'
|
|
2
3
|
import type { AgentBus } from '../../../../bus/index.js'
|
|
3
4
|
import type { WorkingStateManager } from '../../../../compaction/manager.js'
|
|
4
5
|
import type { ContextReducer } from '../../../../compaction/reducer.js'
|
|
@@ -43,6 +44,12 @@ import type { ToolGrantSet } from '../../tool-grants.js'
|
|
|
43
44
|
|
|
44
45
|
export interface IterationContext {
|
|
45
46
|
readonly provider: LLMProvider
|
|
47
|
+
/**
|
|
48
|
+
* The turn runs in a delegated child session. A tool's request for a
|
|
49
|
+
* person then fails the turn instead of pausing it: nobody is watching
|
|
50
|
+
* a child to resume it, and its parent is waiting on a result.
|
|
51
|
+
*/
|
|
52
|
+
readonly delegated?: boolean
|
|
46
53
|
/** Driver-level request shapes negotiated for this turn. */
|
|
47
54
|
readonly providerCapabilities?: ResolvedProviderCapabilities
|
|
48
55
|
/** Refuse a capability mismatch instead of emitting a warning and degrading. */
|
|
@@ -166,6 +173,12 @@ export interface IterationContext {
|
|
|
166
173
|
* asked about again. Absent on paths that do not review tools.
|
|
167
174
|
*/
|
|
168
175
|
readonly toolGrants?: ToolGrantSet
|
|
176
|
+
/**
|
|
177
|
+
* What skills loaded in this turn pre-approve (`allowed-tools`). Read by
|
|
178
|
+
* the review phase to mark covered calls; the review policy decides
|
|
179
|
+
* whether a mark skips the prompt. Absent: nothing is marked.
|
|
180
|
+
*/
|
|
181
|
+
readonly skillGrants?: SkillGrantSet
|
|
169
182
|
/** See `QueryParams.reviewAllowedCalls`. Absent: allowed and granted batches skip review. */
|
|
170
183
|
readonly reviewAllowedCalls?: () => boolean
|
|
171
184
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { GENAI, NAMZU } from '../../../../telemetry/attributes.js'
|
|
2
|
+
import { NamzuError } from '../../../../types/errors/index.js'
|
|
3
|
+
import type { SessionEvent } from '../../../../types/session/index.js'
|
|
4
|
+
import type { ToolCallOutcome } from '../../executor.js'
|
|
5
|
+
import type { IterationContext, PhaseSignal } from './context.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A tool asked for a person: stop before the next model call.
|
|
9
|
+
*
|
|
10
|
+
* Runs after the batch settled, so every result — the one that asked and
|
|
11
|
+
* its siblings — is already in the transcript and queued for the session
|
|
12
|
+
* log. The checkpoint written here is taken from that state, which is what
|
|
13
|
+
* lets a resume continue exactly as it does after a provider pause: the
|
|
14
|
+
* next step is a model call that sees the results.
|
|
15
|
+
*
|
|
16
|
+
* The first request in the batch speaks for it. Two tools that both need a
|
|
17
|
+
* person need the same thing from the operator — to come and look — and
|
|
18
|
+
* one pause answers both.
|
|
19
|
+
*
|
|
20
|
+
* In a delegated child the turn fails with the reason instead. Nobody
|
|
21
|
+
* resumes a child's turn: its parent is waiting on a result, and a failed
|
|
22
|
+
* child with the reason in it is a result the parent's model can act on.
|
|
23
|
+
*/
|
|
24
|
+
export async function* runHandoffPause(
|
|
25
|
+
ctx: IterationContext,
|
|
26
|
+
iterationNum: number,
|
|
27
|
+
results: readonly ToolCallOutcome[],
|
|
28
|
+
): AsyncGenerator<SessionEvent, PhaseSignal> {
|
|
29
|
+
const requested = results.find((result) => result.handoff !== undefined)
|
|
30
|
+
const handoff = requested?.handoff
|
|
31
|
+
if (!requested || !handoff) return 'continue'
|
|
32
|
+
|
|
33
|
+
if (ctx.delegated) {
|
|
34
|
+
ctx.log.info('A tool in a delegated turn needs a person; the turn fails', {
|
|
35
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
36
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
37
|
+
[GENAI.TOOL_NAME]: requested.toolName,
|
|
38
|
+
})
|
|
39
|
+
throw new NamzuError({
|
|
40
|
+
code: 'tool_error',
|
|
41
|
+
message: `${requested.toolName} needs a person: ${handoff.reason}`,
|
|
42
|
+
details: { toolName: requested.toolName, handoff },
|
|
43
|
+
retryable: false,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const checkpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
|
|
48
|
+
await ctx.emitEvent({
|
|
49
|
+
type: 'checkpoint_created',
|
|
50
|
+
turnId: ctx.recorder.turnId,
|
|
51
|
+
checkpointId: checkpoint.id,
|
|
52
|
+
iteration: iterationNum,
|
|
53
|
+
})
|
|
54
|
+
yield* ctx.drainPending()
|
|
55
|
+
|
|
56
|
+
await ctx.emitEvent({
|
|
57
|
+
type: 'turn_paused',
|
|
58
|
+
budget: ctx.recorder.budget?.summary(),
|
|
59
|
+
turnId: ctx.recorder.turnId,
|
|
60
|
+
checkpointId: checkpoint.id,
|
|
61
|
+
reason: handoff.reason,
|
|
62
|
+
handoff,
|
|
63
|
+
})
|
|
64
|
+
yield* ctx.drainPending()
|
|
65
|
+
ctx.recorder.setStopReason('paused')
|
|
66
|
+
ctx.log.info('Turn paused for a person', {
|
|
67
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
68
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
69
|
+
[GENAI.TOOL_NAME]: requested.toolName,
|
|
70
|
+
'namzu.checkpoint.id': checkpoint.id,
|
|
71
|
+
'namzu.runtime.reason': handoff.reason,
|
|
72
|
+
})
|
|
73
|
+
return 'stop'
|
|
74
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { AuthorizationGate } from '../../../../authorization/index.js'
|
|
|
2
2
|
import type { ToolCallSummary } from '../../../../types/hitl/index.js'
|
|
3
3
|
import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
|
|
4
4
|
import type { SessionEvent } from '../../../../types/session/index.js'
|
|
5
|
+
import type { ShellDialect } from '../../../../types/tool/index.js'
|
|
6
|
+
import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
|
|
5
7
|
import type { PreparedToolBatch, ToolCallDenials } from '../../executor.js'
|
|
6
8
|
import {
|
|
7
9
|
awaitProjectInstructionCallback,
|
|
@@ -53,6 +55,17 @@ export async function* runToolReview(
|
|
|
53
55
|
): AsyncGenerator<SessionEvent, ToolReviewOutcome> {
|
|
54
56
|
let executed: readonly import('../../executor.js').ToolCallOutcome[] = []
|
|
55
57
|
let toolMs = 0
|
|
58
|
+
// The shell each call's command line will run in, for the rules and the
|
|
59
|
+
// skill grants to read it the same way. A test double without the method
|
|
60
|
+
// leaves it unset, which reads the line for any POSIX shell.
|
|
61
|
+
const dialectFor = (toolName: string): { commandDialect?: ShellDialect } => {
|
|
62
|
+
const executor = ctx.toolExecutor as {
|
|
63
|
+
commandDialect?: (name: string) => ShellDialect
|
|
64
|
+
}
|
|
65
|
+
return typeof executor.commandDialect === 'function'
|
|
66
|
+
? { commandDialect: executor.commandDialect(toolName) }
|
|
67
|
+
: {}
|
|
68
|
+
}
|
|
56
69
|
|
|
57
70
|
const finish = (decision: ToolReviewDecision): ToolReviewOutcome => ({
|
|
58
71
|
decision,
|
|
@@ -255,6 +268,7 @@ export async function* runToolReview(
|
|
|
255
268
|
toolName: tc.name,
|
|
256
269
|
toolInput: tc.input,
|
|
257
270
|
toolDef: ctx.tools.get(tc.name),
|
|
271
|
+
...dialectFor(tc.name),
|
|
258
272
|
}),
|
|
259
273
|
}))
|
|
260
274
|
for (const gr of gateResults) {
|
|
@@ -331,6 +345,24 @@ export async function* runToolReview(
|
|
|
331
345
|
})
|
|
332
346
|
}
|
|
333
347
|
|
|
348
|
+
// A skill's `allowed-tools` pre-approval, marked on the calls it covers
|
|
349
|
+
// and left for the review policy to honour. Marked, never decided here:
|
|
350
|
+
// only the policy knows the mode, and `plan` and `strict` must refuse a
|
|
351
|
+
// call a skill granted exactly as they refuse any other. Nothing stronger
|
|
352
|
+
// may stand in the way — an operator's deny or explicit ask, a
|
|
353
|
+
// destructive call, a path outside the roots or a sandbox escape all
|
|
354
|
+
// leave the call unmarked, so it is reviewed as though no skill had
|
|
355
|
+
// spoken.
|
|
356
|
+
if (ctx.skillGrants && ctx.skillGrants.size > 0) {
|
|
357
|
+
for (const tc of toolCallSummaries) {
|
|
358
|
+
if (gateDenied.has(tc.id)) continue
|
|
359
|
+
if (tc.authorization?.decision === 'deny' || tc.authorization?.explicitReview) continue
|
|
360
|
+
if (tc.isDestructive || tc.escalation !== undefined) continue
|
|
361
|
+
const skill = ctx.skillGrants.coveringSkill(tc, ctx.tools.get(tc.name), dialectFor(tc.name))
|
|
362
|
+
if (skill !== undefined) tc.skillGrant = { skill }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
334
366
|
// Already approved, at a scope the approver chose — and nothing the
|
|
335
367
|
// operator's policy denied, because `gateDenied` is checked first.
|
|
336
368
|
// Re-asking about a call somebody has already said yes to is how an
|
|
@@ -419,7 +451,11 @@ export async function* runToolReview(
|
|
|
419
451
|
}
|
|
420
452
|
for (const path of tc.escalation?.outsidePaths ?? []) {
|
|
421
453
|
await ctx.recorder.recordAudit({
|
|
422
|
-
what: {
|
|
454
|
+
what: {
|
|
455
|
+
action: 'outside_root_access',
|
|
456
|
+
tool: tc.name,
|
|
457
|
+
resource: path,
|
|
458
|
+
},
|
|
423
459
|
outcome: 'approved',
|
|
424
460
|
reason: "the turn's review approved this call",
|
|
425
461
|
})
|
|
@@ -436,7 +472,7 @@ export async function* runToolReview(
|
|
|
436
472
|
})
|
|
437
473
|
yield* ctx.drainPending()
|
|
438
474
|
|
|
439
|
-
const feedback = reviewDecision.feedback ||
|
|
475
|
+
const feedback = reviewDecision.feedback || DECLINED_TOOL_CALL_FEEDBACK
|
|
440
476
|
const denials = new Map(denyAll(feedback))
|
|
441
477
|
await settleEscalations(denials, undefined)
|
|
442
478
|
await settle(denials)
|
|
@@ -466,7 +502,7 @@ export async function* runToolReview(
|
|
|
466
502
|
}
|
|
467
503
|
}
|
|
468
504
|
if (mod.action === 'deny' && !denials.has(mod.toolCallId)) {
|
|
469
|
-
denials.set(mod.toolCallId,
|
|
505
|
+
denials.set(mod.toolCallId, DECLINED_TOOL_CALL_FEEDBACK)
|
|
470
506
|
}
|
|
471
507
|
}
|
|
472
508
|
|
|
@@ -490,6 +526,7 @@ export async function* runToolReview(
|
|
|
490
526
|
toolName: summary.name,
|
|
491
527
|
toolInput: summary.input,
|
|
492
528
|
toolDef: ctx.tools.get(summary.name),
|
|
529
|
+
...dialectFor(summary.name),
|
|
493
530
|
})
|
|
494
531
|
if (gateResult.decision === 'allow') continue
|
|
495
532
|
const reason =
|
|
@@ -550,6 +587,21 @@ export async function* runToolReview(
|
|
|
550
587
|
if (reviewDecision.action === 'approve_tools' && reviewDecision.remember) {
|
|
551
588
|
ctx.toolGrants?.grant(reviewDecision.remember)
|
|
552
589
|
}
|
|
590
|
+
// A call nobody was asked about, approved because a skill said so,
|
|
591
|
+
// is on the record naming that skill. Only a call that carried the
|
|
592
|
+
// mark counts: a policy that lists an unmarked id has not been
|
|
593
|
+
// given a skill's word for it.
|
|
594
|
+
if (reviewDecision.action === 'approve_tools' && reviewDecision.skillGranted) {
|
|
595
|
+
const listed = new Set(reviewDecision.skillGranted)
|
|
596
|
+
for (const tc of toolCallSummaries) {
|
|
597
|
+
if (!listed.has(tc.id) || !tc.skillGrant || gateDenied.has(tc.id)) continue
|
|
598
|
+
await ctx.recorder.recordAudit({
|
|
599
|
+
what: { action: 'tool_call', tool: tc.name },
|
|
600
|
+
outcome: 'approved',
|
|
601
|
+
reason: `pre-approved by the allowed-tools of skill "${tc.skillGrant.skill}" for this turn; nobody was asked`,
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
}
|
|
553
605
|
|
|
554
606
|
await ctx.emitEvent({
|
|
555
607
|
type: 'tool_review_completed',
|