feat(ploeg): per-team and per-role sandbox RuntimeClass #5

Open
agent-builder wants to merge 4 commits from agent/vik-555 into development
Member

VIK-555 — chart-only change, no Go changes.

What / why

The agent-sandbox executor took one global executor.sandbox.runtimeClassName, applied to every team's and Role's SandboxTemplate (templates/sandbox.yaml). The owner's fleet runs one team on Kata and one on gVisor (homelab RFC rfc-agent-runtime-kagent-vs-glide.md, D3), which the chart could not express. This adds per-team executor.teams[].sandbox.runtimeClassName and per-Role plan[].roles[].sandbox.runtimeClassName, resolved field-by-field like harness: already is:

role → team → global executor.sandbox.runtimeClassName → "" (no runtimeClassName rendered).

  • New ploeg.runtimeClassName helper keeps the resolution in one place.
  • values.schema.json gains a sandboxOverride definition on executor.teams[] and plan[].roles[] (string-only, rejects a non-string value).
  • values.yaml documents placement: Kata on bare metal (own kernel, no nested-virt requirement); gVisor systrap on VMs without nested virt, accepting the file-I/O penalty on clone-heavy runs.
  • docs/reference/configuration.md regenerated.

Evidence

CI is green on the final commit 327f548: all three pull-request workflows succeeded — checks, release-policy, tutorial-smoke (run: https://forgejo.webgrip.dev/webgrip/glide/actions/runs/89).

The checks job runs mise run verify, whose Helm group runs helm lint plus scripts/helm-golden.sh check and whose docs group runs docs.py --check (including docs-configuration.py --check). So the real render, the goldens, the reject fixtures and the regenerated configuration reference are confirmed by CI, not asserted by hand.

  • New golden ci/golden/executor-sandbox-runtimeclass.yaml (fixture ci/executor-sandbox-runtimeclass-values.yaml) renders three distinct runtimeClassName values: kata (global fallback), gvisor (team override), gvisor-systrap (Role override).
  • The existing executor-sandbox-team golden covers the empty-global/no-override case: no runtimeClassName at all.
  • New reject fixture ci/reject-runtimeclass-non-string-values.yaml, registered in helm-golden.sh, proves values.schema.json refuses a non-string sandbox.runtimeClassName.

Locally run and passed: the Ploeg verify script (test -z "$(gofmt -l apps/ploeg)"). helm and the full mise run verify are not available in the agent sandbox, so CI ran them instead.

Risk notes for the reviewer

  • The keda executor path is deliberately untouched. VIK-555 flags as an escalation whether keda should also honour sandbox.runtimeClassName; that design choice is not made here and remains open.
  • ploegd parses PLOEG_TEAM_PLANS with plain json.Unmarshal, so the extra sandbox key now serialised into a plan Role is ignored at boot — worth re-confirming if a future change tightens that decoder.
  • No existing CI values file or test was edited; the changes add new fixtures and one new golden/reject registration line in helm-golden.sh.

Reference: VIK-555


This pull request was created by an AI agent (OpenHands, running as agent-builder) on behalf of the user.

VIK-555 — chart-only change, no Go changes. ## What / why The agent-sandbox executor took one global `executor.sandbox.runtimeClassName`, applied to every team's and Role's `SandboxTemplate` (`templates/sandbox.yaml`). The owner's fleet runs one team on Kata and one on gVisor (homelab RFC `rfc-agent-runtime-kagent-vs-glide.md`, D3), which the chart could not express. This adds per-team `executor.teams[].sandbox.runtimeClassName` and per-Role `plan[].roles[].sandbox.runtimeClassName`, resolved field-by-field like `harness:` already is: `role` → `team` → global `executor.sandbox.runtimeClassName` → `""` (no `runtimeClassName` rendered). - New `ploeg.runtimeClassName` helper keeps the resolution in one place. - `values.schema.json` gains a `sandboxOverride` definition on `executor.teams[]` and `plan[].roles[]` (string-only, rejects a non-string value). - `values.yaml` documents placement: Kata on bare metal (own kernel, no nested-virt requirement); gVisor systrap on VMs without nested virt, accepting the file-I/O penalty on clone-heavy runs. - `docs/reference/configuration.md` regenerated. ## Evidence CI is green on the final commit `327f548`: all three pull-request workflows succeeded — `checks`, `release-policy`, `tutorial-smoke` (run: https://forgejo.webgrip.dev/webgrip/glide/actions/runs/89). The `checks` job runs `mise run verify`, whose Helm group runs `helm lint` plus `scripts/helm-golden.sh check` and whose docs group runs `docs.py --check` (including `docs-configuration.py --check`). So the real render, the goldens, the reject fixtures and the regenerated configuration reference are confirmed by CI, not asserted by hand. - New golden `ci/golden/executor-sandbox-runtimeclass.yaml` (fixture `ci/executor-sandbox-runtimeclass-values.yaml`) renders three distinct `runtimeClassName` values: `kata` (global fallback), `gvisor` (team override), `gvisor-systrap` (Role override). - The existing `executor-sandbox-team` golden covers the empty-global/no-override case: no `runtimeClassName` at all. - New reject fixture `ci/reject-runtimeclass-non-string-values.yaml`, registered in `helm-golden.sh`, proves `values.schema.json` refuses a non-string `sandbox.runtimeClassName`. Locally run and passed: the Ploeg verify script (`test -z "$(gofmt -l apps/ploeg)"`). `helm` and the full `mise run verify` are not available in the agent sandbox, so CI ran them instead. ## Risk notes for the reviewer - The keda executor path is deliberately untouched. VIK-555 flags as an escalation whether keda should also honour `sandbox.runtimeClassName`; that design choice is not made here and remains open. - ploegd parses `PLOEG_TEAM_PLANS` with plain `json.Unmarshal`, so the extra `sandbox` key now serialised into a plan Role is ignored at boot — worth re-confirming if a future change tightens that decoder. - No existing CI values file or test was edited; the changes add new fixtures and one new golden/reject registration line in `helm-golden.sh`. Reference: VIK-555 --- This pull request was created by an AI agent (OpenHands, running as agent-builder) on behalf of the user.
feat(ploeg): per-team and per-role sandbox RuntimeClass
Some checks failed
[Workflow] On Pull Request / tutorial-smoke (pull_request) Successful in 24s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 36s
[Workflow] On Pull Request / checks (pull_request) Has been cancelled
23f7e44f65
The agent-sandbox executor took one global executor.sandbox.runtimeClassName,
so every team and Role shared it. The owner runs one team on Kata and one on
gVisor, which the chart could not express.

Resolve the RuntimeClass through the same field-by-field chain harness: uses:
role sandbox.runtimeClassName > team sandbox.runtimeClassName > global
executor.sandbox.runtimeClassName > "" (render no runtimeClassName). The new
ploeg.runtimeClassName helper keeps that resolution in one place, and
sandbox.yaml sets the field only when the chain resolves one. values.schema.json
gains a sandboxOverride definition on executor.teams[] and plan[].roles[], so a
non-string value is rejected. values.yaml documents the Kata (bare metal) and
gVisor systrap (VMs without nested virt; file-I/O penalty on clone-heavy runs)
placement guidance.

Verification: a new golden executor-sandbox-runtimeclass renders three distinct
runtimeClassName values (kata global fallback, gvisor team override,
gvisor-systrap role override) and is registered in helm-golden.sh; the existing
executor-sandbox-team golden already covers the empty-global no-runtimeClassName
case.

Checks run here: the Ploeg verify script (gofmt) passed, as did
docs-configuration.py --check, docs-decisions.py --check and the offline docs
test scripts. helm and the Go module cache are absent in this sandbox, so
helm-golden.sh check and the Go gates are left to CI.

VIK-555
Agent-Trace-Id: ploeg-148042a86311
Co-authored-by: openhands <openhands@all-hands.dev>
test(ploeg): refuse a non-string sandbox RuntimeClass
All checks were successful
[Workflow] On Pull Request / tutorial-smoke (pull_request) Successful in 24s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 31s
[Workflow] On Pull Request / checks (pull_request) Successful in 3m20s
327f5482b3
The schema criterion needs its negative case, not just the positive render: a
non-string executor.teams[].sandbox.runtimeClassName must make helm refuse the
values. The reject fixture and its helm-golden.sh entry assert that
values.schema.json's sandboxOverride stays string-only.

VIK-555
Agent-Trace-Id: ploeg-148042a86311
Co-authored-by: openhands <openhands@all-hands.dev>
Author
Member

builder — round 1

openhands run opened a PR for 074 · RuntimeClass per team [Ploeg verification passed]

Ploeg verification

Ploeg ran the configured checks on commit 327f5482b3b7 after the agent finished.

Check Result
if [ -f apps/ploeg/go.mod ]; then test -z "$(gofmt -l apps/ploeg)"; fi passed

Posted by Ploeg for the writing Run that pushed this branch.

### builder — round 1 _openhands run opened a PR for 074 · RuntimeClass per team [Ploeg verification passed]_ ### Ploeg verification Ploeg ran the configured checks on commit `327f5482b3b7` after the agent finished. | Check | Result | | --- | --- | | `if [ -f apps/ploeg/go.mod ]; then test -z "$(gofmt -l apps/ploeg)"; fi` | passed | <sub>Posted by Ploeg for the writing Run that pushed this branch.</sub>
Author
Member

reviewer — round 2

Chart-only per-team/per-role RuntimeClass override; all four acceptance criteria met; gofmt and docs-configuration checks pass; helm render/lint left to CI (no helm, no egress).

Verdict: approve

Reviewed git diff development...agent/vik-555 (2 commits, 23f7e44 + 327f548) against VIK-555. Chart-only change; no Go, no agent-instruction/config files touched (none of AGENTS.md / CLAUDE.md / .claude/ / .agents/ / .openhands/ / .mcp.json / .cursorrules appear in the diff), no migrations, no release workflows.

What the change does

  • apps/ploeg/ops/helm/ploeg/templates/_sandbox.tpl:28-42 — new ploeg.runtimeClassName helper resolving role → team → global, exactly the default-chain shape the harness fields use in _helpers.tpl:110-155.
  • apps/ploeg/ops/helm/ploeg/templates/sandbox.yaml:11-14 — sets pod.spec.runtimeClassName only when the helper returns a non-empty value (previously read the global $sb.runtimeClassName directly).
  • values.schema.json:392 (team) and :765 (planRole) reference a new sandboxOverride definition at :722-732 (runtimeClassName: string, additionalProperties: false).
  • values.yaml:199-207 clarifies the global fallback and adds the Kata/gVisor placement guidance; the team example at :376-379 shows the override.
  • New CI fixtures: ci/executor-sandbox-runtimeclass-values.yaml + ci/golden/executor-sandbox-runtimeclass.yaml (render) and ci/reject-runtimeclass-non-string-values.yaml (schema reject), registered in scripts/helm-golden.sh:57,77.

Acceptance criteria — all met

  1. Per-team and per-role override, field by field — helper chain at _sandbox.tpl:39-41; schema permits both executor.teams[].sandbox.runtimeClassName and plan[].roles[].sandbox.runtimeClassName. The golden proves precedence: golden/executor-sandbox-runtimeclass.yaml:389 = kata (team kata takes the global), :600 = gvisor (team override), :773 = gvisor-systrap (reviewer Role override) on ploeg-worker-kata, ploeg-worker-gvisor-builder, ploeg-worker-gvisor-reviewer.
  2. Team without the key → global; all-empty → no field — helper returns "" at every tier and sandbox.yaml:12 guards the set. The empty-global case is already covered by the existing ci/golden/executor-sandbox-team.yaml (0 runtimeClassName occurrences; the fixture sets no global and the copper team no override).
  3. Schema accepts the keys, rejects a non-string — sandboxOverride types the value string under additionalProperties: false; reject-runtimeclass-non-string-values.yaml sets runtimeClassName: 123 and helm-golden.sh:77 asserts helm refuses it with runtimeClassName in the message.
  4. Placement guidance in the values comment — values.yaml:203-206 (Kata → bare metal; gVisor systrap → VMs without nested virt, file-I/O penalty on clone-heavy runs). It flows into the generated docs/reference/configuration.md:204.

Checks I ran

  • PLOEG_VERIFY_SCRIPT (the only configured check: gofmt -l apps/ploeg) — passed.
  • python3 scripts/docs-configuration.py --check — passed; the hand-edited configuration.md is byte-identical to what the generator produces from the changed values.yaml/values.schema.json (including the new ### sandboxOverride shape section and both new rows).
  • python3 scripts/docs.py --check — partial: ADR validators (3 ledgers), the 3 AGENTS.md checks, and repository-link resolution (376 sources / 1981 links) passed; the final mkdocs build --strict could not run (No module named mkdocs, and there is no network to install it). Left to CI.
  • Helm is not on PATH and the sandbox has no egress (get.helm.sh unreachable), so helm-golden.sh check and chart lint were not run — left to CI. As a substitute I verified the new golden structurally: after normalizing the team names, the new golden is byte-identical to the committed ci/golden/executor-sandbox.yaml except for exactly three hunks — the two expected runtimeClassName value changes (kata→gvisor at :600, kata→gvisor-systrap at :773) and the plan's PLOEG_TEAM_PLANS JSON gaining "sandbox":{"runtimeClassName":"gvisor-systrap"}. That whitespace/structure equality with an existing CI-generated golden is strong evidence the new one is a genuine render, not a hand-edit.

Consequence I checked and cleared

The role's sandbox key is serialized into PLOEG_TEAM_PLANS, so it reaches ploegd's plan parser. apps/ploeg/pkg/plan/plan.go uses plain json.Unmarshal into Role and its package doc explicitly states it ignores per-role workload knobs it doesn't consume (model/image/harness, now sandbox). No DisallowUnknownFields, so this cannot stop ploegd at boot and needs no Go change.

Non-blocking notes (no action required)

  • apps/ploeg/docs/contracts/executor.md:116 still reads "the RuntimeClass named in executor.sandbox.runtimeClassName when one is set". With overrides this is now incomplete — a deployment can set per-team/role RuntimeClasses while the global is empty, so the prerequisite is any RuntimeClass reachable through the chain. A one-line update would keep the contract honest; the Work Item only asked for the values comment, so I am not blocking on it.
  • The keda/cronjob worker path still takes no runtimeClassName. The Work Item explicitly defers this ("Escalate if the keda executor path should also honour the key, since that is a design choice this ticket does not make"), so it is not a defect — but a human should know the builder recorded no escalation decision, and the consuming homelab enablement ticket must confirm its target node/executor combination.
  • scripts/helm-golden.sh is edited (a test runner), which the ticket's "Protected" line mentions not editing; the edit only appends two new case registrations and leaves every existing case untouched. That is the required mechanism for the Verification's "Add chart render tests under …/ci/", so I treat it as in scope.
  • Optional/style: the new _sandbox.tpl doc comment is prose; root AGENTS.md says source comments are limited to machine directives/exported API docs. It matches the established convention of the surrounding Helm templates, so I would not change it.

Posted by Ploeg on behalf of the reviewing agent. It could not push to this branch.

### reviewer — round 2 _Chart-only per-team/per-role RuntimeClass override; all four acceptance criteria met; gofmt and docs-configuration checks pass; helm render/lint left to CI (no helm, no egress)._ ## Verdict: approve Reviewed `git diff development...agent/vik-555` (2 commits, `23f7e44` + `327f548`) against VIK-555. Chart-only change; no Go, no agent-instruction/config files touched (none of AGENTS.md / CLAUDE.md / .claude/ / .agents/ / .openhands/ / .mcp.json / .cursorrules appear in the diff), no migrations, no release workflows. ### What the change does - `apps/ploeg/ops/helm/ploeg/templates/_sandbox.tpl:28-42` — new `ploeg.runtimeClassName` helper resolving **role → team → global**, exactly the `default`-chain shape the `harness` fields use in `_helpers.tpl:110-155`. - `apps/ploeg/ops/helm/ploeg/templates/sandbox.yaml:11-14` — sets `pod.spec.runtimeClassName` only when the helper returns a non-empty value (previously read the global `$sb.runtimeClassName` directly). - `values.schema.json:392` (team) and `:765` (planRole) reference a new `sandboxOverride` definition at `:722-732` (`runtimeClassName: string`, `additionalProperties: false`). - `values.yaml:199-207` clarifies the global fallback and adds the Kata/gVisor placement guidance; the team example at `:376-379` shows the override. - New CI fixtures: `ci/executor-sandbox-runtimeclass-values.yaml` + `ci/golden/executor-sandbox-runtimeclass.yaml` (render) and `ci/reject-runtimeclass-non-string-values.yaml` (schema reject), registered in `scripts/helm-golden.sh:57,77`. ### Acceptance criteria — all met 1. **Per-team and per-role override, field by field** — helper chain at `_sandbox.tpl:39-41`; schema permits both `executor.teams[].sandbox.runtimeClassName` and `plan[].roles[].sandbox.runtimeClassName`. The golden proves precedence: `golden/executor-sandbox-runtimeclass.yaml:389` = `kata` (team `kata` takes the global), `:600` = `gvisor` (team override), `:773` = `gvisor-systrap` (reviewer Role override) on `ploeg-worker-kata`, `ploeg-worker-gvisor-builder`, `ploeg-worker-gvisor-reviewer`. 2. **Team without the key → global; all-empty → no field** — helper returns `""` at every tier and `sandbox.yaml:12` guards the `set`. The empty-global case is already covered by the existing `ci/golden/executor-sandbox-team.yaml` (0 `runtimeClassName` occurrences; the fixture sets no global and the `copper` team no override). 3. **Schema accepts the keys, rejects a non-string** — `sandboxOverride` types the value `string` under `additionalProperties: false`; `reject-runtimeclass-non-string-values.yaml` sets `runtimeClassName: 123` and `helm-golden.sh:77` asserts helm refuses it with `runtimeClassName` in the message. 4. **Placement guidance in the values comment** — `values.yaml:203-206` (Kata → bare metal; gVisor systrap → VMs without nested virt, file-I/O penalty on clone-heavy runs). It flows into the generated `docs/reference/configuration.md:204`. ### Checks I ran - `PLOEG_VERIFY_SCRIPT` (the only configured check: `gofmt -l apps/ploeg`) — **passed**. - `python3 scripts/docs-configuration.py --check` — **passed**; the hand-edited `configuration.md` is byte-identical to what the generator produces from the changed `values.yaml`/`values.schema.json` (including the new `### sandboxOverride` shape section and both new rows). - `python3 scripts/docs.py --check` — **partial**: ADR validators (3 ledgers), the 3 `AGENTS.md` checks, and repository-link resolution (376 sources / 1981 links) passed; the final `mkdocs build --strict` could not run (`No module named mkdocs`, and there is no network to install it). Left to CI. - Helm is not on PATH and the sandbox has no egress (`get.helm.sh` unreachable), so `helm-golden.sh check` and chart lint were **not run — left to CI**. As a substitute I verified the new golden structurally: after normalizing the team names, the new golden is byte-identical to the committed `ci/golden/executor-sandbox.yaml` except for exactly three hunks — the two expected `runtimeClassName` value changes (`kata`→`gvisor` at :600, `kata`→`gvisor-systrap` at :773) and the plan's `PLOEG_TEAM_PLANS` JSON gaining `"sandbox":{"runtimeClassName":"gvisor-systrap"}`. That whitespace/structure equality with an existing CI-generated golden is strong evidence the new one is a genuine render, not a hand-edit. ### Consequence I checked and cleared The role's `sandbox` key is serialized into `PLOEG_TEAM_PLANS`, so it reaches ploegd's plan parser. `apps/ploeg/pkg/plan/plan.go` uses plain `json.Unmarshal` into `Role` and its package doc explicitly states it ignores per-role workload knobs it doesn't consume (model/image/harness, now sandbox). No `DisallowUnknownFields`, so this cannot stop ploegd at boot and needs no Go change. ### Non-blocking notes (no action required) - `apps/ploeg/docs/contracts/executor.md:116` still reads "the RuntimeClass named in `executor.sandbox.runtimeClassName` when one is set". With overrides this is now incomplete — a deployment can set per-team/role RuntimeClasses while the global is empty, so the prerequisite is any RuntimeClass reachable through the chain. A one-line update would keep the contract honest; the Work Item only asked for the values comment, so I am not blocking on it. - The keda/cronjob worker path still takes no `runtimeClassName`. The Work Item explicitly defers this ("Escalate if the keda executor path should also honour the key, since that is a design choice this ticket does not make"), so it is not a defect — but a human should know the builder recorded no escalation decision, and the consuming homelab enablement ticket must confirm its target node/executor combination. - `scripts/helm-golden.sh` is edited (a test runner), which the ticket's "Protected" line mentions not editing; the edit only appends two new case registrations and leaves every existing case untouched. That is the required mechanism for the Verification's "Add chart render tests under …/ci/", so I treat it as in scope. - Optional/style: the new `_sandbox.tpl` doc comment is prose; root `AGENTS.md` says source comments are limited to machine directives/exported API docs. It matches the established convention of the surrounding Helm templates, so I would not change it. <sub>Posted by Ploeg on behalf of the reviewing agent. It could not push to this branch.</sub>
Merge branch 'development' into agent/vik-555
All checks were successful
[Workflow] On Pull Request / tutorial-smoke (pull_request) Successful in 28s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 43s
[Workflow] On Pull Request / checks (pull_request) Successful in 2m34s
64a8ec9c1e
Merge branch 'development' into agent/vik-555
All checks were successful
[Workflow] On Pull Request / release-policy (pull_request) Successful in 33s
[Workflow] On Pull Request / tutorial-smoke (pull_request) Successful in 25s
[Workflow] On Pull Request / checks (pull_request) Successful in 3m52s
cbda61dacb
All checks were successful
[Workflow] On Pull Request / release-policy (pull_request) Successful in 33s
[Workflow] On Pull Request / tutorial-smoke (pull_request) Successful in 25s
[Workflow] On Pull Request / checks (pull_request) Successful in 3m52s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin agent/vik-555:agent/vik-555
git switch agent/vik-555

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch development
git merge --no-ff agent/vik-555
git switch agent/vik-555
git rebase development
git switch development
git merge --ff-only agent/vik-555
git switch agent/vik-555
git rebase development
git switch development
git merge --no-ff agent/vik-555
git switch development
git merge --squash agent/vik-555
git switch development
git merge --ff-only agent/vik-555
git switch development
git merge agent/vik-555
git push origin development
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/glide!5
No description provided.