diff --git a/package-lock.json b/package-lock.json index f15560fc..36740455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1300,9 +1300,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1319,9 +1316,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1338,9 +1332,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1357,9 +1348,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1376,9 +1364,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1395,9 +1380,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1414,9 +1396,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1433,9 +1412,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1452,9 +1428,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1477,9 +1450,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1502,9 +1472,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1527,9 +1494,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1552,9 +1516,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1577,9 +1538,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1602,9 +1560,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1627,9 +1582,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ diff --git a/src/adapters/tools.test.ts b/src/adapters/tools.test.ts index 544f1d93..0e7cdda5 100644 --- a/src/adapters/tools.test.ts +++ b/src/adapters/tools.test.ts @@ -104,22 +104,77 @@ describe('validatePath realpath containment', () => { } }); - it('allows warehouse reads while keeping writes outside the project root denied', async () => { + it('validatePath itself accepts warehouse only when allowWarehouseRead is true', async () => { + vi.stubEnv('OPENSWARM_WAREHOUSE_ROOT', WAREHOUSE_DIR); + const file = path.join(WAREHOUSE_DIR, 'direct-validate.env'); + await fs.writeFile(file, 'KEY_NAME=redacted\n'); + try { + const allowed = validatePath(file, TMP_DIR, { allowWarehouseRead: true }); + expect(allowed).toBe(await fs.realpath(file)); + expect(() => validatePath(file, TMP_DIR)).toThrow(/outside the project root/); + expect(() => validatePath(file, TMP_DIR, { allowWarehouseRead: false })).toThrow(/outside the project root/); + } finally { + vi.unstubAllEnvs(); + } + }); + + it('allows warehouse reads when OPENSWARM_WAREHOUSE_ROOT is set', async () => { vi.stubEnv('OPENSWARM_WAREHOUSE_ROOT', WAREHOUSE_DIR); const file = path.join(WAREHOUSE_DIR, 'vega-agent.env'); await fs.writeFile(file, 'KEY_NAME=redacted\n'); try { const read = await executeTool(makeCall('read_file', { path: file }), TMP_DIR); - const write = await executeTool(makeCall('write_file', { path: file, content: 'changed' }), TMP_DIR); expect(read).toMatchObject({ is_error: false }); expect(read.content).toContain('KEY_NAME=redacted'); + } finally { + vi.unstubAllEnvs(); + } + }); + + it('refuses warehouse writes even when OPENSWARM_WAREHOUSE_ROOT is set', async () => { + vi.stubEnv('OPENSWARM_WAREHOUSE_ROOT', WAREHOUSE_DIR); + const file = path.join(WAREHOUSE_DIR, 'vega-agent-write.env'); + await fs.writeFile(file, 'KEY_NAME=redacted\n'); + try { + const write = await executeTool(makeCall('write_file', { path: file, content: 'changed' }), TMP_DIR); expect(write).toMatchObject({ is_error: true }); + expect(write.content).toContain('outside the project root'); await expect(fs.readFile(file, 'utf8')).resolves.toBe('KEY_NAME=redacted\n'); } finally { vi.unstubAllEnvs(); } }); + it('still refuses unrelated outside paths when warehouse is configured', async () => { + vi.stubEnv('OPENSWARM_WAREHOUSE_ROOT', WAREHOUSE_DIR); + try { + const result = await executeTool( + makeCall('read_file', { path: '/etc/passwd' }), + TMP_DIR, + ); + expect(result).toMatchObject({ is_error: true }); + expect(result.content).toContain('outside the project root'); + } finally { + vi.unstubAllEnvs(); + } + }); + + it('still allows /tmp reads when warehouse is configured', async () => { + vi.stubEnv('OPENSWARM_WAREHOUSE_ROOT', WAREHOUSE_DIR); + const filePath = path.join(TMP_DIR, 'warehouse-tmp-unchanged.txt'); + await fs.writeFile(filePath, 'tmp-ok', 'utf-8'); + try { + const result = await executeTool( + makeCall('read_file', { path: filePath }), + path.join(TMP_DIR, 'absent-project-cwd'), + ); + expect(result).toMatchObject({ is_error: false }); + expect(result.content).toContain('tmp-ok'); + } finally { + vi.unstubAllEnvs(); + } + }); + it('allows a read-only file tool to follow a worktree symlink into the warehouse', async () => { vi.stubEnv('OPENSWARM_WAREHOUSE_ROOT', WAREHOUSE_DIR); const target = path.join(WAREHOUSE_DIR, 'INDEX.md'); diff --git a/src/support/warehouseRoutes.test.ts b/src/support/warehouseRoutes.test.ts index 6c5bb550..4acf690b 100644 --- a/src/support/warehouseRoutes.test.ts +++ b/src/support/warehouseRoutes.test.ts @@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { tryHandleWarehouseRoutes } from './warehouseRoutes.js'; +import { resolveWarehouseReadPath, tryHandleWarehouseRoutes } from './warehouseRoutes.js'; const ROOT = await fs.mkdtemp('/var/tmp/openswarm-warehouse-routes-'); const OUTSIDE = await fs.mkdtemp('/var/tmp/openswarm-warehouse-outside-'); @@ -59,6 +59,13 @@ afterAll(async () => { }); describe('warehouse HTTP routes', () => { + it('returns one canonical path used for both authorization and I/O', async () => { + const expected = await fs.realpath(path.join(ROOT, 'INDEX.md')); + const authorized = resolveWarehouseReadPath('INDEX.md'); + expect(authorized).toBe(expected); + await expect(fs.readFile(authorized, 'utf8')).resolves.toBe('# Warehouse\n'); + }); + it('lists dotfiles with size and mtime for browser exploration', async () => { const response = await call('GET', '/api/warehouse/tree?path=vega-agent/env'); expect(response).toMatchObject({ handled: true, status: 200 });