praxis-agent 0.52.0 → 0.53.1
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
|
@@ -220,9 +220,18 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
220
220
|
into mode-`0600` `.txt` files under the session-scoped `tool-results`
|
|
221
221
|
directory; providers and transcripts receive only a bounded instruction with
|
|
222
222
|
the absolute file path. Results at or below the limit remain inline, while
|
|
223
|
-
structured, mixed-media, and binary-resource handling is unchanged.
|
|
224
|
-
|
|
225
|
-
|
|
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. For each
|
|
231
|
+
context assembly, Git status is refreshed from the caller-resolved cwd while
|
|
232
|
+
environment and memory remain lifecycle-stable; collection uses
|
|
233
|
+
`--no-optional-locks`, fails closed on repository/status errors, and bounds
|
|
234
|
+
the rendered status to 2,048 UTF-8 bytes.
|
|
226
235
|
- **Provider-neutral models** — native Provider Registry/Vault routing, API
|
|
227
236
|
adapters, an experimental Codex OAuth adapter, explicit capability checks,
|
|
228
237
|
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';
|
|
@@ -944,7 +944,10 @@ export class ClaudeMcpToolRegistry {
|
|
|
944
944
|
};
|
|
945
945
|
}
|
|
946
946
|
async prepare(call, context) {
|
|
947
|
-
|
|
947
|
+
const tool = this.connectedTools.get(call.name);
|
|
948
|
+
if (tool?.readOnly === true)
|
|
949
|
+
context.toolPermission = { readOnly: true };
|
|
950
|
+
return tool ||
|
|
948
951
|
MCP_RESOURCE_TOOL_DEFINITIONS.some((definition) => definition.name === call.name)
|
|
949
952
|
? call
|
|
950
953
|
: this.options.base.prepare(call, context);
|
package/dist/native/context.js
CHANGED
|
@@ -131,7 +131,7 @@ ${sections.join('\n\n')}`,
|
|
|
131
131
|
tailSections.push({
|
|
132
132
|
id: 'relocated-runtime-context',
|
|
133
133
|
placement: 'first-user',
|
|
134
|
-
stability: '
|
|
134
|
+
stability: 'volatile',
|
|
135
135
|
content: renderClaudeDynamicUserContext({
|
|
136
136
|
environment: dynamic.environment,
|
|
137
137
|
...(dynamic.gitStatus ? { gitStatus: dynamic.gitStatus } : {}),
|
|
@@ -143,8 +143,19 @@ ${sections.join('\n\n')}`,
|
|
|
143
143
|
id: 'runtime-context',
|
|
144
144
|
placement: 'system',
|
|
145
145
|
stability: 'session',
|
|
146
|
-
content: renderClaudeDynamicSystemContext(
|
|
146
|
+
content: renderClaudeDynamicSystemContext({
|
|
147
|
+
environment: dynamic.environment,
|
|
148
|
+
...(dynamic.memory ? { memory: dynamic.memory } : {}),
|
|
149
|
+
}),
|
|
147
150
|
});
|
|
151
|
+
if (dynamic.gitStatus) {
|
|
152
|
+
tailSections.push({
|
|
153
|
+
id: 'git-status',
|
|
154
|
+
placement: 'system',
|
|
155
|
+
stability: 'volatile',
|
|
156
|
+
content: dynamic.gitStatus,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
const composition = this.composer.compose({
|
|
@@ -227,15 +238,22 @@ ${sections.join('\n\n')}`,
|
|
|
227
238
|
throw new Error('Dynamic context loader is unavailable');
|
|
228
239
|
if (!snapshot)
|
|
229
240
|
return load(cwd);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
const current = load(cwd);
|
|
242
|
+
if (!snapshot.stableDynamic) {
|
|
243
|
+
const pending = current.then(({ environment, memory }) => ({
|
|
244
|
+
environment,
|
|
245
|
+
...(memory ? { memory } : {}),
|
|
246
|
+
}));
|
|
247
|
+
snapshot.stableDynamic = pending;
|
|
233
248
|
void pending.catch(() => {
|
|
234
|
-
if (snapshot.
|
|
235
|
-
delete snapshot.
|
|
249
|
+
if (snapshot.stableDynamic === pending)
|
|
250
|
+
delete snapshot.stableDynamic;
|
|
236
251
|
});
|
|
237
252
|
}
|
|
238
|
-
return snapshot.
|
|
253
|
+
return Promise.all([current, snapshot.stableDynamic]).then(([fresh, stable]) => ({
|
|
254
|
+
...stable,
|
|
255
|
+
...(fresh.gitStatus ? { gitStatus: fresh.gitStatus } : {}),
|
|
256
|
+
}));
|
|
239
257
|
}
|
|
240
258
|
loadMcpInstructions(snapshot) {
|
|
241
259
|
const load = this.options.loadMcpInstructions;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { platform, release, type } from 'node:os';
|
|
4
|
-
const MAX_GIT_OUTPUT_BYTES =
|
|
4
|
+
const MAX_GIT_OUTPUT_BYTES = 2_048;
|
|
5
|
+
const MAX_GIT_STATUS_BYTES = 2_048;
|
|
5
6
|
const MAX_GIT_ERROR_BYTES = 8 * 1024;
|
|
6
7
|
const GIT_TIMEOUT_MS = 5_000;
|
|
7
8
|
function collectBounded(chunks, chunk, state, limit) {
|
|
@@ -16,6 +17,22 @@ function collectBounded(chunks, chunk, state, limit) {
|
|
|
16
17
|
if (accepted.length < chunk.length)
|
|
17
18
|
state.truncated = true;
|
|
18
19
|
}
|
|
20
|
+
function boundUtf8(value, limit, marker) {
|
|
21
|
+
if (Buffer.byteLength(value, 'utf8') <= limit)
|
|
22
|
+
return value;
|
|
23
|
+
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
24
|
+
const prefixLimit = Math.max(0, limit - markerBytes);
|
|
25
|
+
let bytes = 0;
|
|
26
|
+
let prefix = '';
|
|
27
|
+
for (const character of value) {
|
|
28
|
+
const characterBytes = Buffer.byteLength(character, 'utf8');
|
|
29
|
+
if (bytes + characterBytes > prefixLimit)
|
|
30
|
+
break;
|
|
31
|
+
prefix += character;
|
|
32
|
+
bytes += characterBytes;
|
|
33
|
+
}
|
|
34
|
+
return `${prefix}${marker}`;
|
|
35
|
+
}
|
|
19
36
|
function defaultRunGit(cwd, args) {
|
|
20
37
|
return new Promise((resolve, reject) => {
|
|
21
38
|
const child = spawn('git', ['-C', cwd, ...args], {
|
|
@@ -93,9 +110,16 @@ async function renderGitStatus(runGit) {
|
|
|
93
110
|
optionalGit(runGit, ['branch', '--show-current']),
|
|
94
111
|
optionalGit(runGit, ['branch', '--format=%(refname:short)']),
|
|
95
112
|
optionalGit(runGit, ['config', 'user.name']),
|
|
96
|
-
optionalGit(runGit, [
|
|
113
|
+
optionalGit(runGit, [
|
|
114
|
+
'--no-optional-locks',
|
|
115
|
+
'status',
|
|
116
|
+
'--short',
|
|
117
|
+
'--untracked-files=all',
|
|
118
|
+
]),
|
|
97
119
|
optionalGit(runGit, ['log', '-5', '--oneline']),
|
|
98
120
|
]);
|
|
121
|
+
if (!status.available)
|
|
122
|
+
return undefined;
|
|
99
123
|
const branch = branchValue.output || 'HEAD';
|
|
100
124
|
const sections = [
|
|
101
125
|
'# gitStatus',
|
|
@@ -108,18 +132,16 @@ async function renderGitStatus(runGit) {
|
|
|
108
132
|
if (user.available && user.output) {
|
|
109
133
|
sections.push('', `Git user: ${user.output}`);
|
|
110
134
|
}
|
|
111
|
-
const renderedStatus =
|
|
112
|
-
? '
|
|
113
|
-
: status.
|
|
114
|
-
?
|
|
115
|
-
:
|
|
116
|
-
? '... [truncated]'
|
|
117
|
-
: 'Clean';
|
|
135
|
+
const renderedStatus = status.output
|
|
136
|
+
? `${status.output}${status.truncated ? '\n... [truncated]' : ''}`
|
|
137
|
+
: status.truncated
|
|
138
|
+
? '... [truncated]'
|
|
139
|
+
: 'Clean';
|
|
118
140
|
sections.push('', 'Status:', renderedStatus);
|
|
119
141
|
if (commits.available && commits.output) {
|
|
120
142
|
sections.push('', 'Recent commits:', commits.output);
|
|
121
143
|
}
|
|
122
|
-
return sections.join('\n');
|
|
144
|
+
return boundUtf8(sections.join('\n'), MAX_GIT_STATUS_BYTES, '\n... [truncated]');
|
|
123
145
|
}
|
|
124
146
|
export async function loadClaudeDynamicContext(options) {
|
|
125
147
|
const configuredRunGit = options.runGit;
|
|
@@ -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
|