From 7d5a0316e806bc715e3763e9b7a739a84e5f0bfe Mon Sep 17 00:00:00 2001 From: Griffin Long Date: Thu, 27 Aug 2026 21:58:28 -0400 Subject: [PATCH] fix(subagent): sanitize id_prefix to prevent assertSafeJobId rejection (#1319) --- src/agent/subagent/fork-resolution.test.ts | 31 ++++++++++++++++++++++ src/agent/subagent/fork-resolution.ts | 3 ++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/agent/subagent/fork-resolution.test.ts b/src/agent/subagent/fork-resolution.test.ts index 8d4b84d5..c9e1a4ca 100644 --- a/src/agent/subagent/fork-resolution.test.ts +++ b/src/agent/subagent/fork-resolution.test.ts @@ -329,6 +329,37 @@ describe('id and resume', () => { expect(resolveForkInputs(args).id).toMatch(/^my-prefix-/); }); + it('sanitizes idPrefix by replacing disallowed characters with hyphens (#1319)', () => { + // A model-supplied id_prefix containing dots, spaces, or other chars outside + // [A-Za-z0-9_-] would cause assertSafeJobId to throw in SubagentLogWriter. + // Verify they are replaced with '-' before the id is assembled. + const args = makeArgs({ + options: { + parent: { sessionId: 'sid' }, + config: { model: 'sonnet' }, + agentType: 'a', + idPrefix: 'research.agent', + }, + counter: 1, + }); + + expect(resolveForkInputs(args).id).toMatch(/^research-agent-/); + }); + + it('sanitizes idPrefix with spaces', () => { + const args = makeArgs({ + options: { + parent: { sessionId: 'sid' }, + config: { model: 'sonnet' }, + agentType: 'a', + idPrefix: 'my agent', + }, + counter: 1, + }); + + expect(resolveForkInputs(args).id).toMatch(/^my-agent-/); + }); + it('defaults to "subagent" when idPrefix is omitted', () => { const args = makeArgs({ options: { diff --git a/src/agent/subagent/fork-resolution.ts b/src/agent/subagent/fork-resolution.ts index 0c6965ce..ed4b7ffb 100644 --- a/src/agent/subagent/fork-resolution.ts +++ b/src/agent/subagent/fork-resolution.ts @@ -92,7 +92,8 @@ export interface ForkResolved { export function resolveForkInputs(args: ResolveForkInputsArgs): ForkResolved { const { options, counter, managerHookRegistry, parentTraceWriter, parentModel } = args; - const id = `${options.idPrefix ?? 'subagent'}-${Date.now()}-${counter}`; + const safePrefix = (options.idPrefix ?? 'subagent').replace(/[^A-Za-z0-9_-]/g, '-'); + const id = `${safePrefix}-${Date.now()}-${counter}`; const resume = options.parent.sessionId; const registry =