server

package
v0.0.0-...-64c266f Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 67 Imported by: 0

Documentation

Overview

Package server wires the HTTP surface: chi router, SPA fallback, JSON API, and the SSE hub. NewRouter accepts the embedded SPA filesystem as a parameter so this package has no compile-time dependency on the embed declaration in cmd/gemba.

For v0 of the scaffold, handlers return placeholder 501 responses. The real surface lands with gm-e2.6.

gm-o9t8.3.5 — server-side GitHub OAuth surface.

This file wires two routes:

  • GET /api/v1/auth/oauth/github/callback — placeholder/status endpoint. The device flow doesn't need a real redirect target, so when OAuth is configured we 200 with a tiny diagnostic body ({status:"ok", flow:"device"}); when OAuth is disabled we 503 with oauth_not_configured.

  • POST /api/v1/auth/oauth/github/login — the device-flow exchange. The CLI completes the device flow against api.github.com on its own and POSTs the resulting GitHub access_token here. We call GET https://api.github.com/user to validate the token + recover the GitHub identity, find-or- create a tenant in the tenant.Store, mint a 32-byte gemba bearer, hash it with argon2id, persist (tenant_id, github_id, hash) and return the plaintext bearer + tenant id to the caller. The plaintext bearer is never persisted server-side.

gm-o9t8.3.5.2/3.5.5 — auth_tokens persistence (with revoke + rotate).

The OAuth login handler mints a 32-byte gemba bearer, hashes it with argon2id, and stores the (tenant_id, github_id, device_label, hash) tuple in the auth_tokens table. The plaintext is never persisted — it's returned to the CLI exactly once and never recoverable from disk.

Two stores are provided:

  • MemTokenStore — in-memory; used by tests and the no-Dolt fallback
  • SQLTokenStore — Dolt/MySQL backed; cmd/gemba serve attaches one against the bd Dolt pool when --dolt-url is configured.

Verification is O(n) over the stored hashes because argon2id is per-record. n is small for v1 (one row per (tenant, device)); when it grows past the comfortable scan threshold we add a lookup index or move bearer verification to a stateless JWT.

gm-o9t8.3.5.5 adds soft-delete (revoked_at), last_used_at touch, and an opaque per-token id so the management surface (list, revoke, rotate) can address rows without leaking either the plaintext or the argon2id hash.

Package server wires the HTTP surface. See doc.go for the package-level overview.

/api/spec-kit/* exposes Spec Kit planning artifacts as a first-class refinement source and syncs approved artifacts into Beads.

Index

Constants

View Source
const ConfirmHeader = "X-GEMBA-Confirm"

ConfirmHeader is the request header every mutating route requires. Echo it back in the response so callers can confirm the server saw the right token (helps debug "did my retry land?" without parsing bodies).

View Source
const CreateAuthTokensTable = `` /* 494-byte string literal not displayed */

CreateAuthTokensTable is the DDL the tenant package's Migrate would run on a multi-tenant rig. Idempotent. Mirrors the tenants table migration convention.

gm-o9t8.3.5.5 changes:

  • id is the primary key (a 32-char opaque token id minted server side). The composite (tenant_id, device_label) becomes a secondary UNIQUE so re-login from the same device still upserts.
  • revoked_at: soft-delete sentinel; VerifyToken skips non-null rows.
  • last_used_at: best-effort touch on successful verify.
View Source
const SoftDeleteRetention = 30 * 24 * time.Hour

SoftDeleteRetention is the default retention window for soft-deleted workspaces. After this window an external sweeper (out of scope for slice a) is expected to archive or hard-delete the row.

Variables

View Source
var ErrBootstrapNotFound = errors.New("bootstrap: analysis not found")

ErrBootstrapNotFound is returned by BootstrapStore.Get / Commit when the analysis id is unknown.

View Source
var ErrNewProjectNotFound = errors.New("newproject: session not found")

ErrNewProjectNotFound is returned by NewProjectStore when the session id is unknown.

View Source
var ErrProbeUnconfigured = errors.New("health: prober upstream not configured")

ErrProbeUnconfigured is the sentinel a prober can return to signal "I am attached but my upstream is intentionally not wired" — the readyz handler treats this as a failure (the operator wired the prober, so they expect it to work). It exists so callers don't have to invent string error messages for the same condition.

View Source
var ErrTokenNotFound = errors.New("oauth: token not found")

ErrTokenNotFound is returned by Revoke/Rotate when the (tenant, token_id) tuple is not present in the store (or already revoked). HTTP handlers translate this to 404.

View Source
var ErrWorkspaceNotFound = errors.New("workspace not found")

ErrWorkspaceNotFound is returned by stores when the requested id is absent.

Functions

func NewPoolWiring

func NewPoolWiring(
	op core.OrchestrationPlaneAdaptor,
	wp core.WorkPlane,
	pool config.ResolvedPool,
	cfg config.PoolConfig,
	readers planner.OperationalContextReaders,
	agentType string,
) *poolWiring

NewPoolWiring constructs a PoolWiring bound to one (rig, persona) pool. The returned struct's methods plug into autodispatch.Daemon's interface fields. agentType is derived from the persona's TOML configuration upstream and threaded into the SessionDispatcher's prompt extension as `gemba:agent_type` so the native adaptor's pane-reuse path can find a slot.

func NewTokenID

func NewTokenID() (string, error)

NewTokenID returns a 32-char hex id used as the auth_tokens primary key. Distinct from the plaintext bearer; safe to expose.

func NewWorkspaceLifecycle

func NewWorkspaceLifecycle(cfg WorkspaceLifecycleConfig) *workspaceLifecycleHandler

NewWorkspaceLifecycle constructs the handler. Returns nil when cfg.Store is nil — the Router treats a nil handler as "endpoints return 503 adaptor_not_configured".

func OpenAPISpec

func OpenAPISpec() []byte

OpenAPISpec returns the embedded OpenAPI document bytes. Exported so cmd/gen-core-types and offline tooling can pull the same source the runtime serves without spinning up the server.

func SetGTRunner

func SetGTRunner(r gtRunner)

SetGTRunner overrides the gt-shellout runner used by the orchestration mutation handlers. Tests use this to inject a fake without spawning processes. Pass nil to restore the default.

Types

type AdminSession

type AdminSession struct {
	ID        string    `json:"id"`
	TenantID  string    `json:"tenant_id"`
	Workspace string    `json:"workspace,omitempty"`
	AgentID   string    `json:"agent_id,omitempty"`
	Status    string    `json:"status"`
	StartedAt time.Time `json:"started_at,omitempty"`
}

AdminSession is the wire shape for the operator console's session inventory. Mirrors core.Session but kept narrow so the console UI gets a stable contract independent of the agent-plane internals.

type AdminSessionLister

type AdminSessionLister func(ctx context.Context) ([]AdminSession, error)

AdminSessionLister is the contract the admin sessions endpoint depends on. Production wiring builds one over the OrchestrationPlane; tests inject a static list.

type AdoptableLister

type AdoptableLister func(ctx context.Context, baseURL string) ([]adoptableDB, error)

AdoptableLister enumerates adoptable beads DBs. Production impl connects to the configured Dolt server. Tests substitute a stub via AttachProjects(AttachConfig{AdoptableLister: ...}) so they don't need a real Dolt server on port 3307.

type AttachConfig

type AttachConfig struct {
	Pinger        BeadsPinger
	GitInitRunner CommandRunner
	Now           func() time.Time
	// AdoptableLister backs GET /api/v1/projects/adoptable (gm-gmyl).
	// Nil falls back to the production lister that opens a mysql
	// connection against the configured Dolt server. Tests inject a
	// stub here.
	AdoptableLister AdoptableLister
}

AttachConfig configures the attach handler. Zero values are fine — Pinger nil falls back to defaultBeadsPinger, GitInitRunner nil falls back to exec.

type BeadsPinger

type BeadsPinger func(ctx context.Context, beadsURL string) error

BeadsPinger verifies a beads DB URL is reachable. The production implementation opens a mysql connection via the go-sql-driver, pings, and closes. Tests substitute a stub so they don't need a real Dolt server on port 3307.

type BindProjectRequest

type BindProjectRequest struct {
	// BeadsDBPath is the absolute path to the directory holding the
	// beads DB (the parent of the `.beads/` subdir). The handler
	// verifies the directory has a `.beads/` child before doing any
	// work.
	BeadsDBPath string `json:"beads_db_path"`
	// TargetRepoPath is the absolute path to the directory that will
	// hold the final project. For mode == "create" this MUST equal
	// BeadsDBPath. For mode == "navigate" this MUST be a pre-existing
	// git working tree.
	TargetRepoPath string `json:"target_repo_path"`
	// Mode is "create" or "navigate" — see the file header.
	Mode string `json:"mode"`
}

BindProjectRequest is the request body for POST /api/v1/projects/bind.

type BootstrapAnalysis

type BootstrapAnalysis struct {
	ID          string                   `json:"analysis_id"`
	Source      BootstrapSource          `json:"source"`
	Config      map[string]any           `json:"config,omitempty"`
	Frames      []BootstrapProgressFrame `json:"-"`
	Plan        []BootstrapPlanItem      `json:"-"`
	Findings    []BootstrapFinding       `json:"-"`
	StartedAt   time.Time                `json:"started_at"`
	CompletedAt time.Time                `json:"completed_at,omitempty"`
	Committed   bool                     `json:"committed"`
}

BootstrapAnalysis is the persisted record for one in-flight analysis. The store mints id; AppendProgress drives the long-poll stream; Plan + Findings are populated on completion; Committed flips on a successful POST /commit (idempotent guard against a second commit).

type BootstrapAnalysisSeed

type BootstrapAnalysisSeed struct {
	Frames   []BootstrapProgressFrame
	Plan     []BootstrapPlanItem
	Findings []BootstrapFinding
}

BootstrapAnalysisSeed bundles the canned analysis output the handler synthesises off the source. The store doesn't care what the values mean — it just persists them so subsequent Get/Frames/ Plan calls can read them back.

type BootstrapCommitResult

type BootstrapCommitResult struct {
	ProjectPath string `json:"project_path"`
	BoardURL    string `json:"board_url"`
}

BootstrapCommitResult is the response shape /commit hands back. project_path lands in the success toast; board_url is the SPA's post-ratify destination.

type BootstrapFinding

type BootstrapFinding struct {
	ID     string `json:"id"`
	Level  string `json:"level"` // pass | warn | fail
	Title  string `json:"title"`
	Detail string `json:"detail,omitempty"`
}

BootstrapFinding is one row in the consistency report. level is pass | warn | fail; the SPA totals the levels for the top-line PASS=12 / WARN=2 / FAIL=0 summary.

type BootstrapPlanItem

type BootstrapPlanItem struct {
	ID       string              `json:"id"`
	Kind     string              `json:"kind"` // epic | milestone | sprint | story
	Title    string              `json:"title"`
	Detail   string              `json:"detail,omitempty"`
	Children []BootstrapPlanItem `json:"children,omitempty"`
}

BootstrapPlanItem is one outline row in the generated plan (epic / milestone / sprint / story). Children render the two-level outline the SPA expects.

type BootstrapProgressFrame

type BootstrapProgressFrame struct {
	Seq  int       `json:"seq"`
	Line string    `json:"line"`
	At   time.Time `json:"at"`
	Done bool      `json:"done,omitempty"`
}

BootstrapProgressFrame is one streamed line of analysis progress. JSON shape mirrors web/src/api/bootstrap.ts BootstrapProgress.

type BootstrapSource

type BootstrapSource string

BootstrapSource enumerates the four source kinds the wizard supports per ui-spec §5.15. Mirrors the typed client's union exactly so the JSON token round-trips without translation.

const (
	BootstrapSourceJira       BootstrapSource = "jira"
	BootstrapSourceBeads      BootstrapSource = "beads"
	BootstrapSourceSourceCode BootstrapSource = "source-code"
	BootstrapSourceFresh      BootstrapSource = "fresh"
)

type BootstrapStore

type BootstrapStore interface {
	// Start allocates a new analysis with frames seeded from
	// `seed` (the synthesised progress sequence keyed off source).
	// Returns the persisted record so the handler can read the
	// minted id back without a second Get.
	Start(ctx context.Context, source BootstrapSource, config map[string]any, seed BootstrapAnalysisSeed) (BootstrapAnalysis, error)

	// Get returns the analysis by id. ErrBootstrapNotFound when
	// unknown.
	Get(ctx context.Context, id string) (BootstrapAnalysis, error)

	// FramesSince returns frames with seq > since, in seq order.
	// Used by GET /progress for the long-poll loop.
	FramesSince(ctx context.Context, id string, since int) ([]BootstrapProgressFrame, error)

	// Commit marks the analysis committed and returns the resulting
	// (project_path, board_url) pair. Idempotent: a second commit
	// returns the same response; ErrBootstrapAlreadyCommitted is
	// surfaced when the caller wants to know they raced (the HTTP
	// handler treats that as success).
	Commit(ctx context.Context, id string) (BootstrapCommitResult, error)
}

BootstrapStore is the persistence interface. All methods are context-aware so a future SQL implementation honours deadlines. Mirrors walk.Store's narrow shape: callers hand pre-built records to mutation methods, the store concerns itself only with id + lifecycle.

func NewMemoryBootstrapStore

func NewMemoryBootstrapStore() BootstrapStore

NewMemoryBootstrapStore returns a fresh in-memory store. cmd/gemba serve calls this once at boot and hands the result to AttachBootstrap.

type ChangeRef

type ChangeRef = newproject.ChangeRef

ChangeRef points at the most-recent skill edit.

type CommandRunner

type CommandRunner func(ctx context.Context, dir, name string, args ...string) ([]byte, error)

CommandRunner is the shell-out abstraction the ratifier uses. The production implementation forwards to exec.CommandContext; tests substitute a recording stub so they can drive failures deterministically without touching the host's bd / git binaries.

type ComponentResult

type ComponentResult struct {
	OK      bool   `json:"ok"`
	Error   string `json:"error,omitempty"`
	Skipped string `json:"skipped,omitempty"`
}

ComponentResult is the per-component health entry surfaced under the "components" key. Skipped is non-empty when a check did not run for a structural reason (e.g. KVM on macOS); a skipped check counts as ok.

type DoltProber

type DoltProber interface {
	Probe(ctx context.Context) error
}

DoltProber is the surface health.go uses to verify the bd/dolt adapter is reachable. Implementations should return nil only when a SELECT-style round-trip would succeed; any non-nil error fails the dolt component. The existing *supervisor.Supervisor already exposes Ready() — wire it via DoltProberFunc.

type DoltProberFunc

type DoltProberFunc func(ctx context.Context) error

DoltProberFunc adapts a plain function to DoltProber.

func (DoltProberFunc) Probe

func (f DoltProberFunc) Probe(ctx context.Context) error

Probe satisfies DoltProber.

type DoltReadyChecker

type DoltReadyChecker interface {
	Ready() bool
}

DoltReadyChecker is the contract /api/readyz uses to ask whether the embedded dolt sql-server is reachable (gm-o9t8.1.2.3). Implemented by *supervisor.Supervisor; tests inject a stub.

type DoltStatusReporter

type DoltStatusReporter interface {
	DoltReadyChecker
	State() string
	Port() int
	DataDir() string
	RestartCount() int64
	LastError() error
}

DoltStatusReporter is the richer status surface /api/v1/workspaces/{wsid}/status type-asserts onto its DoltReadyChecker handle to power the SPA's degraded-state banner (gm-o9t8.1.9). Implemented by *supervisor.Supervisor. The status handler tolerates a checker that does NOT satisfy this interface (test stubs, future no-op modes) — the embedded_dolt field stays null in that case.

type DraftBead

type DraftBead = newproject.DraftBead

DraftBead is the leaf of the plan tree. See internal/skills/newproject for the field-level documentation; the JSON wire shape is canonical at that location.

type DraftEpic

type DraftEpic = newproject.DraftEpic

DraftEpic is one epic in the plan tree.

type DraftMilestone

type DraftMilestone = newproject.DraftMilestone

DraftMilestone is one milestone in the plan tree.

type EgressAuditor

type EgressAuditor func(event audit.Event, wsid string, rule egress.Rule)

EgressAuditor receives one event per egress-rule mutation. Bound at boot via AttachEgressAuditor; nil is fine (handlers skip emission).

type EgressTemplateProvider

type EgressTemplateProvider interface {
	// Lookup returns the named bundle. An empty / unknown name
	// resolves to the provider's "default" bundle. Implementations
	// MUST never return an empty Name — callers persist the resolved
	// name in audit so an unknown template's fallback is observable.
	Lookup(name string) egress.Template
	// SeedRules converts a bundle into per-workspace Rules ready for
	// egress.Store.Put. The slice is priority-ordered (highest
	// first) so the runtime enforcer walks bundle hosts before the
	// hard-coded baseline allow-list.
	SeedRules(wsid string, tpl egress.Template, now time.Time) []egress.Rule
}

EgressTemplateProvider is the contract workspace-create uses to resolve a named template + materialize it as a slice of egress.Rule rows keyed to the new workspace.

type EmbeddedDoltStatus

type EmbeddedDoltStatus struct {
	Enabled      bool   `json:"enabled"`
	State        string `json:"state"`
	Port         int    `json:"port"`
	DataDir      string `json:"data_dir"`
	RestartCount int64  `json:"restart_count"`
	LastError    string `json:"last_error,omitempty"`
}

EmbeddedDoltStatus surfaces the embedded-dolt supervisor's posture so the SPA can render a degraded-state banner when the subprocess is restarting or has given up (gm-o9t8.1.9). Null when the supervisor isn't wired (external Dolt via --dolt-url, or the noop adaptor).

State values:

  • "starting" — Start() in flight; first SELECT 1 hasn't landed yet
  • "ready" — last probe succeeded; subprocess healthy
  • "restarting" — a recent probe failed; supervisor is in backoff / respawn between probes
  • "failed" — maxConsecutiveFailures exhausted; supervisor gave up

type FakeS3Uploader

type FakeS3Uploader struct {
	Bucket  string
	Objects map[string][]byte
	// contains filtered or unexported fields
}

FakeS3Uploader is an in-memory test double. Safe for concurrent use.

func NewFakeS3Uploader

func NewFakeS3Uploader(bucket string) *FakeS3Uploader

NewFakeS3Uploader returns an in-memory S3Uploader stub.

func (*FakeS3Uploader) Upload

func (f *FakeS3Uploader) Upload(_ context.Context, key string, payload []byte) (string, error)

Upload satisfies S3Uploader.

type FilesystemBackend

type FilesystemBackend interface {
	Stat(path string) (os.FileInfo, error)
	MkdirAll(path string, perm os.FileMode) error
	WriteFile(path string, data []byte, perm os.FileMode) error
	RemoveAll(path string) error
}

FilesystemBackend is the file-write abstraction. Production wires it to os.MkdirAll / os.WriteFile / os.RemoveAll; tests substitute a stub that maps writes onto an in-memory tree (or a tmpfs). Splitting it out lets us test step-by-step rollback without spawning subprocesses.

type MemTokenStore

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

MemTokenStore is the in-memory TokenStore — process-local, lost on restart. Tests and single-user fallback use this; production multi-tenant rigs attach SQLTokenStore.

func NewMemTokenStore

func NewMemTokenStore() *MemTokenStore

NewMemTokenStore returns a zero-valued in-memory token store.

func (*MemTokenStore) List

func (m *MemTokenStore) List(_ context.Context, tenantID string) ([]TokenInfo, error)

List returns metadata for non-revoked tokens belonging to tenantID.

func (*MemTokenStore) Put

Put implements TokenStore. Upserts on (tenant_id, device_label) so a re-login from the same device replaces the prior hash rather than stacking dead rows. If the incoming record has no ID, one is minted.

func (*MemTokenStore) Records

func (m *MemTokenStore) Records() []TokenRecord

Records returns a copy of the stored records — test-only accessor.

func (*MemTokenStore) Revoke

func (m *MemTokenStore) Revoke(_ context.Context, tenantID, tokenID string) error

Revoke soft-deletes the record identified by (tenantID, tokenID). The tenant scope is enforced here so a cross-tenant request hits ErrTokenNotFound rather than silently revoking a sibling row.

func (*MemTokenStore) Rotate

func (m *MemTokenStore) Rotate(_ context.Context, tenantID, tokenID string) (string, string, string, error)

Rotate mints a fresh bearer for the (tenantID, tokenID) row, hashes it, revokes the old row, and inserts a new row with a fresh id. The new row inherits device_label + github_id from the prior row. Returns (newBearer, newID, deviceLabel).

func (*MemTokenStore) VerifyToken

func (m *MemTokenStore) VerifyToken(_ context.Context, plaintext string) (tenant.ID, bool, error)

VerifyToken implements TokenStore. Iterates the stored records and runs argon2id Verify against each hash. Returns the matching tenant id on success, or ("", false, nil) on miss. Skips rows whose RevokedAt is non-nil. Best-effort touches LastUsedAt on success.

type MemWorkspaceLifecycleStore

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

MemWorkspaceLifecycleStore is the in-memory default store. Safe for concurrent use.

func NewMemWorkspaceLifecycleStore

func NewMemWorkspaceLifecycleStore() *MemWorkspaceLifecycleStore

NewMemWorkspaceLifecycleStore returns an empty in-memory store.

func (*MemWorkspaceLifecycleStore) Create

Create implements WorkspaceLifecycleStore.

func (*MemWorkspaceLifecycleStore) Get

Get implements WorkspaceLifecycleStore.

func (*MemWorkspaceLifecycleStore) List

List implements WorkspaceLifecycleStore.

func (*MemWorkspaceLifecycleStore) Update

Update implements WorkspaceLifecycleStore.

type NewProjectRatifier

type NewProjectRatifier interface {
	Ratify(ctx context.Context, state NewProjectState) (RatifyResponse, error)
}

NewProjectRatifier executes the atomic ratification transaction. Returns the SPA's success envelope on commit, or a structured RatifyError on any step failure.

func NewRatifier

func NewRatifier(cfg RatifierConfig) NewProjectRatifier

NewRatifier builds a production NewProjectRatifier. cmd/gemba serve calls this once at boot and hands the result to AttachNewProject.

type NewProjectSession

type NewProjectSession struct {
	ID        string
	State     NewProjectState
	StartedAt time.Time
}

NewProjectSession is the in-memory record for one /onboard conversation. Lives only in server process memory — refresh / restart / Ctrl-C discards it (one-shot persistence is by design; see docs/design/newproject.md §One-shot persistence).

type NewProjectState

type NewProjectState = newproject.NewProjectState

NewProjectState is the authoritative state for one /api/v1/newproject conversation session.

type NewProjectStore

type NewProjectStore interface {
	// Start allocates a new session with the empty state and returns
	// the persisted record so the handler can read the minted id back
	// without a second Get.
	Start(ctx context.Context) (NewProjectSession, error)

	// Get returns the session by id. ErrNewProjectNotFound when
	// unknown.
	Get(ctx context.Context, id string) (NewProjectSession, error)

	// Update replaces the state of an existing session. Returns
	// ErrNewProjectNotFound if the id is unknown.
	Update(ctx context.Context, id string, state NewProjectState) error

	// Delete drops the session from the store. Used after a
	// successful ratify to free memory; idempotent — deleting an
	// unknown id is not an error.
	Delete(ctx context.Context, id string)
}

NewProjectStore is the persistence interface for /onboard conversation sessions. Mirrors the bootstrap / walk store pattern: callers hand pre-built records to mutation methods, the store concerns itself only with id + lifecycle.

func NewMemoryNewProjectStore

func NewMemoryNewProjectStore() NewProjectStore

NewMemoryNewProjectStore returns a fresh in-memory store. cmd/gemba serve calls this once at boot and hands the result to AttachNewProject.

type NonceCache

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

NonceCache caches recent (nonce → response) tuples so a replayed mutating request returns the cached response unchanged.

Capacity bounds memory; TTL keeps stale entries from sitting forever once the LRU stops touching them.

func NewNonceCache

func NewNonceCache(capacity int, ttl time.Duration) *NonceCache

NewNonceCache returns a cache with the given capacity (max entries) and TTL. Defaults: capacity=1024, ttl=5min — sized for normal SPA double-click protection without a real footprint cost.

func (*NonceCache) Get

func (c *NonceCache) Get(nonce string) (cachedResponse, bool)

Get returns the cached response for nonce when present and not expired. Touches LRU order so a hot nonce stays warm.

func (*NonceCache) Put

func (c *NonceCache) Put(nonce string, resp cachedResponse)

Put stores resp under nonce, evicting the least-recently-used entry if the cache is at capacity.

type OrchestrationPC

type OrchestrationPC struct {
	Scope   string `json:"scope"`
	Persona string `json:"persona"`
	Name    string `json:"name"`
	State   string `json:"state,omitempty"`
}

OrchestrationPC — one polecat row, gt-only. Empty on native.

type OrchestrationSc

type OrchestrationSc struct {
	ID       string   `json:"id"`
	Kind     string   `json:"kind"` // "rig" | "local"
	Personas []string `json:"personas"`
}

OrchestrationSc — one scope row. For native this is the single implicit local scope ("local"). For gt this is one rig.

type OrchestrationState

type OrchestrationState struct {
	AdaptorID  string            `json:"adaptor_id"`
	Scopes     []OrchestrationSc `json:"scopes"`
	AgentTypes []string          `json:"agent_types"`
	Polecats   []OrchestrationPC `json:"polecats,omitempty"`
}

OrchestrationState is the wire shape /api/orchestration/state returns. Mirrors web/src/api/orchestrationState.ts.

type PersonaFile

type PersonaFile struct {
	ID        string   `json:"id"`         // filename stem
	Name      string   `json:"name"`       // [persona] name (top-level "name" key)
	AgentType string   `json:"agent_type"` // declared agent_type if present
	Skills    []string `json:"skills,omitempty"`
}

PersonaFile is one row in the response.

type PerspectiveContribution

type PerspectiveContribution struct {
	PersonaID   string                      `json:"persona_id"`
	PersonaName string                      `json:"persona_name,omitempty"`
	PersonaIcon string                      `json:"persona_icon,omitempty"`
	Kind        PerspectiveContributionKind `json:"kind"`
	Content     string                      `json:"content"`
	At          time.Time                   `json:"at"`
}

PerspectiveContribution is one persona's volunteered comment on an active agenda item. Stable JSON shape — additive fields can land later without breaking the SPA.

type PerspectiveContributionKind

type PerspectiveContributionKind string

PerspectiveContributionKind classifies an inline perspective. Kept as a typed string so the SPA can branch on it for styling (blocking → red, amendment → amber, note → neutral) without parsing the human-readable content.

const (
	PerspectiveKindBlocking  PerspectiveContributionKind = "blocking"
	PerspectiveKindAmendment PerspectiveContributionKind = "amendment"
	PerspectiveKindNote      PerspectiveContributionKind = "note"
)

type PoolConfigEnvelope

type PoolConfigEnvelope struct {
	Path              string         `json:"path"`
	Body              string         `json:"body"`
	Parsed            poolConfigJSON `json:"parsed"`
	MaxParallel       int            `json:"max_parallel"`
	ReservedForManual int            `json:"reserved_for_manual"`
}

PoolConfigEnvelope is the wire shape both GET and PUT return. Mirrors web/src/api/poolConfig.ts.

type PoolMember

type PoolMember struct {
	SessionID           string     `json:"session_id"`
	PaneID              string     `json:"pane_id,omitempty"`
	Status              string     `json:"status"`
	LastBead            string     `json:"last_bead,omitempty"`
	BeadsDoneThisMember int        `json:"beads_done_this_member,omitempty"`
	LastRecycleAt       *time.Time `json:"last_recycle_at,omitempty"`
}

PoolMember is the wire shape of one pool slot's row.

type PoolState

type PoolState struct {
	Pools      []PoolStateEntry `json:"pools"`
	CapturedAt time.Time        `json:"captured_at"`
}

PoolState is the response envelope for GET /api/pools.

type PoolStateEntry

type PoolStateEntry struct {
	Scope               string       `json:"scope"`
	Persona             string       `json:"persona"`
	SizeTargetDeclared  int          `json:"size_target_declared"`
	SizeTargetEffective int          `json:"size_target_effective"`
	SizeActual          int          `json:"size_actual"`
	Idle                int          `json:"idle"`
	Working             int          `json:"working"`
	Members             []PoolMember `json:"members"`
}

PoolStateEntry is one (scope, persona) pool's exposed state.

gm-s47n.16 (spec §2): the JSON field renamed `rig` → `scope`. Existing SPA consumers reading `rig` are updated; the change ships in lockstep with the editor.

type PoolWiring

type PoolWiring = poolWiring

PoolWiring is the public alias the cli package uses to construct a daemon's adaptors. Produced by NewPoolWiring.

type RatifierConfig

type RatifierConfig struct {
	// HomeDir overrides $HOME for default-dir resolution. Tests set
	// this so the transaction lands inside a t.TempDir() instead of
	// the developer's real home.
	HomeDir string

	// DefaultDirOverride bypasses the config-file lookup entirely.
	// Tests use this to force the projects directory without faking
	// out the home-dir filesystem.
	DefaultDirOverride string

	// Runner shells external commands (bd, git). nil → exec-based
	// default.
	Runner CommandRunner

	// FS abstracts filesystem writes. nil → osFS.
	FS FilesystemBackend

	// Now returns the current time; tests pin this for deterministic
	// timestamps in workspace.toml.
	Now func() time.Time

	// DataRoot is the gemba-server's data directory — the parent of
	// the per-workspace data tree at <DataRoot>/workspaces/<id>/
	// (gm-o9t8.1.2.4). Empty disables per-workspace layout creation;
	// the ratify transaction still succeeds, and the omission is
	// surfaced via SeedWarnings so /api/readyz can detect the
	// inconsistency. Set this in serve wiring once a stable
	// server-level data dir exists.
	DataRoot string
}

RatifierConfig configures the production ratifier. Zero values are fine — DefaultDir empty falls back to ~/.gemba/config.toml resolution, Runner empty falls back to exec, FS empty falls back to osFS.

type RatifyError

type RatifyError struct {
	// Step is the 1-based transaction step that failed (1..10, or
	// "8a" for the dep-resolution sub-step which we encode as 80).
	Step int
	// Code is a short machine-readable token (validation_failed,
	// dir_exists, git_init_failed, beads_init_failed,
	// milestone_create_failed, epic_create_failed, bead_create_failed,
	// dep_resolve_failed, cycle_detected, project_md_failed,
	// commit_failed, …).
	Code string
	// Message is the human-readable diagnostic surfaced in the toast.
	Message string
	// Cause is the underlying error, retained for logging. May be nil
	// when the step itself synthesised the error.
	Cause error
}

RatifyError is the structured error every step of the transaction returns on failure. The HTTP layer maps Step + Code into the JSON envelope so the SPA can render a precise diagnostic.

func (*RatifyError) Error

func (e *RatifyError) Error() string

func (*RatifyError) Unwrap

func (e *RatifyError) Unwrap() error

type RatifyResponse

type RatifyResponse struct {
	ProjectPath    string `json:"project_path"`
	ProjectName    string `json:"project_name"`
	MilestoneCount int    `json:"milestone_count"`
	EpicCount      int    `json:"epic_count"`
	// SeedWarnings carries any non-fatal failures from the FTUX seed
	// steps (.gemba/agents.toml, .gemba/personas/, CLAUDE.md — gm-root.24).
	// These are best-effort additions: a failure here does NOT roll back
	// the project — the SPA renders a "partial seed" toast so the
	// operator can hand-fix and proceed. Empty (or omitted) means every
	// seed step succeeded.
	SeedWarnings []string `json:"seed_warnings,omitempty"`
}

RatifyResponse is the success envelope POST /ratify returns. The SPA owns post-ratify navigation (gm-root.17.7); the server returns the new project's filesystem root, its display name, and the seeded milestone / epic counts so the handoff screen can render "seeded N milestones and M epics" without re-querying the new workspace's beads database.

type RecycleWriter

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

RecycleWriter persists session.recycled events. Constructed via NewRecycleWriter; Run loops until ctx is cancelled.

func NewRecycleWriter

func NewRecycleWriter(db *sql.DB) *RecycleWriter

NewRecycleWriter wraps the given *sql.DB. nil db is allowed — returns a writer whose Run is a pure no-op.

func StartRecycleWriter

func StartRecycleWriter(ctx context.Context, op core.OrchestrationPlaneAdaptor, db *sql.DB) *RecycleWriter

StartRecycleWriter is the convenience constructor + launcher used by cmd/gemba serve. Subscribes the writer to the OrchestrationPlane (filtered to session.recycled events only) and runs the loop on a new goroutine. ctx cancellation terminates the goroutine.

Best-effort: a Subscribe failure is logged but doesn't propagate — the writer becomes a no-op and gemba serve continues. The retro pipeline simply won't have recycle audit data until the next restart.

func (*RecycleWriter) Insert

Insert persists one session.recycled event. Exposed so tests can drive the writer without spinning up a Subscribe channel. Returns nil on success or a wrapped error on insert failure.

func (*RecycleWriter) Run

func (w *RecycleWriter) Run(ctx context.Context, in <-chan core.OrchestrationEvent)

Run reads events from in until ctx is cancelled or in is closed. Each session.recycled event is persisted via Insert; non-fatal errors are logged and the loop continues.

type Router

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

Router is the package-level entry point. cmd/gemba passes in the embedded SPA filesystem so this package doesn't import the embed declaration directly. host carries the bound transport-plane adaptors so handlers can reach the WorkPlane / OrchestrationPlane without re-resolving them per request.

func NewRouter

func NewRouter(cfg config.ServeConfig, spa fs.FS, host *api.Host) *Router

NewRouter builds the chi router. spa must be a filesystem rooted at the built Vite output (with an index.html at the top level); pass nil or an empty FS during development and the handler will return a helpful hint. host may be nil during early bring-up — handlers that need a WorkPlane must check Host() before dereferencing.

func (*Router) ActiveProject

func (r *Router) ActiveProject() string

ActiveProject returns the project name the operator most recently switched to, or an empty string if no switch has occurred this session. Exposed for gm-root.17.7 (start-planning handoff) so it can read the just-created project without re-scanning the disk.

func (*Router) AttachAdminConsole

func (r *Router) AttachAdminConsole(
	emails []string,
	sessionLister AdminSessionLister,
	auditPubKey ed25519.PublicKey,
	auditRecords func(ctx context.Context) ([]audit.Record, error),
)

AttachAdminConsole wires the admin-console seams. Any nil argument leaves the prior value in place so callers can wire pieces incrementally.

func (*Router) AttachAuditEmit

func (r *Router) AttachAuditEmit(fn func(ctx context.Context, event string, payload any) error)

AttachAuditEmit wires a best-effort audit hook the OAuth login handler invokes on success. Tests and the no-audit production default leave it nil. Mirror of the AttachMetricsHandler pattern: optional, attach late, fail open when absent.

func (*Router) AttachAuditor

func (r *Router) AttachAuditor(a audit.Auditor)

AttachAuditor wires a in the admin-plane audit sink. Pass nil to disable audit emission (the handlers behave normally otherwise).

func (*Router) AttachBootstrap

func (r *Router) AttachBootstrap(store BootstrapStore)

AttachBootstrap binds the BootstrapStore used by the /api/bootstrap/* handlers. Calling with nil removes the binding (handlers return 503). cmd/gemba serve calls this once at boot; tests inject memory stores per case.

func (*Router) AttachDoltSupervisor

func (r *Router) AttachDoltSupervisor(s DoltReadyChecker)

AttachDoltSupervisor wires a DoltReadyChecker into the router so the /api/readyz endpoint can reflect embedded-dolt health. Nil detaches.

When no checker is attached, /api/readyz reports the server itself as ready and omits the dolt check from the response — that's the external-dolt and noop modes, where readyz can't make a meaningful claim about the upstream and shouldn't pretend to.

func (*Router) AttachEgressAuditor

func (r *Router) AttachEgressAuditor(a EgressAuditor)

AttachEgressAuditor binds the audit emitter. nil clears it.

func (*Router) AttachEgressStore

func (r *Router) AttachEgressStore(s egress.Store)

AttachEgressStore binds the egress storage implementation to the router. Until called, all four egress endpoints return 503 adaptor_not_configured.

func (*Router) AttachEgressTemplates

func (r *Router) AttachEgressTemplates(p EgressTemplateProvider)

AttachEgressTemplates wires p as the active EgressTemplateProvider. Passing nil restores the package default (BuiltinTemplates).

func (*Router) AttachHealthChecks

func (r *Router) AttachHealthChecks(vault VaultProber, dolt DoltProber, auditDir string)

AttachHealthChecks wires the readiness probes onto the router. Any argument may be empty / nil and is reported as skipped or failing per the contract above. Calling AttachHealthChecks more than once replaces the prior wiring atomically.

func (*Router) AttachMetricsHTTPClient

func (r *Router) AttachMetricsHTTPClient(c *http.Client)

AttachMetricsHTTPClient overrides the http.Client the /api/v1/metrics/series proxy uses to call upstream Prometheus (gm-e9m0). Tests pass a client wired to httptest.Server so they don't depend on a real Prometheus instance. cmd/gemba serve does not call this — production runs use the default 15s-timeout client.

func (*Router) AttachMetricsHandler

func (r *Router) AttachMetricsHandler(h http.Handler)

AttachMetricsHandler binds an http.Handler to GET /metrics. Calling with nil leaves the route returning 503 — useful for tests that want to skip the metrics surface entirely. cmd/gemba serve calls this with a metrics.Collector.Handler() once at boot.

func (*Router) AttachNewProject

func (r *Router) AttachNewProject(store NewProjectStore, turner SkillTurner, ratifier NewProjectRatifier)

AttachNewProject binds the /onboard session store + skill turner + ratify executor (gm-root.17.6). Calling with a nil store removes the binding (handlers return 503). cmd/gemba serve calls this once at boot; tests inject fixtures per case.

turner may be nil — when so, NewStubSkillTurner takes over. ratifier may be nil too — when so, /ratify returns 503 (the conversation surface still works for SPA preview / screenshot purposes).

func (*Router) AttachOAuth

func (r *Router) AttachOAuth(o *oauth.OAuth)

AttachOAuth wires the GitHub OAuth handle (gm-o9t8.3.5). Nil disables the callback + device-login routes (they 503 with oauth_not_configured). cmd/gemba serve calls this once at boot after parsing the OAuth flags.

func (*Router) AttachOAuthTokenStore

func (r *Router) AttachOAuthTokenStore(s TokenStore)

AttachOAuthTokenStore wires the gemba bearer-token store the OAuth login handler mints into. Nil leaves /api/v1/auth/oauth/github/login returning 503 even when an OAuth handle is attached — both halves are required for the device-flow to complete.

func (*Router) AttachOrgGates

func (r *Router) AttachOrgGates(s *oauth.OrgGateStore)

AttachOrgGates wires the per-tenant GitHub OrgGate registry (gm-o9t8.4.4.1). Nil disables org/team gating — every successfully authenticated GitHub user is admitted (the legacy open default).

func (*Router) AttachPersonaDispatcher

func (r *Router) AttachPersonaDispatcher(d *persona.Dispatcher, sr *corepersona.SkillRegistry, pr *corepersona.Registry)

AttachPersonaDispatcher binds the persona consult dispatcher, the skill registry it draws from, and the persona registry the POST endpoint resolves persona_id against (gm-twp2). cmd/gemba serve calls this once at boot after parsing personas + registering skills; tests attach a fixture-built dispatcher per case. Routes under /api/skills* and /api/consults* return 503 until at least the dispatcher + skill registry are bound; POST /api/consults additionally requires the persona registry (the read endpoints don't).

Any argument may be nil to detach (tests that re-use a Router across cases set then clear).

func (*Router) AttachPersonasDir

func (r *Router) AttachPersonasDir(dir string)

AttachPersonasDir binds the directory the /api/personas handler scans. Empty / unset falls back to "<cwd>/.gemba/personas".

func (*Router) AttachPhase

func (r *Router) AttachPhase(store phase.Store)

AttachPhase binds the phase.Store the /api/v1/phase handlers read from. cmd/gemba serve calls this once at boot after seeding the initial WorkspaceState; tests inject a phase.NewMemoryStore (or a pre-seeded NewSeededMemoryStore) per case. Calling with nil detaches the binding — handlers return 503 until the next AttachPhase call.

func (*Router) AttachPoolConfig

func (r *Router) AttachPoolConfig(path string, maxParallel, reservedForManual int)

AttachPoolConfig binds the editor's load/save target. path is the pool.toml filesystem path; maxParallel is the host-wide concurrent pane cap (the same value [pool] clamp arithmetic uses); reservedForManual is the [pool] reserved_for_manual default after the cascade. cmd/gemba serve calls this once at boot.

func (*Router) AttachPools

func (r *Router) AttachPools(resolved []config.ResolvedPool)

AttachPools binds the resolved pool list to the router. cmd/gemba serve calls this once at startup after Resolve runs. Calling with a zero-length slice is the "no pools configured" path — the endpoint then returns an empty list.

func (*Router) AttachProjects

func (r *Router) AttachProjects(cfg AttachConfig)

AttachProjects wires the projects/attach handler dependencies. Optional — zero-value Router uses production defaults (defaultBeadsPinger, execRunner, time.Now). Tests call this to inject stubs.

func (*Router) AttachQuotaLimiter

func (r *Router) AttachQuotaLimiter(l *quota.Limiter)

AttachQuotaLimiter binds the per-tenant token-bucket limiter consumed by QuotaMiddleware. Passing nil disables quota enforcement.

func (*Router) AttachTenantStore

func (r *Router) AttachTenantStore(s tenant.Store)

AttachTenantStore wires a tenant.Store into the Router. cmd/gemba serve constructs the store (SQLStore against the Dolt pool, or MemStore for the no-dolt fallback) and attaches it after the router is built. A nil store turns the /api/v1/tenants/{tid} handler into a 503 — same convention as the metrics / workflow attach points.

func (*Router) AttachTenantWorkspaceCount

func (r *Router) AttachTenantWorkspaceCount(fn tenantWorkspaceCountFn)

AttachTenantWorkspaceCount wires the per-tenant workspace-count probe consulted by DELETE /api/v1/tenants/{tid}. Passing nil clears the hook (defaulting back to "assume zero").

func (*Router) AttachTierQuotaMiddleware

func (r *Router) AttachTierQuotaMiddleware(mw func(http.Handler) http.Handler)

AttachTierQuotaMiddleware installs the tier-aware quota middleware (gm-o9t8.4.2.1) so router assembly layers it onto /api after WithTenant. Passing nil disables the tier middleware. The caller owns the underlying BucketStore + Counters lifetimes; the Router only borrows the wrapped handler.

func (*Router) AttachUsageAggregator

func (r *Router) AttachUsageAggregator(a *billing.Aggregator)

AttachUsageAggregator wires the billing aggregator into the router. Nil disables the /usage endpoint (returns 503).

func (*Router) AttachVault

func (r *Router) AttachVault(v vault.Vault)

AttachVault binds the vault implementation to the router. Until called, all three secrets endpoints return 503 adaptor_not_configured. cmd/gemba serve calls this once at boot. Tests inject a fixture vault; a nil argument is the documented way to detach (e.g. between table-driven cases).

func (*Router) AttachWalk

func (r *Router) AttachWalk(store walk.Store, sources walk.Sources)

AttachWalk binds the walk.Store + walk.Sources used by the /api/v1/walks/* handlers. Calling with nil store removes the binding (handlers return 503). Sources is zero-value-safe — a nil EscalationLister / HITLLister / GateFailureLister / WorkItemLister contributes zero items to BuildAgenda. cmd/gemba serve calls this once at boot; tests inject fakes per case.

func (*Router) AttachWalkSummary

func (r *Router) AttachWalkSummary(s *walk_summary.Skill)

AttachWalkSummary binds the Documentarian's walk_summary skill (gm-77u). When attached, the End handler invokes Run after the store transition so a markdown artifact lands under docs/walks/<date>-<slug>.md. Pass nil to disable — the End handler still records the transition and emits the SSE event, just without producing the artifact.

func (*Router) AttachWorkflowClient

func (r *Router) AttachWorkflowClient(c *workflow.Client)

AttachWorkflowClient binds the workflow.Client the /api/workflows/* surface dispatches to (gm-e12.22.2). Until called, every workflow handler returns 503 adaptor_not_configured. cmd/gemba serve calls this once at boot with a client backed by the bd CLI on PATH; tests inject a stub Runner via NewClientWithRunner.

func (*Router) AttachWorkspaceLifecycle

func (r *Router) AttachWorkspaceLifecycle(h *workspaceLifecycleHandler)

AttachWorkspaceLifecycle wires h as the active lifecycle handler. Pass nil to disable the surface (endpoints will return 503).

func (*Router) AttachWorkspaceRegistry

func (r *Router) AttachWorkspaceRegistry(reg workspaces.Registry)

AttachWorkspaceRegistry binds the multi-tenant workspace registry (gm-o9t8.2.4). When set, handlers that need to resolve a wsid to an on-disk project path consult the registry first and fall back to the legacy projects-config walk on ErrNotFound. Pass nil to detach (used by tests that want the legacy behaviour explicitly).

func (*Router) AttachWorkspacesRoot

func (r *Router) AttachWorkspacesRoot(root string)

AttachWorkspacesRoot binds the on-disk root that holds per-workspace cloned repos (gm-o9t8.1.4.3). The diff handler resolves <root>/<wsid>/repo/. Empty root → /diff returns 503 adaptor_not_configured.

func (*Router) Close

func (r *Router) Close()

Close stops the HealthBus ticker and tears down the events hub. Safe to call when StartHealthBus was never invoked.

func (*Router) EventsHub

func (r *Router) EventsHub() *events.Hub

EventsHub returns the GembaEvent fan-out broker. cmd/gemba serve uses this to AttachOrchestrationStream against the registered adaptor's Subscribe() output, so adaptor events fan to /events SSE subscribers. Returns nil only when the router was constructed without one (zero-value/test paths).

func (*Router) HealthBus

func (r *Router) HealthBus() *registry.HealthBus

HealthBus returns the registry.HealthBus this router caches adaptor status against. cmd/gemba serve uses this to wire the gemba walk's BeadsDegradedLister against the same probe ticker the SPA banner reads from (gm-vch2). Returns nil only when the router was built without one — test paths that don't exercise adaptor health.

func (*Router) Host

func (r *Router) Host() *api.Host

Host returns the api.Host this router was built with, or nil if the router was constructed without one. Tests and handlers use this to reach the registered WorkPlane / OrchestrationPlane.

func (*Router) InstanceID

func (r *Router) InstanceID() string

InstanceID returns the per-process boot id stamped on startup- immutable responses (capabilities, adaptors). The SPA stores the first id it sees and full-reloads if a later response shows a different id, which is how we detect a server restart with new config without a capabilities-changed channel. gm-6m60.

func (*Router) PhaseStore

func (r *Router) PhaseStore() phase.Store

PhaseStore returns the phase.Store this router was last attached with, or nil. cmd/gemba serve uses this to share the store with other subsystems (e.g. gm-3on's Purview-as-gate path will read it to decide whether a Manager mutation is allowed).

func (*Router) QuotaMiddleware

func (r *Router) QuotaMiddleware(next http.Handler) http.Handler

QuotaMiddleware returns an http.Handler middleware that consumes one token from the bearer-bound tenant's bucket on every request. When no limiter is attached the middleware is a passthrough.

On deny: emits 429 with Retry-After (delta-seconds, RFC 9110) and the standard httperr envelope code "rate_limited".

func (*Router) SeedWorkspaceEgress

func (r *Router) SeedWorkspaceEgress(ctx context.Context, store egress.Store, wsid, name string) (egress.Template, error)

SeedWorkspaceEgress materializes the template named name into store. Returns the resolved template (after the empty/unknown fallback) so the caller can audit it. A nil store is a no-op — the lifecycle handler may run in tests without a Dolt pool.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP dispatches through the chi mux built in NewRouter. Router satisfies http.Handler so callers can hand it directly to http.Server.

func (*Router) SetGitHubUserFetcherForTest

func (r *Router) SetGitHubUserFetcherForTest(fn func(ctx context.Context, token string) (oauth.GitHubUser, error))

SetGitHubUserFetcherForTest is the test seam for the OAuth login handler. Production callers never use this. Exported (test-only hook) rather than declared in oauth_test.go so the cli_test package can wire an httptest-backed fetcher without an internal export.

func (*Router) StartHealthBus

func (r *Router) StartHealthBus()

StartHealthBus preserves the old serve wiring hook. With the current zero interval it is intentionally a no-op: adaptor health is checked by explicit refresh paths, not by a periodic background probe. gm-root.7.

type S3Uploader

type S3Uploader interface {
	// Upload writes payload at the given key. Returns the canonical
	// remote URL (e.g. s3://bucket/key) the audit record captures.
	Upload(ctx context.Context, key string, payload []byte) (string, error)
}

S3Uploader is the contract the archive handler calls into to push the layered workspace bundle to long-term object storage. The production implementation wraps an AWS SDK client; tests substitute FakeS3Uploader. Out of scope for slice a: the real wiring lives in slice (c).

type SQLTokenStore

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

SQLTokenStore is the Dolt/MySQL-backed TokenStore. Same contract as MemTokenStore; persistence survives restart.

func NewSQLTokenStore

func NewSQLTokenStore(db *sql.DB) *SQLTokenStore

NewSQLTokenStore wraps db. The pool's lifecycle is the caller's.

func (*SQLTokenStore) List

func (s *SQLTokenStore) List(ctx context.Context, tenantID string) ([]TokenInfo, error)

List returns metadata for non-revoked tokens belonging to tenantID.

func (*SQLTokenStore) Migrate

func (s *SQLTokenStore) Migrate(ctx context.Context) error

Migrate ensures the auth_tokens table exists. Idempotent.

func (*SQLTokenStore) Put

func (s *SQLTokenStore) Put(ctx context.Context, rec TokenRecord) error

Put implements TokenStore via INSERT ... ON DUPLICATE KEY UPDATE on the (tenant_id, device_label) UNIQUE key. Re-login from the same device replaces the hash; the row's id is preserved so prior rotate/revoke handles keep working. created_at is bumped so the operator can see the most recent login time; revoked_at is cleared (a fresh login un-revokes any prior soft delete on the same device).

func (*SQLTokenStore) Revoke

func (s *SQLTokenStore) Revoke(ctx context.Context, tenantID, tokenID string) error

Revoke soft-deletes (tenantID, tokenID).

func (*SQLTokenStore) Rotate

func (s *SQLTokenStore) Rotate(ctx context.Context, tenantID, tokenID string) (string, string, string, error)

Rotate revokes (tenantID, tokenID) and inserts a fresh row carrying the same device_label + github_id in a single transaction so partial state is impossible. Returns (newBearer, newID, deviceLabel).

func (*SQLTokenStore) VerifyToken

func (s *SQLTokenStore) VerifyToken(ctx context.Context, plaintext string) (tenant.ID, bool, error)

VerifyToken scans the auth_tokens table and runs argon2id verify against each non-revoked row. For v1 this is acceptable — token count is small. On a successful verify, last_used_at is best-effort updated; a failed update does not fail the auth check.

type SkillTurner

type SkillTurner interface {
	Turn(ctx context.Context, in NewProjectState, message string, edits map[string]interface{}) (out NewProjectState, reply string, err error)
	// Greeting returns the assistant's opening line on /start. Empty
	// is a valid greeting (the SPA renders nothing).
	Greeting() string
	// Probe is invoked on /start before allocating a session and
	// reports whether the turner can serve the request. Real-mode
	// implementations return an error wrapping the operator-facing
	// diagnostic (e.g. "no LLM client configured") so the handler
	// can return 503 with the diagnostic in the body. Stub turners
	// return nil. May be called concurrently.
	Probe(ctx context.Context) error
}

SkillTurner is the contract the Onboarder persona host (gm-root.17.10) implements: take a state + an operator message + any in-place edits and return the next state + a reply for the conversation pane.

Production serve wiring installs the real Onboarder persona host. A stub implementation remains available for tests and fallback setups; it mirrors the e2e fixture's deterministic plan-tree mutation so screenshot tests stay stable.

func NewStubSkillTurner

func NewStubSkillTurner() SkillTurner

NewStubSkillTurner returns the placeholder turner. Production serve wiring uses the real Onboarder via AttachNewProject(... real-turner) (gm-root.17.10).

type TokenInfo

type TokenInfo struct {
	ID          string     `json:"id"`
	DeviceLabel string     `json:"device_label"`
	CreatedAt   time.Time  `json:"created_at"`
	LastUsedAt  *time.Time `json:"last_used_at,omitempty"`
}

TokenInfo is the metadata view of a stored token, returned by List + Rotate. Carries no plaintext or hash material; safe to return over the wire.

type TokenRecord

type TokenRecord struct {
	ID          string // opaque 32-char hex id; the auth_tokens primary key
	TenantID    tenant.ID
	GitHubID    int64
	DeviceLabel string
	HashPHC     string // argon2id PHC string; the plaintext is never stored
	CreatedAt   time.Time
	LastUsedAt  *time.Time
	RevokedAt   *time.Time
}

TokenRecord is one persisted row in the auth_tokens table.

type TokenStore

type TokenStore interface {
	// Put records (tenantID, githubID, deviceLabel, argon2id-hash).
	// Implementations MUST upsert on (tenantID, deviceLabel) so a
	// re-login from the same device replaces the prior hash rather
	// than accumulating dead rows. If rec.ID is empty Put assigns
	// a fresh opaque token id.
	Put(ctx context.Context, rec TokenRecord) error
	// VerifyToken finds the record whose hashed value matches the
	// supplied plaintext token, returning (tenantID, true) on match.
	// Returns ("", false, nil) when no match — distinct from infra
	// errors which return ("", false, err). Implementations MUST
	// skip rows where revoked_at IS NOT NULL and SHOULD best-effort
	// touch last_used_at on a successful verify.
	VerifyToken(ctx context.Context, plaintext string) (tenant.ID, bool, error)
	// List returns metadata for non-revoked tokens belonging to
	// tenantID. Never returns plaintext or hashes.
	List(ctx context.Context, tenantID string) ([]TokenInfo, error)
	// Revoke soft-deletes (tenantID, tokenID). Returns
	// ErrTokenNotFound when the row is absent or already revoked.
	Revoke(ctx context.Context, tenantID, tokenID string) error
	// Rotate issues a fresh bearer + argon2id hash and revokes the
	// prior row in a single transaction. Returns (plaintext, newID,
	// deviceLabel). ErrTokenNotFound when (tenantID, tokenID) is
	// absent or already revoked.
	Rotate(ctx context.Context, tenantID, tokenID string) (string, string, string, error)
}

TokenStore is the persistence contract for OAuth-minted gemba bearer tokens. The plaintext is never stored — only the argon2id PHC hash. Records are scoped to (tenant_id, github_id) so a single GitHub identity can hold multiple tokens (one per device) without colliding.

type VaultDeleter

type VaultDeleter interface {
	List(ctx context.Context, wsid string) ([]string, error)
	Delete(ctx context.Context, wsid, key string) error
}

VaultDeleter narrows secrets.Vault.Delete so the archive path can shred per-workspace keys without depending on the full vault surface.

type VaultProber

type VaultProber interface {
	Probe(ctx context.Context) error
}

VaultProber is the surface health.go uses to round-trip a sentinel through the secrets vault. The real *vault.boltVault satisfies this directly; tests inject fakes that simulate Put/Inject failure.

We deliberately do NOT depend on internal/vault.Vault here — that interface requires per-workspace ids and would force a workspace allocation just to probe readiness. A narrow surface keeps the probe cheap and the test seam clean.

type VaultProberFunc

type VaultProberFunc func(ctx context.Context) error

VaultProberFunc adapts a plain function to VaultProber.

func (VaultProberFunc) Probe

func (f VaultProberFunc) Probe(ctx context.Context) error

Probe satisfies VaultProber.

type WorkspaceAgentSummary

type WorkspaceAgentSummary struct {
	Active         int    `json:"active"`
	LastRunID      string `json:"last_run_id,omitempty"`
	LastRunStatus  string `json:"last_run_status,omitempty"`
	LastRunSummary string `json:"last_run_summary,omitempty"`
}

WorkspaceAgentSummary captures the "agents" stripe. Active counts sessions that haven't recorded a terminal event in the last agentActiveWindow. LastRunSummary is a best-effort one-line description; today it's just "agent X on bead Y" — proper transcript summarisation is a follow-up.

type WorkspaceBeadCounts

type WorkspaceBeadCounts struct {
	Ready       int `json:"ready"`
	InFlight    int `json:"in_flight"`
	Blocked     int `json:"blocked"`
	OpenTotal   int `json:"open_total"`
	ClosedToday int `json:"closed_today"`
}

WorkspaceBeadCounts mirrors the buckets the dashboard renders. `ready` and `in_flight` are mapped from native bd statuses; the state_category columns aren't used here because the dashboard text is keyed on bd's vocabulary.

type WorkspaceLifecycleConfig

type WorkspaceLifecycleConfig struct {
	Store     WorkspaceLifecycleStore
	Vault     VaultDeleter
	S3        S3Uploader
	Egress    egress.Store
	DataDir   string
	Retention time.Duration
	Clock     func() time.Time
	IDGen     func() string
}

WorkspaceLifecycleConfig bundles the optional collaborators for the lifecycle handler. Store is required; the rest fall back to nil (skipping the corresponding phase) so tests can exercise individual transitions without standing up every collaborator.

type WorkspaceLifecycleStore

type WorkspaceLifecycleStore interface {
	Create(ctx context.Context, w WorkspaceRecord) error
	Get(ctx context.Context, id string) (WorkspaceRecord, error)
	Update(ctx context.Context, w WorkspaceRecord) error
	List(ctx context.Context, tenantID string) ([]WorkspaceRecord, error)
}

WorkspaceLifecycleStore persists WorkspaceRecord rows. Implementations must be safe for concurrent calls. The slice-a default is the in-memory MemWorkspaceLifecycleStore; cmd/gemba serve will swap in a Dolt-backed implementation in a follow-up.

type WorkspaceRecord

type WorkspaceRecord struct {
	ID              string     `json:"id"`
	TenantID        string     `json:"tenant_id"`
	Slug            string     `json:"slug"`
	CreatedAt       time.Time  `json:"created_at"`
	DeletedAt       *time.Time `json:"deleted_at,omitempty"`
	ArchivedAt      *time.Time `json:"archived_at,omitempty"`
	EgressTemplate  string     `json:"egress_template,omitempty"`
	ArchiveLocation string     `json:"archive_location,omitempty"`
}

WorkspaceRecord is the lifecycle-state row managed by this handler. Egress and secrets state live in their own stores; the record here carries the lifecycle bookkeeping (creation, soft-delete, archive) plus the egress-template name we seeded the workspace with so audit consumers can reconstruct the network policy at creation time.

func (WorkspaceRecord) IsArchived

func (w WorkspaceRecord) IsArchived() bool

IsArchived reports whether the record has been archived.

func (WorkspaceRecord) IsSoftDeleted

func (w WorkspaceRecord) IsSoftDeleted() bool

IsSoftDeleted reports whether the record's lifecycle is currently soft-deleted (DeletedAt set, ArchivedAt unset).

type WorkspaceRepoStatus

type WorkspaceRepoStatus struct {
	Head   string `json:"head"`
	Dirty  bool   `json:"dirty"`
	Branch string `json:"branch"`
}

WorkspaceRepoStatus carries the git posture for the workspace repo. Nil-valued when the workspace isn't backed by a clone yet (e.g. a freshly-created project that hasn't run `git init` / clone).

type WorkspaceStatusResponse

type WorkspaceStatusResponse struct {
	WorkspaceID  string                `json:"workspace_id"`
	Mode         string                `json:"mode"`
	Repo         *WorkspaceRepoStatus  `json:"repo"`
	Beads        WorkspaceBeadCounts   `json:"beads"`
	Agents       WorkspaceAgentSummary `json:"agents"`
	EmbeddedDolt *EmbeddedDoltStatus   `json:"embedded_dolt"`
}

WorkspaceStatusResponse is the JSON body GET /api/v1/workspaces/{wsid}/status returns. Fields use pointer / omitempty semantics so an unresolvable subsystem renders as null rather than as a misleading zero.

Directories

Path Synopsis
Package audit is an append-only, hash-chained, signed audit log for agent actions on gemba-server.
Package audit is an append-only, hash-chained, signed audit log for agent actions on gemba-server.
Package dispatch holds the server-side routing policy that decides whether a new bead should co-locate in an existing intra-parallel session or trigger a fresh pane spawn (gm-root.16.4).
Package dispatch holds the server-side routing policy that decides whether a new bead should co-locate in an existing intra-parallel session or trigger a fresh pane spawn (gm-root.16.4).
Package dolthub wraps server-side Dolt remote push/pull for a workspace's project state.
Package dolthub wraps server-side Dolt remote push/pull for a workspace's project state.
Package gitops wraps server-side git operations against the workspace checkout.
Package gitops wraps server-side git operations against the workspace checkout.
Package httperr is the single place every gemba data handler goes to turn a Go error into the wire-stable JSON error envelope.
Package httperr is the single place every gemba data handler goes to turn a Go error into the wire-stable JSON error envelope.
Package metrics binds the GembaEvent stream to a Prometheus /metrics endpoint so operators can scrape adaptor health, session activity, escalation pressure, and budget posture without standing up an OTEL collector.
Package metrics binds the GembaEvent stream to a Prometheus /metrics endpoint so operators can scrape adaptor health, session activity, escalation pressure, and budget posture without standing up an OTEL collector.
gm-o9t8.4.2.1 — tier-aware quota + rate-limit middleware.
gm-o9t8.4.2.1 — tier-aware quota + rate-limit middleware.
Package restore implements gemba-server's "reconstruct from remotes" flow: given a git remote (canonical source) and an optional Dolt remote (canonical project state), reproduce a workable workspace on a fresh machine.
Package restore implements gemba-server's "reconstruct from remotes" flow: given a git remote (canonical source) and an optional Dolt remote (canonical project state), reproduce a workable workspace on a fresh machine.
Package secrets provides a per-workspace encrypted-at-rest secret vault for gemba-server.
Package secrets provides a per-workspace encrypted-at-rest secret vault for gemba-server.
Package workspacelayout owns the on-disk layout for a single Gemba workspace's per-workspace data tree (gm-o9t8.1.2.4).
Package workspacelayout owns the on-disk layout for a single Gemba workspace's per-workspace data tree (gm-o9t8.1.2.4).

Jump to

Keyboard shortcuts

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