praxis-agent 0.20.7 → 0.20.8
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.
|
@@ -19,11 +19,16 @@ export interface ClaudeQuestionResult {
|
|
|
19
19
|
notes?: string;
|
|
20
20
|
}>>;
|
|
21
21
|
}
|
|
22
|
+
export interface ClaudeAllowedPrompt {
|
|
23
|
+
tool: 'Bash';
|
|
24
|
+
prompt: string;
|
|
25
|
+
}
|
|
22
26
|
export interface ClaudePlanApprovalRequest {
|
|
23
27
|
action: 'exit';
|
|
24
28
|
planPath: string;
|
|
25
29
|
plan?: string;
|
|
26
30
|
previousMode: ClaudePermissionMode;
|
|
31
|
+
allowedPrompts?: readonly ClaudeAllowedPrompt[];
|
|
27
32
|
}
|
|
28
33
|
export type ClaudePlanApprovalResult = {
|
|
29
34
|
behavior: 'allow';
|
|
@@ -67,6 +72,7 @@ export declare class ClaudeInteractiveToolManager {
|
|
|
67
72
|
private planPath;
|
|
68
73
|
private resolver;
|
|
69
74
|
private resolve;
|
|
75
|
+
private applyRetainedPrompts;
|
|
70
76
|
private resolvePlanFile;
|
|
71
77
|
private isPlanFileForState;
|
|
72
78
|
enter(sessionId: string, call: ModelToolCall): Promise<ToolExecutionResult>;
|
|
@@ -83,7 +83,7 @@ const EXIT_PLAN_MODE = {
|
|
|
83
83
|
type: 'object',
|
|
84
84
|
properties: {
|
|
85
85
|
allowedPrompts: {
|
|
86
|
-
description: '
|
|
86
|
+
description: 'Bash commands to pre-approve once the plan is approved. Each entry must declare tool "Bash" and a non-empty prompt string.',
|
|
87
87
|
type: 'array',
|
|
88
88
|
items: {
|
|
89
89
|
type: 'object',
|
|
@@ -164,6 +164,24 @@ function questionsFrom(input) {
|
|
|
164
164
|
};
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
+
function allowedPromptsFrom(input) {
|
|
168
|
+
if (input.allowedPrompts === undefined)
|
|
169
|
+
return [];
|
|
170
|
+
if (!Array.isArray(input.allowedPrompts)) {
|
|
171
|
+
throw new Error('allowedPrompts must be an array');
|
|
172
|
+
}
|
|
173
|
+
return input.allowedPrompts.map((rawEntry, index) => {
|
|
174
|
+
const entry = object(rawEntry, `allowedPrompts[${index}]`);
|
|
175
|
+
strictKeys(entry, ['tool', 'prompt'], `allowedPrompts[${index}]`);
|
|
176
|
+
if (entry.tool !== 'Bash') {
|
|
177
|
+
throw new Error(`allowedPrompts[${index}].tool must be "Bash"`);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
tool: 'Bash',
|
|
181
|
+
prompt: nonEmptyString(entry.prompt, `allowedPrompts[${index}].prompt`),
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
167
185
|
class ClaudeInteractiveToolRegistry {
|
|
168
186
|
base;
|
|
169
187
|
manager;
|
|
@@ -318,7 +336,7 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
|
|
|
318
336
|
}
|
|
319
337
|
return resolver;
|
|
320
338
|
}
|
|
321
|
-
resolve(sessionId, call, context) {
|
|
339
|
+
async resolve(sessionId, call, context) {
|
|
322
340
|
const state = this.state(sessionId);
|
|
323
341
|
if (this.enabledNames.has(call.name))
|
|
324
342
|
return { behavior: 'allow' };
|
|
@@ -327,7 +345,18 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
|
|
|
327
345
|
typeof call.input.file_path === 'string') {
|
|
328
346
|
return this.resolvePlanFile(state, call, context);
|
|
329
347
|
}
|
|
330
|
-
|
|
348
|
+
const decision = await this.resolver(this.planPermissionMode(sessionId)).resolve(call, context);
|
|
349
|
+
return this.applyRetainedPrompts(state, call, decision);
|
|
350
|
+
}
|
|
351
|
+
applyRetainedPrompts(state, call, decision) {
|
|
352
|
+
if (decision.behavior !== 'ask' || call.name !== 'Bash')
|
|
353
|
+
return decision;
|
|
354
|
+
if (typeof call.input.command !== 'string')
|
|
355
|
+
return decision;
|
|
356
|
+
const retained = state.allowedPrompts ?? [];
|
|
357
|
+
return retained.some(({ prompt }) => prompt === call.input.command)
|
|
358
|
+
? { behavior: 'allow' }
|
|
359
|
+
: decision;
|
|
331
360
|
}
|
|
332
361
|
async resolvePlanFile(state, call, context) {
|
|
333
362
|
const requestedPath = String(call.input.file_path);
|
|
@@ -378,6 +407,7 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
|
|
|
378
407
|
isError: true,
|
|
379
408
|
};
|
|
380
409
|
}
|
|
410
|
+
const allowedPrompts = allowedPromptsFrom(call.input);
|
|
381
411
|
let plan;
|
|
382
412
|
try {
|
|
383
413
|
plan = await readFile(state.planPath, 'utf8');
|
|
@@ -398,9 +428,11 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
|
|
|
398
428
|
action: 'exit',
|
|
399
429
|
planPath: state.planPath,
|
|
400
430
|
previousMode: state.previousMode,
|
|
431
|
+
allowedPrompts,
|
|
401
432
|
...(plan === undefined ? {} : { plan }),
|
|
402
433
|
}, signal);
|
|
403
434
|
if (approval.behavior === 'deny') {
|
|
435
|
+
delete state.allowedPrompts;
|
|
404
436
|
return {
|
|
405
437
|
content: approval.feedback
|
|
406
438
|
? `User declined the plan with feedback: ${approval.feedback}\n\nRemain in plan mode and revise it.`
|
|
@@ -409,6 +441,7 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
|
|
|
409
441
|
};
|
|
410
442
|
}
|
|
411
443
|
state.mode = approval.permissionMode;
|
|
444
|
+
state.allowedPrompts = allowedPrompts;
|
|
412
445
|
this.transitions.set(call.id, state.mode);
|
|
413
446
|
return {
|
|
414
447
|
content: plan?.trim()
|