Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
Kubernetes operator (Go, Kubebuilder/Operator SDK) that manages OpenClaw instances on OpenShift/Kubernetes. Detailed architecture reference in `docs/architecture.md`.

**CRDs** (API group `claw.sandbox.redhat.com/v1alpha1`):
- `Claw` — Main CRD. Spec: `config` (ConfigSpec: raw RawExtension, mergeMode merge/overwrite), `credentials` ([]CredentialSpec), `auth` (AuthSpec: mode token/password, passwordSecretRef, disableDevicePairing). Status: `conditions`, `url`, `gatewayTokenSecretRef`
- `Claw` — Main CRD. Spec: `config` (ConfigSpec: raw RawExtension, mergeMode merge/overwrite/seedOnly), `credentials` ([]CredentialSpec), `auth` (AuthSpec: mode token/password, passwordSecretRef, disableDevicePairing). Status: `conditions`, `url`, `gatewayTokenSecretRef`
- `ClawDevicePairingRequest` — Device pairing. Spec: `requestID`, `selector` (LabelSelector). Controller matches exactly one pod
- `ClawOperatorConfig` — Cluster-admin policy singleton (name `cluster`, in the operator's own namespace). Spec: `allowedConfigModes` ([]ConfigMode, empty = unrestricted). Gates which `mergeMode` values `Claw` CRs may use; see [ADR-0021](docs/adr/0021-seed-only-config-mode.md)

## Common Commands

Expand Down Expand Up @@ -47,11 +48,11 @@ Detailed architecture reference: @docs/architecture.md

## Key Directories

- `api/v1alpha1/` -- CRD types (`claw_types.go`, `clawdevicepairingrequest_types.go`). Condition/annotation constants live here
- `internal/controller/` -- Reconciler + tests. Key files: `claw_resource_controller.go` (main reconciler), `claw_credentials.go` (credential validation, gateway secret), `claw_providers.go` (centralized `knownProviders` registry, provider defaults/routing), `claw_proxy.go` (proxy config), `claw_deployment.go` (deployment configuration), `claw_status.go` (status updates), `claw_models.go` (model catalog accessor, delegates to `knownProviders`), `claw_auth.go` (auth mode + device pairing)
- `api/v1alpha1/` -- CRD types (`claw_types.go`, `clawdevicepairingrequest_types.go`, `clawoperatorconfig_types.go`). Condition/annotation constants live here
- `internal/controller/` -- Reconciler + tests. Key files: `claw_resource_controller.go` (main reconciler), `claw_credentials.go` (credential validation, gateway secret), `claw_providers.go` (centralized `knownProviders` registry, provider defaults/routing), `claw_proxy.go` (proxy config), `claw_deployment.go` (deployment configuration), `claw_status.go` (status updates), `claw_models.go` (model catalog accessor, delegates to `knownProviders`), `claw_auth.go` (auth mode + device pairing), `claw_operator_config.go` (ClawOperatorConfig mergeMode gating)
- `internal/proxy/` -- MITM proxy library
- `internal/assets/manifests/` -- Embedded Kustomize manifests (two components: `claw/`, `claw-proxy/`)
- `cmd/main.go` -- Manager entrypoint (reads `PROXY_IMAGE`, `KUBECTL_IMAGE`, `IMAGE_PULL_POLICY` env vars)
- `cmd/main.go` -- Manager entrypoint (reads `PROXY_IMAGE`, `KUBECTL_IMAGE`, `IMAGE_PULL_POLICY`, `WATCH_NAMESPACE` env vars)
- `cmd/proxy/main.go` -- Proxy binary entrypoint
- `config/` -- Kustomize overlays for CRDs, RBAC, manager deployment

Expand Down
40 changes: 27 additions & 13 deletions api/v1alpha1/claw_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,25 @@ const (
)

// ConfigMode controls how operator.json is merged into the user's openclaw.json
// at pod start time.
// +kubebuilder:validation:Enum=merge;overwrite
// at pod start time. Not to be confused with SeedMode below, which governs
// per-file workspace seeding (spec.workspace.*.mode) — ConfigMode governs the
// whole openclaw.json file (spec.config.mergeMode).
// +kubebuilder:validation:Enum=merge;overwrite;seedOnly
type ConfigMode string

const (
ConfigModeMerge ConfigMode = "merge"
ConfigModeOverwrite ConfigMode = "overwrite"
// ConfigModeSeedOnly seeds openclaw.json once on first boot, then leaves
// user/agent-owned keys untouched on subsequent restarts; a narrow,
// always-enforced subset of infrastructure/credential keys is still
// reasserted on every restart. See docs/adr/0021-seed-only-config-mode.md.
ConfigModeSeedOnly ConfigMode = "seedOnly"
)

// SeedMode controls how workspace files are seeded into the PVC.
// SeedMode controls how workspace files are seeded into the PVC
// (spec.workspace.*.mode) — a different, per-file mechanism from ConfigMode's
// seedOnly value above, despite the similar naming.
// +kubebuilder:validation:Enum=overwrite;seedIfMissing
type SeedMode string

Expand Down Expand Up @@ -86,14 +95,16 @@ const (

// Condition reasons for Claw status.
const (
ConditionReasonReady = "Ready"
ConditionReasonProvisioning = "Provisioning"
ConditionReasonResolved = "Resolved"
ConditionReasonValidationFailed = "ValidationFailed"
ConditionReasonConfigured = "Configured"
ConditionReasonConfigFailed = "ConfigFailed"
ConditionReasonIdle = "Idle"
ConditionReasonIdledByRequest = "IdledByRequest"
ConditionReasonReady = "Ready"
ConditionReasonProvisioning = "Provisioning"
ConditionReasonResolved = "Resolved"
ConditionReasonValidationFailed = "ValidationFailed"
ConditionReasonConfigured = "Configured"
ConditionReasonConfigFailed = "ConfigFailed"
ConditionReasonIdle = "Idle"
ConditionReasonIdledByRequest = "IdledByRequest"
ConditionReasonIdledByPolicy = "IdledByPolicy"
ConditionReasonConfigModeNotAllowed = "ConfigModeNotAllowed"
)

// SecretRefEntry references a specific key in a Secret.
Expand Down Expand Up @@ -369,9 +380,12 @@ type ConfigSpec struct {
// MergeMode controls how operator config is applied on pod start.
// "merge" (default) deep-merges operator settings into the existing
// user config, preserving user-owned keys. "overwrite" fully replaces
// the config on every pod start.
// the config on every pod start. "seedOnly" seeds the config once on
// first boot, then leaves it untouched except for a narrow,
// always-enforced infrastructure/credential subset. A cluster admin may
// restrict which modes are allowed via ClawOperatorConfig.
// +optional
// +kubebuilder:validation:Enum=merge;overwrite
// +kubebuilder:validation:Enum=merge;overwrite;seedOnly
// +kubebuilder:default=merge
MergeMode ConfigMode `json:"mergeMode,omitempty"`
}
Expand Down
65 changes: 65 additions & 0 deletions api/v1alpha1/clawoperatorconfig_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2026 Red Hat.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ClawOperatorConfigSingletonName is the required name of the single
// ClawOperatorConfig instance the operator reads. Any other name is ignored.
const ClawOperatorConfigSingletonName = "cluster"

// ClawOperatorConfigSpec defines cluster-admin policy for the claw-operator.
type ClawOperatorConfigSpec struct {
// AllowedConfigModes restricts which spec.config.mergeMode values Claw CRs
// in this cluster may use. Empty/absent means all modes are allowed
// (fail-open) — this matches today's unrestricted behavior until an admin
// explicitly opts in to restricting it.
// +optional
AllowedConfigModes []ConfigMode `json:"allowedConfigModes,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:path=clawoperatorconfigs,scope=Namespaced
// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'cluster'",message="ClawOperatorConfig must be named 'cluster'"

// ClawOperatorConfig is the Schema for the clawoperatorconfigs API. A single
// instance named "cluster" must live in the operator's own runtime namespace
// (resolved via the WATCH_NAMESPACE environment variable) — it is never
// looked up in, or honored from, a tenant namespace. The name is enforced
// at admission time by the XValidation rule above (ClawOperatorConfigSingletonName).
type ClawOperatorConfig struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +kubebuilder:validation:Required
Spec ClawOperatorConfigSpec `json:"spec"`
}

// +kubebuilder:object:root=true

// ClawOperatorConfigList contains a list of ClawOperatorConfig
type ClawOperatorConfigList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ClawOperatorConfig `json:"items"`
}

func init() {
SchemeBuilder.Register(&ClawOperatorConfig{}, &ClawOperatorConfigList{})
}
78 changes: 78 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ func main() {
os.Exit(1)
}

// WATCH_NAMESPACE identifies the operator's own runtime namespace (populated
// via the Kubernetes downward API), used to look up the ClawOperatorConfig
// admin-policy singleton — never a tenant namespace. Required: fails fast if
// unset, since a silently-empty value would make the singleton lookup
// ambiguous.
operatorNamespace := os.Getenv("WATCH_NAMESPACE")
if operatorNamespace == "" {
setupLog.Error(nil, "WATCH_NAMESPACE environment variable must be set")
os.Exit(1)
}

clawReconciler := &controller.ClawResourceReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand All @@ -272,6 +283,7 @@ func main() {
GitSyncImage: os.Getenv("GIT_SYNC_IMAGE"),
OTelCollectorImage: os.Getenv("OTEL_COLLECTOR_IMAGE"),
ImagePullPolicy: imagePullPolicy,
OperatorNamespace: operatorNamespace,
MetricsRefreshed: make(chan struct{}),
}
if err = clawReconciler.SetupWithManager(mgr); err != nil {
Expand Down
74 changes: 74 additions & 0 deletions config/crd/bases/claw.sandbox.redhat.com_clawoperatorconfigs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
name: clawoperatorconfigs.claw.sandbox.redhat.com
spec:
group: claw.sandbox.redhat.com
names:
kind: ClawOperatorConfig
listKind: ClawOperatorConfigList
plural: clawoperatorconfigs
singular: clawoperatorconfig
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
ClawOperatorConfig is the Schema for the clawoperatorconfigs API. A single
instance named "cluster" must live in the operator's own runtime namespace
(resolved via the WATCH_NAMESPACE environment variable) — it is never
looked up in, or honored from, a tenant namespace. The name is enforced
at admission time by the XValidation rule above (ClawOperatorConfigSingletonName).
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: ClawOperatorConfigSpec defines cluster-admin policy for the
claw-operator.
properties:
allowedConfigModes:
description: |-
AllowedConfigModes restricts which spec.config.mergeMode values Claw CRs
in this cluster may use. Empty/absent means all modes are allowed
(fail-open) — this matches today's unrestricted behavior until an admin
explicitly opts in to restricting it.
items:
description: |-
ConfigMode controls how operator.json is merged into the user's openclaw.json
at pod start time. Not to be confused with SeedMode below, which governs
per-file workspace seeding (spec.workspace.*.mode) — ConfigMode governs the
whole openclaw.json file (spec.config.mergeMode).
enum:
- merge
- overwrite
- seedOnly
type: string
type: array
type: object
required:
- spec
type: object
x-kubernetes-validations:
- message: ClawOperatorConfig must be named 'cluster'
rule: self.metadata.name == 'cluster'
served: true
storage: true
7 changes: 6 additions & 1 deletion config/crd/bases/claw.sandbox.redhat.com_claws.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,20 @@ spec:
- enum:
- merge
- overwrite
- seedOnly
- enum:
- merge
- overwrite
- seedOnly
default: merge
description: |-
MergeMode controls how operator config is applied on pod start.
"merge" (default) deep-merges operator settings into the existing
user config, preserving user-owned keys. "overwrite" fully replaces
the config on every pod start.
the config on every pod start. "seedOnly" seeds the config once on
first boot, then leaves it untouched except for a narrow,
always-enforced infrastructure/credential subset. A cluster admin may
restrict which modes are allowed via ClawOperatorConfig.
type: string
raw:
description: |-
Expand Down
1 change: 1 addition & 0 deletions config/crd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
resources:
- bases/claw.sandbox.redhat.com_claws.yaml
- bases/claw.sandbox.redhat.com_clawdevicepairingrequests.yaml
- bases/claw.sandbox.redhat.com_clawoperatorconfigs.yaml
# +kubebuilder:scaffold:crdkustomizeresource

# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
Expand Down
4 changes: 4 additions & 0 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ spec:
imagePullPolicy: IfNotPresent
name: manager
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: PROXY_IMAGE
value: claw-proxy:latest
- name: KUBECTL_IMAGE
Expand Down
Loading
Loading