praxis-agent 0.9.0 → 0.11.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.
Files changed (57) hide show
  1. package/README.md +18 -4
  2. package/dist/application/session-service.d.ts +5 -2
  3. package/dist/application/session-service.js +23 -0
  4. package/dist/application/subagent-service.d.ts +4 -2
  5. package/dist/application/subagent-service.js +4 -0
  6. package/dist/cli/interactive.d.ts +4 -3
  7. package/dist/cli/interactive.js +760 -195
  8. package/dist/cli/tui/claude-command-inventory.d.ts +368 -0
  9. package/dist/cli/tui/claude-command-inventory.js +220 -0
  10. package/dist/cli/tui/claude-style.d.ts +12 -14
  11. package/dist/cli/tui/claude-style.js +45 -67
  12. package/dist/cli/tui/config-dashboard.d.ts +4 -8
  13. package/dist/cli/tui/config-dashboard.js +4 -18
  14. package/dist/cli/tui/config-settings.js +1 -1
  15. package/dist/cli/tui/conversation-export.d.ts +1 -1
  16. package/dist/cli/tui/conversation-export.js +2 -11
  17. package/dist/cli/tui/mcp-elicitation.d.ts +75 -0
  18. package/dist/cli/tui/mcp-elicitation.js +497 -0
  19. package/dist/cli/tui/open-url.d.ts +2 -0
  20. package/dist/cli/tui/open-url.js +13 -0
  21. package/dist/cli/tui/permission-settings.d.ts +6 -0
  22. package/dist/cli/tui/permission-settings.js +84 -0
  23. package/dist/cli/tui/runtime-interactions.d.ts +2 -0
  24. package/dist/cli/tui/runtime-interactions.js +44 -17
  25. package/dist/cli/tui/slash-commands.js +3 -28
  26. package/dist/cli/tui/tool-permission.d.ts +36 -0
  27. package/dist/cli/tui/tool-permission.js +386 -0
  28. package/dist/cli.d.ts +2 -2
  29. package/dist/cli.js +28 -13
  30. package/dist/core/runtime.d.ts +50 -2
  31. package/dist/core/runtime.js +30 -3
  32. package/dist/extensions/claude-extension-tools.d.ts +3 -2
  33. package/dist/extensions/claude-extension-tools.js +15 -5
  34. package/dist/extensions/claude-extensions.d.ts +1 -0
  35. package/dist/extensions/claude-extensions.js +10 -0
  36. package/dist/hooks/claude-hook-tools.js +1 -1
  37. package/dist/mcp/claude-mcp-tools.js +12 -6
  38. package/dist/permissions/bash-ast.d.ts +39 -0
  39. package/dist/permissions/bash-ast.js +1046 -0
  40. package/dist/permissions/bash-normalization.d.ts +5 -0
  41. package/dist/permissions/bash-normalization.js +241 -0
  42. package/dist/permissions/bash-path-safety.d.ts +28 -0
  43. package/dist/permissions/bash-path-safety.js +575 -0
  44. package/dist/permissions/claude-permission-resolver.d.ts +7 -0
  45. package/dist/permissions/claude-permission-resolver.js +351 -40
  46. package/dist/permissions/claude-shell-permission.d.ts +4 -0
  47. package/dist/permissions/claude-shell-permission.js +108 -0
  48. package/dist/permissions/permission-updates.d.ts +16 -0
  49. package/dist/permissions/permission-updates.js +320 -0
  50. package/dist/permissions/sed-safety.d.ts +9 -0
  51. package/dist/permissions/sed-safety.js +172 -0
  52. package/dist/permissions/shell-rule-matching.d.ts +14 -0
  53. package/dist/permissions/shell-rule-matching.js +76 -0
  54. package/dist/tools/claude-interactive-tools.d.ts +10 -1
  55. package/dist/tools/claude-interactive-tools.js +10 -4
  56. package/dist/tools/local-tools.js +22 -2
  57. package/package.json +3 -1
@@ -293,6 +293,10 @@ export interface ToolExecutionContext {
293
293
  messages?: readonly ModelMessage[];
294
294
  signal?: AbortSignal;
295
295
  toolResultDirectory?: string;
296
+ originalCall?: ModelToolCall;
297
+ permissionUpdates?: readonly PermissionUpdate[];
298
+ permissionPhase?: 'request' | 'execute';
299
+ permissionApproved?: boolean;
296
300
  }
297
301
  export interface ToolRegistry {
298
302
  definitions(): readonly ModelToolDefinition[];
@@ -300,9 +304,46 @@ export interface ToolRegistry {
300
304
  execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
301
305
  }
302
306
  export type PermissionBehavior = 'allow' | 'ask' | 'deny';
307
+ export type PermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'manual' | 'dontAsk' | 'plan' | 'default';
308
+ export type PermissionUpdateDestination = 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg';
309
+ export type PermissionUpdateMode = Exclude<PermissionMode, 'auto' | 'manual'>;
310
+ export interface PermissionRuleValue {
311
+ toolName: string;
312
+ ruleContent?: string;
313
+ }
314
+ export type PermissionUpdate = {
315
+ type: 'addRules';
316
+ rules: readonly PermissionRuleValue[];
317
+ behavior: PermissionBehavior;
318
+ destination: PermissionUpdateDestination;
319
+ } | {
320
+ type: 'replaceRules';
321
+ rules: readonly PermissionRuleValue[];
322
+ behavior: PermissionBehavior;
323
+ destination: PermissionUpdateDestination;
324
+ } | {
325
+ type: 'removeRules';
326
+ rules: readonly PermissionRuleValue[];
327
+ behavior: PermissionBehavior;
328
+ destination: PermissionUpdateDestination;
329
+ } | {
330
+ type: 'setMode';
331
+ mode: PermissionUpdateMode;
332
+ destination: PermissionUpdateDestination;
333
+ } | {
334
+ type: 'addDirectories';
335
+ directories: readonly string[];
336
+ destination: PermissionUpdateDestination;
337
+ } | {
338
+ type: 'removeDirectories';
339
+ directories: readonly string[];
340
+ destination: PermissionUpdateDestination;
341
+ };
303
342
  export type PermissionApproval = boolean | {
304
343
  behavior: 'allow';
305
344
  updatedInput?: Record<string, unknown>;
345
+ updatedPermissions?: readonly PermissionUpdate[];
346
+ feedback?: string;
306
347
  } | {
307
348
  behavior: 'deny';
308
349
  message: string;
@@ -312,6 +353,9 @@ export interface PermissionResolutionContext {
312
353
  cwd: string;
313
354
  messages?: readonly ModelMessage[];
314
355
  signal?: AbortSignal;
356
+ toolResultDirectory?: string;
357
+ originalCall?: ModelToolCall;
358
+ permissionUpdates?: readonly PermissionUpdate[];
315
359
  }
316
360
  export type PermissionDecisionSource = 'auto-classifier' | 'rule' | 'mode' | 'default';
317
361
  export type AutoModePermissionOutcome = 'blocked' | 'unavailable';
@@ -321,6 +365,8 @@ export type PermissionDecision = {
321
365
  } | {
322
366
  behavior: 'ask';
323
367
  reason?: string;
368
+ suggestions?: readonly PermissionUpdate[];
369
+ metadata?: Readonly<Record<string, unknown>>;
324
370
  source?: PermissionDecisionSource;
325
371
  } | {
326
372
  behavior: 'deny';
@@ -369,7 +415,9 @@ export interface AgentRunRequest {
369
415
  toolResultDirectory?: string;
370
416
  observer?: AgentRunObserver;
371
417
  reloadMessages?: () => Promise<readonly ModelMessage[]>;
372
- approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall) => PermissionApproval | Promise<PermissionApproval>;
418
+ approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
419
+ permissionUpdates?: readonly PermissionUpdate[];
420
+ onPermissionUpdates?: (updates: readonly PermissionUpdate[]) => void | Promise<void>;
373
421
  onStop?: (text: string) => Promise<readonly string[] | {
374
422
  messages: readonly string[];
375
423
  usage?: ModelUsage;
@@ -381,7 +429,7 @@ export interface AgentRunRequest {
381
429
  maxModelTurns?: number;
382
430
  betas?: readonly string[];
383
431
  }
384
- export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'signal' | 'toolResultDirectory'> {
432
+ export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'toolResultDirectory'> {
385
433
  approveRecovery?: (call: ModelToolCall) => boolean | Promise<boolean>;
386
434
  messages?: readonly ModelMessage[];
387
435
  }
@@ -444,7 +444,13 @@ export class AgentRuntime {
444
444
  isError: true,
445
445
  };
446
446
  }
447
- const context = { cwd: request.cwd ?? '', messages };
447
+ const context = {
448
+ cwd: request.cwd ?? '',
449
+ messages,
450
+ originalCall: call,
451
+ permissionPhase: 'request',
452
+ permissionUpdates: request.permissionUpdates ?? [],
453
+ };
448
454
  if (request.signal)
449
455
  context.signal = request.signal;
450
456
  if (request.toolResultDirectory) {
@@ -478,16 +484,26 @@ export class AgentRuntime {
478
484
  let allowed = decision.behavior === 'allow';
479
485
  let denialReason;
480
486
  let interrupt = false;
487
+ let approvalFeedback;
481
488
  if (decision.behavior === 'ask') {
482
489
  this.emit({ type: 'state', state: 'awaiting-permission' });
483
490
  const approval = request.approveTool
484
- ? await request.approveTool(prepared, call)
491
+ ? await request.approveTool(prepared, call, decision)
485
492
  : false;
486
493
  if (typeof approval === 'boolean') {
487
494
  allowed = approval;
488
495
  }
489
496
  else if (approval.behavior === 'allow') {
490
497
  allowed = true;
498
+ approvalFeedback = approval.feedback?.trim() || undefined;
499
+ if (approval.updatedPermissions?.length) {
500
+ const updatedPermissions = [
501
+ ...(context.permissionUpdates ?? []),
502
+ ...approval.updatedPermissions,
503
+ ];
504
+ await request.onPermissionUpdates?.(approval.updatedPermissions);
505
+ context.permissionUpdates = updatedPermissions;
506
+ }
491
507
  if (approval.updatedInput) {
492
508
  try {
493
509
  prepared = await tools.prepare({ ...call, input: approval.updatedInput }, context);
@@ -521,8 +537,19 @@ export class AgentRuntime {
521
537
  if (request.signal?.aborted)
522
538
  return this.cancel();
523
539
  this.emit({ type: 'state', state: 'executing-tools' });
540
+ context.permissionPhase = 'execute';
541
+ context.permissionApproved = true;
524
542
  try {
525
- return await tools.execute(prepared, context);
543
+ const result = await tools.execute(prepared, context);
544
+ if (!approvalFeedback)
545
+ return result;
546
+ return {
547
+ ...result,
548
+ followUpUserMessages: [
549
+ ...(result.followUpUserMessages ?? []),
550
+ approvalFeedback,
551
+ ],
552
+ };
526
553
  }
527
554
  catch (error) {
528
555
  if (request.signal?.aborted)
@@ -10,7 +10,8 @@ export declare class ClaudeExtensionToolRegistry implements ToolRegistry {
10
10
  }
11
11
  export declare class ClaudeExtensionPermissionResolver implements PermissionResolver {
12
12
  private readonly base;
13
- constructor(base: PermissionResolver);
14
- resolve(call: ModelToolCall, context?: PermissionResolutionContext): PermissionDecision | Promise<PermissionDecision>;
13
+ private readonly catalog;
14
+ constructor(base: PermissionResolver, catalog: ClaudeExtensionCatalog);
15
+ resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
15
16
  }
16
17
  //# sourceMappingURL=claude-extension-tools.d.ts.map
@@ -66,13 +66,23 @@ export class ClaudeExtensionToolRegistry {
66
66
  }
67
67
  export class ClaudeExtensionPermissionResolver {
68
68
  base;
69
- constructor(base) {
69
+ catalog;
70
+ constructor(base, catalog) {
70
71
  this.base = base;
72
+ this.catalog = catalog;
71
73
  }
72
- resolve(call, context) {
73
- return call.name === 'Skill'
74
- ? { behavior: 'allow' }
75
- : this.base.resolve(call, context);
74
+ async resolve(call, context) {
75
+ const decision = await this.base.resolve(call, context);
76
+ if (call.name !== 'Skill' || decision.behavior !== 'ask')
77
+ return decision;
78
+ const input = skillInput(call);
79
+ const definition = this.catalog.skill(input.skill.replace(/^\//u, ''));
80
+ if (definition?.modelInvocable && definition.permissionSafe) {
81
+ return { behavior: 'allow' };
82
+ }
83
+ return definition
84
+ ? { ...decision, metadata: { command: definition } }
85
+ : decision;
76
86
  }
77
87
  }
78
88
  //# sourceMappingURL=claude-extension-tools.js.map
@@ -8,6 +8,7 @@ export interface ClaudeExtensionDefinition extends ClaudeTextResource {
8
8
  description: string;
9
9
  body: string;
10
10
  modelInvocable: boolean;
11
+ permissionSafe: boolean;
11
12
  }
12
13
  export interface ClaudeSlashCommandDefinition {
13
14
  name: string;
@@ -8,6 +8,7 @@ const BUILTIN_LOOP = {
8
8
  name: 'loop',
9
9
  description: 'Run a prompt or slash command on a recurring interval; defaults to 10 minutes.',
10
10
  modelInvocable: true,
11
+ permissionSafe: true,
11
12
  body: `# /loop — schedule a recurring prompt
12
13
 
13
14
  Parse the input into an optional interval followed by a prompt, then schedule it with CronCreate.
@@ -128,6 +129,14 @@ function parseDefinition(kind, resource) {
128
129
  ? metadata.name
129
130
  : defaultName(kind, resource.path);
130
131
  const description = typeof metadata.description === 'string' ? metadata.description : '';
132
+ const allowedTools = metadata['allowed-tools'];
133
+ const hasAllowedTools = Array.isArray(allowedTools)
134
+ ? allowedTools.length > 0
135
+ : typeof allowedTools === 'string' && allowedTools.trim().length > 0;
136
+ const hooks = metadata.hooks;
137
+ const hasHooks = hooks !== undefined &&
138
+ hooks !== null &&
139
+ (typeof hooks !== 'object' || Object.keys(hooks).length > 0);
131
140
  return {
132
141
  ...resource,
133
142
  kind,
@@ -135,6 +144,7 @@ function parseDefinition(kind, resource) {
135
144
  description,
136
145
  body,
137
146
  modelInvocable: metadata['disable-model-invocation'] !== true,
147
+ permissionSafe: !hasAllowedTools && !hasHooks,
138
148
  };
139
149
  }
140
150
  function indexed(kind, resources) {
@@ -62,7 +62,7 @@ export class ClaudeHookToolCoordinator {
62
62
  tool_name: call.name,
63
63
  tool_input: prepared?.hookInput ?? call.input,
64
64
  tool_use_id: call.id,
65
- permission_suggestions: [],
65
+ permission_suggestions: decision.suggestions ?? [],
66
66
  }, call.name, prepared?.signal);
67
67
  await this.options.recordOutcome(outcome);
68
68
  if (outcome.blockedReason || outcome.permissionDecision === 'deny') {
@@ -8,6 +8,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
8
8
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
9
9
  import { ElicitRequestSchema, ElicitationCompleteNotificationSchema, PromptListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
10
10
  import { redactSensitiveError, redactSensitiveText, redactSensitiveValue, sanitizeChildEnvironment, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
11
+ import { parsePermissionUpdates } from '../permissions/permission-updates.js';
11
12
  import { loadMcpOAuthProvider, mcpOAuthServerIdentity, } from './claude-mcp-oauth.js';
12
13
  const MAX_TOOL_PAGES = 100;
13
14
  const MAX_TOOLS = 10_000;
@@ -270,20 +271,25 @@ function toolContent(result, sensitiveValues) {
270
271
  }
271
272
  return redactSensitiveText(parts.join('\n'), sensitiveValues);
272
273
  }
273
- const INVALID_PERMISSION_RESULT = "The permission prompt tool returned an invalid permission result. Expected {behavior: 'allow', updatedInput?: object} or {behavior: 'deny', message: string}.";
274
+ const INVALID_PERMISSION_RESULT = "The permission prompt tool returned an invalid permission result. Expected {behavior: 'allow', updatedInput: object} or {behavior: 'deny', message: string}.";
274
275
  function invalidPermissionResult() {
275
276
  return { behavior: 'deny', message: INVALID_PERMISSION_RESULT };
276
277
  }
277
- function parsePermissionResult(source) {
278
+ function parsePermissionResult(source, currentInput) {
278
279
  const value = JSON.parse(source);
279
280
  if (!isRecord(value))
280
281
  return invalidPermissionResult();
281
282
  if (value.behavior === 'allow') {
282
- if (value.updatedInput === undefined)
283
- return { behavior: 'allow' };
284
283
  if (!isRecord(value.updatedInput))
285
284
  return invalidPermissionResult();
286
- return { behavior: 'allow', updatedInput: value.updatedInput };
285
+ const updatedPermissions = parsePermissionUpdates(value.updatedPermissions);
286
+ return {
287
+ behavior: 'allow',
288
+ updatedInput: Object.keys(value.updatedInput).length === 0
289
+ ? currentInput
290
+ : value.updatedInput,
291
+ ...(updatedPermissions ? { updatedPermissions } : {}),
292
+ };
287
293
  }
288
294
  if (value.behavior === 'deny' && typeof value.message === 'string') {
289
295
  return {
@@ -812,7 +818,7 @@ export class ClaudeMcpToolRegistry {
812
818
  return invalidPermissionResult();
813
819
  }
814
820
  try {
815
- return parsePermissionResult(toolContent(result, tool.sensitiveValues));
821
+ return parsePermissionResult(toolContent(result, tool.sensitiveValues), call.input);
816
822
  }
817
823
  catch {
818
824
  return invalidPermissionResult();
@@ -0,0 +1,39 @@
1
+ export declare const MAX_BASH_PERMISSION_COMMANDS = 50;
2
+ export type BashSemanticResult = {
3
+ safe: true;
4
+ } | {
5
+ safe: false;
6
+ reason: string;
7
+ };
8
+ export type BashWrapperResult = {
9
+ ok: true;
10
+ argv: string[];
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
15
+ export declare function normalizeBashWrapperArgv(input: readonly string[]): BashWrapperResult;
16
+ export interface BashStaticCommand {
17
+ text: string;
18
+ argv: readonly string[];
19
+ }
20
+ export interface BashStaticRedirect {
21
+ operator: '>' | '>>' | '<' | '>&' | '<&' | '>|' | '&>' | '&>>' | '<<<';
22
+ target: string;
23
+ }
24
+ export type BashStaticAnalysis = {
25
+ parsed: true;
26
+ commands: readonly BashStaticCommand[];
27
+ redirects: readonly BashStaticRedirect[];
28
+ } | {
29
+ parsed: false;
30
+ reason: string;
31
+ };
32
+ export declare function analyzeBashStructure(source: string): BashStaticAnalysis;
33
+ export declare function validateBashSemantics(source: string): BashSemanticResult;
34
+ export interface BashCommandAnalysis {
35
+ commands: readonly string[];
36
+ parsed: boolean;
37
+ }
38
+ export declare function analyzeBashCommands(source: string): BashCommandAnalysis;
39
+ //# sourceMappingURL=bash-ast.d.ts.map