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

  1. Normalize every hostPath with path.resolve() before adding it to the spec: resolve collapses '..', '.', and duplicate slashes and produces a rooted absolute path.
  2. Trim trailing slashes (path.resolve already does) and assert the result still lies under the intended root before passing the spec to prepare/validateSpec.
  3. 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

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


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/d23fd40f1b086489. Report an issue: GitHub.