Kibotos

Go SDK

The Go SDK lets Go programs call Kibotos. This page covers installing it, connecting to ctrl with an Operator identity, the conventions it carries for write operations, and reaching the files, processes and services of a Workspace.

The SDK speaks the native API described in API overview. Read the API pages for what each service does; this page covers what the SDK does for you.

What the SDK takes care of

Without the SDK you writeWith the SDK
mTLS with only the server CA, TLS 1.3, HTTP/2, no redirects, forward proxykibotos.Dial
Renewing the 30-day certificate and moving requests to connections that present the new oneRuns in the background until Close
Command IDs, and resending a command whose outcome you did not hearNewCommandID, DeriveCommandID, Settle
Checking capability names before using new fieldsClient.Require, Client.Has
Polling a Workspace command until it settlesClient.WaitWorkspace
Issuing, caching, renewing and replacing data-plane credentialsdataplane.Session
/v1/files transfers with integrity checks, resumable process streams, service connectionsMethods of dataplane.Session

Each row follows the rules on the native API pages: Authentication for the connection and the certificate, Write operations and errors for command IDs and capabilities, and Data plane for session credentials. Programs in other languages follow those pages directly.

Install

The SDK is the Go module github.com/felinics/kibotos/sdk/go. Every Kibotos release tags it as sdk/go/vX.Y.Z on the same commit, so the SDK of a release has the release's version. The first release that includes the SDK is v0.3.0:

go get github.com/felinics/kibotos/sdk/go@v0.3.0

The module needs Go 1.27 or later. It brings only connect-go, protobuf, github.com/coder/websocket and golang.org/x/net into your build.

Note

The Kibotos repository is not public yet. Until it is, go get needs read access to it and must not go through the public module proxy:

  1. Ask the Kibotos maintainers for read access to felinics/kibotos.

  2. Tell Go to fetch the module directly. GOPRIVATE also turns off the public checksum database for it:

    go env -w GOPRIVATE=github.com/felinics/kibotos
  3. Give Git credentials for GitHub, in one of two ways:

    • HTTPS with a token, for example through the GitHub CLI:

      gh auth setup-git
    • SSH, rewriting GitHub URLs to SSH:

      git config --global url."git@github.com:".insteadOf "https://github.com/"

In CI, set GOPRIVATE the same way and provide a token or deploy key that can read the repository. Without credentials, go get fails with could not read Username for 'https://github.com'.

Connect to ctrl

kibotos.Dial connects with the Operator identity in an identity store. The store holds the client certificate, its private key, the control-client CA and the server CA:

store, err := managedidentity.Open("/var/lib/example/kibotos-identity")
if err != nil {
	log.Fatal(err)
}
client, err := kibotos.Dial(ctx, "https://ctrl.example.com:18092", store,
	kibotos.WithEnrollmentKey(os.Getenv("KIBOTOS_ENROLLMENT_KEY")))
if err != nil {
	log.Fatal(err)
}
defer client.Close()
  • An empty store is enrolled first when WithEnrollmentKey is given. Generate the key with kibo enrollment create --operator, see Identity and enrollment. A store that already holds an identity never uses the key, so it can stay configured after it was consumed.
  • managedidentity.Open uses the layout of a kibo context directory. A directory enrolled with kibo context enroll works as is. The directory must not grant group or other access. A missing directory is created with the right mode. Give each program its own directory: ctrl accepts only the current and the previous certificate of an identity, so two programs renewing one identity lock each other out.
  • The certificate is renewed in the background. Inside the renewal window (the last 10 of its 30 days) the client renews over its own connections, writes the new certificate to the store, and then sends new requests on connections that present it. Requests in flight finish where they are.
  • Renewal is logged with slog.Default(), or the logger from WithLogger. Pass WithRenewalObserver to handle the events yourself.
  • Forward proxies follow HTTPS_PROXY, ALL_PROXY and NO_PROXY. WithProxy names one explicitly; WithProxy("") dials directly.

Keeping the identity somewhere else

A program that keeps secrets in its own storage implements kibotos.IdentityStore over it: Exists, Load and Publish.

Publish must persist the renewed identity before it returns. A renewal that is lost on restart leaves the program with a certificate that expires within 30 days.

For tests, or to load an identity given as PEM, use managedidentity.ParsePEM and managedidentity.NewMemoryFrom. Set OnPublish on the returned store to persist renewals.

Calling services

The client returns the generated Connect client of each service, for example client.Workspaces(), client.Volumes(), client.Egresses(), and client.Operator().NodeRegistration() for the Operator API. Every call uses the client's mTLS connection.

Write operations

commandID := kibotos.DeriveCommandID("example", "create-workspace", "order-1234")
created, err := client.Workspaces().CreateWorkspace(ctx, connect.NewRequest(&ctrlv1.CreateWorkspaceRequest{
	Metadata: &ctrlv1.CommandMetadata{CommandId: commandID},
	Spec: &ctrlv1.WorkspaceCreateSpec{
		Template:  &ctrlv1.TemplateSelector{TemplateId: "coding"},
		Resources: &ctrlv1.ResourceRequest{CpuMillis: 1000, MemoryBytes: 512 << 20},
	},
	Labels: map[string]string{"example.com/order": "1234"},
}))
if err != nil {
	log.Fatal(err)
}
workspace, err := client.WaitWorkspace(ctx, created.Msg.GetWorkspace(), commandID)
if err != nil {
	log.Fatalf("create failed: %v (reason %s)", err, kibotos.Reason(err))
}
HelperUse it for
NewCommandID(prefix)A random command ID, for example example-3f9c…
DeriveCommandID(prefix, parts...)A command ID from a key your program already has, such as an order number. After a restart the same key names the same command, so ctrl replays it instead of creating a second Workspace
Settle(ctx, options, send)Sending a command again under the same ID while ctrl says it is still executing or did not answer. A settled failure returns at once, because the same ID would only replay it. options.Executing replaces the default judgment StillExecuting
Client.WaitWorkspaceWaiting for a Workspace command until the Workspace is available. A failed command returns the error ctrl recorded
Client.Require(ctx, names...)Failing one operation when ctrl does not announce the capabilities it needs. An older ctrl silently drops fields it does not know
Detail(err), Reason(err)The ErrorDetail ctrl attached to a refusal

For the rules behind command IDs and expected revisions, see Write operations and errors.

The data plane

dataplane.Open prepares a session for one Workspace. It asks ctrl for a credential when the first call needs one:

session, err := dataplane.Open(client, workspace.GetWorkspaceId(), dataplane.WithScopes(
	ctrlv1.DataPlaneScope_DATA_PLANE_SCOPE_FILE_READ,
	ctrlv1.DataPlaneScope_DATA_PLANE_SCOPE_FILE_WRITE,
	ctrlv1.DataPlaneScope_DATA_PLANE_SCOPE_PROCESS,
))
if err != nil {
	log.Fatal(err)
}
defer session.Close()

content := "print('hello')\n"
if _, err := session.PutFile(ctx, "hello.py", strings.NewReader(content), int64(len(content)), dataplane.PutOptions{}); err != nil {
	log.Fatal(err)
}

stream, err := session.StartProcess(ctx, &ctrlv1.StartRequest{
	Argv: []string{"python3", "hello.py"},
	Mode: &ctrlv1.StartRequest_Pipe{Pipe: &ctrlv1.ProcessPipeMode{}},
})
if err != nil {
	log.Fatal(err)
}
defer stream.Close()
for stream.Receive() {
	event := stream.Event()
	switch {
	case event.Output != nil:
		os.Stdout.Write(event.Output.GetData())
	case event.Exited != nil:
		fmt.Println("exit code", event.Exited.GetExitCode())
	}
}
if err := stream.Err(); err != nil {
	log.Fatal(err)
}

How the credential is managed

  • One credential is shared by every call of the session until 70–80% of its lifetime has passed. Concurrent calls wait for one issuance.
  • A refused credential is replaced at once. When the node answers SESSION_EXPIRED or SESSION_REVOKED, the session asks for a new one. Unary and GetFile then send the call once more. The Filesystem and Desktop clients and PutFile do not resend; their next call uses the new credential.
  • Failed issuances back off: the second consecutive failure waits 1 second, doubling up to 30 seconds. Until then calls get the last error without asking ctrl, so a busy program does not turn its traffic into ctrl calls.
  • WithExecution(generation) binds the session to one execution of the Workspace, the generation that GetWorkspace reports. Once the Workspace runs another execution, calls fail with dataplane.ErrExecutionChanged; open a new session to reach the new execution.

What a session offers

MethodWhat it does
PutFile, GetFile/v1/files transfers. An upload is at most 1 GiB and checks the node's reported size and SHA-256; a download refuses a partial or unsized answer and fails a body that ends short. A file over 1 GiB is read in parts and fails with ErrFileChanged if it is rewritten in between; Download does the same for callers that manage their own credential. A transfer that moves no data for a minute ends
StartProcess, AttachProcessProcess streams that resume from the last output offset after a node restart or data-plane upgrade. See Resuming process streams
DialServiceOne connection to a service target in the Workspace, as a net.Conn
ServiceDialerA dialer for one target that renews its own credential, for an adapter that opens connections for as long as it runs
Filesystem, Desktop, UnaryGenerated Connect clients of the data-plane services, with the session's credential on every call

Service targets need WithServiceTargets. A node reached through a relay needs WithDialer: it opens the TCP connection, while TLS still verifies the node's own host name.

Complete programs

The Kibotos repository has programs that import only the SDK. Each one enrolls an empty identity directory on its first run:

go run ./examples/go/quickstart -endpoint https://ctrl.example.com:18092 \
  -identity "$HOME/.local/state/kibotos-quickstart" -enrollment-key-file quickstart.key
  • examples/go/quickstart: connect, create a Workspace, run a command, transfer a file, and delete it.
  • Scenarios: programs for a coding Agent, untrusted code, batches, and a desktop.

Compatibility

The Go API of the SDK changes only in MINOR releases until v1. PATCH releases do not change it. Each change is listed in the upgrade notes of the release in CHANGELOG.md.

The wire protocol keeps its own commitment, described in Compatibility commitments. A program built with an older SDK keeps working against a newer ctrl within the compatibility window it describes.

Next steps

On this page