api

package
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package api holds the operator-facing Connect handlers: tasks and node administration. Identity comes from the transport middleware; nothing here authenticates anyone.

Index

Constants

View Source
const (
	AgentScopeHeader = "X-Podium-Scope"

	// AgentInfraHeader matches internal/agent/api.InfraHeader. The proxy sets it for the
	// dev token and for an admin or owner, and strips a client-supplied copy. A member
	// editing the Slack bot's MCP list is refused by the conductor when this is absent.
	AgentInfraHeader = "X-Podium-Infra"
)

AgentScopeHeader tells the conductor whether this caller is the shared dev token. agentScopeAll is the operator view (every chat). A signed-in user gets no scope header, which the conductor reads as "own chats only". A client-supplied value is stripped with the login header: the browser must not be able to widen its own view.

View Source
const AgentLoginHeader = "X-Podium-Login"

AgentLoginHeader is what the proxy tells the conductor about the human on the other end.

It is a plain header, not a signed assertion. The conductor trusts it only because the bearer proves the request came through this server, which is the only party that knows the token and the only one that authenticated the caller. If the conductor is ever exposed beyond loopback or the compose network, this has to become a signed assertion — docs/security.md says so.

View Source
const ArtifactDownloadPrefix = "/artifacts/"

ArtifactDownloadPrefix is the HTTP path the server proxies artifact bytes on. It is not a Connect procedure on purpose: a 512 MB artifact must stream, and a unary response would have to hold all of it in memory at both ends.

View Source
const DefaultMaxPreviewTTL = 24 * time.Hour

DefaultMaxPreviewTTL caps how long a preview may ask to stay up, PODIUM_PREVIEW_MAX_TTL. A held preview keeps its node slot, so an unbounded one is a slot lost for good.

View Source
const MaxNodeSlots = 256

MaxNodeSlots is the largest slot count SetNodeSlots accepts. It is a guard against a typo, not a considered limit on what a machine can do: the scheduler assigns straight up to this number, so a stray zero on the end would pile hundreds of containers onto one box before anybody noticed. An operator who really wants more can raise max_tasks in the node's own configuration, where the number sits next to the machine it describes.

View Source
const TurnSurfacePrefix = "/turn-surface"

TurnSurfacePrefix is where this server serves the conductor's turn surface: under a path of its own, apart from the UI and the operator APIs, so it can be told apart — routed, logged, rate limited or firewalled — from everything else this server answers. A sandbox is given PODIUM_PUBLIC_URL + TurnSurfacePrefix as PODIUM_AGENT_SANDBOX_URL.

Variables

This section is empty.

Functions

func NewAgentProxy

func NewAgentProxy(agentURL, agentToken string, logger *slog.Logger, infra ...func(context.Context) bool) (http.Handler, error)

NewAgentProxy reverse-proxies the conductor's Connect service.

Mount it behind transport.WithIdentity on /podium.agent.v1.AgentService/ only. Two things happen on every request and both matter: any client-supplied Authorization and X-Podium-Login are deleted, and the server's own pair is set. The browser's credential is the dev token, which the conductor must never see; the login is the server's word about who is calling, which a client must never be able to write.

The conductor's /healthz, /readyz and /metrics are deliberately not proxied. They are its own operational surface, they are unauthenticated on its listener, and publishing them through an authenticated origin would put a second, differently-shaped health story in front of an operator.

infra, when set, says whether a signed-in person may edit the Slack bot's MCP list. The dev token always may. Callers that pass only the logger leave infra unset, so a KindUser is not marked as an operator.

func NewArtifactDownloadHandler

func NewArtifactDownloadHandler(a *artifacts.Service, logger *slog.Logger) http.Handler

NewArtifactDownloadHandler streams an artifact's bytes through the server. It is what `podium artifact get` uses by default: the CLI is guaranteed a route to the control plane and is not guaranteed one to the object store.

func NewTurnSurfaceProxy added in v0.8.0

func NewTurnSurfaceProxy(agentURL string, logger *slog.Logger) (http.Handler, error)

NewTurnSurfaceProxy reverse-proxies the conductor's turn surface, so a cloud sandbox reaches it at this server's public address under TurnSurfacePrefix (PODIUM_AGENT_SANDBOX_URL=PODIUM_PUBLIC_URL/turn-surface) instead of through a tunnel of its own to PODIUM_AGENT_SURFACE_LISTEN.

It is the opposite of NewAgentProxy in the one way that matters: the request's own Authorization — the turn's credential — goes through untouched, and the server's bearer is never added, so nothing that arrives here can act as the server. The headers the conductor reads as the server's word about a person are stripped. Mount it on TurnSurfacePrefix + "/": the prefix is removed, and a path that is not one of the surface's routes is 404 here, never forwarded — the conductor's operator API is on the same listener.

func NewUIHandler

func NewUIHandler(logger *slog.Logger) http.Handler

NewUIHandler serves the embedded single-page app with an SPA fallback: any path that is not a real file is answered with index.html so the client router can take it. Mount it on "/" after the RPC prefixes — it is the last resort, and it is deliberately unauthenticated, because the bundle is not a secret and the API behind it still demands a token.

A binary built with `-tags noui`, or one built before `make web` ever ran, has no index.html; it gets a handler that says so instead of a confusing 404.

Types

type ArtifactService

type ArtifactService struct {
	// contains filtered or unexported fields
}

ArtifactService implements podium.v1.ArtifactService.

func NewArtifactService

func NewArtifactService(a *artifacts.Service, logger *slog.Logger) *ArtifactService

NewArtifactService returns the operator-facing artifact handlers.

func (*ArtifactService) GetArtifactURL

GetArtifactURL mints a presigned GET. A caller with no route to the object store fetches the same bytes from ArtifactDownloadPrefix instead.

func (*ArtifactService) ListArtifacts

ListArtifacts returns everything stored for a task, rolled-up logs included.

type Canceller

type Canceller interface {
	Cancel(ctx context.Context, nodeID, taskID, reason string) error
}

Canceller is the node registry's cancel path.

type Drainer

type Drainer interface {
	SetDrain(ctx context.Context, nodeID string, draining bool) error
	SetSlots(ctx context.Context, nodeID string, maxTasks int32) error
}

Drainer pushes an operator's standing instructions at a connected node: what it may not take, and how much of it.

type Events

type Events interface {
	Subscribe(ctx context.Context, taskID string, fromSeq uint64) (<-chan *podiumv1.TaskEvent, error)
}

Events is the logs service, as much of it as the API needs.

type IdentityService

type IdentityService struct {
	// contains filtered or unexported fields
}

IdentityService implements podium.v1.IdentityService. WhoAmI is still mostly the Identity the transport put in the context; when Google Workspace sign-in is on, it also reports the instance claim so the web UI can show the claim screen instead of the app.

func NewIdentityService

func NewIdentityService(st *store.Store, agentEnabled, googleEnabled bool) *IdentityService

NewIdentityService returns the identity handler. agentEnabled is cfg.AgentEnabled(): whether this server proxies the conductor's API, which is how the web UI knows to show its Agent screen at all. googleEnabled is whether PODIUM_GOOGLE_OAUTH_CLIENT_ID is set.

func (*IdentityService) Claim added in v0.2.0

Claim binds an unclaimed instance to the caller's domain. The typed hosted_domain must match; a second claim from anyone else is FailedPrecondition.

func (*IdentityService) WhoAmI

WhoAmI reports who the transport says the caller is.

The call is behind the same identity middleware as everything else, and that is the point: a client that gets an answer without presenting a credential knows it is on a tailnet and needs no login step, and a client that gets 401 knows it must supply the dev token. The web UI uses exactly that to decide whether to prompt. When Google sign-in is configured, can_claim tells the UI to show the claim screen instead of the rest of the app.

type Injector

type Injector interface {
	Inject(ctx context.Context, nodeID, taskID, text string) error
	EndWait(ctx context.Context, nodeID, taskID string) error
}

Injector is the node registry's path for a human message into a running task, and for ending a finished task's wait for one.

type NodeAdminService

type NodeAdminService struct {
	// contains filtered or unexported fields
}

NodeAdminService implements podium.v1.NodeAdminService.

func NewNodeAdminService

func NewNodeAdminService(st *store.Store, sessions Sessions, drains Drainer, logger *slog.Logger) *NodeAdminService

NewNodeAdminService returns the node administration API.

func (*NodeAdminService) CreateEnrollmentToken

CreateEnrollmentToken mints a single-use token. The plaintext is returned once and never stored; only its SHA-256 reaches Postgres, so it is never recoverable and never logged.

func (*NodeAdminService) DeleteNode

DeleteNode forgets a node. It refuses one that still holds a stream unless it has been drained, and refuses one that still has tasks on it whatever its state: tasks.node_id is a foreign key, and a task whose node row vanished is a task nobody can explain.

func (*NodeAdminService) DrainNode

DrainNode stops a node being given new work. Whatever it is running finishes, which is the whole point: a drain is how a machine is taken out of service without killing the jobs that are already on it.

The flag is a column, so it survives both daemons restarting and applies to a node that is offline right now — draining a machine before you power it back on is a reasonable thing to want. The live session is told as well, so the scheduler stops considering the node immediately rather than at its next heartbeat.

func (*NodeAdminService) ListNodes

ListNodes returns every enrolled node, oldest first, with live slot counts for the ones that currently hold a stream on this server.

func (*NodeAdminService) RekeyNode

RekeyNode unbinds a node from the Tailscale device it enrolled from. It is what an operator runs when a worker is rebuilt or replaced: the node keeps its ID, its labels and its history, and the next Hello binds it to whatever device it arrives from. Until then the node key alone is enough to connect, which is exactly the exposure the binding removes — so rekey a node when you are about to move it, not as a matter of routine.

func (*NodeAdminService) SetNodeLabels

SetNodeLabels changes what a node is eligible for after it has enrolled. Until this existed the labels a node was given at enrollment were the labels it had for good, and tagging a machine meant editing nodes.labels by hand and restarting the daemon so its session re-read the row.

It adds and removes rather than replacing, because a whole-list write is a race between two operators who each meant to change one label. The live session is retagged too, so the scheduler routes on the new set immediately rather than at the next reconnect.

func (*NodeAdminService) SetNodeSlots

SetNodeSlots changes how many tasks a node runs at once. Zero clears the instruction, and the node goes back to the max_tasks in its own configuration file.

It is stored against the node rather than sent and forgotten, for the same reason a drain is: an operator who caps a machine at two tasks means it for the machine, and a value that evaporated on the next reconnect would be worse than no feature at all. A node that is offline right now is a legitimate thing to configure — every stream is sent the count just after its HelloAck, so the node picks it up on its next connection.

func (*NodeAdminService) UndrainNode

UndrainNode puts a drained node back in the pool.

type PreviewReleaser added in v0.6.0

type PreviewReleaser interface {
	ReleasePreview(ctx context.Context, nodeID, taskID, reason string) error
}

PreviewReleaser is the node registry's path for ending a finished task's preview.

type RegistryService

type RegistryService struct {
	// contains filtered or unexported fields
}

RegistryService implements podium.v1.RegistryService. It is the secret store's second table under the same rules: no read endpoint, any authenticated caller, actor recorded.

func NewRegistryService

func NewRegistryService(svc *secrets.Service, logger *slog.Logger) *RegistryService

NewRegistryService returns the registry API.

func (*RegistryService) DeleteRegistry

DeleteRegistry removes a registry login. The next pull from that host is anonymous.

func (*RegistryService) ListRegistries

ListRegistries returns metadata only: hosts, usernames, key ids and who set them.

func (*RegistryService) SetRegistry

SetRegistry creates or replaces the login for one registry host. The request's password is zeroed before the handler returns.

type SandboxProviderService added in v0.7.0

type SandboxProviderService struct {
	// contains filtered or unexported fields
}

SandboxProviderService implements podium.v1.SandboxProviderService: the secret store's third table, under the registries' rules. A change reaches the provider's nodes at once.

func NewSandboxProviderService added in v0.7.0

func NewSandboxProviderService(svc *secrets.Service, nodes SandboxPusher, logger *slog.Logger) *SandboxProviderService

NewSandboxProviderService returns the sandbox provider API.

func (*SandboxProviderService) DeleteSandboxProvider added in v0.7.0

DeleteSandboxProvider removes a provider; its nodes stop taking work.

func (*SandboxProviderService) ListSandboxProviders added in v0.7.0

ListSandboxProviders returns settings and metadata, never a credential.

func (*SandboxProviderService) SetSandboxProvider added in v0.7.0

SetSandboxProvider creates or replaces a provider. The request's secret is zeroed before the handler returns.

type SandboxPusher added in v0.7.0

type SandboxPusher interface {
	PushSandboxConfig(ctx context.Context, provider string)
	SandboxUsage(ctx context.Context) map[string]nodes.SandboxUsage
}

SandboxPusher re-sends a provider's configuration to its connected nodes, and says how many sandboxes they hold.

type SecretChecker

type SecretChecker interface {
	CheckRefs(ctx context.Context, refs []spec.SecretRef) error
}

SecretChecker answers whether the secrets a spec names exist, without reading their values. It is optional: a server with no master key has no secrets to check against and refuses the task at assignment instead.

type SecretService

type SecretService struct {
	// contains filtered or unexported fields
}

SecretService implements podium.v1.SecretService.

No person can read a value back. A value goes in through SetSecret and comes back out inside an Assign, on its way to the node about to run the task that referenced it, or through ReadSecret to the conductor alone, for its own credentials. A list returns names and metadata: every global secret, plus the caller's own personal secrets, and never another login's.

func NewSecretService

func NewSecretService(svc *secrets.Service, users userLookup, logger *slog.Logger) *SecretService

NewSecretService returns the secret API. users may be nil, in which case a signed-in person cannot write a global secret: the role cannot be checked, so the write is refused.

func (*SecretService) DeleteSecret

DeleteSecret removes a secret the caller is allowed to write. Tasks already assigned keep the copy inside their Assign; the next task that references the name fails to resolve.

func (*SecretService) ListSecrets

ListSecrets returns metadata only: every global name plus the caller's own personal names. A dev token and a node have no personal secrets, so they see globals. The conductor sees globals, and, when it names a login, that login's MCP and model provider credentials.

func (*SecretService) ReadSecret added in v0.6.0

ReadSecret returns one value to the conductor, for the names it keeps for its own use. Every other caller and every other name is refused the same way.

func (*SecretService) SetSecret

SetSecret creates or replaces a secret and returns its metadata. The request's value is zeroed before the handler returns. An unspecified scope is global.

type Sessions

type Sessions interface {
	SnapshotOf(nodeID string) (nodes.Snapshot, bool)
	SetLabels(nodeID string, labels []string) bool
}

Sessions is the live-session half of the node registry.

type SnapshotService added in v0.7.2

type SnapshotService struct {
	// contains filtered or unexported fields
}

SnapshotService implements podium.v1.SnapshotService.

func NewSnapshotService added in v0.7.2

func NewSnapshotService(svc *snapshots.Service, sandbox *sandboxsnaps.Service, logger *slog.Logger) *SnapshotService

NewSnapshotService returns the operator-facing snapshot handlers. sandbox may be nil, and then the sandbox snapshot RPCs list nothing and refuse changes.

func (*SnapshotService) DeleteSandboxSnapshots added in v0.9.0

DeleteSandboxSnapshots deletes the images of the sessions named, one or many.

func (*SnapshotService) DeleteSnapshot added in v0.7.2

DeleteSnapshot removes one session's snapshot, row first and object second.

func (*SnapshotService) ListSandboxSnapshots added in v0.9.0

ListSandboxSnapshots returns every session image a sandbox provider keeps, the policy, and which configured providers keep them.

func (*SnapshotService) ListSnapshots added in v0.7.2

ListSnapshots returns every session snapshot and the retention policy. A server with no object store answers with enabled false rather than an error, so the page can say why.

func (*SnapshotService) SetSandboxSnapshotRetention added in v0.9.0

SetSandboxSnapshotRetention replaces the policy and sweeps once with it.

func (*SnapshotService) SetSnapshotRetention added in v0.7.2

SetSnapshotRetention replaces the policy and sweeps once with it.

type TaskService

type TaskService struct {
	// contains filtered or unexported fields
}

TaskService implements podium.v1.TaskService.

func NewTaskService

func NewTaskService(st *store.Store, events Events, canceller Canceller, checker SecretChecker, logger *slog.Logger) *TaskService

NewTaskService returns the task API. injector may be the same object as canceller (the node registry implements both); nil means InjectTask is unavailable.

func (*TaskService) CancelTask

CancelTask cancels a queued task outright and asks the node to stop anything further along. It never waits for the container to die: with the task command as PID 1 a default-disposition SIGTERM is discarded, so a cancel routinely costs the node's full 30s grace period. The terminal status lands when the exited/finished events arrive.

func (*TaskService) CreateTask

CreateTask validates the spec and queues the task. Scheduling is the scheduler's problem.

func (*TaskService) GetTask

GetTask returns one task.

func (*TaskService) InjectTask

InjectTask delivers one human message into a running task. The container stays up. With end_wait it instead tells a task that has answered to stop waiting for a follow-up.

func (*TaskService) ListTasks

ListTasks pages newest-first. next_cursor is empty on the last page; feed it back verbatim.

func (*TaskService) ReleasePreview added in v0.6.0

ReleasePreview ends a finished task's preview before its ttl does. The row is released first and the node told second, so a node that is away tears it down when it reconnects.

func (*TaskService) SetMaxPreviewTTL added in v0.6.0

func (s *TaskService) SetMaxPreviewTTL(d time.Duration)

SetMaxPreviewTTL overrides DefaultMaxPreviewTTL. Zero or less keeps the default.

func (*TaskService) StreamTaskEvents

StreamTaskEvents replays everything stored after from_seq and then follows the task live. The two storage tables share the node's single per-task seq space, so the merged stream is in strict seq order. It ends when the task is terminal and every event has been sent.

type UserService added in v0.4.0

type UserService struct {
	// contains filtered or unexported fields
}

UserService implements podium.v1.UserService.

func NewUserService added in v0.4.0

func NewUserService(st *store.Store, logger *slog.Logger) *UserService

NewUserService returns the users API.

func (*UserService) ListUsers added in v0.4.0

ListUsers returns every recorded login, most recently seen first.

func (*UserService) SetUserRole added in v0.4.0

SetUserRole replaces one login's role. The last owner cannot be demoted.

Jump to

Keyboard shortcuts

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