praxis-agent 0.20.4 → 0.20.7
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/dist/application/background-agent-manager.js +3 -14
- package/dist/cli/tui/claude-style.d.ts +2 -1
- package/dist/cli/tui/claude-style.js +226 -44
- package/dist/cli/tui/git-diff.js +67 -0
- package/dist/cli-runtime.d.ts +172 -0
- package/dist/cli-runtime.js +4434 -0
- package/dist/cli.d.ts +5 -164
- package/dist/cli.js +17 -4418
- package/dist/compatibility/claude/file-resources.js +6 -2
- package/dist/permissions/bash-path-safety.d.ts +1 -0
- package/dist/permissions/bash-path-safety.js +14 -0
- package/dist/permissions/bypass-immune-paths.d.ts +11 -0
- package/dist/permissions/bypass-immune-paths.js +86 -0
- package/dist/tools/local-tools.d.ts +7 -0
- package/dist/tools/local-tools.js +42 -1
- package/package.json +1 -1
|
@@ -87,8 +87,12 @@ async function downloadOne(resource, config) {
|
|
|
87
87
|
? AbortSignal.any([config.signal, timeoutSignal])
|
|
88
88
|
: timeoutSignal;
|
|
89
89
|
const response = await fetcher(filesEndpoint(config.baseUrl, resource.fileId), {
|
|
90
|
-
headers:
|
|
91
|
-
|
|
90
|
+
headers: {
|
|
91
|
+
...(config.headers ?? {
|
|
92
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
93
|
+
}),
|
|
94
|
+
'anthropic-version': config.headers?.['anthropic-version'] ?? '2023-06-01',
|
|
95
|
+
'anthropic-beta': config.headers?.['anthropic-beta'] ?? 'files-api-2025-04-14',
|
|
92
96
|
},
|
|
93
97
|
signal: requestSignal,
|
|
94
98
|
});
|
|
@@ -12,6 +12,7 @@ export interface BashPathSafetyOptions {
|
|
|
12
12
|
sandboxWriteConfig?: FsWriteRestrictionConfig;
|
|
13
13
|
permissionMode: 'acceptEdits' | 'auto' | 'bypassPermissions' | 'manual' | 'dontAsk' | 'plan' | 'default';
|
|
14
14
|
fileRule?: (operation: FileOperation, absolutePath: string) => RuleOutcome;
|
|
15
|
+
protectedWrite?: (absolutePath: string) => string | undefined;
|
|
15
16
|
platform?: NodeJS.Platform;
|
|
16
17
|
}
|
|
17
18
|
export type BashPathSafetyResult = {
|
|
@@ -421,6 +421,20 @@ function pathFailure(rawPath, operation, options, dangerous = false) {
|
|
|
421
421
|
: rawPath;
|
|
422
422
|
const absolutePath = expandPath(validationPath, options);
|
|
423
423
|
const pathsToCheck = pathRepresentations(absolutePath);
|
|
424
|
+
if (operation !== 'read' && options.protectedWrite !== undefined) {
|
|
425
|
+
for (const path of pathsToCheck) {
|
|
426
|
+
const protectedReason = options.protectedWrite(path);
|
|
427
|
+
if (protectedReason !== undefined) {
|
|
428
|
+
return {
|
|
429
|
+
safe: false,
|
|
430
|
+
behavior: 'deny',
|
|
431
|
+
reason: `Refusing to write protected path: ${protectedReason}`,
|
|
432
|
+
path: absolutePath,
|
|
433
|
+
operation,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
424
438
|
const rules = pathsToCheck.map((path) => options.fileRule?.(operation, path) ?? null);
|
|
425
439
|
if (rules.includes('deny')) {
|
|
426
440
|
return {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProtectedWriteOptions {
|
|
2
|
+
homeDirectory: string;
|
|
3
|
+
configRoot: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Returns a human-readable rejection reason when a write to `path` targets a
|
|
7
|
+
* bypass-immune protected credential or configuration file, or undefined when
|
|
8
|
+
* the write is not protected. Reads are never affected.
|
|
9
|
+
*/
|
|
10
|
+
export declare function protectedWritePathReason(path: string, options: ProtectedWriteOptions): string | undefined;
|
|
11
|
+
//# sourceMappingURL=bypass-immune-paths.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
const SSH_CREDENTIAL_BASENAMES = new Set([
|
|
4
|
+
'authorized_keys',
|
|
5
|
+
'id_rsa',
|
|
6
|
+
'id_ed25519',
|
|
7
|
+
'id_ecdsa',
|
|
8
|
+
'id_dsa',
|
|
9
|
+
]);
|
|
10
|
+
const AWS_CREDENTIAL_BASENAMES = new Set(['credentials']);
|
|
11
|
+
function isWithin(candidate, root) {
|
|
12
|
+
const child = relative(resolve(root), resolve(candidate));
|
|
13
|
+
return child === '' || (!child.startsWith('..') && !isAbsolute(child));
|
|
14
|
+
}
|
|
15
|
+
function pathRepresentations(path) {
|
|
16
|
+
const absolute = resolve(path);
|
|
17
|
+
const representations = new Set([absolute]);
|
|
18
|
+
let existing = absolute;
|
|
19
|
+
for (;;) {
|
|
20
|
+
try {
|
|
21
|
+
realpathSync(existing);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
const parent = dirname(existing);
|
|
26
|
+
if (parent === existing)
|
|
27
|
+
return [...representations];
|
|
28
|
+
existing = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
representations.add(resolve(realpathSync.native(existing), relative(existing, absolute)));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A disappearing or inaccessible path retains its lexical representation.
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
representations.add(realpathSync.native(absolute));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Non-existent targets are covered by their deepest existing ancestor.
|
|
42
|
+
}
|
|
43
|
+
return [...representations];
|
|
44
|
+
}
|
|
45
|
+
function lowerBasename(path) {
|
|
46
|
+
const segments = path.split(/[\\/]+/u).filter(Boolean);
|
|
47
|
+
return (segments.at(-1) ?? '').toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns a human-readable rejection reason when a write to `path` targets a
|
|
51
|
+
* bypass-immune protected credential or configuration file, or undefined when
|
|
52
|
+
* the write is not protected. Reads are never affected.
|
|
53
|
+
*/
|
|
54
|
+
export function protectedWritePathReason(path, options) {
|
|
55
|
+
const homeDirectory = resolve(options.homeDirectory);
|
|
56
|
+
const configRoot = resolve(options.configRoot);
|
|
57
|
+
const sshDirectory = resolve(homeDirectory, '.ssh');
|
|
58
|
+
const awsDirectory = resolve(homeDirectory, '.aws');
|
|
59
|
+
const sshDirectories = pathRepresentations(sshDirectory);
|
|
60
|
+
const awsDirectories = pathRepresentations(awsDirectory);
|
|
61
|
+
const configRoots = pathRepresentations(configRoot);
|
|
62
|
+
for (const representation of pathRepresentations(path)) {
|
|
63
|
+
const basename = lowerBasename(representation);
|
|
64
|
+
if (sshDirectories.some((root) => isWithin(representation, root))) {
|
|
65
|
+
return `${sshDirectory} is a protected SSH directory`;
|
|
66
|
+
}
|
|
67
|
+
if (awsDirectories.some((root) => isWithin(representation, root))) {
|
|
68
|
+
return `${awsDirectory} is a protected AWS credentials directory`;
|
|
69
|
+
}
|
|
70
|
+
if (SSH_CREDENTIAL_BASENAMES.has(basename)) {
|
|
71
|
+
return `${basename} is a protected SSH credential file`;
|
|
72
|
+
}
|
|
73
|
+
if (AWS_CREDENTIAL_BASENAMES.has(basename)) {
|
|
74
|
+
return `${basename} is a protected AWS credential file`;
|
|
75
|
+
}
|
|
76
|
+
if (basename === '.env' || basename.startsWith('.env.')) {
|
|
77
|
+
return '.env and .env.* files are protected';
|
|
78
|
+
}
|
|
79
|
+
if (configRoots.some((root) => isWithin(representation, root)) &&
|
|
80
|
+
(basename === 'settings.json' || basename.endsWith('.jsonl'))) {
|
|
81
|
+
return 'settings.json and .jsonl files in the Claude config root are protected';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=bypass-immune-paths.js.map
|
|
@@ -11,6 +11,8 @@ export interface LocalToolRegistryOptions {
|
|
|
11
11
|
enableReportFindings?: boolean;
|
|
12
12
|
environment?: Readonly<Record<string, string>>;
|
|
13
13
|
sandbox?: BashSandboxRuntime;
|
|
14
|
+
homeDirectory?: string;
|
|
15
|
+
configRoot?: string;
|
|
14
16
|
}
|
|
15
17
|
export interface BashSandboxRuntime {
|
|
16
18
|
shouldUseSandbox(input: {
|
|
@@ -41,7 +43,12 @@ export declare class LocalToolRegistry implements ToolRegistry {
|
|
|
41
43
|
private readonly enableReportFindings;
|
|
42
44
|
private readonly environment;
|
|
43
45
|
private readonly sandbox;
|
|
46
|
+
private readonly homeDirectory;
|
|
47
|
+
private readonly configRoot;
|
|
48
|
+
private readonly protectedWriteReason;
|
|
44
49
|
constructor(options: LocalToolRegistryOptions);
|
|
50
|
+
private assertProtectedWritePath;
|
|
51
|
+
private assertProtectedBashCommand;
|
|
45
52
|
private currentCwd;
|
|
46
53
|
definitions(): readonly ModelToolDefinition[];
|
|
47
54
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { mkdir, mkdtemp, open, realpath, stat, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import sharp from 'sharp';
|
|
6
6
|
import { commandShell, commandShellArguments, } from '../platform/command-shell.js';
|
|
7
7
|
import { BoundedProcessRunner, joinedProcessOutput, } from '../platform/bounded-process-runner.js';
|
|
@@ -9,6 +9,8 @@ import { globFiles } from './glob.js';
|
|
|
9
9
|
import { countLineChanges } from './line-changes.js';
|
|
10
10
|
import { editNotebook, formatNotebookForRead } from './notebook.js';
|
|
11
11
|
import { openPdf } from './pdf.js';
|
|
12
|
+
import { validateBashPathSafety } from '../permissions/bash-path-safety.js';
|
|
13
|
+
import { protectedWritePathReason } from '../permissions/bypass-immune-paths.js';
|
|
12
14
|
import { effectiveAdditionalDirectories } from '../permissions/permission-updates.js';
|
|
13
15
|
const REPORT_FINDINGS_DEFINITION = {
|
|
14
16
|
name: 'ReportFindings',
|
|
@@ -436,6 +438,9 @@ export class LocalToolRegistry {
|
|
|
436
438
|
enableReportFindings;
|
|
437
439
|
environment;
|
|
438
440
|
sandbox;
|
|
441
|
+
homeDirectory;
|
|
442
|
+
configRoot;
|
|
443
|
+
protectedWriteReason;
|
|
439
444
|
constructor(options) {
|
|
440
445
|
this.cwd = resolve(options.cwd);
|
|
441
446
|
this.cwdProvider = options.cwdProvider;
|
|
@@ -450,11 +455,43 @@ export class LocalToolRegistry {
|
|
|
450
455
|
this.enableReportFindings = options.enableReportFindings ?? false;
|
|
451
456
|
this.environment = options.environment;
|
|
452
457
|
this.sandbox = options.sandbox;
|
|
458
|
+
this.homeDirectory = options.homeDirectory
|
|
459
|
+
? resolve(options.homeDirectory)
|
|
460
|
+
: homedir();
|
|
461
|
+
this.configRoot = options.configRoot
|
|
462
|
+
? resolve(options.configRoot)
|
|
463
|
+
: process.env.CLAUDE_CONFIG_DIR
|
|
464
|
+
? resolve(process.env.CLAUDE_CONFIG_DIR)
|
|
465
|
+
: resolve(this.homeDirectory, '.claude');
|
|
466
|
+
this.protectedWriteReason = (absolutePath) => protectedWritePathReason(absolutePath, {
|
|
467
|
+
homeDirectory: this.homeDirectory,
|
|
468
|
+
configRoot: this.configRoot,
|
|
469
|
+
});
|
|
453
470
|
this.processRunner = new BoundedProcessRunner({
|
|
454
471
|
cwd: this.cwd,
|
|
455
472
|
maxOutputBytes: this.maxOutputBytes,
|
|
456
473
|
});
|
|
457
474
|
}
|
|
475
|
+
assertProtectedWritePath(filePath) {
|
|
476
|
+
const reason = this.protectedWriteReason(filePath);
|
|
477
|
+
if (reason !== undefined) {
|
|
478
|
+
throw new Error(`Refusing to write protected path: ${reason}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
assertProtectedBashCommand(command, context) {
|
|
482
|
+
const cwd = this.currentCwd(context);
|
|
483
|
+
const result = validateBashPathSafety(command, {
|
|
484
|
+
cwd,
|
|
485
|
+
homeDirectory: this.homeDirectory,
|
|
486
|
+
readRoots: [cwd],
|
|
487
|
+
writeRoots: [cwd],
|
|
488
|
+
permissionMode: 'bypassPermissions',
|
|
489
|
+
protectedWrite: this.protectedWriteReason,
|
|
490
|
+
});
|
|
491
|
+
if (!result.safe && result.behavior === 'deny') {
|
|
492
|
+
throw new Error(result.reason);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
458
495
|
currentCwd(context) {
|
|
459
496
|
return resolve(context?.cwd || this.cwdProvider?.() || this.cwd);
|
|
460
497
|
}
|
|
@@ -910,6 +947,7 @@ export class LocalToolRegistry {
|
|
|
910
947
|
}
|
|
911
948
|
async write(call) {
|
|
912
949
|
const filePath = stringInput(call.input, 'file_path');
|
|
950
|
+
this.assertProtectedWritePath(filePath);
|
|
913
951
|
const content = stringInput(call.input, 'content', true);
|
|
914
952
|
if (Buffer.byteLength(content) > this.maxFileBytes) {
|
|
915
953
|
throw new Error(`Content exceeds ${this.maxFileBytes} byte write limit`);
|
|
@@ -960,6 +998,7 @@ export class LocalToolRegistry {
|
|
|
960
998
|
}
|
|
961
999
|
async edit(call) {
|
|
962
1000
|
const filePath = stringInput(call.input, 'file_path');
|
|
1001
|
+
this.assertProtectedWritePath(filePath);
|
|
963
1002
|
const oldString = stringInput(call.input, 'old_string');
|
|
964
1003
|
const newString = stringInput(call.input, 'new_string', true);
|
|
965
1004
|
const handle = await open(filePath, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
@@ -1006,6 +1045,7 @@ export class LocalToolRegistry {
|
|
|
1006
1045
|
}
|
|
1007
1046
|
async notebookEdit(call) {
|
|
1008
1047
|
const filePath = stringInput(call.input, 'notebook_path');
|
|
1048
|
+
this.assertProtectedWritePath(filePath);
|
|
1009
1049
|
const handle = await open(filePath, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
1010
1050
|
try {
|
|
1011
1051
|
await this.assertStablePath(filePath);
|
|
@@ -1117,6 +1157,7 @@ export class LocalToolRegistry {
|
|
|
1117
1157
|
if (timeout === undefined)
|
|
1118
1158
|
throw new Error('Prepared Bash call has no timeout');
|
|
1119
1159
|
const rawCommand = stringInput(call.input, 'command');
|
|
1160
|
+
this.assertProtectedBashCommand(rawCommand, context);
|
|
1120
1161
|
const sandboxInput = {
|
|
1121
1162
|
command: rawCommand,
|
|
1122
1163
|
...(call.input.dangerouslyDisableSandbox === true
|