diff --git a/src/host/host-commands.test.ts b/src/host/host-commands.test.ts index a9e05b5..943c041 100644 --- a/src/host/host-commands.test.ts +++ b/src/host/host-commands.test.ts @@ -113,6 +113,24 @@ describe('parseTimeout', () => { expect(() => parseTimeout('abc', 5000)).toThrow(/Invalid timeout/); expect(() => parseTimeout('Infinity', 5000)).toThrow(/Invalid timeout/); }); + + // The tenant-side hm.command dispatcher (agentic-hosting + // command-registry.ts) now rejects timeout_ms < 100 with + // `invalid_params`. Pre-flight at the CLI surface so the operator + // gets a clear local error instead of a confusing two-hop reply. + it('rejects values below MIN_TIMEOUT_MS=100ms', () => { + expect(() => parseTimeout('99', 5000)).toThrow(/minimum 100ms/); + expect(() => parseTimeout('50', 5000)).toThrow(/minimum 100ms/); + expect(() => parseTimeout('1', 5000)).toThrow(/minimum 100ms/); + // Decimal that floors below the floor still rejected. + expect(() => parseTimeout('99.9', 5000)).toThrow(/minimum 100ms/); + }); + + it('accepts values at and above MIN_TIMEOUT_MS=100ms', () => { + expect(parseTimeout('100', 5000)).toBe(100); + expect(parseTimeout('100.5', 5000)).toBe(100); // floor stays at 100 + expect(parseTimeout('30000', 5000)).toBe(30000); + }); }); describe('targetPayload', () => { diff --git a/src/host/host-commands.ts b/src/host/host-commands.ts index f755aaf..73f20a0 100644 --- a/src/host/host-commands.ts +++ b/src/host/host-commands.ts @@ -81,13 +81,25 @@ function parseGlobalOpts(cmd: Command): GlobalOpts { return cmd.optsWithGlobals(); } +// Floor used by tenant-side hm.command dispatcher (agentic-hosting +// command-registry.ts MIN_TIMEOUT_MS). Pre-flighting at the CLI surface +// avoids the confusing two-hop error path: CLI → manager → tenant rejects +// with `invalid_params`. Keep aligned with agentic-hosting's constant. +const MIN_TIMEOUT_MS = 100; + function parseTimeout(raw: string | undefined, fallback: number): number { if (!raw) return fallback; const n = Number(raw); if (!Number.isFinite(n) || n <= 0) { throw new Error(`Invalid timeout: ${raw}`); } - return Math.floor(n); + const floored = Math.floor(n); + if (floored < MIN_TIMEOUT_MS) { + throw new Error( + `Invalid timeout: ${raw} (minimum ${MIN_TIMEOUT_MS}ms — values below this are rejected by the tenant dispatcher)`, + ); + } + return floored; } function parseEnvPairs(pairs: readonly string[] | undefined): Record | undefined {