workspace

package
v0.7.2 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ContextWithSessionID

func ContextWithSessionID(ctx context.Context, sessionID string) context.Context

ContextWithSessionID adds the session ID to context for secret injection during activation.

Types

type Config

type Config struct {
	Namespace    string
	OpencodePort int // Port for opencode on sandbox pods. Default: 4096.
}

Config holds workspace service configuration.

type CredentialProvisioner

type CredentialProvisioner interface {
	SeedWorkspaceCredentials(ctx context.Context, workspaceID, userID string, orgID *string) error
	// SeedWorkspaceMCPServers seeds mcp_server_bindings from auto-apply rules.
	// Epic 53. Best-effort: a failure does not block workspace creation.
	SeedWorkspaceMCPServers(ctx context.Context, workspaceID, userID string, orgID *string) error
}

CredentialProvisioner seeds workspace_credential_bindings from credential_auto_apply.

type OrgMembershipChecker

type OrgMembershipChecker interface {
	IsOrgMember(ctx context.Context, orgID, userID string) (bool, error)
	IsOrgAdmin(ctx context.Context, orgID, userID string) (bool, error)
	// GetUserOrgID returns the user's single org ID (or "" if not in any org).
	// Used by CreateWorkspace for D4 auto-attribution.
	GetUserOrgID(ctx context.Context, userID string) (string, error)
}

type PhaseChangeCallback

type PhaseChangeCallback func(workspace *v1.Workspace)

type PolicyChecker

type PolicyChecker interface {
	GetEffectivePolicy(ctx context.Context, orgID string) (*types.OrgPolicyValues, error)
}

PolicyChecker reads the effective org policy for enforcement. The policy service implements this; nil means no policy enforcement (dev/test).

type SecretAutoProvisioner

type SecretAutoProvisioner interface {
	SeedGlobalDefaultSecrets(ctx context.Context, workspaceID, userID string) error
}

SecretAutoProvisioner seeds user_secret_bindings from user_secrets where global_default=true. Called on workspace creation as a best-effort operation.

type Service

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

Service implements apiinterfaces.WorkspaceService.

func New

New creates a validated workspace service. config may be nil to use defaults.

func (*Service) ActivateWorkspace

func (s *Service) ActivateWorkspace(ctx context.Context, userID, workspaceID string) (*types.ActivateWorkspaceResponse, error)

ActivateWorkspace resumes a workspace, suspending the stalest active one if at cap.

func (*Service) CheckOwnership

func (s *Service) CheckOwnership(ctx context.Context, userID string, meta *types.WorkspaceMetadata) error

CheckOwnership is the pure-authorisation half of verifyOwner, operating on a previously-resolved *types.WorkspaceMetadata so callers can avoid a second DB hit. Behavior is identical to the post-fetch portion of verifyOwner:

  • nil meta → fail-closed Forbidden (defense-in-depth; ResolveWorkspace already returns NotFound for missing rows).
  • creator match → for org-attributed workspaces the creator must still be a CURRENT org member (D5 offboarding); personal workspaces pass.
  • non-creator on an org workspace → allowed only as org admin (D6).
  • anything else → Forbidden.

Org-store failures propagate as wrapped errors (NOT *APIError) so callers can distinguish infrastructure failure from authorisation denial — this preserves the exact return-shape contract verifyOwner had before the split.

func (*Service) CreateWorkspace

func (s *Service) CreateWorkspace(ctx context.Context, userID string, req types.CreateWorkspaceRequest) (*types.Workspace, error)

CreateWorkspace validates the request, creates a Workspace CRD, and persists metadata to the database. On database failure the CRD is deleted.

func (*Service) DeleteWorkspace

func (s *Service) DeleteWorkspace(ctx context.Context, userID, workspaceID string) error

DeleteWorkspace marks a workspace as terminating and deletes the CRD.

func (*Service) EnsureSession

func (s *Service) EnsureSession(ctx context.Context, userID, workspaceID string) (*types.EnsureSessionResponse, error)

EnsureSession guarantees the workspace has a Running sandbox and creates a new session on it. If the workspace is suspended it resumes it; if no sandbox exists it creates one. Blocks until the sandbox reaches Running, then creates the session via opencode's POST /session endpoint.

func (*Service) GetWorkspace

func (s *Service) GetWorkspace(ctx context.Context, userID, workspaceID string) (*types.Workspace, error)

GetWorkspace retrieves a workspace by ID, verifying owner.

func (*Service) GetWorkspaceStatus

func (s *Service) GetWorkspaceStatus(ctx context.Context, userID, workspaceID string) (*types.WorkspaceStatusResult, error)

GetWorkspaceStatus returns infrastructure state from the Workspace CRD.

func (*Service) ListWorkspaceSessions

func (s *Service) ListWorkspaceSessions(ctx context.Context, userID, workspaceID string) ([]types.SessionListItem, error)

ListWorkspaceSessions returns session index entries for a workspace.

func (*Service) ListWorkspaces

func (s *Service) ListWorkspaces(ctx context.Context, userID string, opts types.ListOptions) (*types.WorkspaceListResult, error)

ListWorkspaces returns workspace metadata for a user with pagination.

func (*Service) MarkSessionSeen

func (s *Service) MarkSessionSeen(ctx context.Context, userID, workspaceID, sessionID string) error

func (*Service) NeutralizeUserWorkspaces

func (s *Service) NeutralizeUserWorkspaces(ctx context.Context, userID string) error

NeutralizeUserWorkspaces suspends every Active workspace owned by userID and best-effort deletes any legacy workspace-secrets-<id> K8s Secret for each. Post-Epic-35, workspaces no longer create this Secret (secretless injection), so this is an upgrade-path cleanup no-op for post-Epic-35 workspaces. For pre-Epic-35 workspaces with a leftover Secret, it ensures the plaintext is scrubbed.

Suspend is best-effort: workspaces not in the Active phase (Creating, Resuming, already Suspended, ...) are skipped without aborting the loop, and individual failures are logged. The Secret scrub runs for every workspace regardless of phase and ignores NotFound. A nil k8sClient (dev/test) makes the scrub a no-op.

func (*Service) RefreshWorkspaceCompute

func (s *Service) RefreshWorkspaceCompute(ctx context.Context, userID, workspaceID string) (*types.RefreshWorkspaceResult, error)

RefreshWorkspaceCompute re-syncs a workspace CRD with the platform's current defaults, then bumps spec.restartGeneration so the controller rebuilds the pod. The rebuild re-resolves spec.runtime to the latest RuntimeEnvironment image (picking up new image versions) and applies the refreshed resource requests.

Use cases:

  • A new runtime image version is published; the user wants the workspace to pick it up without a full recreate.
  • The platform's default CPU/memory (workspace.defaultResources) increased and the user wants their long-lived workspace to adopt the new values.

Fields re-applied from instance settings (overwritten to current platform defaults when configured): Resources.CPU, Resources.Memory, SecurityLevel, Storage.StorageClassName, MaxActiveSessions. Fields the platform has no opinion on (empty/zero default) are left untouched, so refresh never clobbers a deliberate user setting with a schema default.

Like RestartWorkspace, refresh is REJECTED for Terminating/Terminated phases (they race with finalizer cleanup) and idempotent at the spec layer.

Suspended workspaces have no pod, and handleSuspended only observes spec.suspend (NOT restartGeneration) — so a generation bump alone would be a no-op. When the workspace is Suspended, refresh also requests a resume via ActivateWorkspace (which enforces the active-workspace cap and writes spec.suspend=false), so the controller builds a fresh pod carrying the refreshed spec. If the resume fails after the spec refresh has persisted, the error is returned but the config update remains in effect for the next manual activate.

func (*Service) RenameSession

func (s *Service) RenameSession(ctx context.Context, userID, workspaceID, sessionID, title string) error

RenameSession updates the title of a session in the session index.

func (*Service) RenameWorkspace

func (s *Service) RenameWorkspace(ctx context.Context, userID, workspaceID, name string) error

RenameWorkspace updates the name of a workspace.

func (*Service) ResolveWorkspace

func (s *Service) ResolveWorkspace(ctx context.Context, workspaceID string) (*types.WorkspaceMetadata, error)

ResolveWorkspace fetches workspace metadata by ID. It is the pure-fetch half of verifyOwner and the entry point for WorkspaceAccessMiddleware. Returns a NotFound APIError when the workspace does not exist (or the id is empty, mirroring GetWorkspace's empty-input contract) and an Internal APIError when the underlying lookup fails.

func (*Service) RestartWorkspace

func (s *Service) RestartWorkspace(ctx context.Context, userID, workspaceID string) error

RestartWorkspace bumps spec.restartGeneration so the controller's handleFailed (Epic 21 Change A) or handleActive recovery paths walk the workspace back through Pending and rebuild the pod from scratch.

Use cases:

  • Recover a Failed workspace (the original motivation; previously required `kubectl patch --subresource=status`).
  • Force-restart a stuck Active workspace whose agent is hung but the controller hasn't yet exhausted its transient-failure budget.

Restart is REJECTED for Terminating/Terminated phases — those are genuinely terminal and would race with finalizer cleanup. For all other phases the call is idempotent at the spec layer (each call bumps the field by 1; the controller responds to each bump exactly once via the strict-greater-than check on observedRestartGeneration).

Epic 35: the pod's init container fetches credentials via the bootstrap endpoint at boot — no pre-writing of a K8s Secret is needed.

func (*Service) SetCredentialProvisioner

func (s *Service) SetCredentialProvisioner(cp CredentialProvisioner)

SetCredentialProvisioner installs the credential auto-apply seeder.

func (*Service) SetInstanceSettings

func (s *Service) SetInstanceSettings(svc *settings.InstanceService)

SetInstanceSettings injects the instance settings service for enforcement.

func (*Service) SetOrgStore

func (s *Service) SetOrgStore(store OrgMembershipChecker)

func (*Service) SetPolicyChecker

func (s *Service) SetPolicyChecker(checker PolicyChecker)

SetPolicyChecker installs the org policy checker for workspace quota enforcement.

func (*Service) SetSecretAutoProvisioner

func (s *Service) SetSecretAutoProvisioner(sp SecretAutoProvisioner)

SetSecretAutoProvisioner installs the global-default secret seeder.

func (*Service) SetSessionIndex

func (s *Service) SetSessionIndex(si apiinterfaces.SessionIndexService)

SetSessionIndex injects the session index service. Optional — nil disables session tracking.

func (*Service) Start

func (s *Service) Start() error

func (*Service) Stop

func (s *Service) Stop() error

func (*Service) SuspendWorkspace

func (s *Service) SuspendWorkspace(ctx context.Context, userID, workspaceID string) error

SuspendWorkspace transitions a workspace to Suspending phase.

type VersionSyncCallback

type VersionSyncCallback func(workspaceID, imageTag, agentVersion string)

VersionSyncCallback is called with (workspaceID, imageTag, agentVersion) when the CRD's imageTag changes so the API can persist the new tag to its DB without a K8s Get. Optional — nil-safe on the watcher.

type Watcher

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

func NewWatcher

func NewWatcher(
	k8sClient pkginterfaces.KubernetesClient,
	logger pkginterfaces.LoggerInterface,
	namespace string,
	onPhaseChange PhaseChangeCallback,
) (*Watcher, error)

func (*Watcher) GetAllKnownPhases

func (w *Watcher) GetAllKnownPhases() map[string]string

func (*Watcher) GetKnownPhase

func (w *Watcher) GetKnownPhase(name string) (string, bool)

func (*Watcher) SetKnownPhase

func (w *Watcher) SetKnownPhase(name, phase string)

func (*Watcher) SetUserBroker

func (w *Watcher) SetUserBroker(broker WorkspaceOwnerTracker)

func (*Watcher) SetVersionSyncCallback

func (w *Watcher) SetVersionSyncCallback(cb VersionSyncCallback)

SetVersionSyncCallback sets the callback invoked when a workspace becomes Active with a non-empty imageTag. Must be called before Start(); calling after Start() is a data race (the watch goroutine reads onVersionSync without a lock). Follows the same contract as SetUserBroker.

func (*Watcher) SetWorkspaceUpdateCallback

func (w *Watcher) SetWorkspaceUpdateCallback(cb WorkspaceUpdateCallback)

SetWorkspaceUpdateCallback installs a callback invoked on every Added/Modified event for any Workspace CRD. Optional; nil-safe. Must be called before Start() (same data-race constraint as SetVersionSyncCallback). See WorkspaceUpdateCallback docstring for the callback contract.

func (*Watcher) Start

func (w *Watcher) Start() error

func (*Watcher) Stop

func (w *Watcher) Stop()

type WorkspaceConfig

type WorkspaceConfig = types.WorkspaceConfig

WorkspaceConfig is non-sensitive workspace metadata persisted for pod boot. Re-exported from pkg/types to avoid requiring callers to import both.

type WorkspaceOwnerTracker

type WorkspaceOwnerTracker interface {
	RecordWorkspaceOwner(workspaceID, userID string)
	CleanupWorkspace(workspaceID string)
}

type WorkspaceUpdateCallback

type WorkspaceUpdateCallback func(workspace *v1.Workspace)

WorkspaceUpdateCallback is called on every Added/Modified event (NOT Deleted) for a Workspace, regardless of what changed. Used by the watcher-driven auto-push (worklog 0591): the callback filters on the exact state it cares about (Phase==Active + UserCredsPresent==false + bindings-exist) and no-ops otherwise.

Distinct from PhaseChangeCallback because a pod-recreation may not change phase (e.g. `kubectl delete pod` with immediate controller respawn keeps status.Phase=Active) but does change UserCredsPresent (controller clears it on unreachable, then reports the new agentd's state on the next scrape). Filtering by phase alone misses those.

Callback contract:

  • MUST be fast + non-blocking. Slow callbacks stall the watch goroutine, which delays the SSE stream and phase events for all workspaces on this API replica.
  • MUST tolerate repeat calls for the same workspace — the watcher emits many Modified events per workspace lifetime, and the callback's own state is the source of truth for "already handled this update."
  • MUST NOT panic. Panics propagate to the watch goroutine.

Jump to

Keyboard shortcuts

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