diff --git a/packages/sdk/src/authored-worker-step.ts b/packages/sdk/src/authored-worker-step.ts index 800dc02e9..389f449f4 100644 --- a/packages/sdk/src/authored-worker-step.ts +++ b/packages/sdk/src/authored-worker-step.ts @@ -95,10 +95,24 @@ export function authoredWorkerRunner( + 'if you do not need enforcement, or use the declarative spec\'s `permissions` field, which is real.', ); } + if (options.cli !== undefined && typeof options.cli !== 'string') { + throw new AuthoredFlowExecutionError( + 'agent_cli_unresolved', + `f.agent options.cli must be a string when set (got ${typeof options.cli}).`, + ); + } + if (options.model !== undefined && typeof options.model !== 'string') { + throw new AuthoredFlowExecutionError( + 'agent_cli_unresolved', + `f.agent options.model must be a string when set (got ${typeof options.model}).`, + ); + } const output = await run({ id, type: 'agent', instruction: options.task, ...(localAgentStream === undefined ? {} : { surfaces: { streams: [{ stream: localAgentStream }] } }), ...(options.workspace === undefined ? {} : { surfaces: { workspace: [{ surface: options.workspace }] } }), + ...(options.cli === undefined ? {} : { cli: options.cli }), + ...(options.model === undefined ? {} : { model: options.model }), }); if (typeof output !== 'object' || output === null || Array.isArray(output)) { throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`); diff --git a/packages/sdk/tests/authored-flow.test.ts b/packages/sdk/tests/authored-flow.test.ts index 7e1659197..40ce4111a 100644 --- a/packages/sdk/tests/authored-flow.test.ts +++ b/packages/sdk/tests/authored-flow.test.ts @@ -169,6 +169,31 @@ describe('authored flow journal executor', () => { }); }); + it('refuses non-string cli or model on f.agent options before contacting the journal', async () => { + const disconnectedJournal = new JournalClient('/journal-must-not-be-contacted'); + + // Both preflight and the type check throw `agent_cli_unresolved` + // (matching the shared LLM code pattern) — the diagnostic message + // distinguishes them so authors get an accurate error. + for (const bad of [42, true, {}, []]) { + await expect(executeAuthoredFlow(flow('agent-cli-not-string', async (f) => { + await f.agent('worker', { task: 'x', cli: bad as unknown as string }); + f.done('success'); + }), disconnectedJournal)).rejects.toMatchObject({ + code: 'agent_cli_unresolved', + message: expect.stringMatching(/f\.agent options\.cli must be a string when set/), + }); + + await expect(executeAuthoredFlow(flow('agent-model-not-string', async (f) => { + await f.agent('worker', { task: 'x', model: bad as unknown as string }); + f.done('success'); + }), disconnectedJournal)).rejects.toMatchObject({ + code: 'agent_cli_unresolved', + message: expect.stringMatching(/f\.agent options\.model must be a string when set/), + }); + } + }); + it('rejects invalid raw headers before the executor can contact the journal', async () => { const disconnectedJournal = new JournalClient('/journal-must-not-be-contacted'); diff --git a/packages/sdk/tests/live-kernel.test.ts b/packages/sdk/tests/live-kernel.test.ts index 0006d1210..bf485f2bb 100644 --- a/packages/sdk/tests/live-kernel.test.ts +++ b/packages/sdk/tests/live-kernel.test.ts @@ -345,11 +345,13 @@ process.stdin.on('end', () => { }); `); chmodSync(cli, 0o755); - // f.agent has no way to declare a CLI itself (AgentOptions is - // {task, workspace} only) — it resolves one exactly the way a bare - // `type: agent` YAML step with no explicit `cli` does: the nearest - // flows.json's project default, found searching upward from the flow's - // own path (checkAuthoredFlow, cli/check.ts). + // This test intentionally omits cli/model on f.agent (AgentOptions + // accepts { task, workspace, cli?, model? } as of #310) — it exercises + // the resolution fallback: with no step cli, an authored agent resolves + // exactly like a bare `type: agent` YAML step, walking up to the + // nearest flows.json's project default (checkAuthoredFlow, cli/check.ts). + // The explicit-cli/model path is covered by unit tests in + // authored-flow.test.ts. writeFileSync(join(directory, 'flows.json'), JSON.stringify({ cli })); await startDaemon(dataDir); diff --git a/packages/surface/src/context.ts b/packages/surface/src/context.ts index 164736b57..2594266f8 100644 --- a/packages/surface/src/context.ts +++ b/packages/surface/src/context.ts @@ -10,6 +10,8 @@ export interface AgentResult { export interface AgentOptions { task: string; workspace?: string; + cli?: string; + model?: string; } export interface LlmOptions {