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.
|
|
220
|
-
|
|
221
|
-
|
|
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
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|