# OpenClaw Helm Chart Values
#
# IMPORTANT ARCHITECTURE NOTES:
# - OpenClaw is NOT horizontally scalable
# - Deploy as a SINGLE INSTANCE only
# - Do NOT increase replica count
#
app-template:
  # @schema type:string;required:true
  # -- OpenClaw image version (used by all OpenClaw containers)
  openclawVersion: "2026.4.1"
  # @schema type:string
  # -- Chromium sidecar image version
  chromiumVersion: "147.0.7727.24"
  # @schema type:string;enum:[merge, overwrite];default:merge
  # -- Config mode: `merge` preserves runtime changes, `overwrite` for strict GitOps
  configMode: merge
  # @schema type:array
  # -- ClawHub skills to install on startup. Set to [] to skip skill installation.
  skills:
    - weather
    - gog

  defaultPodOptions:
    # -- Pod security context
    securityContext:
      # @schema type:integer;default:1000
      fsGroup: 1000
      # @schema type:string;enum:[Always, OnRootMismatch];default:OnRootMismatch
      fsGroupChangePolicy: OnRootMismatch

  controllers:
    main:
      # @schema type:integer;minimum:1;maximum:1;default:1
      # -- Number of replicas (must be 1, OpenClaw doesn't support horizontal scaling)
      replicas: 1
      # @schema type:string;enum:[Recreate, RollingUpdate];default:Recreate
      # -- Deployment strategy
      strategy: Recreate

      initContainers:
        # Init container handles config setup with merge/overwrite modes.
        # Uses OpenClaw image for Node.js runtime (required for merge mode).
        init-config:
          image:
            # @schema type:string;default:ghcr.io/openclaw/openclaw
            #repository: ghcr.io/openclaw/openclaw
            repository: registry.cn-shenzhen.aliyuncs.com/shuhui/openclaw
            # @schema type:string
            tag: "{{ .Values.openclawVersion }}"
          # -- Init-config startup script
          command:
            - sh
            - /scripts/init-config.sh
          env:
            CONFIG_MODE: "{{ .Values.configMode | default \"merge\" }}"
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
            runAsNonRoot: true
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL

        # Install ClawHub skills and runtime dependencies (idempotent)
        # Skills are installed from https://clawhub.com
        init-skills:
          image:
            # @schema type:string;default:ghcr.io/openclaw/openclaw
            #repository: ghcr.io/openclaw/openclaw
            repository: registry.cn-shenzhen.aliyuncs.com/shuhui/openclaw
            # @schema type:string
            tag: "{{ .Values.openclawVersion }}"
          # -- Init-skills startup script
          command:
            - sh
            - /scripts/init-skills.sh
          env:
            # @schema type:string
            SKILLS_TO_INSTALL: '{{ .Values.skills | join " " }}'
            HOME: /tmp
            NPM_CONFIG_CACHE: /tmp/.npm
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
            runAsNonRoot: true
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL

      containers:
        # -- Main OpenClaw container
        main:
          image:
            # @schema type:string;default:ghcr.io/openclaw/openclaw
            # -- Container image repository
            repository: ghcr.io/openclaw/openclaw
            # @schema type:string
            # -- Container image tag
            tag: "{{ .Values.openclawVersion }}"
            # @schema type:string;enum:[Always, IfNotPresent, Never];default:IfNotPresent
            # -- Image pull policy
            pullPolicy: IfNotPresent
          command:
            - node
            - dist/index.js
          args:
            - gateway
            - --bind
            - lan
            - --port
            - "18789"
          # Reference a secret containing environment variables (e.g., API keys)
          # Create this secret manually or via external-secrets/vault-operator
          envFrom: []
          # Example:
          # - secretRef:
          #     name: openclaw-env-secret
          env: {}
          # Example: Add runtime dependency paths for skills and extra runtimes
          # env:
          #   PATH: /home/node/.openclaw/pnpm:/home/node/.openclaw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
          #   PNPM_HOME: /home/node/.openclaw/pnpm
          #   PNPM_STORE_DIR: /home/node/.openclaw/.pnpm-store
          #   # For internal CA trust (Python requests library)
          #   REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-bundle.crt
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
            runAsNonRoot: true
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          # -- Resource requests and limits
          resources:
            requests:
              # @schema type:string;default:200m
              cpu: 200m
              # @schema type:string;default:512Mi
              memory: 512Mi
            limits:
              # @schema type:string;default:2000m
              cpu: 2000m
              # @schema type:string;default:2Gi
              memory: 2Gi
          probes:
            liveness:
              # @schema type:boolean;default:true
              enabled: true
              # @schema type:string;default:TCP
              type: TCP
              spec:
                tcpSocket:
                  # @schema type:integer;default:18789
                  port: 18789
                # @schema type:integer;default:30
                initialDelaySeconds: 30
                # @schema type:integer;default:30
                periodSeconds: 30
                # @schema type:integer;default:5
                timeoutSeconds: 5
                # @schema type:integer;default:3
                failureThreshold: 3
            readiness:
              # @schema type:boolean;default:true
              enabled: true
              # @schema type:string;default:TCP
              type: TCP
              spec:
                tcpSocket:
                  # @schema type:integer;default:18789
                  port: 18789
                # @schema type:integer;default:10
                initialDelaySeconds: 10
                # @schema type:integer;default:10
                periodSeconds: 10
                # @schema type:integer;default:5
                timeoutSeconds: 5
                # @schema type:integer;default:3
                failureThreshold: 3
            startup:
              # @schema type:boolean;default:true
              enabled: true
              # @schema type:string;default:TCP
              type: TCP
              spec:
                tcpSocket:
                  # @schema type:integer;default:18789
                  port: 18789
                # @schema type:integer;default:5
                initialDelaySeconds: 5
                # @schema type:integer;default:5
                periodSeconds: 5
                # @schema type:integer;default:5
                timeoutSeconds: 5
                # @schema type:integer;default:30
                failureThreshold: 30

        # -- Chromium sidecar for browser automation (CDP on port 9222)
        chromium:
          # @schema type:boolean;default:true
          # -- Enable/disable the Chromium browser sidecar
          enabled: true
          image:
            # @schema type:string;default:chromedp/headless-shell
            # -- Chromium image repository
            repository: chromedp/headless-shell
            # @schema type:string
            # -- Chromium image tag
            tag: "{{ .Values.chromiumVersion }}"
          env:
            # Fontconfig cache needs a writable directory
            XDG_CACHE_HOME: /tmp
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
            runAsNonRoot: true
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          resources:
            requests:
              # @schema type:string;default:100m
              cpu: 100m
              # @schema type:string;default:256Mi
              memory: 256Mi
            limits:
              # @schema type:string;default:1000m
              cpu: 1000m
              # @schema type:string;default:1Gi
              memory: 1Gi
          probes:
            liveness:
              # @schema type:boolean;default:true
              enabled: true
              # @schema type:boolean;default:true
              custom: true
              spec:
                httpGet:
                  # @schema type:string;default:/json/version
                  path: /json/version
                  # @schema type:integer;default:9222
                  port: 9222
                # @schema type:integer;default:10
                initialDelaySeconds: 10
                # @schema type:integer;default:30
                periodSeconds: 30
                # @schema type:integer;default:5
                timeoutSeconds: 5
                # @schema type:integer;default:6
                failureThreshold: 6
            readiness:
              # @schema type:boolean;default:true
              enabled: true
              # @schema type:boolean;default:true
              custom: true
              spec:
                httpGet:
                  # @schema type:string;default:/json/version
                  path: /json/version
                  # @schema type:integer;default:9222
                  port: 9222
                # @schema type:integer;default:5
                initialDelaySeconds: 5
                # @schema type:integer;default:10
                periodSeconds: 10
            startup:
              # @schema type:boolean;default:true
              enabled: true
              # @schema type:boolean;default:true
              custom: true
              spec:
                httpGet:
                  # @schema type:string;default:/json/version
                  path: /json/version
                  # @schema type:integer;default:9222
                  port: 9222
                # @schema type:integer;default:5
                initialDelaySeconds: 5
                # @schema type:integer;default:5
                periodSeconds: 5
                # @schema type:integer;default:5
                timeoutSeconds: 5
                # @schema type:integer;default:12
                failureThreshold: 12

  configMaps:
    scripts:
      enabled: true
      data:
        init-config.sh: |
          log() { echo "[$(date -Iseconds)] [init-config] $*"; }

          log "Starting config initialization"
          mkdir -p /home/node/.openclaw
          CONFIG_MODE="${CONFIG_MODE:-merge}"

          if [ "$CONFIG_MODE" = "merge" ] && [ -f /home/node/.openclaw/openclaw.json ]; then
            log "Mode: merge - merging Helm config with existing config"
            if node -e "
              const fs = require('fs');
              // Strip JSON5 single-line comments while preserving // inside strings (e.g. URLs)
              const stripComments = (s) => {
                let r = '', q = false, i = 0;
                while (i < s.length) {
                  if (q) {
                    if (s[i] === '\\\\') { r += s[i] + s[i+1]; i += 2; continue; }
                    if (s[i] === '\"') q = false;
                    r += s[i++];
                  } else if (s[i] === '\"') {
                    q = true; r += s[i++];
                  } else if (s[i] === '/' && s[i+1] === '/') {
                    while (i < s.length && s[i] !== '\n') i++;
                  } else { r += s[i++]; }
                }
                return r;
              };
              let existing;
              try {
                existing = JSON.parse(stripComments(fs.readFileSync('/home/node/.openclaw/openclaw.json', 'utf8')));
              } catch (e) {
                console.error('[init-config] Warning: existing config is not valid JSON, will overwrite');
                process.exit(1);
              }
              const helm = JSON.parse(stripComments(fs.readFileSync('/config/openclaw.json', 'utf8')));
              const deepMerge = (target, source) => {
                for (const key of Object.keys(source)) {
                  if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
                    target[key] = target[key] || {};
                    deepMerge(target[key], source[key]);
                  } else {
                    target[key] = source[key];
                  }
                }
                return target;
              };
              const merged = deepMerge(existing, helm);
              fs.writeFileSync('/home/node/.openclaw/openclaw.json', JSON.stringify(merged, null, 2));
            "; then
              log "Config merged successfully"
            else
              log "WARNING: Merge failed (existing config may not be valid JSON), falling back to overwrite"
              cp /config/openclaw.json /home/node/.openclaw/openclaw.json
            fi
          else
            if [ ! -f /home/node/.openclaw/openclaw.json ]; then
              log "Fresh install - writing initial config"
            else
              log "Mode: overwrite - replacing config with Helm values"
            fi
            cp /config/openclaw.json /home/node/.openclaw/openclaw.json
          fi
          log "Config initialization complete"

        init-skills.sh: |
          log() { echo "[$(date -Iseconds)] [init-skills] $*"; }

          log "Starting skills initialization"

          # ============================================================
          # Runtime Dependencies
          # ============================================================
          # Some skills require additional runtimes (Python, Go, etc.)
          # Install them here so they persist across pod restarts.
          #
          # Example: Install uv (Python package manager) for Python skills
          # mkdir -p /home/node/.openclaw/bin
          # if [ ! -f /home/node/.openclaw/bin/uv ]; then
          #   log "Installing uv..."
          #   curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/home/node/.openclaw/bin sh
          # fi
          #
          # Example: Install pnpm and packages for interfaces (e.g., MS Teams)
          # The read-only filesystem and non-root UID prevent writing to default
          # pnpm paths (/usr/local/lib/node_modules, ~/.local/share/pnpm, etc.).
          # Redirect PNPM_HOME to the PVC so the binary persists across restarts.
          # The init container's HOME=/tmp ensures pnpm's cache, state, and config
          # writes land on /tmp (writable emptyDir). The store goes on the PVC so
          # hardlinks work (same filesystem as node_modules) and persist.
          # PNPM_HOME=/home/node/.openclaw/pnpm
          # mkdir -p "$PNPM_HOME"
          # if [ ! -f "$PNPM_HOME/pnpm" ]; then
          #   log "Installing pnpm..."
          #   curl -fsSL https://get.pnpm.io/install.sh | env PNPM_HOME="$PNPM_HOME" SHELL=/bin/sh sh -
          # fi
          # export PATH="$PNPM_HOME:$PATH"
          # log "Installing interface dependencies..."
          # cd /home/node/.openclaw
          # pnpm install <your-package> --store-dir /home/node/.openclaw/.pnpm-store

          # ============================================================
          # Skill Installation
          # ============================================================
          # Installs skills listed in SKILLS_TO_INSTALL (set via the top-level skills: [] value).
          # Set skills: [] in your values to skip skill installation entirely.
          mkdir -p /home/node/.openclaw/workspace/skills
          cd /home/node/.openclaw/workspace
          install_skill() {
            skill="$1"
            SKILL_NAME="$(basename "$skill")"
            if [ -z "$skill" ]; then
              return 0
            fi
            if [ -d "skills/$SKILL_NAME" ]; then
              log "Skill already installed: $skill"
              return 0
            fi
            log "Installing skill: $skill"
            attempts=6
            delay=5
            for i in $(seq 1 "$attempts"); do
              if npx -y clawhub install "$skill" --no-input; then
                log "Installed skill: $skill"
                return 0
              fi
              # Add a little jitter so concurrent pods don't retry in lockstep.
              jitter=$((RANDOM % 5))
              if [ "$i" -lt "$attempts" ]; then
                log "WARNING: Failed to install skill: $skill (attempt $i/$attempts). Retrying in $((delay + jitter))s..."
                sleep $((delay + jitter))
                delay=$((delay * 2))
              fi
            done
            log "WARNING: Failed to install skill after $attempts attempts: $skill"
            return 1
          }
          for skill in $SKILLS_TO_INSTALL; do
            install_skill "$skill" || true
          done
          log "Skills initialization complete"

    config:
      # @schema type:boolean;default:true
      enabled: true
      data:
        # Shell alias for interactive sessions (sourced by ~/.bashrc)
        # @schema type:string
        bash_aliases: |
          alias openclaw='node /app/dist/index.js'
        # OpenClaw configuration - JSON5 format
        # Sensitive values use ${ENV_VAR} substitution from environment secret
        # @schema type:string
        openclaw.json: |
          {
            // Gateway configuration
            "gateway": {
              "port": 18789,
              "mode": "local",
              // IMPORTANT: trustedProxies uses exact IP matching only
              // - CIDR notation is NOT supported - list each proxy IP individually
              // - IPv6 exact addresses may work but are untested
              // - Recommend single-stack IPv4 deployments for simplicity
              "trustedProxies": ["10.0.0.1"]
            },

            // Browser configuration (Chromium sidecar)
            "browser": {
              "enabled": true,
              "defaultProfile": "default",
              "profiles": {
                "default": {
                  "cdpUrl": "http://localhost:9222",
                  "color": "#4285F4"
                }
              }
            },

            // Agent configuration
            "agents": {
              "defaults": {
                "workspace": "/home/node/.openclaw/workspace",
                "model": {
                  // Uses ANTHROPIC_API_KEY from environment
                  "primary": "anthropic/claude-opus-4-6"
                },
                "userTimezone": "UTC",
                "timeoutSeconds": 600,
                "maxConcurrent": 1
              },
              "list": [
                {
                  "id": "main",
                  "default": true,
                  "identity": {
                    "name": "OpenClaw",
                    "emoji": "🦞"
                  }
                }
              ]
            },

            // Session management
            "session": {
              "scope": "per-sender",
              "store": "/home/node/.openclaw/sessions",
              "reset": {
                "mode": "idle",
                "idleMinutes": 60
              }
            },

            // Logging
            "logging": {
              "level": "info",
              "consoleLevel": "info",
              "consoleStyle": "compact",
              "redactSensitive": "tools"
            },

            // Tools configuration
            "tools": {
              "profile": "full",
              "web": {
                "search": {
                  "enabled": false
                },
                "fetch": {
                  "enabled": true
                }
              }
            }

            // Channel configuration can be added here:
            // "channels": {
            //   "telegram": {
            //     "botToken": "${TELEGRAM_BOT_TOKEN}",
            //     "enabled": true
            //   },
            //   "discord": {
            //     "token": "${DISCORD_BOT_TOKEN}"
            //   },
            //   "slack": {
            //     "botToken": "${SLACK_BOT_TOKEN}",
            //     "appToken": "${SLACK_APP_TOKEN}"
            //   }
            // }
          }

  service:
    main:
      controller: main
      # @schema type:string;enum:[SingleStack, PreferDualStack, RequireDualStack];default:SingleStack
      # -- IPv4-only (see trustedProxies note in gateway config)
      ipFamilyPolicy: SingleStack
      ipFamilies:
        - IPv4
      ports:
        http:
          # @schema type:integer;default:18789
          # -- Gateway service port
          port: 18789

  ingress:
    main:
      # @schema type:boolean;default:false
      # -- Enable ingress resource creation
      enabled: false
      # Uncomment and configure for your environment
      # className: nginx
      # annotations:
      #   cert-manager.io/cluster-issuer: "your-issuer"
      #   nginx.ingress.kubernetes.io/ssl-redirect: "true"
      #   nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
      #   nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
      # tls:
      #   - secretName: openclaw-tls
      #     hosts:
      #       - openclaw.example.com
      # hosts:
      #   - host: openclaw.example.com
      #     paths:
      #       - path: /
      #         pathType: Prefix
      #         service:
      #           identifier: main
      #           port: http

  persistence:
    # Init scripts ConfigMap mounted into both init containers
    scripts:
      enabled: true
      type: configMap
      identifier: scripts
      advancedMounts:
        main:
          init-config:
            - path: /scripts
              readOnly: true
          init-skills:
            - path: /scripts
              readOnly: true

    # ConfigMap for init container to copy from
    config:
      # @schema type:boolean;default:true
      enabled: true
      # @schema type:string;default:configMap
      type: configMap
      # @schema type:string;default:config
      identifier: config
      advancedMounts:
        main:
          init-config:
            - path: /config
              readOnly: true

    # Shell alias file mounted from ConfigMap for interactive kubectl exec sessions
    bash-aliases:
      enabled: true
      type: configMap
      identifier: config
      advancedMounts:
        main:
          main:
            - path: /home/node/.bash_aliases
              subPath: bash_aliases
              readOnly: true

    # Entire .openclaw directory as PVC
    data:
      # @schema type:boolean;default:true
      enabled: true
      # @schema type:string;default:persistentVolumeClaim
      type: persistentVolumeClaim
      # @schema type:string;enum:[ReadWriteOnce, ReadWriteMany, ReadOnlyMany];default:ReadWriteOnce
      # -- PVC access mode
      accessMode: ReadWriteOnce
      # @schema type:string;default:5Gi
      # -- PVC storage size
      size: 5Gi
      advancedMounts:
        main:
          init-config:
            - path: /home/node/.openclaw
          init-skills:
            - path: /home/node/.openclaw
          main:
            - path: /home/node/.openclaw

    # Writable /tmp for containers with readOnlyRootFilesystem
    tmp:
      # @schema type:boolean;default:true
      enabled: true
      # @schema type:string;default:emptyDir
      type: emptyDir
      advancedMounts:
        main:
          init-config:
            - path: /tmp
          init-skills:
            - path: /tmp
          main:
            - path: /tmp
          chromium:
            - path: /tmp

    # CA bundle for internal TLS (optional)
    # Use with trust-manager or similar to trust internal CAs
    # ca-bundle:
    #   enabled: true
    #   type: configMap
    #   name: ca-bundle
    #   advancedMounts:
    #     main:
    #       main:
    #         - path: /etc/ssl/certs/ca-bundle.crt
    #           subPath: ca-bundle.crt
    #           readOnly: true

  # ============================================================================
  # Network Policies
  # ============================================================================
  # Network policies provide workload isolation by controlling traffic to/from
  # OpenClaw pods. Requires a CNI that supports NetworkPolicy (Calico, Cilium,
  # Weave). Flannel without network policy controller will NOT enforce these.
  #
  # IMPORTANT: When enabled, ALL traffic is denied by default. You must
  # explicitly allow required traffic via ingress/egress rules.
  networkpolicies:
    main:
      # @schema type:boolean;default:false
      # -- Enable network policy (default deny-all with explicit allow rules)
      enabled: false
      # @schema type:string;default:main
      controller: main
      policyTypes:
        - Ingress
        - Egress
      rules:
        ingress:
          # Allow ingress from gateway namespace (adjust label to match your setup)
          # Example: allows traffic from pods in namespace with label
          # kubernetes.io/metadata.name: gateway-system
          - from:
              - namespaceSelector:
                  matchLabels:
                    kubernetes.io/metadata.name: gateway-system
            ports:
              - protocol: TCP
                port: 18789
          # Alternatively, allow from ingress-nginx namespace:
          # - from:
          #     - namespaceSelector:
          #         matchLabels:
          #           kubernetes.io/metadata.name: ingress-nginx
          #   ports:
          #     - protocol: TCP
          #       port: 18789
        egress:
          # DNS resolution (kube-dns/coredns)
          - to:
              - namespaceSelector:
                  matchLabels:
                    kubernetes.io/metadata.name: kube-system
                podSelector:
                  matchLabels:
                    k8s-app: kube-dns
            ports:
              - protocol: UDP
                port: 53
              - protocol: TCP
                port: 53
          # External internet (API providers, package registries, etc.)
          # Allows all traffic to public IPs while blocking private/reserved ranges
          # This prevents OpenClaw from accessing internal cluster services
          - to:
              - ipBlock:
                  cidr: 0.0.0.0/0
                  except:
                    - 10.0.0.0/8
                    - 172.16.0.0/12
                    - 192.168.0.0/16
                    - 169.254.0.0/16
                    - 100.64.0.0/10
          # Vault access (uncomment if using HashiCorp Vault for secrets)
          # - to:
          #     - namespaceSelector:
          #         matchLabels:
          #           kubernetes.io/metadata.name: vault
          #   ports:
          #     - protocol: TCP
          #       port: 8200
