nanocoai/nanoclaw · error · deniedByPolicy
mount ${mount.hostPath} must be a canonical absolute path (n
Error message
mount ${mount.hostPath} must be a canonical absolute path (no '..', '.', '//', or trailing '/') What it means
validateSpec rejects any mount whose hostPath is not in canonical absolute form: it must start with '/', and contain no empty segments ('//'), no '.', no '..', and no trailing '/'. Every mount class rule downstream is a prefix check against a trusted root, and a non-canonical path like /root/../outside would pass the prefix check while the runtime normalizes it outside the root. Requiring the canonical form makes the string the rules judge identical to the path actually mounted.
Source
Thrown at src/drivers/types.ts:440
// role, and a second container claiming it would make every 'agent'-keyed
// rule (identity-material exclusion, the realization's supervision) apply
// to an ambiguous target.
throw specInvalid('spec must carry exactly one agent container');
}
const pluginsRoot = stampedPluginsRoot(spec, policy);
for (const container of spec.containers) {
const seenTargets = new Set<string>();
for (const mount of container.mounts) {
if (!hostPathCanonical(mount.hostPath)) {
// Every class rule below is a prefix check against a trusted root, and
// a prefix check reads `materialsRoot/../outside` as inside — the
// runtime then normalizes it OUTSIDE the root it was judged against.
// Requiring the canonical absolute form makes the string these rules
// judge the same path the runtime mounts. (A relative source would not
// even be a bind: Docker reads it as a named volume.) Symlinks remain
// beyond a lexical check — that is what `admissionEnforced`
// realizations are for.
throw deniedByPolicy(
`mount ${mount.hostPath} must be a canonical absolute path (no '..', '.', '//', or trailing '/')`,
);
}
if (seenTargets.has(mount.containerPath)) {
// Two sources for one target would make the realized mount an ordering
// 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:View on GitHub (pinned to 294ef2aee8)
Solutions
- Normalize every hostPath with path.resolve() before adding it to the spec: resolve collapses '..', '.', and duplicate slashes and produces a rooted absolute path.
- Trim trailing slashes (path.resolve already does) and assert the result still lies under the intended root before passing the spec to prepare/validateSpec.
- If the path is user-supplied, reject or canonicalize it at the config boundary rather than inside the driver call.
Example fix
// before
spec.containers[0].mounts.push({ hostPath: `${cfg.root}/../secrets/key.pem`, containerPath: '/keys/key.pem', class: 'identity-material', mode: 'ro' });
// after
import { resolve } from 'node:path';
spec.containers[0].mounts.push({ hostPath: resolve(cfg.root, '../secrets/key.pem'), containerPath: '/keys/key.pem', class: 'identity-material', mode: 'ro' }); Defensive patterns
Strategy: validation
Validate before calling
import { resolve, sep } from 'node:path';
function assertCanonicalMounts(spec: SessionSpec): void {
for (const c of spec.containers)
for (const m of c.mounts) {
const r = resolve(m.hostPath);
if (r !== m.hostPath) m.hostPath = r; // normalize
if (!m.hostPath.startsWith(sep) || m.hostPath.includes('/..') || m.hostPath.endsWith('/'))
throw new Error(`non-canonical hostPath: ${m.hostPath}`);
}
} Type guard
function isCanonicalHostPath(p: string): boolean {
return p.startsWith('/') && p.split('/').slice(1).every(s => s !== '' && s !== '.' && s !== '..');
} Prevention
- Always build hostPaths with path.resolve, never string concatenation.
- Assert resolved paths stay under their intended root before composing the spec.
- Reject relative paths at the config-parsing boundary.
When it happens
Trigger: A container spec includes a mount with hostPath such as 'data/keys', '/var/run/../etc', '/opt/x/', or '/opt//x'. This can come from user-supplied config joined with path.join on relative components, or template strings that embed a trailing slash. It is thrown host-side from validateSpec, typically invoked via prepare() before any container is created.
Common situations: Building mount paths from config values with relative segments; copying docker -v style syntax (which tolerates some of this) into a typed spec; constructing paths by string concatenation instead of path.resolve; CI environments where a workspace root env var is empty, yielding paths like '/workspace//group'.
Related errors
- Template ref must be relative to the templates directory: "$
- duplicate containerPath ${mount.containerPath} on ${containe
- mount ${mount.hostPath} must be classed ${required}, not ${m
- install-surface mount ${mount.hostPath} must be ro
- identity-material mount ${mount.hostPath} invalid on role ${
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/d23fd40f1b086489.
Report an issue: GitHub.