Skip to content

🐛 Report ACP readiness on AgentRun and probe the sandbox ACP port - #160

Merged
djzager merged 1 commit into
konveyor:mainfrom
ibolton336:fix/sandbox-readiness-gate
Aug 19, 2026
Merged

djzager merged 1 commit into
konveyor:mainfrom
ibolton336:fix/sandbox-readiness-gate

Conversation

@ibolton336

@ibolton336 ibolton336 commented Aug 19, 2026

Copy link
Copy Markdown
Member

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 #130. Refs #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 (#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 (✨ Add Agent endpoints. 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: #157 edits the adjacent container-spec lines; the two merge cleanly in either order.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ecce35a-4b74-401f-85ce-39384131d933


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jortel

jortel commented Aug 19, 2026

Copy link
Copy Markdown

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>
@ibolton336
ibolton336 force-pushed the fix/sandbox-readiness-gate branch from fe6fc3b to f30937c Compare August 19, 2026 14:48
@ibolton336 ibolton336 changed the title 🐛 Gate AgentRun Running on Sandbox Ready and probe the ACP port 🐛 Report ACP readiness on AgentRun and probe the sandbox ACP port Aug 19, 2026
ibolton336 added a commit to ibolton336/agentcontroller-client that referenced this pull request Aug 19, 2026
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>
@ibolton336

Copy link
Copy Markdown
Member Author

Agreed — that's the better shape, and the PR now does exactly that (force-pushed as f30937c; title/body updated):

  • phase=Running when the sandbox pod phase is Running (agent process executing). Still a fix vs. today, where Running fires when the Sandbox object exists.
  • New condition ACPReady (v1alpha1.AgentRunConditionACPReady): True/Listening once the Sandbox is Ready — the agent container's new tcpSocket:4000 readiness probe passes and the sandbox Service exists — else False/NotListening (carrying the sandbox's message, e.g. "Pod is Running but not Ready"), and False/Finished at the end. Clients dial on this, never on phase.

The kind e2e asserts the split: pod Running + :4000 closed ⇒ phase=Running, ACPReady=False (NotListening); ACPReady flips True together with pod/Sandbox Ready; first dial after that succeeds, no retry. Happy to rename the condition if you'd rather Listening/Ready-something — the existing Ready condition already means "run outcome", which is why I didn't reuse it.

@djzager djzager left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK

@djzager
djzager self-requested a review August 19, 2026 21:55

@djzager djzager left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • ACPReady stays 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.
  • phase stays as the coarse compat mirror (ADR 0012 froze it; waitForRunning/isTerminalPhase key off it). #119 keeps it in lockstep; a later ADR supersedes 0012's phase clause and drops phase + Ready once clients read Succeeded.
  • #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 opaque terminationData.

So: ship the readiness fix now; I'll do the ReadySucceeded 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.

@djzager
djzager merged commit 0360a99 into konveyor:main Aug 19, 2026
15 checks passed
dymurray pushed a commit to djzager/agentic-controller that referenced this pull request Aug 25, 2026
…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>
djzager added a commit that referenced this pull request Aug 26, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Readiness probe on run pods for the ACP port

3 participants