v0wire

package
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

v0wire/agents.go

v0wire/decode.go

Package v0wire is the JSON wire of Rainier's /v0/ HTTP surface: the client- facing view of every resource, the request bodies clients send, the validators those bodies pass, and the closed mapping from a control sentinel to a status code. Its canonical import path is github.com/tokencanopy/rainier/v0wire.

It is shapes and rules, not a server. There is no router here and no handler: routing, authentication, and the store reads that fill a view in stay with each host (ADR-0001 leaves HTTP routing outside the application service), so self-hosted controld and a hosted cell can serve the same wire from different front doors without either copying the shapes.

The package is neutral in three ways:

  • Storage-neutral: it renders control's domain types and nothing else. A view field that no domain type carries — a session's environment NAME, an environment's snapshot runner — is passed in by the host, which is the one that knows how to look it up.
  • Identity-neutral: it decodes no credential, names no actor, and carries no authorization. A request body is data until a host has authorized it.
  • Secret-free: no view type has anywhere to put a secret value, a credential, or a terminal byte, which is the durable way to keep one off the wire. Secrets and credentials views are the host's own.

Two rules hold across every view here, and the golden tests in wire_test.go are what keep them true:

  • No field is omitempty. The key set of a view is identical on every row, because a key that appears only sometimes cannot be told apart by a client from an older server that never had it.
  • A nil slice renders as [] and never as null, so the difference between two stores' nil-vs-empty handling never reaches a client.

v0wire/environments.go

v0wire/errors.go

v0wire/runners.go

v0wire/sessions.go

Index

Constants

View Source
const (
	AgentStatusLoggedIn = "logged_in"
	AgentStatusNone     = "none"
)

The two statuses an agent row can report. They are the whole vocabulary on purpose: custody either holds a credential set for this person and this agent or it does not, and every richer thing a client might want to know — whether the token still works, when it expires, which account it belongs to — is knowledge only the provider has, and knowledge Rainier deliberately never asks it for.

View Source
const DefaultBaseBranch = "main"

DefaultBaseBranch is the branch a github connector clones when it names none.

View Source
const MaxCapabilities = 32

MaxCapabilities bounds how many capabilities one claim may carry.

Variables

This section is empty.

Functions

func DecodeCreateSession

func DecodeCreateSession(req CreateSessionRequest) (control.CreateSession, string)

DecodeCreateSession turns a decoded create body into the command the session service takes, returning the client-facing 400 message (empty when the body is fine) instead of an error: every way this can fail is the caller's own request, named by field and by index.

Two fields are deliberately NOT set here, because neither is in the body: IdempotencyKey travels as a header, and EnvironmentID is the resolution of the body's `environment` reference, which only a host with the name index can perform. The caller sets both on the returned command.

The session's own spec always travels: for a scratch session it is the whole description, for an environment session it is layered over the environment field by field (control.PortableSpec) by the service.

func DecodeJSON

func DecodeJSON(w http.ResponseWriter, r *http.Request, v any, limit int64) bool

DecodeJSON decodes r's body (capped at limit bytes) into v, rejecting unknown fields and a body holding more than one JSON value. An empty body decodes to v's zero value: every field in every body this wire accepts is optional, so "no body at all" and "{}" are the same request.

It writes a 400 invalid_request response and returns false on any failure; callers should return immediately when it does.

func EnvironmentRequirements

func EnvironmentRequirements(placement string, capabilities []string) control.Requirements

EnvironmentRequirements composes an environment's one requirements list out of the two halves the wire keeps apart: the operator's runner pin, which control.Environment cannot name directly and so round-trips through the capability "placement:<runner>", and the portable capabilities the operator asked for. The pin goes first, so the list reads where-then-what, and RenderEnvironment takes the two halves back out by the same rule. No pin and no capabilities is no requirements at all rather than an empty list.

func PlacementOf

func PlacementOf(reqs control.Requirements) string

PlacementOf is the read half: the runner an environment is pinned to, or "" when it is pinned to none.

func StatusFor

func StatusFor(err error) (status int, code, msg string)

StatusFor maps a control sentinel to its status, slug, and a fixed message. It is the one place the wire learns about the closed error set; handlers that already validated a request pass their own specific message for ErrInvalid and ErrConflict, and never invent a second mapping.

A caller that went away gets a zero status and nothing to write: the caller is the one component that cannot read the answer, and there is no dependency to report. WriteControlError honors that by writing nothing for those two.

func ValidateCapabilities

func ValidateCapabilities(what string, caps []string) error

ValidateCapabilities applies that rule to caps, returning a client-facing sentence naming the first thing wrong with it. what names the list in that sentence, so the same rule can answer for an environment's field and for a runner's announce without either message claiming to be the other.

Host prefixes are refused rather than ignored: a capability naming a runner's own name is the HOST's claim about it, and a runner or an operator that could write one could pin any environment to any runner.

func ValidateConnectors

func ValidateConnectors(raw json.RawMessage) ([]control.Connector, error)

ValidateConnectors decodes and validates raw — the "connectors" member of an environment body — into the rows a store persists. An absent or empty array is no connectors at all.

Every returned Connector carries the element's ORIGINAL bytes in Raw: stores render an empty Raw differently, so keeping Raw always-populated here is what keeps that difference out of reachable space, and it is what lets a client read back exactly the object it wrote.

Errors are written for the caller: each names the offending element by index and says what was wrong with it, and none carries internal detail.

func ValidateEnvironmentBasics

func ValidateEnvironmentBasics(name, image string, setupTimeoutSec, initTimeoutSec int) string

ValidateEnvironmentBasics checks the four scalar rules create and patch share, returning a client-facing message (or "" when the row is fine). Placement is deliberately unchecked: an environment may be pinned to a runner that hasn't joined the fleet yet, which is exactly how the hardware case is set up. Neither script is checked either — a shell script is only wrong once it runs.

func WriteControlError

func WriteControlError(w http.ResponseWriter, err error)

WriteControlError answers a service error with StatusFor's fixed mapping. Hosts that need a specific message, or a refinement of their own, compute the triple with StatusFor and call WriteError.

func WriteError

func WriteError(w http.ResponseWriter, status int, code, msg string)

WriteError writes status with a JSON error envelope. msg must never contain anything a caller shouldn't see — log the detail separately instead.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any)

WriteJSON writes v as the response body with the given status and this wire's content type. A body that fails to encode has already had its status line sent, so there is nothing left to tell the caller; a host that wants that failure in its log wraps the writer.

Types

type AgentView added in v0.0.3

type AgentView struct {
	Provider string `json:"provider"`
	Status   string `json:"status"`
	// Since is when custody last recorded a change to this set, and null for a
	// row that says "none" — there is no credential, so there is no instant
	// one was last written.
	//
	// A POINTER rendered as null rather than an omitted key, exactly like
	// SessionView.ChildExitCode and for the package's own reason (doc.go): no
	// field here is omitempty, because a key that appears only sometimes
	// cannot be told apart by a client from an older server that never had
	// it. The key set of this view is identical on every row.
	Since *time.Time `json:"since"`
	// Version is custody's counter for the set: it goes up by one on every
	// put, and it is what `rainier agent login` compares before and after a
	// login session to tell "the agent wrote a credential" from "the person
	// exited without finishing". Zero for a provider with no set.
	Version uint64 `json:"version"`
	// Workspaces are the workspaces the caller is a member of — where this
	// login is, or would be, in force. It is the same list on every row
	// because it describes the CALLER, not the credential: one login reaches
	// every workspace the person belongs to, which is the whole point of
	// keying custody by (user, provider) rather than by workspace.
	Workspaces []string `json:"workspaces"`
}

AgentView is the client-facing rendering of one coding agent's login: which agent, whether this person has logged it in, when custody last saw it move, which version it is at, and the workspaces the login reaches.

It has nowhere to put a credential, and that is the durable form of the promise: controlapp.AgentCredentialStatus — the only thing RenderAgents takes — has no bytes on it either, so no listing path from the store to a client passes through a type that could carry one.

There is no "note" field. sessiond emits a boot note when a fetch is refused (plan §Task 3), but nothing routes those notes from the runner back to the control plane yet, so a note key here would be a promise this version cannot keep. The CLI says "the agent wrote no credential" instead, which is the truth it can actually establish.

type AgentsEnvelope added in v0.0.3

type AgentsEnvelope struct {
	Agents []AgentView `json:"agents"`
}

AgentsEnvelope is the JSON shape of GET /v0/agents.

func RenderAgents added in v0.0.3

func RenderAgents(statuses []controlapp.AgentCredentialStatus, workspaces []control.WorkspaceID) AgentsEnvelope

RenderAgents renders one person's agent logins for the workspaces they belong to. It lists EVERY provider this build knows, in the table's own order, because the listing answers "which agents can I log in, and have I" — a client that only got back the ones already logged in would have no way to discover the others, and `rainier agent ls` would show an empty table to exactly the person who most needs to see what is on offer.

statuses is what custody holds; the order and the completeness of the answer are the table's. A status naming a provider outside the table is dropped rather than rendered: it can only be a set left behind by a build that had a row this one does not, and a client cannot act on it.

type CreateEnvironmentRequest

type CreateEnvironmentRequest struct {
	Name            string          `json:"name,omitempty"`
	Image           string          `json:"image,omitempty"`
	Setup           string          `json:"setup,omitempty"`
	Init            string          `json:"init,omitempty"`
	InitTimeoutSec  int             `json:"init_timeout_sec,omitempty"`
	EgressAllow     []string        `json:"egress_allow,omitempty"`
	SecretRefs      []string        `json:"secret_refs,omitempty"`
	Connectors      json.RawMessage `json:"connectors,omitempty"`
	Placement       string          `json:"placement,omitempty"`
	Capabilities    []string        `json:"capabilities,omitempty"`
	SetupTimeoutSec int             `json:"setup_timeout_sec,omitempty"`
}

type CreateSessionRequest

type CreateSessionRequest struct {
	Name        string   `json:"name,omitempty"`
	Image       string   `json:"image,omitempty"`
	Cmd         []string `json:"cmd,omitempty"`
	EgressAllow []string `json:"egress_allow,omitempty"`
	Environment string   `json:"environment,omitempty"`
	// Repos overrides the repositories the environment's github connectors
	// declare. Absent inherits them; an explicit empty array clones nothing,
	// exactly the nil-vs-empty distinction egress_allow already draws — and
	// for the same reason: "I didn't say" and "I said none" are different
	// answers, and a session that means to be scratch under a repo-carrying
	// environment has no other way to say so.
	Repos []RepoRequest `json:"repos,omitempty"`
}

CreateSessionRequest is POST /v0/sessions's body. Environment names the environment this session starts from, by name or by id; omitting it is a scratch session, exactly as before environments existed. Image and EgressAllow are overrides when it is present — the host resolves them.

type EnvironmentEnvelope

type EnvironmentEnvelope struct {
	Environment EnvironmentView `json:"environment"`
}

type EnvironmentView

type EnvironmentView struct {
	ID              string            `json:"id"`
	Name            string            `json:"name"`
	Image           string            `json:"image"`
	Setup           string            `json:"setup"`
	SetupHash       string            `json:"setup_hash"`
	Init            string            `json:"init"`
	InitTimeoutSec  int               `json:"init_timeout_sec"`
	EgressAllow     []string          `json:"egress_allow"`
	SecretRefs      []string          `json:"secret_refs"`
	Connectors      []json.RawMessage `json:"connectors"`
	Placement       string            `json:"placement"`
	Capabilities    []string          `json:"capabilities"`
	SetupTimeoutSec int               `json:"setup_timeout_sec"`
	SnapshotRef     string            `json:"snapshot_ref"`
	SnapshotRunner  string            `json:"snapshot_runner"`
	SnapshotHash    string            `json:"snapshot_hash"`
	CreatedAt       string            `json:"created_at"`
	UpdatedAt       string            `json:"updated_at"`
}

EnvironmentView is the client-facing rendering of an Environment. Like SessionView, no field is omitempty: the key set is identical on every environment, including the three snapshot fields, which are present and empty until a session build caches one (a client compares snapshot_hash against setup_hash to see staleness for itself).

func RenderEnvironment

func RenderEnvironment(e control.Environment, snapshotRunner control.RunnerID) EnvironmentView

RenderEnvironment renders e as its client-facing view.

Two of the wire's fields have no field of their own on control.Environment, which names no runner: `placement` is carried as the portable capability "placement:<runner>" (EnvironmentRequirements) and read back out of it here, and `snapshot_runner` — which the control model does not carry at all — is passed in by the host, which read it off its own store row for the view. `capabilities` shares Requirements with the pin and is the rest of it: what this environment needs a runner to be able to DO, with the host's own spellings of WHERE (placement:, snapshot:) filtered back out.

type EnvironmentsEnvelope

type EnvironmentsEnvelope struct {
	Environments []EnvironmentView `json:"environments"`
}

type ErrorBody

type ErrorBody struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

ErrorBody is the "error" object inside every error response this wire returns. Code is machine-readable and branchable; Message is for humans and never carries internal detail (upstream bodies, stack traces, SQL).

type ErrorEnvelope

type ErrorEnvelope struct {
	Error ErrorBody `json:"error"`
}

ErrorEnvelope is the JSON shape of every error response: {"error":{"code":..., "message":...}}.

type GitHubConnector

type GitHubConnector struct {
	Type       string  `json:"type"`
	Repo       string  `json:"repo"`
	BaseBranch *string `json:"base_branch"`
}

GitHubConnector is the github connector's v0 shape. BaseBranch is a pointer so an absent base_branch (which means DefaultBaseBranch) is distinguishable from an explicit empty one — an empty branch name is a typo, never a request for the default, and it must not reach a clone as one.

func DecodeGitHubConnector

func DecodeGitHubConnector(elem json.RawMessage) (GitHubConnector, error)

DecodeGitHubConnector strictly decodes elem as a github connector. The returned BaseBranch is never nil: an absent base_branch is filled in with DefaultBaseBranch here, in the decode a clone path repeats against the stored bytes — the default lives here, not in the stored row, so an environment keeps exactly the object its author wrote.

type PatchEnvironmentRequest

type PatchEnvironmentRequest struct {
	Name            *string         `json:"name,omitempty"`
	Image           *string         `json:"image,omitempty"`
	Setup           *string         `json:"setup,omitempty"`
	Init            *string         `json:"init,omitempty"`
	InitTimeoutSec  *int            `json:"init_timeout_sec,omitempty"`
	EgressAllow     *[]string       `json:"egress_allow,omitempty"`
	SecretRefs      *[]string       `json:"secret_refs,omitempty"`
	Connectors      json.RawMessage `json:"connectors,omitempty"`
	Placement       *string         `json:"placement,omitempty"`
	Capabilities    *[]string       `json:"capabilities,omitempty"`
	SetupTimeoutSec *int            `json:"setup_timeout_sec,omitempty"`
}

PatchEnvironmentRequest is PATCH's body: every field is a pointer (or, for connectors, a nil-able raw message) so "absent" is distinguishable from "set to the zero value" — clearing a list and leaving it alone are different requests.

type RepoRequest

type RepoRequest struct {
	Repo       string  `json:"repo"`
	BaseBranch *string `json:"base_branch"`
}

RepoRequest is one entry of that array. BaseBranch is a pointer for the same reason the github connector's is: an explicitly empty base_branch is a typo, never a request for the default, and it must not reach the clone as one.

type RunnerView

type RunnerView struct {
	Name          string `json:"name"`
	Connected     bool   `json:"connected"`
	CapacityUsed  int    `json:"capacity_used"`
	CapacityTotal int    `json:"capacity_total"`
	LastSeenAt    string `json:"last_seen_at"`
}

RunnerView is the client-facing rendering of a Runner: what the fleet listing promises about one runner, and nothing about how it was reached. No field is omitempty, on the same terms as every other view here.

func RenderRunner

func RenderRunner(r control.Runner, connected bool) RunnerView

RenderRunner renders r as its client-facing view. connected is the caller's answer rather than the row's: whether a runner has a control connection is a live fact about one replica, and a host that knows better than the stored column says so here.

type RunnersEnvelope

type RunnersEnvelope struct {
	Runners []RunnerView `json:"runners"`
}

type SessionDerived

type SessionDerived struct {
	Reachable   bool
	Environment string
	QueueReason string
}

SessionDerived carries the three view fields that cannot be read off the session row: each depends on live connection state or on another table, and none is stored. Reachable follows the rule s.Runner != "" && runnerConnected(s.Runner) && !s.State.Terminal(); Environment is the environment's NAME ("" for a scratch session, or one whose environment has since been deleted); QueueReason explains a queued session that is waiting on a specific runner. The host computes all three — they are the questions only it can answer.

type SessionEnvelope

type SessionEnvelope struct {
	Session SessionView `json:"session"`
}

type SessionView

type SessionView struct {
	ID          string   `json:"id"`
	OwnerID     string   `json:"owner_id"`
	Name        string   `json:"name"`
	Image       string   `json:"image"`
	Cmd         []string `json:"cmd"`
	EgressAllow []string `json:"egress_allow"`
	State       string   `json:"state"`
	Runner      string   `json:"runner"`
	Reachable   bool     `json:"reachable"`
	Error       string   `json:"error"`
	Environment string   `json:"environment"`
	QueueReason string   `json:"queue_reason"`
	// ChildExitCode is the exit status of the session's agent process, once
	// it has one, and null until then. A POINTER, and rendered as null rather
	// than omitted, because exit 0 is an ANSWER: a session whose agent
	// finished cleanly has to be distinguishable from one still working, and
	// a plain int would make those two the same 0. Present on every session
	// like every other field here — a key that appears only sometimes cannot
	// be told apart from an older controld that never had it.
	ChildExitCode *int   `json:"child_exit_code"`
	CreatedAt     string `json:"created_at"`
	UpdatedAt     string `json:"updated_at"`
	LastEventAt   string `json:"last_event_at"`
}

SessionView is the client-facing rendering of a Session: every field the route table promises, RFC 3339 UTC timestamps, and nil Cmd/EgressAllow normalized to "[]" so the API never exposes one store's nil-vs-empty-slice difference from another's. No field is omitempty — the key set is meant to be identical on every session, which is what the response-shape regression tests pin.

Image is the image the session ACTUALLY runs: for a session started from an environment that is the resolved one (the environment's image, or its cached snapshot), not the empty override the client sent. Environment and QueueReason are derived, never stored — see SessionDerived.

func RenderSession

func RenderSession(s control.Session, d SessionDerived) SessionView

RenderSession renders s as its client-facing view, with d supplying the fields the row itself cannot answer for.

type SessionsEnvelope

type SessionsEnvelope struct {
	Sessions   []SessionView `json:"sessions"`
	NextCursor string        `json:"next_cursor"`
}

type SuspendRequest

type SuspendRequest struct {
	Warm *bool `json:"warm,omitempty"`
}

SuspendRequest's Warm is a pointer so an absent field is distinguishable from an explicit false — the default is true either way.

Jump to

Keyboard shortcuts

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