nanocoai/nanoclaw · critical · deniedByPolicy
mount ${mount.hostPath} must be classed ${required}, not ${m
Error message
mount ${mount.hostPath} must be classed ${required}, not ${mount.class} What it means
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.
Source
Thrown at src/drivers/types.ts:464
// artifact. Composition resolves collisions (contributed mounts win),
// so a spec reaching a driver has exactly one source per target.
throw specInvalid(`duplicate containerPath ${mount.containerPath} on ${container.role}`);
}
seenTargets.add(mount.containerPath);
const required =
classRequiredByPath(mount.hostPath, policy) ??
(pluginsRoot && underRoot(mount.hostPath, pluginsRoot) ? 'install-surface' : null);
if (required && mount.class !== required) {
// Where a file lives decides what it IS, so the class is not the
// composer's to choose for these roots. Without this the taxonomy is
// only as strong as whoever assigns the class, and two of the four
// classes carry safety properties that a demotion silently drops:
// `allowlisted-extra` is permitted unconditionally, so relabelling a
// session private key as one mounts it INTO THE AGENT — defeating the
// no-credentials invariant outright — and relabelling the runner source
// as one escapes the read-only rule on the code the agent executes.
// Neither is exotic: both are a single word in a mount literal.
throw deniedByPolicy(`mount ${mount.hostPath} must be classed ${required}, not ${mount.class}`);
}
if (mount.class === 'install-surface' && mount.mode !== 'ro') {
throw deniedByPolicy(`install-surface mount ${mount.hostPath} must be ro`);
}
if (mount.class === 'identity-material' && (mount.mode !== 'ro' || container.role === 'agent')) {
// The no-credentials invariant, as a checkable rule: identity materials
// are ro-only and never enter the agent container.
throw deniedByPolicy(`identity-material mount ${mount.hostPath} invalid on role ${container.role}`);
}
if (!mountAllowed(mount, spec, policy)) {
throw deniedByPolicy(`mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}`);
}
}
for (const [key, value] of Object.entries(container.env)) {
if (isSecretShaped(key, value)) {
throw deniedByPolicy(`secret-shaped env '${key}' on ${container.role}`);
}
}View on GitHub (pinned to 294ef2aee8)
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.
Example fix
// before
{ hostPath: '/var/lib/nanoclaw/materials/session-key.pem', containerPath: '/creds/key.pem', class: 'allowlisted-extra', mode: 'ro' }
// after
{ hostPath: '/var/lib/nanoclaw/materials/session-key.pem', containerPath: '/creds/key.pem', class: 'identity-material', mode: 'ro' } Defensive patterns
Strategy: validation
Validate before calling
// Mirror the location rule before calling the driver:
function expectedClass(hostPath: string, policy: MountPolicy, pluginsRoot?: string): string | null {
if ([policy.materialsRoot, policy.groupsRoot, ...policy.surfaceRoots].some(r => hostPath.startsWith(r + '/') || hostPath === r))
return classForRoot(hostPath, policy); // same root->class mapping the policy defines
if (pluginsRoot && hostPath.startsWith(pluginsRoot + '/')) return 'install-surface';
return null;
} Try / catch
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; } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- install-surface mount ${mount.hostPath} must be ro
- identity-material mount ${mount.hostPath} invalid on role ${
- mount ${mount.hostPath} violates class ${mount.class} scope
- mount ${mount.hostPath} must be a canonical absolute path (n
- secret-shaped env '${key}' on ${container.role}
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/bacfb676a510acb2.
Report an issue: GitHub.