praxis-agent 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +13 -2
  2. package/dist/application/subagent-service.d.ts +1 -1
  3. package/dist/application/subagent-service.js +33 -2
  4. package/dist/cli/interactive.d.ts +6 -1
  5. package/dist/cli/interactive.js +197 -5
  6. package/dist/cli/tui/claude-command-inventory.d.ts +35 -19
  7. package/dist/cli/tui/claude-command-inventory.js +98 -18
  8. package/dist/cli/tui/release-notes.d.ts +10 -0
  9. package/dist/cli/tui/release-notes.js +60 -0
  10. package/dist/cli/tui/sandbox-dashboard.d.ts +10 -0
  11. package/dist/cli/tui/sandbox-dashboard.js +78 -0
  12. package/dist/cli/tui/sandbox-settings.d.ts +32 -0
  13. package/dist/cli/tui/sandbox-settings.js +146 -0
  14. package/dist/cli/tui/slash-commands.js +10 -0
  15. package/dist/cli/tui/status-line.d.ts +88 -0
  16. package/dist/cli/tui/status-line.js +234 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +73 -3
  19. package/dist/extensions/claude-extensions.d.ts +1 -0
  20. package/dist/extensions/claude-extensions.js +55 -3
  21. package/dist/permissions/bash-path-safety.d.ts +2 -0
  22. package/dist/permissions/bash-path-safety.js +7 -0
  23. package/dist/permissions/claude-permission-resolver.d.ts +11 -0
  24. package/dist/permissions/claude-permission-resolver.js +15 -0
  25. package/dist/sandbox/claude-sandbox-runtime.d.ts +54 -0
  26. package/dist/sandbox/claude-sandbox-runtime.js +175 -0
  27. package/dist/sandbox/claude-sandbox-settings.d.ts +23 -0
  28. package/dist/sandbox/claude-sandbox-settings.js +286 -0
  29. package/dist/tools/local-tools.d.ts +18 -0
  30. package/dist/tools/local-tools.js +47 -8
  31. package/package.json +2 -1
@@ -0,0 +1,175 @@
1
+ import { SandboxManager as BaseSandboxManager, } from '@anthropic-ai/sandbox-runtime';
2
+ import { rmSync } from 'node:fs';
3
+ import { shellPermissionMatchCandidates } from '../permissions/bash-normalization.js';
4
+ import { shellSubcommands } from '../permissions/permission-updates.js';
5
+ import { parseShellRule, shellRuleMatches, } from '../permissions/shell-rule-matching.js';
6
+ function currentPlatform() {
7
+ if (process.platform === 'darwin')
8
+ return 'macos';
9
+ if (process.platform === 'win32')
10
+ return 'windows';
11
+ if (process.platform === 'linux' &&
12
+ (process.env.WSL_DISTRO_NAME !== undefined ||
13
+ process.env.WSL_INTEROP !== undefined)) {
14
+ return 'wsl';
15
+ }
16
+ return 'linux';
17
+ }
18
+ function matchesExcludedCommand(command, patterns) {
19
+ if (patterns.length === 0)
20
+ return false;
21
+ let subcommands;
22
+ try {
23
+ subcommands = shellSubcommands(command);
24
+ }
25
+ catch {
26
+ subcommands = [command];
27
+ }
28
+ if (subcommands.length === 0)
29
+ subcommands = [command];
30
+ return subcommands.some((subcommand) => shellPermissionMatchCandidates(subcommand, true).some((candidate) => patterns.some((pattern) => shellRuleMatches(parseShellRule(pattern), candidate))));
31
+ }
32
+ export class ClaudeSandboxRuntime {
33
+ backend;
34
+ platform;
35
+ active = false;
36
+ initialized = false;
37
+ initializationError;
38
+ settings;
39
+ constructor(backend = BaseSandboxManager, platform = currentPlatform) {
40
+ this.backend = backend;
41
+ this.platform = platform;
42
+ }
43
+ async initialize(settings) {
44
+ this.initializationError = undefined;
45
+ if (this.initialized && this.active) {
46
+ this.settings = settings;
47
+ const unavailableReason = this.unavailableReason(settings);
48
+ if (!settings.enabled || unavailableReason) {
49
+ await this.backend.reset();
50
+ this.active = false;
51
+ if (unavailableReason && settings.failIfUnavailable) {
52
+ throw new Error(unavailableReason);
53
+ }
54
+ return;
55
+ }
56
+ this.backend.updateConfig(settings.runtimeConfig);
57
+ return;
58
+ }
59
+ this.settings = settings;
60
+ this.active = false;
61
+ this.initialized = true;
62
+ if (!settings.enabled)
63
+ return;
64
+ const unavailableReason = this.unavailableReason(settings);
65
+ if (unavailableReason) {
66
+ if (settings.failIfUnavailable)
67
+ throw new Error(unavailableReason);
68
+ return;
69
+ }
70
+ try {
71
+ await this.backend.initialize(settings.runtimeConfig);
72
+ this.active = true;
73
+ }
74
+ catch (error) {
75
+ this.initializationError =
76
+ error instanceof Error ? error.message : String(error);
77
+ if (settings.failIfUnavailable)
78
+ throw error;
79
+ }
80
+ }
81
+ isActive() {
82
+ return this.active;
83
+ }
84
+ isEnabledInSettings() {
85
+ return this.settings?.enabled ?? false;
86
+ }
87
+ isSupportedPlatform() {
88
+ return this.backend.isSupportedPlatform();
89
+ }
90
+ platformName() {
91
+ return this.platform();
92
+ }
93
+ dependencyCheck(settings = this.requireSettings()) {
94
+ const configuredRipgrep = settings.runtimeConfig.ripgrep;
95
+ return this.backend.checkDependencies(configuredRipgrep
96
+ ? {
97
+ command: configuredRipgrep.command,
98
+ ...(configuredRipgrep.args ? { args: configuredRipgrep.args } : {}),
99
+ }
100
+ : undefined);
101
+ }
102
+ autoAllowsBash() {
103
+ return this.active && (this.settings?.autoAllowBashIfSandboxed ?? true);
104
+ }
105
+ unavailableReason(settings = this.requireSettings()) {
106
+ if (!settings.enabled)
107
+ return undefined;
108
+ if (!this.backend.isSupportedPlatform()) {
109
+ return `sandbox.enabled is set but ${this.platform()} is not supported`;
110
+ }
111
+ if (settings.enabledPlatforms !== undefined &&
112
+ !settings.enabledPlatforms.includes(this.platform())) {
113
+ return `sandbox.enabled is set but ${this.platform()} is not in sandbox.enabledPlatforms`;
114
+ }
115
+ const dependencies = this.dependencyCheck(settings);
116
+ if (dependencies.errors.length > 0) {
117
+ return `sandbox.enabled is set but dependencies are missing: ${dependencies.errors.join(', ')}`;
118
+ }
119
+ if (this.initializationError) {
120
+ return `sandbox.enabled is set but initialization failed: ${this.initializationError}`;
121
+ }
122
+ return undefined;
123
+ }
124
+ shouldUseSandbox(input) {
125
+ if (!this.active)
126
+ return false;
127
+ const settings = this.requireSettings();
128
+ if (input.dangerouslyDisableSandbox === true &&
129
+ settings.allowUnsandboxedCommands) {
130
+ return false;
131
+ }
132
+ return !matchesExcludedCommand(input.command, settings.excludedCommands);
133
+ }
134
+ async wrapCommand(input, options = {}) {
135
+ if (!this.initialized)
136
+ throw new Error('Sandbox has not been initialized');
137
+ if (!this.shouldUseSandbox(input))
138
+ return input.command;
139
+ return this.backend.wrapWithSandbox(input.command, options.shell, undefined, options.signal, {
140
+ ...(options.commandId ? { commandId: options.commandId } : {}),
141
+ commandText: input.command,
142
+ });
143
+ }
144
+ annotateStderr(commandId, stderr) {
145
+ return this.backend.annotateStderrWithSandboxFailures(commandId, stderr);
146
+ }
147
+ cleanupAfterCommand() {
148
+ this.backend.cleanupAfterCommand();
149
+ for (const path of this.settings?.bareGitRepoScrubPaths ?? []) {
150
+ try {
151
+ rmSync(path, { recursive: true });
152
+ }
153
+ catch {
154
+ // Missing or concurrently protected paths are best-effort cleanup.
155
+ }
156
+ }
157
+ }
158
+ getFsWriteConfig() {
159
+ return this.active ? this.backend.getFsWriteConfig() : undefined;
160
+ }
161
+ async reset() {
162
+ this.active = false;
163
+ this.initialized = false;
164
+ this.initializationError = undefined;
165
+ this.settings = undefined;
166
+ await this.backend.reset();
167
+ }
168
+ requireSettings() {
169
+ if (!this.settings)
170
+ throw new Error('Sandbox has not been initialized');
171
+ return this.settings;
172
+ }
173
+ }
174
+ export const claudeSandboxRuntime = new ClaudeSandboxRuntime();
175
+ //# sourceMappingURL=claude-sandbox-runtime.js.map
@@ -0,0 +1,23 @@
1
+ import type { SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime';
2
+ import type { ClaudeJsonResource } from '../compatibility/claude/shared-resources.js';
3
+ export interface ClaudeSandboxSettings {
4
+ enabled: boolean;
5
+ failIfUnavailable: boolean;
6
+ autoAllowBashIfSandboxed: boolean;
7
+ allowUnsandboxedCommands: boolean;
8
+ enabledPlatforms?: readonly ('macos' | 'linux' | 'windows' | 'wsl')[];
9
+ excludedCommands: readonly string[];
10
+ bareGitRepoScrubPaths?: readonly string[];
11
+ runtimeConfig: SandboxRuntimeConfig;
12
+ }
13
+ export declare function claudeSandboxTempDirectory(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string;
14
+ export declare function loadClaudeSandboxSettings({ resources, cwd, originalCwd, configRoot, homeDirectory, additionalDirectories, tempDirectory, }: {
15
+ resources: readonly ClaudeJsonResource[];
16
+ cwd: string;
17
+ originalCwd?: string;
18
+ configRoot: string;
19
+ homeDirectory: string;
20
+ additionalDirectories?: readonly string[];
21
+ tempDirectory: string;
22
+ }): ClaudeSandboxSettings;
23
+ //# sourceMappingURL=claude-sandbox-settings.d.ts.map
@@ -0,0 +1,286 @@
1
+ import { dirname, isAbsolute, resolve, sep } from 'node:path';
2
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { permissionRuleValueFromString } from '../permissions/permission-updates.js';
5
+ export function claudeSandboxTempDirectory(environment = process.env, platform = process.platform) {
6
+ const base = environment.CLAUDE_CODE_TMPDIR || (platform === 'win32' ? tmpdir() : '/tmp');
7
+ let canonicalBase = base;
8
+ try {
9
+ canonicalBase = realpathSync(base);
10
+ }
11
+ catch {
12
+ // Preserve the configured path when it does not exist yet.
13
+ }
14
+ const name = platform === 'win32' ? 'claude' : `claude-${process.getuid?.() ?? 0}`;
15
+ return resolve(canonicalBase, name);
16
+ }
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ function booleanField(value, key, fallback, path) {
21
+ const field = value[key];
22
+ if (field === undefined)
23
+ return fallback;
24
+ if (typeof field !== 'boolean')
25
+ throw new Error(`sandbox.${key} must be a boolean: ${path}`);
26
+ return field;
27
+ }
28
+ function stringArray(value, key, path) {
29
+ const field = value[key];
30
+ if (field === undefined)
31
+ return [];
32
+ if (!Array.isArray(field) || field.some((item) => typeof item !== 'string'))
33
+ throw new Error(`sandbox.${key} must be an array of strings: ${path}`);
34
+ return field;
35
+ }
36
+ function appendUnique(target, values) {
37
+ for (const value of values)
38
+ if (!target.includes(value))
39
+ target.push(value);
40
+ }
41
+ function resourceRoot(resource) {
42
+ return resource.scope === 'user'
43
+ ? dirname(resource.path)
44
+ : resolve(dirname(resource.path), '..');
45
+ }
46
+ function permissionPath(pattern, resource) {
47
+ if (pattern.startsWith('//'))
48
+ return pattern.slice(1);
49
+ if (pattern.startsWith('/') && !pattern.startsWith('//')) {
50
+ return resolve(resourceRoot(resource), pattern.slice(1));
51
+ }
52
+ return pattern;
53
+ }
54
+ function filesystemPath(pattern, resource, homeDirectory) {
55
+ if (pattern.startsWith('//'))
56
+ return pattern.slice(1);
57
+ if (pattern === '~' || pattern.startsWith('~/')) {
58
+ return resolve(homeDirectory, pattern.slice(2));
59
+ }
60
+ return isAbsolute(pattern)
61
+ ? resolve(pattern)
62
+ : resolve(resourceRoot(resource), pattern);
63
+ }
64
+ function additionalDirectoryPath(path, cwd, homeDirectory) {
65
+ if (path === '~' || path.startsWith('~/'))
66
+ return resolve(homeDirectory, path.slice(2));
67
+ return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
68
+ }
69
+ function nestedRecord(value, key, path) {
70
+ const field = value[key];
71
+ if (field === undefined)
72
+ return undefined;
73
+ if (!isRecord(field))
74
+ throw new Error(`sandbox.${key} must be an object: ${path}`);
75
+ return field;
76
+ }
77
+ function worktreeMainRepository(cwd) {
78
+ try {
79
+ const match = /^gitdir:\s*(.+)$/mu.exec(readFileSync(resolve(cwd, '.git'), 'utf8'));
80
+ if (!match?.[1])
81
+ return undefined;
82
+ const gitDirectory = resolve(cwd, match[1].trim());
83
+ const marker = `${sep}.git${sep}worktrees${sep}`;
84
+ const markerIndex = gitDirectory.lastIndexOf(marker);
85
+ return markerIndex > 0 ? gitDirectory.slice(0, markerIndex) : undefined;
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ export function loadClaudeSandboxSettings({ resources, cwd, originalCwd = cwd, configRoot, homeDirectory, additionalDirectories = [], tempDirectory, }) {
92
+ let enabled = false;
93
+ let failIfUnavailable = false;
94
+ let autoAllowBashIfSandboxed = true;
95
+ let allowUnsandboxedCommands = true;
96
+ let enabledPlatforms;
97
+ const excludedCommands = [];
98
+ const allowedDomains = [];
99
+ const deniedDomains = [];
100
+ const allowWrite = ['.', tempDirectory, ...additionalDirectories];
101
+ const protectedDirectories = [
102
+ ...new Set([resolve(originalCwd), resolve(cwd)]),
103
+ ];
104
+ const denyWrite = [resolve(configRoot, 'settings.json')];
105
+ for (const directory of protectedDirectories) {
106
+ denyWrite.push(resolve(directory, '.claude', 'settings.json'), resolve(directory, '.claude', 'settings.local.json'), resolve(directory, '.claude', 'commands'), resolve(directory, '.claude', 'agents'), resolve(directory, '.claude', 'skills'));
107
+ }
108
+ const denyRead = [];
109
+ const allowRead = [];
110
+ let allowUnixSockets;
111
+ let allowAllUnixSockets;
112
+ let allowLocalBinding;
113
+ let httpProxyPort;
114
+ let socksProxyPort;
115
+ let ignoreViolations;
116
+ let enableWeakerNestedSandbox;
117
+ let enableWeakerNetworkIsolation;
118
+ let ripgrep;
119
+ const bareGitRepoScrubPaths = [];
120
+ const worktreeRepository = worktreeMainRepository(cwd);
121
+ if (worktreeRepository && worktreeRepository !== resolve(cwd)) {
122
+ appendUnique(allowWrite, [worktreeRepository]);
123
+ }
124
+ for (const directory of protectedDirectories) {
125
+ for (const name of ['HEAD', 'objects', 'refs', 'hooks', 'config']) {
126
+ const path = resolve(directory, name);
127
+ try {
128
+ statSync(path);
129
+ appendUnique(denyWrite, [path]);
130
+ }
131
+ catch {
132
+ bareGitRepoScrubPaths.push(path);
133
+ }
134
+ }
135
+ }
136
+ for (const resource of resources) {
137
+ if (!isRecord(resource.value))
138
+ continue;
139
+ const permissions = isRecord(resource.value.permissions)
140
+ ? resource.value.permissions
141
+ : undefined;
142
+ if (permissions?.additionalDirectories !== undefined) {
143
+ if (!Array.isArray(permissions.additionalDirectories) ||
144
+ permissions.additionalDirectories.some((directory) => typeof directory !== 'string')) {
145
+ throw new Error(`permissions.additionalDirectories must be an array of strings: ${resource.path}`);
146
+ }
147
+ appendUnique(allowWrite, permissions.additionalDirectories.map((directory) => additionalDirectoryPath(directory, cwd, homeDirectory)));
148
+ }
149
+ for (const behavior of ['allow', 'deny']) {
150
+ const rawRules = permissions?.[behavior];
151
+ if (rawRules === undefined)
152
+ continue;
153
+ if (!Array.isArray(rawRules) ||
154
+ rawRules.some((rule) => typeof rule !== 'string')) {
155
+ throw new Error(`permissions.${behavior} must be an array of strings: ${resource.path}`);
156
+ }
157
+ for (const rawRule of rawRules) {
158
+ const rule = permissionRuleValueFromString(rawRule);
159
+ if (rule.toolName === 'WebFetch' &&
160
+ rule.ruleContent?.startsWith('domain:')) {
161
+ appendUnique(behavior === 'allow' ? allowedDomains : deniedDomains, [
162
+ rule.ruleContent.slice('domain:'.length),
163
+ ]);
164
+ }
165
+ if (rule.toolName === 'Edit' && rule.ruleContent) {
166
+ appendUnique(behavior === 'allow' ? allowWrite : denyWrite, [
167
+ permissionPath(rule.ruleContent, resource),
168
+ ]);
169
+ }
170
+ if (behavior === 'deny' &&
171
+ rule.toolName === 'Read' &&
172
+ rule.ruleContent) {
173
+ appendUnique(denyRead, [permissionPath(rule.ruleContent, resource)]);
174
+ }
175
+ }
176
+ }
177
+ const sandbox = resource.value.sandbox;
178
+ if (sandbox === undefined)
179
+ continue;
180
+ if (!isRecord(sandbox))
181
+ throw new Error(`sandbox must be an object: ${resource.path}`);
182
+ enabled = booleanField(sandbox, 'enabled', enabled, resource.path);
183
+ failIfUnavailable = booleanField(sandbox, 'failIfUnavailable', failIfUnavailable, resource.path);
184
+ autoAllowBashIfSandboxed = booleanField(sandbox, 'autoAllowBashIfSandboxed', autoAllowBashIfSandboxed, resource.path);
185
+ allowUnsandboxedCommands = booleanField(sandbox, 'allowUnsandboxedCommands', allowUnsandboxedCommands, resource.path);
186
+ appendUnique(excludedCommands, stringArray(sandbox, 'excludedCommands', resource.path));
187
+ const platforms = sandbox.enabledPlatforms;
188
+ if (platforms !== undefined) {
189
+ if (!Array.isArray(platforms) ||
190
+ platforms.some((platform) => !['macos', 'linux', 'windows', 'wsl'].includes(String(platform)))) {
191
+ throw new Error(`sandbox.enabledPlatforms contains an unsupported platform: ${resource.path}`);
192
+ }
193
+ enabledPlatforms = platforms;
194
+ }
195
+ const network = nestedRecord(sandbox, 'network', resource.path);
196
+ if (network) {
197
+ appendUnique(allowedDomains, stringArray(network, 'allowedDomains', resource.path));
198
+ const unixSockets = stringArray(network, 'allowUnixSockets', resource.path);
199
+ if (unixSockets.length > 0) {
200
+ allowUnixSockets ??= [];
201
+ appendUnique(allowUnixSockets, unixSockets);
202
+ }
203
+ allowAllUnixSockets = booleanField(network, 'allowAllUnixSockets', allowAllUnixSockets ?? false, resource.path);
204
+ allowLocalBinding = booleanField(network, 'allowLocalBinding', allowLocalBinding ?? false, resource.path);
205
+ for (const key of ['httpProxyPort', 'socksProxyPort']) {
206
+ const value = network[key];
207
+ if (value !== undefined && (typeof value !== 'number' || value <= 0)) {
208
+ throw new Error(`sandbox.network.${key} must be a positive number: ${resource.path}`);
209
+ }
210
+ }
211
+ if (typeof network.httpProxyPort === 'number')
212
+ httpProxyPort = network.httpProxyPort;
213
+ if (typeof network.socksProxyPort === 'number')
214
+ socksProxyPort = network.socksProxyPort;
215
+ }
216
+ const filesystem = nestedRecord(sandbox, 'filesystem', resource.path);
217
+ if (filesystem) {
218
+ for (const [key, target] of [
219
+ ['allowWrite', allowWrite],
220
+ ['denyWrite', denyWrite],
221
+ ['denyRead', denyRead],
222
+ ['allowRead', allowRead],
223
+ ]) {
224
+ appendUnique(target, stringArray(filesystem, key, resource.path).map((path) => filesystemPath(path, resource, homeDirectory)));
225
+ }
226
+ }
227
+ const ignored = sandbox.ignoreViolations;
228
+ if (ignored !== undefined) {
229
+ if (!isRecord(ignored))
230
+ throw new Error(`sandbox.ignoreViolations must be an object: ${resource.path}`);
231
+ ignoreViolations = { ...(ignoreViolations ?? {}) };
232
+ for (const [command, values] of Object.entries(ignored)) {
233
+ if (!Array.isArray(values) ||
234
+ values.some((value) => typeof value !== 'string')) {
235
+ throw new Error(`sandbox.ignoreViolations.${command} must be an array of strings: ${resource.path}`);
236
+ }
237
+ ignoreViolations[command] = [
238
+ ...new Set([...(ignoreViolations[command] ?? []), ...values]),
239
+ ];
240
+ }
241
+ }
242
+ enableWeakerNestedSandbox = booleanField(sandbox, 'enableWeakerNestedSandbox', enableWeakerNestedSandbox ?? false, resource.path);
243
+ enableWeakerNetworkIsolation = booleanField(sandbox, 'enableWeakerNetworkIsolation', enableWeakerNetworkIsolation ?? false, resource.path);
244
+ const configuredRipgrep = nestedRecord(sandbox, 'ripgrep', resource.path);
245
+ if (configuredRipgrep) {
246
+ if (typeof configuredRipgrep.command !== 'string') {
247
+ throw new Error(`sandbox.ripgrep.command must be a string: ${resource.path}`);
248
+ }
249
+ const args = stringArray(configuredRipgrep, 'args', resource.path);
250
+ ripgrep = {
251
+ command: configuredRipgrep.command,
252
+ ...(args.length > 0 ? { args } : {}),
253
+ };
254
+ }
255
+ }
256
+ return {
257
+ enabled,
258
+ failIfUnavailable,
259
+ autoAllowBashIfSandboxed,
260
+ allowUnsandboxedCommands,
261
+ ...(enabledPlatforms ? { enabledPlatforms } : {}),
262
+ excludedCommands,
263
+ bareGitRepoScrubPaths,
264
+ runtimeConfig: {
265
+ network: {
266
+ allowedDomains,
267
+ deniedDomains,
268
+ ...(allowUnixSockets ? { allowUnixSockets } : {}),
269
+ ...(allowAllUnixSockets === undefined ? {} : { allowAllUnixSockets }),
270
+ ...(allowLocalBinding === undefined ? {} : { allowLocalBinding }),
271
+ ...(httpProxyPort === undefined ? {} : { httpProxyPort }),
272
+ ...(socksProxyPort === undefined ? {} : { socksProxyPort }),
273
+ },
274
+ filesystem: { allowWrite, denyWrite, denyRead, allowRead },
275
+ ...(ignoreViolations ? { ignoreViolations } : {}),
276
+ ...(enableWeakerNestedSandbox === undefined
277
+ ? {}
278
+ : { enableWeakerNestedSandbox }),
279
+ ...(enableWeakerNetworkIsolation === undefined
280
+ ? {}
281
+ : { enableWeakerNetworkIsolation }),
282
+ ...(ripgrep ? { ripgrep } : {}),
283
+ },
284
+ };
285
+ }
286
+ //# sourceMappingURL=claude-sandbox-settings.js.map
@@ -10,6 +10,23 @@ export interface LocalToolRegistryOptions {
10
10
  maxShellTimeoutMs?: number;
11
11
  enableReportFindings?: boolean;
12
12
  environment?: Readonly<Record<string, string>>;
13
+ sandbox?: BashSandboxRuntime;
14
+ }
15
+ export interface BashSandboxRuntime {
16
+ shouldUseSandbox(input: {
17
+ command: string;
18
+ dangerouslyDisableSandbox?: boolean;
19
+ }): boolean;
20
+ wrapCommand(input: {
21
+ command: string;
22
+ dangerouslyDisableSandbox?: boolean;
23
+ }, options?: {
24
+ shell?: string;
25
+ signal?: AbortSignal;
26
+ commandId?: string;
27
+ }): Promise<string>;
28
+ annotateStderr(commandId: string, stderr: string): string;
29
+ cleanupAfterCommand(): void;
13
30
  }
14
31
  export declare class LocalToolRegistry implements ToolRegistry {
15
32
  private readonly cwd;
@@ -23,6 +40,7 @@ export declare class LocalToolRegistry implements ToolRegistry {
23
40
  private readonly processRunner;
24
41
  private readonly enableReportFindings;
25
42
  private readonly environment;
43
+ private readonly sandbox;
26
44
  constructor(options: LocalToolRegistryOptions);
27
45
  private currentCwd;
28
46
  definitions(): readonly ModelToolDefinition[];
@@ -434,6 +434,7 @@ export class LocalToolRegistry {
434
434
  processRunner;
435
435
  enableReportFindings;
436
436
  environment;
437
+ sandbox;
437
438
  constructor(options) {
438
439
  this.cwd = resolve(options.cwd);
439
440
  this.cwdProvider = options.cwdProvider;
@@ -447,6 +448,7 @@ export class LocalToolRegistry {
447
448
  this.maxShellTimeoutMs = options.maxShellTimeoutMs ?? 120_000;
448
449
  this.enableReportFindings = options.enableReportFindings ?? false;
449
450
  this.environment = options.environment;
451
+ this.sandbox = options.sandbox;
450
452
  this.processRunner = new BoundedProcessRunner({
451
453
  cwd: this.cwd,
452
454
  maxOutputBytes: this.maxOutputBytes,
@@ -586,11 +588,19 @@ export class LocalToolRegistry {
586
588
  }
587
589
  case 'Bash': {
588
590
  const requestedTimeout = optionalPositiveInteger(call.input, 'timeout');
591
+ const dangerouslyDisableSandbox = call.input.dangerouslyDisableSandbox;
592
+ if (dangerouslyDisableSandbox !== undefined &&
593
+ typeof dangerouslyDisableSandbox !== 'boolean') {
594
+ throw new Error('dangerouslyDisableSandbox must be a boolean');
595
+ }
589
596
  return {
590
597
  ...call,
591
598
  input: {
592
599
  command: stringInput(call.input, 'command'),
593
600
  timeout: Math.min(requestedTimeout ?? this.maxShellTimeoutMs, this.maxShellTimeoutMs),
601
+ ...(dangerouslyDisableSandbox === undefined
602
+ ? {}
603
+ : { dangerouslyDisableSandbox }),
594
604
  },
595
605
  };
596
606
  }
@@ -1077,14 +1087,43 @@ export class LocalToolRegistry {
1077
1087
  const timeout = optionalPositiveInteger(call.input, 'timeout');
1078
1088
  if (timeout === undefined)
1079
1089
  throw new Error('Prepared Bash call has no timeout');
1080
- const result = await this.processRunner.run({
1081
- command: commandShell(),
1082
- args: commandShellArguments(stringInput(call.input, 'command')),
1083
- timeoutMs: timeout,
1084
- cwd: this.currentCwd(context),
1085
- ...(this.environment ? { env: this.environment } : {}),
1086
- ...(context.signal ? { signal: context.signal } : {}),
1087
- });
1090
+ const rawCommand = stringInput(call.input, 'command');
1091
+ const sandboxInput = {
1092
+ command: rawCommand,
1093
+ ...(call.input.dangerouslyDisableSandbox === true
1094
+ ? { dangerouslyDisableSandbox: true }
1095
+ : {}),
1096
+ };
1097
+ const sandboxed = this.sandbox?.shouldUseSandbox(sandboxInput) ?? false;
1098
+ const shell = commandShell();
1099
+ let result;
1100
+ try {
1101
+ const command = sandboxed
1102
+ ? await this.sandbox?.wrapCommand(sandboxInput, {
1103
+ shell,
1104
+ ...(context.signal ? { signal: context.signal } : {}),
1105
+ commandId: call.id,
1106
+ })
1107
+ : rawCommand;
1108
+ result = await this.processRunner.run({
1109
+ command: shell,
1110
+ args: commandShellArguments(command ?? rawCommand),
1111
+ timeoutMs: timeout,
1112
+ cwd: this.currentCwd(context),
1113
+ ...(this.environment ? { env: this.environment } : {}),
1114
+ ...(context.signal ? { signal: context.signal } : {}),
1115
+ });
1116
+ }
1117
+ finally {
1118
+ if (sandboxed)
1119
+ this.sandbox?.cleanupAfterCommand();
1120
+ }
1121
+ if (sandboxed && this.sandbox) {
1122
+ result = {
1123
+ ...result,
1124
+ stderr: this.sandbox.annotateStderr(call.id, result.stderr),
1125
+ };
1126
+ }
1088
1127
  if (result.timedOut) {
1089
1128
  return {
1090
1129
  content: `Command timed out after ${timeout}ms`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -111,6 +111,7 @@
111
111
  "provenance": true
112
112
  },
113
113
  "dependencies": {
114
+ "@anthropic-ai/sandbox-runtime": "^0.0.73",
114
115
  "@anthropic-ai/tokenizer": "^0.0.4",
115
116
  "@modelcontextprotocol/sdk": "^1.30.0",
116
117
  "acorn": "^8.18.0",