From 77f742317202f623376a3273248b011de8d69675 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sun, 22 Feb 2026 23:23:34 +0200 Subject: [PATCH] fix: pass host timezone to container and reject UTC-suffixed timestamps (#371) Containers had no TZ set, so any time-aware code inside ran in UTC while the host interpreted bare timestamps as local time. Now TIMEZONE from config.ts is passed via -e TZ= to the container args. Also rejects Z-suffixed or offset-suffixed timestamps in the container's schedule_task validation, since bare timestamps are expected to be local time and silently accepting UTC suffixes would cause an offset mismatch. Co-authored-by: Claude Opus 4.6 --- container/agent-runner/src/ipc-mcp-stdio.ts | 8 +++++++- src/container-runner.test.ts | 1 + src/container-runner.ts | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/container/agent-runner/src/ipc-mcp-stdio.ts b/container/agent-runner/src/ipc-mcp-stdio.ts index d894fe8..006b812 100644 --- a/container/agent-runner/src/ipc-mcp-stdio.ts +++ b/container/agent-runner/src/ipc-mcp-stdio.ts @@ -112,10 +112,16 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone): }; } } else if (args.schedule_type === 'once') { + if (/[Zz]$/.test(args.schedule_value) || /[+-]\d{2}:\d{2}$/.test(args.schedule_value)) { + return { + content: [{ type: 'text' as const, text: `Timestamp must be local time without timezone suffix. Got "${args.schedule_value}" — use format like "2026-02-01T15:30:00".` }], + isError: true, + }; + } const date = new Date(args.schedule_value); if (isNaN(date.getTime())) { return { - content: [{ type: 'text' as const, text: `Invalid timestamp: "${args.schedule_value}". Use ISO 8601 format like "2026-02-01T15:30:00.000Z".` }], + content: [{ type: 'text' as const, text: `Invalid timestamp: "${args.schedule_value}". Use local time format like "2026-02-01T15:30:00".` }], isError: true, }; } diff --git a/src/container-runner.test.ts b/src/container-runner.test.ts index 99c1cc7..1875c3f 100644 --- a/src/container-runner.test.ts +++ b/src/container-runner.test.ts @@ -14,6 +14,7 @@ vi.mock('./config.js', () => ({ DATA_DIR: '/tmp/nanoclaw-test-data', GROUPS_DIR: '/tmp/nanoclaw-test-groups', IDLE_TIMEOUT: 1800000, // 30min + TIMEZONE: 'America/Los_Angeles', })); // Mock logger diff --git a/src/container-runner.ts b/src/container-runner.ts index ffb6920..acfbe00 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -13,6 +13,7 @@ import { DATA_DIR, GROUPS_DIR, IDLE_TIMEOUT, + TIMEZONE, } from './config.js'; import { readEnvFile } from './env.js'; import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js'; @@ -187,6 +188,9 @@ function readSecrets(): Record { function buildContainerArgs(mounts: VolumeMount[], containerName: string): string[] { const args: string[] = ['run', '-i', '--rm', '--name', containerName]; + // Pass host timezone so container's local time matches the user's + args.push('-e', `TZ=${TIMEZONE}`); + // Run as host user so bind-mounted files are accessible. // Skip when running as root (uid 0), as the container's node user (uid 1000), // or when getuid is unavailable (native Windows without WSL).