nanocoai/nanoclaw · error · deniedByPolicy

mount ${mount.hostPath} violates class ${mount.class} scope

Error message

mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}

What it means

The mount fails the class-scope check in mountAllowed: its class plus groupScope is not permitted for this spec and policy. Classes constrain where a hostPath may live and which sessions/groups may use it (e.g. group-scoped surfaces must stay under that group's root), so a mount whose scope does not cover its actual path or spec is denied.

Source

Thrown at src/drivers/types.ts:475

        // 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
      // to overwrite is the pattern working as intended, and the name check
      // alone denies every such install. Credential VALUES have no sanctioned
      // channel, from anyone: real material rides mounts by reference.
      if (looksLikeCredential(value)) {
        throw deniedByPolicy(`credential value in contributed env '${key}' on ${container.role}`);
      }
    }
  }

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Check the mount's class and groupScope against the policy roots: point hostPath inside the root the class+scope allows (e.g. your own group's root for group-scoped classes).
  2. If the path is intentionally shared, use the class whose scope covers it (such as a policy-allowlisted path) rather than widening a group scope.
  3. After moving group directories or changing policy roots, regenerate the spec so scopes and paths agree.

Example fix

// before
{ hostPath: '/groups/other-group/workspace', containerPath: '/workspace', class: 'group-surface', groupScope: 'my-group', mode: 'rw' }

// after
{ hostPath: '/groups/my-group/workspace', containerPath: '/workspace', class: 'group-surface', groupScope: 'my-group', mode: 'rw' }
Defensive patterns

Strategy: validation

Validate before calling

// Before prepare(), assert each mount's hostPath sits under a root its class+scope permits:
for (const c of spec.containers)
  for (const m of c.mounts)
    if (!allowedRootsFor(m.class, m.groupScope, policy).some(r => m.hostPath === r || m.hostPath.startsWith(r + '/')))
      throw new Error(`mount outside class scope: ${m.hostPath}`);

Type guard

function underRoot(p: string, root: string): boolean {
  return p === root || p.startsWith(root.endsWith('/') ? root : root + '/');
}

Prevention

When it happens

Trigger: A mount declared with a groupScope that does not match the session's group, or a hostPath outside the root its class+scope permits — e.g. an 'allowlisted-extra' path that is not on the allowlist, or a group-scoped mount pointing at another group's directory. Emitted by validateSpec via mountAllowed(mount, spec, policy).

Common situations: Renaming/moving group folders so paths no longer sit under the declared group root; cross-group mounts attempted by composing specs by hand; policy root changes after an upgrade; stale groupScope values in persisted specs.

Related errors


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