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 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-22 23:23:34 +02:00
committed by GitHub
parent 6b85ac59bd
commit 77f7423172
3 changed files with 12 additions and 1 deletions

View File

@@ -112,10 +112,16 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
}; };
} }
} else if (args.schedule_type === 'once') { } 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); const date = new Date(args.schedule_value);
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
return { 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, isError: true,
}; };
} }

View File

@@ -14,6 +14,7 @@ vi.mock('./config.js', () => ({
DATA_DIR: '/tmp/nanoclaw-test-data', DATA_DIR: '/tmp/nanoclaw-test-data',
GROUPS_DIR: '/tmp/nanoclaw-test-groups', GROUPS_DIR: '/tmp/nanoclaw-test-groups',
IDLE_TIMEOUT: 1800000, // 30min IDLE_TIMEOUT: 1800000, // 30min
TIMEZONE: 'America/Los_Angeles',
})); }));
// Mock logger // Mock logger

View File

@@ -13,6 +13,7 @@ import {
DATA_DIR, DATA_DIR,
GROUPS_DIR, GROUPS_DIR,
IDLE_TIMEOUT, IDLE_TIMEOUT,
TIMEZONE,
} from './config.js'; } from './config.js';
import { readEnvFile } from './env.js'; import { readEnvFile } from './env.js';
import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js'; import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js';
@@ -187,6 +188,9 @@ function readSecrets(): Record<string, string> {
function buildContainerArgs(mounts: VolumeMount[], containerName: string): string[] { function buildContainerArgs(mounts: VolumeMount[], containerName: string): string[] {
const args: string[] = ['run', '-i', '--rm', '--name', containerName]; 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. // 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), // Skip when running as root (uid 0), as the container's node user (uid 1000),
// or when getuid is unavailable (native Windows without WSL). // or when getuid is unavailable (native Windows without WSL).