Suggest cortex serve when a context named "cortex" refuses the connection - #40
Closed
esnible wants to merge 1 commit into
Closed
Suggest cortex serve when a context named "cortex" refuses the connection#40esnible wants to merge 1 commit into
cortex serve when a context named "cortex" refuses the connection#40esnible wants to merge 1 commit into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Point a context at a local
rossoctl cortex serve, forget to start it, and the CLI says only this:Accurate, and silent about the remedy — which in this one case the CLI can name, because
cortex serveis 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 fromExecute. That coversagents,tools,status,envvars, and anything added later, instead of repeating it at the eleven call sites that reach a client.Design notes
The condition is
errors.Is(err, syscall.ECONNREFUSED), not the message text.apiclientwraps 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 bareerrors.New("connection refused")to produce no hint. That case still passes, and a new test pins the DNS distinction against an RFC 2606.invalidname.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--serversuppresses it, since--serveroverrides every context and the context name then says nothing about what was dialed.config.TypeCortexis 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 hardcodeTypeAPIandcreate-contexthas no--typeflag, so no condition keyed on it could ever fire.Typeis an unvalidated barestring, so aconfig.yamlwritten 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
apiclientwraps it, rather than from a constructedsyscall.Errno. A fake errno would keep passing ifnet/httpstopped surfacing the syscall — precisely the change that would silently disable this hint in production.Verification
go build ./...,go vet ./...,gofmt -l cmd internal, andgo test ./...all pass. Six new tests incmd/root_test.go; the pre-existing 401 hint and theerrors.New("connection refused")silence are both unregressed.End to end:
cortex, nothing listeningprod, nothing listeningcortex,cortex serverunningNo agents found., exit 0Notes and limitations
rossoctl loginstill gets no hint:fetchAuthConfigForLoginwraps 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.cortexpointed elsewhere gets a slightly wrong one. Both are why this is additive to the real error.staticcheckwas not run: the installed build is Go 1.25 and this module requires 1.26.4. Linting rests ongo vet.typefield, so no documentation changed.agents listfails on a missing namespace before it dials, so reaching this path needscreate-context --namespace.Assisted by Claude.
Signed-off-by: Ed Snible snible@us.ibm.com