praxis-agent 0.55.3 → 0.57.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 +3 -1
- package/dist/application/session-service.js +53 -14
- package/dist/application/subagent-service.js +1 -0
- package/dist/cli/interactive.js +2 -2
- package/dist/cli/process-signal.d.ts +3 -0
- package/dist/cli/process-signal.js +5 -0
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +21 -2
- package/dist/cli-runtime.js +11 -1
- package/dist/cli.js +4 -2
- package/dist/evals/eval-contract.js +9 -1
- package/dist/mcp/praxis-mcp-server.js +13 -1
- package/dist/permissions/claude-permission-resolver.js +147 -4
- package/dist/platform/atomic-write.js +2 -0
- package/dist/tools/apply-patch.d.ts +25 -0
- package/dist/tools/apply-patch.js +128 -0
- package/dist/tools/claude-interactive-tools.js +8 -0
- package/dist/tools/claude-lsp-tool.d.ts +3 -0
- package/dist/tools/claude-lsp-tool.js +251 -26
- package/dist/tools/local-tools.d.ts +1 -0
- package/dist/tools/local-tools.js +102 -1
- package/dist/tools/lsp-diagnostics.d.ts +20 -0
- package/dist/tools/lsp-diagnostics.js +173 -0
- package/dist/tools/team-member-tools.js +5 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -172,7 +172,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
172
172
|
deterministic resize-aware URL/form elicitation rendering, and measured
|
|
173
173
|
context budgets; print mode,
|
|
174
174
|
structured JSON/JSONL, context compaction, tool loops, and bounded execution.
|
|
175
|
-
- **Built-in tools** — read, write, edit,
|
|
175
|
+
- **Built-in tools** — read, write, edit, `ApplyPatch` for bounded ordered exact
|
|
176
|
+
multi-file replacements, configured plugin LSP navigation with fresh bounded
|
|
177
|
+
diagnostics after successful edits, glob, search, shell, notebook, PDF,
|
|
176
178
|
image, web, scheduled prompts, workflows, and worktrees.
|
|
177
179
|
- **Shell lifecycle** — foreground Bash allows up to 10 minutes and carries a
|
|
178
180
|
validated final working directory across calls in the same session without
|
|
@@ -51,6 +51,7 @@ import { completeMeteredModelRequest } from './metered-model-completion.js';
|
|
|
51
51
|
import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, } from './session-memory.js';
|
|
52
52
|
import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
|
|
53
53
|
import { DeferredToolCatalog } from '../tools/deferred-tool-catalog.js';
|
|
54
|
+
import { parseApplyPatchInput } from '../tools/apply-patch.js';
|
|
54
55
|
import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
|
|
55
56
|
import { generateToolUseSummary } from './tool-use-summary.js';
|
|
56
57
|
import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
|
|
@@ -88,6 +89,21 @@ function mainAgentToolNames(tools, agent) {
|
|
|
88
89
|
.map(({ name }) => name)
|
|
89
90
|
.filter((name) => (!requested || requested.has(name)) && !disallowed.has(name));
|
|
90
91
|
}
|
|
92
|
+
function mutationPaths(call) {
|
|
93
|
+
if (call.name === 'ApplyPatch')
|
|
94
|
+
return parseApplyPatchInput(call.input).map((edit) => edit.file_path);
|
|
95
|
+
if (call.name === 'Write' || call.name === 'Edit') {
|
|
96
|
+
return typeof call.input.file_path === 'string'
|
|
97
|
+
? [call.input.file_path]
|
|
98
|
+
: [];
|
|
99
|
+
}
|
|
100
|
+
if (call.name === 'NotebookEdit') {
|
|
101
|
+
return typeof call.input.notebook_path === 'string'
|
|
102
|
+
? [call.input.notebook_path]
|
|
103
|
+
: [];
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
91
107
|
function isSessionCandidateError(error) {
|
|
92
108
|
return (error instanceof NativeTranscriptIndexCandidateError ||
|
|
93
109
|
['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? ''));
|
|
@@ -1168,6 +1184,7 @@ export class ClaudeSessionService {
|
|
|
1168
1184
|
'Read',
|
|
1169
1185
|
'LSP',
|
|
1170
1186
|
'Edit',
|
|
1187
|
+
'ApplyPatch',
|
|
1171
1188
|
'Write',
|
|
1172
1189
|
'NotebookEdit',
|
|
1173
1190
|
'WebFetch',
|
|
@@ -2869,16 +2886,18 @@ export class ClaudeSessionService {
|
|
|
2869
2886
|
}),
|
|
2870
2887
|
prepare: (call, context) => interactiveMessageTools.prepare(call, context),
|
|
2871
2888
|
execute: async (call, context) => {
|
|
2872
|
-
const
|
|
2873
|
-
|
|
2874
|
-
: call.name === 'NotebookEdit'
|
|
2875
|
-
? call.input.notebook_path
|
|
2876
|
-
: undefined;
|
|
2877
|
-
if (typeof path !== 'string') {
|
|
2889
|
+
const paths = mutationPaths(call);
|
|
2890
|
+
if (paths.length === 0) {
|
|
2878
2891
|
return interactiveMessageTools.execute(call, context);
|
|
2879
2892
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2893
|
+
const historyPaths = call.name === 'Write' ||
|
|
2894
|
+
call.name === 'Edit' ||
|
|
2895
|
+
call.name === 'ApplyPatch'
|
|
2896
|
+
? (await Promise.all(paths.map(async (path) => (await this.options.interactiveTools?.isPlanFile(sessionId, path))
|
|
2897
|
+
? null
|
|
2898
|
+
: path))).filter((path) => path !== null)
|
|
2899
|
+
: paths;
|
|
2900
|
+
if (historyPaths.length === 0) {
|
|
2882
2901
|
return interactiveMessageTools.execute(call, context);
|
|
2883
2902
|
}
|
|
2884
2903
|
const snapshotMessageId = currentPromptId ??
|
|
@@ -2888,21 +2907,31 @@ export class ClaudeSessionService {
|
|
|
2888
2907
|
if (!snapshotMessageId || !assistantMessageId) {
|
|
2889
2908
|
throw new Error('Claude file history could not link tool call');
|
|
2890
2909
|
}
|
|
2891
|
-
const
|
|
2910
|
+
const preparedMutations = [];
|
|
2911
|
+
try {
|
|
2912
|
+
for (const path of [...new Set(historyPaths)])
|
|
2913
|
+
preparedMutations.push(await fileHistory.prepareMutation(projectionSnapshot().entries, snapshotMessageId, path));
|
|
2914
|
+
}
|
|
2915
|
+
catch (error) {
|
|
2916
|
+
await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
|
|
2917
|
+
throw error;
|
|
2918
|
+
}
|
|
2892
2919
|
let result;
|
|
2893
2920
|
try {
|
|
2894
2921
|
result = await interactiveMessageTools.execute(call, context);
|
|
2895
2922
|
}
|
|
2896
2923
|
catch (error) {
|
|
2897
|
-
await
|
|
2924
|
+
await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
|
|
2898
2925
|
throw error;
|
|
2899
2926
|
}
|
|
2900
2927
|
if (result.isError) {
|
|
2901
|
-
await
|
|
2928
|
+
await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
|
|
2902
2929
|
return result;
|
|
2903
2930
|
}
|
|
2904
|
-
const
|
|
2905
|
-
|
|
2931
|
+
const entries = preparedMutations
|
|
2932
|
+
.map((mutation) => mutation.commit(assistantMessageId))
|
|
2933
|
+
.filter((entry) => entry !== null);
|
|
2934
|
+
for (const entry of entries) {
|
|
2906
2935
|
await persistence.commit({
|
|
2907
2936
|
kind: 'messages',
|
|
2908
2937
|
input: {
|
|
@@ -3105,7 +3134,9 @@ export class ClaudeSessionService {
|
|
|
3105
3134
|
if (call.name === 'Read') {
|
|
3106
3135
|
turnMemory.recordRead(path);
|
|
3107
3136
|
}
|
|
3108
|
-
else if (call.name === 'Write' ||
|
|
3137
|
+
else if (call.name === 'Write' ||
|
|
3138
|
+
call.name === 'Edit' ||
|
|
3139
|
+
call.name === 'ApplyPatch') {
|
|
3109
3140
|
projectMemoryMaintained = true;
|
|
3110
3141
|
}
|
|
3111
3142
|
}
|
|
@@ -3117,6 +3148,14 @@ export class ClaudeSessionService {
|
|
|
3117
3148
|
}
|
|
3118
3149
|
}
|
|
3119
3150
|
}
|
|
3151
|
+
else if (call.name === 'ApplyPatch') {
|
|
3152
|
+
for (const accessedPath of toolResult.accessedPaths ?? []) {
|
|
3153
|
+
const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
|
|
3154
|
+
if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
|
|
3155
|
+
projectMemoryMaintained = true;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3120
3159
|
}
|
|
3121
3160
|
if (toolResult.isError ||
|
|
3122
3161
|
call.name !== 'Read' ||
|
package/dist/cli/interactive.js
CHANGED
|
@@ -1885,7 +1885,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1885
1885
|
}
|
|
1886
1886
|
break;
|
|
1887
1887
|
case 'tool-call':
|
|
1888
|
-
if (['Edit', 'Write', 'NotebookEdit'].includes(event.call.name))
|
|
1888
|
+
if (['Edit', 'Write', 'ApplyPatch', 'NotebookEdit'].includes(event.call.name))
|
|
1889
1889
|
turnMutatedFilesRef.current = true;
|
|
1890
1890
|
permissionCallsRef.current.set(event.call.id, event.call);
|
|
1891
1891
|
append({
|
|
@@ -4055,7 +4055,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
4055
4055
|
}
|
|
4056
4056
|
if (permission.kind === 'tool' &&
|
|
4057
4057
|
selected.action === 'allow-session-edits') {
|
|
4058
|
-
for (const rule of ['Write', 'Edit', 'NotebookEdit']) {
|
|
4058
|
+
for (const rule of ['Write', 'Edit', 'ApplyPatch', 'NotebookEdit']) {
|
|
4059
4059
|
if (!immediatePermissionRulesRef.current.includes(rule))
|
|
4060
4060
|
immediatePermissionRulesRef.current.push(rule);
|
|
4061
4061
|
}
|
package/dist/cli/protocol.d.ts
CHANGED
package/dist/cli/protocol.js
CHANGED
|
@@ -1929,9 +1929,16 @@ export class StreamJsonOutput {
|
|
|
1929
1929
|
event.state === 'completed' ||
|
|
1930
1930
|
event.state === 'cancelled' ||
|
|
1931
1931
|
event.state === 'failed') {
|
|
1932
|
-
|
|
1933
|
-
this.
|
|
1932
|
+
const shouldFlush = event.state === 'cancelled'
|
|
1933
|
+
? this.turnText.length > 0 ||
|
|
1934
|
+
this.turnThinking.length > 0 ||
|
|
1935
|
+
this.turnCalls.length > 0
|
|
1936
|
+
: event.state !== 'failed' ||
|
|
1937
|
+
this.pendingFailureMessage === undefined;
|
|
1938
|
+
if (shouldFlush)
|
|
1934
1939
|
this.flushAssistant();
|
|
1940
|
+
else if (event.state === 'cancelled')
|
|
1941
|
+
this.flushPartialOnly();
|
|
1935
1942
|
if (event.state === 'awaiting-permission')
|
|
1936
1943
|
this.writeSessionState('requires_action');
|
|
1937
1944
|
}
|
|
@@ -2492,6 +2499,18 @@ export class StreamJsonOutput {
|
|
|
2492
2499
|
});
|
|
2493
2500
|
this.finishPartialTail();
|
|
2494
2501
|
}
|
|
2502
|
+
flushPartialOnly() {
|
|
2503
|
+
if (!this.includePartialMessages ||
|
|
2504
|
+
this.assistantFlushed ||
|
|
2505
|
+
this.partialEvents.length === 0)
|
|
2506
|
+
return;
|
|
2507
|
+
this.assistantFlushed = true;
|
|
2508
|
+
this.writePartialMessageStart();
|
|
2509
|
+
for (const event of this.partialEvents)
|
|
2510
|
+
this.write(event);
|
|
2511
|
+
this.partialEvents = [];
|
|
2512
|
+
this.pendingPartialStop = undefined;
|
|
2513
|
+
}
|
|
2495
2514
|
finishTurn() {
|
|
2496
2515
|
this.flushAssistant();
|
|
2497
2516
|
this.turnActive = false;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -66,6 +66,7 @@ import { launchTmuxWorktree } from './platform/tmux-worktree.js';
|
|
|
66
66
|
import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
|
|
67
67
|
import { nativeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
|
|
68
68
|
import { createErrorResult, createSuccessResult, isHeadlessCostCommand, matchHeadlessColorCommand, parseCliInvocation, projectProtocolTimings, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
|
|
69
|
+
import { isDirectProcessSigint } from './cli/process-signal.js';
|
|
69
70
|
import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
|
|
70
71
|
import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
|
|
71
72
|
import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
|
|
@@ -1795,7 +1796,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1795
1796
|
const filteredTools = new FilteredToolRegistry(extensionAndLspTools, {
|
|
1796
1797
|
...(cli.tools === undefined
|
|
1797
1798
|
? simpleMode
|
|
1798
|
-
? { tools: ['Bash', 'Edit', 'Read'] }
|
|
1799
|
+
? { tools: ['Bash', 'Edit', 'ApplyPatch', 'Read'] }
|
|
1799
1800
|
: {}
|
|
1800
1801
|
: { tools: selectedBaseTools ?? [] }),
|
|
1801
1802
|
disallowedTools: cli.disallowedTools,
|
|
@@ -6016,6 +6017,15 @@ export async function run(argv, io = consoleIO, dependencies = createDefaultDepe
|
|
|
6016
6017
|
}
|
|
6017
6018
|
catch (error) {
|
|
6018
6019
|
if (isCancellation(error, signal)) {
|
|
6020
|
+
if (isDirectProcessSigint(signal)) {
|
|
6021
|
+
try {
|
|
6022
|
+
if (parseCliInvocation(argv).print)
|
|
6023
|
+
return 0;
|
|
6024
|
+
}
|
|
6025
|
+
catch {
|
|
6026
|
+
// Preserve the ordinary cancellation boundary when parsing fails.
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6019
6029
|
io.stderr('Praxis run cancelled.\n');
|
|
6020
6030
|
return 130;
|
|
6021
6031
|
}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { DIRECT_PROCESS_SIGINT } from './cli/process-signal.js';
|
|
5
6
|
export { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
|
|
6
7
|
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
7
8
|
export async function run(argv, io, dependencies, signal) {
|
|
@@ -36,14 +37,15 @@ function isDirectExecution(moduleUrl, argvPath) {
|
|
|
36
37
|
}
|
|
37
38
|
if (isDirectExecution(import.meta.url, process.argv[1])) {
|
|
38
39
|
const controller = new AbortController();
|
|
40
|
+
const cancelWithSigint = () => controller.abort(DIRECT_PROCESS_SIGINT);
|
|
39
41
|
const cancel = () => controller.abort();
|
|
40
|
-
process.on('SIGINT',
|
|
42
|
+
process.on('SIGINT', cancelWithSigint);
|
|
41
43
|
process.on('SIGTERM', cancel);
|
|
42
44
|
try {
|
|
43
45
|
process.exitCode = await run(process.argv.slice(2), undefined, undefined, controller.signal);
|
|
44
46
|
}
|
|
45
47
|
finally {
|
|
46
|
-
process.removeListener('SIGINT',
|
|
48
|
+
process.removeListener('SIGINT', cancelWithSigint);
|
|
47
49
|
process.removeListener('SIGTERM', cancel);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -10,7 +10,15 @@ function toolName(rule) {
|
|
|
10
10
|
}
|
|
11
11
|
function gatedTool(rule) {
|
|
12
12
|
const name = toolName(rule);
|
|
13
|
-
return ([
|
|
13
|
+
return ([
|
|
14
|
+
'Bash',
|
|
15
|
+
'Write',
|
|
16
|
+
'Edit',
|
|
17
|
+
'ApplyPatch',
|
|
18
|
+
'NotebookEdit',
|
|
19
|
+
'WebFetch',
|
|
20
|
+
'WebSearch',
|
|
21
|
+
].includes(name) || name.startsWith('mcp__'));
|
|
14
22
|
}
|
|
15
23
|
function grants(requested, allowed) {
|
|
16
24
|
const name = toolName(requested);
|
|
@@ -247,6 +247,7 @@ class PraxisMcpRuntime {
|
|
|
247
247
|
sensitiveValues = sensitiveEnvironmentValues(process.env);
|
|
248
248
|
toolRegistryPromise;
|
|
249
249
|
closed = false;
|
|
250
|
+
messageHistory = [];
|
|
250
251
|
constructor(options) {
|
|
251
252
|
this.options = options;
|
|
252
253
|
this.localTools = new LocalToolRegistry({
|
|
@@ -261,6 +262,7 @@ class PraxisMcpRuntime {
|
|
|
261
262
|
'Bash',
|
|
262
263
|
'Read',
|
|
263
264
|
'Edit',
|
|
265
|
+
'ApplyPatch',
|
|
264
266
|
'Write',
|
|
265
267
|
'NotebookEdit',
|
|
266
268
|
'Glob',
|
|
@@ -307,10 +309,20 @@ class PraxisMcpRuntime {
|
|
|
307
309
|
const call = { id: randomUUID(), name, input };
|
|
308
310
|
const context = {
|
|
309
311
|
cwd: this.options.cwd,
|
|
312
|
+
messages: [...this.messageHistory],
|
|
310
313
|
...(signal ? { signal } : {}),
|
|
311
314
|
};
|
|
312
315
|
const prepared = await registry.prepare(call, context);
|
|
313
|
-
|
|
316
|
+
const result = await registry.execute(prepared, context);
|
|
317
|
+
if (name === 'Read' && !result.isError) {
|
|
318
|
+
this.messageHistory.push({ role: 'assistant', content: '', toolCalls: [prepared] }, {
|
|
319
|
+
role: 'tool',
|
|
320
|
+
toolCallId: prepared.id,
|
|
321
|
+
content: result.content,
|
|
322
|
+
isError: false,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
314
326
|
}
|
|
315
327
|
async close() {
|
|
316
328
|
if (this.closed)
|
|
@@ -3,7 +3,8 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
4
|
import { resolveProjectIdentity } from '../platform/project-identity.js';
|
|
5
5
|
import { sanitizeProjectPath } from '../platform/project-path-key.js';
|
|
6
|
-
import { annotateAutoModePermissionOutcome, annotatePermissionDecision, } from '../core/runtime.js';
|
|
6
|
+
import { annotateAutoModePermissionOutcome, annotatePermissionDecision, autoModePermissionOutcome, permissionDecisionSource, } from '../core/runtime.js';
|
|
7
|
+
import { parseApplyPatchInput } from '../tools/apply-patch.js';
|
|
7
8
|
import { loadClaudeAutoModeConfig, } from './claude-auto-classifier.js';
|
|
8
9
|
import { analyzeBashCommands, validateBashSemantics } from './bash-ast.js';
|
|
9
10
|
import { shellPermissionMatchCandidates } from './bash-normalization.js';
|
|
@@ -30,6 +31,7 @@ const DEFAULT_BEHAVIOR = {
|
|
|
30
31
|
Grep: 'allow',
|
|
31
32
|
Write: 'ask',
|
|
32
33
|
Edit: 'ask',
|
|
34
|
+
ApplyPatch: 'ask',
|
|
33
35
|
NotebookEdit: 'ask',
|
|
34
36
|
Glob: 'allow',
|
|
35
37
|
LSP: 'allow',
|
|
@@ -50,6 +52,7 @@ const FILE_TOOLS = new Set([
|
|
|
50
52
|
'Read',
|
|
51
53
|
'Write',
|
|
52
54
|
'Edit',
|
|
55
|
+
'ApplyPatch',
|
|
53
56
|
'NotebookEdit',
|
|
54
57
|
'Glob',
|
|
55
58
|
'Grep',
|
|
@@ -206,6 +209,13 @@ function permissionTarget(call) {
|
|
|
206
209
|
? call.input.notebook_path
|
|
207
210
|
: null;
|
|
208
211
|
}
|
|
212
|
+
if (call.name === 'ApplyPatch') {
|
|
213
|
+
const edits = call.input.edits;
|
|
214
|
+
const first = Array.isArray(edits) ? edits[0] : undefined;
|
|
215
|
+
return isRecord(first) && typeof first.file_path === 'string'
|
|
216
|
+
? first.file_path
|
|
217
|
+
: null;
|
|
218
|
+
}
|
|
209
219
|
if (call.name === 'WebFetch') {
|
|
210
220
|
return typeof call.input.url === 'string' ? call.input.url : null;
|
|
211
221
|
}
|
|
@@ -217,7 +227,7 @@ function permissionTarget(call) {
|
|
|
217
227
|
function matchesRule(rule, call, cwd, homeDirectory) {
|
|
218
228
|
const toolMatches = rule.toolName === call.name ||
|
|
219
229
|
(rule.toolName === 'Edit' &&
|
|
220
|
-
['Edit', 'Write', 'NotebookEdit'].includes(call.name)) ||
|
|
230
|
+
['Edit', 'Write', 'NotebookEdit', 'ApplyPatch'].includes(call.name)) ||
|
|
221
231
|
(rule.toolName === 'Read' && ['Read', 'Glob', 'Grep'].includes(call.name));
|
|
222
232
|
if (!toolMatches)
|
|
223
233
|
return false;
|
|
@@ -400,6 +410,131 @@ export class ClaudePermissionResolver {
|
|
|
400
410
|
...call,
|
|
401
411
|
input: { ...call.input, command: subcommand },
|
|
402
412
|
});
|
|
413
|
+
if (call.name === 'ApplyPatch') {
|
|
414
|
+
const edits = parseApplyPatchInput(call.input);
|
|
415
|
+
const originalEdits = context?.originalCall?.name === 'ApplyPatch'
|
|
416
|
+
? parseApplyPatchInput(context.originalCall.input)
|
|
417
|
+
: edits;
|
|
418
|
+
if (originalEdits.length !== edits.length) {
|
|
419
|
+
throw new Error('ApplyPatch original and canonical edits must match');
|
|
420
|
+
}
|
|
421
|
+
const targetInputs = edits.map((edit, index) => {
|
|
422
|
+
const originalEdit = originalEdits[index];
|
|
423
|
+
if (!originalEdit) {
|
|
424
|
+
throw new Error('ApplyPatch original and canonical edits must match');
|
|
425
|
+
}
|
|
426
|
+
const targetCall = {
|
|
427
|
+
...call,
|
|
428
|
+
name: 'Edit',
|
|
429
|
+
input: { ...edit },
|
|
430
|
+
};
|
|
431
|
+
const applyView = {
|
|
432
|
+
...call,
|
|
433
|
+
input: { edits: [{ ...edit }] },
|
|
434
|
+
};
|
|
435
|
+
const originalView = {
|
|
436
|
+
...call,
|
|
437
|
+
input: { edits: [{ ...originalEdit }] },
|
|
438
|
+
};
|
|
439
|
+
const originalTargetCall = {
|
|
440
|
+
...targetCall,
|
|
441
|
+
input: { ...originalEdit },
|
|
442
|
+
};
|
|
443
|
+
return {
|
|
444
|
+
edit,
|
|
445
|
+
originalEdit,
|
|
446
|
+
targetCall,
|
|
447
|
+
applyView,
|
|
448
|
+
originalView,
|
|
449
|
+
originalTargetCall,
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
const directDenied = targetInputs.flatMap(({ applyView, originalView, targetCall, originalTargetCall }) => effectiveRules('deny').filter((rule) => matchesRule(rule, applyView, cwd, this.homeDirectory) ||
|
|
453
|
+
matchesRule(rule, originalView, cwd, this.homeDirectory) ||
|
|
454
|
+
matchesRule(rule, targetCall, cwd, this.homeDirectory) ||
|
|
455
|
+
matchesRule(rule, originalTargetCall, cwd, this.homeDirectory)))[0];
|
|
456
|
+
if (directDenied) {
|
|
457
|
+
const suffix = directDenied.pattern === null ? '' : `(${directDenied.pattern})`;
|
|
458
|
+
return annotatePermissionDecision({
|
|
459
|
+
behavior: 'deny',
|
|
460
|
+
reason: `Denied by Claude permission rule ${directDenied.toolName}${suffix}`,
|
|
461
|
+
}, 'rule');
|
|
462
|
+
}
|
|
463
|
+
const targetDecisions = await Promise.all(targetInputs.map(async ({ originalEdit, targetCall, applyView, originalView }) => {
|
|
464
|
+
if (permissionMode === 'plan') {
|
|
465
|
+
return annotatePermissionDecision({
|
|
466
|
+
behavior: 'deny',
|
|
467
|
+
reason: 'Cannot use ApplyPatch while in plan mode',
|
|
468
|
+
}, 'mode');
|
|
469
|
+
}
|
|
470
|
+
const directAsked = effectiveRules('ask').find((rule) => matchesRule(rule, applyView, cwd, this.homeDirectory) ||
|
|
471
|
+
matchesRule(rule, originalView, cwd, this.homeDirectory));
|
|
472
|
+
if (directAsked)
|
|
473
|
+
return this.askDecision(targetCall, cwd, permissionMode, context, 'rule', undefined, true);
|
|
474
|
+
const directAllowed = effectiveRules('allow').find((rule) => matchesRule(rule, applyView, cwd, this.homeDirectory) ||
|
|
475
|
+
matchesRule(rule, originalView, cwd, this.homeDirectory));
|
|
476
|
+
const resolved = await this.resolve(targetCall, context
|
|
477
|
+
? {
|
|
478
|
+
...context,
|
|
479
|
+
originalCall: {
|
|
480
|
+
...call,
|
|
481
|
+
input: { edits: [{ ...originalEdit }] },
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
: { cwd, originalCall: originalView });
|
|
485
|
+
if (resolved.behavior === 'allow' &&
|
|
486
|
+
permissionDecisionSource(resolved) === 'default' &&
|
|
487
|
+
!directAllowed) {
|
|
488
|
+
return this.askDecision(targetCall, cwd, permissionMode, context
|
|
489
|
+
? {
|
|
490
|
+
...context,
|
|
491
|
+
originalCall: {
|
|
492
|
+
...call,
|
|
493
|
+
input: { edits: [{ ...originalEdit }] },
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
: { cwd, originalCall: originalView }, 'default', undefined, true);
|
|
497
|
+
}
|
|
498
|
+
if (directAllowed &&
|
|
499
|
+
resolved.behavior === 'ask' &&
|
|
500
|
+
resolved.reason !==
|
|
501
|
+
'Path is outside allowed working directories' &&
|
|
502
|
+
(permissionMode !== 'auto' || !this.shouldClassify(targetCall))) {
|
|
503
|
+
return annotatePermissionDecision({ behavior: 'allow' }, 'rule');
|
|
504
|
+
}
|
|
505
|
+
return resolved;
|
|
506
|
+
}));
|
|
507
|
+
const deniedDecision = targetDecisions.find((decision) => decision.behavior === 'deny');
|
|
508
|
+
if (deniedDecision)
|
|
509
|
+
return deniedDecision;
|
|
510
|
+
const askedDecisions = targetDecisions.filter((decision) => decision.behavior === 'ask');
|
|
511
|
+
if (askedDecisions.length > 0) {
|
|
512
|
+
const first = askedDecisions[0];
|
|
513
|
+
if (!first)
|
|
514
|
+
throw new Error('ApplyPatch permission decision is missing');
|
|
515
|
+
const suggestions = [
|
|
516
|
+
...new Map(askedDecisions
|
|
517
|
+
.flatMap((decision) => decision.behavior === 'ask' ? (decision.suggestions ?? []) : [])
|
|
518
|
+
.map((suggestion) => [JSON.stringify(suggestion), suggestion])).values(),
|
|
519
|
+
];
|
|
520
|
+
const aggregate = {
|
|
521
|
+
...first,
|
|
522
|
+
suggestions,
|
|
523
|
+
};
|
|
524
|
+
const source = permissionDecisionSource(first);
|
|
525
|
+
const annotated = source
|
|
526
|
+
? annotatePermissionDecision(aggregate, source)
|
|
527
|
+
: aggregate;
|
|
528
|
+
const outcome = autoModePermissionOutcome(first);
|
|
529
|
+
return outcome
|
|
530
|
+
? annotateAutoModePermissionOutcome(annotated, outcome)
|
|
531
|
+
: annotated;
|
|
532
|
+
}
|
|
533
|
+
const allowed = targetDecisions.find((decision) => decision.behavior === 'allow');
|
|
534
|
+
if (allowed)
|
|
535
|
+
return allowed;
|
|
536
|
+
throw new Error('ApplyPatch permission resolution produced no decision');
|
|
537
|
+
}
|
|
403
538
|
const denied = matchingRule('deny') ??
|
|
404
539
|
subcommands
|
|
405
540
|
.map((subcommand) => matchingRule('deny', subcommandCall(subcommand)))
|
|
@@ -414,6 +549,7 @@ export class ClaudePermissionResolver {
|
|
|
414
549
|
if (permissionMode === 'plan' &&
|
|
415
550
|
(call.name === 'Write' ||
|
|
416
551
|
call.name === 'Edit' ||
|
|
552
|
+
call.name === 'ApplyPatch' ||
|
|
417
553
|
call.name === 'NotebookEdit')) {
|
|
418
554
|
return annotatePermissionDecision({
|
|
419
555
|
behavior: 'deny',
|
|
@@ -558,7 +694,7 @@ export class ClaudePermissionResolver {
|
|
|
558
694
|
absolutePath,
|
|
559
695
|
]),
|
|
560
696
|
];
|
|
561
|
-
const write = ['Write', 'Edit', 'NotebookEdit'].includes(call.name);
|
|
697
|
+
const write = ['Write', 'Edit', 'ApplyPatch', 'NotebookEdit'].includes(call.name);
|
|
562
698
|
const internalRoots = write
|
|
563
699
|
? internalEditableRoots
|
|
564
700
|
: internalReadableRoots;
|
|
@@ -585,6 +721,7 @@ export class ClaudePermissionResolver {
|
|
|
585
721
|
if (permissionMode === 'acceptEdits' &&
|
|
586
722
|
(call.name === 'Write' ||
|
|
587
723
|
call.name === 'Edit' ||
|
|
724
|
+
call.name === 'ApplyPatch' ||
|
|
588
725
|
call.name === 'NotebookEdit')) {
|
|
589
726
|
return annotatePermissionDecision({ behavior: 'allow' }, 'mode');
|
|
590
727
|
}
|
|
@@ -651,7 +788,12 @@ export class ClaudePermissionResolver {
|
|
|
651
788
|
FILE_TOOLS.has(call.name) &&
|
|
652
789
|
permissionTarget(call)
|
|
653
790
|
? {
|
|
654
|
-
suggestions: filePermissionSuggestions(permissionTarget(call) ?? cwd, cwd, [
|
|
791
|
+
suggestions: filePermissionSuggestions(permissionTarget(call) ?? cwd, cwd, [
|
|
792
|
+
'Write',
|
|
793
|
+
'Edit',
|
|
794
|
+
'ApplyPatch',
|
|
795
|
+
'NotebookEdit',
|
|
796
|
+
].includes(call.name)
|
|
655
797
|
? 'write'
|
|
656
798
|
: 'read', (effectivePermissionMode(context?.permissionUpdates) ??
|
|
657
799
|
permissionMode), false),
|
|
@@ -664,6 +806,7 @@ export class ClaudePermissionResolver {
|
|
|
664
806
|
return true;
|
|
665
807
|
if (call.name === 'Write' ||
|
|
666
808
|
call.name === 'Edit' ||
|
|
809
|
+
call.name === 'ApplyPatch' ||
|
|
667
810
|
call.name === 'NotebookEdit' ||
|
|
668
811
|
call.name === 'WebFetch' ||
|
|
669
812
|
call.name === 'WebSearch' ||
|
|
@@ -8,6 +8,8 @@ export async function writeFileAtomically(filePath, content, options = {}) {
|
|
|
8
8
|
const handle = await open(temporary, 'wx', options.mode ?? 0o600);
|
|
9
9
|
try {
|
|
10
10
|
await handle.writeFile(content);
|
|
11
|
+
if (options.mode !== undefined)
|
|
12
|
+
await handle.chmod(options.mode);
|
|
11
13
|
await handle.sync();
|
|
12
14
|
}
|
|
13
15
|
finally {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModelToolDefinition } from '../core/runtime.js';
|
|
2
|
+
export declare const APPLY_PATCH_MAX_EDITS = 32;
|
|
3
|
+
export declare const APPLY_PATCH_MAX_FILES = 8;
|
|
4
|
+
export declare const APPLY_PATCH_MAX_INPUT_BYTES: number;
|
|
5
|
+
export interface ApplyPatchEdit {
|
|
6
|
+
file_path: string;
|
|
7
|
+
old_string: string;
|
|
8
|
+
new_string: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ApplyPatchPlanFile {
|
|
11
|
+
filePath: string;
|
|
12
|
+
before: string;
|
|
13
|
+
after: string;
|
|
14
|
+
linesAdded: number;
|
|
15
|
+
linesRemoved: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ApplyPatchPlan {
|
|
18
|
+
files: readonly ApplyPatchPlanFile[];
|
|
19
|
+
linesAdded: number;
|
|
20
|
+
linesRemoved: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const APPLY_PATCH_DEFINITION: ModelToolDefinition;
|
|
23
|
+
export declare function parseApplyPatchInput(input: unknown): ApplyPatchEdit[];
|
|
24
|
+
export declare function planApplyPatch(edits: readonly ApplyPatchEdit[], sources: ReadonlyMap<string, string>, maxFileBytes: number): ApplyPatchPlan;
|
|
25
|
+
//# sourceMappingURL=apply-patch.d.ts.map
|