Agent-Sandbox Upstream Issues — Operator Integration Findings
Epic: #1155 Component: rossoctl-operator AgentRuntime controller + agents.x-k8s.io Sandbox controller Discovered: 2026-04-29, during live testing of operator PR rossoctl-operator#316
Context
The rossoctl-operator's AgentRuntime controller manages workload configuration (labels, annotations, config-hash) for Deployment, StatefulSet, and now Sandbox targets. When configuration changes (e.g., a namespace-level ConfigMap is updated), the operator recomputes a config-hash and applies it to the workload's pod template. For Deployments and StatefulSets, the annotation change triggers a rolling update automatically. Sandbox has no such mechanism.
Issue 1: Sandbox Pods Do Not Roll Out on PodTemplate Changes
Upstream issue: kubernetes-sigs/agent-sandbox#581
Behavior
When the rossoctl-operator updates spec.podTemplate.metadata.annotations on a Sandbox
CR (e.g., writing a new rossoctl.io/config-hash), the Sandbox controller does not
terminate and recreate the running pod. The existing pod continues running with stale
configuration indefinitely.
This differs from Deployment and StatefulSet, where a change to
spec.template.metadata.annotations triggers a rolling update.
Root cause
The Sandbox controller's reconcilePod method (sandbox_controller.go:480) checks
whether a pod exists by reading the agents.x-k8s.io/pod-name annotation. If the named
pod exists and is running, the controller considers reconciliation complete — it does not
compare the pod's spec against the current spec.podTemplate.
Impact
Any configuration change that the rossoctl-operator applies to the Sandbox (new config-hash from ConfigMap changes, label updates, annotation changes) has no effect on the running pod until the pod is externally deleted or the Sandbox is scaled down and back up.
Our workaround
The operator's restartSandbox method performs an explicit scale 0 → 1 cycle:
- Patch
spec.replicasto 0 (withretry.RetryOnConflict) - Clear the
agents.x-k8s.io/pod-nameannotation (see Issue 2) - Patch
spec.replicasto 1 (withretry.RetryOnConflict)
This forces the Sandbox controller to delete the existing pod and create a new one from
the updated spec.podTemplate.
Issue 2: Sandbox Controller Stuck Loop After Pod Deletion
Behavior
After the operator scales a Sandbox to 0 replicas (deleting the pod) and then scales back to 1, the Sandbox controller enters a persistent error loop:
error: "pod in annotation get failed: Pod \"weather-service-1000\" not found"
The controller retries with exponential backoff (1s, 2s, 4s, 10s, 20s, 40s...) and does
not self-recover. The error loop continues indefinitely until the stale
agents.x-k8s.io/pod-name annotation is manually cleared.
Root cause
The Sandbox controller tracks its adopted pod via the agents.x-k8s.io/pod-name
annotation on the Sandbox CR. When reconciling, the controller:
- Reads
agents.x-k8s.io/pod-namefrom the Sandbox metadata - Attempts
GET pod/<name>in the same namespace - If the pod is not found, returns an error — it does not clear the annotation or attempt to create a new pod
The controller assumes the annotation always points to a valid pod. There is no fallback path for the case where the annotated pod has been deleted externally (by a scale-down, manual deletion, or eviction).
Reproduction
# Start with a healthy Sandbox (replicas=1, pod running)
kubectl get sandbox weather-service-1000 -n team1
# Pod is "weather-service-1000", annotation is set
# Scale to 0 — pod is deleted
kubectl patch sandbox weather-service-1000 -n team1 \
--type merge -p '{"spec":{"replicas":0}}'
# Scale back to 1 — controller should create a new pod
kubectl patch sandbox weather-service-1000 -n team1 \
--type merge -p '{"spec":{"replicas":1}}'
# Controller logs show repeated errors:
# "pod in annotation get failed: Pod \"weather-service-1000\" not found"
# No new pod is created.
# Fix: clear the stale annotation
kubectl annotate sandbox weather-service-1000 -n team1 \
agents.x-k8s.io/pod-name-
# Controller immediately creates a new pod.
Race condition with our workaround
Our restartSandbox method attempts to clear the agents.x-k8s.io/pod-name annotation
between the scale-down and scale-up operations. However, the Sandbox controller runs
concurrently and may re-read the annotation from its informer cache or re-set it during
its own reconciliation loop before our clear takes effect.
The observed sequence:
T0: Operator patches replicas=0
T1: Sandbox controller deletes the pod, starts reconciling
T2: Operator clears agents.x-k8s.io/pod-name annotation
T3: Sandbox controller's concurrent reconcile re-reads the Sandbox object,
sees replicas=0, does nothing — but the informer-cached version still
has the annotation
T4: Operator patches replicas=1
T5: Sandbox controller reconciles, reads the stale annotation from its
own update (or from T3's cached read), tries GET pod → fails
T6: Error loop begins
Even with retry.RetryOnConflict, our annotation-clearing update may succeed on the API
server but lose the race to the Sandbox controller's next write.
Impact
The Sandbox pod is not recreated after a config-driven restart. The AgentRuntime shows
Active with configuredPods: 1 (because the Sandbox's spec.replicas is 1), but no
actual pod exists. The Sandbox controller logs errors continuously.
Manual intervention (clearing the annotation) resolves the issue immediately — the controller then creates a fresh pod within seconds.
Possible upstream fixes
-
Annotation-clearing on pod-not-found: When the controller sees that the annotated pod doesn't exist, it should clear the annotation and fall through to the pod creation path rather than returning an error.
-
Owner reference based discovery: Instead of (or in addition to) the annotation, the controller could discover its pod via
ownerReferences, which is already set on pods created by the Sandbox controller. This is resilient to annotation races. -
Pod template drift detection: The controller should compare the running pod's labels/annotations against
spec.podTemplateand recreate when they diverge. This would fix Issue 1 entirely.
Possible rossoctl-operator workarounds
-
Poll-and-clear loop: After scale-down, poll the Sandbox annotation in a loop and keep clearing it until the pod-name annotation is absent for a stable period, then scale up. Adds complexity and latency but is deterministic.
-
Two-phase restart across reconcile cycles: On config-hash change, scale to 0 in the current reconcile. On the next reconcile (triggered by the Sandbox watch), detect
replicas=0with a "restart pending" annotation, clear the pod-name annotation, and scale to 1. This separates the scale-down and annotation-clear into different reconciliation windows, reducing the race window. -
Accept transient delay: Keep the current implementation. In clusters where the Sandbox controller eventually backs off enough (the retry intervals grow: 1s, 2s, 4s, 10s, 20s, 40s), the operator's annotation clear may win a subsequent retry. Testing showed this does not reliably self-heal — the controller re-reads its own stale state on each retry.
Current recommendation: Option 2 (two-phase restart) is the most robust within the operator's control. It should be filed as a follow-up after the initial PR merges with the current implementation and a documented known limitation.
Issue 3: SandboxClaim Controller Overwrites Operator Config on Sandbox
Behavior
When a SandboxClaim exists for a Sandbox, the SandboxClaim controller copies the
entire spec.podTemplate from the SandboxTemplate to the Sandbox on every
reconciliation. Any annotations or labels the rossoctl-operator writes to
spec.podTemplate.metadata (e.g., rossoctl.io/config-hash) are overwritten within
seconds, triggering a continuous reconcile loop:
Operator writes config-hash → SandboxClaim controller detects change →
copies podTemplate from template (wiping config-hash) →
Operator detects config-hash missing → writes it again → loop
Root cause
This is designed behavior, not a bug. The SandboxClaim controller's purpose is to
keep the Sandbox's spec.podTemplate in sync with the SandboxTemplate. It performs a
full replacement of podTemplate, not a merge. Any fields added by external controllers
are lost on each reconciliation cycle.
Impact
The rossoctl-operator cannot persist any configuration on spec.podTemplate when a
SandboxClaim owns the Sandbox. This means:
rossoctl.io/config-hashannotation is repeatedly wiped — the operator loops forever trying to reapply it.- Any rossoctl-managed labels on
spec.podTemplate.metadata.labelsare wiped. - The two-phase restart mechanism cannot function because Phase 1 (scale to 0) triggers
SandboxClaim reconciliation which may reset
spec.replicasor other fields.
Reproduction
# Create AgentRuntime pointing to a Sandbox owned by a SandboxClaim
# Observe operator logs: continuous "Applying config to workload" messages
kubectl logs deployment/rossoctl-controller-manager -n rossoctl-system --tail=20
# Verify config-hash is never persisted:
kubectl get sandbox weather-service-5000 -n team1 \
-o jsonpath='{.spec.podTemplate.metadata.annotations}'
# Returns null — annotation is wiped every time
Possible workarounds
-
Store config-hash on Sandbox CR
metadata.annotations(not podTemplate) — the SandboxClaim controller doesn't overwrite CR-level metadata. However, any labels or annotations the operator needs on the actual pod must go throughspec.podTemplate, which is still overwritten. -
Write operator config to the SandboxTemplate — changes propagate down through the claim. But config-hash is per-instance (computed from namespace-specific ConfigMaps), not per-template. This would require one SandboxTemplate per agent instance, defeating the template's purpose.
-
Upstream change: Modify the SandboxClaim controller to merge
podTemplatefields instead of replacing them. This is an upstream behavioral change we don't control.
Resolution
Bypass SandboxClaim entirely. The rossoctl backend creates Sandbox CRs directly
(no SandboxTemplate, no SandboxClaim). This eliminates the third writer and gives the
operator full control over spec.podTemplate. See
ADR: Direct Sandbox Creation vs SandboxClaim.
Verified on live cluster: after removing the SandboxClaim (and its ownerReference on the Sandbox), the operator's config-hash persists cleanly and the two-phase restart works end-to-end without reconcile loops.
Test Environment
- Kind cluster, single node
- agent-sandbox controller:
v0.3.10(controller-runtime v0.23.3), upgraded to local build with commit9c6264c(Issue 2 self-healing fix) - rossoctl-operator: branch
feat/sandbox-workload-support(two-phase restart) - Sandbox CRD:
agents.x-k8s.io/v1alpha1