nanocoai/nanoclaw · error
invalid task id: ${series}
Error message
invalid task id: ${series} What it means
appendRunLog validates the task series id against ^[a-z0-9-]+$ before using it as a filename under groups/<folder>/tasks/. This charset guard is the security boundary: it blocks path traversal ('..', '/', uppercase, dots) and guarantees the id is safe to interpolate into a filesystem path. Any other shape throws 'invalid task id'.
Source
Thrown at src/modules/scheduling/run-log.ts:24
* - `ncl tasks append-log` (agent's explicit mid-run/work-log entry)
* - the `task_log` outbound row a task run's final text produces
* (container/agent-runner poll-loop auto-append; delivery.ts routes it here)
*/
import fs from 'fs';
import { GROUPS_DIR } from '../../config.js';
import { resolveGroupTimezone } from '../../container-config.js';
import { getAgentGroup } from '../../db/agent-groups.js';
import { formatLocalStamp } from '../../timezone.js';
export async function appendRunLog(
agentGroupId: string,
series: string,
msg: string,
): Promise<{ series: string; timestamp: string; path: string }> {
// Charset guard is the security boundary: blocks path traversal and keeps
// the id safe as a filename. Callers resolve group scope before this.
if (!/^[a-z0-9-]+$/.test(series)) throw new Error(`invalid task id: ${series}`);
const ag = await getAgentGroup(agentGroupId);
if (!ag) throw new Error(`agent group not found: ${agentGroupId}`);
const timestamp = formatLocalStamp(new Date(), await resolveGroupTimezone(agentGroupId));
const dir = `${GROUPS_DIR}/${ag.folder}/tasks`;
const file = `${dir}/${series}.md`;
fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(file, `${timestamp} — ${msg}\n`);
return { series, timestamp, path: file };
}
export async function deleteRunLog(agentGroupId: string, series: string): Promise<void> {
if (!/^[a-z0-9-]+$/.test(series)) throw new Error(`invalid task id: ${series}`);
const ag = await getAgentGroup(agentGroupId);
if (!ag) throw new Error(`agent group not found: ${agentGroupId}`);
fs.rmSync(`${GROUPS_DIR}/${ag.folder}/tasks/${series}.md`, { force: true });
}
View on GitHub (pinned to 294ef2aee8)
Solutions
- Normalize the id to lowercase alphanumerics and dashes before calling: series.toLowerCase().replace(/[^a-z0-9]+/g, '-').
- If you control task creation, generate ids in the allowed charset from the start (e.g. crypto.randomUUID() — dashes only — is fine).
- Never construct the path yourself; always pass the bare series id.
Example fix
// before await appendRunLog(groupId, 'Run_Log.2024', 'started'); // after await appendRunLog(groupId, 'run-log-2024', 'started');
Defensive patterns
Strategy: validation
Validate before calling
const TASK_ID_RE = /^[a-z0-9-]+$/;
const isSafeTaskId = (id: string): boolean => TASK_ID_RE.test(id);
if (!isSafeTaskId(series)) throw new Error(`task id must match [a-z0-9-]: got "${series}"`); Type guard
const isSafeTaskId = (id: unknown): id is string => typeof id === 'string' && /^[a-z0-9-]+$/.test(id);
Prevention
- Generate ids as lowercase-with-dashes only (crypto.randomUUID() qualifies).
- Centralize id normalization at the API boundary of your app.
When it happens
Trigger: Calling appendRunLog/appendTaskLog with a series containing uppercase letters, underscores, dots, slashes, or empty string — e.g. 'Task_01', 'run.log', '../etc', or a UUID with braces/colons.
Common situations: Generating task ids externally (UUIDs with dashes are fine, but 'uuid v4' with dots or base64 ids are not); copying a task id from a URL where it was encoded; hand-typing an id with an underscore by habit from other naming conventions.
Related errors
- MissingChannelAdapterError(channelType, instance)
- --status must be pending or paused
- task series id is required
- --group is required
- --prompt is required
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/090e3d439b83bfdb.
Report an issue: GitHub.