{"record":{"id":"bacfb676a510acb2","repo":"nanocoai/nanoclaw","slug":"mount-mount-hostpath-must-be-classed-required","errorCode":null,"errorMessage":"mount ${mount.hostPath} must be classed ${required}, not ${mount.class}","messagePattern":"mount (.+?) must be classed (.+?), not (.+?)","errorType":"validation","errorClass":"deniedByPolicy","httpStatus":null,"severity":"critical","filePath":"src/drivers/types.ts","lineNumber":464,"sourceCode":"        // 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') {\n        throw deniedByPolicy(`install-surface mount ${mount.hostPath} must be ro`);\n      }\n      if (mount.class === 'identity-material' && (mount.mode !== 'ro' || container.role === 'agent')) {\n        // The no-credentials invariant, as a checkable rule: identity materials\n        // are ro-only and never enter the agent container.\n        throw deniedByPolicy(`identity-material mount ${mount.hostPath} invalid on role ${container.role}`);\n      }\n      if (!mountAllowed(mount, spec, policy)) {\n        throw deniedByPolicy(`mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}`);\n      }\n    }\n    for (const [key, value] of Object.entries(container.env)) {\n      if (isSecretShaped(key, value)) {\n        throw deniedByPolicy(`secret-shaped env '${key}' on ${container.role}`);\n      }\n    }","sourceCodeStart":446,"sourceCodeEnd":482,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/drivers/types.ts#L446-L482","documentation":"The mount's hostPath falls under a policy root whose class is decided by location (classRequiredByPath, or the stamped plugins root implying 'install-surface'), but the spec declares a different class. Where a file lives decides what it is: allowing the composer to relabel, say, a session private key as 'allowlisted-extra' would mount it into the agent and defeat the no-credentials invariant, and relabeling runner source would escape its read-only rule.","triggerScenarios":"A mount whose hostPath is under policy.materialsRoot (or another classified root / the plugins root) is declared with class 'allowlisted-extra', 'group-surface', or anything other than the location-required class. Typical of hand-written specs or ported docker-compose volume entries where the author picked the most permissive class.","commonSituations":"Copy-pasting a working 'allowlisted-extra' mount literal and changing only the hostPath to point at a classified location; upgrading the policy roots (new materialsRoot) so previously-unclassified paths now require a class; authors choosing a permissive class to dodge the ro-only rules.","solutions":["Set the mount's class to the value named in the error message (the 'required' class) — e.g. class it 'identity-material' for paths under materialsRoot, 'install-surface' for plugins/surface roots.","If the file genuinely is not classified material, move it out of the policy-governed root rather than relabeling it.","Never work around by relabeling to 'allowlisted-extra' — that class is unconditional and the check exists precisely to block it."],"exampleFix":"// before\n{ hostPath: '/var/lib/nanoclaw/materials/session-key.pem', containerPath: '/creds/key.pem', class: 'allowlisted-extra', mode: 'ro' }\n\n// after\n{ hostPath: '/var/lib/nanoclaw/materials/session-key.pem', containerPath: '/creds/key.pem', class: 'identity-material', mode: 'ro' }","handlingStrategy":"validation","validationCode":"// Mirror the location rule before calling the driver:\nfunction expectedClass(hostPath: string, policy: MountPolicy, pluginsRoot?: string): string | null {\n  if ([policy.materialsRoot, policy.groupsRoot, ...policy.surfaceRoots].some(r => hostPath.startsWith(r + '/') || hostPath === r))\n    return classForRoot(hostPath, policy); // same root->class mapping the policy defines\n  if (pluginsRoot && hostPath.startsWith(pluginsRoot + '/')) return 'install-surface';\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try { validateSpec(spec, policy); } catch (e) { if ((e as any).kind === 'denied-by-policy' && /must be classed/.test(e.detail)) { /* fix class from message, never bypass */ throw e; } throw e; }","preventionTips":["Never hand-assign classes for paths under policy roots — derive them from location.","Treat 'allowlisted-extra' as policy-granted, not author-chosen.","Re-run validateSpec in unit tests for every spec template you ship."],"tags":["mount","security","policy","classification"],"backgroundTag":"mount-class-mismatch","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}