Skip to content

Suggest cortex serve when a context named "cortex" refuses the connection - #40

Closed
esnible wants to merge 1 commit into
mainfrom
cortex-serve-hint
Closed

Suggest cortex serve when a context named "cortex" refuses the connection#40
esnible wants to merge 1 commit into
mainfrom
cortex-serve-hint

Conversation

@esnible

@esnible esnible commented Aug 10, 2026

Copy link
Copy Markdown
Member

Problem

Point a context at a local rossoctl cortex serve, forget to start it, and the CLI says only this:

Error: requesting http://localhost:9097/api/v1/agents?namespace=team1: Get "...": dial tcp [::1]:9097: connect: connection refused

Accurate, and silent about the remedy — which in this one case the CLI can name, because cortex serve is the only server rossoctl can start itself.

What this does

Adds the hint to errorHint, the single place every command's error is already printed from Execute. That covers agents, tools, status, envvars, and anything added later, instead of repeating it at the eleven call sites that reach a client.

Error: requesting http://localhost:9097/api/v1/agents?namespace=team1: Get "...": dial tcp [::1]:9097: connect: connection refused
Hint: nothing is listening at http://localhost:9097/api/v1/. Run `rossoctl cortex serve` to start the local API.

Design notes

The condition is errors.Is(err, syscall.ECONNREFUSED), not the message text. apiclient wraps the dial failure with %w, so the errno survives in the chain. Matching the syscall distinguishes a refused connection from a timeout or an unresolvable host — neither of which a local server would fix, and the second of which is the likelier typo. Text matching would also have broken an existing test requiring a bare errors.New("connection refused") to produce no hint. That case still passes, and a new test pins the DNS distinction against an RFC 2606 .invalid name.

The hint fires only for a context named cortex. Anywhere else it would be misdirection: a production API that is down is not fixed by running a local server, and saying so sends the user after the wrong problem. A name as the marker is a convention, not a guarantee, which is why this is appended to the real error and never replaces it. An explicit --server suppresses it, since --server overrides every context and the context name then says nothing about what was dialed.

config.TypeCortex is removed rather than used. The original request was to key on it, and it could not work — the type was declared but never set. All four context-creating sites hardcode TypeAPI and create-context has no --type flag, so no condition keyed on it could ever fire. Type is an unvalidated bare string, so a config.yaml written when "cortex" existed still parses and carries the value as-is; no migration, and a test keeps a literal "cortex" to pin that such a context still routes to the HTTP client. A local cortex is an ordinary HTTP server reached by pointing a context at its address — which "api" already describes.

The context lookup is defensive. Checking the name can itself fail, and it happens while an error is already being reported, so a config that will not load leaves the hint silent rather than replacing the failure the user is trying to read. A test writes a malformed config to require that.

The test chains come from a real dial to a closed port, wrapped the way apiclient wraps it, rather than from a constructed syscall.Errno. A fake errno would keep passing if net/http stopped surfacing the syscall — precisely the change that would silently disable this hint in production.

Verification

go build ./..., go vet ./..., gofmt -l cmd internal, and go test ./... all pass. Six new tests in cmd/root_test.go; the pre-existing 401 hint and the errors.New("connection refused") silence are both unregressed.

End to end:

Situation Result
Context cortex, nothing listening raw error + the hint
Context prod, nothing listening raw error, no hint
Context cortex, cortex serve running No agents found., exit 0

Notes and limitations

  • rossoctl login still gets no hint: fetchAuthConfigForLogin wraps with %v, severing the chain. Pre-existing, deliberate at that site for the 401 circularity its comment describes, and left alone here. Worth a follow-up.
  • The trigger is a naming convention, so a context pointed at a local cortex under a different name gets no hint, and a context named cortex pointed elsewhere gets a slightly wrong one. Both are why this is additive to the real error.
  • staticcheck was not run: the installed build is Go 1.25 and this module requires 1.26.4. Linting rests on go vet.
  • Neither README documents the context type field, so no documentation changed.
  • Incidental finding: agents list fails on a missing namespace before it dials, so reaching this path needs create-context --namespace.

Assisted by Claude.

Signed-off-by: Ed Snible snible@us.ibm.com

…ection

A user who points a context at a local `rossoctl cortex serve` and forgets to
start it gets only the transport error: "dial tcp [::1]:9097: connect:
connection refused". That is accurate and says nothing about the remedy, which
in this one case the CLI can name, because `cortex serve` is the only server
rossoctl can start itself. The hint goes in errorHint, the single place every
command's error is already printed from Execute, so it covers agents, tools,
status, envvars, and anything added later rather than being repeated at the
eleven call sites that reach a client.

The condition is errors.Is(err, syscall.ECONNREFUSED), not the words "connection
refused" in the message. apiclient wraps the dial failure with %w, so the errno
survives in the chain, and matching the syscall distinguishes a refused
connection from a timeout or an unresolvable host — neither of which starting a
local server would fix, and the second of which is the likelier typo. Text
matching would also have broken an existing test that requires a bare
errors.New("connection refused") to produce no hint; that case still passes, and
a new test pins the DNS-failure distinction by resolving an RFC 2606 .invalid
name.

The hint fires only for a context named "cortex". Everywhere else the advice
would be misdirection: a production API that is down is not fixed by running a
local server, and saying so would send the user after the wrong problem. That a
name is the marker is a convention rather than a guarantee, which is why this
is a hint appended to the real error and never a replacement for it. An explicit
--server suppresses it, since --server overrides every context and the current
context's name then says nothing about what was actually dialed.

The request was originally to key on config.TypeCortex, and that constant is
removed here instead. It could not work: the type was declared but never set.
All four context-creating sites hardcode TypeAPI, and create-context has no
--type flag, so no condition keyed on it could ever have fired. Type is an
unvalidated bare string, so a config.yaml written when "cortex" existed still
parses and carries the value as-is — no migration, and a test keeps a literal
"cortex" string to pin that such a context still routes to the HTTP client. What
a local cortex is, is an ordinary HTTP server reached by pointing a context at
its address, which "api" already describes.

Reading the context to check its name can itself fail. It happens while an error
is already being reported, so a config that will not load leaves the hint silent
rather than replacing the failure the user is trying to read; a test writes a
malformed config to require that.

The ECONNREFUSED test chains come from a real dial to a closed port, wrapped the
way apiclient wraps it, rather than from a constructed syscall.Errno. A fake
errno would keep passing if net/http stopped surfacing the syscall, which is
precisely the change that would silently disable this hint in production.

`rossoctl login` still gets no hint: fetchAuthConfigForLogin wraps with %v,
severing the chain. That is pre-existing, deliberate at that site for the 401
circularity its comment describes, and left alone here.

Verified end to end. With a context named cortex and nothing listening, the raw
error is followed by the hint; with a context named prod and nothing listening,
there is no hint; with `cortex serve` running, the same command succeeds and the
hint is gone. go build, go vet, gofmt, and the full test suite pass. staticcheck
was not run, as the installed build is Go 1.25 and this module requires 1.26.4.
Neither README documents the context type field, so no documentation changed.

Assisted by Claude.

Signed-off-by: Ed Snible <snible@us.ibm.com>
@esnible
esnible marked this pull request as draft August 11, 2026 11:30
@esnible esnible closed this in #41 Aug 11, 2026
@esnible
esnible deleted the cortex-serve-hint branch August 11, 2026 13:02
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.

1 participant