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 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, glob, search, shell, notebook, PDF,
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 path = call.name === 'Write' || call.name === 'Edit'
2873
- ? call.input.file_path
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
- if ((call.name === 'Write' || call.name === 'Edit') &&
2881
- (await this.options.interactiveTools?.isPlanFile(sessionId, path))) {
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 prepared = await fileHistory.prepareMutation(projectionSnapshot().entries, snapshotMessageId, path);
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 prepared.rollback();
2924
+ await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
2898
2925
  throw error;
2899
2926
  }
2900
2927
  if (result.isError) {
2901
- await prepared.rollback();
2928
+ await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
2902
2929
  return result;
2903
2930
  }
2904
- const entry = prepared.commit(assistantMessageId);
2905
- if (entry) {
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' || call.name === 'Edit') {
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' ||
@@ -299,6 +299,7 @@ const BACKGROUND_AGENT_TOOLS = new Set([
299
299
  'Glob',
300
300
  'Bash',
301
301
  'Edit',
302
+ 'ApplyPatch',
302
303
  'Write',
303
304
  'NotebookEdit',
304
305
  'Skill',
@@ -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
  }
@@ -0,0 +1,3 @@
1
+ export declare const DIRECT_PROCESS_SIGINT: unique symbol;
2
+ export declare function isDirectProcessSigint(signal: AbortSignal | undefined): boolean;
3
+ //# sourceMappingURL=process-signal.d.ts.map
@@ -0,0 +1,5 @@
1
+ export const DIRECT_PROCESS_SIGINT = Symbol('praxis.direct-process-sigint');
2
+ export function isDirectProcessSigint(signal) {
3
+ return signal?.aborted === true && signal.reason === DIRECT_PROCESS_SIGINT;
4
+ }
5
+ //# sourceMappingURL=process-signal.js.map
@@ -323,6 +323,7 @@ export declare class StreamJsonOutput {
323
323
  private finishPartial;
324
324
  private finishPartialTail;
325
325
  private flushAssistant;
326
+ private flushPartialOnly;
326
327
  private finishTurn;
327
328
  }
328
329
  //# sourceMappingURL=protocol.d.ts.map
@@ -1929,9 +1929,16 @@ export class StreamJsonOutput {
1929
1929
  event.state === 'completed' ||
1930
1930
  event.state === 'cancelled' ||
1931
1931
  event.state === 'failed') {
1932
- if (event.state !== 'failed' ||
1933
- this.pendingFailureMessage === undefined)
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;
@@ -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', cancel);
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', cancel);
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 (['Bash', 'Write', 'Edit', 'NotebookEdit', 'WebFetch', 'WebSearch'].includes(name) || name.startsWith('mcp__'));
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
- return registry.execute(prepared, context);
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, ['Write', 'Edit', 'NotebookEdit'].includes(call.name)
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