Documentation
¶
Overview ¶
Package controlapp implements the portable application behavior behind the frozen public control contract: session lifecycle, environment definition, provider-neutral fleet truth and within-pool scheduling, and authorized terminal attachment with bounded workspace diff/push/pull orchestration. Its canonical import path is github.com/tokencanopy/rainier/controlapp.
The package is a service, not a transport. It depends only on the control ports (github.com/tokencanopy/rainier/control) and the public protocol packages (github.com/tokencanopy/rainier/protocol/{runner,terminal,workspace}). Everything else is an adapter the host injects: HTTP and WebSocket transport, identity, SQL, Docker, GitHub, cloud providers, and billing all live outside this seam and are never imported here, nor is any internal/ package.
Each service is a deep module over narrow ports. SessionService owns session creation, authorization, lifecycle dispatch, and result recording. EnvironmentService owns environment definition and the setup-hash identity of an environment's build inputs. FleetService owns fleet truth and within-pool scheduling, while injected adapters own connections, persistence, eligible-pool policy, sensitive launch material, and provider execution. AttachmentService resolves and authorizes a session, grants the repository's next controller generation, and delegates terminal transport to an AttachmentBroker.
Terminal bytes stay opaque. This package never parses a socket, logs a terminal message, persists terminal bytes, or duplicates the terminal protocol; workspace operations share one private session-RPC implementation over control.RunnerTransport, which is the single downward request/response seam. Terminal and workspace message structs remain the already-public protocol packages; this package references those contracts and never duplicates them.
Every command commits its mutation and the event that describes it in one control.UnitOfWork: the service opens the unit, makes every repository and recorder call on the context it hands back, and wakes the scheduler only after the unit returns, so a host with transactions makes the two writes one commit and a host without them (an in-memory store) runs the closure directly. Placement asks the host's control.CheckpointLocator where an environment's cached checkpoint can boot, prefers those runners, and falls back to any eligible runner with a rebuild from the environment's own image — a cache is a head start, never a pin.
Adapter failures are normalized at every port boundary: portError maps any error a port returns into the closed control sentinel vocabulary, so an adapter's SQL, connection string, or provider text can never leave this package through a control interface.
Index ¶
- Constants
- Variables
- func AgentHomeVolume(ws control.WorkspaceID, creator control.ActorID) string
- func AgentRefusalSentence(err error) (string, bool)
- func AgentsEnv(providers []AgentProvider) map[string]string
- func DefaultDeveloperEgressHosts() []string
- func GitHubGitEgressHosts() []string
- type AgentCredentialOption
- type AgentCredentialService
- func (s *AgentCredentialService) AnswerFetch(ctx context.Context, row control.Session, provider string) (AgentCredentialSet, error)
- func (s *AgentCredentialService) AnswerPut(ctx context.Context, row control.Session, provider string, ...) (uint64, error)
- func (s *AgentCredentialService) List(ctx context.Context, sc control.Scope) ([]AgentCredentialStatus, error)
- func (s *AgentCredentialService) Logout(ctx context.Context, sc control.Scope, provider string) error
- func (s *AgentCredentialService) Withdraw(ctx context.Context, ws control.WorkspaceID, user control.ActorID) error
- type AgentCredentialSet
- type AgentCredentialStatus
- type AgentCredentialStore
- type AgentHomeSeedFile
- type AgentProvider
- type AgentWorkspaceLister
- type AttachmentOptions
- type AttachmentPolicy
- type AttachmentService
- func (s *AttachmentService) AttachTerminal(ctx context.Context, scope control.Scope, cmd control.AttachTerminal, ...) error
- func (s *AttachmentService) PullWorkspace(ctx context.Context, scope control.Scope, cmd control.PullWorkspace) error
- func (s *AttachmentService) PushWorkspace(ctx context.Context, scope control.Scope, cmd control.PushWorkspace) error
- func (s *AttachmentService) WorkspaceDiff(ctx context.Context, scope control.Scope, cmd control.WorkspaceDiff) (workspace.DiffAnswer, error)
- type EgressHost
- type EnvironmentOptions
- type EnvironmentService
- func (s *EnvironmentService) CreateEnvironment(ctx context.Context, scope control.Scope, cmd control.CreateEnvironment) (control.Environment, error)
- func (s *EnvironmentService) DeleteEnvironment(ctx context.Context, scope control.Scope, cmd control.DeleteEnvironment) error
- func (s *EnvironmentService) GetEnvironment(ctx context.Context, scope control.Scope, id control.EnvironmentID) (control.Environment, error)
- func (s *EnvironmentService) ListEnvironments(ctx context.Context, scope control.Scope, q control.EnvironmentQuery) (control.EnvironmentPage, error)
- func (s *EnvironmentService) UpdateEnvironment(ctx context.Context, scope control.Scope, cmd control.UpdateEnvironment) (control.Environment, error)
- type FleetOptions
- type FleetService
- func (s *FleetService) ApplyRunnerEvent(ctx context.Context, event control.RunnerEvent) error
- func (s *FleetService) ListRunners(ctx context.Context, scope control.Scope, q control.RunnerQuery) (control.RunnerPage, error)
- func (s *FleetService) ReconcileRunner(ctx context.Context, snap control.RunnerSnapshot) (control.ReconcileResult, error)
- func (s *FleetService) RegisterRunner(ctx context.Context, r control.RunnerRegistration) (control.RunnerRegistrationResult, error)
- func (s *FleetService) Run(ctx context.Context) error
- func (s *FleetService) Wake(pool control.PoolID)
- type LaunchMaterial
- type LaunchMaterialResolver
- type SessionOptions
- type SessionService
- func (s *SessionService) CreateSession(ctx context.Context, scope control.Scope, cmd control.CreateSession) (control.Session, error)
- func (s *SessionService) DeleteSession(ctx context.Context, scope control.Scope, cmd control.DeleteSession) error
- func (s *SessionService) GetSession(ctx context.Context, scope control.Scope, id control.SessionID) (control.Session, error)
- func (s *SessionService) ListSessions(ctx context.Context, scope control.Scope, q control.SessionQuery) (control.SessionPage, error)
- func (s *SessionService) ResumeSession(ctx context.Context, scope control.Scope, cmd control.ResumeSession) (control.Session, error)
- func (s *SessionService) SnapshotSession(ctx context.Context, scope control.Scope, cmd control.SnapshotSession) (control.Checkpoint, error)
- func (s *SessionService) SuspendSession(ctx context.Context, scope control.Scope, cmd control.SuspendSession) (control.Session, error)
Constants ¶
const ( // EcosystemNPM is the Node package registries the image's npm and yarn // resolve against. EcosystemNPM = "npm" // EcosystemPyPI is the Python index and the file host it links to. EcosystemPyPI = "pypi" // EcosystemGo is the Go module mirror and its checksum database. EcosystemGo = "go" // EcosystemGitHubGit is the source-control half of GitHub: what a clone, // fetch, push and LFS transfer reach. These are the hosts the launch // material has always added for a CLONING session; they are in the // baseline because a session that clones nothing still routinely needs to // read a public dependency that lives in a repository. EcosystemGitHubGit = "github-git" // EcosystemGitHubAPI is api.github.com — the REST and GraphQL surface `gh` // and an agent's tooling use. EcosystemGitHubAPI = "github-api" // EcosystemGitHubContent is GitHub's read-only content hosts: raw files // and published release assets. EcosystemGitHubContent = "github-content" )
Ecosystems named by the baseline. They exist so a caller can filter the table without matching on hostnames, and so a reader can tell a package registry from a source host at a glance.
const AgentCredentialSetMaxBytes = 64 << 10
AgentCredentialSetMaxBytes bounds one put: the sum of every file's bytes and every file's name. It is the same 64 KiB sessiond refuses to send above (plan §Task 3), enforced a second time here because the sandbox is an untrusted peer and a bound only the sender applies is not a bound.
const HomeMountPath = "/rainier/agents"
HomeMountPath is where the agent home lands inside every session: one writable volume per (creator, workspace), with a subdirectory per provider under it. It is outside /workspace deliberately — a credential set must not be something a repository, a diff, a push, or a checkpoint can reach — and outside $HOME, which the read-only rootfs owns.
Variables ¶
var ( // ErrAgentSessionHasNoCreator refuses a session row that names nobody. // The creator IS the authority a fetch acts with, and a lookup for the // empty user is one stray row away from handing a sandbox a credential // nobody granted it — the same reasoning the git mint's ownerless // refusal is written on. ErrAgentSessionHasNoCreator error = &agentRefusal{ "this session has no creator to fetch an agent credential for", control.ErrInvalid} // ErrUnknownAgentProvider refuses a provider that is not a row of // AgentProviders(). Custody stores what the table names and nothing else, // so an unknown name is refused rather than stored under. ErrUnknownAgentProvider error = &agentRefusal{ "unknown agent provider", control.ErrInvalid} // ErrAgentMembershipGone is what a denied authorization becomes. It says // membership and not "denied" because that is what a denial of // ActionAttach on your own session in your own workspace actually means: // you are no longer a member of it. ErrAgentMembershipGone error = &agentRefusal{ "your workspace membership no longer allows this", control.ErrDenied} // ErrAgentCredentialTooLarge refuses a put over AgentCredentialSetMaxBytes. ErrAgentCredentialTooLarge error = &agentRefusal{ "the agent credential set is too large", control.ErrInvalid} // ErrAgentFileNotAllowed refuses a file name that is not a bare name on // the provider's own allowlist. The allowlist is what makes the whole // scheme mechanical — it is what the sync reads, what a revoke deletes, // and what a checkpoint excludes — so a name outside it is refused here // rather than written into somebody's agent home. ErrAgentFileNotAllowed error = &agentRefusal{ "that file is not part of this agent's credential set", control.ErrInvalid} // ErrAgentCredentialUnreadable is every failure of the store on the way // out, flattened. A store's own text may name a row, a DSN, or a column, // and none of that is something a sandbox may learn. ErrAgentCredentialUnreadable error = &agentRefusal{ "the agent credential could not be read", control.ErrUnavailable} // ErrAgentCredentialUnwritable is the same flattening on the way in. ErrAgentCredentialUnwritable error = &agentRefusal{ "the agent credential could not be stored", control.ErrUnavailable} // ErrAgentCredentialStale refuses a put that began before logout advanced // custody's revoke fence. ErrAgentCredentialStale error = &agentRefusal{ "the agent credential changed after this session last read it", control.ErrConflict} // ErrAgentCredentialNotYours refuses a logout by anyone but the account. // A credential set is not the workspace's property, so no role over the // workspace reaches it — an owner and an admin get this too. ErrAgentCredentialNotYours error = &agentRefusal{ "an agent credential belongs to the person who logged in, not to a workspace role", control.ErrDenied} )
The complete refusal vocabulary of AnswerFetch and AnswerPut. Every failure those two can produce is one of these; there is no path on which a store's error, a provider's response, or a sandbox's own words reach the wire.
var EnableTestAgentProvider bool
EnableTestAgentProvider adds the synthetic "test" provider to the table. It is a package-level switch a host sets ONCE at startup, before it serves anything, and only the end-to-end suite's controld sets it: the row's login command writes a fixture into its own file so the whole credential path — mount, fetch, put, revoke — can be proven on a fleet without a real account and without a real credential ever existing. It defaults to off so a production control plane cannot be talked into offering it.
var ErrRunnerRefused = fmt.Errorf("controlapp: runner refused the command: %w", control.ErrUnavailable)
ErrRunnerRefused is the ErrUnavailable a runner ANSWERED with: it received the command, ran it, and reported failure. It wraps control.ErrUnavailable, so every caller that knows only the closed sentinel set keeps working unchanged — and a host that wants to tell "the runner said no" apart from "the runner said nothing" can ask, which is the difference between an internal failure and an unreachable dependency on the wire.
It carries no detail of its own. The runner's failure text stays inside the transport (control/errors.go): a provider-neutral service does not relay a sandbox's words, and this sentinel is the whole answer.
Functions ¶
func AgentHomeVolume ¶ added in v0.0.3
func AgentHomeVolume(ws control.WorkspaceID, creator control.ActorID) string
AgentHomeVolume is the name of the volume that holds one person's agent homes in one workspace. It is a hash, not a composed identifier, for a reason that has nothing to do with length: `docker volume ls` on a runner is readable by anyone who can reach the host, and an account id printed there is a disclosure the mount does not need to make. Stability is the other half — the same pair must resolve to the same volume on every boot, forever, or "log in once" quietly becomes "log in once per session".
The NUL between the two ids keeps ("ws_a", "bc") and ("ws_ab", "c") apart; without it two people could be handed one credential set.
func AgentRefusalSentence ¶ added in v0.0.3
AgentRefusalSentence returns the fixed user-facing sentence err carries, and false for anything else. A host relaying a custody refusal into a sandbox uses it so that ONLY these sentences can ever be relayed: an error this service did not author renders as the host's own flat sentence instead of leaking whatever text it happens to hold.
func AgentsEnv ¶ added in v0.0.3
func AgentsEnv(providers []AgentProvider) map[string]string
AgentsEnv is the environment every session gets so that the agents inside it find their homes: each provider's own variable pointed at its subdirectory, plus the manifest sessiond reads to know what to fetch, write, watch, and delete. It carries no credential — it is a map of paths — which is why it can sit in the container environment at all.
The scheduler reserves these keys when it merges resolved launch material: provider homes and the manifest are credential-custody boundaries, not workspace-configurable environment defaults.
func DefaultDeveloperEgressHosts ¶ added in v0.0.10
func DefaultDeveloperEgressHosts() []string
DefaultDeveloperEgressHosts returns just the hostnames, in table order. This is what the scheduler unions into a dispatched session's allowlist.
func GitHubGitEgressHosts ¶ added in v0.0.10
func GitHubGitEgressHosts() []string
GitHubGitEgressHosts returns the source-control hosts a clone, fetch, push and LFS transfer reach, in table order. A launch resolver adds these for a cloning session even where the baseline is switched off, because they are not a convenience there: the connector said "acme/app", not three host names, and a clone the caller asked for must not fail on a host the caller had no way to know about.
Types ¶
type AgentCredentialOption ¶ added in v0.0.3
type AgentCredentialOption func(*AgentCredentialService)
AgentCredentialOption configures the two things the four constructor arguments cannot carry: the host's placement context, and its membership index. Both default to the narrowest honest behavior, so a host that supplies neither still gets a correct service.
func WithAgentPlacement ¶ added in v0.0.3
func WithAgentPlacement(p control.PlacementScope) AgentCredentialOption
WithAgentPlacement supplies the placement context the service stamps on the scope it authorizes a sandbox's request in. A sandbox-initiated request arrives with no scope of its own — the row and the runner are all there is — so the service builds the creator's scope, and the placement half of it is knowledge only the host has. Without this option the constructed scope carries no placement, which the self-hosted authorizer ignores and a hosted one would refuse; every host that has a placement should pass it.
func WithAgentWorkspaces ¶ added in v0.0.3
func WithAgentWorkspaces(l AgentWorkspaceLister) AgentCredentialOption
WithAgentWorkspaces supplies the membership index Logout sweeps. See AgentWorkspaceLister for why it is not a constructor argument.
type AgentCredentialService ¶ added in v0.0.3
type AgentCredentialService struct {
// contains filtered or unexported fields
}
AgentCredentialService is custody's behavior: the two answers a sandbox's upward RPC gets, the logout a person performs, the withdrawal a lost membership triggers, and the listing the API renders. It owns the authorization for both hosts — a hosted gateway and self-hosted controld answer the same RPC through this same service — so a rule added here is added for both at once.
func NewAgentCredentialService ¶ added in v0.0.3
func NewAgentCredentialService(store AgentCredentialStore, authz control.Authorizer, sessions control.SessionRepository, rpc *AttachmentService, opts ...AgentCredentialOption) *AgentCredentialService
NewAgentCredentialService builds the service over its four dependencies: the custody store, the host's current authorization authority, the session repository the downward sweep reads, and the attachment service whose sessionRPC carries a revoke into a running sandbox.
It returns no error, unlike its three siblings, because it holds no defaults to validate and no goroutine to start. A nil store or authorizer is a composition bug rather than a runtime condition, and every method refuses with control.ErrInvalid rather than panicking on one.
func (*AgentCredentialService) AnswerFetch ¶ added in v0.0.3
func (s *AgentCredentialService) AnswerFetch(ctx context.Context, row control.Session, provider string) (AgentCredentialSet, error)
AnswerFetch answers one sandbox's boot-time request for its creator's credential set. Every check that makes the answer safe happens here, in this order: the row names somebody, the provider is one this build knows, and that somebody may still attach to this session in this workspace.
The membership re-check is the point of the whole method. The host's SessionRequest guard has already established that the ASKING RUNNER is the row's runner (internal/controld/srpc.go does; a hosted gateway does), so what is left to establish is that the PERSON is still entitled — and "the creator may still attach to their own session in this workspace" is exactly "the creator is a current member of this workspace", asked with an action the frozen contract already has.
func (*AgentCredentialService) AnswerPut ¶ added in v0.0.3
func (s *AgentCredentialService) AnswerPut(ctx context.Context, row control.Session, provider string, files map[string][]byte, expected uint64) (uint64, error)
AnswerPut records the set a sandbox observed on disk and returns the version custody assigned it. The authorization is the fetch's, and then two bounds the sandbox cannot be trusted to have applied itself: the 64 KiB cap and the provider's file allowlist.
The wire's put also carries the version the sandbox last saw (runner.MethodPutAgentCredentials). Custody uses it only at a revoke boundary: ordinary concurrent writes remain last-writer-wins, while a put older than a logout tombstone is refused. Nothing is answered before the store has accepted the write.
func (*AgentCredentialService) List ¶ added in v0.0.3
func (s *AgentCredentialService) List(ctx context.Context, sc control.Scope) ([]AgentCredentialStatus, error)
List returns one status per set the caller holds, in the store's order and carrying no bytes. Like Logout it answers about the CALLER and nobody else.
func (*AgentCredentialService) Logout ¶ added in v0.0.3
func (s *AgentCredentialService) Logout(ctx context.Context, sc control.Scope, provider string) error
Logout replaces the caller's own set with a versioned tombstone and then tells every running sandbox of theirs to forget the copy it holds.
There is no "other user" parameter, and that is the access control: a workspace owner and an admin have no path to somebody else's set, because the method cannot express one. What is left to check is that the caller is a PERSON — a service principal acts for background work, and background work does not own a subscription login.
The store revoke happens first and is the operation's result. The downward sends are best effort and cannot fail it: a session that is gone, wedged, or on a disconnected runner has not kept a credential the control plane still honors, and refusing the logout over it would leave the person unable to log out at all.
func (*AgentCredentialService) Withdraw ¶ added in v0.0.3
func (s *AgentCredentialService) Withdraw(ctx context.Context, ws control.WorkspaceID, user control.ActorID) error
Withdraw tells the user's running sandboxes IN ONE WORKSPACE to forget every provider's credential set, and leaves custody untouched. It is what a lost membership means: the person still owns their login — they may still have it in another workspace, and they will have it again if they are re-added — but nothing running in this workspace may keep holding it.
The asymmetry with Logout is the whole design: Logout destroys and sweeps, Withdraw only sweeps.
type AgentCredentialSet ¶ added in v0.0.3
AgentCredentialSet is one provider's credential files for one person, plus the version custody has assigned them. Version 0 with no files is the truthful answer for a person who has not logged that agent in. A positive version with no files is a revoke tombstone. Both are answers, not refusals.
type AgentCredentialStatus ¶ added in v0.0.3
AgentCredentialStatus is everything about a stored set EXCEPT the set: the provider it belongs to, the version, and when it last moved. Revoked tombstones are not listed. Listing answers with these, so no listing path can return a credential byte — AgentCredentialStore.ListAgentCredentials has no way to express one.
type AgentCredentialStore ¶ added in v0.0.3
type AgentCredentialStore interface {
// FetchAgentCredentials returns the stored set. A set that was never put is
// version 0 with no files; a revoked set is its positive tombstone version
// with no files. Both are answers rather than failures.
FetchAgentCredentials(ctx context.Context, user control.ActorID, provider string) (AgentCredentialSet, error)
// PutAgentCredentials replaces the set with files and returns the new
// version, which is one more than the version it replaced (1 for a first
// put). expected is the version the sandbox last saw. Ordinary concurrent
// writes remain last-writer-wins, but a version older than a revoke
// tombstone is control.ErrConflict and cannot resurrect a logged-out set.
PutAgentCredentials(ctx context.Context, user control.ActorID, provider string, files map[string][]byte, expected uint64) (uint64, error)
// RevokeAgentCredentials replaces the set with a versioned tombstone and
// returns its version. It is idempotent in outcome, while each call still
// advances the fence so every earlier in-flight put is stale.
RevokeAgentCredentials(ctx context.Context, user control.ActorID, provider string) (uint64, error)
// ConditionalRevokeAgentCredentials is a session-originated deletion. It
// atomically rejects expected versions older than the durable logout fence.
ConditionalRevokeAgentCredentials(ctx context.Context, user control.ActorID, provider string, expected uint64) (uint64, error)
// ListAgentCredentials returns one status per provider the user has a set
// for, and no bytes.
ListAgentCredentials(ctx context.Context, user control.ActorID) ([]AgentCredentialStatus, error)
}
AgentCredentialStore is custody's persistence port. It is keyed by (user, provider) and knows nothing about workspaces or sessions: the projection of one person's login into every workspace they belong to is the service's doing, above this line.
The port takes and returns PLAINTEXT file maps and the implementation seals, because sealing is the host's: the self-hosted store seals under the fleet key, a hosted one under its KMS, and neither wants the other's ciphertext format in this contract. Implementations must therefore treat every map that crosses this interface as secret material.
controlapp/repotest.RunAgentCredentialStore is the executable contract; these comments say what the behavior is, and that suite is where it is pinned.
type AgentHomeSeedFile ¶ added in v0.0.9
AgentProvider is one coding agent's row: everything Rainier needs to know about it and nothing more. The table is data, not code — adding a third agent is a row plus the probes that fill it in — and it is the ONLY place in this repository where a provider is named. sessiond, the driver, runnerd, the stores, and the RPC carry the strings and never spell one.
Each field answers one question the probes had to settle:
- HomeEnv is the variable the agent already honors to move its configuration directory. Pointing it at the home is the entire integration: the tool is unmodified and does not know it is in Rainier.
- Files is the allowlist of credential-bearing file names inside the provider's directory. Bare file names, never paths: this list is what the sync reads, what a revoke deletes, and what a checkpoint excludes, so a row that could name "../../workspace/x" would be a hole in all three at once.
- SeedFiles is non-secret state to create only when custody restored a credential into a fresh home. It is not synchronized or revoked, and it never overwrites workspace-local state already on the volume.
- Egress is the hosts the agent's login, token refresh, and inference reach, read off the egress proxy's log during the probes. Apex names are literal: an entry is not a wildcard pattern.
- LoginCmd is the command `rainier agent login` runs in a throwaway session so the person completes the tool's own login flow — unmodified, with no credential pasted anywhere.
AgentHomeSeedFile is non-secret provider state that makes a restored credential usable in a fresh home. It is created only when absent and never joins account-wide credential custody.
type AgentProvider ¶ added in v0.0.3
type AgentProvider struct {
Name string
HomeEnv string
Files []string
SeedFiles []AgentHomeSeedFile
Egress []string
LoginCmd []string
}
func AgentProviders ¶ added in v0.0.3
func AgentProviders() []AgentProvider
AgentProviders returns the provider table, in the order a manifest and an egress list present it. Every call builds a fresh slice with fresh inner slices: the table is handed to callers that sort, append to, and merge it, and none of them may change what the next caller sees.
type AgentWorkspaceLister ¶ added in v0.0.3
type AgentWorkspaceLister interface {
AgentWorkspaces(ctx context.Context, user control.ActorID) ([]control.WorkspaceID, error)
}
AgentWorkspaceLister answers which workspaces a person is a member of. It is optional, and it exists because control.SessionRepository has no cross-workspace scan: every one of its methods is keyed by workspace, by design. Without it Logout can only sweep the scope's own workspace — which is the whole truth for a self-hosted installation, since that installation IS one workspace — and a host with many workspaces supplies one so that "logged out everywhere" means everywhere.
type AttachmentOptions ¶
type AttachmentOptions struct {
Authorizer control.Authorizer
Policy AttachmentPolicy
Sessions control.SessionRepository
Transport control.RunnerTransport
Broker control.AttachmentBroker
Events control.EventRecorder
Clock control.Clock
IDs control.IDGenerator
// UnitOfWork is the host's atomicity: an attachment's event is a write
// like any other and commits inside one Run. The effect it describes is a
// transport call rather than a store write, so it stays outside the unit
// — the unit holds every store write the operation makes, which here is
// the event alone.
UnitOfWork control.UnitOfWork
// MaxTransferBytes bounds one push or pull's compressed bytes. Zero means
// workspace.MaxBytes; a negative value is control.ErrInvalid. Hosts lower
// it in tests so the overrun path is exercised without streaming the full
// limit, and a host relaying transfers through its own memory lowers it in
// production for the same reason it bounds any other relay.
MaxTransferBytes int64
}
AttachmentOptions carries the host-supplied dependencies the attachment service needs. Each is a narrow control port; there is no catch-all host or backend store. A nil Policy is rejected: the mode-aware attachment policy is a real seam the self-hosted and Cloud composers map to their own current collaboration rules.
type AttachmentPolicy ¶
type AttachmentPolicy interface {
AuthorizeAttachment(context.Context, control.Scope, control.Resource, control.AttachmentMode) error
}
AttachmentPolicy is the mode-aware attachment authorization seam. The frozen control.ActionAttach value alone cannot distinguish a view grant from a control grant, so after the generic Authorizer permits ActionAttach the service asks this policy whether the validated mode is also permitted for the same authoritative resource. Self-hosted maps it to creator/installation policy; Cloud maps it to current session collaboration grants.
type AttachmentService ¶
type AttachmentService struct {
// contains filtered or unexported fields
}
AttachmentService implements control.Attachments: AttachTerminal and the three bounded workspace operations. It owns the correlation counter, the controller-generation fence, and every authorization/readiness/bound check before a byte crosses the broker, runner, reader, or writer seam.
func NewAttachmentService ¶
func NewAttachmentService(opts AttachmentOptions) (*AttachmentService, error)
NewAttachmentService builds an AttachmentService, rejecting any missing dependency with control.ErrInvalid. It holds no lease table and starts no goroutine: the controller generation is the session repository's, so it survives a restart and is shared by every replica over the same store.
func (*AttachmentService) AttachTerminal ¶
func (s *AttachmentService) AttachTerminal(ctx context.Context, scope control.Scope, cmd control.AttachTerminal, stream control.TerminalStream) error
AttachTerminal authorizes and fences one terminal attach, then hands the stream to the broker. Terminal messages stay opaque: the module never reads, logs, or persists a stream message.
func (*AttachmentService) PullWorkspace ¶
func (s *AttachmentService) PullWorkspace(ctx context.Context, scope control.Scope, cmd control.PullWorkspace) error
PullWorkspace streams the gzipped tar archive of Path out of the session's workspace into Body, one bounded chunk per RPC, counting the compressed bytes as they arrive and refusing to write past workspace.MaxBytes.
func (*AttachmentService) PushWorkspace ¶
func (s *AttachmentService) PushWorkspace(ctx context.Context, scope control.Scope, cmd control.PushWorkspace) error
PushWorkspace streams the gzipped tar archive at Body into Path inside the session's workspace, one bounded chunk per RPC, without buffering the whole archive. The total compressed bytes (against the injected transfer bound) and every chunk are bounded before any byte crosses the runner seam.
func (*AttachmentService) WorkspaceDiff ¶
func (s *AttachmentService) WorkspaceDiff(ctx context.Context, scope control.Scope, cmd control.WorkspaceDiff) (workspace.DiffAnswer, error)
WorkspaceDiff asks a running sandbox for its per-repository diff and bounds the untrusted answer before returning it.
type EgressHost ¶ added in v0.0.10
EgressHost is one row of the baseline: the literal host, the ecosystem that reaches it, and the reason it is there. The reason is not decoration — it is what a reviewer checks a proposed row against, and what a removal has to falsify.
func DefaultDeveloperEgress ¶ added in v0.0.10
func DefaultDeveloperEgress() []EgressHost
DefaultDeveloperEgress returns a copy of the baseline table.
type EnvironmentOptions ¶
type EnvironmentOptions struct {
Authorizer control.Authorizer
Environments control.EnvironmentRepository
Events control.EventRecorder
Clock control.Clock
IDs control.IDGenerator
// UnitOfWork is the host's atomicity, used for the same reason
// SessionOptions uses one: an environment write and its event are one
// fact, and they commit together or not at all.
UnitOfWork control.UnitOfWork
}
EnvironmentOptions carries the host-supplied dependencies of EnvironmentService. Every field is required.
type EnvironmentService ¶
type EnvironmentService struct {
// contains filtered or unexported fields
}
EnvironmentService owns reusable environment CRUD and setup-checkpoint invalidation behind the frozen control.Environments interface.
func NewEnvironmentService ¶
func NewEnvironmentService(o EnvironmentOptions) (*EnvironmentService, error)
NewEnvironmentService returns ErrInvalid for a missing dependency.
func (*EnvironmentService) CreateEnvironment ¶
func (s *EnvironmentService) CreateEnvironment(ctx context.Context, scope control.Scope, cmd control.CreateEnvironment) (control.Environment, error)
CreateEnvironment validates every scalar bound before generating an ID, authorizes the create, copies every input, computes SetupHash, stores, records, and returns a copy.
func (*EnvironmentService) DeleteEnvironment ¶
func (s *EnvironmentService) DeleteEnvironment(ctx context.Context, scope control.Scope, cmd control.DeleteEnvironment) error
DeleteEnvironment reads and authorizes the environment, then refuses the delete while any non-terminal session still references it. A repository ErrNotFound remains ErrNotFound even if another workspace holds the same opaque ID.
func (*EnvironmentService) GetEnvironment ¶
func (s *EnvironmentService) GetEnvironment(ctx context.Context, scope control.Scope, id control.EnvironmentID) (control.Environment, error)
GetEnvironment reads by (workspace, id), authorizes ActionGet, and returns a copy. A row outside the authoritative workspace is ErrNotFound.
func (*EnvironmentService) ListEnvironments ¶
func (s *EnvironmentService) ListEnvironments(ctx context.Context, scope control.Scope, q control.EnvironmentQuery) (control.EnvironmentPage, error)
ListEnvironments authorizes before repository access and normalizes an empty page to a non-nil empty slice.
func (*EnvironmentService) UpdateEnvironment ¶
func (s *EnvironmentService) UpdateEnvironment(ctx context.Context, scope control.Scope, cmd control.UpdateEnvironment) (control.Environment, error)
UpdateEnvironment reads the current row, authorizes ActionUpdate, applies only non-nil fields, validates the complete result, recomputes SetupHash, and preserves the old Snapshot and SnapshotHash so a hash change leaves the snapshot visibly stale rather than erased.
type FleetOptions ¶
type FleetOptions struct {
Authorizer control.Authorizer
Sessions control.SessionRepository
Environments control.EnvironmentRepository
Fleet control.FleetRepository
Pools control.PoolResolver
Transport control.RunnerTransport
Events control.EventRecorder
Clock control.Clock
IDs control.IDGenerator
SafetyInterval time.Duration
// LaunchMaterial resolves sensitive launch material (repositories, git
// attribution, secret environment) at dispatch time. It is the one real
// adapter seam this extraction introduces.
LaunchMaterial LaunchMaterialResolver
// DefaultSetupTimeoutSec and DefaultInitTimeoutSec bound a setup or init
// hook whose environment declares no bound of its own (zero or negative).
// They are host policy the application only applies; zero leaves such a
// hook unbounded here and defers to the runner's own default.
DefaultSetupTimeoutSec int
DefaultInitTimeoutSec int
// UnitOfWork is the host's atomicity: a runner event's transition and the
// event recording it are one fact, committed together by one Run. Wake
// follows the commit, never precedes it.
UnitOfWork control.UnitOfWork
// Checkpoints is the host's knowledge of where a checkpoint can boot. The
// scheduler prefers a runner that admits one and falls back to a rebuild
// elsewhere; nothing in this task asks it yet.
Checkpoints control.CheckpointLocator
// DefaultEgress replaces the built-in developer egress baseline
// (DefaultDeveloperEgressHosts) that every dispatched session's allowlist
// is unioned with. It is a POINTER because "leave it alone" and "make it
// empty" are different instructions and an operator needs both: nil takes
// the baseline, a non-nil empty slice turns it off entirely and returns
// the fleet to declare-every-host behaviour, and a non-nil non-empty slice
// is a host's own baseline in place of this one.
//
// It is host policy, not workspace configuration: there is no API field
// and no environment column behind it, so a tenant cannot widen its own
// default. Whatever it holds is ADDITIVE at dispatch and is never written
// to a session row — the row keeps what its caller or environment
// declared, exactly as it did before.
DefaultEgress *[]string
}
FleetOptions carries the host-supplied ports the fleet service depends on. Every port is required; NewFleetService rejects a missing port or a non-positive safety interval.
type FleetService ¶
type FleetService struct {
// contains filtered or unexported fields
}
FleetService implements control.Fleet and owns runner registration, generation-fenced reconciliation, stale-safe event application, and within-pool FIFO scheduling.
func NewFleetService ¶
func NewFleetService(opts FleetOptions) (*FleetService, error)
NewFleetService builds a FleetService. It requires every port and a positive safety interval. No goroutine starts in the constructor.
func (*FleetService) ApplyRunnerEvent ¶
func (s *FleetService) ApplyRunnerEvent(ctx context.Context, event control.RunnerEvent) error
ApplyRunnerEvent applies one unsolicited runner lifecycle report. Identity and generation are fenced before any state logic or event record; a mismatch available in this contract returns ErrStale with no effects. The accepted mutation and the event describing it commit in one unit of work, and the pool is woken only after that unit commits.
func (*FleetService) ListRunners ¶
func (s *FleetService) ListRunners(ctx context.Context, scope control.Scope, q control.RunnerQuery) (control.RunnerPage, error)
ListRunners is the ordinary scoped query half of the fleet contract. It authorizes first, asks the pool resolver for every pool visible to the scope, merges those pools' runners, and returns them copied and sorted by (RunnerID, PoolID) with cursor pagination. No provider filter exists.
func (*FleetService) ReconcileRunner ¶
func (s *FleetService) ReconcileRunner(ctx context.Context, snap control.RunnerSnapshot) (control.ReconcileResult, error)
ReconcileRunner makes the store agree with a runner's authoritative current-state report. A snapshot older than the store's runner generation is fenced without any session read; a newer one first records the new authoritative generation; the result's generation is always the store-authoritative one, never merely the generation the caller sent.
func (*FleetService) RegisterRunner ¶
func (s *FleetService) RegisterRunner(ctx context.Context, r control.RunnerRegistration) (control.RunnerRegistrationResult, error)
RegisterRunner accepts a runner's registration claim. A claim older than the store-authoritative generation is refused without touching the store; a claim at or above it is converted into a copied Runner marked connected and upserted. The claimed session list is deliberately not applied here: ReconcileRunner owns that behavior and may run immediately after.
func (*FleetService) Run ¶
func (s *FleetService) Run(ctx context.Context) error
Run hosts the single scheduler loop: it wakes on explicit wake calls and on a safety tick that re-drains every pool observed through an accepted registration or a wake. It returns when ctx is done.
func (*FleetService) Wake ¶
func (s *FleetService) Wake(pool control.PoolID)
Wake requests a placement pass for pool. It never blocks: when the wake buffer is full the request is dropped, because the safety tick re-drains every known pool anyway.
type LaunchMaterial ¶
type LaunchMaterial struct {
Repos []runner.RepoSpec
GitAuthorName string
GitAuthorEmail string
Environment map[string]string
// EgressAllow lists hosts the resolved material needs reachable — for
// example the source-control hosts Repos clone from. createSpec unions
// them into the session's egress list, in order, without duplicates.
EgressAllow []string
}
LaunchMaterial is the sensitive, in-memory-only material a create needs that the scheduler must never persist: the resolved repositories, the git author identity, and the secret environment. It exists only between its resolver and the runner.ToRunner it is copied into.
type LaunchMaterialResolver ¶
type LaunchMaterialResolver interface {
ResolveLaunchMaterial(context.Context, control.Session, *control.Environment) (LaunchMaterial, error)
}
LaunchMaterialResolver is the one real adapter seam this extraction introduces: self-hosted and Cloud resolve secret values and source-control attribution differently. It returns in-memory material only; values are never stored, logged, included in errors, events, or test output.
type SessionOptions ¶
type SessionOptions struct {
Authorizer control.Authorizer
Sessions control.SessionRepository
Environments control.EnvironmentRepository
Pools control.PoolResolver
Events control.EventRecorder
Clock control.Clock
IDs control.IDGenerator
Wake func(control.PoolID)
Fleet control.FleetRepository
Transport control.RunnerTransport
// UnitOfWork is the host's atomicity: every command's mutation and the
// event describing it run inside one Run, so they commit together or not
// at all. Wake is called only after that unit commits.
UnitOfWork control.UnitOfWork
}
SessionOptions carries the host-supplied dependencies of SessionService. Every field is required; NewSessionService refuses a missing dependency with control.ErrInvalid. Wake is called only after a queued session is durably stored, so the scheduler never learns about a session that did not commit.
type SessionService ¶
type SessionService struct {
// contains filtered or unexported fields
}
SessionService owns session creation, authorization, lifecycle dispatch, and result recording. It is independently constructible so it compiles before the aggregate control.Application composer exists.
func NewSessionService ¶
func NewSessionService(o SessionOptions) (*SessionService, error)
NewSessionService validates that every dependency is present and returns a SessionService with private fields only.
func (*SessionService) CreateSession ¶
func (s *SessionService) CreateSession(ctx context.Context, scope control.Scope, cmd control.CreateSession) (control.Session, error)
CreateSession authorizes the create, replays a matching idempotency key when one exists, resolves the environment within the authoritative workspace, selects the eligible pool with the most free capacity, and stores the queued row and its event in one unit of work, waking the chosen pool only once that unit has committed.
func (*SessionService) DeleteSession ¶
func (s *SessionService) DeleteSession(ctx context.Context, scope control.Scope, cmd control.DeleteSession) error
DeleteSession applies the guarded delete state machine: queued cancels outright, creating is refused, a placed session is destroyed on its runner then destroyed in the store, a failed session is destroyed the same way, and any other terminal state is row-idempotent. Every success wakes the row's pool.
func (*SessionService) GetSession ¶
func (s *SessionService) GetSession(ctx context.Context, scope control.Scope, id control.SessionID) (control.Session, error)
GetSession reads the workspace-keyed row, authorizes it, and only then returns a copy. A row outside the authoritative workspace is ErrNotFound, never ErrDenied.
func (*SessionService) ListSessions ¶
func (s *SessionService) ListSessions(ctx context.Context, scope control.Scope, q control.SessionQuery) (control.SessionPage, error)
ListSessions authorizes the list against a workspace-scoped empty-ID session resource before touching the repository, then returns copied rows. An empty page is a non-nil empty slice.
func (*SessionService) ResumeSession ¶
func (s *SessionService) ResumeSession(ctx context.Context, scope control.Scope, cmd control.ResumeSession) (control.Session, error)
ResumeSession accepts only a warm/cold suspended session. A cold resume must fit on the runner already holding the volume; dispatch precedes the guarded transition back to running. A cold resume is a placement: it names the session's runner again so the repository opens a new generation for the sandbox it starts; a warm resume unpauses the sandbox it has.
func (*SessionService) SnapshotSession ¶
func (s *SessionService) SnapshotSession(ctx context.Context, scope control.Scope, cmd control.SnapshotSession) (control.Checkpoint, error)
SnapshotSession allows snapshots only from running, warm-suspended, or cold-suspended sessions and returns a portable checkpoint. The opaque reference never enters a returned error or an event fact.
func (*SessionService) SuspendSession ¶
func (s *SessionService) SuspendSession(ctx context.Context, scope control.Scope, cmd control.SuspendSession) (control.Session, error)
SuspendSession accepts only a running session, dispatches suspend before the guarded transition to warm/cold, records the event, and returns the authoritative persisted row.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package repotest is the executable contract of the three control repository ports: control.SessionRepository, control.EnvironmentRepository, and control.FleetRepository.
|
Package repotest is the executable contract of the three control repository ports: control.SessionRepository, control.EnvironmentRepository, and control.FleetRepository. |