{"record":{"id":"424dd1d8d3b5ba7d","repo":"nanocoai/nanoclaw","slug":"duplicate-containerpath-mount-containerpath-on","errorCode":null,"errorMessage":"duplicate containerPath ${mount.containerPath} on ${container.role}","messagePattern":"duplicate containerPath (.+?) on (.+?)","errorType":"validation","errorClass":"specInvalid","httpStatus":null,"severity":"error","filePath":"src/drivers/types.ts","lineNumber":448,"sourceCode":"    for (const mount of container.mounts) {\n      if (!hostPathCanonical(mount.hostPath)) {\n        // Every class rule below is a prefix check against a trusted root, and\n        // a prefix check reads `materialsRoot/../outside` as inside — the\n        // runtime then normalizes it OUTSIDE the root it was judged against.\n        // Requiring the canonical absolute form makes the string these rules\n        // judge the same path the runtime mounts. (A relative source would not\n        // even be a bind: Docker reads it as a named volume.) Symlinks remain\n        // beyond a lexical check — that is what `admissionEnforced`\n        // realizations are for.\n        throw deniedByPolicy(\n          `mount ${mount.hostPath} must be a canonical absolute path (no '..', '.', '//', or trailing '/')`,\n        );\n      }\n      if (seenTargets.has(mount.containerPath)) {\n        // Two sources for one target would make the realized mount an ordering\n        // artifact. Composition resolves collisions (contributed mounts win),\n        // so a spec reaching a driver has exactly one source per target.\n        throw specInvalid(`duplicate containerPath ${mount.containerPath} on ${container.role}`);\n      }\n      seenTargets.add(mount.containerPath);\n      const required =\n        classRequiredByPath(mount.hostPath, policy) ??\n        (pluginsRoot && underRoot(mount.hostPath, pluginsRoot) ? 'install-surface' : null);\n      if (required && mount.class !== required) {\n        // Where a file lives decides what it IS, so the class is not the\n        // composer's to choose for these roots. Without this the taxonomy is\n        // only as strong as whoever assigns the class, and two of the four\n        // classes carry safety properties that a demotion silently drops:\n        // `allowlisted-extra` is permitted unconditionally, so relabelling a\n        // session private key as one mounts it INTO THE AGENT — defeating the\n        // no-credentials invariant outright — and relabelling the runner source\n        // as one escapes the read-only rule on the code the agent executes.\n        // Neither is exotic: both are a single word in a mount literal.\n        throw deniedByPolicy(`mount ${mount.hostPath} must be classed ${required}, not ${mount.class}`);\n      }\n      if (mount.class === 'install-surface' && mount.mode !== 'ro') {","sourceCodeStart":430,"sourceCodeEnd":466,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/drivers/types.ts#L430-L466","documentation":"Two mounts on the same container target the same containerPath. Which source wins would then depend on mount ordering inside the runtime, an artifact the model refuses to rely on; composition is supposed to resolve collisions (contributed mounts win) so a spec reaching the driver has exactly one source per container path.","triggerScenarios":"A spec literally lists two mounts with the same containerPath on one container, e.g. both '/workspace' and '/plugins' mapping to '/opt/stuff', or a hand-merged spec where a base mount and an added mount both claim '/app'. Raised as kind 'spec-invalid' from validateSpec during prepare().","commonSituations":"Merging mount lists from multiple sources (base template + group config + provider contributions) without deduping by containerPath; refactoring a containerPath constant and forgetting one occurrence; adding a new mount that shadows an existing default like /workspace.","solutions":["Search the spec for the duplicated containerPath named in the message and remove or rename one of the mounts so each target has exactly one source.","If merging contributed mounts, resolve collisions before validateSpec: keep the contributed mount and drop the base one (matching the composition rule).","Dedupe by containerPath with a Map keyed on the target when building the spec programmatically."],"exampleFix":"// before\nmounts: [\n  { hostPath: '/groups/a/workspace', containerPath: '/workspace', class: 'group-surface', mode: 'rw' },\n  { hostPath: '/shared/stubs', containerPath: '/workspace', class: 'allowlisted-extra', mode: 'ro' },\n]\n\n// after\nmounts: [\n  { hostPath: '/groups/a/workspace', containerPath: '/workspace', class: 'group-surface', mode: 'rw' },\n  { hostPath: '/shared/stubs', containerPath: '/stubs', class: 'allowlisted-extra', mode: 'ro' },\n]","handlingStrategy":"validation","validationCode":"function dedupeMountTargets(spec: SessionSpec): void {\n  for (const c of spec.containers) {\n    const byTarget = new Map(c.mounts.map(m => [m.containerPath, m]));\n    if (byTarget.size !== c.mounts.length) {\n      // keep the contributed mount per composition rule, or throw explicitly\n      throw new Error(`duplicate containerPath on ${c.role}`);\n    }\n  }\n}","typeGuard":"function hasUniqueMountTargets(mounts: MountSpec[]): boolean {\n  return new Set(mounts.map(m => m.containerPath)).size === mounts.length;\n}","tryCatchPattern":null,"preventionTips":["Dedupe merged mount lists by containerPath before validateSpec.","When contributing mounts, drop the base mount with the same target instead of appending.","Key mount collections by containerPath in spec builders so duplicates are structurally impossible."],"tags":["mount","duplicate","spec-validation","container"],"backgroundTag":"duplicate-mount-target","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}