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:
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user