praxis-agent 0.51.0 → 0.53.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
@@ -216,9 +216,18 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
216
216
  tool selection defers `mcp__*` schemas behind a turn-scoped `ToolSearch`;
217
217
  each query activates at most eight deterministic matches for the next model
218
218
  request, and published MCP tool descriptions are capped at 2,048 Unicode
219
- code points. Explicit concrete `--tools` selections load selected tools
220
- directly, while `--disallowedTools ToolSearch` restores the complete tool
221
- list.
219
+ code points. Text-only MCP results above 100,000 UTF-8 bytes are redacted
220
+ into mode-`0600` `.txt` files under the session-scoped `tool-results`
221
+ directory; providers and transcripts receive only a bounded instruction with
222
+ the absolute file path. Results at or below the limit remain inline, while
223
+ structured, mixed-media, and binary-resource handling is unchanged. MCP
224
+ tools that declare `readOnlyHint: true` default to allow through
225
+ provider-neutral permission metadata; explicit PreToolUse and permission
226
+ ask/deny decisions retain precedence, and missing or false hints retain the
227
+ existing default behavior. This is a Praxis permission contract, not a claim
228
+ of verified Claude Code 2.1.208 parity. Explicit concrete `--tools`
229
+ selections load selected tools directly, while
230
+ `--disallowedTools ToolSearch` restores the complete tool list.
222
231
  - **Provider-neutral models** — native Provider Registry/Vault routing, API
223
232
  adapters, an experimental Codex OAuth adapter, explicit capability checks,
224
233
  separate per-attempt connect, byte-idle, and absolute-total timeouts, typed
@@ -342,6 +342,7 @@ export interface ToolExecutionContext {
342
342
  toolResultDirectory?: string;
343
343
  originalCall?: ModelToolCall;
344
344
  permissionUpdates?: readonly PermissionUpdate[];
345
+ toolPermission?: ToolPermissionMetadata;
345
346
  permissionPhase?: 'request' | 'execute';
346
347
  permissionApproved?: boolean;
347
348
  /** Internal marker for an explicit PreToolUse allow decision. */
@@ -405,6 +406,9 @@ export type PermissionApproval = boolean | {
405
406
  message: string;
406
407
  interrupt?: boolean;
407
408
  };
409
+ export interface ToolPermissionMetadata {
410
+ readonly readOnly: true;
411
+ }
408
412
  export interface PermissionResolutionContext {
409
413
  cwd: string;
410
414
  messages?: readonly ModelMessage[];
@@ -412,6 +416,7 @@ export interface PermissionResolutionContext {
412
416
  toolResultDirectory?: string;
413
417
  originalCall?: ModelToolCall;
414
418
  permissionUpdates?: readonly PermissionUpdate[];
419
+ toolPermission?: ToolPermissionMetadata;
415
420
  }
416
421
  export type PermissionDecisionSource = 'auto-classifier' | 'rule' | 'mode' | 'default';
417
422
  export type AutoModePermissionOutcome = 'blocked' | 'unavailable';
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { mkdir, mkdtemp, open, rm } from 'node:fs/promises';
3
3
  import { homedir, tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, resolve as resolvePath } from 'node:path';
5
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
6
6
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
7
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
@@ -26,6 +26,7 @@ function capMcpToolDescription(value) {
26
26
  return [...value].slice(0, MAX_MCP_TOOL_DESCRIPTION_CODE_POINTS).join('');
27
27
  }
28
28
  const MAX_RESOURCE_BYTES = 25 * 1024 * 1024;
29
+ const MAX_INLINE_MCP_TEXT_RESULT_BYTES = 100_000;
29
30
  const NO_MCP_RESOURCES = 'No resources found. MCP servers may still provide tools even if they have no resources.';
30
31
  const MCP_RESOURCE_TOOL_DEFINITIONS = [
31
32
  {
@@ -458,11 +459,21 @@ async function mcpBinaryText(options, sensitiveValues, createdFiles) {
458
459
  : `Resource from ${options.serverName} at ${options.uri}`;
459
460
  return mcpTextBlock(`[${origin}] Binary content (${options.mimeType}, ${options.bytes.length} bytes) saved to ${filePath}`, sensitiveValues);
460
461
  }
462
+ async function externalizeMcpTextResult(serverName, content, context, sensitiveValues, createdFiles) {
463
+ if (!context.toolResultDirectory) {
464
+ throw new Error('MCP large text tool result output directory is unavailable');
465
+ }
466
+ const bytes = Buffer.from(content, 'utf8');
467
+ const filePath = await writeToolBlob(resolvePath(context.cwd, context.toolResultDirectory), serverName, bytes, '.txt');
468
+ createdFiles.push(filePath);
469
+ return mcpTextBlock(`[MCP tool result from ${serverName}] Text content (${bytes.length} bytes) saved to ${filePath}`, sensitiveValues);
470
+ }
461
471
  async function mcpToolResultUnchecked(serverName, result, context, sensitiveValues, createdFiles) {
462
472
  if (!Array.isArray(result.content)) {
463
473
  throw new Error('MCP tool result content must be an array');
464
474
  }
465
475
  const blocks = [];
476
+ let textOnly = result.structuredContent === undefined;
466
477
  let resultBytes = 0;
467
478
  const consumeBytes = (bytes) => {
468
479
  resultBytes += bytes;
@@ -487,6 +498,7 @@ async function mcpToolResultUnchecked(serverName, result, context, sensitiveValu
487
498
  }
488
499
  continue;
489
500
  }
501
+ textOnly = false;
490
502
  if (item.type === 'image') {
491
503
  if (typeof item.mimeType !== 'string' ||
492
504
  !MCP_IMAGE_MEDIA_TYPES.has(item.mimeType) ||
@@ -573,6 +585,15 @@ async function mcpToolResultUnchecked(serverName, result, context, sensitiveValu
573
585
  if (Buffer.byteLength(content) > MAX_RESOURCE_BYTES) {
574
586
  throw new Error(`MCP tool result exceeded ${MAX_RESOURCE_BYTES} bytes`);
575
587
  }
588
+ if (textOnly &&
589
+ Buffer.byteLength(content) > MAX_INLINE_MCP_TEXT_RESULT_BYTES) {
590
+ const pointerBlock = await externalizeMcpTextResult(serverName, content, context, sensitiveValues, createdFiles);
591
+ return {
592
+ content: pointerBlock.text,
593
+ contentBlocks: [pointerBlock],
594
+ isError: result.isError === true,
595
+ };
596
+ }
576
597
  const images = blocks.filter((block) => block.type === 'image');
577
598
  return {
578
599
  content,
@@ -923,7 +944,10 @@ export class ClaudeMcpToolRegistry {
923
944
  };
924
945
  }
925
946
  async prepare(call, context) {
926
- return this.connectedTools.has(call.name) ||
947
+ const tool = this.connectedTools.get(call.name);
948
+ if (tool?.readOnly === true)
949
+ context.toolPermission = { readOnly: true };
950
+ return tool ||
927
951
  MCP_RESOURCE_TOOL_DEFINITIONS.some((definition) => definition.name === call.name)
928
952
  ? call
929
953
  : this.options.base.prepare(call, context);
@@ -613,7 +613,9 @@ export class ClaudePermissionResolver {
613
613
  if (permissionMode === 'auto' && call.name === 'Bash') {
614
614
  return annotatePermissionDecision({ behavior: 'allow' }, 'default');
615
615
  }
616
- const defaultBehavior = DEFAULT_BEHAVIOR[call.name];
616
+ const defaultBehavior = context?.toolPermission?.readOnly === true
617
+ ? 'allow'
618
+ : DEFAULT_BEHAVIOR[call.name];
617
619
  return defaultBehavior === 'ask'
618
620
  ? this.askDecision(call, cwd, permissionMode, context, 'default', command
619
621
  ? (subcommand) => matchingRule('allow', subcommandCall(subcommand)) === undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",