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.
- package/README.md +13 -2
- package/dist/application/subagent-service.d.ts +1 -1
- package/dist/application/subagent-service.js +33 -2
- package/dist/cli/interactive.d.ts +6 -1
- package/dist/cli/interactive.js +197 -5
- package/dist/cli/tui/claude-command-inventory.d.ts +35 -19
- package/dist/cli/tui/claude-command-inventory.js +98 -18
- package/dist/cli/tui/release-notes.d.ts +10 -0
- package/dist/cli/tui/release-notes.js +60 -0
- package/dist/cli/tui/sandbox-dashboard.d.ts +10 -0
- package/dist/cli/tui/sandbox-dashboard.js +78 -0
- package/dist/cli/tui/sandbox-settings.d.ts +32 -0
- package/dist/cli/tui/sandbox-settings.js +146 -0
- package/dist/cli/tui/slash-commands.js +10 -0
- package/dist/cli/tui/status-line.d.ts +88 -0
- package/dist/cli/tui/status-line.js +234 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +73 -3
- package/dist/extensions/claude-extensions.d.ts +1 -0
- package/dist/extensions/claude-extensions.js +55 -3
- package/dist/permissions/bash-path-safety.d.ts +2 -0
- package/dist/permissions/bash-path-safety.js +7 -0
- package/dist/permissions/claude-permission-resolver.d.ts +11 -0
- package/dist/permissions/claude-permission-resolver.js +15 -0
- package/dist/sandbox/claude-sandbox-runtime.d.ts +54 -0
- package/dist/sandbox/claude-sandbox-runtime.js +175 -0
- package/dist/sandbox/claude-sandbox-settings.d.ts +23 -0
- package/dist/sandbox/claude-sandbox-settings.js +286 -0
- package/dist/tools/local-tools.d.ts +18 -0
- package/dist/tools/local-tools.js +47 -8
- 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
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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.
|
|
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",
|