kosa8 docs

Guides / Go / Errors and troubleshooting

Go errors and troubleshooting

What can go wrong when you drive kosa8 from Go, what the SDK hands back when it does, and what to do about it. The messages on this page are real: they were printed by the programs below, against a kosa8 0.9.36 daemon. For the calls themselves, see the developer guide and the API reference.

How this page was tested

The two programs on this page were run against a real kosa8 daemon 0.9.36 on an Apple silicon Mac, with github.com/kosa8ai/kosa8-go v0.1.2 from the Go module proxy and go1.26.9 darwin/arm64, and their output is pasted unchanged (the first had also been run on v0.1.0; the second printed identical output on both). The Exec, ExecFrames, WriteStdin and capture messages quoted below come from the developer guide's programs. Everything was run on v0.1.1; the sandbox-free first program and the usage messages that v0.1.2 reworded were run again on v0.1.2. The sandbox they create was deleted. No org policy was installed and no remote worker was used, so the policy, approval and authentication (401) errors were not produced live; what this page says about them comes from reading the SDK's source, and is marked. The locked-screen message is quoted from our tests of the same daemon version for the JavaScript pages; it was not reproduced for this page.

What this is, in plain words

When kosa8 cannot do what your program asked, the SDK returns an error instead of a result. In Go, that is the second value every call returns. Each error says what kind of problem it is (kosa8 is not running, the sandbox does not exist, a company rule forbids it, it took too long), and, when kosa8 itself refused, it carries kosa8's own explanation and its advice about what to do next.

One thing is not an error: a command that runs and fails. If your command exits with code 1, you get a normal result with ExitCode 1 and err is nil. An error means the command could not be run at all.

The error type

Every error this package creates is a *kosa8.Error. Two kinds of failure come back as they are, without that wrapper:

FieldSet whenMeaning
KindalwaysWhich kind of failure; table below.
MessagealwaysWhat Error() returns. For a daemon refusal: "<error> (<remediation>)", the way the CLI prints it.
Statusthe daemon answered with HTTP 400 or aboveThe HTTP status. 0 otherwise, including for stream errors.
DaemonMessagedaemon refusals, stream error framesThe daemon's own error text, verbatim.
Remediationdaemon refusals that include adviceWhat the daemon says to do. Can be empty (a bad egress value had none).
Method, PathHTTP and connection errors, most stream errorsThe request, such as POST /kosa8/v1/sandboxes/x/run.
Bodydaemon refusalsThe decoded response body (map[string]any or a string).
ApprovalKindApprovalRequiredThe daemon's refusal object (it has a why key, and whatever else the daemon sends).
Stdout, StderrKindStream, KindTimeout from Run/Exec/AttachOutput produced before the failure. The returned ExecResult holds the same.
Errconnection errors, timeouts, decode errors, a failed rewindThe underlying cause; Unwrap returns it, so errors.Is/As see through.

Kinds and sentinels

KindSentinelWhen
connectionErrConnectionThe daemon could not be reached, or the answer could not be read.
bad_requestErrBadRequestHTTP 400: the daemon rejected the arguments.
authErrAuthHTTP 401: a remote worker rejected the token. From the source.
policyErrPolicyHTTP 403: the org or label policy refused.
approval_requiredErrApprovalRequiredHTTP 403 whose body has an approval object with why: a signed approval would allow it. From the source.
not_foundErrNotFoundHTTP 404: no such sandbox or snapshot.
conflictErrConflictHTTP 409: name or id taken, sandbox not running.
unsupportedErrUnsupportedHTTP 501: this daemon or platform cannot do it.
apiErrAPIAny other HTTP error status (500, 503, …), or an answer that could not be decoded.
streamErrStreamA Run/Exec/Attach stream ended with an error frame, a broken frame, or no final frame.
timeoutErrTimeoutThe context's deadline (or RunOptions.Timeout) passed.
checkpointErrCheckpointClient-side checkpoint bookkeeping: bad name, nothing to rewind to, unknown checkpoint, or a rewind whose restore step failed.
usageErrUsage (new in v0.1.1)Bad arguments caught before sending.

(*Error).Is matches a target *Error whose Message is empty (a sentinel, or &kosa8.Error{Kind: k}) by Kind, with two extra rules:

So test the specific kinds first. A handler that covers everything:

res, err := sb.Run(ctx, image, args, nil)
switch {
case err == nil:
	// It ran. res.ExitCode says how it went; non-zero is not an error.
case errors.Is(err, context.Canceled):
	return err // we were told to stop
case errors.Is(err, kosa8.ErrTimeout):
	log.Printf("too slow; output so far: %q", res.Stdout())
case errors.Is(err, kosa8.ErrApprovalRequired):
	e, _ := kosa8.AsError(err)
	log.Printf("needs approval: %v", e.Approval["why"]) // retry later with RunOptions{Approval: id}
case errors.Is(err, kosa8.ErrPolicy):
	return err // a rule says no; do not retry
case errors.Is(err, kosa8.ErrNotFound):
	// the sandbox is gone: recreate it, or give up
case errors.Is(err, kosa8.ErrConflict):
	// e.g. "sandbox is not running": sb.Start(ctx), then retry once
case errors.Is(err, kosa8.ErrStream):
	// the command could not start (no image, no such executable, no container)
case errors.Is(err, kosa8.ErrConnection):
	// kosa8 is not running: tell the user to run `kosa8 daemon start`
case errors.Is(err, kosa8.ErrUsage):
	panic(err) // a bug in our own arguments
default:
	return err
}

For logs, err.Error() is already the full sentence with the daemon's advice. kosa8.AsError(err) is errors.As into *kosa8.Error, for reading the fields; var e *kosa8.Error; errors.As(err, &e) does the same.

Every error, provoked

The first program needs no sandbox. Its show function prints the kind, the status, which sentinels match, and the fields:

package main

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	kosa8 "github.com/kosa8ai/kosa8-go"
)

func show(label string, err error) {
	fmt.Printf("== %s\n", label)
	if err == nil {
		fmt.Println("   nil")
		return
	}
	e, ok := kosa8.AsError(err)
	if !ok {
		fmt.Printf("   not a *kosa8.Error: %T %v (Canceled=%v)\n", err, err, errors.Is(err, context.Canceled))
		return
	}
	var is []string
	for n, s := range map[string]error{"ErrConnection": kosa8.ErrConnection, "ErrAPI": kosa8.ErrAPI, "ErrBadRequest": kosa8.ErrBadRequest,
		"ErrPolicy": kosa8.ErrPolicy, "ErrNotFound": kosa8.ErrNotFound, "ErrConflict": kosa8.ErrConflict, "ErrStream": kosa8.ErrStream,
		"ErrCheckpoint": kosa8.ErrCheckpoint, "ErrTimeout": kosa8.ErrTimeout, "ErrUnsupported": kosa8.ErrUnsupported, "ErrAuth": kosa8.ErrAuth, "ErrUsage": kosa8.ErrUsage} {
		if errors.Is(err, s) {
			is = append(is, n)
		}
	}
	fmt.Printf("   Kind=%s Status=%d errors.Is: %s\n   Message: %s\n", e.Kind, e.Status, strings.Join(sorted(is), ", "), e.Message)
	if e.DaemonMessage != "" {
		fmt.Printf("   DaemonMessage: %s\n   Remediation: %s\n", e.DaemonMessage, e.Remediation)
	}
	if e.Method != "" {
		fmt.Printf("   %s %s\n", e.Method, e.Path)
	}
	if e.Err != nil {
		fmt.Printf("   Err: %T\n", e.Err)
	}
}

func sorted(a []string) []string {
	for i := range a {
		for j := i + 1; j < len(a); j++ {
			if a[j] < a[i] {
				a[i], a[j] = a[j], a[i]
			}
		}
	}
	return a
}

func main() {
	ctx := context.Background()
	gone, _ := kosa8.New(kosa8.Options{SocketPath: "/tmp/k8-nowhere.sock"})
	_, err := gone.Version(ctx)
	show("daemon not reachable", err)

	k, _ := kosa8.New(kosa8.Options{})
	_, err = k.CreateSandbox(ctx, kosa8.CreateOptions{Egress: "any", EgressAllow: []string{"pypi.org"}})
	show("EgressAllow with Egress any", err)
	_, err = k.Explore(ctx, "x", []string{"true"}, kosa8.ExploreOptions{})
	show("Explore with one variant", err)
	_, err = k.CreateSandbox(ctx, kosa8.CreateOptions{Name: "doc-go-g-bad", Egress: "sometimes"})
	show("bad egress value", err)
	_, err = k.CreateSandbox(ctx, kosa8.CreateOptions{Name: "doc-go-g-bad", Mounts: []string{"/tmp"}})
	show("a mount with no guest path", err)

	_, err = k.StopSandbox(ctx, "doc-go-g-nope")
	show("StopSandbox on a missing sandbox", err)
	_, err = k.Run(ctx, "doc-go-g-nope", "alpine:latest", []string{"true"}, nil)
	show("Run on a missing sandbox", err)
	_, err = k.DeleteSnapshot(ctx, "doc-go-g-nope-snap", kosa8.SnapshotDeleteOptions{})
	show("DeleteSnapshot missing", err)
	_, err = k.RestoreSnapshot(ctx, "doc-go-g-nope-snap", kosa8.RestoreOptions{})
	show("RestoreSnapshot missing", err)
	_, err = k.DeleteSandbox(ctx, "doc-go-g-nope", kosa8.DeleteOptions{})
	show("DeleteSandbox missing", err)

	_, err = k.Rewind(ctx, "doc-go-g-never", "")
	show("Rewind with no checkpoints", err)
	_, err = k.Checkpoint(ctx, "../etc", "x")
	show("Checkpoint bad name", err)

	tctx, cancel := context.WithTimeout(ctx, time.Nanosecond)
	defer cancel()
	time.Sleep(time.Millisecond)
	_, err = k.Health(tctx)
	show("expired deadline", err)
	cctx, cancel2 := context.WithCancel(ctx)
	cancel2()
	_, err = k.Health(cctx)
	show("cancelled context", err)
}
$ go run .
== daemon not reachable
   Kind=connection Status=0 errors.Is: ErrConnection
   Message: kosa8 daemon unreachable at unix:///tmp/k8-nowhere.sock: Get "http://kosa8/kosa8/v1/health": dial unix /tmp/k8-nowhere.sock: connect: no such file or directory (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)
   GET /kosa8/v1/health
   Err: *url.Error
== EgressAllow with Egress any
   Kind=usage Status=0 errors.Is: ErrUsage
   Message: EgressAllow already limits the network to the hosts it lists; do not also set Egress
== Explore with one variant
   Kind=usage Status=0 errors.Is: ErrUsage
   Message: give at least two variants; with one there is nothing to compare
== bad egress value
   Kind=bad_request Status=400 errors.Is: ErrAPI, ErrBadRequest
   Message: egress policy "sometimes": want any, deny, fakenet, or allow:HOST[,HOST...]
   DaemonMessage: egress policy "sometimes": want any, deny, fakenet, or allow:HOST[,HOST...]
   Remediation:
   POST /kosa8/v1/sandboxes
== a mount with no guest path
   Kind=usage Status=0 errors.Is: ErrUsage
   Message: mount "/tmp": want host:guest or host:guest:ro
== StopSandbox on a missing sandbox
   Kind=not_found Status=404 errors.Is: ErrAPI, ErrNotFound
   Message: no such sandbox: doc-go-g-nope (list sandboxes with `kosa8 sandbox ls`)
   DaemonMessage: no such sandbox: doc-go-g-nope
   Remediation: list sandboxes with `kosa8 sandbox ls`
   POST /kosa8/v1/sandboxes/doc-go-g-nope/down
== Run on a missing sandbox
   Kind=not_found Status=404 errors.Is: ErrAPI, ErrNotFound
   Message: no such sandbox: doc-go-g-nope (list sandboxes with `kosa8 sandbox ls`)
   DaemonMessage: no such sandbox: doc-go-g-nope
   Remediation: list sandboxes with `kosa8 sandbox ls`
   POST /kosa8/v1/sandboxes/doc-go-g-nope/run
== DeleteSnapshot missing
   Kind=not_found Status=404 errors.Is: ErrAPI, ErrNotFound
   Message: no such snapshot: doc-go-g-nope-snap (list snapshots with `kosa8 snapshot ls`)
   DaemonMessage: no such snapshot: doc-go-g-nope-snap
   Remediation: list snapshots with `kosa8 snapshot ls`
   DELETE /kosa8/v1/snapshots/doc-go-g-nope-snap
== RestoreSnapshot missing
   Kind=api Status=500 errors.Is: ErrAPI
   Message: no such snapshot: doc-go-g-nope-snap (list snapshots with `kosa8 snapshot ls`)
   DaemonMessage: no such snapshot: doc-go-g-nope-snap
   Remediation: list snapshots with `kosa8 snapshot ls`
   POST /kosa8/v1/snapshots/doc-go-g-nope-snap/restore
== DeleteSandbox missing
   Kind=not_found Status=404 errors.Is: ErrAPI, ErrNotFound
   Message: no such sandbox: doc-go-g-nope (list sandboxes with `kosa8 sandbox ls`)
   DaemonMessage: no such sandbox: doc-go-g-nope
   Remediation: list sandboxes with `kosa8 sandbox ls`
   DELETE /kosa8/v1/sandboxes/doc-go-g-nope
== Rewind with no checkpoints
   Kind=checkpoint Status=0 errors.Is: ErrCheckpoint
   Message: no checkpoints for "doc-go-g-never" — take one with Checkpoint or `kosa8 checkpoint doc-go-g-never`
== Checkpoint bad name
   Kind=checkpoint Status=0 errors.Is: ErrCheckpoint
   Message: invalid sandbox name "../etc"
== expired deadline
   Kind=timeout Status=0 errors.Is: ErrTimeout
   Message: GET /kosa8/v1/health timed out talking to unix:///Users/dhruvit/.kosa8/run/kosa8.sock
   GET /kosa8/v1/health
   Err: *url.Error
== cancelled context
   not a *kosa8.Error: *errors.errorString context canceled (Canceled=true)

An earlier version of this program also created a sandbox named "bad name!", expecting a refusal. The daemon accepted the name (on both SDK versions), so that case was dropped and the sandbox deleted. Worth knowing from this output:

The second program creates one sandbox and gets it into trouble:

func show(label string, err error) {
	fmt.Printf("== %s\n", label)
	e, ok := kosa8.AsError(err)
	if !ok {
		fmt.Printf("   %T %v\n", err, err)
		return
	}
	fmt.Printf("   Kind=%s Status=%d ErrAPI=%v\n   Message: %s\n", e.Kind, e.Status, errors.Is(err, kosa8.ErrAPI), e.Message)
	if e.Stdout != "" || e.Stderr != "" {
		fmt.Printf("   Stdout=%q Stderr=%q\n", e.Stdout, e.Stderr)
	}
}

func main() {
	ctx := context.Background()
	k, _ := kosa8.New(kosa8.Options{})
	const name = "doc-go-g-errs"
	if _, err := k.CreateSandbox(ctx, kosa8.CreateOptions{Name: name, Egress: "deny"}); err != nil {
		show("create", err)
		os.Exit(1)
	}
	defer k.DeleteSandbox(context.Background(), name, kosa8.DeleteOptions{})
	_, err := k.CreateSandbox(ctx, kosa8.CreateOptions{Name: name})
	show("create the same name twice", err)
	_, err = k.Run(ctx, name, "alpine:latest", []string{"true"}, nil)
	show("Run before PullIntoSandbox", err)
	_, err = k.PullIntoSandbox(ctx, name, "docker.io/library/no-such-image-kosa8:v0")
	show("pull an image that does not exist", err)
	_, _ = k.PullIntoSandbox(ctx, name, "alpine:latest")
	_, err = k.Run(ctx, name, "alpine:latest", []string{"no-such-binary"}, nil)
	show("Run a command that does not exist", err)
	_, err = k.Exec(ctx, name, "k99-nope", []string{"true"}, nil)
	show("Exec in a container that does not exist", err)
	_, err = k.Attach(ctx, name, "k99-nope", nil)
	show("Attach to a container that does not exist", err)
	_, err = k.Fork(ctx, name, 1, kosa8.ForkOptions{Label: "experiment"})
	show("Fork with a free-text Label", err)
	_, err = k.Snapshot(ctx, name, kosa8.SnapshotOptions{ID: "doc-go-g-errs-s1"})
	show("first snapshot", err)
	_, err = k.Snapshot(ctx, name, kosa8.SnapshotOptions{ID: "doc-go-g-errs-s1"})
	show("snapshot id taken twice", err)
	_, err = k.DeleteSnapshot(ctx, "doc-go-g-errs-s1", kosa8.SnapshotDeleteOptions{})
	show("delete snapshot", err)
	_, err = k.StopSandbox(ctx, name)
	_, err = k.Run(ctx, name, "alpine:latest", []string{"true"}, nil)
	show("Run on a stopped sandbox", err)
	_, err = k.Snapshot(ctx, name, kosa8.SnapshotOptions{})
	show("Snapshot a stopped sandbox", err)
}
$ go run .
== create the same name twice
   Kind=conflict Status=409 ErrAPI=true
   Message: sandbox doc-go-g-errs already exists (choose another name, or remove the existing sandbox with `kosa8 sandbox rm NAME` (`kosa8 sandbox ls` lists them))
== Run before PullIntoSandbox
   Kind=stream Status=0 ErrAPI=false
   Message: image "docker.io/library/alpine:latest" not found in engine: image "docker.io/library/alpine:latest": not found
== pull an image that does not exist
   Kind=api Status=500 ErrAPI=true
   Message: pull : manifest: unauthorized after token refresh (check the image reference and network)
== Run a command that does not exist
   Kind=stream Status=0 ErrAPI=false
   Message: failed to create shim task: OCI runtime create failed: executable file `no-such-binary` not found in $PATH: No such file or directory
== Exec in a container that does not exist
   Kind=stream Status=0 ErrAPI=false
   Message: no such container: k99-nope
== Attach to a container that does not exist
   Kind=stream Status=0 ErrAPI=false
   Message: no such container: k99-nope
== Fork with a free-text Label
   Kind=policy Status=403 ErrAPI=true
   Message: refused by label policy: label "experiment": level "EXPERIMENT" is not defined (defined: UNCLASSIFIED, CUI); an org policy defines levels under "labels" (a fork keeps its parent's label or raises it; see `kosa8 label ls`)
== first snapshot
   <nil> <nil>
== snapshot id taken twice
   Kind=conflict Status=409 ErrAPI=true
   Message: snapshot already exists: doc-go-g-errs-s1 (choose another id, or delete the existing snapshot first)
== delete snapshot
   <nil> <nil>
== Run on a stopped sandbox
   Kind=conflict Status=409 ErrAPI=true
   Message: sandbox is not running: doc-go-g-errs (start it with `kosa8 sandbox up doc-go-g-errs`)
== Snapshot a stopped sandbox
   Kind=api Status=500 ErrAPI=true
   Message: sandbox is not running: doc-go-g-errs (sandbox must be running)

(<nil> <nil> is how this show prints a call that succeeded.) Note the same condition, "sandbox is not running", is a 409 for Run and a 500 for Snapshot; errors.Is(err, kosa8.ErrConflict) only catches the first.

Each kind: when, and what to do

connection: the daemon cannot be reached

When. The socket file does not exist, nothing is listening on it, a remote worker does not answer, or the answer was cut off before it could be read. The message names the endpoint, and adds (is kosa8 installed and its daemon running? …) when the cause looks like "not running". Err is the *url.Error from net/http.

What to do. kosa8 daemon start, then kosa8 version. Check that your program and the daemon agree on HOME, since the socket is $HOME/.kosa8/run/kosa8.sock. If KOSA8_HOST is set in your shell, the SDK goes there instead; k.Endpoint().Target() prints where it is going. Retrying is reasonable only right after a daemon restart.

usage: caught before sending

When. From New: an Actor other than human, agent, tool; a KOSA8_HOST/Host with an unsupported scheme or no host name; plain http:// to a host that is not loopback without Insecure or KOSA8_INSECURE=1. From calls: EgressAllow with an Egress other than ""/"deny"; Explore with fewer than two variants, a fork that returned fewer branches than variants, or a Keep that names no branch (that one also returns the result, with every branch kept); a malformed or relative Mounts entry; a Capture with no Path:

mount "hostdir:/work": both paths must be absolute (use filepath.Abs for the host side)
Capture.Path is required: the host file the capture is written to

New in v0.1.1, two mistakes that used to pass silently:

Exec does not take Env: the container keeps the environment it was started with. Set it at StartInteractive (InteractiveOptions.Env), or wrap the command: []string{"env", "K=V", "cmd"}
stdin of container k8-e5df7b9b in doc-go-g-run was already sent and is closed: the daemon treats one WriteStdin body as the whole of stdin. Pass an io.Pipe reader to stream input

The first is Exec with RunOptions.Env (the daemon's exec takes no environment; use []string{"env", "K=V", "cmd", …}, or start the container with StartInteractive and its InteractiveOptions.Env). The second is a second WriteStdin to the same container from the same Client. Other real messages are in the guide and the output above.

What to do. Fix the arguments; these never succeed on retry. Test with errors.Is(err, kosa8.ErrUsage) (v0.1.0 had no sentinel; e.Kind == kosa8.KindUsage works in both).

bad_request (400)

When. The daemon rejected a value, such as an unknown egress policy. What to do. Fix the value; read DaemonMessage, which says what it wanted. Not retryable.

auth (401)

When (from the source; not produced live). A remote worker rejected or did not get the bearer token. The common cause is setting Options.Host in code while the token is only in KOSA8_TOKEN: the env token is used only with an env host. What to do. Pass Token explicitly, or set both KOSA8_HOST and KOSA8_TOKEN and leave Host empty. A unix socket never sends a token.

policy and approval_required (403)

When. A rule refused the action. Live, we got one from the label policy, by passing a free text ForkOptions.Label (above): Label on forks and restores is a classification level, not a description. The other 403s (org policy rules on registries, egress, mounts, and so on, and refusals that a signed approval would lift) were not produced live, since no policy was installed. From the source: a 403 whose body has an approval object with a why key becomes approval_required, with that object in e.Approval; anything else is policy.

What to do. For policy, do not retry: change what you ask for, or ask whoever owns the policy (kosa8 policy show, Company rules). For approval_required, show the user e.Approval["why"] and the remediation, get the approval recorded, then repeat the call with its id in Call{Approval: id} or the options' Approval field.

if errors.Is(err, kosa8.ErrApprovalRequired) {
	e, _ := kosa8.AsError(err)
	fmt.Println("needs approval:", e.Approval["why"])
	fmt.Println("then:", e.Remediation)
	return err
}

not_found (404)

When. No sandbox or snapshot by that name, for most calls. Exceptions seen live: RestoreSnapshot of a missing snapshot is a 500 (api); and Exec/Attach on a missing container is a stream error. GetSandbox never returns not_found: a missing sandbox is nil, nil.

What to do. In clean-up code, treat it as success (err != nil && !errors.Is(err, kosa8.ErrNotFound)). Elsewhere, it usually means something else deleted the sandbox, or a rewind failed halfway (see below).

conflict (409)

When. A sandbox name or snapshot id is taken; the sandbox is stopped and you asked it to run something. Checkpoint handles its own case: when two checkpoints of one sandbox race for the same id, the loser's snapshot gets snapshot already exists and, from v0.1.1, moves to the next number (up to 10 tries), so you do not see it (see Goroutines). Any other 409 from that snapshot is returned. In v0.1.0 this race came back to you as:

snapshot already exists: doc-go-g-tick-ckpt-2 (choose another id, or delete the existing snapshot first)

What to do. Pick another name, delete the old one, or sb.Start(ctx) and retry.

unsupported (501)

When (from the source). The daemon does not implement the call on this platform or version. What to do. Upgrade kosa8 (kosa8 version shows both sides), or use another way to get the same result.

api: everything else

When. Any other HTTP error, mostly 500s with the daemon's own explanation: the guest did not boot in time, a pull failed, a restore failed, a snapshot of a stopped sandbox. From kosa8 0.9.36 a restore refused because a Mac's screen is locked is a 503. Also used when a successful answer could not be decoded (decode …, with Err set).

What to do. Read Message; the daemon's remediation is in it. Many of these are about the machine (memory, disk, a locked screen), so a retry after fixing that can work. See the table below.

stream: the command could not run

When. Run, Exec and Attach answer with a stream of frames, so the HTTP status is already 200 by the time the daemon finds out the command cannot start. It then sends a final frame with an error, and the SDK returns KindStream with Status 0. Seen live: image not pulled into the sandbox, an executable that does not exist, a container id that does not exist. The SDK also returns KindStream for a frame it cannot decode, and for a stream that ends without a final frame (stream ended unexpectedly (no final frame)), which is what a daemon restart in the middle of a run looks like. Stdout/Stderr on the error hold any output that arrived first.

What to do. Pull the image with sb.Pull; check the command's spelling (a missing executable is an error, not exit 127); check the container id with sb.Containers. Remember errors.Is(err, kosa8.ErrAPI) is false for these. ExecFrames passes the error frame to your function (f.Done and f.Error set) and then returns the same KindStream error, from v0.1.1 (v0.1.0 returned nil):

  frame done=true error="no such container: k99-nope"
ExecFrames on a missing container: true no such container: k99-nope

timeout, and cancellation

When. The context's deadline passed, or RunOptions.Timeout did. Before the answer started: <METHOD> <path> timed out talking to <endpoint>, with Err wrapping context.DeadlineExceeded. During a stream: … timed out mid-stream, with the output so far. A cancelled context returns the plain context.Canceled, not a *kosa8.Error.

What to do. Either way, the work inside the sandbox keeps running: only your program stopped waiting. In our test, two sleep 30 containers whose Run had timed out or been cancelled were still listed as running afterwards. Stop or delete the sandbox if that matters, or bound the command itself (timeout 30 …). Give long operations (create, fork, explore) generous deadlines: on our busy machine a create took 12 to 14 s, the quickstart's took 21 s, and an explore with three variants took 87.5 s.

checkpoint: bookkeeping, and a failed rewind

When. All before anything is sent, except the last:

What to do. For the last one, fix the cause (usually a locked Mac screen, see below), then run exactly the call in the message:

if errors.Is(err, kosa8.ErrCheckpoint) && errors.Is(err, kosa8.ErrAPI) {
	// Rewind removed the sandbox and could not restore it. After fixing the cause:
	_, err = k.RestoreSnapshot(ctx, target.ID, kosa8.RestoreOptions{Name: sandbox})
}

Here target is the checkpoint you rewound to (from ListCheckpoints before the call). The checkpoint records are left as they were, so after a successful manual restore, later checkpoints are still in the stack.

Troubleshooting

SymptomCauseFix
kosa8 daemon unreachable at unix://…: … no such file or directoryThe daemon is not running, or runs under another HOME.kosa8 daemon start; run your program with the same HOME.
Connection error, bind: invalid argument in the daemon log (macOS)Socket path over 104 bytes: HOME is a deep temporary folder (CI, test harnesses).Use a short HOME or a symlink to it, for the daemon and your program. See the JavaScript notes.
guest not ready on vsock port 1024 after 25s (run `kosa8 doctor`; check ~/.kosa8/log/) from CreateSandboxThe VM did not boot in time; in our tests, low free memory with several other sandboxes running.Delete sandboxes you do not need, give new ones less MemMB, and retry after a pause (recipe). Nothing was left behind by the failed create.
image "…" not found in engine (KindStream)The image was not pulled into this sandbox. k.Pull pulls into the host, not a sandbox.sb.Pull(ctx, image) first. Forks inherit their parent's images.
pull : manifest: unauthorized after token refreshThe image does not exist, or is private.Check the reference (name and tag) with kosa8 pull on the host.
executable file `x` not found in $PATHThe program is not in the image. It is an error, not exit 127.Use a full path, another image, or sh -c for shell builtins.
no such container: k…Wrong id, or the container exited, or the sandbox was restarted (containers do not survive a stop).sb.Containers(ctx); start a new one.
Exec does not take Env (KindUsage)The daemon's exec has no environment; v0.1.0 silently dropped it.Wrap the command: []string{"env", "FOO=bar", "sh", "-c", "…"}.
mount "…": want host:guest or host:guest:ro, or both paths must be absoluteA Mounts entry without both parts, with a suffix other than ro/rw, or (v0.1.2) with a relative path."/abs/host/dir:/guest/dir:ro". (On v0.1.0, every mount failed with mount has no guest path; upgrade.)
capture: open …: is a directory (500)CaptureOptions.Path is the pcapng file to write, not a directory.Pass a file path, such as dir/traffic.pcapng.
stdin of container … was already sent and is closedA second WriteStdin: one body is the whole of stdin.One call per container, with an io.Pipe reader to feed input over time.
refused by label policy: label "…": level "…" is not definedForkOptions.Label/RestoreOptions.Label is a classification level.Leave it empty, or use a level from kosa8 label ls. Use CreateOptions.Labels for tags.
sandbox … already exists (409)The name is taken, often by a sandbox an earlier run did not delete.Delete it, or use unique names; see the clean-up recipe.
snapshot already exists: NAME-ckpt-N from Checkpointv0.1.0: two checkpoints of one sandbox at the same time.Upgrade to v0.1.1, which steps to the next id.
sandbox is not running (409 or 500)The sandbox was stopped, by you or by a daemon restart.sb.Start(ctx); running containers are gone after a stop.
macOS blocks restoring a VM while the screen is locked; unlock and retry (503), from Rewind, Fork, Explore, RestoreSnapshotApple's Virtualization framework cannot restore a saved VM while the Mac is locked. Saving still works.Keep the Mac unlocked during jobs that restore, or use Linux. After a failed rewind, run the RestoreSnapshot call from its message. Details on the JavaScript troubleshooting page.
… timed out mid-stream, but the work goes onTimeouts and cancellation stop the client only.Stop or delete the sandbox, or bound the command with timeout.
kosa8 rewind does not see checkpoints taken in GoThe client uses a private store (Options.Checkpoints), or a different HOME.Use the default store; check ~/.kosa8/checkpoints.json.
401 from a remote worker though KOSA8_TOKEN is setOptions.Host was set in code; the env token is only used with KOSA8_HOST.Pass Token as well.
refusing to talk to HOST over plain httpPlain HTTP to a non-loopback worker.Use https://, or Insecure: true / KOSA8_INSECURE=1 on a private link.
Leftover sandboxes after a crash or Ctrl-CThe program exited without running its deferred deletes (os.Exit, log.Fatal, a signal).signal.NotifyContext and defers with a fresh context, as in the recipe; clean up with kosa8 sandbox ls / kosa8 snapshot ls.
Still stuck?

kosa8 doctor checks the installation, and ~/.kosa8/log/kosa8d.log has the daemon's side of every refusal. When something is wrong covers the daemon itself. The developer guide shows each call working.