Skip to content

fix(controller): don't mount the declarative agent config on BYO agents - #2573

Open
rtemperini wants to merge 3 commits into
kagent-dev:release/v0.10.xfrom
rtemperini:fix/byo-config-secret
Open

fix(controller): don't mount the declarative agent config on BYO agents#2573
rtemperini wants to merge 3 commits into
kagent-dev:release/v0.10.xfrom
rtemperini:fix/byo-config-secret

Conversation

@rtemperini

Copy link
Copy Markdown

Fixes #2571.

Targets release/v0.10.x deliberately, not main — see Why this branch below.

What's broken

Since b04769e8 (#2171) the compiler builds a minimal AgentConfig for BYO agents.
buildConfigSecret gated the rendered config and its volume on a bare if cfg != nil
with no agent-type distinction, so BYO agents started receiving
/config/config.json containing {"model":null,"description":"…","instruction":""}.

The runtime schema requires a model (model: ModelUnion = Field(discriminator="type"),
required, no default), so a BYO image that loads that file on startup fails validation
and crashloops:

pydantic_core._pydantic_core.ValidationError: 1 validation error for AgentConfig
model
  Input should be a valid dictionary or object to extract fields from
  [type=model_attributes_type, input_value=None, input_type=NoneType]

Rendering the config succeeds, so the controller logs nothing — the only symptom is
the agent pod crash looping, which points at the user's image rather than at the
rendered config.

First affected tag is v0.10.0-beta7; v0.10.0-beta6 is the last clean 0.10.x. No
v0.9* tag contains b04769e8.

The fix

1. Gate the rendered config and its volume on agent type (manifest_builder.go).
A new needsAgentConfig predicate sits next to the existing needsSRTSettings
carve-out, matching how BYO is already special-cased in this file. This restores the
pre-b04769e8 behaviour where BYO agents received no config volume at all.

I chose this over restoring cfg = nil for BYO in the compiler, because that is not a
clean revert: compiler.go:160 dereferences cfg.SessionDBURL unguarded when the
agent runs in sandbox workload mode, so a nil cfg would panic there. Gating in
buildConfigSecret also leaves the declarative path byte-identical — the only golden
fixture that changes is the BYO one.

Two details worth flagging for review:

  • The Secret keeps its config.json key, now empty, rather than dropping it. The
    substrate backend injects that key as a secret-backed env var
    (agent_lifecycle.gosecretEnv("KAGENT_CONFIG_JSON", secretName, "config.json")),
    and removing the key would break that reference. An empty value is inert on both
    paths: the volume isn't mounted for BYO, and _config_materialize.py skips
    materializing empty values.
  • Sandboxed BYO agents still get the /config volume, because they need
    srt-settings.json. Their config.json stays empty. There's a test for this.

2. omitempty on AgentConfig.Model (go/api/adk/types.go) — defence in depth,
not the fix. Please don't let this one line stand in for the change above:

Rendered config.json pydantic result
{"model":null,…} (before) type='model_attributes_type'"Input should be a valid dictionary or object…"
{"description":…} (with omitempty only) type='missing'"Field required"

The file still exists and is still mounted; only the error changes. omitempty is
worth having because a nil interface should not serialize to null in the first place,
and it's the part that still applies on main, where AgentConfig survives but this
BYO branch does not. AgentConfig.UnmarshalJSON already treats an absent model the
same as a null one, so the config still round-trips.

Why this branch

main won't help anyone here. 26732e86 (#2565) deleted
go/core/internal/controller/translator/ outright; git tag --contains 26732e86
returns nothing and that commit is not on release/v0.10.x. On main there is no
Agent reconciler, so a BYO Agent isn't reconciled at all.

Meanwhile v0.10.0-rc3 is the newest tag and the newest published image, with no
0.10.0 final and no rc4rc3 is what users are running today. release/v0.10.x
has unreleased commits past rc3, none touching this code
(git diff v0.10.0-rc3 origin/release/v0.10.x -- .../translator/agent/ is empty).

Happy to also open a main PR carrying just the omitempty change if you'd like the
two branches consistent.

Tests

Added to manifest_builder_test.go, alongside the existing needsSRTSettings coverage:

  • TestNeedsAgentConfig — the predicate itself.
  • TestBuildConfigSecret_BYOOmitsAgentConfig — the regression guard: a BYO agent gets
    an empty config.json, no volume and no mount.
  • TestBuildConfigSecret_BYOWithSandboxMountsOnlySRTSettings — sandboxed BYO still
    gets /config for srt-settings.json, with config.json still empty.
  • TestBuildConfigSecret_DeclarativeKeepsAgentConfig — pins the declarative path.

I verified the regression guard actually catches this: reverting just the
needsAgentConfig call to cfg != nil fails both BYO tests with
config.json = "{\"description\":\"A BYO test agent\",\"instruction\":\"\"}" — which
is also the concrete demonstration that omitempty alone leaves a config.json in
place.

The byo_agent.json golden fixture is regenerated (UPDATE_GOLDEN=true). It had been
recording the "model": null config and the /config mount as expected output, which
is why nothing flagged this. Note the fixture's kagent.dev/config-hash goes to "0",
matching the pre-b04769e8 empty hash input; existing BYO pods will roll once on
upgrade.

Results

$ go test ./api/... ./core/internal/controller/...
ok  github.com/kagent-dev/kagent/go/api/adk                                         0.305s
ok  github.com/kagent-dev/kagent/go/api/v1alpha2                                   19.134s
ok  github.com/kagent-dev/kagent/go/core/internal/controller                        1.105s
ok  github.com/kagent-dev/kagent/go/core/internal/controller/predicates             0.659s
ok  github.com/kagent-dev/kagent/go/core/internal/controller/provider               0.330s
ok  github.com/kagent-dev/kagent/go/core/internal/controller/reconciler             2.983s
ok  github.com/kagent-dev/kagent/go/core/internal/controller/translator/agent       0.709s

gofmt -l clean and go vet clean on all three touched files.

One note on linting: make -C go lint does not run in my environment — the
locally-built kube-api-linter plugin fails to load with
fatal error: runtime: no plugin module data (a Go plugin ABI mismatch against the
go1.26.5 toolchain). I confirmed this reproduces identically on an unmodified
release/v0.10.x worktree, so it's environmental and not caused by this change. As a
substitute I ran golangci-lint run --no-config over the two touched packages: it
reports 3 pre-existing errcheck findings, all in files this PR does not touch
(api/adk/types_test.go:725-726, adk_translator_golden_test.go:47) and none in the
changed code. Worth a maintainer confirming CI lint passes.

BYO agents run their own image and do not share the declarative runtime's
configuration schema. Since b04769e the compiler builds a minimal
AgentConfig for them, and buildConfigSecret rendered it into config.json
and mounted it at /config because it only checked that the config was
non-nil.

That config carries no model. The runtime schema requires one, so a BYO
image that loads /config/config.json on startup fails validation and
crashloops. Nothing is logged, because rendering the config succeeds.

Gate the rendered config and its volume on the agent type, restoring the
behaviour BYO agents had before b04769e, where they received no config
volume at all. The Secret keeps its config.json key, since the substrate
backend injects it as a secret-backed env var, but leaves it empty; the
runtime skips materializing empty values.

The one case where a BYO agent still needs the volume is a sandbox
config, which populates srt-settings.json. That path is unchanged and is
covered by a test.

Signed-off-by: Ricardo Temperini <29879569+rtemperini@users.noreply.github.com>
A nil Model marshalled to "model":null, which the python runtime rejects
with a model_attributes_type validation error before it can read any
other field.

This is defence in depth, not a fix on its own. The runtime declares
model as required with no default, so a config with no model is invalid
input either way; omitting the key only changes the error from
model_attributes_type to "Field required". Callers that must not receive
an agent config have to be excluded upstream of marshalling.

AgentConfig.UnmarshalJSON already treats an absent model the same as a
null one, so the config still round-trips.

Signed-off-by: Ricardo Temperini <29879569+rtemperini@users.noreply.github.com>
@rtemperini
rtemperini requested a review from a team as a code owner August 26, 2026 11:21
@github-actions github-actions Bot added the bug Something isn't working label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants