refactor(sandbox): address PR #250 round-3 review — dead import, env validation, uuid validator, docs (#243)

Must-fix:
- mcp.module: drop the now-dead EnvironmentModule import (and its stale
  comment). McpService no longer injects EnvironmentService; EnvironmentModule
  is @Global and imported at the app root, so DI still resolves.

Stability:
- environment.service: route getSandboxTtlMs + the three SANDBOX_MAX_*_BYTES
  caps through a shared getPositiveIntEnv() helper that warns once per key and
  falls back to the default on a non-integer or <= 0 value (previously the byte
  caps did a bare parseInt, so SANDBOX_MAX_TOTAL_BYTES=0 made every stash_page
  fail against a 0-byte cap). TTL behavior is unchanged.

Simplification:
- sandbox.controller: replace the homemade UUID_RE with the project's shared
  `uuid` validator (import { validate as isValidUUID } from 'uuid'), matching
  the attachment routes; update the spec fixtures to valid v4 UUIDs.
- mcp.service: inline the single-caller one-liner buildSandboxConfig() to
  this.sandboxStore.asSink() at the wiring site.

Docs:
- CHANGELOG: add an [Unreleased] > Added entry for #243 (stash_page tool,
  anonymous GET /api/sb/:id, five SANDBOX_* env vars).
- AGENTS.md: note that GET /api/sb/:id is in the workspace-gate preHandler's
  excludedPaths and is fully tokenless, unlike /api/files/public/... which
  still resolves a workspace and needs an attachment JWT.

Tests: cap-getter validation (0/-5/abc -> default, valid -> parsed), updated
UUID fixtures. apps/server jest sandbox/environment/mcp: 233 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-28 20:21:31 +03:00
parent 8842bc8bf3
commit aff58646d1
9 changed files with 113 additions and 62 deletions

View File

@@ -35,7 +35,10 @@ function makeReq(headers: Record<string, any> = {}) {
return { headers } as any;
}
const VALID_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
// A syntactically valid v4 UUID (version nibble 4, variant nibble 8). The
// shared `uuid` validator is stricter than a bare hex-shape regex, so the id
// must carry a real version/variant.
const VALID_ID = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
function entry(buf: Buffer, mime: string, sha256: string): SandboxEntry {
return { buf, mime, sha256, expiresAt: Date.now() + 60_000 };

View File

@@ -1,14 +1,9 @@
import { Controller, Get, Param, Req, Res } from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';
import { validate as isValidUUID } from 'uuid';
import { SandboxStore } from './sandbox.store';
import { SANDBOX_ROUTE_SEGMENT } from './sandbox.constants';
// Strict UUID v-agnostic shape. This is anti-traversal / input hygiene (so `:id`
// can never be a path like `../...`), NOT authorization — the capability is the
// unguessable id itself plus the short TTL plus TLS.
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
// MIME types safe to render inline in a browser. SVG is deliberately EXCLUDED
// (it can carry script), as are text/html and the JSON document blob — anything
// not on this list is served as an attachment so an attacker-controlled mime can
@@ -51,9 +46,13 @@ export class SandboxController {
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
): Promise<void> {
// Non-UUID id (including any traversal attempt) → 404 before touching the
// store. No stack trace leaks out.
if (!UUID_RE.test(id)) {
// Validate `:id` as a real UUID via the shared `uuid` validator (same as the
// attachment routes). This is anti-traversal / input hygiene (so `:id` can
// never be a path like `../...`), NOT authorization — the capability is the
// unguessable id itself plus the short TTL plus TLS. A non-UUID id (including
// any traversal attempt) → 404 before touching the store; no stack trace
// leaks out.
if (!isValidUUID(id)) {
res.status(404).send();
return;
}

View File

@@ -1,4 +1,5 @@
import { createHash } from 'node:crypto';
import { validate as isValidUUID } from 'uuid';
import { SandboxStore } from './sandbox.store';
// Build a minimal EnvironmentService stub with overridable caps/TTL.
@@ -26,9 +27,6 @@ function makeEnv(
} as any;
}
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
describe('SandboxStore', () => {
let store: SandboxStore;
@@ -43,7 +41,7 @@ describe('SandboxStore', () => {
const buf = Buffer.from('{"type":"doc","content":[]}', 'utf8');
const res = store.put(buf, 'application/json');
expect(res.id).toMatch(UUID_RE);
expect(isValidUUID(res.id)).toBe(true);
expect(res.size).toBe(buf.length);
const entry = store.get(res.id);