🐛 Report ACP readiness on AgentRun and probe the sandbox ACP port - #160
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Q: Is it true (correct) that sandbox accepting ACP means Phase=running? Or would it be more correct for Phase=Running when the sandbox pod phase=Running and there be a condition that indicates READY (or ALIVE, LISTENING, etc)? |
phase=Running was set the moment the Sandbox object existed — before
the pod was scheduled, the image pulled, the repo cloned, goose started
or the harness bound :4000 — and the sandbox pod carried no readiness
probe, so nothing told a client when :4000 would accept. Every ACP
client papered over the same window with dial-retry loops.
Two facts were hiding in one field, so they are now two fields:
phase follows the agent process. Pending until the sandbox pod
is Running, then Running (one-way), then Succeeded/Failed
when the Sandbox reports Finished. StartTime is the pod's
start. A run that finishes before the controller sees its
pod run may go straight from Pending to a terminal phase.
ACPReady a new condition that says whether the agent's ACP endpoint
accepts connections. The sandbox pod's agent container now
declares port acp/4000 with a tcpSocket readiness probe;
the Sandbox Ready condition (pod Ready + Service present)
turns ACPReady True with reason Listening, else False with
NotListening (carrying the sandbox's message) and Finished
once the run ends. Clients dial on ACPReady=True, never
on phase.
The harness already makes an accepting :4000 a true readiness signal:
it binds the port only after clone, grounding, goose serve, a 30s
WaitReadyDial and session/new (goose owns :4000 directly with
HARNESS_ACP_TEE=off and is dialed the same way), so the tcpSocket probe
holds for both topologies. The controller watches sandbox pods through
the konveyor.io/agentrun label they already carry, with the manager's
Pod cache restricted to that label; RBAC gains pods get/list/watch.
The kind e2e stub never listened on :4000 (sleep infinity) and nothing
asserted phase; it now serves the port like a real agent (python3
http.server after a configurable startup delay) and hack/run-e2e.sh
asserts the contract: pod running + :4000 closed reads phase=Running /
ACPReady=False(NotListening); ACPReady turns True together with pod and
Sandbox Ready; pod Ready did not precede the stub binding the port; a
single dial of the pod's :4000 right after ACPReady succeeds from an
in-cluster curl pod that was Ready before the run existed — no retry
(the Service-name dial gets a short bounded window, since its A record
also depends on EndpointSlice -> CoreDNS). setup-e2e.sh now restarts
the manager so reruns on a reused cluster test the rebuilt image.
Verified: make lint (0 issues), make test (envtest), kind e2e 16/16 on
a fresh cluster; the same script against a controller built from main
fails the readiness assertions.
Fixes konveyor#130
Refs konveyor#65
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: ibolton336 <ibolton@redhat.com>
fe6fc3b to
f30937c
Compare
agentic-controller-system manager → ghcr.io/ibolton336/agentic-controller @sha256:6986585c… (konveyor/agentic-controller#160 @ f30937c; ClusterRole patched with pods get/list/watch; rollback quay.io/konveyor/ agentic-controller:latest) and deploy/tackle2-ui → sha256:4e1cc1fa… (feature/agent-runs @ d6d50ba50, dials once on ACPReady). Rollback refs kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Agreed — that's the better shape, and the PR now does exactly that (force-pushed as f30937c; title/body updated):
The kind e2e asserts the split: pod Running + |
djzager
left a comment
There was a problem hiding this comment.
Approving — this fixes a real, measured client-facing bug (#130, the Running-before-dialable window that's been forcing retry loops in the hub relay and tackle2-ui), and the core moves are right: splitting the live-endpoint fact out of phase is the correct instinct, ACPReady is a genuinely orthogonal condition, and the pod-read plumbing (label-restricted cache, resolve-by-name, pods: get/list/watch RBAC) is exactly what the status story needs. The e2e that asserts pod-Running + :4000-closed reads Running/ACPReady=False is a nice touch.
Heads-up on what's coming next so it's not a surprise, not a request to change anything here: when I implement #119 (LimitReached + terminationData, ADR 0011), I'm going to introduce a Knative/Job/Tekton-style Succeeded condition (True/False/Unknown + reason) as the terminal signal, and retire Ready-as-terminal into it. Reasoning: AgentRun is a run-to-completion object, and the k8s primitive it resembles is Job (which uses Complete/Failed conditions), not Pod — so Ready (a Pod steady-state 'serving traffic' property) is the wrong shape for 'did this run finish cleanly, and if not why' (Failed vs LimitReached vs, later, Refused/NoChanges). It also matches the Tekton alignment we already took with $(params.x) substitution.
What that means concretely, and why it doesn't touch this PR:
ACPReadystays exactly as you built it — it's the one legitimately Pod-Ready-like fact (the live ACP window), orthogonal to the terminal outcome. No change.phasestays as the coarse compat mirror (ADR 0012 froze it;waitForRunning/isTerminalPhasekey off it). #119 keeps it in lockstep; a later ADR supersedes 0012's phase clause and dropsphase+Readyonce clients readSucceeded.- #119 builds on this PR's pod-read — it just extends the read to pull container
exitCode+terminationMessage, maps exit 2 →Succeeded{False, LimitReached}+phase Failed, and stores opaqueterminationData.
So: ship the readiness fix now; I'll do the Ready→Succeeded terminal correction in #119 before more clients pile onto Ready. Flagging early so we're aligned on the direction rather than discovering it in the #119 diff.
…nveyor#160) ## Why `AgentRun.status.phase=Running` is set the moment the Sandbox object exists — before the pod is scheduled, the image pulled, the repo cloned, `goose serve` started, or the harness bound `:4000`. The sandbox pod has no readiness probe, so pod `Ready` and Sandbox `Ready` both flip true when the process starts, and nothing tells a client when `:4000` will accept. Every ACP client (the hub relay single-dials and closes the browser socket on failure; tackle2-ui and the earlier hub-shim grew dial-retry loops) has been papering over the same window. Two facts were hiding in one field. This PR makes them two fields, the way pods separate phase from the `Ready` condition. Fixes konveyor#130. Refs konveyor#65 (cancel: `Pending` can now have a live Sandbox — see below). ## What - **`phase` follows the agent process.** `Pending` until the sandbox pod is Running, then `Running` (one-way), then `Succeeded`/`Failed` when the Sandbox reports Finished. `StartTime` is the pod's start (container `startedAt`, else pod `startTime`, else Sandbox creation), so `Duration` is the run's wall time. A run that finishes before the controller sees its pod run may go straight from `Pending` to a terminal phase. This is already a fix vs. today (Running at Sandbox-object creation). - **`ACPReady` condition (new)** says whether the agent's ACP endpoint accepts connections: - the sandbox pod's `agent` container declares port `acp/4000` and a `tcpSocket:4000` readiness probe (`periodSeconds: 2`). Readiness only gates the pod's `Ready` condition; it never restarts the container. Expect kubelet `Unhealthy` events during startup — that window is the point. - the Sandbox `Ready` condition (agent-sandbox v0.5.0: pod Running + Ready + podIP, and the headless Service present) turns `ACPReady=True` / reason `Listening` with the address in the message; otherwise `False` / `NotListening` carrying the sandbox's own message ("Pod is Running but not Ready" …); `False` / `Finished` once the run ends. - **clients dial on `ACPReady=True`, never on `phase`.** The constant is exported as `v1alpha1.AgentRunConditionACPReady`. - **Controller plumbing:** the reconciler reads the sandbox pod (named after the Sandbox) and watches Pods via the `konveyor.io/agentrun` label it already stamps on the PodTemplate; the manager's Pod cache is restricted to that label (`controller.SandboxPodCacheOptions()`), so this does not mean caching every pod in the cluster. RBAC gains `pods: get/list/watch`. `Finished` is still checked first, so a crash before binding goes `Pending → Failed`. - **Field docs + CRD regenerated** to say what `phase`, `StartTime`, `Ready` and `ACPReady` mean. - **E2E:** the kind e2e stub (`images/agentic-controller-agent`) used to `sleep infinity` and never listen; nothing asserted phase. It now serves `:4000` like a real agent (`python3 -m http.server` after a configurable startup delay — `STUB_ACP_DELAY_SECONDS`, 12 s in `hack/e2e/resources.yaml`), and `hack/run-e2e.sh` asserts the contract: **pod Running + `:4000` closed reads `phase=Running` / `ACPReady=False (NotListening)`** (dialed by pod IP so no DNS negative cache is seeded; SKIP, never a false pass, if the window isn't observable); **`ACPReady` turns True** together with pod Ready + Sandbox Ready; **pod Ready did not precede the stub's own "listening" line** (timing-independent); **a single dial of the pod's `:4000` right after `ACPReady` returns 200** from an in-cluster curl pod that was Ready before the run existed — no retry; and `<sandbox>.<ns>.svc:4000` answers within a bounded 10 s window (its A record rides EndpointSlice → CoreDNS, which nothing in the status chain waits for). The dialer reuses the already-loaded e2e agent image (no registry pull); `hack/setup-e2e.sh` restarts the manager so reruns on a reused cluster test the rebuilt image. Resulting status on a live run: ``` phase: Running Ready=False (Running): Agent is running ACPReady=True (Listening): ACP endpoint e2e-run.default.svc:4000 accepts connections ``` ## What changes for clients Dial on `conditions[type=ACPReady].status == "True"`. A refused dial after that is a real error, not startup lag. Two honest caveats: (1) the sandbox Service is headless without `publishNotReadyAddresses`, so before readiness the name has no A record — a client that dials by DNS early sees "no such host" instead of connection refused; (2) the Service name's record lands via EndpointSlice → CoreDNS, independent of the Sandbox → AgentRun status chain, so a DNS-name dial within ~a second of the flip can in principle still race (pod-IP dials cannot). The multi-second startup race is closed; that sub-second DNS tail is not something this controller can promise. `phase` keeps meaning "executing": a run whose listener never binds (tee bind failure — harness README updated) shows `Running` with `ACPReady=False (NotListening)` instead of silently looking dialable. Runs whose pod never starts (ImagePullBackOff under `restartPolicy: Never`, unschedulable) stay `Pending` with reason `PodNotRunning` instead of reporting `Running`. Cancel (konveyor#65, ADR 0006) assumed `Pending` implies no Sandbox; after this change Pending can have a live Sandbox, so cancel must delete the Sandbox in Pending too. ## Docs ADRs are immutable and none records the old Running trigger; ADR 0003's flow (wait, then dial `svc:4000`) is what `ACPReady` makes true. The contract lives in the `AgentRun` field docs/CRD, the exported condition constant, the changelog fragment, and a clause in `harness/README.md`. ## Verified - `make lint` (0 issues), `make test` (envtest controller suite green), `make manifests`/`generate` clean. The contract itself is covered by the kind e2e rather than envtest: it needs a real kubelet probe and the sandbox controller to mean anything. - `CONTAINER_TOOL=docker make e2e` on a fresh kind cluster + agent-sandbox v0.5.0: 16/16 PASS, 0 skipped. The same script against a controller built from `origin/main` fails the readiness assertions (no `ACPReady`, Running before the port accepts). - Context from the real harness on OpenShift (konveyor/tackle2-hub#1119 findings): the pre-fix window measured ~9–10 s past Running, 4 dial attempts from the client; this is what lets the UI/hub retry loops go. Note: konveyor#157 edits the adjacent container-spec lines; the two merge cleanly in either order. Signed-off-by: ibolton336 <ibolton@redhat.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Implements ADRs 0009/0011 on the controller, with amendments captured in a new **ADR 0018**. Closes #115 Closes #116 Closes #119 Closes #87 ## What's here **Params (#115/#116)** - Rename `AgentParam`/`AgentRunParam`/`AgentParamType` → `Param`/`ParamValue`/`ParamType` (Go only; CRD field names unchanged). - Deliver typed params via `/run/konveyor/params.json` (workflow/agent/execution sections), mounted read-only; remove the old `KONVEYOR_PARAM_*` env vars. - `$(scope.name)` substitution across the agent prompt, run instructions, workflow guide, and stage instructions. Unresolved refs and bad type coercion fail the run terminally with `InvalidParams`. - Workflow-level params on `AgentWorkflow`; resolved workflow params stamped onto stage AgentRuns. **Execution controls (#115)** - `ExecutionLimits` (`maxTurns`, `maxCost`) are Agent defaults, overridable per stage; `ExecutionSpec` (`mode` + limits) lives on AgentRun and stages. The Agent **cannot** declare `mode` (execution-time concern — ADR 0011/0018). Single `resolveExecution` rule. **Terminal model (#119)** - AgentRun gains a Knative/Tekton-style `Succeeded` condition (`Unknown` while running, `True`/`False` terminal) and opaque `terminationData` read from the pod termination message. - Harness exit `0`/`1`/`2` → `Succeeded` / `Failed` / `LimitReached`. - `Ready` is removed from AgentRun; `ACPReady` (serving) and `phase` (coarse mirror) are unchanged. **Workflow snapshot (#87)** - AgentWorkflowRun snapshots the full stage definitions, guide, and params at initialization and executes from the snapshot, so a mid-run workflow edit cannot change stages already planned. ## ADR 0018 Amends ADR 0011 (execution controls) and ADR 0009 (parameter delivery) — three places where the merged text didn't survive contact with the controller: stage limits had nowhere to land, exit-2 → `Succeeded` contradicted workflow sequencing, and a workflow guide has no single agent scope. Builds on the pod-read plumbing merged in #160. ## Test plan - `make test` — envtest suite green (unit coverage 78.4%), including terminal-outcome exit-code mapping and the three-section params.json. - `make lint` — 0 issues. - `make generate manifests` — CRDs regenerated and committed. --------- Signed-off-by: David Zager <david.j.zager@gmail.com>
Why
AgentRun.status.phase=Runningis set the moment the Sandbox object exists — before the pod is scheduled, the image pulled, the repo cloned,goose servestarted, or the harness bound:4000. The sandbox pod has no readiness probe, so podReadyand SandboxReadyboth flip true when the process starts, and nothing tells a client when:4000will accept. Every ACP client (the hub relay single-dials and closes the browser socket on failure; tackle2-ui and the earlier hub-shim grew dial-retry loops) has been papering over the same window.Two facts were hiding in one field. This PR makes them two fields, the way pods separate phase from the
Readycondition.Fixes #130. Refs #65 (cancel:
Pendingcan now have a live Sandbox — see below).What
phasefollows the agent process.Pendinguntil the sandbox pod is Running, thenRunning(one-way), thenSucceeded/Failedwhen the Sandbox reports Finished.StartTimeis the pod's start (containerstartedAt, else podstartTime, else Sandbox creation), soDurationis the run's wall time. A run that finishes before the controller sees its pod run may go straight fromPendingto a terminal phase. This is already a fix vs. today (Running at Sandbox-object creation).ACPReadycondition (new) says whether the agent's ACP endpoint accepts connections:agentcontainer declares portacp/4000and atcpSocket:4000readiness probe (periodSeconds: 2). Readiness only gates the pod'sReadycondition; it never restarts the container. Expect kubeletUnhealthyevents during startup — that window is the point.Readycondition (agent-sandbox v0.5.0: pod Running + Ready + podIP, and the headless Service present) turnsACPReady=True/ reasonListeningwith the address in the message; otherwiseFalse/NotListeningcarrying the sandbox's own message ("Pod is Running but not Ready" …);False/Finishedonce the run ends.ACPReady=True, never onphase. The constant is exported asv1alpha1.AgentRunConditionACPReady.konveyor.io/agentrunlabel it already stamps on the PodTemplate; the manager's Pod cache is restricted to that label (controller.SandboxPodCacheOptions()), so this does not mean caching every pod in the cluster. RBAC gainspods: get/list/watch.Finishedis still checked first, so a crash before binding goesPending → Failed.phase,StartTime,ReadyandACPReadymean.images/agentic-controller-agent) used tosleep infinityand never listen; nothing asserted phase. It now serves:4000like a real agent (python3 -m http.serverafter a configurable startup delay —STUB_ACP_DELAY_SECONDS, 12 s inhack/e2e/resources.yaml), andhack/run-e2e.shasserts the contract: pod Running +:4000closed readsphase=Running/ACPReady=False (NotListening)(dialed by pod IP so no DNS negative cache is seeded; SKIP, never a false pass, if the window isn't observable);ACPReadyturns True together with pod Ready + Sandbox Ready; pod Ready did not precede the stub's own "listening" line (timing-independent); a single dial of the pod's:4000right afterACPReadyreturns 200 from an in-cluster curl pod that was Ready before the run existed — no retry; and<sandbox>.<ns>.svc:4000answers within a bounded 10 s window (its A record rides EndpointSlice → CoreDNS, which nothing in the status chain waits for). The dialer reuses the already-loaded e2e agent image (no registry pull);hack/setup-e2e.shrestarts the manager so reruns on a reused cluster test the rebuilt image.Resulting status on a live run:
What changes for clients
Dial on
conditions[type=ACPReady].status == "True". A refused dial after that is a real error, not startup lag. Two honest caveats: (1) the sandbox Service is headless withoutpublishNotReadyAddresses, so before readiness the name has no A record — a client that dials by DNS early sees "no such host" instead of connection refused; (2) the Service name's record lands via EndpointSlice → CoreDNS, independent of the Sandbox → AgentRun status chain, so a DNS-name dial within ~a second of the flip can in principle still race (pod-IP dials cannot). The multi-second startup race is closed; that sub-second DNS tail is not something this controller can promise.phasekeeps meaning "executing": a run whose listener never binds (tee bind failure — harness README updated) showsRunningwithACPReady=False (NotListening)instead of silently looking dialable. Runs whose pod never starts (ImagePullBackOff underrestartPolicy: Never, unschedulable) stayPendingwith reasonPodNotRunninginstead of reportingRunning. Cancel (#65, ADR 0006) assumedPendingimplies no Sandbox; after this change Pending can have a live Sandbox, so cancel must delete the Sandbox in Pending too.Docs
ADRs are immutable and none records the old Running trigger; ADR 0003's flow (wait, then dial
svc:4000) is whatACPReadymakes true. The contract lives in theAgentRunfield docs/CRD, the exported condition constant, the changelog fragment, and a clause inharness/README.md.Verified
make lint(0 issues),make test(envtest controller suite green),make manifests/generateclean. The contract itself is covered by the kind e2e rather than envtest: it needs a real kubelet probe and the sandbox controller to mean anything.CONTAINER_TOOL=docker make e2eon a fresh kind cluster + agent-sandbox v0.5.0: 16/16 PASS, 0 skipped. The same script against a controller built fromorigin/mainfails the readiness assertions (noACPReady, Running before the port accepts).Note: #157 edits the adjacent container-spec lines; the two merge cleanly in either order.