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

  1. Normalize the id to lowercase alphanumerics and dashes before calling: series.toLowerCase().replace(/[^a-z0-9]+/g, '-').
  2. If you control task creation, generate ids in the allowed charset from the start (e.g. crypto.randomUUID() — dashes only — is fine).
  3. 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

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


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/090e3d439b83bfdb. Report an issue: GitHub.