Skip to main content

Go SDK guide

Connect a service or agent.

Give your service a durable device identity so it can take part in private resource access. Enroll once, keep its state across restarts, and use the returned client in your application. Enrollment uses an authenticated outbound channel; publishing a resource and opening an access link are separate steps.

Snippets are written against github.com/layervai/qurl-go@v0.12.0.

Install

Prepare the deployment and local state.

Use the Go version required by the SDK’s go.mod or newer. The terminal block initializes a new module; skip that line in an existing project. Set QURL_DEPLOYMENT to the deployment file supplied during LayerV setup. It supplies the Hub trust root used for enrollment. See the versioned SDK guide for deployment configuration and WithAgentRuntimeHub.

Terminal
# In a new directory; skip init if your project already has go.mod.
go mod init example.com/private-agent
go get github.com/layervai/qurl-go/qurl@v0.12.0
Deployment configuration
# Use the deployment file supplied for your environment.
export QURL_DEPLOYMENT=/path/to/deployment.json
mkdir -p .qurl
chmod 700 .qurl
01

Open durable state first

OpenFileAgentState gives the SDK somewhere to keep the registration. Everything after the first successful enrollment depends on this file surviving restarts — it holds the device credential the platform minted.

02

Supply the credential you were issued

WithAgentRuntimeEnrollmentCredential takes either an API key or a one-shot enrollment token. It is used only when nothing is registered yet; a start that finds an existing registration ignores it entirely.

03

Say how this runtime proves itself

Install an OTP provider for the default path, or declare the runtime headless. The two options contradict each other and cannot be combined — one says how to read a code, the other says no code can be read.

04

Then keep calling it on every start

The same call enrolls when nothing is registered, resumes an interrupted enrollment, and otherwise opens the existing registration and renews its assignment when needed. A process restarting after a weekend outage runs the same code as one restarting after thirty seconds.

Default path

An API key and an emailed code. Nothing minted in advance.

With deployment configuration in place, give the runtime your API key and a callback that returns the emailed code. The complete file below includes the callback. Set QURL_API_KEY through your secret manager or environment, then run go run . from this directory. Use your own application version in the metadata. Keep .qurl out of source control.

main.go
package main

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"os/signal"
	"strings"

	"github.com/layervai/qurl-go/qurl"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	// Durable state. Keep this file across restarts: it is where the device
	// credential lands once enrollment completes.
	store, err := qurl.OpenFileAgentState("./.qurl/agent-state.json")
	if err != nil {
		log.Fatal(err)
	}
	defer store.Close()

	hostname, _ := os.Hostname()

	client, binding, err := qurl.ConnectAgentRuntime(ctx, store,
		qurl.WithAgentRuntimeEnrollmentCredential(os.Getenv("QURL_API_KEY")),
		qurl.WithAgentRuntimeMetadata(hostname, "example"),
		qurl.WithAgentRuntimeOTPProvider(readOneTimeCode),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer binding.Destroy()

	_ = client // use it to create portals and share resources
	<-ctx.Done() // keep the runtime alive until interrupted
}

// Read the emailed code. For unattended services, replace the
// terminal prompt with your own mailbox or operator workflow.
// Reuse the reader so a retry retains buffered input. This CLI example exits
// if enrollment is cancelled; a pending terminal read ends with the process.
var otpInput = bufio.NewReader(os.Stdin)

func readOneTimeCode(ctx context.Context, challenge qurl.AgentOTPChallenge) (string, error) {
	type result struct {
		code string
		err  error
	}
	input := make(chan result, 1)
	fmt.Fprint(os.Stderr, "Email one-time code: ")
	go func() {
		line, err := otpInput.ReadString('\n')
		if err == io.EOF && len(line) > 0 {
			err = nil
		}
		input <- result{strings.TrimSpace(line), err}
	}()
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case reply := <-input:
		return reply.code, reply.err
	}
}

What that call is doing

  • It enrolls only when nothing is registered yet. On every later start it finds the existing registration, renews its assignment when needed, and returns — so this is the code you run on every boot, not a one-time setup script.
  • It blocks inside your provider while the platform emails the code and waits for you to return it.
  • When enrollment completes, the platform mints a device credential and the SDK writes it into the state file. That credential — not the API key — is what authenticates the machine from then on.
  • Keep the state file and keep the metadata stable. The hostname and version you pass become part of the saved registration.

The one-time code

The code goes to the account behind the credential.

The platform emails an 8-digit code to the address on the account, and your callback returns it. This is not a “human” path: agents increasingly have their own mailboxes, and a service account or shared operations alias works just as well. All that matters is that something can read the address the code went to.

What the platform guarantees

At most one code is sent per attempt. The SDK never writes the code to disk. The challenge value passed to your callback is bounded, non-secret context for logging and correlation — it carries neither the credential nor anything replayable.

Return exactly 8 decimal digits, and honor the context: it is already bounded by the enrollment window, so a provider that blocks past it will be canceled rather than left to hang.

Headless path

No mailbox? Mint a token and say so explicitly.

An unattended build agent or a container in a pipeline — when no address in reach can receive the code, mint a one-shot enrollment token instead and declare the runtime headless. Mount durable storage at /var/lib/layerv/qurl, owned by the service user with mode 0700. The token was minted for exactly one enrollment, so it is its own proof and no code is sent.

First, mint the token

Create an enrollment token with target: agent from the console, or from an API key holding qurl:agent. It expires within 24 hours whether it is used or not, so mint it close to the run that will consume it.

Use this complete file instead of the interactive enrollment example.

main.go — headless enrollment
package main

import (
	"context"
	"log"
	"os"
	"os/signal"

	"github.com/layervai/qurl-go/qurl"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	// Mount durable, owner-only storage at this path.
	store, err := qurl.OpenFileAgentState("/var/lib/layerv/qurl/agent-state.json")
	if err != nil {
		log.Fatal(err)
	}
	defer store.Close()

	hostname, _ := os.Hostname()
	client, binding, err := qurl.ConnectAgentRuntime(ctx, store,
		qurl.WithAgentRuntimeEnrollmentCredential(os.Getenv("QURL_ENROLLMENT_TOKEN")),
		qurl.WithAgentRuntimeMetadata(hostname, "example"),
		qurl.WithAgentRuntimeHeadlessEnrollment(),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer binding.Destroy()

	_ = client // use it to create portals and share resources
	<-ctx.Done()
}

Rules for the headless path

  • It is the escape hatch, not the shortcut. Prefer the emailed code whenever an address in reach can receive one.
  • It cannot be combined with an OTP provider. One option says no code can be read, the other says how to read one; passing both is rejected before any network call.
  • It accepts one-shot enrollment tokens only. An API key handed to this path is refused, because honoring it would require a code this runtime has already said it cannot get.
  • A token is consumed by its first successful enrollment. Do not reuse one across machines, and do not use one concurrently.

After enrollment

The machine holds a device credential now.

Enrollment is the only step that needs an API key or an enrollment token. Once it completes, the state file holds the device credential the platform minted, and that is the machine’s identity from then on.

Restarts use the saved device credential

Run the same call on every start. It finds the registration and serves it, renewing the lease and following any relocation on the way.

If your service should not hold an enrollment credential at runtime, drop the credential option entirely: the call can then renew and serve an existing registration but can never create one.

The state file is the thing to protect

Losing it means the machine has to enroll again with a fresh credential. Keep it on durable storage, back it up as secret-bearing, and do not share one state file between two concurrently running processes.

Device credentials appear in your credential list, so you can see and revoke a machine from the account side.

A service that holds no enrollment secret

When enrollment happens somewhere else — an installer, a provisioning job, the first run of an image — leave the credential option off. The same call still renews the lease, follows relocations, and serves the registration; it simply has no way to create one.

This is why ConnectAgentRuntime is the only call your service needs: it does not have to know whether this particular start is the one that enrolls.

Every start
// Enrollment happened elsewhere — an installer, a provisioning job, a
// first run that already completed. Without a credential this call can
// renew and serve the existing registration, and can never create one.
client, binding, err := qurl.ConnectAgentRuntime(ctx, store,
	qurl.WithAgentRuntimeMetadata(hostname, "example"),
)
if err != nil {
	log.Fatal(err)
}
defer binding.Destroy()

_ = client // use it to create portals and share resources

Errors

Not sure which credential you have? Run the default and read the error.

The credential will not tell you what it is — you cannot parse a kind out of the token string, and the platform reports it on the first authenticated call. Go by how you got it: issued against an address means the emailed-code path, pre-issued for a machine means headless. Correct the reported credential or enrollment-path error, then retry with the same state file. Preserve that state so interrupted enrollment can resume with the same agent identity.

Handling the two enrollment-path errors
client, binding, err := qurl.ConnectAgentRuntime(ctx, store,
	qurl.WithAgentRuntimeEnrollmentCredential(credential),
	qurl.WithAgentRuntimeMetadata(hostname, "example"),
	qurl.WithAgentRuntimeOTPProvider(readOneTimeCode),
)
switch {
case errors.Is(err, qurl.ErrAgentOTPRequired):
	// The default path needs a way to read the emailed code. Install
	// WithAgentRuntimeOTPProvider, or say this runtime has no mailbox with
	// WithAgentRuntimeHeadlessEnrollment.
	return err
case errors.Is(err, qurl.ErrRegistrationKeyKindDisallowed):
	// The credential is a different kind than this enrollment path accepts.
	// The error message names the credential the platform actually saw and
	// the remedy for it.
	return err
case err != nil:
	return err
}

// On the success path, use the enrolled runtime.
_ = client
defer binding.Destroy()

ErrNoDeploymentHub

Set QURL_DEPLOYMENT to the deployment file supplied for your environment. Agent enrollment needs its Hub trust root; WithAgentRuntimeHub can supply it explicitly.

ErrInsecureAgentStatePermissions

Make the state directory accessible only to the runtime owner (mode 0700). Keep the state file private and on durable storage.

ErrAgentOTPRequired

The default enrollment path was taken with no way to read the emailed code. Returned before any network call, so nothing was sent and nothing enrolled. Install WithAgentRuntimeOTPProvider, or switch to WithAgentRuntimeHeadlessEnrollment with a one-shot enrollment token.

ErrRegistrationKeyKindDisallowed

The credential is real, but it is a different kind than this enrollment path accepts — most often a one-shot enrollment token handed to the default code path, or an API key handed to the headless path. The message names the kind the platform reported and the fix for it.

ErrOTPIncorrect / ErrOTPExpired

The code your provider returned was wrong or arrived too late. Nothing is registered, and a retry reuses the same agent identity rather than creating a second one.

ErrNoAccountEmail

The account behind the credential has no address the code could be sent to. Add one, or enroll headlessly with an enrollment token.

ErrDeviceKeyQuotaExceeded

The account cannot hold another active device credential. Revoke a device credential that no longer has a machine behind it, then retry.

Next

Enroll the machine, then use the client.

The client returned by enrollment creates portals and shares resources through the same API the reference documents.