{"record":{"id":"297f9bca16e216be","repo":"nanocoai/nanoclaw","slug":"mount-mount-hostpath-violates-class-mount-cla","errorCode":null,"errorMessage":"mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}","messagePattern":"mount (.+?) violates class (.+?) scope (.+?)","errorType":"validation","errorClass":"deniedByPolicy","httpStatus":null,"severity":"error","filePath":"src/drivers/types.ts","lineNumber":475,"sourceCode":"        // only as strong as whoever assigns the class, and two of the four\n        // classes carry safety properties that a demotion silently drops:\n        // `allowlisted-extra` is permitted unconditionally, so relabelling a\n        // session private key as one mounts it INTO THE AGENT — defeating the\n        // no-credentials invariant outright — and relabelling the runner source\n        // as one escapes the read-only rule on the code the agent executes.\n        // Neither is exotic: both are a single word in a mount literal.\n        throw deniedByPolicy(`mount ${mount.hostPath} must be classed ${required}, not ${mount.class}`);\n      }\n      if (mount.class === 'install-surface' && mount.mode !== 'ro') {\n        throw deniedByPolicy(`install-surface mount ${mount.hostPath} must be ro`);\n      }\n      if (mount.class === 'identity-material' && (mount.mode !== 'ro' || container.role === 'agent')) {\n        // The no-credentials invariant, as a checkable rule: identity materials\n        // are ro-only and never enter the agent container.\n        throw deniedByPolicy(`identity-material mount ${mount.hostPath} invalid on role ${container.role}`);\n      }\n      if (!mountAllowed(mount, spec, policy)) {\n        throw deniedByPolicy(`mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}`);\n      }\n    }\n    for (const [key, value] of Object.entries(container.env)) {\n      if (isSecretShaped(key, value)) {\n        throw deniedByPolicy(`secret-shaped env '${key}' on ${container.role}`);\n      }\n    }\n    for (const [key, value] of Object.entries(container.contributedEnv ?? {})) {\n      // The sanctioned lane: credential-shaped NAMES are its purpose — a\n      // provider registering `ANTHROPIC_AUTH_TOKEN=placeholder` for the proxy\n      // to overwrite is the pattern working as intended, and the name check\n      // alone denies every such install. Credential VALUES have no sanctioned\n      // channel, from anyone: real material rides mounts by reference.\n      if (looksLikeCredential(value)) {\n        throw deniedByPolicy(`credential value in contributed env '${key}' on ${container.role}`);\n      }\n    }\n  }","sourceCodeStart":457,"sourceCodeEnd":493,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/drivers/types.ts#L457-L493","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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).","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.","After moving group directories or changing policy roots, regenerate the spec so scopes and paths agree."],"exampleFix":"// before\n{ hostPath: '/groups/other-group/workspace', containerPath: '/workspace', class: 'group-surface', groupScope: 'my-group', mode: 'rw' }\n\n// after\n{ hostPath: '/groups/my-group/workspace', containerPath: '/workspace', class: 'group-surface', groupScope: 'my-group', mode: 'rw' }","handlingStrategy":"validation","validationCode":"// Before prepare(), assert each mount's hostPath sits under a root its class+scope permits:\nfor (const c of spec.containers)\n  for (const m of c.mounts)\n    if (!allowedRootsFor(m.class, m.groupScope, policy).some(r => m.hostPath === r || m.hostPath.startsWith(r + '/')))\n      throw new Error(`mount outside class scope: ${m.hostPath}`);","typeGuard":"function underRoot(p: string, root: string): boolean {\n  return p === root || p.startsWith(root.endsWith('/') ? root : root + '/');\n}","tryCatchPattern":null,"preventionTips":["Derive hostPaths from the group/policy roots at build time instead of hardcoding.","After moving directories or changing policy roots, regenerate specs rather than reusing persisted ones.","Keep a unit test asserting every shipped spec passes validateSpec against the current policy."],"tags":["mount","policy","scope","security"],"backgroundTag":"mount-outside-allowed-scope","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}