Skip to content

Commit 55709c5

Browse files
committed
env_artifact attest verdict + implement skill spec
attest + qa.test_attestation: When master_run AND fix_run BOTH fail with overlapping compile-class anchors (undefined reference / cannot find -l / fatal error / linker errors / build-script failure), raise env_artifact instead of test_fails_on_fix. attest_cycle's exception handler routes env_artifact to sink (state=OPEN_ENV_ARTIFACT) — terminal on our side, alive upstream. Substrate-env-incompat is real (Catch2 in slang-server's build, docker-root chmod skew in conserve#300 per memory feedback_docker_root_invalidates_chmod_tests) and shouldn't false-fail PRs whose upstream CI is green. _looks_like_env_artifact: structural overlap of compile-class anchors between master and fix slices, with hex/line/path normalization to ignore instance-level noise. skills/implement.md (new): Engagement-lane implementation skill spec. Takes a prior /investigate artifact that named the fix (file:line + change shape) + maintainer feedback, IMPLEMENTS the named change, verifies, pushes amend. Contract is "finish the work" — distinct from /investigate (which produces the plan but may stop at "pending implementation"). Halt conditions are sparing: named location no longer exists, contradictory feedback, test_attestation actually fails after the patch (real implementation failure), push rejected. Actor wiring (sweep/activities/implement.py + lifecycle registration) not yet built — skill spec captures the structural shape. Today's EnzymeAD/Enzyme#2819 H1 patch was applied manually by operator using exactly this shape (read prior artifact + maintainer's inline comment → moved aggregate fallback from TypeAnalysis to AdjointGenerator.h:1894 → committed amend → pushed); the next card of this shape should run through the implement-actor.
1 parent fb0815a commit 55709c5

4 files changed

Lines changed: 239 additions & 73 deletions

File tree

Lines changed: 53 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,76 @@
1-
# VictoriaMetrics/helm-charts#2882Operator admissionWebhooks Aren't Working As Expected
1+
# VictoriaMetrics/helm-charts#2882 — admissionWebhooks accept malformed manifests
22

3-
**Report:** user installs `victoria-metrics-operator` chart v0.62.1. A `VMAgent` manifest containing a non-existent field (`test: true`) is accepted by the cluster. With the upstream `install-with-webhook.yaml` from the operator repo, the same manifest is rejected.
3+
## H₀ (observation)
4+
Helm chart's admission webhooks accept malformed VMAgent manifests (e.g. `spec.test: true`); upstream `install-with-webhook.yaml` from the operator release rejects the same input. Both ship the same operator binary (v0.70.0).
45

5-
## Diagnosis
6-
7-
The bad manifest is silently pruned/accepted because the helm chart, by default, installs **specless CRDs** — every CRD's `spec.spec` schema is just `{ type: object, x-kubernetes-preserve-unknown-fields: true }`. The kube-apiserver's strict field validation (which is what actually rejects `test: true` in the upstream manifest) is bypassed: with `preserveUnknownFields: true` every key under `spec.*` is allowed.
8-
9-
The webhook **is** running and reachable; the webhook config that helm renders is wired correctly (path, port, CA, service all match what the operator expects). The webhook just doesn't catch this kind of error: the VMAgent validator (`internal/webhook/operator/v1beta1/vmagent_webhook.go`) only checks `obj.Status.ParsingSpecError` (empty at CREATE time) and `obj.Validate()` (typed struct validation — unknown fields were already dropped by the JSON decode). It has no way to see `test: true`.
10-
11-
The upstream `install-with-webhook.yaml` works because it ships the **full** openAPIV3Schema (~3000 lines per CRD). The apiserver's structural schema check rejects unknown fields under `spec` before the webhook ever runs.
12-
13-
## Why the chart is shaped this way
6+
## H₁ — webhook routing broken in chart (KILLED, deduction)
7+
**Perturbation**: `helm template charts/victoria-metrics-operator/`; diff rendered Service/Deployment/ValidatingWebhookConfiguration against upstream `install-with-webhook.yaml`.
148

15-
- Commit `0f6a51d19f` / PR [#2419](https://github.com/VictoriaMetrics/helm-charts/pull/2419) made the templated CRDs specless on purpose, to fix #2420: the full schema pushed the helm release Secret over 1 MiB (`Too long: may not be more than 1048576 bytes`) and broke upgrades.
16-
- The full strict CRDs are available via the `crds` subchart, gated on `crds.plain: true`. That path uses a kubectl-apply Job (init container `bzcat` of `crd.yaml.bz2`) to install CRDs server-side instead of through helm-managed manifests, dodging the 1 MiB limit.
9+
- Webhook path `/validate-operator-victoriametrics-com-v1beta1-vmagent` matches upstream.
10+
- Service port 9443 → targetPort `webhook` (containerPort 9443). Plumbing is fine.
11+
- TLS cert SANs cover `<svc>.<ns>.svc` and `.svc.<cluster.dnsDomain>`.
12+
- `failurePolicy: Fail`, so an unreachable webhook would *reject*, not accept. The manifest being accepted proves the webhook is reachable and returns "allow".
1713

18-
## Workaround for users
14+
Trajectory: divergent against. Webhook plumbing is correct.
1915

20-
Set:
16+
## H₂ — operator's validator doesn't see unknown fields (CONFIRMED, deduction)
17+
**Perturbation**: read `internal/webhook/operator/v1beta1/vmagent_webhook.go@v0.70.0`.
2118

22-
```yaml
23-
crds:
24-
plain: true
25-
admissionWebhooks:
26-
enabled: true
19+
```go
20+
func (*VMAgentCustomValidator) ValidateCreate(_ context.Context, obj *vmv1beta1.VMAgent) {
21+
if obj.Status.ParsingSpecError != "" { return ..., errors.New(...) }
22+
if err := obj.Validate(); err != nil { return ..., err }
23+
return nil, nil
24+
}
2725
```
2826

29-
With `crds.plain: true`, the operator chart short-circuits its specless `templates/crd.yaml` (see `{{- if and (not .Values.crds.plain) .Values.crds.enabled }}` guard) and lets the `crds` subchart deliver the full schema via an upgrade Job. Apiserver-side strict validation then rejects unknown fields like `test: true` without any webhook involvement.
27+
Validator runs on a parsed Go struct. Unknown JSON fields (`test: true`) are dropped by `json.Unmarshal` before the validator sees them. The webhook **cannot** catch unknown-field errors on the protocol level — it operates after apiserver→struct decode.
3028

31-
Note: the subchart's `upgrade.enabled` is also `false` by default; users likely need `crds.upgrade.enabled: true` as well for the Job to actually run.
29+
## H₃ — chart ships specless CRDs; upstream ships full schema (CONFIRMED, induction)
30+
**Perturbation**: compare CRDs.
3231

33-
## Hypothesis graph
32+
| Source | VMAgent CRD `spec` schema |
33+
|---|---|
34+
| `charts/victoria-metrics-operator/crd.yaml` (default, `crds.plain: false`) | `{"required":["remoteWrite"],"type":"object","x-kubernetes-preserve-unknown-fields":true}`**specless** |
35+
| `charts/victoria-metrics-operator/charts/crds/crds/crd.yaml` (when `crds.plain: true`) | Full `properties:` schema, no preserve-unknown-fields. 45,807-line file. |
36+
| Upstream `install-with-webhook.yaml` | Full `properties:` schema. |
3437

35-
### H₀ — webhook config malformed (path/port/CA wrong)
38+
With `x-kubernetes-preserve-unknown-fields: true` and no `properties`, the **apiserver itself** accepts any field under `spec`. There is no schema-level rejection.
3639

37-
- **Perturbation:** render the chart, compare each webhook entry to upstream `install-with-webhook.yaml`.
38-
- **Result:** path scheme matches (`/validate-operator-victoriametrics-com-<version>-<singular>`). Port is `9443` (explicit) vs upstream omits (defaults 443) — but the helm Service exposes port 9443 → targetPort `webhook` (=9443 in deployment). CA is self-generated and inlined; cert dir matches operator default `/tmp/k8s-webhook-server/serving-certs`. ObjectSelector excludes `app.kubernetes.io/name = victoria-metrics-operator`; a user-applied VMAgent without that label still matches `NotIn` (label-selector semantics: missing key satisfies NotIn).
39-
- **Trajectory:** convergent — the webhook IS reachable.
40-
- **Status:** killed. Wiring isn't the issue.
41-
- **Mode:** deduction (read the rendered manifest, compared to upstream).
40+
Upstream rejects `test: true` at the **apiserver layer**, before the webhook runs, via CRD-pruning. The webhook is incidental for this failure mode.
4241

43-
### H₁ — operator webhook validator only checks known fields
42+
**Provenance**: helm-charts PR #2419 ("operator: make templated crds specless"), merged 2025-09-27. Closed #2420 and #2334 (rendering perf issues). Deliberate tradeoff: apiserver-side schema validation for chart-render speed.
4443

45-
- **Perturbation:** read `internal/webhook/operator/v1beta1/vmagent_webhook.go`.
46-
- **Result:** `ValidateCreate` returns based on `obj.Status.ParsingSpecError` (not set at create) and `obj.Validate()` (typed struct validation). At decode time, unknown JSON fields not in the Go struct are silently dropped — webhook can't see `test: true`.
47-
- **Trajectory:** divergent (against "webhook can catch this"). Webhook is structurally incapable of catching unknown fields.
48-
- **Status:** confirmed.
49-
- **Mode:** deduction.
50-
51-
### H₂ — specless CRD allows unknown fields through
52-
53-
- **Perturbation:** extract VMAgent CRD from helm `crd.yaml` vs `install-with-webhook.yaml`. Compare schema for `.spec.versions[].schema.openAPIV3Schema.properties.spec`.
54-
- **Result:**
55-
- Helm chart (templated path): 107 lines for VMAgent CRD; `spec: { type: object, x-kubernetes-preserve-unknown-fields: true }`.
56-
- Upstream install-with-webhook: 3096 lines for VMAgent CRD; full property list, no preserveUnknownFields.
57-
- **Trajectory:** divergent (for the diagnosis). With `preserveUnknownFields: true`, apiserver structural-schema rejection doesn't fire on unknown keys under `spec`.
58-
- **Status:** confirmed root cause.
59-
- **Mode:** induction (compared the actual rendered CRDs).
60-
61-
### H₃ — chart provides an opt-in to the strict CRDs
62-
63-
- **Perturbation:** inspect `templates/crd.yaml` and `Chart.yaml`. Trace the `crds.plain` flag.
64-
- **Result:** `crds.plain: true` disables the specless templated CRDs AND enables the `crds` subchart (gated by `condition: crds.plain`), which ships `crds/crd.yaml` containing the full 3000-line schema per CRD and applies via a kubectl Job.
65-
- **Trajectory:** convergent — the chart already has the right escape hatch, it's just not the default.
66-
- **Status:** confirmed.
67-
- **Mode:** deduction.
44+
## Diagnosis
45+
The chart's default-mode (`crds.plain: false`) ships specless CRDs that disable apiserver-side schema validation. The admission webhook validates a Go struct, which silently drops unknown JSON keys. So malformed manifests slip through.
6846

69-
### H₄ — default flip would regress #2420 (>1 MiB helm release Secret)
47+
`install-with-webhook.yaml` includes full-schema CRDs; apiserver rejects unknown fields before the webhook runs.
7048

71-
- **Perturbation:** read PR #2419 and issue #2420.
72-
- **Result:** specless was a deliberate fix for the helm release Secret hitting 1 MiB. Flipping the default back to plain CRDs in `templates/crd.yaml` would resurrect that bug.
73-
- **Trajectory:** divergent against "just change the default". The subchart path (kubectl Job applying CRDs out of helm's Secret) is the only safe way to ship the full schema, and it has its own UX cost (extra Job, extra ServiceAccount).
74-
- **Status:** confirmed — there's no clean default flip.
75-
- **Mode:** deduction (read the prior PR + issue).
49+
Setting `crds.plain: true` (or pre-applying upstream CRDs) restores rejection. Workaround already available; no chart bug in the routing sense.
7650

77-
## Graph state
51+
## Candidate fixes
7852

79-
| Node | Status | Shape | Mode |
80-
|------|--------|-------|------|
81-
| H₀ webhook wiring wrong | killed | convergent | deduction |
82-
| H₁ webhook can't see unknown fields | confirmed | divergent | deduction |
83-
| H₂ specless CRD preserves unknown fields | confirmed (root cause) | divergent | induction |
84-
| H₃ `crds.plain: true` opt-in exists | confirmed | convergent | deduction |
85-
| H₄ default flip would regress #2420 | confirmed | divergent | deduction |
53+
1. **Doc fix (low risk)**: clarify in `values.yaml` and README that `crds.plain: false` ships specless CRDs and that admission webhooks alone do not reject unknown fields; recommend `crds.plain: true` (or applying the upstream CRD bundle) for strict validation.
54+
2. **Default flip to `crds.plain: true`**: re-introduces the rendering perf hit PR #2419 explicitly chose to avoid. Needs maintainer signoff.
55+
3. **Specless-but-strict**: drop `x-kubernetes-preserve-unknown-fields: true` from the specless CRD. Apiserver would then reject **all** spec fields (no properties defined). Breaks valid manifests. Not viable.
8656

87-
## Reframe
57+
## Recommendation
58+
Halt at diagnosis. The fix is a tradeoff only the maintainer can pick.
8859

89-
Original framing: "admissionWebhooks aren't working". Actual finding: webhooks **are** working as designed; the cluster's structural schema validation isn't, because the templated CRDs are deliberately specless to fit under helm's 1 MiB Secret cap. The webhook layer was never the right place to catch unknown-field errors — that's the CRD schema's job.
60+
- Option (1) is a one-paragraph doc clarification.
61+
- Option (2) reverses the explicit decision in PR #2419 and needs maintainer judgment.
62+
- Option (3) is structurally impossible.
9063

91-
## Verdict
64+
Substrate shouldn't ship "a fix" that papers over a deliberate design decision. Route to operator for a yes/no on PRing option (1).
9265

93-
No fix to ship. The behavior is a documented trade-off: chart maintainers picked "small helm releases" over "strict schema by default". Users who want strict validation should set `crds.plain: true` and `crds.upgrade.enabled: true`. A README/values.yaml note clarifying that field-validation strictness depends on `crds.plain` would help future users hit this less, but that's a docs-only nit and not a behavioral bug.
66+
## Reasoning mode table
67+
| Claim | Mode | Confidence |
68+
|---|---|---|
69+
| Webhook routing is correct | deduction (rendered manifests) | 98% |
70+
| Webhook can't see unknown JSON fields | deduction (Go source) | 99% |
71+
| Chart's specless CRDs accept unknown fields; upstream's full CRDs don't | deduction (schema comparison) | 99% |
72+
| PR #2419 was a deliberate perf tradeoff | deduction (PR description) | 95% |
9473

95-
→ Route to tissue (report findings, no PR).
74+
## Frontier (open)
75+
- Live verify in `kind` that `crds.plain: true` restores rejection (untested; pure inference).
76+
- Confirm v0.62.1 (user's version) ships the same specless CRDs as v0.63.0 — almost certainly yes; PR #2419 merged well before v0.62.1.

skills/implement.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
name: implement
3+
description: Take a prior /investigate artifact that named the fix (file:line + change shape) plus maintainer feedback, IMPLEMENT the named change in the existing PR's branch, verify, and push an amend. Distinct from /investigate (which produces the plan but stops at "pending implementation"). Implement's job is to FINISH.
4+
argument-hint: <repo#pr>
5+
allowed-tools: Read, Write, Edit, Grep, Glob, Bash, Agent
6+
---
7+
8+
# Implement: take the plan to a pushed amend
9+
10+
This skill is called by `implement-actor` in the engagement lane,
11+
AFTER /investigate has already produced an artifact that names the
12+
fix (file:line + change shape) and AFTER a maintainer has either
13+
raised a concern naming a different location, requested changes, or
14+
left an inline comment with explicit direction.
15+
16+
The contract is **finish the work**: read the maintainer feedback +
17+
the prior /investigate artifact, IMPLEMENT the named fix, verify
18+
locally, push an amend commit. Do NOT re-run a hypothesis graph —
19+
/investigate already did that. If the artifact doesn't have a clear
20+
named fix, halt and let the actor escalate back to /investigate.
21+
22+
## Phase order
23+
24+
1. **Read the prior artifact**`repo-hypotheses/<owner>__<repo>__<issue>.md`.
25+
It almost certainly names the file, line range, and shape of the
26+
fix the maintainer wants. If the artifact is missing or doesn't
27+
contain a clear directive, escalate to /investigate (you can't
28+
"finish" what hasn't been planned).
29+
30+
2. **Read the maintainer feedback** — inline comments + top-level
31+
comments + review states. The freshest non-author message is
32+
authoritative direction. Quote it verbatim in your reasoning.
33+
34+
3. **Re-confirm location** — open the named file in the worktree,
35+
re-read the relevant lines. The artifact's file:line should still
36+
match the current code; if drift has occurred (rebase against
37+
master moved the line), re-locate via `grep`/`Glob`.
38+
39+
4. **Implement** — Edit the file. Apply the named change. Keep diff
40+
minimal: only what the maintainer asked for. Don't refactor
41+
surrounding code, don't add tests beyond what's needed to verify
42+
the fix.
43+
44+
5. **Verify locally** — run the repo's test command. If a test was
45+
added to the original PR, it should still pass. If new behavior
46+
needs a test, add one focused on the maintainer-named scenario.
47+
The fix MUST fail-on-master / pass-on-fix (test_attestation gate).
48+
49+
6. **Amend + force-with-lease push** — amend the existing top commit
50+
(don't add a new "Address feedback" commit on top — maintainers
51+
prefer rebased history for substantive reworks). Push to the fork
52+
branch. Use `--force-with-lease` not `--force`; protects against
53+
parallel pushes.
54+
55+
7. **Emit decision** — write the per-issue attestation at
56+
`~/.sweep/attestations/implement/<owner>__<repo>__<issue>.md`
57+
with `decision: pushed-amend` and the new HEAD SHA.
58+
59+
## Halt conditions (use sparingly)
60+
61+
You may halt and route to operator only when:
62+
63+
- The artifact's named location no longer exists in the current code
64+
AND grep cannot find a plausible replacement
65+
- The maintainer feedback contradicts itself or is genuinely ambiguous
66+
(rare — most "I don't know what they want" is laziness; re-read)
67+
- The test_attestation gate fails after the patch — meaning your
68+
implementation doesn't actually fix the named bug (real failure;
69+
surface for operator review)
70+
- The push is rejected for reasons beyond `--force-with-lease`
71+
(auth, branch protection)
72+
73+
Do NOT halt for:
74+
75+
- "I'm not sure if this is what the maintainer meant" — they wrote
76+
the comment; take it at face value and implement
77+
- "There might be a better approach" — there might be; the maintainer
78+
chose theirs. Ship what they asked for.
79+
- "The test infrastructure feels brittle" — fix what's named, leave
80+
the rest
81+
82+
## Output contract
83+
84+
After all narration, the **last printed line** must be a single JSON
85+
object matching this schema:
86+
87+
```json
88+
{
89+
"decision": "pushed-amend | halt | escalate-to-investigate",
90+
"head_sha": "<10-char SHA if pushed-amend, else empty>",
91+
"reason": "short string, ≤200 chars",
92+
"rejected": false,
93+
"reject_reason": null
94+
}
95+
```
96+
97+
`decision: pushed-amend` is the success path. The downstream wrapper
98+
kicks reqa to verify the new SHA against the maintainer's named
99+
behavior; on green, respond posts a brief "addressed feedback at
100+
SHA <head_sha>" to the PR.

sweep/activities/attest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,29 @@ async def attest_cycle(msg: Message) -> dict:
535535
return {"verdict": "skip", "target": "sink",
536536
"reason": "test_passes_on_master",
537537
"msg_id": msg.msg_id}
538+
# `env_artifact` is "substrate's local env failed both branches
539+
# with the same error class" — the PR is fine upstream (CI green
540+
# is the ground truth), our container is the problem. Sink as
541+
# OPEN_ENV_ARTIFACT: terminal on our side, alive upstream. The
542+
# week-stale-CI sweep (future) evicts the repo if maintainer
543+
# never moves. Distinct from test_fails_on_fix where master
544+
# actually failed the test (the real bug) and fix should've
545+
# passed but didn't.
546+
if "env_artifact" in reason:
547+
from sweep.activities.pr_state import _sink_pr
548+
if msg.pr:
549+
_sink_pr(msg.repo, int(msg.pr),
550+
reason=f"env_artifact: {reason[:200]}",
551+
state="OPEN_ENV_ARTIFACT")
552+
observe.event(
553+
"attest_routed", repo=msg.repo, branch=msg.branch,
554+
verdict="skip", target="sink",
555+
reason="env_artifact",
556+
msg_id=msg.msg_id,
557+
)
558+
return {"verdict": "skip", "target": "sink",
559+
"reason": "env_artifact",
560+
"msg_id": msg.msg_id}
538561
# "test_fails_on_fix" is a genuine, routable verdict, not
539562
# broken substrate. Everything else (toolchain missing, master
540563
# passing, git checkout failure) propagates to halt the actor.

0 commit comments

Comments
 (0)