Answers, errors and hints
The basics are on Writing an rta plugin: a handler returns data, and a failure is a view.Error with a code and a hint. This page is the rest of what an answer can carry, which a plugin that talks to a service meets sooner or later.
Hints, in the words of whoever reads them
A hint is read on the surface the call came through, and each spells a capability and an input its own way: rta weather stations and --city at a terminal, the weather_stations tool and its "city" argument to an agent, weather.stations and the city box in the TUI. req.Surface().CapabilityName and req.Surface().InputName spell them for the reader, because an agent told to pass a flag has no flag to pass.
Beside them, ArgumentName names a positional input, which the CLI takes by its place rather than as a flag; CapabilityWith names a call with inputs given; Call spells one whole with its values, for a cell a person or an agent copies, a []string among them as the flag once per element at a terminal, a JSON array to an agent and the box's comma-separated text in the TUI, an element holding a comma, a double quote or space at an end in double quotes, as the list flag takes it; InputTo("jobs", 1) names one input with the value to give it, as Call spells that argument — --jobs 1 at a terminal, the "jobs" argument set to 1 to an agent, the jobs box set to 1 in the TUI — for an input not declared Local, since an agent is told to set an argument its tool has; and WithoutInputs says how to leave inputs out.
An input declared Local is in no agent's schema at all, so a hint read over MCP says who can give it — the operator — rather than naming an argument that is not there. A connection's settings are that kind of input — the host, the password, the CA file — and req.Surface().SettingName("ca-file") names one the way its reader changes it: --ca-file at a terminal, the ca-file box in the TUI, and to an agent the operator's ca-file setting. SettingName("host", "port") names several at once, SettingTo("sslmode", "disable") one with the value to give it — as it does any Local input, a path only the operator may name among them — SettingsHint("<capability>") sends the reader to the page that says which of the command line, the rta config, a profile and the environment can set each, which over MCP the operator is asked to look up, and DNSHint(host) to the net.dns call that shows what DNS answers for a name that did not resolve.
Your Summary, Description and Help are shown on every surface at once and have no surface to ask, so they name an input as city and a capability by its ID. A command only the person at the terminal can run reaches an agent as plugin.AskOperator("grant allow weather.stations"), the one form in which an agent reads a command line.
A receipt that hands its reader the next call — the restore that puts a dump back — names the connection the way the reader reaches it again. req.Profile() is the operator's profile the call came through, "" for none, and req.Tunnel() the kind of forward rta opened on it for this call, plugin.TunnelKube or plugin.TunnelSSH, or plugin.TunnelNone. Through a forward, the host and port your handler was given are the 127.0.0.1 end of a tunnel that closes when the call does, and a restore line naming them names a port nothing listens on any more: give the profile instead. req.ReachArgs(endpoint...) is the arguments that reach the same server again — the profile whenever req.Profile() names one, since the credentials the call used may be the profile's, and the endpoint inputs you pass it only when req.Tunnel() is plugin.TunnelNone: then they are the address the call reached, possibly one the caller typed over the profile's, which the profile alone would send through its forward instead. Append what else reaching it takes (a TLS mode, a CA file) and hand the lot to Call, which spells the profile --profile prod at a terminal, the tool's "profile" argument to an agent and the form's profile box in the TUI; over MCP leave out what an agent cannot give, a Local input. req.Reached(endpoint) is the same rule as a phrase, for a sentence naming where a call went: the address, the address and its profile, or profile prod (through its kube: forward). A line naming a command that is not rta's own — a createdb to run first — takes its values through plugin.ShellWord, so a name holding a space or a $(…) pastes as the word it is. A profile's name is all rta tells a plugin about it — never its coordinate, which of your values it filled, or where its credentials come from — and a host older than the field sends none, which reads as "".
When a connection fails
A connection that failed is worded by why it failed, and three of the questions every connection plugin asks are answered in the SDK. plugin.DialRefused(err) is a port the host refused — nothing listening there. plugin.DialUnroutable(err) is a dial that found no way to the host at all, which no port change cures: a VPN or a tunnel that is down, an address on a network this machine is not on — and never one the host refused on another of its addresses.
plugin.CertUntrusted(err) is a certificate nothing here vouches for, whose CA belongs in your CA file setting — read as Go's verifier says it, and as the one macOS asks with no CA given says it in untyped words of its own, "certificate is not trusted", and no other: a certificate macOS answers revoked, blocked or not standards compliant is not untrusted, since a CA file would not cure it but go around it, running Go's verifier in place of the system's revocation and policy checks, so answer those with the system's own words. plugin.CertRevoked(err) is that revocation verdict — macOS's, the one verifier here that checks and says so — for a refusal that reads a certificate by what it lacks: one naming no host has a mode that checks the chain without a name, and like a CA file it runs Go's verifier, which checks no revocation, so ask this first and answer a revoked certificate in the system's words, with no way round. req.Surface().CAHint("ca-file") is the hint for an untrusted one: your CA file setting named for its reader, and what naming a CA there costs — it replaces the system's checks. plugin.CertPolicyHint(err) is the hint for one macOS refused by a rule of Apple's, or "" for any other error: a TLS server certificate valid for longer than the 825 days Apple allows, the usual ten-year self-signed one, answered "not standards compliant" — it names the limit and says to reissue the certificate within it, and never points to a CA file. Each reads the operating system's error rather than the *net.OpError every failed dial is wrapped in, and a name that did not resolve is none of them, whatever words the resolver kept: ask errors.As for the *net.DNSError first, and hand the reader DNSHint(host).
Two more readers cover what a server that was reached says about TLS: plugin.CertNames(hostErr.Certificate) lists the names a certificate refused for its name is for — DNS names and IP addresses, never the common name Go ignores — for a refusal that says what it is for beside what it was checked for, and plugin.ForwardNameRefusal(req, code, address, answerer, hostErr) is the whole refusal when that certificate was checked for the end of a forward the host opened (127.0.0.1) and is for the name the server answers as, with your own code, the input that names the server as it holds it, and what answers (a server, a member, an instance), naming tls-server-name as the way on and never a mode that checks less; and plugin.TLSExpected(err) is a plain-HTTP request met by a server that speaks only TLS, read in both shapes it arrives in (a TLS alert quoted as a malformed response, or the 400 Go's own TLS listener answers with), whose cure is the call's scheme, not the server. The two dial questions never answer an error that holds a TLS handshake's or a certificate's failure, typed or flattened into gRPC's text — a certificate's names are the server's to choose, and one named "connect: connection refused" is still a certificate — and they read a flattened error's words only as a dial spells them, the call before the system's words, so none of these takes another's failure whatever order you ask them in. The order that does matter is yours: read your server's own coded answer, and anything your driver says the server said, before any of these, since a server's message relaying a dial of its own that failed reads the same.
A verdict only macOS gives is tested on Linux CI too: sdktest.VerifierSystem(t, "darwin") makes CertUntrusted, CertRevoked, CertPolicyHint and CAHint read an error as macOS's verifier would have worded it until the test ends, and sdktest.SystemVerdict(name, sdktest.VerdictRevoked) builds the error that verifier returns, so the answer your plugin gives a revoked certificate, and the way round it must not offer, is exercised wherever CI runs.
Refusals and warnings
One error is not like the others: a policy gate — your handler declining a call over who is asking, not over what went wrong, like a capability that refuses the MCP surface outright. Build that one with view.Refusef instead of view.Errorf. The host's audit trail records refusals and failures as different outcomes, and only your handler knows which one it returned — an unmarked gate is recorded on the operator's machine as your plugin breaking.
A table or a page that could not cover everything it was asked about says so with Warnings, one view.Error for each part it could not read — a directory it could not enter, a namespace the credential may not list — and every surface carries them, coded: under the rows, in the csv notes, in the JSON. The TUI heads such an answer partial. A warning about rows that are all there — something a person should know about them rather than a part that is missing — sets Advisory: true, and the answer is headed as warned about, never as partial.
Next
Safety, credentials and what rta does to your process — the claims a capability makes about itself, and the one that decides what an agent may do without a person.