Repository navigation
fix(operations): forward DevLoop release pins to mctl-agents workflows - #378
Conversation
StripUndeclared drops every parameter the registry does not declare. The DevLoop sends agent_image and agent_version to mctl-agents-investigate, -implement and -shepherd, and the incident responder sends them to mctl-agents-incidents. Every one of those CWFTs declares both, but none of the operations did, so each run used the CWFT's default image and registry promotions and rollbacks had no effect (#372). Declare both on the four operations. agent_image must be ghcr.io/mctlhq/mctl-agents by semver tag or sha256 digest, the two shapes mctl-agents builds from a published version. agent_version must be <agent>@<semver> for that operation's own agent. Add ParameterDef.OmitWhenEmpty. ApplyDefaults would otherwise fill an absent pin with "", and Argo would run that empty string as the image instead of the CWFT default. With the flag set, an absent or empty pin is not sent at all. Tests: a handler-level table of every parameter set mctl-agents submits per operation, asserting none is dropped; the params still pending a CWFT declaration (human_input_responses, temporal_workflow_id, temporal_run_id, execution_request_id) are asserted to still be stripped. Refs #372 Claude-Session: https://claude.ai/code/session_01TpRNAyt63CpPb9xpQPBy2G
|
Claude finished @mashkovd's task in 3m 33s —— View job Review complete — approved (3 P3, no P1/P2)
What I verifiedThe core mechanism holds end to end:
The two-sided test table ( Findings (all non-blocking)
Fix the pairing gap → · Add the OmitWhenEmpty invariant test → Caveat
|
| {Name: "agent_image", Type: "string", Required: false, OmitWhenEmpty: true, | ||
| Description: "Optional. Released image to run, from the agent registry (ghcr.io/mctlhq/mctl-agents:<version> or @sha256:<digest>). Omit to run the ClusterWorkflowTemplate's default image.", | ||
| Pattern: agentImagePattern}, | ||
| {Name: "agent_version", Type: "string", Required: false, OmitWhenEmpty: true, |
There was a problem hiding this comment.
P3 — agent_version and agent_image are validated independently, so nothing enforces the pairing the description promises ("Omit when agent_image is omitted"). A caller can submit agent_version=issue-investigator@1.33.0 with agent_image=ghcr.io/mctlhq/mctl-agents:1.54.0, or agent_version alone with no image at all; both are accepted and the mismatched value lands in the audit entry (redactSecrets(op, input) in handlers_write.go) as the record of which release ran. Since agent_version is documented as "recorded only", a wrong record is the whole failure mode. ValidateInput is per-parameter with no cross-field hook, but handlers_write.go already does per-operation coupling checks (the approver branch), so a small check there — reject agent_version without agent_image, and require the version's semver to match the image tag when the image is a tag — would close it. Non-blocking: today's only sender builds both from the same registry row.
| result[k] = v | ||
| } | ||
| for _, p := range op.Parameters { | ||
| if p.OmitWhenEmpty { |
There was a problem hiding this comment.
P3 — OmitWhenEmpty silently makes Default dead. The branch continues before the defaulting block, so a future ParameterDef{OmitWhenEmpty: true, Default: "something"} would never apply that default, with no compile-time or test-time signal. The doc comment on the field explains why the value must not be filled, but not that Default is ignored. Cheapest fix is an invariant test over builtinOperations asserting !(p.OmitWhenEmpty && p.Default != ""), plus one line on the field comment. Same shape as the existing registry-wide invariant tests in registry_test.go.
| }, | ||
| // Release pin (mctlhq/mctl-api#372): mctl-agents' IncidentResponderWorkflow | ||
| // sends it; cwft-mctl-agents-run declares both. | ||
| }, agentPinParams("incident-responder")...), |
There was a problem hiding this comment.
P3 — verification note, not a defect found in this diff. mctl-agents-incidents points at the shared mctl-agents-run CWFT, which also backs mctl-agents-run, -mentor-only and -single-service (see the block comment at registry.go:549-556). The correctness of declaring the pin here rests entirely on that one template declaring agent_image/agent_version, which I cannot check from this repo — the PR body's audit against mctl-gitops 94cb4db is the only evidence. Worth re-confirming against the gitops commit that will actually be deployed, since a mismatch fails the workflow at submission for every incident-responder run, not just pinned ones. The three siblings stay unpinned and keep the CWFT default, which matches the audit table.
There was a problem hiding this comment.
No P1/P2 findings (3 P3). Good to merge.
Verified the mechanism end to end: ApplyDefaults deletes the key, ValidateInput already skips empty values so an explicit agent_image="" is not rejected on its way out, and Executor.Submit builds arguments.parameters via buildArgoParams(params) over the map — so an omitted pin genuinely reaches Argo as an absence and the CWFT default applies. The image pattern is anchored with Go's default non-multiline $ (= \z), so the trailing-newline case the test covers is really rejected, and fixing the repository is the right call given the value becomes the pod image on an admin-only operation.
P3s: no cross-field validation between agent_image and agent_version; OmitWhenEmpty silently makes Default dead with no invariant test; and the mctl-agents-incidents declaration depends on the shared mctl-agents-run CWFT, which is worth re-confirming against the gitops commit that ships.
Note: go test could not be executed in this review sandbox, so the PR's claim that go test -p 1 ./... passes was not independently confirmed; CI covers it.
Antigravity (agy) review — informationalModel: gemini-3.8-flash-medium · commit No significant issues found. |
Part of #372: the DevLoop release pins. The loop-id params (
temporal_workflow_id,temporal_run_id,execution_request_id) stay held back until the mctl-agents release containing the investigator CLI flags ships, andhuman_input_responsesneeds its gitops CWFT declaration first.Problem
StripUndeclareddroppedagent_imageandagent_versionfor everymctl-agents-*operation. Every DevLoop run therefore used its CWFT's default image, and registry promote or rollback had no effect.What
agent_imageandagent_versionare declared, optional, onmctl-agents-investigate,-implement,-shepherdand-incidents, throughagentPinParams(agent).agent_image:^ghcr\.io/mctlhq/mctl-agents(:<semver>[-pre]|@sha256:<64 hex>)$. The repository is fixed because the value becomes the pod image.agent_version:<agent>@<semver>[-pre], where each operation accepts only its own agent name (issue-investigator,implementer,shepherd,incident-responder).ParameterDef.OmitWhenEmpty. Declaring the pin alone would have been harmful:ApplyDefaultsfills a missing param with"", and forwardingagent_image=""overrides the CWFT's default image with an empty one for every unpinned caller (the directive poller, the implement sweep, MCP triggers, and the incident responder when its registry lookup fails). WithOmitWhenEmpty, such a param is never defaulted, and it is removed when empty, so the CWFT default still applies.Audit (mctl-agents
271beaa, mctl-gitops94cb4db)dev_loop.py)dev_loop.py)dev_loop.py)incidents.py)Behaviour change on release
Once released, DevLoop runs use the registry-released image (today 1.54.0) instead of the CWFT default (1.53.1). This is the intended fix, and it belongs in the release notes. The comment in mctl-agents
incidents.pyclaiming that mctl-api passes undeclared params through is wrong; it needs a follow-up there.Tests
internal/api/handlers_write_devloop_params_test.go, which goes through the real handler asmctl-agent, covers three things:"", sends noagent_imageto Argo.registry_test.go: pin pass-through, pin patterns (tag, digest,latest, a doubled tag, a foreign repo, a newline, the wrong agent), and omission when empty.OmitWhenEmpty;go test -p 1 ./...passes. golangci-lint reports only the existing G710.mctl-agents-*operations" section indocs/agent-platform-registry.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01TpRNAyt63CpPb9xpQPBy2G