@arnilo/prism-coding-agent 0.0.3 → 0.0.5
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/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/dist/edit.d.ts +3 -1
- package/dist/edit.js +15 -4
- package/dist/execution-policy.d.ts +9 -0
- package/dist/execution-policy.js +26 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +13 -6
- package/dist/read.d.ts +27 -8
- package/dist/read.js +69 -14
- package/dist/shell.d.ts +3 -1
- package/dist/shell.js +16 -1
- package/dist/write.d.ts +3 -1
- package/dist/write.js +16 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.5] - 2026-07-16
|
|
11
|
+
|
|
12
|
+
- Pinned the required `@arnilo/prism` peer and package metadata to 0.0.5; runtime behavior is unchanged.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [0.0.4] - 2026-07-14
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `read` tool image bounds: `maxImageBytes` (default 10 MB), optional `transformImage` callback, `DEFAULT_MAX_IMAGE_BYTES`, and `ReadOperations.statFile` for stat-first rejection.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Shell tools expose `exclusive: true`; all coding tools can apply host `ExecutionPolicy` checks before side effects.
|
|
24
|
+
- `autoResizeImages` on `read` is deprecated; it is ignored unless `transformImage` is also provided.
|
|
25
|
+
- Image read metadata now includes `image.bytes` and `image.resized` reflects whether `transformImage` ran.
|
|
26
|
+
|
|
10
27
|
## [0.0.3] - 2026-07-08
|
|
11
28
|
|
|
12
29
|
### Added
|
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ import { createReadOnlyTools } from "@arnilo/prism-coding-agent";
|
|
|
33
33
|
const tools = createToolRegistry(createReadOnlyTools(process.cwd()));
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Shared `ToolsOptions.executionPolicy` applies to every tool returned by full, all, and read-only aggregators unless a per-tool policy overrides it.
|
|
37
|
+
|
|
36
38
|
Individual tools with options:
|
|
37
39
|
|
|
38
40
|
```ts
|
package/dist/edit.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* completed, the edit is real and is reported as success rather than a misleading "aborted".
|
|
21
21
|
*/
|
|
22
22
|
import { Buffer } from "node:buffer";
|
|
23
|
-
import type { ToolDefinition } from "@arnilo/prism";
|
|
23
|
+
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
24
24
|
export interface Edit {
|
|
25
25
|
oldText: string;
|
|
26
26
|
newText: string;
|
|
@@ -47,6 +47,8 @@ export interface EditOperations {
|
|
|
47
47
|
access: (absolutePath: string) => Promise<void>;
|
|
48
48
|
}
|
|
49
49
|
export interface EditToolOptions {
|
|
50
|
+
/** Structured pre-execution policy checked before filesystem writes. */
|
|
51
|
+
executionPolicy?: ExecutionPolicy;
|
|
50
52
|
/** Custom operations backend (default: local filesystem). */
|
|
51
53
|
operations?: EditOperations;
|
|
52
54
|
}
|
package/dist/edit.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
+
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
3
4
|
import { resolveToCwd } from "./path-utils.js";
|
|
4
5
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
5
6
|
import { applyEditsToNormalizedContent, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
|
|
@@ -95,12 +96,22 @@ export function createEditTool(cwd, options) {
|
|
|
95
96
|
const edits = editsOrError;
|
|
96
97
|
try {
|
|
97
98
|
const absolutePath = resolveToCwd(prepared.path, cwd);
|
|
98
|
-
|
|
99
|
+
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
100
|
+
kind: "edit",
|
|
101
|
+
operation: "edit",
|
|
102
|
+
paths: [absolutePath],
|
|
103
|
+
risk: "medium",
|
|
104
|
+
metadata: { editCount: edits.length, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
|
|
105
|
+
}, toolCallId, "edit");
|
|
106
|
+
if (!policyCheck.allowed)
|
|
107
|
+
return policyCheck.result;
|
|
108
|
+
const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
|
|
109
|
+
return await withFileMutationQueue(allowedPath, async () => {
|
|
99
110
|
if (context.signal?.aborted)
|
|
100
111
|
return errorResult(toolCallId, "Operation aborted");
|
|
101
112
|
// Check the file is readable + writable.
|
|
102
113
|
try {
|
|
103
|
-
await ops.access(
|
|
114
|
+
await ops.access(allowedPath);
|
|
104
115
|
}
|
|
105
116
|
catch (error) {
|
|
106
117
|
if (context.signal?.aborted)
|
|
@@ -111,7 +122,7 @@ export function createEditTool(cwd, options) {
|
|
|
111
122
|
}
|
|
112
123
|
if (context.signal?.aborted)
|
|
113
124
|
return errorResult(toolCallId, "Operation aborted");
|
|
114
|
-
const buffer = await ops.readFile(
|
|
125
|
+
const buffer = await ops.readFile(allowedPath);
|
|
115
126
|
if (context.signal?.aborted)
|
|
116
127
|
return errorResult(toolCallId, "Operation aborted");
|
|
117
128
|
const rawContent = buffer.toString("utf-8");
|
|
@@ -132,7 +143,7 @@ export function createEditTool(cwd, options) {
|
|
|
132
143
|
if (context.signal?.aborted)
|
|
133
144
|
return errorResult(toolCallId, "Operation aborted");
|
|
134
145
|
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
135
|
-
await ops.writeFile(
|
|
146
|
+
await ops.writeFile(allowedPath, finalContent);
|
|
136
147
|
const diffResult = generateDiffString(baseContent, newContent);
|
|
137
148
|
const patch = generateUnifiedPatch(prepared.path, baseContent, newContent);
|
|
138
149
|
return {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExecutionAction, ExecutionPolicy } from "@arnilo/prism";
|
|
2
|
+
import type { ToolResult } from "@arnilo/prism";
|
|
3
|
+
export declare function enforceExecutionPolicy(policy: ExecutionPolicy | undefined, action: ExecutionAction, toolCallId: string, toolName: string): Promise<{
|
|
4
|
+
allowed: true;
|
|
5
|
+
action: ExecutionAction;
|
|
6
|
+
} | {
|
|
7
|
+
allowed: false;
|
|
8
|
+
result: ToolResult;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { assertExecutionAllowed, ExecutionDeniedError } from "@arnilo/prism";
|
|
2
|
+
export async function enforceExecutionPolicy(policy, action, toolCallId, toolName) {
|
|
3
|
+
if (!policy)
|
|
4
|
+
return { allowed: true, action };
|
|
5
|
+
try {
|
|
6
|
+
const allowedAction = await assertExecutionAllowed(policy, action);
|
|
7
|
+
return { allowed: true, action: allowedAction };
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof ExecutionDeniedError
|
|
11
|
+
? error.decision.reason ?? error.message
|
|
12
|
+
: error instanceof Error
|
|
13
|
+
? error.message
|
|
14
|
+
: String(error);
|
|
15
|
+
return {
|
|
16
|
+
allowed: false,
|
|
17
|
+
result: {
|
|
18
|
+
toolCallId,
|
|
19
|
+
name: toolName,
|
|
20
|
+
content: [{ type: "text", text: message }],
|
|
21
|
+
error: { message },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=execution-policy.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
export { createShellTool, createLocalBashOperations, getShellConfig, killProcessTree, waitForChildProcess, } from "./shell.js";
|
|
2
2
|
export type { ShellToolOptions, ShellConfig, BashOperations, BashExecOptions, BashSpawnContext, BashSpawnHook, } from "./shell.js";
|
|
3
|
-
export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, } from "./read.js";
|
|
4
|
-
export type { ReadToolOptions, ReadOperations } from "./read.js";
|
|
3
|
+
export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, DEFAULT_MAX_IMAGE_BYTES, } from "./read.js";
|
|
4
|
+
export type { ReadToolOptions, ReadOperations, TransformImage, TransformImageInput, } from "./read.js";
|
|
5
5
|
export { createWriteTool } from "./write.js";
|
|
6
6
|
export type { WriteToolOptions, WriteOperations } from "./write.js";
|
|
7
7
|
export { createEditTool } from "./edit.js";
|
|
8
8
|
export type { EditToolOptions, EditOperations, EditToolDetails, Edit } from "./edit.js";
|
|
9
9
|
export { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
10
|
-
|
|
10
|
+
export { enforceExecutionPolicy } from "./execution-policy.js";
|
|
11
|
+
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
11
12
|
import type { ShellToolOptions } from "./shell.js";
|
|
12
13
|
import type { ReadToolOptions } from "./read.js";
|
|
13
14
|
import type { WriteToolOptions } from "./write.js";
|
|
14
15
|
import type { EditToolOptions } from "./edit.js";
|
|
15
16
|
/** Per-tool options combined for the aggregator factories. */
|
|
16
17
|
export interface ToolsOptions {
|
|
18
|
+
/** Shared execution policy applied to every coding tool unless overridden per tool. */
|
|
19
|
+
executionPolicy?: ExecutionPolicy;
|
|
17
20
|
shell?: ShellToolOptions;
|
|
18
21
|
read?: ReadToolOptions;
|
|
19
22
|
write?: WriteToolOptions;
|
package/dist/index.js
CHANGED
|
@@ -5,29 +5,36 @@
|
|
|
5
5
|
// `createToolRegistry(createCodingTools(cwd))`). No tools are auto-registered — import what you need.
|
|
6
6
|
// --- per-tool factories & types ---
|
|
7
7
|
export { createShellTool, createLocalBashOperations, getShellConfig, killProcessTree, waitForChildProcess, } from "./shell.js";
|
|
8
|
-
export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, } from "./read.js";
|
|
8
|
+
export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, DEFAULT_MAX_IMAGE_BYTES, } from "./read.js";
|
|
9
9
|
export { createWriteTool } from "./write.js";
|
|
10
10
|
export { createEditTool } from "./edit.js";
|
|
11
11
|
// --- generic primitives (re-exported for hosts that want them) ---
|
|
12
12
|
export { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
13
|
+
export { enforceExecutionPolicy } from "./execution-policy.js";
|
|
13
14
|
import { createShellTool } from "./shell.js";
|
|
14
15
|
import { createReadTool } from "./read.js";
|
|
15
16
|
import { createWriteTool } from "./write.js";
|
|
16
17
|
import { createEditTool } from "./edit.js";
|
|
18
|
+
function withSharedExecutionPolicy(toolOptions, shared) {
|
|
19
|
+
if (!shared)
|
|
20
|
+
return (toolOptions ?? {});
|
|
21
|
+
return { ...(toolOptions ?? {}), executionPolicy: toolOptions?.executionPolicy ?? shared };
|
|
22
|
+
}
|
|
17
23
|
/**
|
|
18
24
|
* The four coding tools: `shell`, `read`, `write`, `edit`. Register all of them for a coding agent.
|
|
19
25
|
*/
|
|
20
26
|
export function createCodingTools(cwd, options) {
|
|
27
|
+
const policy = options?.executionPolicy;
|
|
21
28
|
return [
|
|
22
|
-
createShellTool(cwd, options?.shell),
|
|
23
|
-
createReadTool(cwd, options?.read),
|
|
24
|
-
createWriteTool(cwd, options?.write),
|
|
25
|
-
createEditTool(cwd, options?.edit),
|
|
29
|
+
createShellTool(cwd, withSharedExecutionPolicy(options?.shell, policy)),
|
|
30
|
+
createReadTool(cwd, withSharedExecutionPolicy(options?.read, policy)),
|
|
31
|
+
createWriteTool(cwd, withSharedExecutionPolicy(options?.write, policy)),
|
|
32
|
+
createEditTool(cwd, withSharedExecutionPolicy(options?.edit, policy)),
|
|
26
33
|
];
|
|
27
34
|
}
|
|
28
35
|
/** Read-only subset: `read` only (this package ships no grep/find/ls). */
|
|
29
36
|
export function createReadOnlyTools(cwd, options) {
|
|
30
|
-
return [createReadTool(cwd, options?.read)];
|
|
37
|
+
return [createReadTool(cwd, withSharedExecutionPolicy(options?.read, options?.executionPolicy))];
|
|
31
38
|
}
|
|
32
39
|
/** Every tool this package provides — identical to {@link createCodingTools} for now. */
|
|
33
40
|
export function createAllTools(cwd, options) {
|
package/dist/read.d.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* the model-aware non-vision note (Prism's `ToolExecutionContext` has no model field).
|
|
9
9
|
*
|
|
10
10
|
* Deviations from pi (documented):
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* when
|
|
11
|
+
* - **Image resize is host-owned.** pi resizes images to ≤2000×2000 via a photon/WASM +
|
|
12
|
+
* `worker_threads` helper (`utils/image-process.js`); this package rejects oversize images by
|
|
13
|
+
* `stat`/`buffer.length` against `maxImageBytes` and accepts an optional `transformImage`
|
|
14
|
+
* callback for host-provided resizing. `autoResizeImages` is deprecated — it only takes effect
|
|
15
|
+
* when paired with `transformImage`.
|
|
16
16
|
* - Abort + all read failures return a Prism `error` result (pi throws/rejects). Prism's
|
|
17
17
|
* `dispatchToolCall` would catch a throw anyway, but returning a clean error result is predictable
|
|
18
18
|
* for direct-`execute` callers and matches the package's `shell` tool.
|
|
@@ -20,12 +20,21 @@
|
|
|
20
20
|
* named `shell`.
|
|
21
21
|
*/
|
|
22
22
|
import { Buffer } from "node:buffer";
|
|
23
|
-
import type { ToolDefinition } from "@arnilo/prism";
|
|
23
|
+
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
24
24
|
import { type TruncationResult } from "./truncate.js";
|
|
25
25
|
/** Detect a supported image MIME type from a buffer's leading bytes. Returns null for non-images. */
|
|
26
26
|
export declare function detectSupportedImageMimeType(buffer: Buffer): string | null;
|
|
27
27
|
/** Sniff the leading bytes of a file and return its image MIME type (null if not a supported image). */
|
|
28
28
|
export declare function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null>;
|
|
29
|
+
/** Default maximum image file size before read/transform (10 MB). */
|
|
30
|
+
export declare const DEFAULT_MAX_IMAGE_BYTES = 10000000;
|
|
31
|
+
/** Input passed to an optional host-owned image transformer. */
|
|
32
|
+
export interface TransformImageInput {
|
|
33
|
+
readonly buffer: Buffer;
|
|
34
|
+
readonly mimeType: string;
|
|
35
|
+
}
|
|
36
|
+
/** Host callback to resize or re-encode an image before base64 encoding. */
|
|
37
|
+
export type TransformImage = (input: TransformImageInput) => Promise<Buffer>;
|
|
29
38
|
/**
|
|
30
39
|
* Pluggable operations for the read tool. Override to delegate file reading to remote systems
|
|
31
40
|
* (e.g. SSH) while keeping the tool's truncation/offset/limit behavior.
|
|
@@ -35,15 +44,25 @@ export interface ReadOperations {
|
|
|
35
44
|
readFile: (absolutePath: string) => Promise<Buffer>;
|
|
36
45
|
/** Check the file is readable (throw if not). */
|
|
37
46
|
access: (absolutePath: string) => Promise<void>;
|
|
47
|
+
/** Return file size in bytes for image bound checks (default: local `fs.stat`). */
|
|
48
|
+
statFile?: (absolutePath: string) => Promise<{
|
|
49
|
+
size: number;
|
|
50
|
+
}>;
|
|
38
51
|
/** Detect image MIME type from the file; return null/undefined for non-images. */
|
|
39
52
|
detectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;
|
|
40
53
|
}
|
|
41
54
|
export interface ReadToolOptions {
|
|
55
|
+
/** Structured pre-execution policy checked before filesystem access. */
|
|
56
|
+
executionPolicy?: ExecutionPolicy;
|
|
42
57
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
58
|
+
* @deprecated Use `transformImage` instead. When `transformImage` is absent this flag is ignored.
|
|
59
|
+
* When both are set, `transformImage` runs and `image.resized` is `true` on success.
|
|
45
60
|
*/
|
|
46
61
|
autoResizeImages?: boolean;
|
|
62
|
+
/** Reject image reads larger than this many bytes (default {@link DEFAULT_MAX_IMAGE_BYTES}). */
|
|
63
|
+
maxImageBytes?: number;
|
|
64
|
+
/** Optional host callback to resize or re-encode images before base64 encoding. */
|
|
65
|
+
transformImage?: TransformImage;
|
|
47
66
|
/** Custom operations backend (default: local filesystem). */
|
|
48
67
|
operations?: ReadOperations;
|
|
49
68
|
/** Max lines kept from the head (default 2000). */
|
package/dist/read.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* the model-aware non-vision note (Prism's `ToolExecutionContext` has no model field).
|
|
9
9
|
*
|
|
10
10
|
* Deviations from pi (documented):
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* when
|
|
11
|
+
* - **Image resize is host-owned.** pi resizes images to ≤2000×2000 via a photon/WASM +
|
|
12
|
+
* `worker_threads` helper (`utils/image-process.js`); this package rejects oversize images by
|
|
13
|
+
* `stat`/`buffer.length` against `maxImageBytes` and accepts an optional `transformImage`
|
|
14
|
+
* callback for host-provided resizing. `autoResizeImages` is deprecated — it only takes effect
|
|
15
|
+
* when paired with `transformImage`.
|
|
16
16
|
* - Abort + all read failures return a Prism `error` result (pi throws/rejects). Prism's
|
|
17
17
|
* `dispatchToolCall` would catch a throw anyway, but returning a clean error result is predictable
|
|
18
18
|
* for direct-`execute` callers and matches the package's `shell` tool.
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { Buffer } from "node:buffer";
|
|
23
23
|
import { constants } from "node:fs";
|
|
24
|
-
import { access as fsAccess, open, readFile as fsReadFile } from "node:fs/promises";
|
|
24
|
+
import { access as fsAccess, open, readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
25
|
+
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
25
26
|
import { resolveReadPathAsync } from "./path-utils.js";
|
|
26
27
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, } from "./truncate.js";
|
|
27
28
|
// --- magic-byte image MIME detection (faithful port of pi utils/mime.js, pure JS, no deps) ---
|
|
@@ -140,11 +141,51 @@ function startsWithAscii(buffer, offset, text) {
|
|
|
140
141
|
}
|
|
141
142
|
return true;
|
|
142
143
|
}
|
|
144
|
+
// --- read tool ---
|
|
145
|
+
/** Default maximum image file size before read/transform (10 MB). */
|
|
146
|
+
export const DEFAULT_MAX_IMAGE_BYTES = 10_000_000;
|
|
143
147
|
const defaultReadOperations = {
|
|
144
148
|
readFile: (path) => fsReadFile(path),
|
|
145
149
|
access: (path) => fsAccess(path, constants.R_OK),
|
|
150
|
+
statFile: async (path) => {
|
|
151
|
+
const info = await fsStat(path);
|
|
152
|
+
return { size: info.size };
|
|
153
|
+
},
|
|
146
154
|
detectImageMimeType: detectSupportedImageMimeTypeFromFile,
|
|
147
155
|
};
|
|
156
|
+
function imageSizeError(actualBytes, maxImageBytes) {
|
|
157
|
+
return `Image file is ${formatSize(actualBytes)}, exceeds ${formatSize(maxImageBytes)} limit.`;
|
|
158
|
+
}
|
|
159
|
+
async function loadImageBuffer(absolutePath, mimeType, ops, options) {
|
|
160
|
+
if (options.signal?.aborted) {
|
|
161
|
+
throw new Error("Operation aborted");
|
|
162
|
+
}
|
|
163
|
+
if (ops.statFile) {
|
|
164
|
+
const { size } = await ops.statFile(absolutePath);
|
|
165
|
+
if (size > options.maxImageBytes) {
|
|
166
|
+
throw new Error(imageSizeError(size, options.maxImageBytes));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let buffer = await ops.readFile(absolutePath);
|
|
170
|
+
if (buffer.length > options.maxImageBytes) {
|
|
171
|
+
throw new Error(imageSizeError(buffer.length, options.maxImageBytes));
|
|
172
|
+
}
|
|
173
|
+
let resized = false;
|
|
174
|
+
if (options.transformImage) {
|
|
175
|
+
if (options.signal?.aborted) {
|
|
176
|
+
throw new Error("Operation aborted");
|
|
177
|
+
}
|
|
178
|
+
buffer = await options.transformImage({ buffer, mimeType });
|
|
179
|
+
resized = true;
|
|
180
|
+
if (buffer.length > options.maxImageBytes) {
|
|
181
|
+
throw new Error(`Transformed image is ${formatSize(buffer.length)}, exceeds ${formatSize(options.maxImageBytes)} limit.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (options.autoResizeImages) {
|
|
185
|
+
// Deprecated flag without a transformer — intentionally ignored for backward compatibility.
|
|
186
|
+
}
|
|
187
|
+
return { buffer, resized };
|
|
188
|
+
}
|
|
148
189
|
function errorResult(toolCallId, message) {
|
|
149
190
|
return {
|
|
150
191
|
toolCallId,
|
|
@@ -154,9 +195,10 @@ function errorResult(toolCallId, message) {
|
|
|
154
195
|
};
|
|
155
196
|
}
|
|
156
197
|
export function createReadTool(cwd, options) {
|
|
157
|
-
const ops = options?.operations
|
|
198
|
+
const ops = { ...defaultReadOperations, ...options?.operations };
|
|
158
199
|
const maxLines = options?.maxLines ?? DEFAULT_MAX_LINES;
|
|
159
200
|
const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
201
|
+
const maxImageBytes = options?.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
|
|
160
202
|
return {
|
|
161
203
|
name: "read",
|
|
162
204
|
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp); images are returned as image content. For text files, output is truncated to ${maxLines} lines or ${maxBytes / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
|
|
@@ -183,12 +225,25 @@ export function createReadTool(cwd, options) {
|
|
|
183
225
|
}
|
|
184
226
|
try {
|
|
185
227
|
const absolutePath = await resolveReadPathAsync(path, cwd);
|
|
186
|
-
await
|
|
187
|
-
|
|
228
|
+
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
229
|
+
kind: "read",
|
|
230
|
+
operation: "read",
|
|
231
|
+
paths: [absolutePath],
|
|
232
|
+
risk: "low",
|
|
233
|
+
metadata: { offset, limit, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
|
|
234
|
+
}, toolCallId, "read");
|
|
235
|
+
if (!policyCheck.allowed)
|
|
236
|
+
return policyCheck.result;
|
|
237
|
+
const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
|
|
238
|
+
await ops.access(allowedPath);
|
|
239
|
+
const mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(allowedPath) : undefined;
|
|
188
240
|
if (mimeType) {
|
|
189
|
-
const buffer = await ops
|
|
190
|
-
|
|
191
|
-
|
|
241
|
+
const { buffer, resized } = await loadImageBuffer(allowedPath, mimeType, ops, {
|
|
242
|
+
maxImageBytes,
|
|
243
|
+
transformImage: options?.transformImage,
|
|
244
|
+
autoResizeImages: options?.autoResizeImages,
|
|
245
|
+
signal: context.signal,
|
|
246
|
+
});
|
|
192
247
|
return {
|
|
193
248
|
toolCallId,
|
|
194
249
|
name: "read",
|
|
@@ -196,11 +251,11 @@ export function createReadTool(cwd, options) {
|
|
|
196
251
|
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
197
252
|
{ type: "image", data: buffer.toString("base64"), mimeType },
|
|
198
253
|
],
|
|
199
|
-
metadata: { image: { mimeType, resized:
|
|
254
|
+
metadata: { image: { mimeType, resized, bytes: buffer.length } },
|
|
200
255
|
};
|
|
201
256
|
}
|
|
202
257
|
// Text path: faithful port of pi's offset/limit → truncateHead → continuation logic.
|
|
203
|
-
const buffer = await ops.readFile(
|
|
258
|
+
const buffer = await ops.readFile(allowedPath);
|
|
204
259
|
const textContent = buffer.toString("utf-8");
|
|
205
260
|
const allLines = textContent.split("\n");
|
|
206
261
|
const totalFileLines = allLines.length;
|
package/dist/shell.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* (argv `-c` only). Default spawn env is `process.env` (no pi CLI binDir PATH injection).
|
|
22
22
|
*/
|
|
23
23
|
import { type ChildProcess } from "node:child_process";
|
|
24
|
-
import type { ToolDefinition } from "@arnilo/prism";
|
|
24
|
+
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
25
25
|
export interface ShellConfig {
|
|
26
26
|
shell: string;
|
|
27
27
|
args: string[];
|
|
@@ -45,6 +45,8 @@ export interface BashOperations {
|
|
|
45
45
|
}>;
|
|
46
46
|
}
|
|
47
47
|
export interface ShellToolOptions {
|
|
48
|
+
/** Structured pre-execution policy checked before spawn. */
|
|
49
|
+
executionPolicy?: ExecutionPolicy;
|
|
48
50
|
/** Custom operations backend (default: local shell). Override to delegate to remote shells. */
|
|
49
51
|
operations?: BashOperations;
|
|
50
52
|
/** Command prefix prepended to every command (e.g. shell setup commands). */
|
package/dist/shell.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
import { constants, existsSync } from "node:fs";
|
|
25
25
|
import { access as fsAccess } from "node:fs/promises";
|
|
26
|
+
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
26
27
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
27
28
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "./truncate.js";
|
|
28
29
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
@@ -247,6 +248,7 @@ export function createShellTool(cwd, options) {
|
|
|
247
248
|
const tempFilePrefix = options?.tempFilePrefix ?? "prism-shell";
|
|
248
249
|
return {
|
|
249
250
|
name: "shell",
|
|
251
|
+
exclusive: true,
|
|
250
252
|
description: `Execute a shell command in the current working directory. Returns combined stdout and stderr. Output is truncated to the last ${maxLines} lines or ${maxBytes / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
|
251
253
|
parameters: {
|
|
252
254
|
type: "object",
|
|
@@ -270,9 +272,22 @@ export function createShellTool(cwd, options) {
|
|
|
270
272
|
};
|
|
271
273
|
}
|
|
272
274
|
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
|
273
|
-
|
|
275
|
+
let spawnContext = spawnHook
|
|
274
276
|
? spawnHook({ command: resolvedCommand, cwd, env: { ...process.env } })
|
|
275
277
|
: { command: resolvedCommand, cwd, env: { ...process.env } };
|
|
278
|
+
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
279
|
+
kind: "shell",
|
|
280
|
+
operation: "execute",
|
|
281
|
+
command: spawnContext.command,
|
|
282
|
+
paths: [spawnContext.cwd],
|
|
283
|
+
risk: "high",
|
|
284
|
+
metadata: { timeout, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
|
|
285
|
+
}, toolCallId, "shell");
|
|
286
|
+
if (!policyCheck.allowed)
|
|
287
|
+
return policyCheck.result;
|
|
288
|
+
if (policyCheck.action.command) {
|
|
289
|
+
spawnContext = { ...spawnContext, command: policyCheck.action.command };
|
|
290
|
+
}
|
|
276
291
|
const output = new OutputAccumulator({ maxLines, maxBytes, tempFilePrefix });
|
|
277
292
|
let acceptingOutput = true;
|
|
278
293
|
const handleData = (data) => {
|
package/dist/write.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@arnilo/prism";
|
|
1
|
+
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
2
2
|
/**
|
|
3
3
|
* Pluggable operations for the write tool. Override to delegate file writing to remote systems
|
|
4
4
|
* (e.g. SSH) while keeping the tool's directory-creation + per-path serialization behavior.
|
|
@@ -10,6 +10,8 @@ export interface WriteOperations {
|
|
|
10
10
|
mkdir: (dir: string) => Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
export interface WriteToolOptions {
|
|
13
|
+
/** Structured pre-execution policy checked before filesystem writes. */
|
|
14
|
+
executionPolicy?: ExecutionPolicy;
|
|
13
15
|
/** Custom operations backend (default: local filesystem). */
|
|
14
16
|
operations?: WriteOperations;
|
|
15
17
|
}
|
package/dist/write.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { Buffer } from "node:buffer";
|
|
18
18
|
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "node:fs/promises";
|
|
19
19
|
import { dirname } from "node:path";
|
|
20
|
+
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
20
21
|
import { resolveToCwd } from "./path-utils.js";
|
|
21
22
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
22
23
|
const defaultWriteOperations = {
|
|
@@ -62,8 +63,18 @@ export function createWriteTool(cwd, options) {
|
|
|
62
63
|
}
|
|
63
64
|
try {
|
|
64
65
|
const absolutePath = resolveToCwd(path, cwd);
|
|
65
|
-
const
|
|
66
|
-
|
|
66
|
+
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
67
|
+
kind: "write",
|
|
68
|
+
operation: "write",
|
|
69
|
+
paths: [absolutePath],
|
|
70
|
+
risk: "medium",
|
|
71
|
+
metadata: { bytes: Buffer.byteLength(content, "utf-8"), sessionId: context.sessionId, runId: context.runId, signal: context.signal },
|
|
72
|
+
}, toolCallId, "write");
|
|
73
|
+
if (!policyCheck.allowed)
|
|
74
|
+
return policyCheck.result;
|
|
75
|
+
const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
|
|
76
|
+
const dir = dirname(allowedPath);
|
|
77
|
+
return await withFileMutationQueue(allowedPath, async () => {
|
|
67
78
|
// Check abort before each fs op — do not start a new operation once aborted. We intentionally
|
|
68
79
|
// do NOT throw from an abort listener: that could release the mutation queue mid-operation.
|
|
69
80
|
if (context.signal?.aborted)
|
|
@@ -71,7 +82,7 @@ export function createWriteTool(cwd, options) {
|
|
|
71
82
|
await ops.mkdir(dir);
|
|
72
83
|
if (context.signal?.aborted)
|
|
73
84
|
return errorResult(toolCallId, "Operation aborted");
|
|
74
|
-
await ops.writeFile(
|
|
85
|
+
await ops.writeFile(allowedPath, content);
|
|
75
86
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
76
87
|
const lines = countLines(content);
|
|
77
88
|
return {
|
|
@@ -80,10 +91,10 @@ export function createWriteTool(cwd, options) {
|
|
|
80
91
|
content: [
|
|
81
92
|
{
|
|
82
93
|
type: "text",
|
|
83
|
-
text: `Successfully wrote ${bytes} bytes (${lines} lines) to ${
|
|
94
|
+
text: `Successfully wrote ${bytes} bytes (${lines} lines) to ${allowedPath}`,
|
|
84
95
|
},
|
|
85
96
|
],
|
|
86
|
-
metadata: { bytes, lines, path:
|
|
97
|
+
metadata: { bytes, lines, path: allowedPath },
|
|
87
98
|
};
|
|
88
99
|
});
|
|
89
100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-coding-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Optional coding-agent tools (shell, read, write, edit) package for Prism.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"diff": "^8.0.4"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@arnilo/prism": "0.0.
|
|
31
|
+
"@arnilo/prism": "0.0.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@arnilo/prism": "file:../.."
|