{"record":{"id":"090e3d439b83bfdb","repo":"nanocoai/nanoclaw","slug":"invalid-task-id-series","errorCode":null,"errorMessage":"invalid task id: ${series}","messagePattern":"invalid task id: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/modules/scheduling/run-log.ts","lineNumber":24,"sourceCode":" *   - `ncl tasks append-log` (agent's explicit mid-run/work-log entry)\n *   - the `task_log` outbound row a task run's final text produces\n *     (container/agent-runner poll-loop auto-append; delivery.ts routes it here)\n */\nimport fs from 'fs';\n\nimport { GROUPS_DIR } from '../../config.js';\nimport { resolveGroupTimezone } from '../../container-config.js';\nimport { getAgentGroup } from '../../db/agent-groups.js';\nimport { formatLocalStamp } from '../../timezone.js';\n\nexport async function appendRunLog(\n  agentGroupId: string,\n  series: string,\n  msg: string,\n): Promise<{ series: string; timestamp: string; path: string }> {\n  // Charset guard is the security boundary: blocks path traversal and keeps\n  // the id safe as a filename. Callers resolve group scope before this.\n  if (!/^[a-z0-9-]+$/.test(series)) throw new Error(`invalid task id: ${series}`);\n  const ag = await getAgentGroup(agentGroupId);\n  if (!ag) throw new Error(`agent group not found: ${agentGroupId}`);\n\n  const timestamp = formatLocalStamp(new Date(), await resolveGroupTimezone(agentGroupId));\n  const dir = `${GROUPS_DIR}/${ag.folder}/tasks`;\n  const file = `${dir}/${series}.md`;\n  fs.mkdirSync(dir, { recursive: true });\n  fs.appendFileSync(file, `${timestamp} — ${msg}\\n`);\n  return { series, timestamp, path: file };\n}\n\nexport async function deleteRunLog(agentGroupId: string, series: string): Promise<void> {\n  if (!/^[a-z0-9-]+$/.test(series)) throw new Error(`invalid task id: ${series}`);\n  const ag = await getAgentGroup(agentGroupId);\n  if (!ag) throw new Error(`agent group not found: ${agentGroupId}`);\n  fs.rmSync(`${GROUPS_DIR}/${ag.folder}/tasks/${series}.md`, { force: true });\n}\n","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/modules/scheduling/run-log.ts#L6-L42","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait appendRunLog(groupId, 'Run_Log.2024', 'started');\n\n// after\nawait appendRunLog(groupId, 'run-log-2024', 'started');","handlingStrategy":"validation","validationCode":"const TASK_ID_RE = /^[a-z0-9-]+$/;\nconst isSafeTaskId = (id: string): boolean => TASK_ID_RE.test(id);\nif (!isSafeTaskId(series)) throw new Error(`task id must match [a-z0-9-]: got \"${series}\"`);","typeGuard":"const isSafeTaskId = (id: unknown): id is string => typeof id === 'string' && /^[a-z0-9-]+$/.test(id);","tryCatchPattern":null,"preventionTips":["Generate ids as lowercase-with-dashes only (crypto.randomUUID() qualifies).","Centralize id normalization at the API boundary of your app."],"tags":["validation","path-traversal","filesystem","tasks"],"backgroundTag":"invalid-identifier","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}