{"record":{"id":"297c802e91123e7d","repo":"nanocoai/nanoclaw","slug":"identity-material-mount-mount-hostpath-invalid","errorCode":null,"errorMessage":"identity-material mount ${mount.hostPath} invalid on role ${container.role}","messagePattern":"identity-material mount (.+?) invalid on role (.+?)","errorType":"validation","errorClass":"deniedByPolicy","httpStatus":null,"severity":"critical","filePath":"src/drivers/types.ts","lineNumber":472,"sourceCode":"      if (required && mount.class !== required) {\n        // Where a file lives decides what it IS, so the class is not the\n        // composer's to choose for these roots. Without this the taxonomy is\n        // 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}`);","sourceCodeStart":454,"sourceCodeEnd":490,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/drivers/types.ts#L454-L490","documentation":"A mount classified 'identity-material' (session private keys and similar credentials-by-reference) violates the no-credentials invariant: either its mode is not 'ro', or it is attached to the container with role 'agent'. Identity materials are ro-only and never enter the agent container — the agent is the untrusted party the material authenticates.","triggerScenarios":"validateSpec sees an 'identity-material' mount with mode 'rw', or any 'identity-material' mount on the container whose role is 'agent'. E.g. wiring a proxy credential mount onto the agent container because both containers were built from one shared mount list.","commonSituations":"Sharing a single mounts array between the agent container and a proxy/sidecar container; debugging by mounting credentials everywhere; refactors that changed container roles so a mount previously on the proxy now sits on the agent.","solutions":["Move the identity-material mount off the agent container — it belongs only on non-agent roles (e.g. the proxy) that need to present the credential.","Ensure its mode is 'ro'.","If the agent needs to authenticate, route it through the proxy that holds the material, never mount the material directly."],"exampleFix":"// before (agent container)\nagentMounts: [{ hostPath: mats + '/session-key.pem', containerPath: '/creds/key.pem', class: 'identity-material', mode: 'ro' }]\n\n// after (only on the proxy container)\nproxyMounts: [{ hostPath: mats + '/session-key.pem', containerPath: '/creds/key.pem', class: 'identity-material', mode: 'ro' }]","handlingStrategy":"validation","validationCode":"for (const c of spec.containers) {\n  if (c.role === 'agent')\n    for (const m of c.mounts)\n      if (m.class === 'identity-material') throw new Error(`identity-material on agent: ${m.hostPath}`);\n  for (const m of c.mounts)\n    if (m.class === 'identity-material' && m.mode !== 'ro') throw new Error(`identity-material must be ro: ${m.hostPath}`);\n}","typeGuard":"function identityMaterialOk(m: MountSpec, role: string): boolean {\n  return m.class !== 'identity-material' || (m.mode === 'ro' && role !== 'agent');\n}","tryCatchPattern":null,"preventionTips":["Build agent-container mounts from a dedicated list; never share the proxy's mount array.","Treat 'identity-material' as proxy-only by convention and lint for it on agent containers.","Authenticate the agent via the proxy that holds the credential, not by direct mount."],"tags":["mount","security","credentials","policy"],"backgroundTag":"credential-mounted-into-agent","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}