bay

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package bay is the orchestration layer: it turns "make me a bay" into a worktree, a port block, a set of containers, and a hostname, and turns "remove it" back into nothing.

The pieces below it each do one thing and are independently testable. This package exists because creating and destroying them has to be atomic: a worktree without containers is a checkout nobody asked for, containers without a worktree are pointing at a directory that no longer exists, and a port block without either is a leak that is invisible until allocation starts failing.

Index

Constants

View Source
const DefaultMaxResident = 5

DefaultMaxResident is how many bays devbay will leave running at once.

Five, because that is the number the whole design was sized against: five bays of a real application, on one developer's machine, without the machine becoming unusable. It is a default rather than a limit -- a developer who wants eight can say so -- and it is enforced by cooling, never by refusing.

View Source
const MaxAlias = 12

MaxAlias is the longest useful label. Past this a browser tab truncates it and the label stops doing its job.

Variables

View Source
var ManifestNames = []string{"devbay.yaml", "devbay.yml"}

ManifestNames are the files a bay is described by, in precedence order.

Functions

func AwaitingApproval added in v0.2.0

func AwaitingApproval(err error) bool

AwaitingApproval reports whether an error is a pending human decision.

func DeriveAlias

func DeriveAlias(branch string) string

DeriveAlias turns a branch name into a short label.

Agents produce branch names like feat/refactor-auth-middleware-to-support-refresh-token-rotation, which is useless in a tab strip or an `ls` column. The meaningful head of the name is almost always enough to tell two bays apart.

func EnvName

func EnvName(ref string) string

EnvName is the environment variable a reference falls back to: "stripe/test" becomes DEVBAY_SECRET_STRIPE_TEST.

Types

type Bay

type Bay struct {
	Name   string
	Branch string
	// Alias is the short human label. Agents generate branch names far too
	// long to read in a tab strip, so a bay carries a separate name capped
	// short enough to survive being truncated.
	Alias string

	Worktree string
	Adopted  bool

	Manifest *manifest.Manifest
	Engine   *engine.Engine
}

Bay is one running instance of a repository.

type BootError added in v0.4.0

type BootError struct {
	Bay string
	Err error
}

BootError says a bay exists but something in it did not come up.

A distinct type because the two outcomes need different handling and used to be conflated: "the bay could not be created" is a dead end, while "the bay is running with one broken service" is the ordinary state of a stack somebody is still working on. The bay it names is real -- it will appear in `devbay ls`, its logs can be read, its healthy services serve, and `devbay rm` removes it.

func Degraded added in v0.4.0

func Degraded(err error) (*BootError, bool)

Degraded reports whether err is a bay that exists with something broken in it.

func (*BootError) Error added in v0.4.0

func (e *BootError) Error() string

func (*BootError) Unwrap added in v0.4.0

func (e *BootError) Unwrap() error

type Booter

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

Booter boots a candidate manifest in a throwaway bay and tears it down.

The bay is disposable on purpose. Verification exists to answer one question -- does this configuration actually work -- and answering it by leaving containers, volumes and a port block behind would make `devbay init` a command that quietly costs resources every time it is run.

func (*Booter) Boot

func (b *Booter) Boot(ctx context.Context, m *manifest.Manifest) *verify.Failure

Boot brings a candidate up and reports the first thing that went wrong.

The failure carries the service and its own logs, because a patcher handed "boot failed" can do nothing useful with it, and the cause is nearly always in what the container printed: a missing variable, a refused connection, a migration that has not run.

type CreateOptions

type CreateOptions struct {
	Name   string
	Branch string
	From   string
	Alias  string
	// Boot brings the services up. When false the worktree is prepared and
	// the manifest validated, but nothing is started.
	Boot bool
}

CreateOptions describe a bay to create.

type Info

type Info struct {
	Name     string            `json:"name"`
	Alias    string            `json:"alias"`
	Branch   string            `json:"branch"`
	State    string            `json:"state"`
	Worktree string            `json:"worktree"`
	Adopted  bool              `json:"adopted,omitempty"`
	URLs     map[string]string `json:"urls,omitempty"`
	Ports    map[string]int    `json:"ports,omitempty"`
	Services []ServiceInfo     `json:"services,omitempty"`
	MemoryMB int64             `json:"memory_mb,omitempty"`
	Focused  bool              `json:"focused,omitempty"`
}

Info is the serialisable view of a bay, and is what crosses the MCP boundary.

type Manager

type Manager struct {
	RepoRoot string

	Log func(format string, args ...any)
	// contains filtered or unexported fields
}

Manager owns every bay for one repository.

func Open

func Open(ctx context.Context, opts Options) (*Manager, error)

Open prepares a manager for the repository containing opts.Dir.

func (*Manager) Adopt

func (m *Manager) Adopt(b *Bay)

Adopt registers an already-created bay, used when the daemon restarts and finds containers it did not start in this process.

func (*Manager) Approvals added in v0.2.0

func (m *Manager) Approvals(ctx context.Context, project string) ([]approve.Record, error)

Approvals returns what has been granted for a project.

func (*Manager) Broker

func (m *Manager) Broker() *broker.Broker

Broker exposes the credential broker.

func (*Manager) Close

func (m *Manager) Close() error

Close releases the manager's resources. Bays keep running.

func (*Manager) Create

func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Bay, error)

Create makes a bay, and removes every trace of it if any step fails.

func (*Manager) Describe

func (m *Manager) Describe(ctx context.Context, b *Bay) (Info, error)

Describe reports a bay's current state.

func (*Manager) Destroy

func (m *Manager) Destroy(ctx context.Context, name string, force bool) error

Destroy removes a bay completely.

The order matters: containers, volumes and network first, then the port block, then the worktree. Removing the worktree while containers still bind-mount it would leave them pointing at a path that no longer exists.

func (*Manager) EnsureWritable added in v0.1.1

func (m *Manager) EnsureWritable(ctx context.Context, worktree string)

EnsureWritable gives a bay's worktree back to the developer if a container has taken it.

This is about the primary activity, not about cleanup. Containers write into the bind-mounted worktree as whatever user they run as, which for most images is root, and on Linux that is the filesystem's ownership -- so after a bay boots, the developer can find their own source tree read-only. Editing is the thing devbay exists to let you do in parallel; a bay you cannot edit is not a working bay.

Checked with a write rather than by inspecting modes, because the question is exactly "can this process write here" and permissions, ownership, ACLs and the platform all bear on the answer. Costs a stat and a create when nothing is wrong, which is the common case and the one that must stay cheap.

func (*Manager) Focus

func (m *Manager) Focus(ctx context.Context, name string) error

Focus moves the canonical hostname to one bay and takes it from the others.

func (*Manager) Get

func (m *Manager) Get(name string) (*Bay, bool)

Get returns a bay by name.

func (*Manager) GrantApproval added in v0.2.0

func (m *Manager) GrantApproval(ctx context.Context, project string, d manifest.Diagnostic) error

GrantApproval records a human's decision.

func (*Manager) List

func (m *Manager) List(ctx context.Context) ([]Info, error)

List returns every bay, sorted by name.

func (*Manager) NewBooter

func (m *Manager) NewBooter(worktree, name string) *Booter

NewBooter returns a Booter that verifies against a worktree.

func (*Manager) NotFound added in v0.5.2

func (m *Manager) NotFound(ctx context.Context, name string) error

NotFound explains a name this repository has no bay for.

func (*Manager) OwningProject added in v0.5.2

func (m *Manager) OwningProject(ctx context.Context, name string) (string, bool)

OwningProject names the project a bay belongs to, when that is some project other than this repository's. Bays are scoped to a project and the state database is not, so a name absent here may be running one directory away.

func (*Manager) PendingApprovals added in v0.2.0

func (m *Manager) PendingApprovals(ctx context.Context, mf *manifest.Manifest, res *manifest.Result) []manifest.Diagnostic

PendingApprovals is the subset of a validation's approvals not yet granted.

func (*Manager) Proxy

func (m *Manager) Proxy() *proxy.Proxy

Proxy exposes the shared proxy, or nil when hostname routing is off.

func (*Manager) RequireApprovals added in v0.2.0

func (m *Manager) RequireApprovals(ctx context.Context, mf *manifest.Manifest, res *manifest.Result) error

RequireApprovals refuses to boot a bay whose manifest runs a command no human has agreed to.

The rule is only a boundary if it blocks. Printing "this needs approval" and then running the command teaches a developer that the warning is noise -- and the next warning, the one that matters, is scrolled past at the same speed. So an unapproved argv stops the bay before any container starts.

The error names the exact command and the exact way to allow it, because a refusal a developer cannot act on is indistinguishable from a bug, and the thing they will do about it is stop using the tool.

func (*Manager) RevokeApproval added in v0.2.0

func (m *Manager) RevokeApproval(ctx context.Context, key string) (bool, error)

RevokeApproval withdraws one by key.

func (*Manager) RunTask

func (m *Manager) RunTask(ctx context.Context, name, task string) (*engine.TaskResult, error)

RunTask runs a declared task in a bay.

func (*Manager) Scrubber

func (m *Manager) Scrubber() *scrub.Scrubber

Scrubber exposes the shared scrubber.

func (*Manager) Secrets

func (m *Manager) Secrets() *Secrets

Secrets exposes the registry that resolves ${secret:...} references.

func (*Manager) SetSecret

func (m *Manager) SetSecret(ref, value string)

SetSecret registers a value, teaching both the resolver and the scrubber about it in one call so the two can never disagree.

func (*Manager) VerifyManifest

func (m *Manager) VerifyManifest(ctx context.Context, candidate []byte, patcher verify.Patcher) (*verify.Result, error)

VerifyManifest boots a candidate against a scratch worktree of the current branch, repairing it when a patcher is configured.

type Options

type Options struct {
	// Dir is any path inside the repository.
	Dir string
	// StatePath is the SQLite file; empty uses ~/.devbay/state.db.
	StatePath string
	// WorktreeRoot is where new worktrees go; empty uses ~/.devbay/worktrees.
	WorktreeRoot string
	// ProxyPort is the host port for bay hostnames; 0 tries 80 then 8080.
	ProxyPort int
	// AdminPort is where the proxy's config API is published, on loopback.
	AdminPort int
	// NoProxy disables hostname routing, leaving bays reachable at
	// 127.0.0.1:<port> only.
	NoProxy bool
	// Egress enforces per-service network allowlists. Off by default because
	// it costs a privileged sidecar per service; on, a service reaches only
	// what its manifest declares.
	Egress bool
	// AuditPath is the credential log; empty uses ~/.devbay/audit.jsonl.
	AuditPath string
	// SecretCommand is a secret manager to shell out to, with {ref}
	// substituted -- for example ["op", "read", "op://{ref}"]. Configured by
	// the developer, never by a manifest.
	SecretCommand []string
	Log           func(format string, args ...any)
}

Options configure a Manager.

type Secrets

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

Secrets resolves ${secret:path} references at container spawn time.

This is deliberately the smallest thing that can be correct rather than an attempt at a secrets manager. The ecosystem already converged on one shape -- `op run --`, `direnv exec`, `sops exec-env`, `dotenvx run --` all inject values into a subprocess and hold nothing -- so the useful thing to build is a consumer of those, not a competitor to them. What lives here is the registry that resolution and scrubbing share, plus an environment fallback.

Two properties matter more than where the values come from:

  • A value registered here is registered with the scrubber in the same call. Resolving a secret without teaching the scrubber about it would hand an application a credential that devbay could no longer recognise in that application's own logs.
  • Lookup happens when a container is created, not when a manifest is read, so a value is never held longer than it has to be.

func NewSecrets

func NewSecrets(s *scrub.Scrubber) *Secrets

NewSecrets returns a registry that reports every value it hands out to s.

func (*Secrets) Known

func (s *Secrets) Known() []string

Known lists the registered references, never the values.

func (*Secrets) Lookup

func (s *Secrets) Lookup(ref string) (string, bool)

Lookup resolves a reference, falling back to the environment.

The environment fallback is what makes devbay work with every tool listed above without integrating with any of them: `op run -- devbay run ...` already puts the values in devbay's environment, and this finds them there.

func (*Secrets) Set

func (s *Secrets) Set(ref, value string)

Set registers a value for a reference.

type ServiceInfo

type ServiceInfo struct {
	Name  string `json:"name"`
	State string `json:"state"`
	URL   string `json:"url,omitempty"`
}

ServiceInfo is one service's observable state.

Jump to

Keyboard shortcuts

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