{"record":{"id":"bdc135f39a1218c4","repo":"nanocoai/nanoclaw","slug":"chat-sdk-bridge-instance-json-stringify-config-i","errorCode":null,"errorMessage":"chat-sdk bridge instance ${JSON.stringify(config.instance)} must be URL-safe: non-empty, only letters, digits, '.', '_' or '-'","messagePattern":"chat-sdk bridge instance (.+?) must be URL-safe: non-empty, only letters, digits, '\\.', '_' or '-'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/channels/chat-sdk-bridge.ts","lineNumber":432,"sourceCode":"    if (cut <= 0) cut = limit;\n    chunks.push(remaining.slice(0, cut).trimEnd());\n    remaining = remaining.slice(cut).trimStart();\n  }\n  if (remaining.length > 0) chunks.push(remaining);\n  return chunks;\n}\n\nexport function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter {\n  const { adapter } = config;\n  // The instance name becomes a webhook route segment (the route regex is\n  // [^/?]+) and ':' is the state-namespace delimiter — reject anything that\n  // would break either, at construction time rather than at first webhook.\n  // Positive allow-list (not a deny-list): also rejects '' and\n  // whitespace-only names, which are config bugs — '' is falsy, so it\n  // would skip a truthiness guard, dead-end the webhook route, and\n  // collapse the state namespace into the default instance's keyspace.\n  if (config.instance !== undefined && !INSTANCE_KEY_RE.test(config.instance)) {\n    throw new Error(\n      `chat-sdk bridge instance ${JSON.stringify(config.instance)} must be URL-safe: ` +\n        `non-empty, only letters, digits, '.', '_' or '-'`,\n    );\n  }\n  const transformText = (t: string): string => (config.transformOutboundText ? config.transformOutboundText(t) : t);\n  /** Registry/routing key for this bridge — also the app-context cache\n   *  namespace. Default instances key by the platform name. */\n  const instanceKey = config.instance ?? adapter.name;\n  let chat: Chat;\n  let state: SqliteStateAdapter;\n  let setupConfig: ChannelSetup;\n  let gatewayAbort: AbortController | null = null;\n\n  async function messageToInbound(\n    message: ChatMessage,\n    isMention: boolean,\n    isGroup?: boolean,\n  ): Promise<InboundMessage> {","sourceCodeStart":414,"sourceCodeEnd":450,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/channels/chat-sdk-bridge.ts#L414-L450","documentation":"Any symlink encountered anywhere inside the plugin tree throws with the offending relative path. Symlinks could escape the containment boundary (e.g. linking to ~/.ssh), so plugins must ship real files only.","triggerScenarios":"A symlink anywhere in the plugin directory — e.g. skills/ -> ../../shared/skills, or a node_modules-style link — even if it points inside the tree.","commonSituations":"Developers symlinking shared skill folders into a template during development and forgetting to materialize them; some editors/git workflows creating links; extraction tools preserving stored symlinks.","solutions":["Replace the symlink with a real copy of the target's contents","If developing, script a sync step that copies instead of links","Re-package/extract the plugin with symlink dereferencing (e.g. 'cp -rL')"],"exampleFix":"# before\nln -s ../../shared/skills skills\n# after\ncp -r ../../shared/skills skills","handlingStrategy":"validation","validationCode":"function hasSymlink(dir: string): boolean {\n  let found = false;\n  const walk = (d: string) => {\n    for (const e of fs.readdirSync(d, { withFileTypes: true })) {\n      if (e.isSymbolicLink()) { found = true; return; }\n      const p = path.join(d, e.name);\n      if (e.isDirectory()) walk(p);\n    }\n  };\n  walk(dir);\n  return found;\n}\nif (hasSymlink(dir)) { /* copy -rL to materialize */ }","typeGuard":null,"tryCatchPattern":"try { walkPluginDir(dir); } catch (e) { if (e instanceof Error && e.message.includes('is a symlink')) { /* cp -rL into a fresh dir and retry */ } else throw e; }","preventionTips":["Package plugins with 'cp -rL' or an archive step that dereferences symlinks","Never symlink shared assets into a template; script a copy step instead"],"tags":["security","filesystem","symlink","templates"],"backgroundTag":"symlink-rejected","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}