Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
} from '@pymodel/transcript';

import { WireRecordCache, type ContextRecord } from './wireCache';
import { resolveStoragePath } from '../../lib/storagePath';
import { toWireQuestion } from '../../protocol/question-wire';
import { projectPromptContentParts } from '../messages/messageProjection';
import {
Expand Down Expand Up @@ -528,7 +529,7 @@ export class TranscriptService {
let meta: SessionMeta;
try {
const raw = await readFile(
join(this.deps.homeDir, SESSIONS_ROOT, summary.workspaceId, sessionId, STATE_FILE),
resolveStoragePath(this.deps.homeDir, SESSIONS_ROOT, summary.workspaceId, sessionId, STATE_FILE),
'utf-8',
);
meta = JSON.parse(raw) as SessionMeta;
Expand All @@ -549,7 +550,7 @@ export class TranscriptService {
if (!isPlainAgentId(agentId)) {
return groupMessagesIntoSnapshot([]);
}
const wirePath = join(
const wirePath = resolveStoragePath(
this.deps.homeDir,
SESSIONS_ROOT,
summary.workspaceId,
Expand Down
18 changes: 18 additions & 0 deletions packages/agent-gateway/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ describe('server-v2 /api/v1/config', () => {
expect(cfg.yolo).toBe(false);
});

it('POST rejects bodies with prototype-poisoning keys before they reach the config', async () => {
await boot();
for (const body of [
'{"providers":{"__proto__":{"type":"openai","base_url":"https://api.example.com/v1"}}}',
'{"__proto__":{"default_model":"x"}}',
'{"thinking":{"constructor":{"prototype":{"enabled":true}}}}',
]) {
const res = await authedFetch(server as RunningServer, base, '/api/v1/config', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body,
});
const envelope = (await res.json()) as Envelope<unknown>;
expect(envelope.code).not.toBe(0);
expect(envelope.msg).toContain('not valid JSON');
}
});

it('POST { yolo: true } sets default_permission_mode = yolo and echoes yolo = true', async () => {
await boot();
const cfg = await patchConfig({ yolo: true });
Expand Down
37 changes: 37 additions & 0 deletions packages/agent-gateway/test/services/transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2793,6 +2793,43 @@ describe('AgentTranscriptProjector', () => {
}
});

it('cold reads stay inside the sessions root when the index names an escaping workspace', async () => {
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-escape-'));
try {
const outside = join(home, 'outside', 's1');
await mkdir(join(outside, 'agents', 'main'), { recursive: true });
await writeFile(
join(outside, 'state.json'),
JSON.stringify({ agents: { main: { type: 'main' } } }),
);
await writeFile(
join(outside, 'agents', 'main', 'wire.jsonl'),
`${JSON.stringify({ type: 'metadata', protocol_version: 1 })}\n`,
);
const service = new TranscriptService({
homeDir: home,
core: {
accessor: {
get: (token: unknown) => {
if (token === ISessionManager) {
return { get: () => undefined, list: () => [] };
}
if (token === IWorkspaceInstanceManager) {
return { list: () => [], onDidChange: () => ({ dispose: () => undefined }) };
}
if (token === ISessionIndex) return { get: async () => ({ workspaceId: '../outside' }) };
return undefined;
},
},
} as unknown as Scope,
});
expect(await service.readColdRoster('s1')).toEqual([]);
await expect(service.readColdSnapshot('s1', 'main')).rejects.toThrow('Storage path escapes its root');
} finally {
await rm(home, { recursive: true, force: true });
}
});

it('readColdSnapshot folds task/todo/goal/plan/interaction records into the cold snapshot', async () => {
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-facts-'));
try {
Expand Down
Loading