@namzu/sdk 57.0.0 → 58.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +3 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/file-definitions.d.ts +35 -4
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +98 -8
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +9 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -1
- package/dist/authorization/protected-paths.js +29 -3
- package/dist/authorization/protected-paths.js.map +1 -1
- package/dist/compaction/index.d.ts +1 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/manual.d.ts +7 -0
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +31 -12
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/model-summary.d.ts +101 -0
- package/dist/compaction/model-summary.d.ts.map +1 -0
- package/dist/compaction/model-summary.js +333 -0
- package/dist/compaction/model-summary.js.map +1 -0
- package/dist/compaction/verifier.d.ts +6 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +3 -3
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +62 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +16 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +8 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +75 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +2 -0
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +2 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +2 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +2 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +25 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +2 -1
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +2 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/lifecycle.d.ts +8 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -2
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook-http.d.ts +39 -0
- package/dist/plugin/shell-hook-http.d.ts.map +1 -0
- package/dist/plugin/shell-hook-http.js +163 -0
- package/dist/plugin/shell-hook-http.js.map +1 -0
- package/dist/plugin/shell-hook.d.ts +93 -11
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +223 -39
- package/dist/plugin/shell-hook.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 +2 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/executor.d.ts +4 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +47 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +8 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +29 -1
- 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 +6 -2
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +52 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +5 -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/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +2 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +7 -4
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +2 -2
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts +2 -0
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +25 -0
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +3 -2
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +32 -13
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/scheduler/local.js +1 -1
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/retention/archive.js +1 -1
- package/dist/session/retention/archive.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +6 -0
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +28 -0
- package/dist/session/workspace/git-worktree.js.map +1 -1
- 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 +7 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +53 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +2 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts +52 -0
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
- package/dist/tools/builtins/bash-cwd.js +127 -0
- package/dist/tools/builtins/bash-cwd.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +41 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
- package/dist/tools/builtins/bounded-file-read.js +67 -0
- package/dist/tools/builtins/bounded-file-read.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +6 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +187 -24
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +113 -28
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-media.d.ts +68 -0
- package/dist/tools/builtins/read-media.d.ts.map +1 -0
- package/dist/tools/builtins/read-media.js +397 -0
- package/dist/tools/builtins/read-media.js.map +1 -0
- package/dist/tools/builtins/search-tools.js +1 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +4 -2
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +61 -2
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/sleep-guard.d.ts +15 -0
- package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
- package/dist/tools/builtins/sleep-guard.js +33 -0
- package/dist/tools/builtins/sleep-guard.js.map +1 -0
- package/dist/tools/builtins/view-image.d.ts +23 -1
- package/dist/tools/builtins/view-image.d.ts.map +1 -1
- package/dist/tools/builtins/view-image.js +66 -58
- package/dist/tools/builtins/view-image.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +31 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/toolsets/manager.js +3 -3
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/agent/base.d.ts +8 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +8 -0
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +6 -2
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/agent/task.js.map +1 -1
- package/dist/types/connector/mcp.d.ts +46 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +1 -1
- package/dist/types/plugin/index.d.ts +74 -7
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +12 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/session/sub-session.d.ts +6 -2
- package/dist/types/session/sub-session.d.ts.map +1 -1
- package/dist/types/skills/index.d.ts +36 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/skills/index.js +9 -0
- package/dist/types/skills/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +37 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +2 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +12 -0
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +6 -0
- package/dist/utils/frontmatter.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +3 -0
- package/src/agents/file-definitions.ts +126 -6
- package/src/authorization/protected-paths.ts +30 -2
- package/src/compaction/index.ts +1 -0
- package/src/compaction/manual.ts +68 -30
- package/src/compaction/model-summary.ts +412 -0
- package/src/compaction/verifier.ts +3 -3
- package/src/config/runtime.ts +16 -0
- package/src/connector/mcp/client.ts +87 -1
- package/src/constants/mcp/index.ts +3 -0
- package/src/constants/plugin/index.ts +2 -0
- package/src/manager/agent/lifecycle.ts +24 -5
- package/src/manager/session/turn-recorder.ts +2 -1
- package/src/persona/assembler.ts +2 -2
- package/src/plugin/index.ts +5 -0
- package/src/plugin/lifecycle.ts +16 -2
- package/src/plugin/shell-hook-http.ts +181 -0
- package/src/plugin/shell-hook.ts +386 -89
- package/src/prompt/coding-agent-doctrine.ts +2 -1
- package/src/public-runtime.ts +6 -0
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +5 -0
- package/src/runtime/query/executor.ts +54 -10
- package/src/runtime/query/index.ts +44 -0
- package/src/runtime/query/iteration/index.ts +11 -3
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
- package/src/runtime/query/iteration/phases/compaction.ts +67 -5
- package/src/runtime/query/iteration/phases/context.ts +6 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
- package/src/runtime/query/path-escalation.ts +11 -5
- package/src/runtime/query/plugin-hooks.ts +2 -2
- package/src/runtime/query/prepare-turn.ts +33 -0
- package/src/runtime/query/review-policy.ts +45 -16
- package/src/scheduler/local.ts +1 -1
- package/src/session/retention/archive.ts +1 -1
- package/src/session/workspace/driver.ts +6 -0
- package/src/session/workspace/git-worktree.ts +31 -0
- package/src/skills/index.ts +6 -1
- package/src/skills/loader.ts +54 -0
- package/src/skills/registry.ts +7 -6
- package/src/tools/builtins/bash-cwd.ts +156 -0
- package/src/tools/builtins/bash.ts +79 -34
- package/src/tools/builtins/bounded-file-read.ts +80 -0
- package/src/tools/builtins/grep.ts +198 -28
- package/src/tools/builtins/read-file.ts +151 -30
- package/src/tools/builtins/read-media.ts +483 -0
- package/src/tools/builtins/search-tools.ts +1 -1
- package/src/tools/builtins/skill.ts +91 -2
- package/src/tools/builtins/sleep-guard.ts +31 -0
- package/src/tools/builtins/view-image.ts +74 -62
- package/src/tools/file-read-tracker.ts +30 -0
- package/src/toolsets/manager.ts +3 -3
- package/src/types/agent/base.ts +9 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/agent/scheduler.ts +9 -0
- package/src/types/agent/task.ts +6 -2
- package/src/types/connector/mcp.ts +48 -0
- package/src/types/hitl/index.ts +1 -1
- package/src/types/plugin/index.ts +102 -16
- package/src/types/session/sub-session.ts +6 -2
- package/src/types/skills/index.ts +48 -0
- package/src/types/tool/index.ts +38 -0
- package/src/types/workspace/ref.ts +2 -0
- package/src/utils/frontmatter.ts +21 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory a `bash` call starts in, carried from one call to the next.
|
|
3
|
+
*
|
|
4
|
+
* A model that runs `cd packages/app` expects the next call to be there; shell
|
|
5
|
+
* state (variables, functions, aliases) does not carry over, but the directory
|
|
6
|
+
* does. After a foreground command the shell writes its physical directory to a
|
|
7
|
+
* private file, and the next call of the same session starts there.
|
|
8
|
+
*
|
|
9
|
+
* Two limits keep this from widening what a command can reach. The new
|
|
10
|
+
* directory has to lie inside the turn's roots (the working directory, the
|
|
11
|
+
* directories added for the session, any path this call was approved for); a
|
|
12
|
+
* command that leaves them is put back at the session's working directory and
|
|
13
|
+
* the result says so. And a directory that no longer exists is dropped without
|
|
14
|
+
* ceremony. Nothing here applies to a sandboxed command or to Windows shells,
|
|
15
|
+
* whose paths a host process cannot use as they are printed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { randomUUID } from 'node:crypto'
|
|
19
|
+
import { statSync } from 'node:fs'
|
|
20
|
+
import { rm } from 'node:fs/promises'
|
|
21
|
+
import { tmpdir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
|
|
24
|
+
import { type CommandShell, hostCommandShell } from '../command-shell.js'
|
|
25
|
+
import { pathOutsideRoots, toolRoots } from '../paths.js'
|
|
26
|
+
import { readBoundedFile } from './bounded-file-read.js'
|
|
27
|
+
|
|
28
|
+
interface Carried {
|
|
29
|
+
/** The working directory the session started with; the fallback. */
|
|
30
|
+
readonly base: string
|
|
31
|
+
cwd: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Sessions remembered; the oldest is forgotten first. */
|
|
35
|
+
const MAX_SESSIONS = 256
|
|
36
|
+
const carried = new Map<string, Carried>()
|
|
37
|
+
|
|
38
|
+
/** The notice appended to a result when the command left the allowed directories. */
|
|
39
|
+
export function cwdResetNotice(base: string): string {
|
|
40
|
+
return `Shell cwd was reset to ${base}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Single-quote `text` for a POSIX shell. */
|
|
44
|
+
function shellQuote(text: string): string {
|
|
45
|
+
return `'${text.replace(/'/g, `'\\''`)}'`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs `command` unchanged and records the directory it ended in. The exit
|
|
50
|
+
* status is the command's own. A command that ends the shell itself (`exit`)
|
|
51
|
+
* records nothing, so the directory stays as it was.
|
|
52
|
+
*/
|
|
53
|
+
export function withCwdCapture(command: string, file: string): string {
|
|
54
|
+
return `eval ${shellQuote(command)}\n__namzu_status=$?\npwd -P >| ${shellQuote(file)} 2>/dev/null\nexit $__namzu_status`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function newCwdFile(): string {
|
|
58
|
+
return join(tmpdir(), `namzu-cwd-${randomUUID()}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Where this session's next foreground call starts. Synchronous: the spawn must not wait on it. */
|
|
62
|
+
export function startingDirectory(sessionId: string, base: string): string {
|
|
63
|
+
const entry = carried.get(sessionId)
|
|
64
|
+
if (entry === undefined) return base
|
|
65
|
+
if (entry.base !== base) {
|
|
66
|
+
carried.delete(sessionId)
|
|
67
|
+
return base
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (statSync(entry.cwd).isDirectory()) return entry.cwd
|
|
71
|
+
} catch {
|
|
72
|
+
// Removed or unreadable: start again from the base.
|
|
73
|
+
}
|
|
74
|
+
entry.cwd = base
|
|
75
|
+
return base
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The cwd used by bash execution and host authorization. Hosts must resolve it
|
|
80
|
+
* for each call, not cache the session's initial directory. Sandboxed commands
|
|
81
|
+
* (including sandbox escapes) and Windows never use the carried POSIX cwd.
|
|
82
|
+
*/
|
|
83
|
+
export function effectiveBashWorkingDirectory(
|
|
84
|
+
sessionId: string | undefined,
|
|
85
|
+
workingDirectory: string,
|
|
86
|
+
options: {
|
|
87
|
+
sandboxed?: boolean
|
|
88
|
+
shell?: Pick<CommandShell, 'path' | 'windows'>
|
|
89
|
+
} = {},
|
|
90
|
+
): string {
|
|
91
|
+
const shell = options.shell ?? hostCommandShell()
|
|
92
|
+
if (options.sandboxed || sessionId === undefined || shell.windows || shell.path === undefined)
|
|
93
|
+
return workingDirectory
|
|
94
|
+
return startingDirectory(sessionId, workingDirectory)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reads what the finished command recorded and keeps it for the next call.
|
|
99
|
+
* Returns the notice to append when the directory was outside the roots.
|
|
100
|
+
*/
|
|
101
|
+
export async function recordDirectory(
|
|
102
|
+
sessionId: string,
|
|
103
|
+
file: string,
|
|
104
|
+
context: Parameters<typeof toolRoots>[0],
|
|
105
|
+
): Promise<string | undefined> {
|
|
106
|
+
let reported: string
|
|
107
|
+
try {
|
|
108
|
+
reported = (await readBoundedFile(file, 64 * 1024)).toString('utf8').trim()
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(file, { force: true })
|
|
113
|
+
}
|
|
114
|
+
if (reported === '') return undefined
|
|
115
|
+
const base = context.workingDirectory
|
|
116
|
+
const outside = await pathOutsideRoots(toolRoots(context), reported)
|
|
117
|
+
const next = outside === undefined ? reported : base
|
|
118
|
+
carried.delete(sessionId)
|
|
119
|
+
if (next !== base) {
|
|
120
|
+
carried.set(sessionId, { base, cwd: next })
|
|
121
|
+
if (carried.size > MAX_SESSIONS) {
|
|
122
|
+
const oldest = carried.keys().next().value
|
|
123
|
+
if (oldest !== undefined) carried.delete(oldest)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return outside === undefined ? undefined : cwdResetNotice(base)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** For tests: forget every session. */
|
|
130
|
+
export function forgetCarriedDirectories(): void {
|
|
131
|
+
carried.clear()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Enumerable symbol crosses the executor's context copies, never the model's
|
|
135
|
+
// JSON input. No public ToolContext field or caller-authored cwd is trusted.
|
|
136
|
+
const REVIEWED_CWD = Symbol('namzu.bash.reviewed-cwd')
|
|
137
|
+
|
|
138
|
+
export function pinBashWorkingDirectory(context: object, cwd: string): void {
|
|
139
|
+
Object.defineProperty(context, REVIEWED_CWD, { value: cwd, enumerable: true, configurable: true })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function bashWorkingDirectoryForExecution(context: {
|
|
143
|
+
sessionId?: string
|
|
144
|
+
workingDirectory: string
|
|
145
|
+
sandbox?: unknown
|
|
146
|
+
}): string {
|
|
147
|
+
const current = effectiveBashWorkingDirectory(context.sessionId, context.workingDirectory, {
|
|
148
|
+
sandboxed: context.sandbox !== undefined,
|
|
149
|
+
})
|
|
150
|
+
const reviewed = (context as { [REVIEWED_CWD]?: string })[REVIEWED_CWD]
|
|
151
|
+
if (reviewed !== undefined && current !== reviewed)
|
|
152
|
+
throw new Error(
|
|
153
|
+
'Shell working directory changed after this call was reviewed. Nothing ran. Retry the call so its paths can be checked in the current directory.',
|
|
154
|
+
)
|
|
155
|
+
return reviewed ?? current
|
|
156
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises'
|
|
1
2
|
import { z } from 'zod'
|
|
2
3
|
import { isReadOnlyCommandLine } from '../../authorization/readonly-commands.js'
|
|
3
4
|
import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
|
|
@@ -24,8 +25,15 @@ import { defineTool } from '../defineTool.js'
|
|
|
24
25
|
import { scrubInheritedEnv, withheldEnvNoticeText } from '../env-scrub.js'
|
|
25
26
|
import { createHostOutputDecoder } from '../host-output.js'
|
|
26
27
|
import { looksBinary, windowText } from '../text-window.js'
|
|
28
|
+
import {
|
|
29
|
+
bashWorkingDirectoryForExecution,
|
|
30
|
+
newCwdFile,
|
|
31
|
+
recordDirectory,
|
|
32
|
+
withCwdCapture,
|
|
33
|
+
} from './bash-cwd.js'
|
|
27
34
|
import { judgeExit } from './exit-semantics.js'
|
|
28
35
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
36
|
+
import { leadingLongSleep, longSleepRefusal } from './sleep-guard.js'
|
|
29
37
|
|
|
30
38
|
// Namzu owns its own bash timeout knob — `NAMZU_BASH_TIMEOUT_MS` — and
|
|
31
39
|
// reads no consumer's env name. A consumer that wants one knob aliases
|
|
@@ -526,7 +534,7 @@ export const SANDBOX_ESCAPE_NOT_APPROVED =
|
|
|
526
534
|
'dangerously_disable_sandbox was not approved for this call, so the command did not run. Leaving the sandbox needs the user to confirm it each time, and this turn either cannot ask or does not allow it. Run the command inside the sandbox, or tell the user what it needs and let them run it.'
|
|
527
535
|
|
|
528
536
|
const BASH_DESCRIPTION =
|
|
529
|
-
'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
|
|
537
|
+
'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. The working directory persists between calls, but shell state does not. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
|
|
530
538
|
|
|
531
539
|
const BashToolBase = defineTool({
|
|
532
540
|
name: 'bash',
|
|
@@ -579,6 +587,7 @@ const BashToolBase = defineTool({
|
|
|
579
587
|
},
|
|
580
588
|
|
|
581
589
|
async execute(input, context) {
|
|
590
|
+
const startDirectory = bashWorkingDirectoryForExecution(context)
|
|
582
591
|
if (isDangerousCommand(input.command)) {
|
|
583
592
|
return {
|
|
584
593
|
success: false,
|
|
@@ -588,6 +597,15 @@ const BashToolBase = defineTool({
|
|
|
588
597
|
}
|
|
589
598
|
}
|
|
590
599
|
|
|
600
|
+
// Where a job registry exists to wait on, a long leading sleep only
|
|
601
|
+
// holds the turn; it is refused with the better ways to wait.
|
|
602
|
+
if (!input.run_in_background && context.backgroundJobs && !backgroundTasksDisabled()) {
|
|
603
|
+
const longSleep = leadingLongSleep(input.command)
|
|
604
|
+
if (longSleep !== null) {
|
|
605
|
+
return { success: false, output: '', error: longSleepRefusal(longSleep) }
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
591
609
|
// Asked to leave the sandbox. Honoured only on the executor's word that
|
|
592
610
|
// a reviewer confirmed THIS call (`sandboxEscapeApproved`); the input
|
|
593
611
|
// alone is the model's request, never the permission. Without a
|
|
@@ -648,7 +666,7 @@ const BashToolBase = defineTool({
|
|
|
648
666
|
try {
|
|
649
667
|
const job = context.backgroundJobs.start({
|
|
650
668
|
command: input.command,
|
|
651
|
-
workingDirectory:
|
|
669
|
+
workingDirectory: startDirectory,
|
|
652
670
|
})
|
|
653
671
|
// `start` returns before the process exists, and a spawn that
|
|
654
672
|
// fails (no such shell, bad directory) reports a tick later.
|
|
@@ -799,38 +817,57 @@ const BashToolBase = defineTool({
|
|
|
799
817
|
let withdrawOffer: (() => void) | undefined
|
|
800
818
|
const offer = context.backgroundJobs?.registerForeground?.bind(context.backgroundJobs)
|
|
801
819
|
|
|
820
|
+
// The directory carries over between calls of a session on a POSIX host
|
|
821
|
+
// shell; the shell records where it ended, read back in `finally`.
|
|
822
|
+
const hostShell = hostCommandShell()
|
|
823
|
+
const carryDirectory =
|
|
824
|
+
context.sandbox === undefined &&
|
|
825
|
+
typeof context.sessionId === 'string' &&
|
|
826
|
+
hostShell.path !== undefined &&
|
|
827
|
+
hostShell.windows !== true
|
|
828
|
+
const cwdFile = carryDirectory ? newCwdFile() : undefined
|
|
829
|
+
let settled = false
|
|
830
|
+
const settle = async (): Promise<string | undefined> => {
|
|
831
|
+
if (cwdFile === undefined) return undefined
|
|
832
|
+
settled = true
|
|
833
|
+
return recordDirectory(context.sessionId, cwdFile, context)
|
|
834
|
+
}
|
|
835
|
+
|
|
802
836
|
try {
|
|
803
837
|
const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId, backgroundedBy } =
|
|
804
|
-
await execHostShell(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
? {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
838
|
+
await execHostShell(
|
|
839
|
+
cwdFile === undefined ? input.command : withCwdCapture(input.command, cwdFile),
|
|
840
|
+
{
|
|
841
|
+
cwd: startDirectory,
|
|
842
|
+
timeout: input.timeout,
|
|
843
|
+
env: { ...inherited.env, ...context.env },
|
|
844
|
+
maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
|
|
845
|
+
signal: context.abortSignal,
|
|
846
|
+
onOutput,
|
|
847
|
+
...(canMoveOnTimeout ? { onTimeout: adoptRunning } : {}),
|
|
848
|
+
...(canHandOff
|
|
849
|
+
? {
|
|
850
|
+
adopt: adoptRunning,
|
|
851
|
+
adoptOnInterrupt: true,
|
|
852
|
+
...(offer
|
|
853
|
+
? {
|
|
854
|
+
onRunning: (control: {
|
|
855
|
+
background(reason: 'user' | 'message'): boolean
|
|
856
|
+
}) => {
|
|
857
|
+
offerTimer = setTimeout(() => {
|
|
858
|
+
withdrawOffer = offer({
|
|
859
|
+
command: input.command,
|
|
860
|
+
background: (reason) => control.background(reason),
|
|
861
|
+
})
|
|
862
|
+
}, FOREGROUND_HANDOFF_AFTER_MS)
|
|
863
|
+
offerTimer.unref?.()
|
|
864
|
+
},
|
|
865
|
+
}
|
|
866
|
+
: {}),
|
|
867
|
+
}
|
|
868
|
+
: {}),
|
|
869
|
+
},
|
|
870
|
+
)
|
|
834
871
|
if (backgroundedJobId !== undefined) {
|
|
835
872
|
const timedOut = backgroundedBy === 'timeout'
|
|
836
873
|
return {
|
|
@@ -853,9 +890,10 @@ const BashToolBase = defineTool({
|
|
|
853
890
|
facts: { kind: 'command', command: input.command, background: true, continued: true },
|
|
854
891
|
}
|
|
855
892
|
}
|
|
893
|
+
const reset = await settle()
|
|
856
894
|
return {
|
|
857
895
|
success: true,
|
|
858
|
-
output: formatShellOutput(stdout, stderr) || '(no output)',
|
|
896
|
+
output: withResetNotice(formatShellOutput(stdout, stderr) || '(no output)', reset),
|
|
859
897
|
data: {
|
|
860
898
|
exitCode: 0,
|
|
861
899
|
...(leaveSandbox ? { sandboxed: false, sandboxEscape: true } : {}),
|
|
@@ -903,6 +941,7 @@ const BashToolBase = defineTool({
|
|
|
903
941
|
const judged =
|
|
904
942
|
!timedOut && exitCode !== undefined ? judgeExit(input.command, exitCode) : undefined
|
|
905
943
|
const answered = judged !== undefined && !judged.isError
|
|
944
|
+
const reset = await settle()
|
|
906
945
|
const output = [
|
|
907
946
|
formatShellOutput(failure.stdout, failure.stderr),
|
|
908
947
|
clipped.length > 0
|
|
@@ -914,7 +953,7 @@ const BashToolBase = defineTool({
|
|
|
914
953
|
|
|
915
954
|
return {
|
|
916
955
|
success: answered,
|
|
917
|
-
output: output || '(no output)',
|
|
956
|
+
output: withResetNotice(output || '(no output)', reset),
|
|
918
957
|
data: {
|
|
919
958
|
...(exitCode !== undefined ? { exitCode } : {}),
|
|
920
959
|
...(judged?.note ? { exitNote: judged.note } : {}),
|
|
@@ -949,6 +988,7 @@ const BashToolBase = defineTool({
|
|
|
949
988
|
}),
|
|
950
989
|
}
|
|
951
990
|
} finally {
|
|
991
|
+
if (cwdFile !== undefined && !settled) await rm(cwdFile, { force: true })
|
|
952
992
|
if (offerTimer !== undefined) clearTimeout(offerTimer)
|
|
953
993
|
withdrawOffer?.()
|
|
954
994
|
}
|
|
@@ -1002,6 +1042,11 @@ function formatShellOutput(stdout: string | undefined, stderr: string | undefine
|
|
|
1002
1042
|
.join('\n\n')
|
|
1003
1043
|
}
|
|
1004
1044
|
|
|
1045
|
+
/** The result text with the directory-reset line, when there is one, on its own last line. */
|
|
1046
|
+
function withResetNotice(output: string, notice: string | undefined): string {
|
|
1047
|
+
return notice === undefined ? output : `${output}\n${notice}`
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1005
1050
|
/**
|
|
1006
1051
|
* The notice text for the credential-shaped variables left out of a command's
|
|
1007
1052
|
* environment (see {@link withheldEnvNoticeText}). It is sent as a notice after
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { constants } from 'node:fs'
|
|
2
|
+
import { open } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
const CHUNK_BYTES = 64 * 1024
|
|
5
|
+
|
|
6
|
+
/** A hard input ceiling, independent of output windows and allow_large. */
|
|
7
|
+
export const TEXT_MAX_INPUT_BYTES = 32 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read at most the limit plus one sentinel byte, even if the file grows after
|
|
11
|
+
* stat. No allocation or request is derived from an untrusted file size.
|
|
12
|
+
* The first bounded chunk can choose a stricter limit from the file signature.
|
|
13
|
+
*/
|
|
14
|
+
export async function readBoundedChunks(
|
|
15
|
+
read: (offset: number, length: number) => Promise<Buffer>,
|
|
16
|
+
maximum: number,
|
|
17
|
+
signal?: AbortSignal,
|
|
18
|
+
selectLimit?: (prefix: Buffer) => number,
|
|
19
|
+
): Promise<Buffer> {
|
|
20
|
+
const chunks: Buffer[] = []
|
|
21
|
+
let offset = 0
|
|
22
|
+
let limit = maximum
|
|
23
|
+
for (;;) {
|
|
24
|
+
signal?.throwIfAborted()
|
|
25
|
+
const length = Math.min(CHUNK_BYTES, limit + 1 - offset)
|
|
26
|
+
const chunk = await read(offset, length)
|
|
27
|
+
signal?.throwIfAborted()
|
|
28
|
+
if (chunk.length > length)
|
|
29
|
+
throw new Error('The file backend returned more bytes than the requested bounded range.')
|
|
30
|
+
if (offset === 0 && selectLimit) limit = Math.min(maximum, selectLimit(chunk))
|
|
31
|
+
offset += chunk.length
|
|
32
|
+
if (offset > limit) throw inputTooLarge(limit)
|
|
33
|
+
chunks.push(chunk)
|
|
34
|
+
if (chunk.length < length) return Buffer.concat(chunks, offset)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readBoundedFile(
|
|
39
|
+
path: string,
|
|
40
|
+
maximum: number,
|
|
41
|
+
signal?: AbortSignal,
|
|
42
|
+
selectLimit?: (prefix: Buffer) => number,
|
|
43
|
+
): Promise<Buffer> {
|
|
44
|
+
// O_NONBLOCK prevents a substituted FIFO from blocking open before fstat.
|
|
45
|
+
signal?.throwIfAborted()
|
|
46
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK)
|
|
47
|
+
try {
|
|
48
|
+
const info = await file.stat()
|
|
49
|
+
if (!info.isFile()) throw new Error('Read requires a regular file.')
|
|
50
|
+
if (info.size > maximum) throw inputTooLarge(maximum)
|
|
51
|
+
return await readBoundedChunks(
|
|
52
|
+
async (offset, length) => {
|
|
53
|
+
const buffer = Buffer.allocUnsafe(length)
|
|
54
|
+
let used = 0
|
|
55
|
+
while (used < length) {
|
|
56
|
+
signal?.throwIfAborted()
|
|
57
|
+
const { bytesRead } = await file.read(buffer, used, length - used, offset + used)
|
|
58
|
+
if (bytesRead === 0) break
|
|
59
|
+
used += bytesRead
|
|
60
|
+
}
|
|
61
|
+
return buffer.subarray(0, used)
|
|
62
|
+
},
|
|
63
|
+
maximum,
|
|
64
|
+
signal,
|
|
65
|
+
(prefix) => {
|
|
66
|
+
const limit = selectLimit?.(prefix) ?? maximum
|
|
67
|
+
if (info.size > limit) throw inputTooLarge(limit)
|
|
68
|
+
return limit
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
} finally {
|
|
72
|
+
await file.close()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inputTooLarge(limit: number): Error {
|
|
77
|
+
return new Error(
|
|
78
|
+
`File exceeds the bounded read limit (${limit} bytes). Extract a smaller file with a shell command.`,
|
|
79
|
+
)
|
|
80
|
+
}
|