#!/usr/bin/env bash set -euo pipefail # Generates a sanitized JSON snapshot of an OpenClaw container deployment. # Safe-by-default: excludes environment values, secrets, mounts with secret-looking # paths, and full labels/annotations. Intended for sharing with the agent. # # Usage: # ./export-openclaw-host-facts.sh [output.json] # Example: # ./export-openclaw-host-facts.sh openclaw ./openclaw-host-facts.json CONTAINER="${1:-}" OUT="${2:-./openclaw-host-facts.json}" if [[ -z "$CONTAINER" ]]; then echo "Usage: $0 [output.json]" >&2 exit 1 fi if ! command -v docker >/dev/null 2>&1; then echo "docker not found" >&2 exit 1 fi TMP=$(mktemp) trap 'rm -f "$TMP"' EXIT docker inspect "$CONTAINER" > "$TMP" node - <<'NODE' "$TMP" "$OUT" const fs = require('fs'); const inFile = process.argv[2]; const outFile = process.argv[3]; const data = JSON.parse(fs.readFileSync(inFile, 'utf8')); if (!Array.isArray(data) || data.length === 0) { throw new Error('No inspect data found'); } const c = data[0]; const secretish = /(secret|token|key|passwd|password|cookie|session|credential|auth|oauth|api[-_]?key|webhook)/i; const sensitivePath = /(secret|secrets|private|\.ssh|gnupg|aws|gcloud|kube|\.env|credentials?)/i; function uniq(arr) { return [...new Set(arr.filter(Boolean))]; } function safeMounts(mounts) { return (mounts || []) .filter(m => !(sensitivePath.test(m.Source || '') || sensitivePath.test(m.Destination || ''))) .map(m => ({ type: m.Type, source: m.Source, destination: m.Destination, mode: m.Mode || '', rw: !!m.RW, })); } function safeEnv(env) { const out = {}; for (const entry of env || []) { const idx = entry.indexOf('='); const key = idx === -1 ? entry : entry.slice(0, idx); const value = idx === -1 ? '' : entry.slice(idx + 1); if (secretish.test(key)) continue; // Keep only clearly non-sensitive runtime facts. if (/^(NODE_ENV|TZ|HOSTNAME|OPENCLAW_VARIANT|OPENCLAW_INSTALL_BROWSER|OPENCLAW_INSTALL_DOCKER_CLI)$/i.test(key)) { out[key] = value; } else { out[key] = ''; } } return out; } const networkSettings = c.NetworkSettings || {}; const hostConfig = c.HostConfig || {}; const config = c.Config || {}; const ports = []; for (const [containerPort, bindings] of Object.entries(networkSettings.Ports || {})) { if (!bindings) { ports.push({ container: containerPort, published: null }); continue; } for (const b of bindings) { ports.push({ container: containerPort, hostIp: b.HostIp, hostPort: b.HostPort, }); } } const summary = { generatedAt: new Date().toISOString(), source: 'docker inspect (sanitized)', deploymentHint: 'komodo-or-docker', container: { name: (c.Name || '').replace(/^\//, ''), idShort: (c.Id || '').slice(0, 12), image: config.Image || null, entrypoint: config.Entrypoint || null, cmd: config.Cmd || null, user: config.User || null, workingDir: config.WorkingDir || null, }, runtime: { running: c.State?.Running || false, status: c.State?.Status || null, startedAt: c.State?.StartedAt || null, restartPolicy: hostConfig.RestartPolicy?.Name || null, privileged: !!hostConfig.Privileged, networkMode: hostConfig.NetworkMode || null, pidMode: hostConfig.PidMode || null, ipcMode: hostConfig.IpcMode || null, readOnlyRootfs: !!hostConfig.ReadonlyRootfs, }, ports, mounts: safeMounts(c.Mounts), networks: Object.keys(networkSettings.Networks || {}), aliases: uniq(Object.values(networkSettings.Networks || {}).flatMap(n => n.Aliases || [])), env: safeEnv(config.Env), labels: Object.fromEntries( Object.entries(config.Labels || {}).filter(([k]) => { const lk = String(k).toLowerCase(); return lk.startsWith('com.komodo.') || lk.startsWith('komodo.') || lk.startsWith('com.docker.compose.'); }) ), }; fs.writeFileSync(outFile, JSON.stringify(summary, null, 2)); console.log(`Wrote sanitized facts to ${outFile}`); NODE