Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Please choose versions by [Semantic Versioning](http://semver.org/).
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.

## Unreleased

- feat(helm): `clusterAssigner.excludeAssignees` (list, default `[]`) renders `EXCLUDE_ASSIGNEES` on the cluster-assigner CronJob, comma-joined; tasks of a listed assignee are never newly tagged sun; an empty or absent list renders nothing; `clusterAssigner.image.tag` default raised v0.1.0→v0.2.0 so the value works without an override; `hack/test-helm-render.sh` covers the absent, empty, one-element and two-element renders and pins the v0.2.0 default tag; chart 0.14.1→0.15.0

## v0.116.0

- feat(helm): render `rerunOnLostClaim` on the agent Config whenever `agents[].rerunOnLostClaim` is set, `false` included (presence test via `hasKey`, not truthiness), as a bare bool; nothing renders when unset, so no existing agent's Config changes; new `hack/test-helm-render.sh`, run by `make test`, asserts the false/true/unset renders; chart 0.14.0→0.14.1. Takes effect only with a Config CRD that declares `spec.rerunOnLostClaim` (older CRDs prune the field) and an agent-task-executor that reads it; older executors ignore the field.
Expand Down
84 changes: 83 additions & 1 deletion hack/test-helm-render.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
#
# Renders helm/templates/agents.yaml and asserts that the optional per-agent
# rerunOnLostClaim field renders whenever it is present — including `false` —
# and not at all when unset. Run from repo root.
# and not at all when unset.
#
# Also renders helm/templates/cluster-assigner-cronjob.yaml and asserts that
# clusterAssigner.excludeAssignees renders EXCLUDE_ASSIGNEES comma-joined for a
# non-empty list and no env entry at all for the empty default.
# Run from repo root.

set -euo pipefail

Expand Down Expand Up @@ -106,6 +111,83 @@ run_case false "$tmpdir/false.yaml" false
run_case true "$tmpdir/true.yaml" true
run_case unset "$tmpdir/unset.yaml" unset

# clusterAssigner.excludeAssignees → EXCLUDE_ASSIGNEES

# write_ca_values <file> <excludeAssignees YAML list literal or empty to omit the key>
write_ca_values() {
local file="$1"
local exclude="$2"
{
cat <<'EOF'
namespace: test
executor:
kafkaBrokers: kafka:9092
sentry:
dsn: dummy
clusterAssigner:
enabled: true
kafkaBrokers: kafka:9092
gitRestURL: http://git-rest
EOF
if [ -n "$exclude" ]; then
printf ' excludeAssignees: %s\n' "$exclude"
fi
} >"$file"
}

# run_ca_case <case> <values file> <empty|"joined value">
run_ca_case() {
local name="$1"
local values="$2"
local expect="$3"
local out="$tmpdir/$name.out"

"$helm_bin" template x "$CHART_DIR" -f "$values" \
--show-only templates/cluster-assigner-cronjob.yaml >"$out"

# Pin the chart default introduced alongside this feature: on defaults the
# assigner must run a tag that honours EXCLUDE_ASSIGNEES, otherwise the
# value is a silent no-op.
if ! grep -q 'agent-cluster-assigner:v0\.2\.0' "$out"; then
echo "FAIL: $name: default image tag is not v0.2.0"
fail=1
return
fi

if [ "$expect" = "empty" ]; then
if grep -q 'EXCLUDE_ASSIGNEES' "$out"; then
echo "FAIL: $name: unexpected EXCLUDE_ASSIGNEES entry for an empty list"
fail=1
return
fi
echo "OK: $name"
return
fi

if ! grep -qx ' - name: EXCLUDE_ASSIGNEES' "$out"; then
echo "FAIL: $name: missing 'EXCLUDE_ASSIGNEES' env entry"
fail=1
return
fi
if ! grep -qxF " value: \"$expect\"" "$out"; then
echo "FAIL: $name: EXCLUDE_ASSIGNEES value is not \"$expect\""
fail=1
return
fi

echo "OK: $name"
}

write_ca_values "$tmpdir/ca-empty.yaml" "[]"
write_ca_values "$tmpdir/ca-one.yaml" "[releaser]"
write_ca_values "$tmpdir/ca-two.yaml" "[releaser, claude]"
write_ca_values "$tmpdir/ca-absent.yaml" ""

run_ca_case ca-empty "$tmpdir/ca-empty.yaml" empty
run_ca_case ca-absent "$tmpdir/ca-absent.yaml" empty
run_ca_case ca-one "$tmpdir/ca-one.yaml" "releaser"
run_ca_case ca-two "$tmpdir/ca-two.yaml" "releaser,claude"

if [ "$fail" -ne 0 ]; then
exit 1
fi
Expand Down
2 changes: 1 addition & 1 deletion helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: agent
description: bborbe agent framework — event-driven, Kafka-based, K8s-native task platform (controller, executor, and example agents)
type: application
# Chart version — bumped on chart changes, independent of appVersion.
version: 0.14.1
version: 0.15.0
# Default image tag for the framework components when values don't override.
appVersion: "0.3.1"
home: https://github.com/bborbe/agent
Expand Down
3 changes: 2 additions & 1 deletion helm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ the OpenClaw vault by setting `cluster:` frontmatter — it publishes the task c
| Key | Default | Description |
|---|---|---|
| `clusterAssigner.enabled` | `false` | Deploy the cluster-assigner CronJob (nothing renders when false). |
| `clusterAssigner.image.repository` / `image.tag` | `bborbe/agent-cluster-assigner` / `v0.1.0` | Image; versioned independently of the chart (no `appVersion` fallback). |
| `clusterAssigner.image.repository` / `image.tag` | `bborbe/agent-cluster-assigner` / `v0.2.0` | Image; versioned independently of the chart (no `appVersion` fallback). |
| `clusterAssigner.schedule` / `successfulJobsHistoryLimit` / `failedJobsHistoryLimit` | `*/5 * * * *` / `1` / `3` | CronJob schedule and history. Overlapping runs are forbidden. |
| `clusterAssigner.kafkaBrokers` | `""` | Kafka bootstrap brokers. Required when enabled. |
| `clusterAssigner.topicPrefix` | `""` | Prefix for `<topicPrefix>-agent-task-v1-request` (nuke prod: `master`). |
Expand All @@ -287,6 +287,7 @@ the OpenClaw vault by setting `cluster:` frontmatter — it publishes the task c
| `clusterAssigner.vault` / `taskGlob` | `openclaw` / `tasks/*.md` | Vault name + task file glob. |
| `clusterAssigner.defaultClusterProd` / `defaultClusterDev` | `nukeprod` / `nukedev` | Cluster assigned to unstarted tasks per environment. |
| `clusterAssigner.sunRouting` / `dryRun` | `false` / `false` | Route via the sun API / log instead of publishing. |
| `clusterAssigner.excludeAssignees` | `[]` | Assignees never newly tagged sun (→ `EXCLUDE_ASSIGNEES`, comma-joined; needs `image.tag` v0.2.0 or later, which is the default). Empty renders nothing. Entries must not contain a comma. |
| `clusterAssigner.sun.apiURL` / `sun.namespace` / `sun.secretName` | `""` / `a2a` / `""` | Sun API base URL, namespace, and an existing Secret with keys `sun-token` / `sun-ca-data`. Empty `secretName` = the chart renders Secret `cluster-assigner-sun` from `sun.token` / `sun.caData`. |
| `clusterAssigner.sun.token` / `sun.caData` | `""` / `""` | Sun API token and CA data. With `sunRouting` on and no `sun.secretName`, both are required and rendered into Secret `cluster-assigner-sun`; pass them at apply time via `--set-json` from TeamVault, never commit them. |
| `clusterAssigner.kafkaUser.*` | as `recurringTaskCreator.kafkaUser` | Strimzi `KafkaUser` + mTLS cert mounts; derived user name = `<namespace>-cluster-assigner`. |
Expand Down
4 changes: 4 additions & 0 deletions helm/templates/cluster-assigner-cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ spec:
value: {{ $ca.sunRouting | quote }}
- name: DRY_RUN
value: {{ $ca.dryRun | quote }}
{{- with $ca.excludeAssignees }}
- name: EXCLUDE_ASSIGNEES
value: {{ join "," . | quote }}
{{- end }}
- name: SUN_API_URL
value: {{ $ca.sun.apiURL | quote }}
- name: SUN_NAMESPACE
Expand Down
10 changes: 8 additions & 2 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -537,8 +537,9 @@ clusterAssigner:
image:
repository: bborbe/agent-cluster-assigner
# Versioned independently of the chart (like a2aWorkerController) — no
# appVersion fallback.
tag: v0.1.0
# appVersion fallback. v0.2.0 is the first tag that honours
# EXCLUDE_ASSIGNEES; anything older silently ignores it.
tag: v0.2.0
logLevel: "2"
schedule: "*/5 * * * *"
successfulJobsHistoryLimit: 1
Expand Down Expand Up @@ -569,6 +570,11 @@ clusterAssigner:
sunRouting: false
# Log the commands instead of publishing them.
dryRun: false
# Assignees whose tasks are never newly tagged sun; they get their stage
# default (→ EXCLUDE_ASSIGNEES, comma-joined). Empty renders no env entry.
# Needs image.tag v0.2.0 or later (the default); older images ignore it.
# Entries must not contain a comma — the value is comma-joined and re-split.
excludeAssignees: []
sun:
# Sun routing API base URL (SUN_API_URL).
apiURL: ""
Expand Down
98 changes: 98 additions & 0 deletions prompts/completed/245-helm-cluster-assigner-exclude-assignees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
status: completed
execution_id: agent-cluster-assigner-exclude-env-exec-245-helm-cluster-assigner-exclude-assignees
dark-factory-version: v0.198.7
created: "2026-10-21T18:54:17Z"
queued: "2026-10-21T18:54:17Z"
started: "2026-10-21T18:54:27Z"
completed: "2026-10-21T19:53:10Z"
---
# feat(helm): cluster-assigner passes EXCLUDE_ASSIGNEES

<summary>
- An operator can name agents whose tasks the cluster assigner must never send to the sun cluster, straight from the chart values
- The list reaches the assigner as its `EXCLUDE_ASSIGNEES` setting (agent-cluster-assigner v0.1.3 and later)
- Leaving the list empty changes nothing: existing installs render exactly what they render today
- The values reference, chart version and changelog are updated
</summary>

<objective>
Add an optional `clusterAssigner.excludeAssignees` value (a YAML list of strings, default `[]`) to the Helm chart in `helm/`. When it is non-empty, the cluster-assigner CronJob container gets env `EXCLUDE_ASSIGNEES` set to the entries joined with `,`. When it is empty, no env entry is rendered, so every existing render stays byte-identical apart from the `helm.sh/chart` version label.
</objective>

<context>
- `helm/templates/cluster-assigner-cronjob.yaml` — the CronJob. Its container `env:` list already renders `SUN_ROUTING` and `DRY_RUN` from `$ca` (`{{- $ca := .Values.clusterAssigner }}`). Add the new entry directly after `DRY_RUN`.
- `helm/values.yaml` — the `clusterAssigner:` block (`sunRouting`, `dryRun`, `sun:` …). Add `excludeAssignees: []` after `dryRun`, with a comment in the file's style.
- `helm/README.md` — the `### clusterAssigner (optional)` table, which has a row for `clusterAssigner.sunRouting` / `dryRun`. Add one row after it.
- `helm/Chart.yaml` — currently `version: 0.13.0`.
- `CHANGELOG.md` — no `## Unreleased` section exists yet if the top heading is a version; add one above it.
- `prompts/completed/244-helm-cluster-assigner-sun-secret.md` — previous prompt on this component; its `<verification>` shows the helm install precondition and the `/tmp/ca-base.yaml` render base reused below.
- The binary (bborbe/agent-cluster-assigner) reads `EXCLUDE_ASSIGNEES` as a comma-separated list; tasks of a listed assignee are never newly tagged sun.
</context>

<requirements>
1. **Snapshot first.** Install helm per the `<verification>` precondition, then `rm -rf /tmp/helm-orig && cp -r helm /tmp/helm-orig` before editing. Create `/tmp/ca-base.yaml`:
```yaml
namespace: test
executor:
kafkaBrokers: kafka:9092
sentry:
dsn: dummy
```
2. **`helm/values.yaml`.** In the `clusterAssigner:` block, after `dryRun`, add:
```yaml
# Assignees whose tasks are never newly tagged sun; they get their stage
# default (→ EXCLUDE_ASSIGNEES, comma-joined). Empty renders no env entry.
# Needs image.tag v0.1.3 or later; older images ignore the variable.
excludeAssignees: []
```
3. **`helm/templates/cluster-assigner-cronjob.yaml`.** Directly after the `DRY_RUN` env entry, add an entry rendered only when the list is non-empty, matching the surrounding indentation exactly:
```yaml
{{- with $ca.excludeAssignees }}
- name: EXCLUDE_ASSIGNEES
value: {{ join "," . | quote }}
{{- end }}
```
4. **`helm/README.md`.** In the `### clusterAssigner (optional)` table, add this exact row directly after the `sunRouting` / `dryRun` row:
```markdown
| `clusterAssigner.excludeAssignees` | `[]` | Assignees never newly tagged sun (→ `EXCLUDE_ASSIGNEES`, comma-joined; needs `image.tag` v0.1.3 or later). Empty renders nothing. |
```
5. **`helm/Chart.yaml`.** `version: 0.13.0` → `version: 0.14.0`. Leave `appVersion` unchanged.
6. **`CHANGELOG.md`.** Add a `## Unreleased` section above the top version heading (or reuse an existing `## Unreleased`) with this bullet:
```markdown
- feat(helm): `clusterAssigner.excludeAssignees` (list, default `[]`) renders `EXCLUDE_ASSIGNEES` on the cluster-assigner CronJob, comma-joined; tasks of a listed assignee are never newly tagged sun; an empty list renders nothing; chart 0.13.0→0.14.0
```
</requirements>

<constraints>
- Do NOT commit.
- Touch only `helm/values.yaml`, `helm/templates/cluster-assigner-cronjob.yaml`, `helm/README.md`, `helm/Chart.yaml`, `CHANGELOG.md`. No Go code, no mocks, no other template.
- No other new values keys.
- An empty list must render no `EXCLUDE_ASSIGNEES` entry at all, not an empty string.
</constraints>

<verification>
Precondition: the container does not ship helm; install it from the Go proxy. If this fails, report the prompt as FAILED and never skip the render checks:
`command -v helm >/dev/null 2>&1 || { go install helm.sh/helm/v3/cmd/helm@v3.16.4; export PATH="$PATH:$(go env GOPATH)/bin"; }`
`helm version --short | grep -q '^v3\.' || { echo "PRECONDITION FAILED: helm v3 unavailable in container"; exit 1; }`

Run every render check below inside ONE `bash <<'EOS' … EOS` block, because the tool shell is zsh and `$B` relies on word-splitting. Every command must exit 0 and every `diff` must print nothing:
```bash
B="-f /tmp/ca-base.yaml --set clusterAssigner.enabled=true --set clusterAssigner.kafkaBrokers=b:1 --set clusterAssigner.gitRestURL=http://g"
f() { grep -v 'helm.sh/chart:'; }
# disabled: unchanged
diff <(helm template x /tmp/helm-orig -f /tmp/ca-base.yaml | f) <(helm template x helm/ -f /tmp/ca-base.yaml | f)
# enabled, list empty: unchanged, no env entry
diff <(helm template x /tmp/helm-orig $B | f) <(helm template x helm/ $B | f)
! helm template x helm/ $B | grep -q EXCLUDE_ASSIGNEES
# one entry
helm template x helm/ $B --set-json 'clusterAssigner.excludeAssignees=["releaser"]' --show-only templates/cluster-assigner-cronjob.yaml | grep -A1 'name: EXCLUDE_ASSIGNEES$' | grep -qx ' value: "releaser"'
# two entries, comma-joined
helm template x helm/ $B --set-json 'clusterAssigner.excludeAssignees=["releaser","claude"]' --show-only templates/cluster-assigner-cronjob.yaml | grep -A1 'name: EXCLUDE_ASSIGNEES$' | grep -qx ' value: "releaser,claude"'
helm lint helm/ -f /tmp/ca-base.yaml
helm lint helm/ $B --set-json 'clusterAssigner.excludeAssignees=["releaser"]'
grep -qx 'version: 0.14.0' helm/Chart.yaml
awk '/^## /{sec=$0} /excludeAssignees/{print sec; exit}' CHANGELOG.md | grep -qx '## Unreleased'
```
Then run `ROOTDIR=/workspace make precommit`. If its ONLY failure is the govulncheck finding GO-2026-6809 (Go standard library crypto/x509 in the container's Go 1.27.2, fixed in 1.27.3), it is an environment issue unrelated to this chart-only change: record it in the summary and report success. Any other precommit failure means FAILED.
</verification>
Loading