@steerable/agent-shell 0.6.45 → 0.6.47

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.
@@ -18,7 +18,7 @@
18
18
  * should know an image was attached even when it was too large to send.
19
19
  */
20
20
  import { getNativeImage } from './runtime.js';
21
- import { statSync } from 'fs';
21
+ import { readFileSync, statSync } from 'fs';
22
22
  import { basename, extname } from 'path';
23
23
  /** Refuse to read source files larger than this (10 MB). */
24
24
  export const IMAGE_MAX_SOURCE_BYTES = 10 * 1024 * 1024;
@@ -82,13 +82,6 @@ export function processViewImage(input, nativeImage = getNativeImage()) {
82
82
  needsFollowup: true,
83
83
  };
84
84
  }
85
- if (!nativeImage) {
86
- return {
87
- success: false,
88
- error: '当前运行环境不支持图片解码',
89
- needsFollowup: true,
90
- };
91
- }
92
85
  let sourceBytes;
93
86
  try {
94
87
  sourceBytes = statSync(input.path).size;
@@ -111,6 +104,9 @@ export function processViewImage(input, nativeImage = getNativeImage()) {
111
104
  needsFollowup: true,
112
105
  };
113
106
  }
107
+ if (!nativeImage) {
108
+ return viewImageWithoutDecoder(input, sourceBytes, maxEdge);
109
+ }
114
110
  let rendered = nativeImage.createFromPath(input.path);
115
111
  if (rendered.isEmpty()) {
116
112
  return { success: false, error: '不是可识别的图片', needsFollowup: true };
@@ -159,6 +155,88 @@ export function processViewImage(input, nativeImage = getNativeImage()) {
159
155
  },
160
156
  };
161
157
  }
158
+ function viewImageWithoutDecoder(input, sourceBytes, maxEdge) {
159
+ if (input.region) {
160
+ return {
161
+ success: false,
162
+ error: '当前环境不能裁剪图片',
163
+ needsFollowup: true,
164
+ };
165
+ }
166
+ if (sourceBytes > IMAGE_MAX_ENCODED_BYTES) {
167
+ return {
168
+ success: false,
169
+ error: `源文件 ${formatMb(sourceBytes)}MB 超过 ${formatMb(IMAGE_MAX_ENCODED_BYTES)}MB,当前环境不能缩放`,
170
+ needsFollowup: true,
171
+ };
172
+ }
173
+ const sourceExt = extname(input.path).toLowerCase();
174
+ if (sourceExt !== '.png' && sourceExt !== '.jpg' && sourceExt !== '.jpeg') {
175
+ return {
176
+ success: false,
177
+ error: '当前环境只能直接发送 PNG 和 JPEG',
178
+ needsFollowup: true,
179
+ };
180
+ }
181
+ const natural = sourceExt === '.png' ? 'png' : 'jpeg';
182
+ if (input.format != null && input.format !== natural) {
183
+ return {
184
+ success: false,
185
+ error: '当前环境不能转码',
186
+ needsFollowup: true,
187
+ };
188
+ }
189
+ const bytes = readFileSync(input.path);
190
+ const size = readRasterSize(bytes);
191
+ if (size && (size.width > maxEdge || size.height > maxEdge)) {
192
+ return {
193
+ success: false,
194
+ error: '当前环境不能缩放',
195
+ needsFollowup: true,
196
+ };
197
+ }
198
+ const mediaType = natural === 'jpeg' ? 'image/jpeg' : 'image/png';
199
+ return {
200
+ success: true,
201
+ data: {
202
+ width: size?.width ?? 0,
203
+ height: size?.height ?? 0,
204
+ sourcePath: input.path,
205
+ mediaType,
206
+ _image: { b64: bytes.toString('base64'), media_type: mediaType },
207
+ },
208
+ };
209
+ }
210
+ function readRasterSize(bytes) {
211
+ if (bytes.length >= 24 &&
212
+ bytes[0] === 137 &&
213
+ bytes.toString('ascii', 1, 4) === 'PNG') {
214
+ return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
215
+ }
216
+ if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8)
217
+ return null;
218
+ let offset = 2;
219
+ while (offset + 9 < bytes.length) {
220
+ if (bytes[offset] !== 0xff)
221
+ return null;
222
+ const marker = bytes[offset + 1];
223
+ if (marker === 0xd8 || marker === 0xd9) {
224
+ offset += 2;
225
+ continue;
226
+ }
227
+ const length = bytes.readUInt16BE(offset + 2);
228
+ if (length < 2 || offset + 2 + length > bytes.length)
229
+ return null;
230
+ if (marker >= 0xc0 && marker <= 0xc3) {
231
+ return {
232
+ height: bytes.readUInt16BE(offset + 5),
233
+ width: bytes.readUInt16BE(offset + 7),
234
+ };
235
+ }
236
+ offset += 2 + length;
237
+ }
238
+ return null;
239
+ }
162
240
  function resolveCrop(region, size) {
163
241
  const values = [region.x, region.y, region.w, region.h];
164
242
  if (!values.every(Number.isFinite))
@@ -208,8 +286,25 @@ export function processImageAttachments(files) {
208
286
  }
209
287
  const nativeImage = getNativeImage();
210
288
  if (!nativeImage) {
211
- // Non-Electron host (BS server, unit tests, headless) — can't decode images.
212
- notes.push(`- ${label}:当前运行环境不支持图片解码,未附加`);
289
+ // Tauri and the browser server run plain Node, which has no Electron
290
+ // nativeImage. The bytes are already a PNG/JPEG the provider can read;
291
+ // pass them through when they fit the encoded cap. Resize stays on the
292
+ // Electron path.
293
+ if (sourceBytes > IMAGE_MAX_ENCODED_BYTES) {
294
+ notes.push(`- ${label}:源文件 ${formatMb(sourceBytes)}MB 超过 ${formatMb(IMAGE_MAX_ENCODED_BYTES)}MB,当前环境不能缩放,未附加`);
295
+ continue;
296
+ }
297
+ const ext = extname(file.path).toLowerCase();
298
+ const mediaType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
299
+ : ext === '.gif' ? 'image/gif'
300
+ : ext === '.webp' ? 'image/webp'
301
+ : ext === '.bmp' ? 'image/bmp'
302
+ : 'image/png';
303
+ images.push({
304
+ data: readFileSync(file.path).toString('base64'),
305
+ mediaType,
306
+ });
307
+ notes.push(`- ${label}(原图)`);
213
308
  continue;
214
309
  }
215
310
  const image = nativeImage.createFromPath(file.path);
@@ -508,11 +508,19 @@ export class SidecarSupervisor extends EventEmitter {
508
508
  (allowResolver ? ' + resolver only (web tools egress via the per-host proxy)' : '') +
509
509
  ')');
510
510
  this.sandboxPosture = { backend: 'seatbelt', enforcement: 'partial', reason: 'active' };
511
+ // macOS TMPDIR is /var/folders, which the Seatbelt profile does not
512
+ // allow. run_code / run_js materialize their workers there and fail
513
+ // with EPERM, so the model cannot list or read local folders.
514
+ const confinedTmp = join(writableRoots[0], 'tmp');
515
+ mkdirSync(confinedTmp, { recursive: true });
511
516
  return {
512
517
  command: SEATBELT_EXECUTABLE,
513
518
  args: ['-p', profile, command, ...args],
514
519
  env: {
515
520
  PYTHONDONTWRITEBYTECODE: '1',
521
+ TMPDIR: confinedTmp,
522
+ TMP: confinedTmp,
523
+ TEMP: confinedTmp,
516
524
  // macOS denies a nested sandbox_apply once the outer profile allows
517
525
  // outbound network, so a layer-1-confined sidecar cannot wrap its own
518
526
  // run_code child. The marker tells the sidecar to let that child
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell",
3
- "version": "0.6.45",
3
+ "version": "0.6.47",
4
4
  "description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://steerableframework.com/",
@@ -55,9 +55,9 @@
55
55
  "he": "^1.2.0",
56
56
  "node-pty": "^1.1.0",
57
57
  "semver": "^7.8.0",
58
- "@steerable/agent-protocol": "0.6.45",
59
- "@steerable/pack-sdk": "0.6.45",
60
- "@steerable/agent-harness": "0.6.45"
58
+ "@steerable/pack-sdk": "0.6.47",
59
+ "@steerable/agent-harness": "0.6.47",
60
+ "@steerable/agent-protocol": "0.6.47"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/better-sqlite3": "^7.6.13",