api

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package api is a thin client for the Aquanode control API. Today it covers the device-login (pairing) endpoints used by `aq login`.

Index

Constants

View Source
const ImportObservationSchema = 1

ImportObservationSchema is the only ImportObservation.Schema value this aq build understands. The contract requires every consumer to reject an unknown schema loudly rather than guess at a shape it was never told about.

View Source
const SSHPort = 22

SSHPort is the internal logical port a box's sshd is published on. Deployment rows key their service_urls entries by this port on every provider.

Variables

View Source
var Version = "0.0.0-dev"

Version is the aq build version. `main` overwrites it at startup from its own ldflags-injected `version`, so this package can label its requests without importing main.

Functions

func IsTimeout added in v0.5.0

func IsTimeout(err error) bool

IsTimeout reports whether an error is this CLI's own HTTP client giving up, as opposed to the server answering with a refusal.

THE DISTINCTION IS THE WHOLE POINT, and collapsing it is how `aq` reported a FAILED attach on an attach that had completed. A timeout says nothing about what the server did: the request may have been served in full a moment later. It is never evidence of failure, only evidence that we stopped listening, so a caller that hits one must go and ASK what happened rather than announce an outcome it did not observe.

Types

type APIError

type APIError struct {
	Status  int
	Message string
	Data    json.RawMessage
}

APIError carries a non-success orchestrator response.

func (*APIError) Error

func (e *APIError) Error() string

type ActivateExternalResult added in v0.4.0

type ActivateExternalResult struct {
	Status         string `json:"status"`
	AgentLastSeen  string `json:"agentLastSeenAt"`
	Error          string `json:"error"`
	UnreachableWhy string `json:"reason"`
	// TerminalAvailable is whether the orchestrator confirmed the box's browser
	// terminal answers FROM ITS OWN INFRASTRUCTURE, not merely that the box
	// reported a listener to itself. Reported here so attach can say what the
	// user actually got instead of leaving them to find a dead Terminal tab.
	//
	// A POINTER because it is three-state. `nil` is "not determined yet": the
	// box configuration outlives the activate request (see Provisioning), and an
	// orchestrator predating that field omits it entirely. A plain bool would
	// render both of those as a confirmed dead terminal.
	TerminalAvailable *bool `json:"terminalAvailable"`
	// TerminalUnavailableReason is the orchestrator's machine-readable cause,
	// empty when the terminal IS available or when the orchestrator predates the
	// field. Empty is UNKNOWN, never "no reason".
	TerminalUnavailableReason string `json:"terminalUnavailableReason"`
	// ResticConfigured is whether the box holds the credential that makes a
	// snapshot taken on it readable later. Three-state for the same reason as
	// TerminalAvailable.
	ResticConfigured *bool `json:"resticConfigured"`
	// Provisioning is "complete" when every verdict above is a real answer, and
	// "in_progress" when the attach SUCCEEDED and the box configuration is still
	// running server-side. Empty from an orchestrator predating the field, which
	// is the old always-synchronous behaviour and reads as complete.
	//
	// The attach itself is done in both cases. This distinguishes "we know what
	// you got" from "you are attached and the optional extras are still being
	// set up", which is the distinction that used to be lost entirely.
	Provisioning string `json:"provisioning"`
}

ActivateExternalResult is the response to POST /deployments/external/:id/activate.

func (*ActivateExternalResult) Attached added in v0.5.0

func (r *ActivateExternalResult) Attached() bool

Attached reports whether the orchestrator says this box is attached, which is the only question `aq attach` succeeds or fails on. Deliberately independent of every verdict above: the terminal and the restic credential are optional capabilities configured AFTER the attachment, and neither one failing means the box is not attached.

func (*ActivateExternalResult) StillProvisioning added in v0.5.0

func (r *ActivateExternalResult) StillProvisioning() bool

StillProvisioning reports whether the box configuration outlived the request.

type AdoptExternalRequest added in v0.4.0

type AdoptExternalRequest struct {
	Name      string `json:"name"`
	Host      string `json:"host"`
	OgrePort  int    `json:"ogrePort"`
	GPU       string `json:"gpu,omitempty"`
	GPUCount  int    `json:"gpuCount,omitempty"`
	CPU       int    `json:"cpu,omitempty"`
	Memory    string `json:"memory,omitempty"`
	Storage   string `json:"storage,omitempty"`
	MountPath string `json:"mountPath,omitempty"`
}

AdoptExternalRequest is the body of POST /deployments/external.

Hardware fields are all optional and all nullable: this is a box we did not provision, so anything we did not observe stays absent rather than being guessed at or zeroed. A zero GPU count would read as "no GPUs", which is a different claim from "we did not look".

type AdoptExternalResult added in v0.4.0

type AdoptExternalResult struct {
	DeploymentID int    `json:"deploymentId"`
	InstallToken string `json:"installToken"`
	ExpiresAt    string `json:"expiresAt"`
	OgrePort     int    `json:"ogrePort"`
}

AdoptExternalResult is the response to POST /deployments/external.

type ArmWatchRequest added in v0.7.0

type ArmWatchRequest struct {
	GPUModel        string   `json:"gpuModel"`
	GPUCount        int      `json:"gpuCount"`
	MaxPricePerHour *float64 `json:"maxPricePerHour,omitempty"`
	ProviderScope   string   `json:"providerScope,omitempty"`
	Tier            string   `json:"tier,omitempty"`
	ExpiresInHours  *float64 `json:"expiresInHours,omitempty"`
	// IncludeUnreported is sent only when true; the key is absent otherwise.
	IncludeUnreported bool `json:"includeUnreported,omitempty"`
}

ArmWatchRequest is the body of POST /availability-watches. Every optional field is omitted when unset: the server's zod enums reject "" and a ceiling of 0 is not "no ceiling".

type AvailabilityWatch added in v0.7.0

type AvailabilityWatch struct {
	ID                  int      `json:"id"`
	GPUModel            string   `json:"gpuModel"`
	GPUCount            int      `json:"gpuCount"`
	MaxPricePerHour     *float64 `json:"maxPricePerHour"`
	ProviderScope       *string  `json:"providerScope"`
	Tier                *string  `json:"tier"`
	Status              string   `json:"status"` // WATCHING | NOTIFIED | EXPIRED | CANCELLED
	ExpiresAt           string   `json:"expiresAt"`
	NotifiedAt          *string  `json:"notifiedAt"`
	MatchedProvider     *string  `json:"matchedProvider"`
	MatchedPricePerHour *float64 `json:"matchedPricePerHour"`
	CreatedAt           string   `json:"createdAt"`
	ResolvedAt          *string  `json:"resolvedAt"`
	IncludeUnreported   bool     `json:"includeUnreported"`

	Raw json.RawMessage `json:"-"`
}

AvailabilityWatch is one row of /availability-watches. Raw holds the row exactly as the server sent it, so `--json` can print the server's own keys rather than a re-marshal that drops any this struct does not know.

func (*AvailabilityWatch) UnmarshalJSON added in v0.7.0

func (w *AvailabilityWatch) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes the row and keeps its raw bytes.

type BatchJob added in v0.6.0

type BatchJob struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Shape     string `json:"shape"`
	CreatedAt string `json:"createdAt"`
	Run       Run    `json:"run"`
}

BatchJob mirrors one element of `GET /jobs?shape=batch`, the object `GET /jobs/:id` returns for a batch job, and the 201 body of `POST /jobs` and `POST /jobs/:id/rerun` for a command entrypoint — the jobs-are-jobs spec's batch wire shape.

This is a SEPARATE type from Job, never a widened version of it: Job is the service-shape row `aq endpoint` still reads (status, runUrl, runningInstances, the scaling/schedule/token fields), and every one of those is explicitly ABSENT from a batch row's body under the new contract — a shared struct would silently decode zero values for fields that were never sent, which reads exactly like a row that legitimately has none. Run is ALWAYS present, never null: a batch job is 1:1 with the one Run it was created with.

type Checkpoint added in v0.5.0

type Checkpoint struct {
	Paths   []string `json:"paths"`
	Exclude []string `json:"exclude,omitempty"`
}

Checkpoint is the `{ paths, exclude? }` shape job.service.ts stores verbatim (the field is typed `unknown` server-side; only Paths is ever read, by hasCheckpointPaths). Paths carries no `omitempty`: once Checkpoint itself is present, the key must still read as present so the server's own "checkpoint.paths is required" refusal fires on an empty list, never silently on an absent key. Exclude keeps `omitempty` since most jobs need no exclusions and the console sends none when that field is blank.

type Client

type Client struct {
	BaseURL string
	HTTP    *http.Client
	APIKey  string // x-api-key (the aq_sk_… token from `aq login`)
	TeamID  string // x-team-id (required by team-scoped routes like /deployments)
}

Client talks to the Aquanode API base (e.g. https://server.aquanode.io/api/v1).

The device-login endpoints are public; the funnel endpoints (`up`, status, ssh-keys) require auth. When APIKey/TeamID are set they are sent as the `x-api-key` / `x-team-id` headers the orchestrator expects.

func New

func New(baseURL string) *Client

New returns a Client for the given base URL with a sane default timeout.

func NewAuthed

func NewAuthed(baseURL, apiKey, teamID string) *Client

NewAuthed returns a Client that sends the API key + team id on every request.

func NewPublic added in v0.4.0

func NewPublic(baseURL string) *Client

NewPublic returns a Client for the marketplace and any other unauthenticated route: no APIKey/TeamID is ever set on it, so it is impossible for a call made through it to trip the login gate regardless of what's on disk in ~/.config/aq. Callers must not call SetAuth-equivalent fields on the result.

func (*Client) ActivateExternal added in v0.4.0

func (c *Client) ActivateExternal(deploymentID int) (*ActivateExternalResult, error)

ActivateExternal asks the orchestrator to probe the box and, only if a real round-trip succeeds, flip it ACTIVE.

The probe is deliberately server-side: what has to be true is that OUR infrastructure can reach the box, and the CLI proving it can reach it from the user's laptop answers a different question. A failure returns ExternalUnreachableError carrying the orchestrator's own reason string, which callers surface verbatim — a paraphrase of a dial error is worse than useless to whoever has to open the firewall.

func (*Client) AdoptExternal added in v0.4.0

func (c *Client) AdoptExternal(req AdoptExternalRequest) (*AdoptExternalResult, error)

AdoptExternal creates the Deployment row for a box we did not rent. It bills nothing: the orchestrator sets pricePerSecond and providerCostPerSecond to zero, leaves leaseId null, and does not emit a billing START.

func (*Client) ArmAvailabilityWatch added in v0.7.0

func (c *Client) ArmAvailabilityWatch(req ArmWatchRequest) (*AvailabilityWatch, error)

ArmAvailabilityWatch arms a notify-when-available watch.

func (*Client) CancelAvailabilityWatch added in v0.7.0

func (c *Client) CancelAvailabilityWatch(id int) error

CancelAvailabilityWatch cancels an active watch; a 404 means it is not active.

func (*Client) CancelQueuedDeployment added in v0.7.0

func (c *Client) CancelQueuedDeployment(id int) error

CancelQueuedDeployment cancels a queued request; a 404 means it is not active.

func (*Client) CancelRun added in v0.5.0

func (c *Client) CancelRun(jobID, runID string) (*Run, error)

CancelRun asks for a run to stop. Cancelling an already-finished run is a no-op success server-side, so the CLI does not have to special-case a race it cannot see.

func (*Client) CloseDeployment

func (c *Client) CloseDeployment(deploymentID int) (*CloseResult, error)

CloseDeployment requests termination of a deployment (`aq down`). The orchestrator validates team ownership and tears the box down asynchronously.

func (*Client) CompleteImport added in v0.4.0

func (c *Client) CompleteImport(req ImportCompleteRequest) (*ImportCompleteResult, error)

CompleteImport registers the capture as the volume's first history point, consuming the single-use import token. The token is deleted server-side on read, so a retried call after a transport error (rather than a genuine second import) will be refused. That's a real gap in this v1 client, noted rather than papered over.

func (*Client) CreateBatchJob added in v0.6.0

func (c *Client) CreateBatchJob(req CreateBatchJobRequest) (*BatchJob, error)

CreateBatchJob creates a batch job AND its one Run in a single call — POST /jobs with a command entrypoint. Unlike the old create-then-run (create)/(job runs) split, this is the only way to make a batch job at all now: there is no follow-up call that starts it.

func (*Client) CreateEndpoint added in v0.4.0

func (c *Client) CreateEndpoint(req CreateEndpointRequest) (*Job, error)

CreateEndpoint makes an image callable over HTTP, returning the created job row (shape "service").

func (*Client) CreateSSHKey

func (c *Client) CreateSSHKey(name, publicKey string) (*SSHKey, error)

CreateSSHKey registers a public key and returns the created row (with its id).

func (*Client) CreateSecret added in v0.5.0

func (c *Client) CreateSecret(teamID string, req CreateSecretRequest) (*Secret, error)

CreateSecret stores a new team secret. The response never carries the value back, not even this once, only the id/name/type/timestamps.

func (*Client) CreateSetup added in v0.6.0

func (c *Client) CreateSetup(req CreateSetupRequest) (*Setup, error)

CreateSetup creates a new pod and starts it on the given offer in one call, POST /setups. See CreateSetupRefusedStart for the one error shape this unwraps specially; every other failure (a validation 400, a name conflict 409, ...) surfaces as a plain *APIError with no pod created.

func (*Client) DeleteEnvironment added in v0.6.0

func (c *Client) DeleteEnvironment(environmentID string) error

DeleteEnvironment deletes a Kept or Shared environment: DELETE /environments/:id. Breaks nothing running; existing share links to it stop working.

func (*Client) DeleteJob added in v0.5.0

func (c *Client) DeleteJob(jobID string) error

DeleteJob removes a job — DELETE /jobs/:id.

func (*Client) DeleteSecret added in v0.5.0

func (c *Client) DeleteSecret(teamID, secretID string) error

DeleteSecret removes a team secret. Any Job still naming it in `secrets` or `image.registrySecret` starts failing that reference at its next dispatch: the orchestrator does not cascade-clean Job rows on delete.

func (*Client) DeleteVolume added in v0.6.0

func (c *Client) DeleteVolume(volumeID string) (*DeleteVolumeResult, error)

DeleteVolume deletes a volume and its whole history, DELETE /volumes/:id. Refused as a *VolumeInUseError while a pod's live box has it; otherwise every STOPPED pod that referenced it is detached (its config and history untouched, just pointed at no volume) and returned in DetachedPods.

func (*Client) Deploy

func (c *Client) Deploy(req DeployRequest) (*UpResult, error)

Deploy rents the cheapest matching GPU and restores the given snapshot onto it.

func (*Client) DeploymentStatus

func (c *Client) DeploymentStatus(deploymentID int) (*DeploymentStatusResult, error)

DeploymentStatus fetches the current status of a deployment.

func (*Client) DownloadRunArtifactURL added in v0.5.0

func (c *Client) DownloadRunArtifactURL(jobID, runID, key string) (*RunArtifactDownload, error)

DownloadRunArtifactURL mints a download URL for one artifact key. A key that no longer resolves (the run moved on, the box is gone) surfaces as an *APIError -- 404 for "no attempt"/"no box", 503 for "could not reach the box" (RunArtifactUnreachableError, orchestrator jobs.controller.ts) -- which the caller must not treat as an empty artifact.

func (*Client) DuplicateVolume added in v0.6.0

func (c *Client) DuplicateVolume(volumeID string, name string) (*Volume, error)

DuplicateVolume forks a volume into a brand new one at its latest point, an honest fork, writes never merge back. The copy lands server-side (Copying is true on the result until it does); it may take a moment to become attachable.

func (*Client) GetAvailabilityWatch added in v0.7.0

func (c *Client) GetAvailabilityWatch(id int) (*AvailabilityWatch, error)

GetAvailabilityWatch reads one watch in ANY status. A 404 means it is missing or belongs to another team.

func (*Client) GetDeployment added in v0.4.0

func (c *Client) GetDeployment(deploymentID int) (*Deployment, error)

GetDeployment fetches the raw deployment row by id (GET /deployments/:id), including ProjectID — the fields the transformed /status and list endpoints don't carry.

func (*Client) GetEnvironmentPreview added in v0.6.0

func (c *Client) GetEnvironmentPreview(setupID string) (*EnvironmentPreview, error)

GetEnvironmentPreview fetches the outgoing-paths preview for setupID's current (working) environment: GET /setups/:id/environment/preview.

func (*Client) GetIdlePolicy added in v0.3.0

func (c *Client) GetIdlePolicy(deploymentID int) (*IdlePolicy, error)

GetIdlePolicy fetches a deployment's merged idle-auto-stop policy and its current live verdict.

func (*Client) GetProjectDeployment

func (c *Client) GetProjectDeployment(projectID string) (*Deployment, error)

GetProjectDeployment returns the current (active or paused) deployment under a project, via GET /deployments/project/:projectId. `aq down`/`aq status` use it to accept a project id (the UUID in the console URL) in place of the numeric deployment id (#209). A project with no live deployment yields a 404 APIError.

func (*Client) GetRun added in v0.5.0

func (c *Client) GetRun(jobID, runID string) (*Run, error)

GetRun fetches one run by id — GET /jobs/:id/runs/:runId.

func (*Client) GetRunLogs added in v0.5.0

func (c *Client) GetRunLogs(jobID, runID string, offset int64, attempt int) (*RunLogChunk, error)

GetRunLogs tails a run's log from a byte offset. `attempt` of 0 means the latest attempt — a run that failed over has several, and concatenating them would produce a log whose timestamps go backwards in the middle.

func (*Client) GetSetup added in v0.6.0

func (c *Client) GetSetup(setupID string) (*Setup, error)

GetSetup fetches one pod by id, GET /setups/:id (sendPod in setups.controller.ts: the same serializePods DTO a list row gets, just for one row). `aq status` uses this to recover the pod's real Volume save state from the deployment's setup_id: the deployment endpoints carry no save/volume information of their own, only the setup does.

func (*Client) GetSetupVersion added in v0.4.0

func (c *Client) GetSetupVersion(versionRowID int) (*SetupVersion, error)

GetSetupVersion fetches one version row by its own global row id — GET /setups/versions/:id. `aq job point` uses this to learn which lineage (setup id + name) an endpoint's CURRENT version belongs to: the repoint API and the `<version>` a user types are both scoped to a version NUMBER within one lineage, but an Endpoint only carries its current VersionID, not the owning setup — this is the lookup that recovers it, the same way ListSetupVersions recovers a row id from a (setup, version-number) pair.

func (*Client) GetShareStatus added in v0.6.0

func (c *Client) GetShareStatus(shareID string) (*ShareStatus, error)

GetShareStatus polls a share's publish-job status.

func (*Client) GetVolume added in v0.6.0

func (c *Client) GetVolume(volumeID string) (*Volume, error)

GetVolume fetches one volume's detail, including who references it and its full point history, GET /volumes/:id.

func (*Client) HardwareAvailability added in v0.5.0

func (c *Client) HardwareAvailability(diskGB int) (*HardwareAvailability, error)

HardwareAvailability fetches the GPU model universe the marketplace can currently serve for a disk-size constraint: GET /jobs/hardware-availability (team-scoped, unlike the public /marketplace feed `aq gpus` reads).

Used only by `aq job create --image ... --any-gpu`, the explicit opt-in that mirrors the console's "no card picked means any card" default (console/app/jobs/new/page.tsx:510-516): fetch every model the market has right now and send all of them, rather than leaving hardware.gpuModels empty, which placement refuses outright with no_gpu_models.

func (*Client) KeepSetupEnvironment added in v0.6.0

func (c *Client) KeepSetupEnvironment(setupID string, name string) (*KeepEnvironmentResult, error)

KeepSetupEnvironment names setupID's current environment so it lists under Yours in the New pod picker and survives pod deletion. It mints nothing by itself: Share is the only thing that mints a version, and Keep on a pod whose environment already has one just files the existing lineage under a name.

func (*Client) ListAllSetupVersions added in v0.4.0

func (c *Client) ListAllSetupVersions() ([]SetupVersion, error)

ListAllSetupVersions returns every version row the caller can see across every one of their setups — GET /setups/versions with no `name` filter. The orchestrator's listSnapshotVersions only takes the legacy-lineage merge path when `name` is unset (listSnapshotVersions in setups.controller.ts), so this also carries any legacy/external (backup_id-owned, no SetupID) rows mixed in; callers matching on SetupID filter those out for free.

This is the only way to recover a setup's latest saved version — the `Setup` row itself (GET /setups) carries no nested "latest version" field on the wire (see the Setup doc comment), so `aq setups`' VERSION column and `aq share`'s (setup, version-number) resolution both derive it from this list instead of trusting anything nested.

func (*Client) ListAvailabilityWatches added in v0.7.0

func (c *Client) ListAvailabilityWatches() ([]AvailabilityWatch, error)

ListAvailabilityWatches lists the team's WATCHING rows.

func (*Client) ListBatchJobs added in v0.6.0

func (c *Client) ListBatchJobs() ([]BatchJob, error)

ListBatchJobs returns the caller's batch (command-entrypoint) jobs only — GET /jobs?shape=batch. `aq job ls` must never show a service (endpoint) row: the two read as unrelated concepts to a caller, same reasoning as ListJobsByShape("service") for `aq endpoint list`.

func (*Client) ListDeployments added in v0.2.0

func (c *Client) ListDeployments() ([]Deployment, error)

ListDeployments returns every deployment on the caller's team, newest first.

Two properties of GET /deployments to keep in mind: it takes no filters (the documented `type`/`provider`/`sortByTime` query params are parsed by a schema the controller never reads), and it returns the team's FULL history including CLOSED and FAILED rows. Callers must filter client-side, and should only call it where a list is genuinely needed — on a long-lived team it is a large response.

func (*Client) ListEnvironmentVersions added in v0.6.0

func (c *Client) ListEnvironmentVersions(environmentID string) ([]EnvironmentVersion, error)

ListEnvironmentVersions returns every version of a named (Kept or Shared) Environment: GET /environments/:id/versions. An owner sees every version; a recipient sees only the one version shared with them.

func (*Client) ListEnvironments added in v0.6.0

func (c *Client) ListEnvironments() (*EnvironmentsResult, error)

ListEnvironments fetches every environment the caller can pick from: GET /environments.

func (*Client) ListJobs added in v0.5.0

func (c *Client) ListJobs() ([]Job, error)

ListJobs returns every job the caller owns, of every shape.

func (*Client) ListJobsByShape added in v0.5.0

func (c *Client) ListJobsByShape(shape string) ([]Job, error)

ListJobsByShape returns only the caller's jobs of one shape ("batch" or "service") — GET /jobs?shape=. Used by `aq endpoint list`, which must never show a batch job: the two read as unrelated concepts to a caller (a job you run vs. a URL you call), and mixing them on one page is the exact confusion the shape split exists to avoid.

func (*Client) ListQueuedDeployments added in v0.7.0

func (c *Client) ListQueuedDeployments() ([]QueuedDeployment, error)

ListQueuedDeployments lists the team's QUEUED rows.

func (*Client) ListRunArtifacts added in v0.5.0

func (c *Client) ListRunArtifacts(jobID, runID string) (*RunArtifactsList, error)

ListRunArtifacts lists one run's landed artifacts (the latest attempt, server-selected -- this client never sends its own ?attempt=, there is no CLI surface that picks one). Always inspect Source before trusting Artifacts; see RunArtifactsList's doc.

func (*Client) ListRuns added in v0.5.0

func (c *Client) ListRuns(jobID string) ([]Run, error)

ListRuns returns a job's recent runs — GET /jobs/:id/runs.

func (*Client) ListSSHKeys

func (c *Client) ListSSHKeys() ([]SSHKey, error)

ListSSHKeys returns the caller's registered SSH keys.

func (*Client) ListSecrets added in v0.5.0

func (c *Client) ListSecrets(teamID string) ([]Secret, error)

ListSecrets returns every secret on the team, names and metadata only. Any team member may call this: it is what `aq job create --secret` resolves a name against, not an admin-only operation.

func (*Client) ListSetupEnvironmentVersions added in v0.6.0

func (c *Client) ListSetupEnvironmentVersions(setupID string) ([]EnvironmentVersion, error)

ListSetupEnvironmentVersions returns every version of setupID's own environment lineage: GET /setups/:id/environment/versions. The owner sees the full history.

func (*Client) ListSetupVersions added in v0.4.0

func (c *Client) ListSetupVersions(name string) ([]SetupVersion, error)

ListSetupVersions returns every version row named `name`, GET /setups/versions?name=<name>. The name alone is NOT unique to one setup (two different setups' lineages can share a chosen name), so a caller that needs one particular setup's version must additionally filter the result on SetupID. This is `aq job point`'s path from a (setup, version-number) pair to the version ROW id a repoint request actually needs: version and id are different counters (see GetSetupVersion), and this is the one lookup that resolves one to the other instead of guessing.

func (*Client) ListSetups added in v0.4.0

func (c *Client) ListSetups() ([]Setup, error)

ListSetups returns every setup the caller owns.

func (*Client) ListVolumes added in v0.6.0

func (c *Client) ListVolumes() ([]Volume, error)

ListVolumes returns every volume the caller owns, GET /volumes.

func (*Client) Marketplace added in v0.4.0

func (c *Client) Marketplace() ([]MarketplaceOffer, error)

Marketplace fetches every live GPU offer across all providers — GET /marketplace. It sends no auth headers (see NewPublic) and requires no stored credential; a non-success envelope or a decode failure surfaces as a clear error rather than an empty list.

func (*Client) MoveSetup added in v0.6.0

func (c *Client) MoveSetup(setupID string, req MoveSetupRequest) (*Setup, error)

MoveSetup stops the pod (both captures confirmed, box released only after) then starts it again on the given offer. A failed Start after the Stop leaves the pod Stopped with its data intact (see the pod/environment/ volume plan's mechanism 7).

func (*Client) NewRunLogsStreamRequest added in v0.5.0

func (c *Client) NewRunLogsStreamRequest(ctx context.Context, jobID, runID string, offset int64, attempt int) (*http.Request, error)

NewRunLogsStreamRequest builds (but does not send) the GET request for the run-log SSE stream, sending the same x-api-key/x-team-id headers GetRunLogs sends. It only builds the request: reading an SSE body a frame at a time isn't something the JSON-envelope helpers in device.go (do/getJSON/...) know how to do, so the caller drives resp.Body itself.

func (*Client) OgreDownloadURL added in v0.4.0

func (c *Client) OgreDownloadURL(goos, goarch string) (*OgreDownloadURLResult, error)

OgreDownloadURL fetches a presigned download URL for the ogre binary this account's orchestrator expects, for a laptop that has no `ogre` on PATH.

goos/goarch are the CALLER's platform (runtime.GOOS / runtime.GOARCH). The server selects the matching build and answers 404 when it publishes none — which the caller must surface as its own refusal, because there is no runnable second choice.

func (*Client) PauseDeployment added in v0.4.0

func (c *Client) PauseDeployment(projectID string, deploymentID int) error

PauseDeployment saves the deployment's current state and releases its rented box (`aq pause`), via the existing project-scoped pause route. Pause always targets the ACTIVE deployment under a project, so it's keyed by projectID in the path plus deploymentID in the body, not by deployment id alone.

func (*Client) PollDevice

func (c *Client) PollDevice(deviceCode string) (*DevicePoll, error)

PollDevice polls a pairing by its device code.

func (*Client) QueueDeployment added in v0.7.0

func (c *Client) QueueDeployment(req QueueDeploymentRequest) (*QueuedDeployment, error)

QueueDeployment queues a deploy-when-available request.

func (*Client) RedeemExternalInstallConfig added in v0.4.0

func (c *Client) RedeemExternalInstallConfig(deploymentID int, installToken string) (*ExternalInstallConfig, error)

RedeemExternalInstallConfig exchanges the single-use install token for the box's credentials.

It authenticates with the install token as a Bearer header and NOTHING else — deliberately not the user's API key. The token is the whole authority for this one call, it is consumed on first success, and scoping the request to it means a leaked install token grants exactly one box's config rather than acting as the user.

func (*Client) RefreshImportCredentials added in v0.4.0

func (c *Client) RefreshImportCredentials(volumeID string) (*ImportCredentialsRefreshResult, error)

RefreshImportCredentials re-mints everything needed to resume volumeID's still-pending import: scoped write credentials, the storage location, and a fresh single-use completion token. This is `aq import --resume`'s ONLY source of that state — aq keeps no local copy of any of it.

func (*Client) ReleaseExternal added in v0.4.0

func (c *Client) ReleaseExternal(deploymentID int) (*ReleaseResult, error)

ReleaseExternal clears the state Aquanode pushed to the box, revokes its credentials and drops its row.

It never reaches a provider adapter and nothing is torn down. The machine keeps running exactly as it was — it is on a lease we do not hold and have no business ending. This is why the verb is `release` and not `terminate`.

It DOES contact the box, over ogre's own API, and reports three-state whether that succeeded. The daemon itself is stopped by `aq release` over ssh, which is the half no HTTP call can do: ogre exposes no shutdown endpoint on purpose.

func (*Client) RerunJob added in v0.6.0

func (c *Client) RerunJob(jobID string) (*BatchJob, error)

RerunJob makes a NEW job copying the source job's spec, plus its own new Run — POST /jobs/:id/rerun. "Running it again" always means a new job under this contract (jobs-are-jobs spec, "The contract" section); the source job named here is never mutated.

func (*Client) RestoreVolumePoint added in v0.6.0

func (c *Client) RestoreVolumePoint(volumeID string, pointID string) (*Volume, error)

RestoreVolumePoint sets a volume's head to an earlier point, POST /volumes/:id/restore. Refused (409) while a pod's live box has the volume (InUseBy, not merely "some pod references it": a stopped pod's volume can still be restored); the server never interprets "restore" as "newest by time".

func (*Client) RevokeShare added in v0.6.0

func (c *Client) RevokeShare(shareID string) error

RevokeShare revokes a share link, DELETE /shares/:shareId. Deletes the published repo once no link to it remains.

func (*Client) RotateSecret added in v0.5.0

func (c *Client) RotateSecret(teamID, secretID string, req RotateSecretRequest) (*Secret, error)

RotateSecret replaces a secret's stored value in place, keeping its id and name. Returns the updated summary with RotatedAt now set.

func (*Client) SetIdlePolicy added in v0.3.0

func (c *Client) SetIdlePolicy(deploymentID int, req IdlePolicyUpdate) (*IdlePolicy, error)

SetIdlePolicy updates a deployment's idle-auto-stop policy and returns the resulting merged policy plus live verdict. The orchestrator is the real authority on validity (e.g. warnAfterMinutes < actAfterMinutes), this only shapes the request.

func (*Client) SetSetupAutostop added in v0.6.0

func (c *Client) SetSetupAutostop(setupID string, enabled bool) (*Setup, error)

SetSetupAutostop replaces the old SetSetupAutopause: same mechanism (a per-pod stop-when-idle preference layered underneath `aq idle`'s per-deployment thresholds), renamed route and field per the pod/environment/volume plan's vocabulary sweep: "pause" is retired everywhere except environment versions (D15).

func (*Client) ShareEnvironmentByID added in v0.6.0

func (c *Client) ShareEnvironmentByID(environmentID string, versionID string) (*ShareResult, error)

ShareEnvironmentByID shares one version of a named (Kept or Shared) Environment directly, without going through the pod that made it: POST /environments/:id/share.

func (*Client) ShareSetupEnvironment added in v0.6.0

func (c *Client) ShareSetupEnvironment(setupID string, versionID string) (*ShareResult, error)

ShareSetupEnvironment shares one version of setupID's own environment (POST /setups/:id/environment/share). Absent versionID shares the latest; on a Running pod the server captures fresh first.

func (*Client) StartDevice

func (c *Client) StartDevice(clientName string, scopes []string) (*DeviceStart, error)

StartDevice begins a pairing.

func (*Client) StartImport added in v0.4.0

func (c *Client) StartImport(req ImportStartRequest) (*ImportStartResult, error)

StartImport creates the Volume that will receive the capture and mints scoped write credentials + a single-use completion token for it.

func (*Client) StartSetup added in v0.6.0

func (c *Client) StartSetup(setupID string, req StartSetupRequest) (*Setup, error)

StartSetup starts a Stopped pod on the given offer, returning the pod's updated state. Restoring the environment and the volume both gate readiness; the pod is Running only once both land (`ready_at`), never on the first reachable signal.

func (*Client) StopSetup added in v0.6.0

func (c *Client) StopSetup(setupID string) (*Setup, error)

StopSetup saves the pod (environment capture and volume sync, both confirmed) and releases its box. The pod's config and history are untouched: Stop never deletes anything; see StartSetup for bringing it back. A failed save keeps the box Running with the error reported on the pod, rather than releasing a box whose save just failed.

func (*Client) Up

func (c *Client) Up(req UpRequest) (*UpResult, error)

Up rents the cheapest matching GPU and brings up the requested template env.

type CloseResult

type CloseResult struct {
	Status string `json:"status"`
}

CloseResult mirrors POST /deployments/close.

type CreateBatchJobRequest added in v0.6.0

type CreateBatchJobRequest struct {
	Name               string        `json:"name"`
	VersionID          int           `json:"versionId,omitempty"`
	Image              *ImageSource  `json:"image,omitempty"`
	Entrypoint         *Entrypoint   `json:"entrypoint,omitempty"`
	Hardware           *Hardware     `json:"hardware,omitempty"`
	Placement          *JobPlacement `json:"placement,omitempty"`
	PinnedDeploymentID int           `json:"pinnedDeploymentId,omitempty"`
	// Secrets names `type: "env"` team secrets (POST /secrets/teams/:teamId,
	// see internal/api/secrets.go) this job's Run needs injected at dispatch.
	// omitempty: absent means "none", the same convention every optional
	// field on this request already follows; never sent as an empty array.
	// A name with no matching live secret on the team is refused (400).
	Secrets []string `json:"secrets,omitempty"`
	// Checkpoint names the paths ogre snapshots so a reclaimed or
	// price-hopped run can resume (job.service.ts's checkpointRequired,
	// hardware.ts:99). Pointer + omitempty: the key is ABSENT on the wire
	// when the caller passes neither --checkpoint-path nor
	// --checkpoint-exclude, and the server's own refusal fires with its own
	// message. Deliberately no local required-check here: the server owns
	// this rule and is moving it to a defaulted value, so a local mirror
	// would be a second copy of a rule already scheduled for deletion.
	Checkpoint *Checkpoint `json:"checkpoint,omitempty"`
}

CreateBatchJobRequest is the body of POST /jobs for a command (batch) entrypoint, per the jobs-are-jobs spec: creating a batch job creates its ONE Run in the same transaction, so there is no separate "now start it" call any more, and no per-job scaling knob to size — MaxInstances and MonthlySpendCapCents are GONE from this request entirely, not merely left unset, because the backend 400s a batch create that carries either key by name. Keeping them as accepted no-ops would be worse than removing them: a struct field is a promise the wire still means something for it, and it does not any more.

PinnedDeploymentID pins the job to a box the customer already owns (attached via `aq host add` + `aq attach`) instead of hardware Aquanode rents. It carries `omitempty` deliberately: the zero value must never reach the wire as a present-but-empty key, only as an absent one, the server reads an absent key as "today's managed behaviour" and a present zero/negative one as a malformed pin. jobRun resolves this from a `--on <alias>` flag locally and refuses before ever building this request unless the alias names a genuinely attached deployment.

VersionID and Image are the job's SOURCE XOR (job.service.ts:618-632): exactly one is sent, never both, never neither. VersionID carries `omitempty` for exactly that reason: an image-source create leaves it at its zero value in Go, and without `omitempty` that would post `"versionId":0` and trip the backend's both-or-neither check, which reads 0 as "sent" rather than "absent". A version row id is never legitimately 0, so `omitempty` is safe on the version-source path too.

type CreateEndpointRequest added in v0.4.0

type CreateEndpointRequest struct {
	Name         string          `json:"name"`
	Image        *ImageSource    `json:"image,omitempty"`
	Entrypoint   *HttpEntrypoint `json:"entrypoint,omitempty"`
	Hardware     *Hardware       `json:"hardware,omitempty"`
	MaxInstances int             `json:"maxInstances"`
	MinInstances int             `json:"minInstances,omitempty"`
}

CreateEndpointRequest is the body of POST /jobs for `aq endpoint create` — the service-shaped sibling of CreateBatchJobRequest. A deliberately separate type rather than reusing CreateBatchJobRequest's `Entrypoint *Entrypoint` field: that field's concrete type only ever carries a command entrypoint (argv, outputPath — neither means anything on an http entrypoint), so widening it to an interface would turn every existing command-create caller's `.Entrypoint.Argv` access into a type assertion for no benefit, since job and endpoint creates never share a request body in practice (mirrors the console's own separate /jobs/new and /endpoints/new pages and separate CreateJobParams-shaped bodies).

MinInstances carries `omitempty`: absent means "scales to zero between calls" (today's default), and `aq endpoint create` only ever sends it as the literal 1, iff --keep-warm — never a bare 0, which would be indistinguishable from "not set" on the wire and is why this is a plain int rather than a pointer.

type CreateSecretRequest added in v0.5.0

type CreateSecretRequest struct {
	Name  string `json:"name"`
	Type  string `json:"type"`
	Value any    `json:"value"`
}

CreateSecretRequest is the body of POST /secrets/teams/:teamId. Value holds either a plain string (Type == "env") or a RegistryCredential (Type == "registry"); json.Marshal serializes whichever one the caller put there, so the two shapes never both need a field on this struct.

type CreateSetupRefusedStart added in v0.6.0

type CreateSetupRefusedStart struct {
	Setup *Setup
	Err   error
}

CreateSetupRefusedStart is returned by CreateSetup when POST /setups creates the pod but the immediate Start it also attempts is refused (a bad SSH key, the chosen offer gone, insufficient credits, ...). The pod is real: created, Stopped, and already listed by `aq pods`. Setup carries its state as of the refusal. Never retry the create on this error (a second call mints a second pod); `aq start` the one that already exists once the refusal reason is fixed.

func (*CreateSetupRefusedStart) Error added in v0.6.0

func (e *CreateSetupRefusedStart) Error() string

func (*CreateSetupRefusedStart) Unwrap added in v0.6.0

func (e *CreateSetupRefusedStart) Unwrap() error

type CreateSetupRequest added in v0.6.0

type CreateSetupRequest struct {
	Name                 string         `json:"name,omitempty"`
	Offer                OfferSelection `json:"offer"`
	EnvironmentVersionID string         `json:"environmentVersionId"`
	VolumeID             *string        `json:"volumeId"`
}

CreateSetupRequest is the body of POST /setups (New pod): create the pod and start it on Offer in one call (confirmed against createSetupSchema, orchestrator/src/schemas/setups.schemas.ts, 2026-09-26).

Name is omitted (never sent empty) when the caller has none; the server derives one from the environment's template or the GPU.

VolumeID is ALWAYS present on the wire, never omitted: `"new"` mints a fresh volume named after the pod, an existing volume's id attaches it, and a nil pointer serializes to JSON `null` and runs the pod with no volume at all (D4). The schema is explicit about this ("never defaulted server-side"), so this field has no `omitempty` even though it's a pointer.

type DeleteVolumeResult added in v0.6.0

type DeleteVolumeResult struct {
	ID           string        `json:"id"`
	DetachedPods []DetachedPod `json:"detachedPods"`
}

DeleteVolumeResult is the data of a successful DELETE /volumes/:id.

type DeployRequest

type DeployRequest struct {
	// SnapshotSource is the synthetic `ext-<backupId>` id a standalone-CLI
	// snapshot gets (#177). The pod/environment/volume plan retires the other
	// shape this once accepted, a live numeric deployment id resuming a
	// paused deployment, along with the `aq pause` that produced one: there
	// is nothing left to resume, so this bridge only ever restores an
	// external capture now.
	SnapshotSource string  `json:"snapshotSource"`
	SSHKeyID       string  `json:"sshKeyId"`
	Template       string  `json:"template,omitempty"`
	GPUModel       string  `json:"gpuModel,omitempty"`
	MaxPrice       float64 `json:"maxPrice,omitempty"`
	Provider       string  `json:"provider,omitempty"`
	// GPUCount asks for a multi-GPU box; 0 means "no opinion". See UpRequest.
	GPUCount int `json:"gpuCount,omitempty"`
	// Name sets the deployment's display name; empty → orchestrator-generated.
	// A stable `ticket-<N>-<label>` makes the throwaway box reapable (#310).
	Name string `json:"name,omitempty"`
}

DeployRequest is the body of POST /deployments/deploy-snapshot, the OSS to compute bridge. It rents the cheapest matching GPU and restores a snapshot onto it, optionally relaunching an app template on the restored data (#180). This is unrelated to a managed pod's own Environment/Volume: the source here is always a standalone-CLI capture (`ogre snapshot --to aquanode`) that never ran on Aquanode before, never a pod's own history.

type Deployment

type Deployment struct {
	ID     int    `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
	// ProjectID is only populated by GetDeployment (GET /deployments/:id, a
	// raw untransformed row) — the pause route is project-scoped rather than
	// deployment-scoped, so it's the one field that route needs beyond the
	// deployment id itself.
	ProjectID string `json:"project_id"`
	// SetupID is the pod this deployment rents compute for
	// (deployments.setup_id), present snake_case on both the raw GET
	// /deployments/:id row and the nested `deployment` object GET
	// /deployments/:id/status returns (confirmed against a live capture,
	// 2026-09-27), unlike most of /status's other fields, which ARE camelCased. `aq status` uses this to recover the pod's real save
	// state from GET /setups/:id, since the deployment itself carries no
	// volume/save information. Empty for a deployment with no setup at all.
	SetupID            string              `json:"setup_id"`
	AppURL             string              `json:"app_url"`
	ServiceCredentials *ServiceCredentials `json:"service_credentials"`
	// ServiceURLs stays raw so a malformed or unexpectedly-shaped value (the
	// column is a free-form Json defaulting to "[]") can never fail the whole
	// row's decode. SSHServiceURL parses it on demand.
	ServiceURLs json.RawMessage `json:"service_urls"`
	// RestoreStatus/RestoreError carry the server-side snapshot restore outcome
	// for `aq deploy` (#235): SUCCESS / PARTIAL / FAILED, plus a detail string.
	// Empty for a plain `aq up` (no restore) or a backend that predates the field.
	RestoreStatus string `json:"restore_status"`
	RestoreError  string `json:"restore_error"`
	// RestoreCompatibility/RestoreWarnings carry the server-side CUDA/vendor
	// skew verdict computed on every restore (host_compat.go -> the deployment
	// row via vms/index.ts), previously computed and persisted but never
	// decoded here — a CLI user restoring a CUDA-12 environment onto a
	// CUDA-11 box saw nothing unless the restore hard-failed. Both fields stay
	// raw: their exact shape is not confirmed on the wire, and a decode
	// surprise here must never fail the whole Deployment/list decode — the
	// same defensive reasoning as ServiceURLs above. CompatibilityLabel/
	// RestoreWarningMessages parse them leniently on demand.
	RestoreCompatibility json.RawMessage `json:"restore_compatibility"`
	RestoreWarnings      json.RawMessage `json:"restore_warnings"`

	// SSHLoginUser is the account name this box's SSH key actually landed on,
	// recorded by the orchestrator at provision time from the provider's own
	// capability declaration (deployments.ssh_login_user).
	//
	// THREE-STATE, and the empty string is a real answer: "no login user was
	// recorded for this box". That covers every deployment created before the
	// column shipped, every provider that has not established its own login
	// user, and a backend too old to send the field at all. It must NEVER be
	// read as "root" — assuming root platform-wide is the bug this field
	// exists to fix (managed hyperstack boxes refuse it outright, see
	// sshconfig.go's sshUser).
	SSHLoginUser string `json:"ssh_login_user"`

	// Inventory fields for `aq ls`. All snake_case only: GET /deployments
	// returns raw untransformed rows, unlike /:id and /:id/status.
	Provider  string `json:"provider"`
	GPU       string `json:"gpu"`
	GPUCount  int    `json:"gpu_count"`
	CreatedAt string `json:"created_at"`
	// PricePerSecond is the rate in Currency's units — NOT necessarily USD.
	// `deployments.currency` carries the provider's own denomination (USD, AKT,
	// USPON, ...) and conversion happens only at bucket-mint time, so
	// price_per_second * 3600 is AKT/hour on an Akash row. Anything rendering
	// this MUST print the currency code alongside it and must never prefix a
	// bare "$". The USD rate lives on billing_buckets_v2, which the CLI has no
	// route to.
	PricePerSecond FlexFloat `json:"price_per_second"`
	Currency       string    `json:"currency"`
}

Deployment is the subset of the deployment row `aq up` polling cares about.

Every tag MUST stay in snake_case form. GET /deployments/:id/status runs rows through transformDeploymentData and so carries snake_case *plus* camelCase duplicates, but GET /deployments (the list) does not transform — its rows are snake_case only. A camelCase tag would decode the status endpoint and silently yield zero values for every list row.

func (Deployment) CompatibilityLabel added in v0.4.0

func (d Deployment) CompatibilityLabel() string

CompatibilityLabel parses RestoreCompatibility leniently: it may arrive as a bare string (e.g. "compatible"/"minor"/"major"/"vendor_mismatch") or be absent on a backend that predates the field. Any other shape yields "" — never an error, since this only ever feeds an informational print.

func (Deployment) RestoreWarningMessages added in v0.4.0

func (d Deployment) RestoreWarningMessages() []string

RestoreWarningMessages parses RestoreWarnings leniently: the field may be a JSON array of strings, a single string, or absent. Never asserted to one shape — see the field's own doc comment for why.

func (Deployment) SSHServiceURL added in v0.2.0

func (d Deployment) SSHServiceURL() (string, bool)

SSHServiceURL returns the deployment's port-22 service URL.

This is authoritative for SSH in a way app_url is not: simplepod silently publishes the ogre agent's HTTP port as app_url when the box maps no SSH port, so dialing app_url there reaches an HTTP server rather than sshd. The service_urls entry is keyed by the *internal* port, so port 22 is unambiguous and simply absent on a box with no SSH.

type DeploymentStatusResult

type DeploymentStatusResult struct {
	DeploymentID int        `json:"deploymentId"`
	Status       string     `json:"status"`
	Deployment   Deployment `json:"deployment"`
}

DeploymentStatusResult mirrors GET /deployments/:id/status.

type DetachedPod added in v0.6.0

type DetachedPod struct {
	PodID   string `json:"podId"`
	PodName string `json:"podName"`
}

DetachedPod is one pod DeleteVolume detached because it referenced the deleted volume while STOPPED. A running pod's volume is never detached this way; DELETE refuses outright instead (VolumeInUseError).

type DevicePoll

type DevicePoll struct {
	Status string `json:"status"` // pending | approved | denied | expired | consumed | slow_down
	// Interval, when > 0, is the cadence (seconds) the server wants the CLI to
	// poll at from now on (RFC 8628 §3.5). The CLI only ever slows down to it.
	Interval int      `json:"interval"`
	Token    string   `json:"token"`
	Scopes   []string `json:"scopes"`
	TeamID   string   `json:"teamId"`
	KeyName  string   `json:"keyName"`
}

DevicePoll is the result of POST /api-keys/device/token.

type DeviceStart

type DeviceStart struct {
	DeviceCode              string   `json:"deviceCode"`
	UserCode                string   `json:"userCode"`
	Scopes                  []string `json:"scopes"`
	VerificationURI         string   `json:"verificationUri"`
	VerificationURIComplete string   `json:"verificationUriComplete"`
	Interval                int      `json:"interval"`
	ExpiresIn               int      `json:"expiresIn"`
}

DeviceStart is the result of POST /api-keys/device/start.

type DuplicateVolumeRequest added in v0.6.0

type DuplicateVolumeRequest struct {
	Name string `json:"name"`
}

DuplicateVolumeRequest is the body of POST /volumes/:id/duplicate.

type Entrypoint added in v0.5.0

type Entrypoint struct {
	Kind       string   `json:"kind"`
	Argv       []string `json:"argv"`
	OutputPath string   `json:"outputPath"`
}

Entrypoint is a `kind: "command"` entrypoint, the only kind this CLI can express. `http`/`comfyui` entrypoints carry a port, a body template or a whole workflow_api.json graph with no CLI-typeable shape, so `aq job run` only ever emits `command`. Argv comes verbatim from everything after a bare `--` on the command line (see splitRemoteCommand), never shell-parsed, matching the "no quoting gymnastics" the ticket asked for. OutputPath is required and must be absolute: entrypoint.go's parseEntrypoint refuses a relative or empty one outright.

type EnvironmentPreview added in v0.6.0

type EnvironmentPreview struct {
	IncludedRoots []string               `json:"includedRoots"`
	Excluded      []string               `json:"excluded"`
	StartupScript string                 `json:"startupScript"`
	SecretHits    []EnvironmentSecretHit `json:"secretHits"`
}

EnvironmentPreview is the data returned by GET /setups/:id/environment/preview: everything a share/keep confirmation needs to show BEFORE anything leaves the box (no box required, since it's rendered from the version's already-stored includedRoots/excluded).

type EnvironmentSecretHit added in v0.6.0

type EnvironmentSecretHit struct {
	Line int    `json:"line"`
	Kind string `json:"kind"`
}

EnvironmentSecretHit is one line of the startup script that looked like a credential (hf_, sk-, AKIA, a PEM header, ...). Any hit refuses the share server-side; this is surfaced so the CLI can show exactly where before the server says no.

type EnvironmentSummary added in v0.6.0

type EnvironmentSummary struct {
	ID            string                 `json:"id"`
	Name          string                 `json:"name"`
	Kind          string                 `json:"kind"`
	LatestVersion *EnvironmentVersionRef `json:"latestVersion"`
}

EnvironmentSummary is one row of GET /environments' builtin/yours/shared arrays: enough to pick from in "aq env ls"/"aq pods create --env" and resolve a name to an id. Kind is "builtin"|"kept"|"shared" on THIS listing specifically (never "working": an unnamed working environment isn't a listable object until Kept or Shared, unlike Setup.environment.kind which does include "working"). LatestVersion is nil only for a genuinely version-less environment; nothing the picker offers should be version-less in practice, but a caller must still check rather than assume.

type EnvironmentVersion added in v0.6.0

type EnvironmentVersion struct {
	ID            string   `json:"id"`
	Version       int      `json:"version"`
	CreatedAt     string   `json:"createdAt"`
	IncludedRoots []string `json:"includedRoots"`
	Excluded      []string `json:"excluded"`
}

EnvironmentVersion mirrors one row returned by GET /setups/:id/environment/versions and GET /environments/:id/versions.

type EnvironmentVersionRef added in v0.6.0

type EnvironmentVersionRef struct {
	ID        string `json:"id"`
	Version   int    `json:"version"`
	CreatedAt string `json:"createdAt"`
}

EnvironmentVersionRef is the {id,version,createdAt} triple GET /environments nests as an item's latestVersion (confirmed against EnvironmentListItem, orchestrator/src/services/environments/ environment.service.ts, w3-backend 2026-09-26). ID here is the version's own row id, an EnvironmentVersion.ID, not the environment's id: this is what a caller creating a pod from this environment must send as POST /setups' environmentVersionId.

type EnvironmentsResult added in v0.6.0

type EnvironmentsResult struct {
	Builtin []EnvironmentSummary `json:"builtin"`
	Yours   []EnvironmentSummary `json:"yours"`
	Shared  []EnvironmentSummary `json:"shared"`
}

EnvironmentsResult is the data returned by GET /environments: the three groups the New pod picker shows (Built-in, Yours, Shared with me).

type ExternalInstallConfig added in v0.4.0

type ExternalInstallConfig struct {
	OgreJWTSecret   string `json:"ogreJwtSecret"`
	OgreProxyPass   string `json:"ogreProxyPassword"`
	TLSCertPEM      string `json:"tlsCertPem"`
	TLSKeyPEM       string `json:"tlsKeyPem"`
	OgrePort        int    `json:"ogrePort"`
	OrchestratorURL string `json:"orchestratorUrl"`
	// The deployment the token named, echoed back by the orchestrator as a
	// NUMBER — the same JSON type `AdoptExternalResult.DeploymentID` above
	// carries, because both fields are the same `Deployment.id`. It was typed
	// `string` here and nothing read it, so `json.Unmarshal` failed on the very
	// first live redeem and attach could never complete. Typed and CHECKED now:
	// an unread echo is worth nothing, and a wrong one means we are about to
	// write another box's credentials onto this machine.
	DeploymentID int `json:"deploymentId"`
}

ExternalInstallConfig is the response to POST /deployments/external/:id/install-config — the only place these credentials ever leave the orchestrator.

The token that redeems it is single-use with a TTL, so aq must treat this as a one-shot: it is never re-fetchable, never cached to disk, and a failure after redeeming means starting the attach over rather than retrying the redemption.

type ExternalUnreachableError added in v0.4.0

type ExternalUnreachableError struct {
	Reason string
}

ExternalUnreachableError is the orchestrator refusing to activate a box it could not reach. Its Reason is the server's own words, never a rewrite.

func (*ExternalUnreachableError) Error added in v0.4.0

func (e *ExternalUnreachableError) Error() string

type FlexFloat added in v0.4.0

type FlexFloat float64

FlexFloat decodes a JSON number that may arrive as a bare number or as a quoted string. Prisma serializes Decimal columns as strings, and a strict float64 field would fail the whole row's decode on one — the same defensive reasoning behind ServiceURLs staying raw.

func (*FlexFloat) UnmarshalJSON added in v0.4.0

func (f *FlexFloat) UnmarshalJSON(b []byte) error

type Hardware added in v0.5.0

type Hardware struct {
	GPUModels []string `json:"gpuModels"`
	GPUCount  int      `json:"gpuCount"`
	DiskGB    int      `json:"diskGb"`
}

Hardware constraints for an image-source Job. See the orchestrator's hardware.ts HardwareSchema. GPUCount is one of the closed set 1, 2, 4, 8 (job.go's validateJobGPUCount), matched EXACTLY against a node's own GPU count by job-placement.ts, never `aq up`'s free-form --gpus request. GPUModels are exact marketplace names (see `aq gpus`), never `aq up`'s substring --gpu match.

type HardwareAvailability added in v0.5.0

type HardwareAvailability struct {
	Models []HardwareAvailabilityModel `json:"models"`
}

HardwareAvailability is what GET /jobs/hardware-availability returns. `--any-gpu` only needs the model universe, so this omits Offers/TotalOffers (they scope to a gpuModels filter this call never sends).

type HardwareAvailabilityModel added in v0.5.0

type HardwareAvailabilityModel struct {
	// GPUModel is spelled exactly as the marketplace feed reports it; this
	// exact string is what goes back out as a `hardware.gpuModels` entry.
	GPUModel string `json:"gpuModel"`
}

HardwareAvailabilityModel is one element of GET /jobs/hardware-availability's `models` array: the full GPU model UNIVERSE the marketplace can currently offer, independent of any gpuModels filter on the request.

type HttpEntrypoint added in v0.5.0

type HttpEntrypoint struct {
	Kind       string `json:"kind"`
	Port       int    `json:"port"`
	Path       string `json:"path"`
	Method     string `json:"method"`
	ResultMode string `json:"resultMode"`
}

HttpEntrypoint is a `kind: "http"` entrypoint — a generic HTTP app, called by POSTing a rendered body to a port inside the box (orchestrator entrypoint.ts's HttpEntrypoint). `aq endpoint create` only ever emits Method "POST" and ResultMode "inline": the other combinations (GET/PUT, resultMode "poll" + resultFrom, bodyTemplate) exist on the wire but have no CLI-typeable shape yet, mirroring why Entrypoint above only ever emits `kind: "command"` — there is no console-parity flag set for them either.

type IdlePolicy added in v0.3.0

type IdlePolicy struct {
	WarnAfterMinutes        int  `json:"warnAfterMinutes"`
	ActAfterMinutes         int  `json:"actAfterMinutes"`
	GPUIdleThresholdPercent int  `json:"gpuIdleThresholdPercent"`
	AutoStopEnabled         bool `json:"autoStopEnabled"`
	// State is one of UNKNOWN | ACTIVE | IDLE_WARN | IDLE_ACT. UNKNOWN means the
	// box has reported no usable usage data yet, render it as unknown, never as
	// active or idle: presenting a no-data verdict as either extreme is a lie the
	// user could act on.
	State string `json:"state"`
	// IdleMinutes is only meaningful once State is IDLE_WARN or IDLE_ACT.
	IdleMinutes int `json:"idleMinutes"`
}

IdlePolicy mirrors GET /deployments/:deploymentId/idle-policy: the deployment's merged idle-auto-stop policy plus a live verdict computed from its current usage data.

type IdlePolicyUpdate added in v0.3.0

type IdlePolicyUpdate struct {
	WarnAfterMinutes        *int  `json:"warnAfterMinutes,omitempty"`
	ActAfterMinutes         *int  `json:"actAfterMinutes,omitempty"`
	GPUIdleThresholdPercent *int  `json:"gpuIdleThresholdPercent,omitempty"`
	AutoStopEnabled         *bool `json:"autoStopEnabled,omitempty"`
}

IdlePolicyUpdate is the body of PUT /deployments/:deploymentId/idle-policy. Every field is optional and a nil field is omitted from the request entirely (via `omitempty` on the pointer), so a flag the user never passed can never round-trip as a zero-value that silently overwrites one of their other settings.

type ImageSource added in v0.5.0

type ImageSource struct {
	Ref            string `json:"ref"`
	RegistrySecret string `json:"registrySecret,omitempty"`
}

ImageSource is the `{ ref, registrySecret? }` shape job.service.ts's normalizeImageSource accepts. RegistrySecret is a NAME resolved server-side against the team's `type: registry` secrets (missingSecretNames), never a token typed on the command line. `aq secret set --type registry` already mints and stores those. `omitempty` because a public image sends no key at all, matching the console's own spread.

type ImportApp added in v0.4.0

type ImportApp struct {
	Name       string `json:"name"`
	Command    string `json:"command"`
	Dir        string `json:"dir"`
	Port       int    `json:"port"`
	HealthPath string `json:"health_path"`
}

ImportApp is the workload ogre's DetectApp found on the box. It is nil (Observation.App) when nothing was detected — never fabricated.

type ImportCapture added in v0.4.0

type ImportCapture struct {
	MountPath string   `json:"mount_path"`
	Paths     []string `json:"paths"`
	Excludes  []string `json:"excludes"`
}

ImportCapture is what ogre captured (or was told to capture) and from where.

type ImportCaptureEntry added in v0.4.0

type ImportCaptureEntry struct {
	Path string `json:"path"`
	// Bytes is a FLOOR, not an exact size, when BytesTruncated is true — the
	// walk hit a budget partway through this path. Render as ">= N" then.
	Bytes          int64  `json:"bytes"`
	BytesTruncated bool   `json:"bytes_truncated"`
	Source         string `json:"source"` // "detected" | "explicit"
}

ImportCaptureEntry is one path counted into the "capturing" survey block.

type ImportCompleteRequest added in v0.4.0

type ImportCompleteRequest struct {
	VolumeID       string            `json:"volume_id"`
	ImportToken    string            `json:"import_token"`
	OgreSnapshotID string            `json:"ogre_snapshot_id"`
	Observation    ImportObservation `json:"observation"`
}

ImportCompleteRequest is the body of POST /volumes/import/complete (completeImportSchema, orchestrator/src/schemas/volumes.schemas.ts:103-108, confirmed against source, aquanode-backend#803): exactly volume_id, import_token, ogre_snapshot_id, and observation. `ogre capture` also reports a path and a size on its own stdout (ogreCaptureOutput), but the orchestrator's schema never defined those keys, so they are not sent here at all, never a field that gets silently stripped server-side (rule 4, delete never alias).

type ImportCompleteResult added in v0.4.0

type ImportCompleteResult struct {
	VolumeID string   `json:"volume_id"`
	Warnings []string `json:"warnings"`
}

ImportCompleteResult is the data returned by POST /volumes/import/complete. There is no recipe/version here any more (that was the old Setup-shaped flow's synthesized launch config). A Volume carries /workspace data only, nothing installable, so nothing is synthesized on completion.

type ImportCredentials added in v0.4.0

type ImportCredentials struct {
	Endpoint        string `json:"endpoint"`
	Bucket          string `json:"bucket"`
	AccessKeyID     string `json:"access_key_id"`
	SecretAccessKey string `json:"secret_access_key"`
	Region          string `json:"region"`
}

ImportCredentials are scoped, time-limited write credentials for the new volume's storage prefix (minted by /volumes/import/start and re-mintable via /volumes/import/credentials for an upload that outlives one minting).

type ImportCredentialsRefreshRequest added in v0.4.0

type ImportCredentialsRefreshRequest struct {
	VolumeID string `json:"volume_id"`
}

ImportCredentialsRefreshRequest is the body of POST /volumes/import/credentials: re-mints EVERYTHING `aq import --resume` needs for a still-pending import, keyed by volume id alone.

type ImportCredentialsRefreshResult added in v0.4.0

type ImportCredentialsRefreshResult struct {
	VolumeID       string            `json:"volume_id"`
	StoragePrefix  string            `json:"storage_prefix"`
	ResticBackupID string            `json:"restic_backup_id"`
	ResticPassword string            `json:"restic_password"`
	ImportToken    string            `json:"import_token"`
	ExpiresAt      string            `json:"expires_at"`
	Credentials    ImportCredentials `json:"credentials"`
}

ImportCredentialsRefreshResult is the data returned by POST /volumes/import/credentials. This is the WHOLE point of the route: it returns everything --resume needs: StoragePrefix/ResticBackupID/ ResticPassword alongside a freshly-minted ImportToken and scoped write Credentials, so aq never has to persist a single one of these locally. ImportToken here SUPERSEDES any token from a prior /start or /credentials call for this volume; using a remembered one for /complete will be refused.

type ImportGPU added in v0.4.0

type ImportGPU struct {
	Vendor      *string `json:"vendor,omitempty"`
	Name        *string `json:"name,omitempty"`
	Count       *int    `json:"count,omitempty"`
	DriverCUDA  *string `json:"driver_cuda,omitempty"`
	ToolkitCUDA *string `json:"toolkit_cuda,omitempty"`
	ROCmVersion *string `json:"rocm_version,omitempty"`
	ComputeCap  *string `json:"compute_cap,omitempty"`
	Skew        *string `json:"skew,omitempty"`
}

ImportGPU is the observed GPU, if any. Skew is "unknown" (never "none") when no GPU is visible, absence is not a match, per the contract. Every field is independently nullable on the wire, same reasoning as ImportHost.

type ImportHost added in v0.4.0

type ImportHost struct {
	Hostname  *string `json:"hostname,omitempty"`
	OS        *string `json:"os,omitempty"`
	Kernel    *string `json:"kernel,omitempty"`
	CPUCores  *int    `json:"cpu_cores,omitempty"`
	MemoryGB  *int    `json:"memory_gb,omitempty"`
	StorageGB *int    `json:"storage_gb,omitempty"`
}

ImportHost is the observed host's identity, as `ogre capture` reports it. Every field is independently nullable on the wire (importObservationSchema, orchestrator/src/schemas/volumes.schemas.ts:28-51, confirmed against source, aquanode-backend#803): ogre can report some facts about a host without all of them (hostname known, kernel version not, say), so each field is a pointer with omitempty, never a bare string/int that would decode a field ogre never sent as "" or 0 and then re-send that as if it were a real answer.

type ImportManifest added in v0.4.0

type ImportManifest struct {
	Collected      bool                 `json:"collected"`
	PythonEnvs     []ImportPythonEnv    `json:"python_envs"`
	SystemPackages ImportSystemPackages `json:"system_packages"`
}

ImportManifest is the "recorded but not restorable" package manifest — a reference for the user to rebuild by hand, never a restore instruction.

type ImportObservation added in v0.4.0

type ImportObservation struct {
	Schema   int            `json:"schema"`
	Host     ImportHost     `json:"host"`
	GPU      ImportGPU      `json:"gpu"`
	App      *ImportApp     `json:"app"`
	Capture  ImportCapture  `json:"capture"`
	Survey   ImportSurvey   `json:"survey"`
	Manifest ImportManifest `json:"manifest"`
}

ImportObservation is what `ogre capture` observed on the foreign box (CONTRACT.md section A). aq decodes it to render the survey, then re-encodes the SAME decoded value when forwarding it to the orchestrator on completion, it is not raw passthrough bytes, so every field here has to carry real nullability (see ImportHost/ImportGPU) or a fact ogre never reported gets fabricated as a zero value on the way back out.

type ImportPythonEnv added in v0.4.0

type ImportPythonEnv struct {
	Env       string   `json:"env"`
	Kind      string   `json:"kind"` // "venv" | "conda"
	Packages  []string `json:"packages"`
	Truncated bool     `json:"truncated"`
}

ImportPythonEnv is one recorded Python environment's package manifest. This is recorded for the user's reference only — the manifest is never replayed.

type ImportSkippedEntry added in v0.4.0

type ImportSkippedEntry struct {
	Path           string `json:"path"`
	Bytes          int64  `json:"bytes"`
	BytesTruncated bool   `json:"bytes_truncated"`
	Reason         string `json:"reason"`
}

ImportSkippedEntry is one path counted into the "not_capturing" survey block — the actionable one: a user who sees a large skipped directory here either --includes it or accepts the loss knowingly.

type ImportStartRequest added in v0.4.0

type ImportStartRequest struct {
	Name      string `json:"name,omitempty"`
	MountPath string `json:"mount_path,omitempty"`
}

ImportStartRequest is the body of POST /volumes/import/start. Both fields are optional: the orchestrator names the volume and picks a mount path when they're empty.

type ImportStartResult added in v0.4.0

type ImportStartResult struct {
	VolumeID       string            `json:"volume_id"`
	StoragePrefix  string            `json:"storage_prefix"`
	ResticPassword string            `json:"restic_password"`
	ResticBackupID string            `json:"restic_backup_id"`
	ImportToken    string            `json:"import_token"`
	ExpiresAt      string            `json:"expires_at"`
	Credentials    ImportCredentials `json:"credentials"`
}

ImportStartResult is the data returned by POST /volumes/import/start: a real Volume already exists at this point, with a real (billed, visible, deletable) storage prefix, before a single byte has been captured. This is the pod/environment/volume plan's D12: the route used to be POST /snapshots/external and mint a whole Setup; a box captured from outside Aquanode is now just its /workspace data, adopted as a Volume (the setup-adopt.service.ts pattern) -- the Environment/recipe half of the old flow is gone, since a bare import carries no installed-package manifest to synthesize one from.

ResticBackupID is the SERVER's own convention for the trailing path segment of the restic repo (`setup.service.ts`'s resticRepositoryUrl: currently the literal "repo" for every portable setup, since StoragePrefix already makes the repo unique — see CONTRACT.md section G). aq passes it straight through to `ogre capture` uninterpreted; it must NEVER be guessed or defaulted client-side; a value like the volume's own uuid writes to a path nothing ever reads, and the uploaded bytes then sit there billing forever with no error anywhere.

type ImportSurvey added in v0.4.0

type ImportSurvey struct {
	Capturing    []ImportCaptureEntry    `json:"capturing"`
	NotCapturing []ImportSkippedEntry    `json:"not_capturing"`
	Unreadable   []ImportUnreadableEntry `json:"unreadable"`
	// MinReportBytes is the size floor NotCapturing was built with (contract
	// H2, default 1 GiB). Every renderer of this survey MUST state it:
	// without it the "not capturing" block reads as EXHAUSTIVE, when a
	// directory under the floor appears in neither list and nothing says a
	// floor was ever applied — the same class of failure as a silent skip.
	MinReportBytes int64 `json:"min_report_bytes"`
	WalkTruncated  bool  `json:"walk_truncated"`
	DeadlineHit    bool  `json:"deadline_hit"`
}

ImportSurvey is the full pre-capture report: what will be captured, what won't, what couldn't even be read, and whether the walk itself ran out of budget before finishing (in which case every size above is a floor).

Capturing and Unreadable are mutually exclusive at the top level (contract H1, ogre 94d34a5): a capture path whose own root cannot be opened is dropped from Capturing and reported in Unreadable only — never both, which would otherwise claim capture of a tree that couldn't be read. A path whose root IS readable stays in Capturing even when some descendants fail; those descendants are listed individually in Unreadable (partial capture is real capture, not discarded).

func (ImportSurvey) Incomplete added in v0.4.0

func (s ImportSurvey) Incomplete() bool

Incomplete reports whether the survey hit a budget before it could finish walking the box — if so, every size in the survey is a floor, and paths past the budget may be missing from either block entirely.

type ImportSystemPackages added in v0.4.0

type ImportSystemPackages struct {
	Manager   string   `json:"manager"` // "dpkg" | "rpm" | "none"
	Packages  []string `json:"packages"`
	Truncated bool     `json:"truncated"`
}

ImportSystemPackages is the observed OS package manager's install list.

type ImportUnreadableEntry added in v0.4.0

type ImportUnreadableEntry struct {
	Path   string `json:"path"`
	Reason string `json:"reason"`
}

ImportUnreadableEntry is one path ogre could not even stat/list — permission denied, most often. This is a THIRD state, distinct from "not_capturing": a path we couldn't read must never be silently dropped or reported as "skipped by choice."

type Job added in v0.5.0

type Job struct {
	ID                   string `json:"id"`
	Name                 string `json:"name"`
	VersionID            int    `json:"versionId"`
	Status               string `json:"status"`
	SpentCents           int64  `json:"spentCents"`
	MonthlySpendCapCents *int64 `json:"monthlySpendCapCents"`
	RunningInstances     int    `json:"runningInstances"`
	MaxInstances         int    `json:"maxInstances"`
	RunsThisPeriod       int    `json:"runsThisPeriod"`
	// Shape is DERIVED server-side from entrypoint.kind, never stored
	// (job.service.ts jobShapeFor): "batch" for a command entrypoint,
	// "service" for http/comfyui. It is what `?shape=` filters on and what
	// `aq endpoint` vs `aq job` mean by the split.
	Shape string `json:"shape"`
	// RunURL is this orchestrator's own `/api/v1/run/:jobId` route (still
	// gated by an `x-job-token`), never the box's own address. Null when
	// the server has no public base configured (job.service.ts runUrlFor) —
	// three-state, not "" — so `aq endpoint url` can tell "not configured"
	// from a row that genuinely has no id.
	RunURL *string `json:"runUrl"`
}

Job mirrors one row of GET /jobs.

type JobPlacement added in v0.5.0

type JobPlacement struct {
	GPUOrder string `json:"gpuOrder,omitempty"`
}

JobPlacement is a Job's placement preferences. See the orchestrator's hardware.ts PlacementSchema. Named JobPlacement rather than the bare Placement, so a future concept sharing that name (control.go used to have one, for a deploy-snapshot resume the pod/environment/volume plan retired) never collides with it by accident. GPUOrder carries `omitempty`: an absent key already means "cheapest" (today's behaviour), so that value is never written explicitly, exactly as the console omits it.

type KeepEnvironmentRequest added in v0.6.0

type KeepEnvironmentRequest struct {
	Name string `json:"name"`
}

KeepEnvironmentRequest is the body of POST /setups/:id/environment/keep.

type KeepEnvironmentResult added in v0.6.0

type KeepEnvironmentResult struct {
	EnvironmentID string `json:"environmentId"`
}

KeepEnvironmentResult is the data returned by POST /setups/:id/environment/keep.

type MarketplaceOffer added in v0.4.0

type MarketplaceOffer struct {
	Address          string     `json:"address"`
	Type             string     `json:"type"`
	LocationID       string     `json:"location_id"`
	CloudType        string     `json:"cloud_type"`
	Available        int        `json:"available"`
	GPUCount         int        `json:"gpuCount"`
	GPUShortName     string     `json:"gpuShortName"`
	AvailableCPU     int        `json:"availableCpu"`
	AvailableMemory  MemorySize `json:"availableMemory"`
	AvailableStorage MemorySize `json:"availableStorage"`
	Interface        string     `json:"interface"`
	GPUVendor        string     `json:"gpuVendor"`
	GPUVendorFamily  string     `json:"gpuVendorFamily"`
	GPUArchitecture  string     `json:"gpuArchitecture"`
	GPUMemory        string     `json:"gpuMemory"`
	IsPersistent     bool       `json:"isPersistent"`
	// Price is the offer's total $/hr — see the type doc comment.
	Price      float64 `json:"price"`
	Region     string  `json:"region"`
	Provider   string  `json:"provider"`
	ProviderID string  `json:"providerId"`
	// StoragePerGpu/CpuCoresPerGpu/RamPerGpu arrive as a JSON number that is
	// sometimes fractional (a whole-node provider's resources divided evenly
	// across its GPUs, e.g. 2403.5) — float64, never int, or decoding fails
	// outright on those rows.
	StoragePerGpu  float64 `json:"storagePerGpu"`
	CpuCoresPerGpu float64 `json:"cpuCoresPerGpu"`
	RamPerGpu      float64 `json:"ramPerGpu"`
	ProviderName   string  `json:"providerName"`
	// AvailabilityLevel/AvailabilityReason say how much the provider reports
	// about stock (a provider that publishes none carries level UNKNOWN and
	// reason provider_never_reports). Both are optional on the wire, so they
	// are pointers: an absent key stays absent in `aq gpus --json` instead of
	// turning into "".
	AvailabilityLevel  *string `json:"availabilityLevel,omitempty"`
	AvailabilityReason *string `json:"availabilityReason,omitempty"`
}

MarketplaceOffer mirrors one element of GET /marketplace's data array: one rentable GPU configuration at one provider/region.

Price is the TOTAL $/hr for the whole offer (all GpuCount GPUs) for every provider EXCEPT Akash, whose feed reports Price as an already-flat per-GPU rate (confirmed empirically: akash H100 prices at 2.709 regardless of gpuCount 1-4; RTX5090 at 0.609 for both x1 and x8) — see akashProvider in gpus.go, the one place that exception is allowed to live. For every other provider it scales linearly with GpuCount (runpod 1x B200 = 6.79, datacrunch 4x B200 = 24.44, i.e. ~6.11/GPU — a total). Any renderer must normalize through gpus.go's perGPUHourlyRate/totalHourlyRate rather than reading Price directly, or Akash and everyone else end up compared in different units.

type MemorySize added in v0.4.0

type MemorySize struct {
	Value float64 `json:"value"`
	Unit  string  `json:"unit"`
}

MemorySize is a `{value,unit}` pair as the marketplace serializes availableMemory/availableStorage — never a bare number, since the unit varies (GB seen in practice, but nothing on the wire promises it won't change).

type MoveSetupRequest added in v0.6.0

type MoveSetupRequest struct {
	Offer OfferSelection `json:"offer"`
}

MoveSetupRequest is the body of POST /setups/:id/move.

type OfferSelection added in v0.6.0

type OfferSelection struct {
	Resource ResourceSpec `json:"resource"`
	Provider ProviderSpec `json:"provider"`
	SSHKeyID string       `json:"sshKeyId"`
}

OfferSelection is the `offer` object POST /setups, /setups/:id/start, and /setups/:id/move all take (the pod/environment/volume plan's REST amendment, 2026-09-26): a single, ALREADY-CHOSEN marketplace offer, not a filter for the orchestrator to resolve server-side the way UpRequest/DeployRequest's flattened gpuModel/maxPrice/provider fields are. The caller (aq's start.go/move.go) is responsible for querying the marketplace, picking the cheapest offer matching its own filters, and building this from it. Image, ports, startup script, and template are NOT here: they come from the pod's own config columns (D10).

type OgreDownloadURLResult added in v0.4.0

type OgreDownloadURLResult struct {
	URL       string `json:"url"`
	ExpiresAt string `json:"expires_at"`
	SHA256    string `json:"sha256"`
	Version   string `json:"version"`
	// Asset is the file the URL serves, e.g. ogre_Darwin_arm64.tar.gz. It is
	// the wire's own statement that this is a TAR.GZ ARCHIVE for one platform
	// rather than a bare binary — the thing nothing on this path ever said,
	// which is why aq installed the tarball itself as the executable and every
	// `aq import` died with `exec format error` (#970).
	Asset string `json:"asset"`
}

OgreDownloadURLResult is the data returned by GET /artifacts/ogre/download-url: a presigned URL for the pinned ogre release, good only until ExpiresAt, plus the sha256 aq MUST verify before executing anything it downloads.

type ProviderSpec added in v0.6.0

type ProviderSpec struct {
	Name string `json:"name"`
}

ProviderSpec is the `provider` object inside an OfferSelection.

type QueueDeploymentRequest added in v0.7.0

type QueueDeploymentRequest struct {
	GPUModel            string           `json:"gpuModel"`
	GPUCount            int              `json:"gpuCount"`
	PriceCeilingPerHour float64          `json:"priceCeilingPerHour"`
	ProviderScope       string           `json:"providerScope,omitempty"`
	ExpiresInHours      *float64         `json:"expiresInHours,omitempty"`
	Request             QueuedDeployBody `json:"request"`
}

QueueDeploymentRequest is the body of POST /queued-deployments. PriceCeilingPerHour is mandatory on the wire: it is the spend rail for a create that fires with nobody watching.

type QueuedDeployBody added in v0.7.0

type QueuedDeployBody struct {
	Resource      ResourceSpec        `json:"resource"`
	Provider      ProviderSpec        `json:"provider"`
	ServiceConfig QueuedServiceConfig `json:"serviceConfig"`
}

QueuedDeployBody is the `request` of a queued deployment: a CreateDeployment body for a bare box. At fire time the server rewrites the offer identity (provider, desiredInstanceId, location_id, region) to the matched offer, so what is sent here only has to parse.

type QueuedDeployment added in v0.7.0

type QueuedDeployment struct {
	ID                  int     `json:"id"`
	GPUModel            string  `json:"gpuModel"`
	GPUCount            int     `json:"gpuCount"`
	PriceCeilingPerHour float64 `json:"priceCeilingPerHour"`
	ProviderScope       *string `json:"providerScope"`
	Status              string  `json:"status"`
	ExpiresAt           string  `json:"expiresAt"`
	DeploymentID        *int    `json:"deploymentId"`
	LastError           *string `json:"lastError"`
	CreatedAt           string  `json:"createdAt"`

	Raw json.RawMessage `json:"-"`
}

QueuedDeployment is one row of /queued-deployments.

func (*QueuedDeployment) UnmarshalJSON added in v0.7.0

func (q *QueuedDeployment) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes the row and keeps its raw bytes.

type QueuedServiceConfig added in v0.7.0

type QueuedServiceConfig struct {
	Type              string `json:"type"` // "VMS", the orchestrator's ServiceTypeEnum.VM
	VMSlug            string `json:"vmSlug"`
	SSHKeyID          string `json:"sshKeyId"`
	SnapshotterConfig string `json:"snapshotterConfig"`
}

QueuedServiceConfig is the VM service block of a queued bare box. It is the shape the server's own podOfferRequestPayload builds.

type RegistryCredential added in v0.5.0

type RegistryCredential struct {
	Server   string `json:"server"`
	Username string `json:"username"`
	Token    string `json:"token"`
}

RegistryCredential is the `value` shape for a `type: "registry"` secret: POST /secrets/teams/:teamId's body when Type is "registry", and the shape RotateSecretRequest.Value takes for rotating one.

type ReleaseResult added in v0.4.0

type ReleaseResult struct {
	Released bool `json:"released"`
	// BoxCleared is THREE-STATE and is a *bool for exactly that reason: true we
	// cleared every credential we pushed to the box, false the box refused,
	// nil we could not look (or the orchestrator predates the field). The last
	// two both mean live credentials may remain on the customer's machine, and
	// a bool would make them indistinguishable from a clean hand-back.
	BoxCleared *bool `json:"boxCleared"`
	// BoxClearReason is the orchestrator's own words for why, present whenever
	// BoxCleared is not true.
	BoxClearReason string `json:"boxClearReason"`
}

ReleaseResult is the response to POST /deployments/:id/release.

type ResourceSpec added in v0.6.0

type ResourceSpec struct {
	Name              string `json:"name,omitempty"`
	CPU               int    `json:"cpu"`
	Memory            string `json:"memory"`
	Storage           string `json:"storage"`
	GPUUnits          int    `json:"gpuUnits,omitempty"`
	GPUModel          string `json:"gpuModel,omitempty"`
	DesiredInstanceID string `json:"desiredInstanceId,omitempty"`
	Region            string `json:"region,omitempty"`
	LocationID        string `json:"location_id,omitempty"`
}

ResourceSpec is the `resource` object inside an OfferSelection, mirroring the orchestrator's own ResourceSchema (deployment.schemas.ts) byte for byte: the pod/environment/volume plan's REST amendment (2026-09-26) states this is the SAME shape POST /deployments/deploy already takes, not a new one invented for pods. CPU/Memory/Storage are REQUIRED on the wire (no omitempty): the schema has no default for them, and a caller must resolve a real number/string from the chosen marketplace offer rather than omit the key.

DesiredInstanceID is the SERVER's own name for what a marketplace offer's own `address` field already is (console: `desiredInstanceId: chosenOffer.address`, aquanode-backend gotcha 1: "format: <provider_address>/<preset_id>"). It is `.optional()` on the zod schema but effectively REQUIRED by createDeployment (400s without it), so a caller building this from a real offer must always send it.

Name is the deployment's display name on the raw deploy path (deployment.controller.ts reads `resource.name`). It is omitted when unset and only `aq up --when-available --name` sets it; a pod offer never does.

type RestoreVolumePointRequest added in v0.6.0

type RestoreVolumePointRequest struct {
	PointID string `json:"pointId"`
}

RestoreVolumePointRequest is the body of POST /volumes/:id/restore.

type RotateSecretRequest added in v0.5.0

type RotateSecretRequest struct {
	Value any `json:"value"`
}

RotateSecretRequest is the body of POST /secrets/teams/:teamId/:secretId/rotate. Unlike CreateSecretRequest it carries no Type: the secret's type never changes at rotation, only its stored value, so the caller resends only Value shaped for whatever type the secret already is.

type Run added in v0.5.0

type Run struct {
	ID         string `json:"id"`
	Status     string `json:"status"`
	Reason     string `json:"reason"`
	AcceptedAt string `json:"acceptedAt"`
	StartedAt  string `json:"startedAt"`
	FinishedAt string `json:"finishedAt"`
	Phase      string `json:"phase"`
	// CostCents is the billed cost of this run in US cents, computed
	// server-side from the ledger (round(sum(billing_buckets_v2.amount_usd)
	// * 100) over this run's attempts' deployments). nil means UNKNOWN --
	// the run has no attempt with a deployment yet, or every such
	// deployment is customer-owned (unmetered) -- never "free" or "$0.00".
	// Clients render this number and never compute cost themselves.
	CostCents *int64 `json:"costCents"`
	// Attempts is the run's own history (run.service.ts serializeRunForOwner):
	// a failover shows up here as more than one entry, each with its own
	// provider, rate and window. `aq job ls` derives its GPU/duration
	// columns from this. omitempty because the CALLER's view
	// (serializeRunForCaller, what a run-token holder sees) never carries
	// this key at all.
	Attempts []RunAttempt `json:"attempts,omitempty"`
}

Run mirrors one row of GET /jobs/:id/runs, and the object returned by GET /jobs/:id/runs/:runId.

Status is one of queued|running|succeeded|failed|unservable. "unservable" must never be conflated with "failed": it means Aquanode could not get the caller a box at all (capacity, budget, or provider failure) — the workload itself never ran — while "failed" means it ran and the workload errored. Reason carries the detail for both, but especially matters for unservable, which is otherwise indistinguishable from a normal decline.

type RunArtifact added in v0.5.0

type RunArtifact struct {
	Key          string `json:"key"`
	SizeBytes    int64  `json:"sizeBytes"`
	LastModified string `json:"lastModified"`
}

RunArtifact is one entry of a run's landed artifacts: its log object, and every declared output. Key is a relative name, never the underlying S3 key or bucket layout -- mjolnir owns that (run-artifacts.ts), the orchestrator never hands it to a caller.

type RunArtifactDownload added in v0.5.0

type RunArtifactDownload struct {
	URL            string `json:"url"`
	ExpiresAt      string `json:"expiresAt"`
	AttemptOrdinal int    `json:"attemptOrdinal"`
}

RunArtifactDownload is GET .../artifacts/download?key=...'s response: a fresh, short-lived download URL for one artifact key named by a prior ListRunArtifacts response. Never stored anywhere -- it is a bearer credential, minted per request.

type RunArtifactsList added in v0.5.0

type RunArtifactsList struct {
	Source         string        `json:"source"`
	AttemptOrdinal *int          `json:"attemptOrdinal"`
	Artifacts      []RunArtifact `json:"artifacts"`
	Truncated      bool          `json:"truncated"`
}

RunArtifactsList is GET /jobs/:id/runs/:runId/artifacts's response.

Source is FOUR-state, deliberately mirroring GetRunLogs's own Source field: "no_attempt_yet" (the run has never had a box), "no_box" (this attempt's box was never assigned), "ok" (mjolnir answered -- which may still carry zero Artifacts, a legitimate answer for a run still executing), and "unreachable" (we could not ask). Collapsing "unreachable" into an empty "ok" list would tell a caller "no outputs" when the true answer is "we don't know yet" -- never do that locally either.

type RunAttempt added in v0.6.0

type RunAttempt struct {
	ID       string `json:"id"`
	Ordinal  int    `json:"ordinal"`
	Provider string `json:"provider"`
	// GPUModel and PriceCentsPerHour are nullable on the wire (an attempt
	// that never got a box has neither yet), so both are pointers -- nil
	// means UNKNOWN, never "free" or "no GPU".
	GPUModel          *string `json:"gpuModel"`
	PriceCentsPerHour *int64  `json:"priceCentsPerHour"`
	PlacedAt          string  `json:"placedAt"`
	StartedAt         *string `json:"startedAt"`
	EndedAt           *string `json:"endedAt"`
}

RunAttempt is one entry of Run.Attempts. Only the fields `aq job ls` and a future run-detail view need are named here; the owner view carries more (checkpoint bookkeeping, stall verdicts) that no CLI surface renders yet — an unlisted field simply decodes as absent, never breaking this struct.

type RunLogChunk added in v0.5.0

type RunLogChunk struct {
	Chunk          string `json:"chunk"`
	NextOffset     int64  `json:"nextOffset"`
	Size           int64  `json:"size"`
	Truncated      bool   `json:"truncated"`
	AttemptOrdinal *int   `json:"attemptOrdinal"`
	Source         string `json:"source"`
	LogRef         string `json:"logRef,omitempty"`
}

RunLogChunk is one slice of a run's log, served by byte offset.

`NextOffset` comes from the SERVER, never computed by advancing the local cursor by len(Chunk): a capped read would make the follower skip bytes it never saw. `Source` is a real answer and not decoration — `unreachable` means we could not read the log at all, which must never be printed as an empty log, since an empty tail reads as "nothing is being written".

type SSHKey

type SSHKey struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	PublicKey string `json:"public_key"`
}

SSHKey mirrors a row from GET /settings/ssh-keys.

type Secret added in v0.5.0

type Secret struct {
	ID        string  `json:"id"`
	Name      string  `json:"name"`
	Type      string  `json:"type"` // "env" | "registry"
	CreatedAt string  `json:"createdAt"`
	UpdatedAt string  `json:"updatedAt"`
	RotatedAt *string `json:"rotatedAt"`
}

Secret mirrors one row returned by the secrets store: the `data` of create/rotate, and one entry of list's `data.secrets`. RotatedAt is a pointer because "never rotated" is a real, common state (every secret starts there), not the zero value of a string.

type ServiceCredentials

type ServiceCredentials struct {
	Template string `json:"template"`
	URL      string `json:"url"`
	Username string `json:"username"`
	Password string `json:"password"`
	Status   string `json:"status"`
}

ServiceCredentials is the running template service's reachable URL + auth, as surfaced on the deployment row once ogre has started the service.

type ServiceURL added in v0.2.0

type ServiceURL struct {
	URL  string `json:"url"`
	Port portID `json:"port"`
}

ServiceURL is one entry of a deployment's service_urls array: a reachable URL keyed by the internal logical port it maps.

type Setup added in v0.4.0

type Setup struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
	// Environment is nil on the rare row whose environmentVersionId doesn't
	// resolve (see SetupEnvironmentSummary's doc comment). Never render a nil
	// Environment as an empty-but-real one.
	Environment *SetupEnvironmentSummary `json:"environment"`
	// Volume is nil for a pod running with no volume attached (D4 of the
	// pod/environment/volume plan: a bare pod is allowed, and the New pod
	// flow warns about it). Never render a nil Volume as an empty one.
	Volume *SetupVolumeSummary `json:"volume"`
	// Restore is non-nil only while Start is restoring this pod's
	// environment/volume onto its box.
	Restore *SetupRestoreProgress `json:"restore"`
	// AutostopEnabled is three-state on the wire: nil = never explicitly
	// chosen (the pod follows the platform default), non-nil = explicitly
	// set true/false. NEVER collapse nil into false when rendering this —
	// see setups.controller.ts's comment on why (it's the whole point of the
	// column). Renamed from AutopauseEnabled/autopauseEnabled per the
	// pod/environment/volume plan's vocabulary sweep (PUT
	// /setups/:id/autostop replaces PUT /setups/:id/autopause; no alias).
	AutostopEnabled *bool `json:"autostopEnabled"`
	// DeploymentID is populated on POST /setups and POST /setups/:id/start's
	// response (w3-backend, 2026-09-26): the freshly-rented deployment id, so
	// the caller can poll `aq status <id>` while the box restores. Absent on
	// every other response that decodes a Setup (Stop/Move return the pod
	// with no such field), nil there, never a stale or guessed id.
	DeploymentID *int `json:"deploymentId,omitempty"`
	// AttachedDeploymentID is the deployment currently running this pod, nil
	// once Stopped or once that deployment closes. Replaces the pre-model
	// LeaseDeploymentID (the lease moved to Volume and is no longer
	// serialized on the pod itself, w3-backend 2026-09-26): Running() derives
	// from this field alone.
	AttachedDeploymentID *int `json:"attachedDeploymentId"`
	// Stopping is true while a Stop, or the first half of a Move, is in
	// flight: the pod is neither cleanly Running nor cleanly Stopped, and
	// collapsing it into either would misreport an in-progress release as
	// one of its two settled states.
	Stopping  bool   `json:"stopping"`
	CreatedAt string `json:"createdAt"`
	UpdatedAt string `json:"updatedAt"`
}

Setup mirrors one row of GET /setups: what the caller owns, independent of whether the underlying compute is currently rented.

ID is a UUID string (`model Setup { id String @id @default(uuid()) ... }`) — never the numeric Deployment.ID from control.go, even though a running setup has one associated via AttachedDeploymentID. There is also no boolean "running" field on the wire: Running derives it from AttachedDeploymentID, which is only non-nil while a live deployment is attached. There is likewise no "latest version" field nested here at all, see ListAllSetupVersions.

This struct mirrors the confirmed PodDTO shape (w3-backend, pod-serializer.ts, 2026-09-26), not the pre-pod/environment/volume serializeSetup shape the doc comment above used to describe: that serializer sent `mountPath`, `sizeBytes` and `lastSyncAt` on every row, none of which PodDTO sends any more, and reading them here would silently decode to a zero value that looks like a real (empty) answer rather than an absent field.

Every tag below is camelCase, matching PodDTO field for field.

func (Setup) Running added in v0.4.0

func (s Setup) Running() bool

Running reports whether a deployment currently holds this setup's lease.

type SetupAutostopRequest added in v0.6.0

type SetupAutostopRequest struct {
	Enabled *bool `json:"enabled"`
}

SetupAutostopRequest is the body of PUT /setups/:id/autostop. Enabled is three-state on the wire (enabled:boolean|null, null clears back to the platform default). `aq autostop` itself only ever sends true or false today (there is no "unset" verb yet, matching the old `aq autopause`'s own limitation), but the pointer keeps the client honest about what the wire allows.

type SetupEnvironmentSummary added in v0.6.0

type SetupEnvironmentSummary struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Version *int   `json:"version"`
	Kind    string `json:"kind"`
}

SetupEnvironmentSummary mirrors the `environment` object nested on GET /setups, GET /setups/:id. The plan's wire contract (section 2) describes it as always present, but PodDTO's own type (pod-serializer.ts, aquanode-backend) is `environment: {...} | null`, and the serializer genuinely returns null when a pod's `environmentVersionId` doesn't resolve to a live version row. The plan wins on prose, the running code wins on what the wire actually sends, and this is why `Setup.Environment` is a pointer rather than a plain value: decoding a JSON `null` into a non-pointer struct silently zeroes every field, which then renders identically to a normal (if unusual) empty name, exactly the "environment version -" a console `Running` pod must never show for `aq pods`. Every pod observed live (224 rows, 2026-09-26) had a non-null environment, so this is the rare path, not the common one; callers still must not assume non-nil. Version is nullable on the wire (int|null): the working environment has no minted EnvironmentVersion until the pod's environment is Kept or Shared for the first time.

type SetupRestoreProgress added in v0.6.0

type SetupRestoreProgress struct {
	Phase      string `json:"phase"`
	BytesDone  int64  `json:"bytesDone"`
	BytesTotal int64  `json:"bytesTotal"`
}

SetupRestoreProgress mirrors the `restore` object nested on GET /setups, GET /setups/:id while a Start is restoring the pod's environment/volume onto its box. Nil once the pod reaches Running (`ready_at`) or if it was never restoring.

type SetupVersion added in v0.4.0

type SetupVersion struct {
	ID              int    `json:"id"`
	Name            string `json:"name"`
	Version         int    `json:"version"`
	Label           string `json:"label"`
	Description     string `json:"description"`
	Visibility      string `json:"visibility"`
	Path            string `json:"path"`
	SizeBytes       int64  `json:"size_bytes"`
	CreatedAt       string `json:"created_at"`
	Provenance      string `json:"provenance"`
	Pinned          bool   `json:"pinned"`
	SetupID         string `json:"setup_id"`
	DeploymentCount int    `json:"deployment_count"`
}

SetupVersion mirrors one row of the setup_versions table, as returned by GET /setups/versions[?name=...]. Pods stop WRITING this table under the pod/environment/volume model (D11: `aq save` and its route are gone, saves mint EnvironmentVersion/VolumePoint rows now), it survives read-only, because Jobs still reference a SnapshotVersion as a source (`aq job create <pod> <version>`, job.go) and existing rows must stay resolvable. There is no "latest version" field nested on Setup itself, see ListAllSetupVersions for how `aq pods`/`aq job create`/`aq job point` recover a setup's latest/named version instead.

SetupID is a string for the same reason Setup.ID is, see the package doc. The version row's own ID is left an int: unlike Setup, nothing in the ticket's Prisma excerpt confirms its type, and control.go already shows this schema mixing int autoincrement ids (Deployment) with uuid ids (Setup) rather than using one convention everywhere. If a live server returns a non-numeric version id, GetSetupVersion's request will surface that as a clear decode/404 error rather than a silent wrong-row resolve, but it hasn't been confirmed either way.

type SetupVolumeSummary added in v0.6.0

type SetupVolumeSummary struct {
	ID            string  `json:"id"`
	Name          string  `json:"name"`
	SizeBytes     *int64  `json:"sizeBytes"`
	HeadSavedAt   *string `json:"headSavedAt"`
	SaveState     string  `json:"saveState"`
	LastSaveError *string `json:"lastSaveError"`
}

SetupVolumeSummary mirrors the `volume` object nested on GET /setups, GET /setups/:id (PodVolumeSummary, orchestrator/src/services/volumes/volume.service.ts, confirmed against source 2026-09-28, #811). SaveState is three-state on the wire ("saved"|"failing"|"never_saved" -- renamed from "unknown" in #811, same meaning: nothing has landed yet, not a warning) and must never collapse "never_saved" into "saved", see the workspace's three-state signal rule. LastSaveError is nil except while SaveState is "failing". SizeBytes and HeadSavedAt are both nullable: SizeBytes is null before storage metering has ever measured this volume, HeadSavedAt is null before its first save ever lands. Neither collapses to a zero value that would read as a real answer.

type ShareEnvironmentByIDRequest added in v0.6.0

type ShareEnvironmentByIDRequest struct {
	VersionID string `json:"versionId"`
}

ShareEnvironmentByIDRequest is the body of POST /environments/:id/share. Unlike the pod-scoped share above, versionId is REQUIRED here: a named Environment has no single "current pod" to default the latest from.

type ShareEnvironmentRef added in v0.6.0

type ShareEnvironmentRef struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

ShareEnvironmentRef is the {id,name} pair GET /shares/:shareId nests as environment: the environment the link names, present on every successful response regardless of state (a link whose environment is gone 404s instead, environment.service.ts's openShare).

type ShareEnvironmentRequest added in v0.6.0

type ShareEnvironmentRequest struct {
	VersionID string `json:"versionId,omitempty"`
}

ShareEnvironmentRequest is the body of POST /setups/:id/environment/share. VersionID is optional (absent = the lineage's latest version; a fresh capture runs first if the pod is Running).

type ShareResult added in v0.6.0

type ShareResult struct {
	ShareID  string `json:"shareId"`
	ShareURL string `json:"shareUrl"`
	State    string `json:"state"`
}

ShareResult is the data returned by POST /setups/:id/environment/share and POST /environments/:id/share. State is "preparing" until the server-side publish job (a restic copy into a fresh per-version repo) finishes; GET /shares/:shareId polls it to "ready" or "failed". Never assume ready from this response alone.

type ShareStatus added in v0.6.0

type ShareStatus struct {
	State       string              `json:"state"`
	Error       *string             `json:"error"`
	Environment ShareEnvironmentRef `json:"environment"`
	VersionID   *string             `json:"versionId"`
	Version     *int                `json:"version"`
}

ShareStatus is the data returned by GET /shares/:shareId (confirmed against environment.service.ts's openShare, aquanode-backend#803, 2026-09-26). State is three-state ("preparing"|"ready"|"failed"); Error is non-nil only once State is "failed". Environment is always present on a successful response regardless of State: openShare 404s the whole call (EnvironmentNotFoundError) rather than ever returning it null or absent.

VersionID and Version are BOTH nullable, and null is not tied to a specific State the way it might look: they are null while a Running pod's share is still capturing (or that capture failed, so State is "preparing"/"failed" with no version minted yet), but can already be non-nil even while State is still "preparing", once the version exists and only its publish-to-a-shareable-copy job is still running. Never assume one implies the other; read both independently.

type StartSetupRequest added in v0.6.0

type StartSetupRequest struct {
	Offer OfferSelection `json:"offer"`
}

StartSetupRequest is the body of POST /setups/:id/start.

type UpRequest

type UpRequest struct {
	// Template must be OMITTED, not sent empty, to ask for a bare box. The
	// orchestrator validates it as an optional enum: absent means a bare box,
	// while `""` is a present-but-invalid enum value and 400s with the
	// unhelpful "Invalid request data". `omitempty` is only safe here because
	// that schema no longer carries a `.default(COMFYUI)` — while it did,
	// omitting the key silently installed ComfyUI on a box the CLI had just
	// told the user was bare.
	Template string  `json:"template,omitempty"`
	SSHKeyID string  `json:"sshKeyId"`
	GPUModel string  `json:"gpuModel,omitempty"`
	MaxPrice float64 `json:"maxPrice,omitempty"`
	Provider string  `json:"provider,omitempty"`
	// GPUCount asks for a multi-GPU box. Omitted (0) means "no opinion", which
	// the orchestrator reads as its own default of one — the same
	// omit-means-defaults contract IdlePolicy uses below. Never send 1
	// explicitly for an unspecified request: an older orchestrator that does
	// not know the field would ignore it either way, but a caller that always
	// sends a number cannot tell "I want exactly one" from "I did not ask".
	GPUCount int `json:"gpuCount,omitempty"`
	// Name sets the deployment's display name. When empty the orchestrator
	// falls back to a generated "<Adjective> <GPU> from <Region>" name. Passing
	// a stable `ticket-<N>-<label>` here lets the session-scoped reaper attribute
	// and clean up a throwaway box that would otherwise bill as an orphan (#310).
	Name string `json:"name,omitempty"`
	// IdlePolicy opts a fresh deployment into idle-auto-stop at creation time.
	// Nil (the Go zero value for a pointer) omits the key entirely, meaning "no
	// opinion, use the orchestrator's defaults," never "explicitly off." Those
	// defaults are OFF everywhere (DEFAULT_IDLE_POLICY.autoStopEnabled is false
	// and the console's deploy-sheet toggle starts unchecked), so a CLI deploy
	// that says nothing is not enrolled, same as every other surface.
	IdlePolicy *IdlePolicyUpdate `json:"idlePolicy,omitempty"`
}

UpRequest is the body of POST /deployments/up.

type UpResult

type UpResult struct {
	DeploymentID int    `json:"deploymentId"`
	ProjectID    string `json:"projectId"`
	Status       string `json:"status"`
	Message      string `json:"message"`
}

UpResult is the data returned by POST /deployments/up and POST /deployments/deploy-snapshot.

type Volume added in v0.6.0

type Volume struct {
	ID          string  `json:"id"`
	Name        string  `json:"name"`
	SizeBytes   *int64  `json:"sizeBytes"`
	HeadSavedAt *string `json:"headSavedAt"`
	// SaveState is three-state on the wire ("saved"|"failing"|"never_saved"
	// -- "never_saved" renamed from "unknown" in #811, same field, same
	// meaning: nothing has been saved yet, not a warning). Never collapse
	// "never_saved" into "saved"; same rule as SetupVolumeSummary.
	SaveState     string        `json:"saveState"`
	LastSaveError *string       `json:"lastSaveError"`
	MountPath     string        `json:"mountPath"`
	InUseBy       *VolumeHolder `json:"inUseBy"`
	// Copying is true while this volume's bytes are still landing
	// server-side (a Duplicate's target, or a share link's files copying
	// in), bounded to a few hours. Never attach/restore/rely on its data
	// until this is false.
	Copying   bool   `json:"copying"`
	CreatedAt string `json:"createdAt"`
	// UsedBy is only populated by GetVolume (GET /volumes/:id); empty on a
	// ListVolumes row, same rule as Points.
	UsedBy []VolumeUser  `json:"usedBy"`
	Points []VolumePoint `json:"points"`
}

Volume mirrors one row of GET /volumes, GET /volumes/:id (confirmed against orchestrator/src/services/volumes/volume.service.ts's VolumeDTO/ VolumeDetailDTO, w3-backend PR #811, 2026-09-28). UsedBy and Points are only populated on the single-volume fetch; a list-all response leaves both empty, which callers must render as "no history yet" / nothing to list, never as an error.

SizeBytes and HeadSavedAt are both nullable (never a bare number/string on the wire): SizeBytes is null before storage metering has ever measured this volume's prefix, and HeadSavedAt is null before its first save ever lands. Neither must ever decode to a bare zero value that reads as a real answer ("0 B held" / some real timestamp) when the true state is "not measured yet" / "never saved yet".

InUseBy replaces the pre-model AttachedPodID/AttachedPodName/Running: under the one-pod-per-volume model those three fields conflated "references this volume" with "is writing it right now"; under this model many pods may reference a volume; delete, never alias. InUseBy alone answers "is writing it right now" (nil = no); Delete refuses on it alone. UsedBy (detail only) lists every REFERENCING pod, running or not.

type VolumeHolder added in v0.6.0

type VolumeHolder struct {
	PodID   *string `json:"podId"`
	PodName *string `json:"podName"`
}

VolumeHolder is the pod whose live box has a volume right now (`inUseBy`). PodID and PodName are BOTH nullable, and nil together for the same reason: a volume's write lease can outlive the pod row that claimed it, so a non-nil VolumeHolder with a nil PodName means "some pod holds it, but no pod row could be found for it", never "attached to nothing in particular". Check the *VolumeHolder pointer itself for "not in use", not these fields.

type VolumeInUseError added in v0.6.0

type VolumeInUseError struct {
	Message string
	InUseBy *VolumeHolder
}

VolumeInUseError is DELETE /volumes/:id's 409 while a pod's live box has the volume (single writer). InUseBy names the holder in the same shape Volume.InUseBy carries; never blind-retry this delete, stop that pod first (or wait for it to stop).

func (*VolumeInUseError) Error added in v0.6.0

func (e *VolumeInUseError) Error() string

type VolumePoint added in v0.6.0

type VolumePoint struct {
	ID         string  `json:"id"`
	CreatedAt  string  `json:"createdAt"`
	Provenance string  `json:"provenance"`
	Label      *string `json:"label"`
}

VolumePoint is one row of a volume's history, as nested under Volume's own `points` field. Provenance is three-state-ish on the wire ("stop" | "idle_stop"), never invented client-side; Label is nil except on a point migrated from a named legacy SnapshotVersion (D10).

type VolumeUser added in v0.6.0

type VolumeUser struct {
	PodID   string `json:"podId"`
	PodName string `json:"podName"`
	State   string `json:"state"` // "running" | "stopped"
}

VolumeUser is one pod that references a volume (`usedBy`, GET /volumes/:id only), running or not: any number of pods may reference one, and State is what tells them apart from the single pod actually writing it (VolumeHolder above).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL