maka-agent 0.2.1-dev.84.20261014 → 0.2.1-dev.85.20261014

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.
Files changed (36) hide show
  1. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  2. package/node_modules/@maka/core/dist/image-delivery.js +83 -0
  3. package/node_modules/@maka/core/dist/image-markdown.js +44 -0
  4. package/node_modules/@maka/core/package.json +4 -1
  5. package/node_modules/@maka/runtime/dist/filesystem-executor.js +2 -0
  6. package/node_modules/@maka/runtime/dist/filesystem-worker/operations.js +7 -3
  7. package/node_modules/@maka/runtime/dist/filesystem-worker/protocol.js +6 -4
  8. package/node_modules/@maka/runtime/dist/image-file-reader.js +83 -0
  9. package/node_modules/@maka/runtime/dist/image-file.js +43 -10
  10. package/node_modules/@maka/runtime/dist/network/public-network-policy.js +97 -0
  11. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +57 -4
  12. package/node_modules/@maka/runtime/dist/system-prompt/main-session-prompt.js +3 -0
  13. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.mjs +69 -12
  14. package/node_modules/@maka/runtime/dist/workspace-executor.js +3 -3
  15. package/node_modules/@maka/runtime/package.json +2 -0
  16. package/node_modules/@maka/runtime-host/dist/protocol/artifact.js +2 -0
  17. package/node_modules/@maka/runtime-host/dist/protocol/image-delivery.js +70 -0
  18. package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
  19. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  20. package/node_modules/@maka/runtime-host/dist/server/artifact-coordinator.js +10 -1
  21. package/node_modules/@maka/runtime-host/dist/server/chat-image-artifact.js +41 -0
  22. package/node_modules/@maka/runtime-host/dist/server/chat-image-delivery.js +232 -0
  23. package/node_modules/@maka/runtime-host/dist/server/chat-image-markdown.js +26 -0
  24. package/node_modules/@maka/runtime-host/dist/server/chat-image-source.js +121 -0
  25. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +67 -2
  26. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +11 -1
  27. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +3 -0
  28. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +40 -0
  29. package/node_modules/@maka/storage/dist/artifact-image-storage.js +62 -0
  30. package/node_modules/@maka/storage/dist/artifact-metadata-codec.js +3 -0
  31. package/node_modules/@maka/storage/dist/artifact-store.js +67 -5
  32. package/node_modules/@maka/storage/dist/artifact-stores.js +14 -0
  33. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +44 -0
  34. package/node_modules/@maka/storage/dist/sqlite-artifact-schema.js +8 -1
  35. package/node_modules/@maka/storage/dist/sqlite-image-delivery.js +63 -0
  36. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /** Client-safe delivery contract. No filesystem or networking capabilities live here. */
20
+ export const IMAGE_DELIVERY_IDENTITY_MAX_LENGTH = 512;
21
+ export const IMAGE_DELIVERY_SOURCE_MAX_LENGTH = 4096;
22
+ /** JavaScript string length, shared by stream capture and Markdown consumers. */
23
+ export const IMAGE_MARKDOWN_MAX_LENGTH = 1024 * 1024;
24
+ export function isImageDeliverySource(value) {
25
+ return (typeof value === 'string' &&
26
+ value.length > 0 &&
27
+ value.length <= IMAGE_DELIVERY_SOURCE_MAX_LENGTH &&
28
+ !/[\x00-\x1f\x7f]/.test(value));
29
+ }
30
+ export function isRemoteImageSource(source) {
31
+ return /^https?:/i.test(source);
32
+ }
33
+ export function isImageDeliveryRequest(value) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ return false;
36
+ const v = value;
37
+ const identity = (s) => typeof s === 'string' &&
38
+ s.length > 0 &&
39
+ s.length <= IMAGE_DELIVERY_IDENTITY_MAX_LENGTH &&
40
+ !/[\x00-\x1f\x7f]/.test(s);
41
+ return (Object.keys(v).every((k) => ['turnId', 'messageId', 'source', 'retry', 'loadRemote'].includes(k)) &&
42
+ identity(v.turnId) &&
43
+ identity(v.messageId) &&
44
+ isImageDeliverySource(v.source) &&
45
+ (v.retry === undefined || typeof v.retry === 'boolean') &&
46
+ (v.loadRemote === undefined || typeof v.loadRemote === 'boolean'));
47
+ }
48
+ export const IMAGE_DELIVERY_FAILURES = [
49
+ 'not_found',
50
+ 'not_allowed',
51
+ 'too_large',
52
+ 'unsupported_mime',
53
+ 'quota_exceeded',
54
+ 'download_failed',
55
+ 'read_failed',
56
+ 'queue_full',
57
+ ];
58
+ export function isImageDeliveryAttempt(value) {
59
+ if (!value || typeof value !== 'object' || Array.isArray(value))
60
+ return false;
61
+ const v = value;
62
+ return (Object.keys(v).every((k) => ['status', 'reason'].includes(k)) &&
63
+ (v.status === 'pending'
64
+ ? v.reason === undefined
65
+ : v.status === 'failed' && IMAGE_DELIVERY_FAILURES.includes(v.reason)));
66
+ }
67
+ export const DEFAULT_IMAGE_ARCHIVE_LIMITS = Object.freeze({
68
+ sessionBytes: 100 * 1024 * 1024,
69
+ workspaceBytes: 1024 * 1024 * 1024,
70
+ });
71
+ export function isImageDeliveryMetadata(value) {
72
+ if (!value || typeof value !== 'object' || Array.isArray(value))
73
+ return false;
74
+ const v = value;
75
+ return (Object.keys(v).every((k) => ['messageId', 'source', 'status', 'contentSha256'].includes(k)) &&
76
+ typeof v.messageId === 'string' &&
77
+ v.messageId.length > 0 &&
78
+ v.messageId.length <= IMAGE_DELIVERY_IDENTITY_MAX_LENGTH &&
79
+ isImageDeliverySource(v.source) &&
80
+ v.status === 'ready' &&
81
+ typeof v.contentSha256 === 'string' &&
82
+ /^[a-f0-9]{64}$/.test(v.contentSha256));
83
+ }
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { Lexer, Marked } from 'marked';
20
+ import { IMAGE_MARKDOWN_MAX_LENGTH } from './image-delivery.js';
21
+ const parser = new Marked({ gfm: true });
22
+ /** Canonical image destinations in real Markdown nodes, including attachment refs.
23
+ * Consumers own their source policy; parsing grants no filesystem or network access.
24
+ */
25
+ export function markdownImageSources(text) {
26
+ return [...new Set(markdownImages(text).map((image) => image.source))];
27
+ }
28
+ /** Original token spelling and canonical destination, including reference images. */
29
+ export function markdownImages(text) {
30
+ if (!text.includes('![') || text.length > IMAGE_MARKDOWN_MAX_LENGTH)
31
+ return [];
32
+ const images = [];
33
+ parser.walkTokens(parser.lexer(text), (token) => {
34
+ if (token.type === 'image')
35
+ images.push({ raw: token.raw, source: token.href });
36
+ });
37
+ return images;
38
+ }
39
+ /** Resolve a complete inline destination, rejecting trailing or partial syntax. */
40
+ export function parseMarkdownImageDestination(source) {
41
+ const markdown = `![](${source})`;
42
+ const [token] = Lexer.lexInline(markdown, { gfm: true });
43
+ return token?.type === 'image' && token.raw === markdown ? token.href : undefined;
44
+ }
@@ -63,6 +63,8 @@
63
63
  "./context-offload": "./dist/context-offload.js",
64
64
  "./tool-result-archive-evidence": "./dist/tool-result-archive-evidence.js",
65
65
  "./attachments": "./dist/attachments.js",
66
+ "./image-delivery": "./dist/image-delivery.js",
67
+ "./image-markdown": "./dist/image-markdown.js",
66
68
  "./artifacts": "./dist/artifacts.js",
67
69
  "./pet": "./dist/pet.js",
68
70
  "./skill-invocation": "./dist/skill-invocation.js",
@@ -169,6 +171,7 @@
169
171
  "./workhub-routing": "./dist/workhub-routing.js"
170
172
  },
171
173
  "dependencies": {
172
- "intl-messageformat": "11.2.15"
174
+ "intl-messageformat": "11.2.15",
175
+ "marked": "18.0.11"
173
176
  }
174
177
  }
@@ -270,6 +270,8 @@ function createWorkspaceFilesystemExecutor(workspace) {
270
270
  const result = await workspace.readFile({
271
271
  cwd,
272
272
  path,
273
+ ...(operation.imagePurpose ? { imagePurpose: operation.imagePurpose } : {}),
274
+ ...(abortSignal ? { abortSignal } : {}),
273
275
  });
274
276
  if ('bytes' in result) {
275
277
  return { kind: 'read_image', bytes: result.bytes, mimeType: result.mimeType };
@@ -30,7 +30,7 @@ import { readPage } from '../read-page.js';
30
30
  import { formatJsonText } from '../format-json.js';
31
31
  import { createEditUnifiedDiff, createUnifiedDiff } from '../unified-diff.js';
32
32
  import { compareAndDeleteEntry, hostVisibilityAfterWrite, openStableTarget, readModifyWriteThroughHandle, StableWriteFailure, writeThroughHandle, } from '../file-stable-write.js';
33
- import { readWorkspaceFile } from '../image-file.js';
33
+ import { ImageFileError, readWorkspaceFile } from '../image-file.js';
34
34
  import { FILESYSTEM_WORKER_PROTOCOL_VERSION, operationAccess, operationUsesDirectoryEntry, } from './protocol.js';
35
35
  import { isLikelySandboxDenial } from '../sandbox/detect.js';
36
36
  // Canonicalisation must match the sandbox the worker runs in: realpath-based
@@ -61,8 +61,10 @@ export async function executeFilesystemOperation(operation, operationBoundary, d
61
61
  switch (operation.kind) {
62
62
  case 'read': {
63
63
  const path = await resolveExistingAllowed(operation.cwd, operation.path, 'Read', 'read', operationBoundary);
64
- const file = await readWorkspaceFile(path).catch((error) => {
65
- throw operationError('filesystem_error', error instanceof Error ? error.message : 'File could not be read.');
64
+ const file = await readWorkspaceFile(path, {
65
+ ...(operation.imagePurpose ? { imagePurpose: operation.imagePurpose } : {}),
66
+ }).catch((error) => {
67
+ throw operationError(normalizeOperationError(error).code, error instanceof Error ? error.message : 'File could not be read.');
66
68
  });
67
69
  if ('bytes' in file) {
68
70
  return {
@@ -318,6 +320,8 @@ function operationError(code, message) {
318
320
  function normalizeOperationError(error) {
319
321
  if (error instanceof FilesystemOperationError)
320
322
  return error;
323
+ if (error instanceof ImageFileError)
324
+ return operationError(error.code === 'ERR_IMAGE_TOO_LARGE' ? 'image_too_large' : 'invalid_image', error.message);
321
325
  if (error instanceof StableWriteFailure) {
322
326
  return operationError(error.code, error.message);
323
327
  }
@@ -20,10 +20,9 @@ import { z } from 'zod';
20
20
  import { readContinuationSchema, readPageSchema } from '../read-page.js';
21
21
  import { validateSandboxBoundaryExpansion } from '@maka/core/sandbox-boundary';
22
22
  import { GREP_MAX_LINES, GREP_MAX_LINES_PER_FILE, GREP_MAX_MATCH_BYTES } from '../grep-search.js';
23
- // v10 adds bounded Read pages to v9's exact Grep counts; v11 adds Glob's
24
- // truncation flag to v10's result contract. Older workers cannot satisfy the
25
- // combined result contract and must be rejected at the handshake.
26
- export const FILESYSTEM_WORKER_PROTOCOL_VERSION = 11;
23
+ // v12 adds chat image reads with a bounded payload and no model dimension limit.
24
+ // Older workers cannot satisfy this read policy and must be rejected at the handshake.
25
+ export const FILESYSTEM_WORKER_PROTOCOL_VERSION = 12;
27
26
  /** The single authority on which operation kinds are writes. Shared by the
28
27
  * client (permission/identity decisions) and the worker (operation guards) so
29
28
  * the set cannot drift. */
@@ -98,6 +97,7 @@ export const FilesystemWorkerOperationSchema = z.union([
98
97
  kind: z.literal('read'),
99
98
  cwd,
100
99
  path,
100
+ imagePurpose: z.literal('chat').optional(),
101
101
  offset: z.number().int().nonnegative().optional(),
102
102
  limit: z.number().int().positive().optional(),
103
103
  continuation: readContinuationSchema.optional(),
@@ -236,6 +236,8 @@ export const FilesystemWorkerErrorCodeSchema = z.enum([
236
236
  'grep_unavailable',
237
237
  'sandbox_denied',
238
238
  'filesystem_denied',
239
+ 'image_too_large',
240
+ 'invalid_image',
239
241
  'filesystem_error',
240
242
  // The worker may have applied the mutation before it lost the ability to
241
243
  // report back (e.g. it wrote the file then the post-write identity check
@@ -0,0 +1,83 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { createBoundaryFilesystemExecutor, } from './filesystem-executor.js';
20
+ import { createLocalWorkspaceExecutor } from './workspace-executor.js';
21
+ import { ImageFileError, imageFileFailureReason } from './image-file.js';
22
+ import { FilesystemWorkerClientError } from './filesystem-worker/client.js';
23
+ import { SandboxCommandError } from './sandbox/errors.js';
24
+ /** Normalizes filesystem backends at the image reader boundary. */
25
+ export class ImageFileReadError extends Error {
26
+ reason;
27
+ constructor(reason, cause) {
28
+ super(cause instanceof Error ? cause.message : `Image read failed: ${reason}`, { cause });
29
+ this.reason = reason;
30
+ this.name = 'ImageFileReadError';
31
+ }
32
+ }
33
+ function readFailure(error) {
34
+ if (error instanceof ImageFileError)
35
+ return imageFileFailureReason(error);
36
+ if (error instanceof SandboxCommandError)
37
+ return 'not_allowed';
38
+ const code = error instanceof FilesystemWorkerClientError
39
+ ? error.reason
40
+ : error?.code;
41
+ switch (code) {
42
+ case 'image_too_large':
43
+ return 'too_large';
44
+ case 'invalid_image':
45
+ return 'unsupported_mime';
46
+ case 'ENOENT':
47
+ case 'ENOTDIR':
48
+ case 'not_found':
49
+ return 'not_found';
50
+ case 'EACCES':
51
+ case 'EPERM':
52
+ case 'filesystem_denied':
53
+ case 'path_denied':
54
+ case 'sandbox_denied':
55
+ case 'sandbox_required':
56
+ case 'sandbox_boundary_required':
57
+ return 'not_allowed';
58
+ default:
59
+ return 'read_failed';
60
+ }
61
+ }
62
+ /** The same filesystem boundary as Read, without model snapshots or tool execution. */
63
+ export function createImageFileReader(options = {}) {
64
+ const filesystem = createBoundaryFilesystemExecutor({
65
+ workspace: options.executor ?? createLocalWorkspaceExecutor(),
66
+ ...(options.filesystemWorker ? { worker: options.filesystemWorker } : {}),
67
+ ...(options.permissionProfile ? { permissionProfile: options.permissionProfile } : {}),
68
+ });
69
+ return async (input) => {
70
+ const { path, ...context } = input;
71
+ const result = await filesystem
72
+ .execute({
73
+ ...context,
74
+ operation: { kind: 'read', path, imagePurpose: 'chat' },
75
+ })
76
+ .catch((error) => {
77
+ throw new ImageFileReadError(readFailure(error), error);
78
+ });
79
+ if (result.kind !== 'read_image')
80
+ throw new ImageFileReadError('unsupported_mime');
81
+ return { bytes: result.bytes, mimeType: result.mimeType };
82
+ };
83
+ }
@@ -16,18 +16,42 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ import { constants } from 'node:fs';
19
20
  import { open } from 'node:fs/promises';
20
21
  import { extname } from 'node:path';
21
22
  import { imageDimensionsFromData } from 'image-dimensions';
22
23
  import { ATTACHMENT_MIME_SNIFF_BYTES, MAX_MODEL_IMAGE_EDGE, MAX_READ_IMAGE_BYTES, READ_IMAGE_TOO_LARGE_MESSAGE, sniffAttachmentMimeType, } from '@maka/core/attachments';
24
+ import { ARTIFACT_IMAGE_PREVIEW_MAX_BYTES } from '@maka/core/artifacts';
23
25
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
26
+ export class ImageFileError extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.code = code;
31
+ }
32
+ }
33
+ /** Translate validated image failures without changing backend/transport errors. */
34
+ export function imageFileFailureReason(error) {
35
+ return error.code === 'ERR_IMAGE_TOO_LARGE' ? 'too_large' : 'unsupported_mime';
36
+ }
24
37
  export function isSupportedImagePath(path) {
25
38
  return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
26
39
  }
27
40
  /** Classify a bounded prefix before decoding text or allocating an image body. */
28
- export async function readWorkspaceFile(path) {
29
- const file = await open(path, 'r');
41
+ export async function readWorkspaceFile(path, options = {}) {
42
+ options.abortSignal?.throwIfAborted();
43
+ // Chat capture must never block opening a FIFO or read an unbounded text file.
44
+ const file = await open(path, options.imagePurpose === 'chat' ? constants.O_RDONLY | constants.O_NONBLOCK : 'r');
30
45
  try {
46
+ options.abortSignal?.throwIfAborted();
47
+ if (options.imagePurpose === 'chat') {
48
+ const size = await file.stat();
49
+ if (!size.isFile())
50
+ throw new Error('Image path is not a file.');
51
+ if (size.size > ARTIFACT_IMAGE_PREVIEW_MAX_BYTES)
52
+ throw imageTooLargeError('chat');
53
+ return validateImageBytes(await file.readFile({ signal: options.abortSignal }), 'chat');
54
+ }
31
55
  const prefix = Buffer.alloc(ATTACHMENT_MIME_SNIFF_BYTES);
32
56
  // A positioned read leaves the descriptor's offset at zero for readFile.
33
57
  const { bytesRead } = await file.read(prefix, 0, prefix.length, 0);
@@ -45,12 +69,12 @@ export async function readWorkspaceFile(path) {
45
69
  await file.close();
46
70
  }
47
71
  }
48
- export function validateImageBytes(bytes) {
49
- if (bytes.length > MAX_READ_IMAGE_BYTES)
50
- throw imageTooLargeError();
72
+ export function validateImageBytes(bytes, purpose = 'model') {
73
+ if (bytes.length > (purpose === 'chat' ? ARTIFACT_IMAGE_PREVIEW_MAX_BYTES : MAX_READ_IMAGE_BYTES))
74
+ throw imageTooLargeError(purpose);
51
75
  const mimeType = sniffImageMime(bytes);
52
76
  if (!mimeType)
53
- throw new Error('Image content is not a supported PNG, JPEG, GIF, or WebP file.');
77
+ throw new ImageFileError('ERR_INVALID_IMAGE', 'Image content is not a supported PNG, JPEG, GIF, or WebP file.');
54
78
  const dimensions = imageDimensionsFromData(bytes);
55
79
  if (!dimensions ||
56
80
  !Number.isFinite(dimensions.width) ||
@@ -59,15 +83,24 @@ export function validateImageBytes(bytes) {
59
83
  !Number.isInteger(dimensions.height) ||
60
84
  dimensions.width <= 0 ||
61
85
  dimensions.height <= 0) {
62
- throw new Error('Image dimensions could not be read; verify the image file is valid.');
86
+ throw new ImageFileError('ERR_INVALID_IMAGE', 'Image dimensions could not be read; verify the image file is valid.');
63
87
  }
64
- if (Math.max(dimensions.width, dimensions.height) > MAX_MODEL_IMAGE_EDGE) {
88
+ if (purpose === 'model' && Math.max(dimensions.width, dimensions.height) > MAX_MODEL_IMAGE_EDGE) {
65
89
  throw new Error(`Image dimensions ${dimensions.width}x${dimensions.height} exceed the ${MAX_MODEL_IMAGE_EDGE}px model input limit; downscale it and try again.`);
66
90
  }
91
+ // Compressed byte size does not bound renderer decoding memory. Permit large
92
+ // screenshots while rejecting pathological headers before archival/preview.
93
+ if (purpose === 'chat' &&
94
+ (Math.max(dimensions.width, dimensions.height) > 16_384 ||
95
+ dimensions.width * dimensions.height > 32 * 1024 * 1024)) {
96
+ throw imageTooLargeError(purpose);
97
+ }
67
98
  return { bytes, mimeType };
68
99
  }
69
- function imageTooLargeError() {
70
- return new Error(READ_IMAGE_TOO_LARGE_MESSAGE);
100
+ function imageTooLargeError(purpose = 'model') {
101
+ return new ImageFileError('ERR_IMAGE_TOO_LARGE', purpose === 'chat'
102
+ ? 'Image exceeds a chat preview limit (2 MiB, 16384px per edge, or 32 megapixels); resize it before publishing.'
103
+ : READ_IMAGE_TOO_LARGE_MESSAGE);
71
104
  }
72
105
  function sniffImageMime(bytes) {
73
106
  // Core owns the byte signatures (shared with the attachment and artifact
@@ -0,0 +1,97 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { lookup } from 'node:dns/promises';
20
+ import { isIP } from 'node:net';
21
+ export class PublicNetworkPolicyError extends Error {
22
+ name = 'PublicNetworkPolicyError';
23
+ constructor() {
24
+ super('Network target is not allowed by the public network policy');
25
+ }
26
+ }
27
+ /** Validate every URL, then resolve and pin direct destinations. Configured
28
+ * proxies resolve named destinations themselves and are trusted to enforce
29
+ * their egress boundary. Local DNS cannot establish a proxy's final address. */
30
+ export async function preparePublicNetworkTarget(url, useProxy, signal) {
31
+ signal.throwIfAborted();
32
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
33
+ throw new PublicNetworkPolicyError();
34
+ const host = url.hostname.replace(/^\[|\]$/g, '');
35
+ const family = isIP(host);
36
+ if (family) {
37
+ if (!publicAddress(host))
38
+ throw new PublicNetworkPolicyError();
39
+ return useProxy ? undefined : { address: host, family };
40
+ }
41
+ if (!publicHostname(host))
42
+ throw new PublicNetworkPolicyError();
43
+ if (useProxy)
44
+ return undefined;
45
+ const addresses = await lookupWithSignal(host, signal);
46
+ signal.throwIfAborted();
47
+ if (!addresses.length || addresses.some(({ address }) => !publicAddress(address)))
48
+ throw new PublicNetworkPolicyError();
49
+ return addresses[0];
50
+ }
51
+ function lookupWithSignal(host, signal) {
52
+ return new Promise((resolve, reject) => {
53
+ const abort = () => {
54
+ signal.removeEventListener('abort', abort);
55
+ reject(signal.reason);
56
+ };
57
+ signal.addEventListener('abort', abort, { once: true });
58
+ if (signal.aborted)
59
+ abort();
60
+ else
61
+ void lookup(host, { all: true })
62
+ .then(resolve, reject)
63
+ .finally(() => {
64
+ signal.removeEventListener('abort', abort);
65
+ });
66
+ });
67
+ }
68
+ function publicHostname(host) {
69
+ const name = host.toLowerCase().replace(/\.$/, '');
70
+ return (name.includes('.') &&
71
+ !/(^|\.)(localhost|local|lan|internal|home|arpa|test|invalid)$/.test(name) &&
72
+ ![
73
+ 'metadata.google.internal',
74
+ 'metadata.goog',
75
+ 'metadata.tencentyun.com',
76
+ 'instance-data.ec2.internal',
77
+ ].includes(name));
78
+ }
79
+ function publicAddress(address) {
80
+ if (isIP(address) === 4) {
81
+ const [a = 0, b = 0] = address.split('.').map(Number);
82
+ return (a > 0 &&
83
+ a !== 10 &&
84
+ a !== 127 &&
85
+ a < 224 &&
86
+ !(a === 169 && b === 254) &&
87
+ !(a === 172 && b >= 16 && b <= 31) &&
88
+ !(a === 192 && b === 168) &&
89
+ !(a === 100 && b >= 64 && b <= 127) &&
90
+ !(a === 198 && (b === 18 || b === 19)));
91
+ }
92
+ if (isIP(address) !== 6)
93
+ return false;
94
+ // Only global-unicast IPv6 is eligible, excluding the benchmarking prefix.
95
+ const normalized = new URL(`http://[${address}]`).hostname;
96
+ return /^[23][0-9a-f]{3}:/i.test(address) && !/^\[2001:2:(?::|0:)/i.test(normalized);
97
+ }
@@ -20,6 +20,8 @@ import { Agent, fetch as undiciFetch } from 'undici';
20
20
  import { matchesBypassList } from './bypass-matcher.js';
21
21
  import { buildProxyDispatcher } from './proxy-dispatcher.js';
22
22
  import { buildAbortableConnector } from './abortable-connector.js';
23
+ import { preparePublicNetworkTarget } from './public-network-policy.js';
24
+ export { PublicNetworkPolicyError } from './public-network-policy.js';
23
25
  export const FETCH_PROXY_SNAPSHOT = Symbol.for('maka.fetch.proxy-snapshot');
24
26
  const TRANSPORT_CLOSE_GRACE_MS = 1_000;
25
27
  export function inheritFetchProxySnapshot(fetch, source) {
@@ -40,6 +42,7 @@ export function createProxiedFetchTransport(proxy) {
40
42
  // direct and proxy connection establishment too, including TLS handshakes.
41
43
  const connections = new AbortController();
42
44
  const directDispatcher = new Agent({ connect: buildAbortableConnector(connections.signal) });
45
+ const publicDispatchers = new Set();
43
46
  let proxyDispatcher;
44
47
  let closePromise;
45
48
  let closed = false;
@@ -48,12 +51,61 @@ export function createProxiedFetchTransport(proxy) {
48
51
  throw new Error('Proxied fetch transport is closed');
49
52
  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
50
53
  const useProxy = proxySnapshot !== null && !matchesBypassList(new URL(url).hostname, proxySnapshot.bypassList);
54
+ const { targetPolicy, ...requestInit } = init ?? {};
55
+ let publicDispatcher;
56
+ if (targetPolicy !== undefined) {
57
+ if (targetPolicy !== 'public')
58
+ throw new Error('Unknown network target policy');
59
+ const inputRequest = typeof input === 'string' || input instanceof URL ? undefined : input;
60
+ const redirect = requestInit.redirect ?? inputRequest?.redirect ?? 'follow';
61
+ if (redirect !== 'manual' && redirect !== 'error')
62
+ throw new Error('Public network requests require manual redirects or redirect: error');
63
+ const requestSignal = requestInit.signal === undefined ? inputRequest?.signal : requestInit.signal;
64
+ const signal = requestSignal
65
+ ? AbortSignal.any([connections.signal, requestSignal])
66
+ : connections.signal;
67
+ const target = await preparePublicNetworkTarget(new URL(url), useProxy, signal);
68
+ signal.throwIfAborted();
69
+ requestInit.signal = signal;
70
+ if (target) {
71
+ // A separate dispatcher prevents reuse of an unchecked connection and
72
+ // binds this request to exactly the DNS answer admitted above.
73
+ publicDispatcher = new Agent({
74
+ connect: buildAbortableConnector(signal, {
75
+ lookup: (_host, options, callback) => options.all
76
+ ? callback(null, [target])
77
+ : callback(null, target.address, target.family),
78
+ }),
79
+ });
80
+ publicDispatchers.add(publicDispatcher);
81
+ }
82
+ }
51
83
  if (useProxy)
52
84
  proxyDispatcher ??= buildProxyDispatcher(proxySnapshot, connections.signal);
53
- return (await undiciFetch(input, {
54
- ...init,
55
- dispatcher: useProxy ? proxyDispatcher : directDispatcher,
56
- }));
85
+ try {
86
+ const response = (await undiciFetch(
87
+ // Keep a mutable URL object bound to the destination checked before DNS awaited.
88
+ (targetPolicy === 'public' && input instanceof URL ? url : input), {
89
+ ...requestInit,
90
+ dispatcher: publicDispatcher ?? (useProxy ? proxyDispatcher : directDispatcher),
91
+ }));
92
+ if (publicDispatcher) {
93
+ const dispatcher = publicDispatcher;
94
+ // Graceful close waits for the body, without delaying delivery of headers.
95
+ void dispatcher
96
+ .close()
97
+ .catch(() => { })
98
+ .finally(() => publicDispatchers.delete(dispatcher));
99
+ }
100
+ return response;
101
+ }
102
+ catch (error) {
103
+ if (publicDispatcher) {
104
+ await publicDispatcher.destroy().catch(() => { });
105
+ publicDispatchers.delete(publicDispatcher);
106
+ }
107
+ throw error;
108
+ }
57
109
  };
58
110
  Object.defineProperty(fetch, FETCH_PROXY_SNAPSHOT, {
59
111
  value: proxySnapshot,
@@ -64,6 +116,7 @@ export function createProxiedFetchTransport(proxy) {
64
116
  return closePromise;
65
117
  closed = true;
66
118
  const destroyed = Promise.all([
119
+ ...[...publicDispatchers].map((dispatcher) => dispatcher.destroy(new Error('Connection effect fetch transport closed')).catch(() => { })),
67
120
  directDispatcher
68
121
  .destroy(new Error('Connection effect fetch transport closed'))
69
122
  .catch(() => { }),
@@ -49,6 +49,9 @@ Keep simple answers simple; do not add headings or lists to simple answers.
49
49
  Use short headings and flat lists to organize longer answers.
50
50
  Use fenced code blocks for multiline code and backticks for inline commands, paths, identifiers, and literal values.
51
51
  Prefer descriptive link text for external sources when it is available.
52
+ To deliver a generated screenshot or local image, include a Markdown image with its absolute path. The Host automatically saves supported local images after the assistant message completes. Supported remote images in completed assistant messages load automatically near the viewport, subject to the application's outbound/privacy policy and proxy settings. This media loading is independent of session subprocess network restrictions; include remote images only when relevant to the user's request. Reading an image for your own inspection does not deliver it.
53
+ Keep source files available until capture finishes. Images above the save limits cannot be previewed automatically.
54
+ Including image Markdown does not confirm that capture succeeded or that the client displayed it.
52
55
  Follow a more specific format requested by the user or task.`;
53
56
  }
54
57
  function buildProgressUpdatesPromptFragment() {