Security: - stash_page: reject path-traversal / percent-encoded srcs before the authed loopback fetch (resolveInternalFilePath), closing an SSRF/exfiltration hole where a crafted node.attrs.src could read an arbitrary internal GET endpoint into the anonymous sandbox. Stability: - stash_page: revert + recount mirrors FIFO-evicted by a later put in the same stash (no dangling sandbox refs, honest images.mirrored/failed); free image blobs if the final document put throws. - Reject/clamp non-positive SANDBOX_TTL_MS to the 1h default (warn once). - Log mirror failures unconditionally (console.warn, no blob bodies). Cleanup / architecture: - Remove dead expiresAt from SandboxPutResult. - Centralize the /api/sb route in SANDBOX_ROUTE_SEGMENT/SANDBOX_API_PATH and move URL composition into SandboxStore.putAndLink; drop the duplicated sink closures and the now-unused EnvironmentService injection from McpService and AiChatToolsService. - Un-export isInternalFileUrl; document the process-local (instance-bound) sandbox limitation in the tool description and .env.example. Docs/tests: - README/README.ru: 38 -> 39 tools + stash_page entry. - Add traversal/normalize/recursion unit tests, stash self-eviction + doc-put-throw + empty/octet-stream mock tests, controller If-None-Match (wildcard/weak/list) + Cache-Control tests, and SANDBOX_TTL_MS validation tests. Regenerate packages/mcp/build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -63,9 +63,8 @@ describe('AiChatToolsService deletePage guardrail (H4)', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
// environmentService + sandboxStore (only used by the stash tool closure,
|
||||
// which these tests do not execute).
|
||||
{} as never,
|
||||
// sandboxStore (only used by the stash tool closure, which these tests do
|
||||
// not execute).
|
||||
{} as never,
|
||||
);
|
||||
});
|
||||
@@ -179,9 +178,8 @@ describe('AiChatToolsService expanded toolset guardrails', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
// environmentService + sandboxStore (only used by the stash tool closure,
|
||||
// which these tests do not execute).
|
||||
{} as never,
|
||||
// sandboxStore (only used by the stash tool closure, which these tests do
|
||||
// not execute).
|
||||
{} as never,
|
||||
);
|
||||
});
|
||||
@@ -298,9 +296,8 @@ describe('AiChatToolsService node-arg JSON-string coercion', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
// environmentService + sandboxStore (only used by the stash tool closure,
|
||||
// which these tests do not execute).
|
||||
{} as never,
|
||||
// sandboxStore (only used by the stash tool closure, which these tests do
|
||||
// not execute).
|
||||
{} as never,
|
||||
);
|
||||
});
|
||||
@@ -452,9 +449,8 @@ describe('AiChatToolsService model-friendly input validation (#190)', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
// environmentService + sandboxStore (only used by the stash tool closure,
|
||||
// which these tests do not execute).
|
||||
{} as never,
|
||||
// sandboxStore (only used by the stash tool closure, which these tests do
|
||||
// not execute).
|
||||
{} as never,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { resolveCurrentPageResult } from './current-page.util';
|
||||
import { parseNodeArg } from './parse-node-arg';
|
||||
import { modelFriendlyInput } from './model-friendly-input';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import { SandboxStore } from '../../../integrations/sandbox/sandbox.store';
|
||||
|
||||
/**
|
||||
@@ -43,7 +42,6 @@ export class AiChatToolsService {
|
||||
private readonly pageEmbeddingRepo: PageEmbeddingRepo,
|
||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
// Shared singleton in-RAM blob store backing the stash tool.
|
||||
private readonly sandboxStore: SandboxStore,
|
||||
) {}
|
||||
@@ -91,22 +89,23 @@ export class AiChatToolsService {
|
||||
aiChatId,
|
||||
});
|
||||
|
||||
// Bind the stash tool to the shared in-RAM SandboxStore and compose the
|
||||
// anonymous public URL here (the MCP package never touches env or the
|
||||
// store). put() returns the read URL + sha256/size; sha256 is also the
|
||||
// blob's ETag for integrity.
|
||||
const sandboxPut = (buf: Buffer, mime: string) => {
|
||||
const stored = this.sandboxStore.put(buf, mime);
|
||||
const base = this.environmentService.getSandboxPublicUrl();
|
||||
return { uri: `${base}/api/sb/${stored.id}`, sha256: stored.sha256, size: stored.size };
|
||||
};
|
||||
// Bind the stash tool to the shared in-RAM SandboxStore. The store owns the
|
||||
// anonymous-URL composition (putAndLink) and the live/evict probes the MCP
|
||||
// package needs to keep its mirror counts honest under FIFO eviction (the
|
||||
// package never touches env or the store). The sink speaks `uri`s, so the
|
||||
// probes map a uri back to its id (the last path segment).
|
||||
const idOf = (uri: string) => uri.substring(uri.lastIndexOf('/') + 1);
|
||||
|
||||
const { DocmostClient, sharedToolSpecs } = await loadDocmostMcp();
|
||||
const client: DocmostClientLike = new DocmostClient({
|
||||
apiUrl,
|
||||
getToken,
|
||||
getCollabToken,
|
||||
sandbox: { put: sandboxPut },
|
||||
sandbox: {
|
||||
put: (buf, mime) => this.sandboxStore.putAndLink(buf, mime),
|
||||
has: (uri) => this.sandboxStore.has(idOf(uri)),
|
||||
evict: (uri) => this.sandboxStore.remove(idOf(uri)),
|
||||
},
|
||||
});
|
||||
|
||||
// Build an ai-SDK tool from a shared, zod-agnostic spec. The spec owns the
|
||||
|
||||
@@ -171,11 +171,15 @@ export type DocmostClientConfig = {
|
||||
getCollabToken?: () => Promise<string>;
|
||||
// Optional blob-sandbox sink for the stash tool. `put` stores a blob in the
|
||||
// host's in-RAM SandboxStore and returns the anonymous read URL + integrity.
|
||||
// The optional `has`/`evict` probes let stashPage keep its mirror counts
|
||||
// honest under the store's FIFO eviction (mirror of the package's sink type).
|
||||
sandbox?: {
|
||||
put: (
|
||||
buf: Buffer,
|
||||
mime: string,
|
||||
) => { uri: string; sha256: string; size: number };
|
||||
has?: (uri: string) => boolean;
|
||||
evict?: (uri: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user