66
77import { mkdirSync , writeFileSync , chmodSync , chownSync , rmSync } from 'node:fs' ;
88import { join } from 'node:path' ;
9+ import { validateHostname } from '../secrets/manager.js' ;
910
1011/**
1112 * Try to change file ownership, but gracefully skip if not permitted.
1213 * chown requires root privileges; in CI/dev environments we may not have them.
1314 */
14- function tryChown ( path : string , uid : number , gid : number ) : void {
15+ function tryChown ( path : string , uid : number , gid : number ) : boolean {
1516 try {
1617 chownSync ( path , uid , gid ) ;
18+ return true ;
1719 } catch ( err ) {
18- if ( ( err as NodeJS . ErrnoException ) . code === 'EPERM' ) {
19- // Not running as root - skip chown (acceptable in dev/CI)
20- return ;
21- }
20+ if ( ( err as NodeJS . ErrnoException ) . code === 'EPERM' ) return false ;
2221 throw err ;
2322 }
2423}
2524
25+ const OPENCLAW_UID = 1000 ;
26+ const OPENCLAW_GID = 1000 ;
27+
28+ function setOwnership ( path : string , mode : number ) : void {
29+ const owned = tryChown ( path , OPENCLAW_UID , OPENCLAW_GID ) ;
30+ // If chown failed, widen permissions so container UID 1000 can still write
31+ chmodSync ( path , owned ? mode : mode | 0o022 ) ;
32+ }
33+
2634export interface BotPersona {
2735 name : string ;
2836 identity : string ;
@@ -257,51 +265,25 @@ function generateIdentityMd(persona: BotPersona): string {
257265export function createBotWorkspace ( dataDir : string , config : BotWorkspaceConfig ) : void {
258266 const botDir = join ( dataDir , 'bots' , config . botHostname ) ;
259267 const workspaceDir = join ( botDir , 'workspace' ) ;
268+ const agentDir = join ( botDir , 'agents' , 'main' , 'agent' ) ;
269+ const sessionsDir = join ( botDir , 'agents' , 'main' , 'sessions' ) ;
270+ const sandboxDir = join ( botDir , 'sandbox' ) ;
260271
261- // Create directories with permissions for bot container (runs as uid 1000)
262- mkdirSync ( botDir , { recursive : true , mode : 0o777 } ) ;
263- mkdirSync ( workspaceDir , { recursive : true , mode : 0o777 } ) ;
264- // Ensure parent dir has correct permissions (recursive: true doesn't set mode on existing dirs)
265- chmodSync ( botDir , 0o777 ) ;
266- chmodSync ( workspaceDir , 0o777 ) ;
272+ for ( const dir of [ botDir , workspaceDir , agentDir , sessionsDir , sandboxDir ] ) {
273+ mkdirSync ( dir , { recursive : true , mode : 0o755 } ) ;
274+ setOwnership ( dir , 0o755 ) ;
275+ }
267276
268- // Write openclaw.json at root of bot directory (OPENCLAW_STATE_DIR)
269- const openclawConfig = generateOpenclawConfig ( config ) ;
270277 const configPath = join ( botDir , 'openclaw.json' ) ;
271- writeFileSync ( configPath , JSON . stringify ( openclawConfig , null , 2 ) ) ;
272- chmodSync ( configPath , 0o666 ) ;
278+ writeFileSync ( configPath , JSON . stringify ( generateOpenclawConfig ( config ) , null , 2 ) ) ;
279+ setOwnership ( configPath , 0o644 ) ;
273280
274- // Write only persona files — OpenClaw's ensureAgentWorkspace() will create
275- // AGENTS.md, BOOTSTRAP.md, TOOLS.md, HEARTBEAT.md from its own templates
276- // (using writeFileIfMissing / wx flag, so our files won't be overwritten).
277281 const soulPath = join ( workspaceDir , 'SOUL.md' ) ;
278282 const identityPath = join ( workspaceDir , 'IDENTITY.md' ) ;
279283 writeFileSync ( soulPath , generateSoulMd ( config . persona ) ) ;
280284 writeFileSync ( identityPath , generateIdentityMd ( config . persona ) ) ;
281- chmodSync ( soulPath , 0o666 ) ;
282- chmodSync ( identityPath , 0o666 ) ;
283-
284- // OpenClaw runs as uid 1000 (node user), so we need to set ownership
285- const OPENCLAW_UID = 1000 ;
286- const OPENCLAW_GID = 1000 ;
287-
288- const agentDir = join ( botDir , 'agents' , 'main' , 'agent' ) ;
289- mkdirSync ( agentDir , { recursive : true , mode : 0o777 } ) ;
290- chmodSync ( agentDir , 0o777 ) ;
291- tryChown ( agentDir , OPENCLAW_UID , OPENCLAW_GID ) ;
292-
293- // Pre-create sessions directory for OpenClaw runtime use
294- const sessionsDir = join ( botDir , 'agents' , 'main' , 'sessions' ) ;
295- mkdirSync ( sessionsDir , { recursive : true , mode : 0o777 } ) ;
296- chmodSync ( sessionsDir , 0o777 ) ;
297- tryChown ( sessionsDir , OPENCLAW_UID , OPENCLAW_GID ) ;
298-
299- // Pre-create sandbox directory for OpenClaw code execution
300- // OpenClaw hardcodes /app/workspace for sandbox operations
301- const sandboxDir = join ( botDir , 'sandbox' ) ;
302- mkdirSync ( sandboxDir , { recursive : true , mode : 0o777 } ) ;
303- chmodSync ( sandboxDir , 0o777 ) ;
304- tryChown ( sandboxDir , OPENCLAW_UID , OPENCLAW_GID ) ;
285+ setOwnership ( soulPath , 0o644 ) ;
286+ setOwnership ( identityPath , 0o644 ) ;
305287}
306288
307289/**
@@ -323,6 +305,7 @@ export function getBotWorkspacePath(dataDir: string, hostname: string): string {
323305 * @param hostname - Bot hostname
324306 */
325307export function deleteBotWorkspace ( dataDir : string , hostname : string ) : void {
308+ validateHostname ( hostname ) ;
326309 const botDir = join ( dataDir , 'bots' , hostname ) ;
327310 rmSync ( botDir , { recursive : true , force : true } ) ;
328311}
0 commit comments