@animalabs/agent-framework 0.6.6 → 0.6.7
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 +78 -0
- package/dist/src/agent.d.ts +39 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +172 -56
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server.js +0 -0
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/canonical-json.d.ts +2 -0
- package/dist/src/canonical-json.d.ts.map +1 -0
- package/dist/src/canonical-json.js +23 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/framework.d.ts +147 -3
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +2719 -125
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +9 -2
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +3 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +29 -15
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +36 -3
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +142 -28
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +12 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +661 -2
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +4 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
- package/dist/src/recovery/discord-awareness-outbox.js +334 -0
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
- package/dist/src/recovery/offline-branch.d.ts +45 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -0
- package/dist/src/recovery/offline-branch.js +362 -0
- package/dist/src/recovery/offline-branch.js.map +1 -0
- package/dist/src/recovery/recover-cli.d.ts +3 -0
- package/dist/src/recovery/recover-cli.d.ts.map +1 -0
- package/dist/src/recovery/recover-cli.js +129 -0
- package/dist/src/recovery/recover-cli.js.map +1 -0
- package/dist/src/types/agent.d.ts +22 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +31 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +200 -93
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
- package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
- package/dist/test/busy-agent-scheduler.test.js +62 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
- package/dist/test/channel-lifecycle-provenance.test.js +47 -0
- package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
- package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
- package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
- package/dist/test/discord-awareness-outbox.test.js +248 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -0
- package/dist/test/helpers/mock-membrane.js +1 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +100 -0
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +845 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +15 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/offline-recovery-branch.test.d.ts +2 -0
- package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
- package/dist/test/offline-recovery-branch.test.js +226 -0
- package/dist/test/offline-recovery-branch.test.js.map +1 -0
- package/dist/test/present-while-acting.test.d.ts +3 -2
- package/dist/test/present-while-acting.test.d.ts.map +1 -1
- package/dist/test/present-while-acting.test.js +31 -3
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
- package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
- package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
- package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
- package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
- package/dist/test/same-round-think-policy.test.d.ts +2 -0
- package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
- package/dist/test/same-round-think-policy.test.js +853 -0
- package/dist/test/same-round-think-policy.test.js.map +1 -0
- package/dist/test/workspace-read-image.test.d.ts +2 -0
- package/dist/test/workspace-read-image.test.d.ts.map +1 -0
- package/dist/test/workspace-read-image.test.js +518 -0
- package/dist/test/workspace-read-image.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -3
- package/src/agent.ts +265 -62
- package/src/api/server.ts +8 -2
- package/src/canonical-json.ts +22 -0
- package/src/framework.ts +3561 -145
- package/src/gate/event-gate.ts +15 -3
- package/src/index.ts +20 -0
- package/src/mcpl/channel-registry.ts +31 -16
- package/src/mcpl/server-connection.ts +163 -28
- package/src/modules/workspace/index.ts +759 -2
- package/src/modules/workspace/types.ts +5 -0
- package/src/recovery/discord-awareness-outbox.ts +459 -0
- package/src/recovery/offline-branch.ts +510 -0
- package/src/recovery/recover-cli.ts +130 -0
- package/src/types/agent.ts +26 -0
- package/src/types/framework.ts +17 -0
- package/src/types/module.ts +33 -0
- package/src/types/trace.ts +2 -0
|
@@ -4,11 +4,408 @@
|
|
|
4
4
|
* Provides a unified workspace with mount-based filesystem access,
|
|
5
5
|
* auto-sync between real filesystem and Chronicle, and manual materialization.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { constants as fsConstants } from 'node:fs';
|
|
8
|
+
import { open, readFile, stat, writeFile, unlink, mkdir, lstat, realpath } from 'node:fs/promises';
|
|
9
|
+
import { join, resolve, relative, dirname, sep } from 'node:path';
|
|
9
10
|
import { WORKSPACE_FS_EVENT_TYPES, opToEventType } from './types.js';
|
|
10
11
|
import { MountWatcher } from './watcher.js';
|
|
11
12
|
import { syncFromFs, materializeToFs, hashContent, DEFAULT_MAX_FILE_SIZE } from './sync.js';
|
|
13
|
+
class WorkspaceImageReadError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(code, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.name = 'WorkspaceImageReadError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
22
|
+
const JPEG_SIGNATURE = Buffer.from([0xff, 0xd8, 0xff]);
|
|
23
|
+
const GIF87A_SIGNATURE = Buffer.from('GIF87a', 'ascii');
|
|
24
|
+
const GIF89A_SIGNATURE = Buffer.from('GIF89a', 'ascii');
|
|
25
|
+
const RIFF_SIGNATURE = Buffer.from('RIFF', 'ascii');
|
|
26
|
+
const WEBP_SIGNATURE = Buffer.from('WEBP', 'ascii');
|
|
27
|
+
const JPEG_SOF_MARKERS = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf]);
|
|
28
|
+
const GIF_TRAILER = 0x3b;
|
|
29
|
+
const GIF_EXTENSION = 0x21;
|
|
30
|
+
const GIF_IMAGE_DESCRIPTOR = 0x2c;
|
|
31
|
+
const JPEG_SOI = 0xd8;
|
|
32
|
+
const JPEG_EOI = 0xd9;
|
|
33
|
+
const JPEG_SOS = 0xda;
|
|
34
|
+
const JPEG_TEM = 0x01;
|
|
35
|
+
function isErrnoCode(err, code) {
|
|
36
|
+
return typeof err === 'object' && err !== null && 'code' in err && err.code === code;
|
|
37
|
+
}
|
|
38
|
+
function startsWithBytes(buffer, prefix) {
|
|
39
|
+
return buffer.length >= prefix.length && buffer.subarray(0, prefix.length).equals(prefix);
|
|
40
|
+
}
|
|
41
|
+
function matchesPartialPrefix(buffer, prefix) {
|
|
42
|
+
return buffer.length > 0 && prefix.subarray(0, buffer.length).equals(buffer);
|
|
43
|
+
}
|
|
44
|
+
function isContainedPath(root, candidate) {
|
|
45
|
+
if (candidate === root)
|
|
46
|
+
return true;
|
|
47
|
+
const normalizedRoot = root.endsWith(sep) ? root : root + sep;
|
|
48
|
+
return candidate.startsWith(normalizedRoot);
|
|
49
|
+
}
|
|
50
|
+
function invalidImage(format, mountPrefixedPath) {
|
|
51
|
+
throw new WorkspaceImageReadError('invalid', `Invalid ${format} image: ${mountPrefixedPath}`);
|
|
52
|
+
}
|
|
53
|
+
function requireBufferRange(bytes, start, length, format, mountPrefixedPath) {
|
|
54
|
+
if (start < 0 || length < 0 || start + length > bytes.length) {
|
|
55
|
+
invalidImage(format, mountPrefixedPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readUInt24LE(bytes, offset) {
|
|
59
|
+
return bytes[offset] + (bytes[offset + 1] << 8) + (bytes[offset + 2] << 16);
|
|
60
|
+
}
|
|
61
|
+
function parseGifSubBlocks(bytes, offset, mountPrefixedPath) {
|
|
62
|
+
while (true) {
|
|
63
|
+
requireBufferRange(bytes, offset, 1, 'GIF', mountPrefixedPath);
|
|
64
|
+
const blockLength = bytes[offset];
|
|
65
|
+
offset += 1;
|
|
66
|
+
if (blockLength === 0)
|
|
67
|
+
return offset;
|
|
68
|
+
requireBufferRange(bytes, offset, blockLength, 'GIF', mountPrefixedPath);
|
|
69
|
+
offset += blockLength;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function validatePng(bytes, mountPrefixedPath) {
|
|
73
|
+
let offset = PNG_SIGNATURE.length;
|
|
74
|
+
let sawIend = false;
|
|
75
|
+
let sawNonEmptyIdat = false;
|
|
76
|
+
while (!sawIend) {
|
|
77
|
+
requireBufferRange(bytes, offset, 12, 'PNG', mountPrefixedPath);
|
|
78
|
+
const chunkLength = bytes.readUInt32BE(offset);
|
|
79
|
+
const chunkType = bytes.toString('ascii', offset + 4, offset + 8);
|
|
80
|
+
const chunkDataOffset = offset + 8;
|
|
81
|
+
const chunkEnd = chunkDataOffset + chunkLength;
|
|
82
|
+
const nextOffset = chunkEnd + 4;
|
|
83
|
+
requireBufferRange(bytes, chunkDataOffset, chunkLength + 4, 'PNG', mountPrefixedPath);
|
|
84
|
+
if (offset === PNG_SIGNATURE.length) {
|
|
85
|
+
if (chunkType !== 'IHDR' || chunkLength !== 13) {
|
|
86
|
+
invalidImage('PNG', mountPrefixedPath);
|
|
87
|
+
}
|
|
88
|
+
const width = bytes.readUInt32BE(chunkDataOffset);
|
|
89
|
+
const height = bytes.readUInt32BE(chunkDataOffset + 4);
|
|
90
|
+
if (width === 0 || height === 0) {
|
|
91
|
+
invalidImage('PNG', mountPrefixedPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (chunkType === 'IDAT' && chunkLength > 0) {
|
|
95
|
+
sawNonEmptyIdat = true;
|
|
96
|
+
}
|
|
97
|
+
if (chunkType === 'IEND') {
|
|
98
|
+
if (chunkLength !== 0 || nextOffset !== bytes.length || !sawNonEmptyIdat) {
|
|
99
|
+
invalidImage('PNG', mountPrefixedPath);
|
|
100
|
+
}
|
|
101
|
+
sawIend = true;
|
|
102
|
+
}
|
|
103
|
+
offset = nextOffset;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function validateGif(bytes, mountPrefixedPath) {
|
|
107
|
+
requireBufferRange(bytes, 0, 13, 'GIF', mountPrefixedPath);
|
|
108
|
+
const width = bytes.readUInt16LE(6);
|
|
109
|
+
const height = bytes.readUInt16LE(8);
|
|
110
|
+
if (width === 0 || height === 0) {
|
|
111
|
+
invalidImage('GIF', mountPrefixedPath);
|
|
112
|
+
}
|
|
113
|
+
let offset = 13;
|
|
114
|
+
const packed = bytes[10];
|
|
115
|
+
if ((packed & 0x80) !== 0) {
|
|
116
|
+
const globalColorTableBytes = 3 * (1 << ((packed & 0x07) + 1));
|
|
117
|
+
requireBufferRange(bytes, offset, globalColorTableBytes, 'GIF', mountPrefixedPath);
|
|
118
|
+
offset += globalColorTableBytes;
|
|
119
|
+
}
|
|
120
|
+
let sawImage = false;
|
|
121
|
+
while (offset < bytes.length) {
|
|
122
|
+
const marker = bytes[offset];
|
|
123
|
+
offset += 1;
|
|
124
|
+
if (marker === GIF_TRAILER) {
|
|
125
|
+
if (!sawImage || offset !== bytes.length) {
|
|
126
|
+
invalidImage('GIF', mountPrefixedPath);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (marker === GIF_EXTENSION) {
|
|
131
|
+
requireBufferRange(bytes, offset, 1, 'GIF', mountPrefixedPath);
|
|
132
|
+
offset += 1; // extension label
|
|
133
|
+
offset = parseGifSubBlocks(bytes, offset, mountPrefixedPath);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (marker !== GIF_IMAGE_DESCRIPTOR) {
|
|
137
|
+
invalidImage('GIF', mountPrefixedPath);
|
|
138
|
+
}
|
|
139
|
+
sawImage = true;
|
|
140
|
+
requireBufferRange(bytes, offset, 9, 'GIF', mountPrefixedPath);
|
|
141
|
+
const imageWidth = bytes.readUInt16LE(offset + 4);
|
|
142
|
+
const imageHeight = bytes.readUInt16LE(offset + 6);
|
|
143
|
+
if (imageWidth === 0 || imageHeight === 0) {
|
|
144
|
+
invalidImage('GIF', mountPrefixedPath);
|
|
145
|
+
}
|
|
146
|
+
const imagePacked = bytes[offset + 8];
|
|
147
|
+
offset += 9;
|
|
148
|
+
if ((imagePacked & 0x80) !== 0) {
|
|
149
|
+
const localColorTableBytes = 3 * (1 << ((imagePacked & 0x07) + 1));
|
|
150
|
+
requireBufferRange(bytes, offset, localColorTableBytes, 'GIF', mountPrefixedPath);
|
|
151
|
+
offset += localColorTableBytes;
|
|
152
|
+
}
|
|
153
|
+
requireBufferRange(bytes, offset, 1, 'GIF', mountPrefixedPath);
|
|
154
|
+
const lzwMinimumCodeSize = bytes[offset];
|
|
155
|
+
if (lzwMinimumCodeSize < 2 || lzwMinimumCodeSize > 8) {
|
|
156
|
+
invalidImage('GIF', mountPrefixedPath);
|
|
157
|
+
}
|
|
158
|
+
offset += 1;
|
|
159
|
+
offset = parseGifSubBlocks(bytes, offset, mountPrefixedPath);
|
|
160
|
+
}
|
|
161
|
+
invalidImage('GIF', mountPrefixedPath);
|
|
162
|
+
}
|
|
163
|
+
function scanJpegEntropyData(bytes, offset, mountPrefixedPath) {
|
|
164
|
+
// Scan data can contain 0xFF byte-stuffing and restart markers.
|
|
165
|
+
while (offset < bytes.length) {
|
|
166
|
+
const value = bytes[offset];
|
|
167
|
+
offset += 1;
|
|
168
|
+
if (value !== 0xff)
|
|
169
|
+
continue;
|
|
170
|
+
while (offset < bytes.length && bytes[offset] === 0xff) {
|
|
171
|
+
offset += 1;
|
|
172
|
+
}
|
|
173
|
+
if (offset >= bytes.length)
|
|
174
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
175
|
+
const marker = bytes[offset];
|
|
176
|
+
if (marker === 0x00) {
|
|
177
|
+
offset += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (marker >= 0xd0 && marker <= 0xd7) {
|
|
181
|
+
offset += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
return offset - 1;
|
|
185
|
+
}
|
|
186
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
187
|
+
}
|
|
188
|
+
function validateJpeg(bytes, mountPrefixedPath) {
|
|
189
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== JPEG_SOI) {
|
|
190
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
191
|
+
}
|
|
192
|
+
let offset = 2;
|
|
193
|
+
let sawSof = false;
|
|
194
|
+
let sawSos = false;
|
|
195
|
+
while (offset < bytes.length) {
|
|
196
|
+
if (bytes[offset] !== 0xff) {
|
|
197
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
198
|
+
}
|
|
199
|
+
while (offset < bytes.length && bytes[offset] === 0xff) {
|
|
200
|
+
offset += 1;
|
|
201
|
+
}
|
|
202
|
+
if (offset >= bytes.length)
|
|
203
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
204
|
+
const marker = bytes[offset];
|
|
205
|
+
offset += 1;
|
|
206
|
+
if (marker === JPEG_EOI) {
|
|
207
|
+
if (!sawSof || !sawSos || offset !== bytes.length) {
|
|
208
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (marker === JPEG_TEM || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
requireBufferRange(bytes, offset, 2, 'JPEG', mountPrefixedPath);
|
|
216
|
+
const segmentLength = bytes.readUInt16BE(offset);
|
|
217
|
+
if (segmentLength < 2) {
|
|
218
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
219
|
+
}
|
|
220
|
+
offset += 2;
|
|
221
|
+
const payloadOffset = offset;
|
|
222
|
+
const payloadLength = segmentLength - 2;
|
|
223
|
+
requireBufferRange(bytes, payloadOffset, payloadLength, 'JPEG', mountPrefixedPath);
|
|
224
|
+
if (JPEG_SOF_MARKERS.has(marker)) {
|
|
225
|
+
if (payloadLength < 6) {
|
|
226
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
227
|
+
}
|
|
228
|
+
const height = bytes.readUInt16BE(payloadOffset + 1);
|
|
229
|
+
const width = bytes.readUInt16BE(payloadOffset + 3);
|
|
230
|
+
const componentCount = bytes[payloadOffset + 5];
|
|
231
|
+
if (componentCount === 0 || payloadLength < 6 + (componentCount * 3)) {
|
|
232
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
233
|
+
}
|
|
234
|
+
if (width === 0 || height === 0) {
|
|
235
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
236
|
+
}
|
|
237
|
+
sawSof = true;
|
|
238
|
+
}
|
|
239
|
+
if (marker === JPEG_SOS) {
|
|
240
|
+
if (!sawSof) {
|
|
241
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
242
|
+
}
|
|
243
|
+
if (payloadLength < 6) {
|
|
244
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
245
|
+
}
|
|
246
|
+
const componentCount = bytes[payloadOffset];
|
|
247
|
+
if (componentCount === 0 || payloadLength < 1 + (componentCount * 2) + 3) {
|
|
248
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
249
|
+
}
|
|
250
|
+
sawSos = true;
|
|
251
|
+
offset = scanJpegEntropyData(bytes, payloadOffset + payloadLength, mountPrefixedPath);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
offset = payloadOffset + payloadLength;
|
|
255
|
+
}
|
|
256
|
+
invalidImage('JPEG', mountPrefixedPath);
|
|
257
|
+
}
|
|
258
|
+
function validateWebpVp8Chunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath) {
|
|
259
|
+
if (chunkLength <= 10)
|
|
260
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
261
|
+
if (bytes[chunkDataOffset + 3] !== 0x9d || bytes[chunkDataOffset + 4] !== 0x01 || bytes[chunkDataOffset + 5] !== 0x2a) {
|
|
262
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
263
|
+
}
|
|
264
|
+
const width = bytes.readUInt16LE(chunkDataOffset + 6) & 0x3fff;
|
|
265
|
+
const height = bytes.readUInt16LE(chunkDataOffset + 8) & 0x3fff;
|
|
266
|
+
if (width === 0 || height === 0) {
|
|
267
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function validateWebpVp8lChunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath) {
|
|
271
|
+
if (chunkLength <= 5 || bytes[chunkDataOffset] !== 0x2f) {
|
|
272
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
273
|
+
}
|
|
274
|
+
const packed = bytes.readUInt32LE(chunkDataOffset + 1);
|
|
275
|
+
const width = (packed & 0x3fff) + 1;
|
|
276
|
+
const height = ((packed >> 14) & 0x3fff) + 1;
|
|
277
|
+
if (width === 0 || height === 0) {
|
|
278
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function validateWebpVp8xChunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath) {
|
|
282
|
+
if (chunkLength !== 10)
|
|
283
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
284
|
+
const width = 1 + readUInt24LE(bytes, chunkDataOffset + 4);
|
|
285
|
+
const height = 1 + readUInt24LE(bytes, chunkDataOffset + 7);
|
|
286
|
+
if (width === 0 || height === 0) {
|
|
287
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function validateWebpAnmfChunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath) {
|
|
291
|
+
if (chunkLength <= 16)
|
|
292
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
293
|
+
const frameWidth = 1 + readUInt24LE(bytes, chunkDataOffset + 6);
|
|
294
|
+
const frameHeight = 1 + readUInt24LE(bytes, chunkDataOffset + 9);
|
|
295
|
+
if (frameWidth === 0 || frameHeight === 0) {
|
|
296
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
297
|
+
}
|
|
298
|
+
const chunkDataEnd = chunkDataOffset + chunkLength;
|
|
299
|
+
let nestedOffset = chunkDataOffset + 16;
|
|
300
|
+
let sawFramePayload = false;
|
|
301
|
+
while (nestedOffset < chunkDataEnd) {
|
|
302
|
+
requireBufferRange(bytes, nestedOffset, 8, 'WebP', mountPrefixedPath);
|
|
303
|
+
const nestedChunkType = bytes.toString('ascii', nestedOffset, nestedOffset + 4);
|
|
304
|
+
const nestedChunkLength = bytes.readUInt32LE(nestedOffset + 4);
|
|
305
|
+
const nestedChunkDataOffset = nestedOffset + 8;
|
|
306
|
+
const nestedChunkEnd = nestedChunkDataOffset + nestedChunkLength;
|
|
307
|
+
const nestedPaddedChunkEnd = nestedChunkEnd + (nestedChunkLength % 2);
|
|
308
|
+
requireBufferRange(bytes, nestedChunkDataOffset, nestedChunkLength, 'WebP', mountPrefixedPath);
|
|
309
|
+
if (nestedPaddedChunkEnd > chunkDataEnd) {
|
|
310
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
311
|
+
}
|
|
312
|
+
if (nestedChunkType === 'VP8 ') {
|
|
313
|
+
validateWebpVp8Chunk(bytes, nestedChunkDataOffset, nestedChunkLength, mountPrefixedPath);
|
|
314
|
+
sawFramePayload = true;
|
|
315
|
+
}
|
|
316
|
+
else if (nestedChunkType === 'VP8L') {
|
|
317
|
+
validateWebpVp8lChunk(bytes, nestedChunkDataOffset, nestedChunkLength, mountPrefixedPath);
|
|
318
|
+
sawFramePayload = true;
|
|
319
|
+
}
|
|
320
|
+
nestedOffset = nestedPaddedChunkEnd;
|
|
321
|
+
}
|
|
322
|
+
if (!sawFramePayload || nestedOffset !== chunkDataEnd) {
|
|
323
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function validateWebp(bytes, mountPrefixedPath) {
|
|
327
|
+
requireBufferRange(bytes, 0, 12, 'WebP', mountPrefixedPath);
|
|
328
|
+
const declaredLength = bytes.readUInt32LE(4);
|
|
329
|
+
if (declaredLength + 8 !== bytes.length) {
|
|
330
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
331
|
+
}
|
|
332
|
+
if (!bytes.subarray(8, 12).equals(WEBP_SIGNATURE)) {
|
|
333
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
334
|
+
}
|
|
335
|
+
let offset = 12;
|
|
336
|
+
let sawVp8x = false;
|
|
337
|
+
let sawImagePayload = false;
|
|
338
|
+
while (offset < bytes.length) {
|
|
339
|
+
requireBufferRange(bytes, offset, 8, 'WebP', mountPrefixedPath);
|
|
340
|
+
const chunkType = bytes.toString('ascii', offset, offset + 4);
|
|
341
|
+
const chunkLength = bytes.readUInt32LE(offset + 4);
|
|
342
|
+
const chunkDataOffset = offset + 8;
|
|
343
|
+
const chunkDataEnd = chunkDataOffset + chunkLength;
|
|
344
|
+
const paddedChunkEnd = chunkDataEnd + (chunkLength % 2);
|
|
345
|
+
requireBufferRange(bytes, chunkDataOffset, chunkLength, 'WebP', mountPrefixedPath);
|
|
346
|
+
if (paddedChunkEnd > bytes.length) {
|
|
347
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
348
|
+
}
|
|
349
|
+
if (chunkType === 'VP8 ') {
|
|
350
|
+
validateWebpVp8Chunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath);
|
|
351
|
+
sawImagePayload = true;
|
|
352
|
+
}
|
|
353
|
+
else if (chunkType === 'VP8L') {
|
|
354
|
+
validateWebpVp8lChunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath);
|
|
355
|
+
sawImagePayload = true;
|
|
356
|
+
}
|
|
357
|
+
else if (chunkType === 'VP8X') {
|
|
358
|
+
if (sawVp8x || offset !== 12) {
|
|
359
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
360
|
+
}
|
|
361
|
+
validateWebpVp8xChunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath);
|
|
362
|
+
sawVp8x = true;
|
|
363
|
+
}
|
|
364
|
+
else if (chunkType === 'ANMF') {
|
|
365
|
+
if (!sawVp8x) {
|
|
366
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
367
|
+
}
|
|
368
|
+
validateWebpAnmfChunk(bytes, chunkDataOffset, chunkLength, mountPrefixedPath);
|
|
369
|
+
sawImagePayload = true;
|
|
370
|
+
}
|
|
371
|
+
offset = paddedChunkEnd;
|
|
372
|
+
}
|
|
373
|
+
if (!sawImagePayload) {
|
|
374
|
+
invalidImage('WebP', mountPrefixedPath);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function detectImageMimeType(bytes, mountPrefixedPath) {
|
|
378
|
+
if (startsWithBytes(bytes, PNG_SIGNATURE)) {
|
|
379
|
+
validatePng(bytes, mountPrefixedPath);
|
|
380
|
+
return 'image/png';
|
|
381
|
+
}
|
|
382
|
+
if (matchesPartialPrefix(bytes, PNG_SIGNATURE)) {
|
|
383
|
+
throw new WorkspaceImageReadError('truncated', `Truncated image signature: ${mountPrefixedPath}`);
|
|
384
|
+
}
|
|
385
|
+
if (startsWithBytes(bytes, JPEG_SIGNATURE)) {
|
|
386
|
+
validateJpeg(bytes, mountPrefixedPath);
|
|
387
|
+
return 'image/jpeg';
|
|
388
|
+
}
|
|
389
|
+
if (matchesPartialPrefix(bytes, JPEG_SIGNATURE)) {
|
|
390
|
+
throw new WorkspaceImageReadError('truncated', `Truncated image signature: ${mountPrefixedPath}`);
|
|
391
|
+
}
|
|
392
|
+
if (startsWithBytes(bytes, GIF87A_SIGNATURE) || startsWithBytes(bytes, GIF89A_SIGNATURE)) {
|
|
393
|
+
validateGif(bytes, mountPrefixedPath);
|
|
394
|
+
return 'image/gif';
|
|
395
|
+
}
|
|
396
|
+
if (matchesPartialPrefix(bytes, GIF87A_SIGNATURE) || matchesPartialPrefix(bytes, GIF89A_SIGNATURE)) {
|
|
397
|
+
throw new WorkspaceImageReadError('truncated', `Truncated image signature: ${mountPrefixedPath}`);
|
|
398
|
+
}
|
|
399
|
+
if (bytes.length >= 12 && startsWithBytes(bytes, RIFF_SIGNATURE) && bytes.subarray(8, 12).equals(WEBP_SIGNATURE)) {
|
|
400
|
+
validateWebp(bytes, mountPrefixedPath);
|
|
401
|
+
return 'image/webp';
|
|
402
|
+
}
|
|
403
|
+
if ((bytes.length < RIFF_SIGNATURE.length && matchesPartialPrefix(bytes, RIFF_SIGNATURE))
|
|
404
|
+
|| (bytes.length >= RIFF_SIGNATURE.length && startsWithBytes(bytes, RIFF_SIGNATURE) && bytes.length < 12)) {
|
|
405
|
+
throw new WorkspaceImageReadError('truncated', `Truncated image signature: ${mountPrefixedPath}`);
|
|
406
|
+
}
|
|
407
|
+
throw new WorkspaceImageReadError('unsupported', `Unsupported image format: ${mountPrefixedPath}`);
|
|
408
|
+
}
|
|
12
409
|
export class WorkspaceModule {
|
|
13
410
|
name = 'workspace';
|
|
14
411
|
ctx = null;
|
|
@@ -207,6 +604,17 @@ export class WorkspaceModule {
|
|
|
207
604
|
required: ['path'],
|
|
208
605
|
},
|
|
209
606
|
},
|
|
607
|
+
{
|
|
608
|
+
name: 'read_image',
|
|
609
|
+
description: 'Read an image file from the workspace and return native image content.',
|
|
610
|
+
inputSchema: {
|
|
611
|
+
type: 'object',
|
|
612
|
+
properties: {
|
|
613
|
+
path: { type: 'string', description: 'Image file path (mount-prefixed, e.g., "project/assets/logo.png")' },
|
|
614
|
+
},
|
|
615
|
+
required: ['path'],
|
|
616
|
+
},
|
|
617
|
+
},
|
|
210
618
|
{
|
|
211
619
|
name: 'write',
|
|
212
620
|
description: 'Create or overwrite a file in the workspace.',
|
|
@@ -324,6 +732,7 @@ export class WorkspaceModule {
|
|
|
324
732
|
const input = call.input;
|
|
325
733
|
switch (call.name) {
|
|
326
734
|
case 'read': return await this.handleRead(input);
|
|
735
|
+
case 'read_image': return await this.handleReadImage(input);
|
|
327
736
|
case 'write': return await this.handleWrite(input);
|
|
328
737
|
case 'edit': return await this.handleEdit(input);
|
|
329
738
|
case 'delete': return await this.handleDelete(input);
|
|
@@ -425,6 +834,52 @@ export class WorkspaceModule {
|
|
|
425
834
|
return null;
|
|
426
835
|
}
|
|
427
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Write binary content (e.g. an image pulled from the agent's context) to a
|
|
839
|
+
* mount, through the same Chronicle-tree + auto-materialize path as the
|
|
840
|
+
* `write` tool. Public API for the framework's synthesized `save_image`
|
|
841
|
+
* tool and other peer callers that hold bytes rather than text.
|
|
842
|
+
*/
|
|
843
|
+
async writeBinary(mountPrefixedPath, data, mimeType) {
|
|
844
|
+
let mount;
|
|
845
|
+
let relativePath;
|
|
846
|
+
try {
|
|
847
|
+
({ mount, relativePath } = this.parsePath(mountPrefixedPath));
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
return {
|
|
851
|
+
success: false,
|
|
852
|
+
error: error instanceof Error ? error.message : String(error),
|
|
853
|
+
isError: true,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (mount.config.mode === 'read-only') {
|
|
857
|
+
return { success: false, error: `Mount "${mount.config.name}" is read-only`, isError: true };
|
|
858
|
+
}
|
|
859
|
+
const maxSize = mount.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
860
|
+
if (data.byteLength > maxSize) {
|
|
861
|
+
return { success: false, error: `Content exceeds max file size (${maxSize} bytes)`, isError: true };
|
|
862
|
+
}
|
|
863
|
+
const store = this.getStore();
|
|
864
|
+
const blobHash = store.storeBlob(data, mimeType);
|
|
865
|
+
store.treeSet(mount.treeStateId, relativePath, {
|
|
866
|
+
blobHash,
|
|
867
|
+
size: data.byteLength,
|
|
868
|
+
mode: 0o644,
|
|
869
|
+
});
|
|
870
|
+
const materializeError = await this.autoMaterialize(mount, relativePath, 'write', data);
|
|
871
|
+
if (materializeError) {
|
|
872
|
+
return {
|
|
873
|
+
success: false,
|
|
874
|
+
error: `Wrote to Chronicle but failed to materialize "${mountPrefixedPath}" to disk: ${materializeError}.`,
|
|
875
|
+
isError: true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
success: true,
|
|
880
|
+
data: { path: mountPrefixedPath, size: data.byteLength, mimeType },
|
|
881
|
+
};
|
|
882
|
+
}
|
|
428
883
|
/**
|
|
429
884
|
* Parse a mount-prefixed path into (mountName, relativePath).
|
|
430
885
|
*/
|
|
@@ -449,6 +904,138 @@ export class WorkspaceModule {
|
|
|
449
904
|
throw new Error('WorkspaceModule: store not initialized. Call initStore() first.');
|
|
450
905
|
return this.store;
|
|
451
906
|
}
|
|
907
|
+
validateImageBytes(bytes, mountPrefixedPath, maxSize) {
|
|
908
|
+
if (bytes.byteLength === 0) {
|
|
909
|
+
throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
|
|
910
|
+
}
|
|
911
|
+
if (bytes.byteLength > maxSize) {
|
|
912
|
+
throw new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
bytes,
|
|
916
|
+
mimeType: detectImageMimeType(bytes, mountPrefixedPath),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
tryReadImageFromTree(mount, relativePath, mountPrefixedPath, maxSize) {
|
|
920
|
+
const store = this.getStore();
|
|
921
|
+
const entry = store.treeGet(mount.treeStateId, relativePath);
|
|
922
|
+
if (!entry)
|
|
923
|
+
return null;
|
|
924
|
+
const blob = store.getBlob(entry.blobHash);
|
|
925
|
+
if (!blob) {
|
|
926
|
+
throw new WorkspaceImageReadError('blob_missing', `Blob not found for: ${mountPrefixedPath}`);
|
|
927
|
+
}
|
|
928
|
+
return this.validateImageBytes(blob, mountPrefixedPath, maxSize);
|
|
929
|
+
}
|
|
930
|
+
async readImageFromFilesystem(mount, relativePath, mountPrefixedPath, maxSize) {
|
|
931
|
+
const lexicalPath = resolve(mount.config.path, relativePath);
|
|
932
|
+
const useNoFollow = !mount.config.followSymlinks && typeof fsConstants.O_NOFOLLOW === 'number';
|
|
933
|
+
let fileInfo;
|
|
934
|
+
try {
|
|
935
|
+
fileInfo = await lstat(lexicalPath);
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
if (isErrnoCode(err, 'ENOENT')) {
|
|
939
|
+
throw new WorkspaceImageReadError('not_found', `File not found: ${mountPrefixedPath}`);
|
|
940
|
+
}
|
|
941
|
+
throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
|
|
942
|
+
}
|
|
943
|
+
if (fileInfo.isDirectory()) {
|
|
944
|
+
throw new WorkspaceImageReadError('directory', `Path is a directory: ${mountPrefixedPath}`);
|
|
945
|
+
}
|
|
946
|
+
if (fileInfo.isSymbolicLink() && !mount.config.followSymlinks) {
|
|
947
|
+
throw new WorkspaceImageReadError('symlink', `Symlinks are not allowed: ${mountPrefixedPath}`);
|
|
948
|
+
}
|
|
949
|
+
let realMountRoot;
|
|
950
|
+
try {
|
|
951
|
+
realMountRoot = await realpath(mount.config.path);
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
throw new WorkspaceImageReadError('mount_unavailable', `Mount unavailable: ${mount.config.name}`);
|
|
955
|
+
}
|
|
956
|
+
let handle = null;
|
|
957
|
+
try {
|
|
958
|
+
handle = await open(lexicalPath, fsConstants.O_RDONLY | (useNoFollow ? fsConstants.O_NOFOLLOW : 0));
|
|
959
|
+
}
|
|
960
|
+
catch (err) {
|
|
961
|
+
if (isErrnoCode(err, 'ENOENT')) {
|
|
962
|
+
throw new WorkspaceImageReadError('not_found', `File not found: ${mountPrefixedPath}`);
|
|
963
|
+
}
|
|
964
|
+
if (!mount.config.followSymlinks && isErrnoCode(err, 'ELOOP')) {
|
|
965
|
+
throw new WorkspaceImageReadError('symlink', `Symlinks are not allowed: ${mountPrefixedPath}`);
|
|
966
|
+
}
|
|
967
|
+
throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
const fileStat = await handle.stat();
|
|
971
|
+
if (fileStat.isDirectory()) {
|
|
972
|
+
throw new WorkspaceImageReadError('directory', `Path is a directory: ${mountPrefixedPath}`);
|
|
973
|
+
}
|
|
974
|
+
if (!fileStat.isFile()) {
|
|
975
|
+
throw new WorkspaceImageReadError('not_found', `File not found: ${mountPrefixedPath}`);
|
|
976
|
+
}
|
|
977
|
+
if (fileStat.size === 0) {
|
|
978
|
+
throw new WorkspaceImageReadError('empty', `Image file is empty: ${mountPrefixedPath}`);
|
|
979
|
+
}
|
|
980
|
+
if (fileStat.size > maxSize) {
|
|
981
|
+
throw new WorkspaceImageReadError('too_large', `Image file exceeds max size (${maxSize} bytes): ${mountPrefixedPath}`);
|
|
982
|
+
}
|
|
983
|
+
if (!mount.config.followSymlinks && !useNoFollow) {
|
|
984
|
+
let postOpenInfo;
|
|
985
|
+
try {
|
|
986
|
+
postOpenInfo = await lstat(lexicalPath);
|
|
987
|
+
}
|
|
988
|
+
catch (err) {
|
|
989
|
+
if (isErrnoCode(err, 'ENOENT')) {
|
|
990
|
+
throw new WorkspaceImageReadError('changed', `Image file changed during read: ${mountPrefixedPath}`);
|
|
991
|
+
}
|
|
992
|
+
throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
|
|
993
|
+
}
|
|
994
|
+
if (postOpenInfo.isSymbolicLink()) {
|
|
995
|
+
throw new WorkspaceImageReadError('symlink', `Symlinks are not allowed: ${mountPrefixedPath}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
let realFilePath;
|
|
999
|
+
try {
|
|
1000
|
+
realFilePath = await realpath(lexicalPath);
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
if (isErrnoCode(err, 'ENOENT')) {
|
|
1004
|
+
throw new WorkspaceImageReadError('changed', `Image file changed during read: ${mountPrefixedPath}`);
|
|
1005
|
+
}
|
|
1006
|
+
throw new WorkspaceImageReadError('not_found', `Unable to resolve image file: ${mountPrefixedPath}`);
|
|
1007
|
+
}
|
|
1008
|
+
if (!isContainedPath(realMountRoot, realFilePath)) {
|
|
1009
|
+
throw new WorkspaceImageReadError('escape', `Symlink escape detected: ${mountPrefixedPath}`);
|
|
1010
|
+
}
|
|
1011
|
+
let pathStat;
|
|
1012
|
+
try {
|
|
1013
|
+
pathStat = await stat(realFilePath);
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
if (isErrnoCode(err, 'ENOENT')) {
|
|
1017
|
+
throw new WorkspaceImageReadError('changed', `Image file changed during read: ${mountPrefixedPath}`);
|
|
1018
|
+
}
|
|
1019
|
+
throw new WorkspaceImageReadError('not_found', `Unable to stat image file: ${mountPrefixedPath}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (pathStat.dev !== fileStat.dev || pathStat.ino !== fileStat.ino) {
|
|
1022
|
+
throw new WorkspaceImageReadError('changed', `Image file changed during read: ${mountPrefixedPath}`);
|
|
1023
|
+
}
|
|
1024
|
+
const bytes = await handle.readFile();
|
|
1025
|
+
return this.validateImageBytes(bytes, mountPrefixedPath, maxSize);
|
|
1026
|
+
}
|
|
1027
|
+
catch (err) {
|
|
1028
|
+
if (err instanceof WorkspaceImageReadError)
|
|
1029
|
+
throw err;
|
|
1030
|
+
if (isErrnoCode(err, 'ENOENT')) {
|
|
1031
|
+
throw new WorkspaceImageReadError('not_found', `File not found: ${mountPrefixedPath}`);
|
|
1032
|
+
}
|
|
1033
|
+
throw new WorkspaceImageReadError('not_found', `Unable to read image file: ${mountPrefixedPath}`);
|
|
1034
|
+
}
|
|
1035
|
+
finally {
|
|
1036
|
+
await handle?.close();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
452
1039
|
/**
|
|
453
1040
|
* Persist a single write/edit/delete to disk when the mount opts in via
|
|
454
1041
|
* `autoMaterialize`. Required for cross-agent pipelines — another agent's
|
|
@@ -566,6 +1153,78 @@ export class WorkspaceModule {
|
|
|
566
1153
|
},
|
|
567
1154
|
};
|
|
568
1155
|
}
|
|
1156
|
+
async handleReadImage(input) {
|
|
1157
|
+
let mount;
|
|
1158
|
+
let relativePath;
|
|
1159
|
+
try {
|
|
1160
|
+
({ mount, relativePath } = this.parsePath(input.path));
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
return {
|
|
1164
|
+
success: false,
|
|
1165
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1166
|
+
isError: true,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
if (!relativePath) {
|
|
1170
|
+
return { success: false, error: `Path is a directory: ${input.path}`, isError: true };
|
|
1171
|
+
}
|
|
1172
|
+
const maxSize = mount.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
1173
|
+
let treeError = null;
|
|
1174
|
+
try {
|
|
1175
|
+
const image = this.tryReadImageFromTree(mount, relativePath, input.path, maxSize);
|
|
1176
|
+
if (image) {
|
|
1177
|
+
return {
|
|
1178
|
+
success: true,
|
|
1179
|
+
data: [
|
|
1180
|
+
{
|
|
1181
|
+
type: 'text',
|
|
1182
|
+
text: `Path: ${input.path}\nMIME: ${image.mimeType}\nBytes: ${image.bytes.byteLength}`,
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
type: 'image',
|
|
1186
|
+
data: image.bytes.toString('base64'),
|
|
1187
|
+
mimeType: image.mimeType,
|
|
1188
|
+
},
|
|
1189
|
+
],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
if (err instanceof WorkspaceImageReadError) {
|
|
1195
|
+
treeError = err;
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
throw err;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
const image = await this.readImageFromFilesystem(mount, relativePath, input.path, maxSize);
|
|
1203
|
+
return {
|
|
1204
|
+
success: true,
|
|
1205
|
+
data: [
|
|
1206
|
+
{
|
|
1207
|
+
type: 'text',
|
|
1208
|
+
text: `Path: ${input.path}\nMIME: ${image.mimeType}\nBytes: ${image.bytes.byteLength}`,
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
type: 'image',
|
|
1212
|
+
data: image.bytes.toString('base64'),
|
|
1213
|
+
mimeType: image.mimeType,
|
|
1214
|
+
},
|
|
1215
|
+
],
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
if (err instanceof WorkspaceImageReadError) {
|
|
1220
|
+
if (err.code === 'not_found' && treeError) {
|
|
1221
|
+
return { success: false, error: treeError.message, isError: true };
|
|
1222
|
+
}
|
|
1223
|
+
return { success: false, error: err.message, isError: true };
|
|
1224
|
+
}
|
|
1225
|
+
throw err;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
569
1228
|
async handleWrite(input) {
|
|
570
1229
|
const { mount, relativePath } = this.parsePath(input.path);
|
|
571
1230
|
if (mount.config.mode === 'read-only') {
|