nanocoai/nanoclaw · critical · deniedByPolicy

install-surface mount ${mount.hostPath} must be ro

Error message

install-surface mount ${mount.hostPath} must be ro

What it means

A mount classified 'install-surface' (code the runtime installs/executes: runner source, plugins) is declared with mode 'rw'. Install surfaces must be read-only so the agent cannot modify the very code it runs — a writable install surface is self-modification by another name.

Source

Thrown at src/drivers/types.ts:467

      }
      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}`);
      }
    }
    for (const [key, value] of Object.entries(container.contributedEnv ?? {})) {
      // The sanctioned lane: credential-shaped NAMES are its purpose — a
      // provider registering `ANTHROPIC_AUTH_TOKEN=placeholder` for the proxy

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Change the mount's mode to 'ro' as the message states.
  2. If the container genuinely must write there, that location is not an install surface — mount a different (writable-class) target path for the writes and keep the install surface ro.
  3. Audit spec generators/templates so 'install-surface' entries always emit mode 'ro'.

Example fix

// before
{ hostPath: '/opt/nanoclaw/runner', containerPath: '/runner', class: 'install-surface', mode: 'rw' }

// after
{ hostPath: '/opt/nanoclaw/runner', containerPath: '/runner', class: 'install-surface', mode: 'ro' }
Defensive patterns

Strategy: type-guard

Validate before calling

const bad = spec.containers.flatMap(c => c.mounts.filter(m => m.class === 'install-surface' && m.mode !== 'ro'));
if (bad.length) throw new Error(`install-surface mounts must be ro: ${bad.map(m => m.hostPath).join(', ')}`);

Type guard

function isReadOnlyInstallSurface(m: MountSpec): boolean {
  return m.class !== 'install-surface' || m.mode === 'ro';
}

Prevention

When it happens

Trigger: A spec mount with class 'install-surface' and mode: 'rw' (or any non-'ro' value) on any container. Commonly a typo or a copied mount literal from an rw workspace mount.

Common situations: Cloning a group-surface rw mount entry and changing hostPath to a plugins/surface root without flipping mode; debugging sessions where someone temporarily made everything writable and forgot to revert; automated spec generators that default mode to 'rw'.

Related errors


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