{"record":{"id":"e8da9e0e98779d80","repo":"nanocoai/nanoclaw","slug":"install-surface-mount-mount-hostpath-must-be-ro","errorCode":null,"errorMessage":"install-surface mount ${mount.hostPath} must be ro","messagePattern":"install-surface mount (.+?) must be ro","errorType":"validation","errorClass":"deniedByPolicy","httpStatus":null,"severity":"critical","filePath":"src/drivers/types.ts","lineNumber":467,"sourceCode":"      }\n      seenTargets.add(mount.containerPath);\n      const required =\n        classRequiredByPath(mount.hostPath, policy) ??\n        (pluginsRoot && underRoot(mount.hostPath, pluginsRoot) ? 'install-surface' : null);\n      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","sourceCodeStart":449,"sourceCodeEnd":485,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/drivers/types.ts#L449-L485","documentation":"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.","triggerScenarios":"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.","commonSituations":"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'.","solutions":["Change the mount's mode to 'ro' as the message states.","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.","Audit spec generators/templates so 'install-surface' entries always emit mode 'ro'."],"exampleFix":"// before\n{ hostPath: '/opt/nanoclaw/runner', containerPath: '/runner', class: 'install-surface', mode: 'rw' }\n\n// after\n{ hostPath: '/opt/nanoclaw/runner', containerPath: '/runner', class: 'install-surface', mode: 'ro' }","handlingStrategy":"type-guard","validationCode":"const bad = spec.containers.flatMap(c => c.mounts.filter(m => m.class === 'install-surface' && m.mode !== 'ro'));\nif (bad.length) throw new Error(`install-surface mounts must be ro: ${bad.map(m => m.hostPath).join(', ')}`);","typeGuard":"function isReadOnlyInstallSurface(m: MountSpec): boolean {\n  return m.class !== 'install-surface' || m.mode === 'ro';\n}","tryCatchPattern":null,"preventionTips":["Make spec builders default mode to 'ro' and require an explicit opt-in for 'rw'.","Code-review any 'rw' mount whose hostPath touches runner/plugin source.","Keep writable scratch on a separate containerPath from install surfaces."],"tags":["mount","security","read-only","policy"],"backgroundTag":"writable-install-surface","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}