Skip to content

Go client SDK (pkg/client)

github.com/netsec-ethz/debuglet/pkg/client is the native Go HTTP client for a Debuglet dispatcher. It uses only the standard library and supports the local alpha’s TEST payment method. The guest-side WASI SDK that a debuglet itself imports is the separate package pkg/debuglet. Client and server no longer have to come from the same revision: they agree on the published HTTP API contract, and client.APIVersion is the contract version this SDK is written against.

From your application’s Go module, select the release candidate:

go get github.com/netsec-ethz/debuglet/pkg/client@v0.2.0-rc.3

Import github.com/netsec-ethz/debuglet/pkg/client. Public fetching needs no credentials or GOPRIVATE setting. v0.2.0-rc.3 is the current release candidate; v0.2.0-rc.1 was the first to contain this client, and v0.1.0 predates it. Release candidates may change the HTTP contract before the final v0.2.0, as the API guide describes, so pin the exact tag.

func New(endpoint string, options Options) (*Client, error)
func Prepare(requests []Request) (*PreparedBatch, error)
func (c *Client) SubmitTEST(ctx context.Context, batch *PreparedBatch) (Submission, error)
func (c *Client) Nodes(ctx context.Context) ([]Node, error)
func (c *Client) Status(ctx context.Context, id string) (State, error)
func (c *Client) Logs(ctx context.Context, id string, options LogOptions) (LogPage, error)
func (c *Client) Cancel(ctx context.Context, id, executorID string) error
func (c *Client) Version(ctx context.Context) (ServerVersion, error)
func (c *Client) Connection(ctx context.Context) (ConnectionInfo, error)
func (c *Client) CreateAccount(ctx context.Context, name string) (Account, error)
func (c *Client) Login(ctx context.Context, accountKey string) (Session, error)
func (c *Client) Logout(ctx context.Context) error
func (c *Client) Recover(ctx context.Context, recoveryCode string) (Account, error)
func (c *Client) Whoami(ctx context.Context) (User, error)
func (c *Client) WithCredential(token string) (*Client, error)

Types mirror the dispatcher’s JSON keys (order_id, executor_id, floor_bw, timeout_ms, tesla_anchor_key, has_more, …). State.State is an opaque string; client.StateExited (RunStateExited) is the terminal value. An exited debuglet with an empty Error succeeded; a nonempty Error is a workload failure such as debuglet exited with code 7, timeout of 30s exceeded, cancelled via API, destination refused: ..., module does not compile: ... or debuglet failed; the executor log has the details (the recorded results), and it is one bounded line that the client preserves verbatim.

Every request announces Debuglet-API-Version: 1.3, the contract version in client.APIVersion. A dispatcher that cannot serve it answers HTTP 400 with an explicit incompatibility error, which reaches the caller as an ordinary *HTTPError; Version and the contract document itself always answer, so a rejected client can still read what the dispatcher speaks. A dispatcher written before contract versioning ignores the header, so this SDK also works against an older one. docs/API.md states what a minor version may change and what requires a major one; the SDK ignores unknown response fields accordingly.

A dispatcher authenticates every request that is not public. Options.Credential is the session token the client presents, as an Authorization: Bearer header, on every request it makes:

account, err := anonymous.CreateAccount(ctx, "researcher") // returns the credentials once
session, err := anonymous.Login(ctx, account.AccountKey)
c, err := anonymous.WithCredential(session.Token) // same endpoint, now authenticated
  • CreateAccount returns Account{AccountKey, RecoveryCode}. The dispatcher stores only their digests and cannot produce them again. Both are secrets: store them with owner-only permissions and keep them out of logs and command output.
  • Login exchanges an account key for a Session. Session.Token goes in Options.Credential; Session.ExpiresAt is Unix seconds, 12 hours after issuance, and is not extended by use. Session.CSRFToken matters only to a browser client that received the session as a cookie.
  • Logout revokes the presented session immediately. Recover exchanges a recovery code for replacement credentials and revokes every session of that account.
  • Whoami reports the account a credential authenticates as, and is the cheapest way to tell a working credential from an expired or revoked one.
  • WithCredential returns a copy of the client bound to the same origin and base path. A credential therefore cannot be attached to another dispatcher by reusing a client: a different dispatcher means a different Client.

An expired, revoked or absent credential answers *HTTPError with StatusCode 401 and Code client.CodeUnauthorized; the action behind it is to log in again. client.CodeForbidden (403) is an authenticated account that may not perform the operation, which logging in again does not change. A run or payment order belonging to another account answers 404 client.CodeNotFound, the same as one that does not exist.

The credential is never written to an error, a diagnostic or a returned value the caller did not ask for: it is added to the values this package redacts, so a server that echoes it back, and a transport failure that quotes the request, both produce a diagnostic with it removed.

A dispatcher serving the local development profile also answers without a credential; Login with an empty account key asks such a dispatcher for a credential for its own local account, and every other dispatcher refuses that with 401.

  • The endpoint is an explicit absolute base URL. A path prefix such as https://host/api is preserved on every route; the client never guesses or appends /api. Userinfo, query strings, fragments and schemes other than http/https are rejected.
  • Plaintext http is accepted only for literal loopback IPs (127.0.0.0/8, ::1). Any other host, including the name localhost, requires https with ordinary certificate verification. There is no insecure option.
  • TEST submission to an endpoint that is not a literal loopback IP additionally requires Options.AllowRemoteTEST. Read-only calls do not. An SSH-forwarded loopback endpoint is still an operator-selected remote service; this check is an accident guard, not server authorization.
  • Redirects are rejected, also on a caller-supplied http.Client, so a submission cannot silently move to another origin and a credential cannot follow one. The supplied client is cloned and never mutated; no cookie jar is added, and the credential is sent as a header rather than as ambient cookie authority, so a browser-only cookie assumption never gets in a native client’s way.

Prepare validates the batch (nonempty batch, WASM and executor ID; unique order IDs; positive TimeoutMS at most MaxInt64/int64(time.Millisecond); 0 <= FloorBW <= CeilBW; encoded intent envelope at most 32 MiB) and serializes the debuglets array once. Bandwidth values are bits per second. That frozen array is sent unchanged for both steps of SubmitTEST: PUT payment/intent with payment_method: "TEST", then PUT debuglet with the returned transaction ID and auth key (empty for the current TEST server). Because the dispatcher hashes the decoded request values, the identical bytes pass its hash check; mutating the caller’s requests after Prepare changes nothing.

Both complete request bodies have a 32 MiB limit. The actual submission body is checked after the server supplies its opaque transaction ID and auth key, before sending the second request. A batch can pass Prepare and exceed this second bound. That failure retains the known transaction ID and reports a definite pre-send failure; the intent already exists, but no submission is attempted. Metadata has no separate arbitrary length limit. The dispatcher applies the same 32 MiB bound to every request body, answering 413 payload_too_large (client.CodePayloadTooLarge, an ordinary *HTTPError) to a declared length above it and cutting a body of unknown length at it; the SDK declares the length and checks the envelope before sending, so its own check is what a caller sees.

SubmitTEST returns the job IDs in batch order and the transaction ID. It never retries, never polls payment status and never invents a transaction ID. Failures are *SubmissionError{Stage, TransactionID, OutcomeUnknown, Err}:

SituationStageTransactionIDOutcomeUnknown
Validation or remote-TEST guard before sendingintentemptyfalse
Intent rejected (4xx, or a 503 that carries service_unavailable or payments_disabled) or a transport/connection failure while requesting the intentintentemptyfalse
Intent answered with a malformed or inconsistent success body, or any other 5xxintentemptytrue
Submission validation/size or canceled context detected before sendingsubmitknownfalse
Submission rejected (4xx)submitknownfalse
Transport or read failure after attempting submission, malformed or inconsistent success body, unexpected 2xx, 5xxsubmitknowntrue

OutcomeUnknown means the server may have accepted the request although no valid response was observed. It is uncertainty, not acceptance; inspect available transaction/job state before considering resubmission. A transport failure while requesting the intent sets this flag to false because no submission was attempted; an intent 5xx or malformed intent response is reported as unknown because the intent’s server bookkeeping may exist. A 503 that carries service_unavailable or payments_disabled at the intent stage is a rejection: nothing was priced or written. An unexpected submission 201/202 is still an HTTP protocol error, but cannot establish rejection. Neither case triggers an automatic retry.

  • Status requires a lowercase canonical UUID and returns the reported state, error and executor ID. Unknown state strings are preserved.
  • Logs returns one page after the cursor After with Limit entries (zero defaults to 100, otherwise 1–1000). Output holds the exact guest bytes. Each page is validated against the requested cursor: entry IDs strictly increase and exceed the cursor, a nonempty page’s After is its last ID, an empty page’s After equals the request cursor, and an empty page cannot report HasMore. A full page may report HasMore although no later entry exists; the following page is then empty.
  • Cancel sends the dispatcher’s cancellation and returns nil on the server’s acknowledgement (HTTP 204), which says that the cancellation is recorded as the job’s result, or that the job already had one. Acknowledgement does not certify remote termination. While the job’s control session is live, cancellation of a finished job also requires the executor’s acknowledgement; if the executor no longer holds it, cancellation is refused. When the stored control session has ended, the dispatcher records cancellation locally without contacting an executor, or acknowledges an already-terminal job without changing its result. A newly recorded cancellation then says that the control session had ended and the executor’s outcome was not observed. Status preserves the server’s error verbatim: cancelled via API for cancellation through a live session, that reason plus the unobserved-outcome qualification for a dead session, or the earlier exit error for an already-terminal job. A 500 internal_error saying the cancellation was acknowledged but its result was not recorded means the dispatcher could not record that result, or could not confirm that it did: the executor may be stopping the job even if its recorded state does not show the cancellation, so read Status before deciding, and repeat Cancel to record it. Not-found, refused and other server-reported cancellation errors are *HTTPError values. A transport failure or deadline after Cancel sent the request leaves the outcome unknown: client.IsTransportError identifies it, and the caller reads Status and repeats Cancel.
  • Version reports the dispatcher’s separate identities: Version (its configured string), APIVersion and APIVersions (the HTTP contract it serves), BinaryVersion and BinaryRevision (the build), and ProtocolVersion (the executor control protocol, which no HTTP client speaks). A dispatcher written before the contract was versioned answers with Version only and leaves the rest empty.
  • Connection reads optional local executor-join metadata: SchemaVersion, Mode, GRPCAddress, and YamuxAddress. Current metadata has schema version 1 and mode local-test. This is not a dispatcher directory or authenticated enrollment. Ordinary submission and result queries need only the HTTP endpoint; older servers can lack this route and return a typed HTTP error.

Every other status, including other 2xx codes, is an *HTTPError{Method, Path, StatusCode, Code, Message}. Code is the dispatcher’s stable failure code (client.CodeCapacityExhausted, client.CodeNotFound, … ; see docs/API.md): branch on it instead of matching on Message. It is empty when the server sent no documented envelope or a code that is not a bounded lowercase identifier, so a code can never smuggle response text. (*SubmissionError).Code() reaches the same value for a failed submission without changing OutcomeUnknown, which remains the only statement about whether the submission may have been accepted. Safe Echo message values, JSON-string errors and plaintext messages are retained. Unrecognized JSON envelopes/arrays and malformed or truncated JSON-shaped error bodies produce a generic diagnostic; the client does not print the whole response as a fallback. Recognized structured auth_key values are removed before messages are derived, and a known nonempty submission key is redacted from response diagnostics. This also covers decoded intent-validation errors and the public HTTPError.Message field. Arbitrary server text can contain information the client cannot identify as secret.

Successful JSON bodies are bounded to 4 MiB and error bodies to 8 KiB; oversized, malformed or trailing successful JSON is an error rather than a truncated success. Formatted transport/read errors also redact a known submission key, including one quoted by the HTTP response parser. Context cancellation/deadlines and underlying error types remain available through errors.Is/errors.As; explicitly unwrapping a transport error exposes the original error rather than sanitized copies of all its fields. Request bodies and WASM are excluded from SDK-generated diagnostics.

c, err := client.New("http://127.0.0.1:9000", client.Options{})
if err != nil { ... }
batch, err := client.Prepare([]client.Request{{
OrderID: 0,
ExecutorID: "executor-id",
Args: []string{"127.0.0.1:8080"},
Wasm: wasmBytes,
Policy: client.Policy{FloorBW: 1_000_000, CeilBW: 1_000_000, TimeoutMS: 10_000, Addresses: []string{"127.0.0.1"}},
}})
if err != nil { ... }
sub, err := c.SubmitTEST(ctx, batch)
if err != nil { ... }
state, err := c.Status(ctx, sub.IDs[0])

TEST only; no wallet, chain payment, retry or idempotency. The dispatcher’s hash check protects intent/submission identity, not authorization: authorization is the credential, and a payment order may only be spent by the account that created it. This package stores no credential of its own and refreshes none; obtaining, keeping and renewing one is the caller’s job, which is what the dbl profile store in docs/CLI.md does. Output pages reflect stored logs, not a delivery guarantee. Requests are bounded by the caller’s context and the per-request timeout; the server’s job budget is TimeoutMS.

Start dbl dispatcher up and dbl executor up --dispatcher http://127.0.0.1:9000 in separate terminals, or use the combined dbl up. In a source checkout with Go 1.25.11, build a guest and run the complete client example:

make wasm SAMPLE_DIR=examples/debuglets/go/hello-local
go run -mod=readonly ./examples/client --wasm examples/debuglets/go/hello-local/debuglet.wasm

The example discovers the sole ready executor, submits the guest with TEST bookkeeping, and prints its stored output. Use --executor ID when multiple executors are ready, or --endpoint URL for a different local port. It uses only the public pkg/client API, so you can copy it into your own module after adding the dependency above. Arguments following -- go to the guest.

The same example reaches a dispatcher on another host with three more flags: --register NAME creates an account and logs in for the rest of the run, --allow-remote-test sets Options.AllowRemoteTEST, and --allow HOST[,HOST] fills the request’s address allowlist. It prints the account id and neither the account key nor the session token, as in go run -mod=readonly ./examples/client --endpoint https://HOST:PORT --register researcher --allow-remote-test --allow example.com --wasm GUEST.wasm -- -addr example.com:80 -count 3. The cross-host quickstart has the dispatcher and executor side of that setup.

The SDK does not read the CLI’s saved profiles or start daemons. Pass the printed HTTP URL to client.New (or the example’s --endpoint). This keeps application configuration independent of the operator’s selected CLI connection. A native client application needs neither the full daemon package nor a wallet.