workspace

package
v0.37.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: AGPL-3.0 Imports: 48 Imported by: 0

Documentation

Index

Constants

View Source
const (
	LabelApp       = "app"
	LabelComponent = "component"
	LabelWorkspace = "llmsafespaces.dev/workspace"
	LabelRuntime   = "runtime"
	LabelTenant    = "llmsafespaces.dev/tenant"

	AppName            = "llmsafespaces"
	ComponentWorkspace = "workspace"
)

Labels applied to workspace pods.

View Source
const (

	// AgentdPinsConfigMapName caches the last successfully resolved pins
	// per digest so a controller restart during a registry outage does
	// not brick startup.
	AgentdPinsConfigMapName = "llmsafespaces-agentd-pins"
)
View Source
const (

	// OpencodePinsConfigMapName caches the last successfully resolved
	// opencode pins per digest so a controller restart during a registry
	// outage does not brick startup.
	OpencodePinsConfigMapName = "llmsafespaces-opencode-pins"
)
View Source
const (

	// SidecarRestartMarkerEnv wires LLMSAFESPACES_RESTART_MARKER_PATH on
	// both containers so the sidecar's and the supervisor's marker writes
	// land on one cross-uid-readable file (0640 + shared gid 1000).
	SidecarRestartMarkerEnv = agentd.SidecarRestartMarkerPath
)
View Source
const WorkspaceFinalizer = "workspace.llmsafespaces.dev/finalizer"

Variables

View Source
var ErrAgentdPinsUnavailable = errors.New("agentd pins unavailable (registry unreachable and no usable cache)")

ErrAgentdPinsUnavailable wraps every agentd resolution failure where the registry was unreachable AND no usable cache existed. Callers can errors.Is this to offer the manual-pin hint.

View Source
var ErrOpencodePinsUnavailable = errors.New("opencode pins unavailable (registry unreachable and no usable cache)")

ErrOpencodePinsUnavailable wraps every opencode resolution failure where the registry was unreachable AND no usable cache existed. Callers can errors.Is this to offer the manual-pin hint.

Functions

func ProbeRelayRouter added in v0.34.4

func ProbeRelayRouter(ctx context.Context, routerURL string) error

ProbeRelayRouter verifies the router is reachable and serving (GET /healthz). This is the loud half of the guard: a controller enabled for relay-only delivery must refuse to start against a dead router rather than fail every workspace reconcile.

func ValidateAgentdDelivery added in v0.15.8

func ValidateAgentdDelivery(image, amd64, arm64 string) error

ValidateAgentdDelivery is the exported startup guard used by controller main. Wraps validateAgentdDeliveryConfig.

func ValidateAgentdSidecar added in v0.18.0

func ValidateAgentdSidecar(sidecarEnabled bool, agentdImage string) error

ValidateAgentdSidecar is the exported startup guard used by controller main. Wraps validateAgentdSidecarConfig.

func ValidateOpencodeDelivery added in v0.25.5

func ValidateOpencodeDelivery(image, amd64, arm64 string) error

ValidateOpencodeDelivery is the exported startup guard used by controller main. Wraps validateOpencodeDeliveryConfig.

func ValidateRelayStagingFlags added in v0.34.4

func ValidateRelayStagingFlags(enabled bool, routerURL, apiServiceURL, namespace string) error

ValidateRelayStagingFlags checks the pure flag contract: enabled requires the router URL and the credential-source API URL (worklog D1), and a namespace.

func ValidateRelayStagingStartup added in v0.34.4

func ValidateRelayStagingStartup(ctx context.Context, cfg *RelayStagingConfig, c client.Client) error

ValidateRelayStagingStartup is the full boot gate: flags probed, router reachable, pub Secret readable + parseable (the router bootstrapped; this also proves the controller's name-scoped get-on-pub RBAC — a successful read implies the namespace exists, so no cluster-scoped Namespace GET is needed or performed), and the mint key created. c must be a DIRECT (non-cached) client — the controller-runtime cache is not started this early.

Types

type AgentdDeliveryConfig added in v0.15.8

type AgentdDeliveryConfig struct {
	Image             string
	BinarySHA256AMD64 string
	BinarySHA256ARM64 string
}

AgentdDeliveryConfig is the #863 overlay-delivery configuration passed from controller main (flags) to the reconciler. Zero value = legacy baked-in mode.

type BinaryPins added in v0.25.5

type BinaryPins struct {
	SHA256AMD64 string
	SHA256ARM64 string
}

BinaryPins are the resolved per-arch binary sha256s of one overlay artifact.

func ResolveOpencodePinsWithCache added in v0.25.5

func ResolveOpencodePinsWithCache(ctx context.Context, image, flagAMD64, flagARM64 string) (BinaryPins, error)

ResolveOpencodePinsWithCache is the opencode startup entrypoint used by controller main. validateOpencodeDeliveryConfig has already run.

func ResolvePinsWithCache added in v0.15.10

func ResolvePinsWithCache(ctx context.Context, image, flagAMD64, flagARM64 string) (BinaryPins, error)

ResolvePinsWithCache is the agentd startup entrypoint used by controller main. validateAgentdDeliveryConfig has already run.

type CachedLLMProviderSource added in v0.34.4

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

CachedLLMProviderSource is the LLMProviderSource backed by the API service's internal llm-providers endpoint with a short success-only cache. Unlike org-status there is NO stale-serve: a fetch failure fails the staging pass loudly (credentials must be fresh — a stale set could seal revoked material and miss newly-bound providers).

func NewCachedLLMProviderSource added in v0.34.4

func NewCachedLLMProviderSource(baseURL, token string, ttl time.Duration) *CachedLLMProviderSource

NewCachedLLMProviderSource constructs the source. baseURL is the API service root; token is the X-Internal-Token (LLMSAFESPACES_INTERNAL_TOKEN — the same Secret that authenticates the org-status call, mounted on both sides by the chart).

func (*CachedLLMProviderSource) LLMProviders added in v0.34.4

func (c *CachedLLMProviderSource) LLMProviders(ctx context.Context, ownerUserID, workspaceID string) ([]secrets.LLMProviderData, error)

type CachedOrgStatusClient

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

CachedOrgStatusClient is an OrgStatusClient backed by the API service's internal org-status endpoint with a per-org TTL cache.

Cache semantics (D20):

  • On a fresh cache hit (< TTL): return the cached status, no fetch.
  • On a stale entry (>= TTL) or cache miss: attempt a fetch.
  • Fetch success: update the cache and return the new status.
  • Fetch failure + prior entry: serve the STALE cached status so a transient API outage does not flip the controller into fail-open and leave suspended orgs running. This is the "30s cache absorbs transient failures" guarantee.
  • Fetch failure + no entry: return ok=false (fail open — D20: an unwarranted suspension is more disruptive than leaving a pod running).

Concurrency: each orgID has its own entry guarded by a mutex, so concurrent refreshes for the SAME org are deduplicated (only one in-flight fetch per org) while different orgs refresh in parallel. The mutex is held for the duration of the fetch, bounding concurrent same-org fetches to one.

func NewCachedOrgStatusClient

func NewCachedOrgStatusClient(baseURL, token string, ttl time.Duration, logger OrgStatusLogger) *CachedOrgStatusClient

NewCachedOrgStatusClient constructs the client. baseURL is the API service root (e.g. http://llmsafespaces-api.llmsafespaces.svc:8080); leave empty to disable org-suspension (the reconciler then never org-suspends). token, when non-empty, is sent as X-Internal-Token. ttl is the cache freshness window (production: 30s). logger may be nil.

func (*CachedOrgStatusClient) GetOrgStatus

func (c *CachedOrgStatusClient) GetOrgStatus(ctx context.Context, orgID string) (string, bool)

GetOrgStatus implements OrgStatusClient. See CachedOrgStatusClient docs for the cache + fail-open contract.

type FailureClass

type FailureClass string
const (
	FailureClassNone           FailureClass = ""
	FailureClassInfrastructure FailureClass = "Infrastructure"
	FailureClassResource       FailureClass = "Resource"
	FailureClassProcess        FailureClass = "Process"
	FailureClassConfiguration  FailureClass = "Configuration"
)

type HTTPRelayRouterClient added in v0.34.4

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

HTTPRelayRouterClient is the RelayRouterClient against the llm-relay router's internal API (US-72.2): POST /internal/v1/tokens and POST /internal/v1/keys/rotate, both authenticated by the controller-held mint key (Authorization: Bearer, constant-time compared server-side).

func NewHTTPRelayRouterClient added in v0.34.4

func NewHTTPRelayRouterClient(routerURL, namespace string, reader client.Reader) *HTTPRelayRouterClient

NewHTTPRelayRouterClient constructs the client. reader resolves the llm-relay-mint-key Secret in namespace (the controller creates it in the staging pass / startup guard; reads are cheap because mint and rotate are rare operations).

func (*HTTPRelayRouterClient) MintToken added in v0.34.4

func (*HTTPRelayRouterClient) RotateKeys added in v0.34.4

type HostResolver

type HostResolver interface {
	LookupHost(ctx context.Context, host string) ([]string, error)
}

HostResolver resolves a hostname to a list of IP strings. Tests inject a stub via WorkspaceReconciler.HostResolver to avoid hitting real DNS; production uses net.DefaultResolver via defaultHostResolver.

type LLMProviderSource added in v0.34.4

type LLMProviderSource interface {
	LLMProviders(ctx context.Context, ownerUserID, workspaceID string) ([]secrets.LLMProviderData, error)
}

LLMProviderSource is the credential-source seam (worklog D1): the controller→API internal llm-providers endpoint. Interface for test injection.

type OpencodeDeliveryConfig added in v0.25.5

type OpencodeDeliveryConfig struct {
	Image             string
	BinarySHA256AMD64 string
	BinarySHA256ARM64 string
}

OpencodeDeliveryConfig is the design 0053 §4.2 overlay-delivery configuration passed from controller main (flags) to the reconciler. Zero value = opencode stays baked into runtimes/base (S1 default).

type OrgStatusClient

type OrgStatusClient interface {
	GetOrgStatus(ctx context.Context, orgID string) (status string, ok bool)
}

OrgStatusClient reports an org's operational status. ok is false when the status could not be determined (API unreachable and no cached value); callers MUST fail open in that case (D20). ok is true whenever a value — fresh or stale — is available.

The interface lets tests inject a fake without an HTTP server.

type OrgStatusLogger

type OrgStatusLogger interface {
	Info(msg string, keysAndValues ...any)
	Error(err error, msg string, keysAndValues ...any)
}

OrgStatusLogger is the minimal logging surface CachedOrgStatusClient uses. controller-runtime's logr.Logger satisfies it; nil disables logging.

type PodObservation

type PodObservation struct {
	Exists             bool
	Phase              corev1.PodPhase
	Reason             string
	Message            string
	ContainerReason    string
	ContainerExitCode  int32
	ContainerOOMKilled bool
	CrashLoop          bool
	Unschedulable      bool
	Scheduled          bool
}

type RecoveryPolicy

type RecoveryPolicy struct {
	MaxAttempts    int
	BackoffBase    time.Duration
	BackoffMax     time.Duration
	BackoffFactor  int
	StabilityReset time.Duration
	// ExhaustionAfter is the ConsecutiveFailures count at which the
	// recovery episode is flagged exhausted (#760): a RecoveryExhausted
	// condition, a warning Event, and a
	// WorkspaceRecoveryExhaustedTotal increment fire once, on the
	// crossing. Retries are NOT halted — backoff continues, and
	// spec.suspend=true (#699) is the operator's halt.
	ExhaustionAfter int32
}

type RelayMintRequest added in v0.34.4

type RelayMintRequest struct {
	WorkspaceID    string   `json:"workspaceID"`
	ProviderSlug   string   `json:"providerSlug"`
	BaseURL        string   `json:"baseURL"`
	ModelAllowlist []string `json:"modelAllowlist"`
	TTLSec         int64    `json:"ttlSeconds"`
	KeyID          string   `json:"keyID"`
}

RelayMintRequest is the POST /internal/v1/tokens body (US-72.2 contract).

type RelayRotateReceipt added in v0.34.4

type RelayRotateReceipt struct {
	KeyID      string `json:"keyID"`
	Generation int64  `json:"generation"`
	PublicKey  []byte `json:"publicKey"`
}

RelayRotateReceipt is the POST /internal/v1/keys/rotate response.

type RelayRouterClient added in v0.34.4

type RelayRouterClient interface {
	MintToken(ctx context.Context, req RelayMintRequest) (string, error)
	RotateKeys(ctx context.Context) (RelayRotateReceipt, error)
}

RelayRouterClient is the router internal-API surface the staging pass needs (mint + rotate). Interface for test injection.

type RelayStagingConfig added in v0.34.4

type RelayStagingConfig struct {
	RouterURL      string
	Namespace      string
	TokenTTL       time.Duration
	ProviderSource LLMProviderSource
	RouterClient   RelayRouterClient

	Now func() time.Time

	// APIReader is the DIRECT (non-cached) reader every llm-relay Secret
	// read goes through (pub, mint-key). The reconciler's cached client
	// cannot serve these: a namespace outside the cache scope fails
	// without an API call, and an informer inside it needs LIST+WATCH —
	// verbs the llm-relay Role deliberately withholds (§4.3 deviation
	// note). Writes stay on the reconciler client (controller-runtime
	// writes bypass the cache). REQUIRED at construction
	// (mgr.GetAPIReader() in production) — there is deliberately no
	// cached-client fallback.
	APIReader client.Reader
	// contains filtered or unexported fields
}

RelayStagingConfig wires the deployment flag. A nil *RelayStagingConfig on the reconciler disables staging entirely (flag off). Construction is NewRelayStagingConfig, which pins a NON-NIL redactor (design §4.9 amendment: fail-open is test-adoption only).

func NewRelayStagingConfig added in v0.34.4

func NewRelayStagingConfig(routerURL, namespace string, tokenTTL time.Duration, source LLMProviderSource, routerClient RelayRouterClient, redactor secrets.StagedKeyRedactor, apiReader client.Reader) (*RelayStagingConfig, error)

NewRelayStagingConfig constructs the production staging config. The redactor is mandatory (US-72.1 §4.9 amendment binding on US-72.3's construction path) — a nil redactor means an unregistered staged key could echo through diagnostics, which the design forbids. The reader is likewise mandatory: llm-relay reads MUST go through a DIRECT client (mgr.GetAPIReader()) — the cached reconciler client cannot serve them (review r3 finding 1) — so the production wiring cannot silently drop it.

type UploadStagingConfig added in v0.34.7

type UploadStagingConfig struct {
	BudgetBytes         int64
	CredentialFloorByte int64
	MaxConcurrent       int
	TTLMilliseconds     int64
	ApplyTimeoutMilli   int64
	// contains filtered or unexported fields
}

UploadStagingConfig carries the helm → controller → pod env knobs. Zero = unset = agentd defaults.

func ParseUploadStagingFlag added in v0.34.7

func ParseUploadStagingFlag(raw string) (UploadStagingConfig, error)

ParseUploadStagingFlag is FlagString's inverse (the controller's flag entry point). Unknown keys are REJECTED loudly — a typo'd helm knob must not silently default (Rule: explicit over implicit).

func (UploadStagingConfig) EnvVars added in v0.34.7

func (c UploadStagingConfig) EnvVars() []corev1.EnvVar

EnvVars lands the SET fields as agentd-consumed env — the exact names cmd/workspace-agentd parses (upload_staging.go / upload_apply.go). Unset fields emit nothing: agentd defaults.

func (UploadStagingConfig) FlagString added in v0.34.7

func (c UploadStagingConfig) FlagString() string

FlagString renders the --upload-staging flag payload (k=v, comma- separated; empty when fully unset — the flag can be omitted).

type WorkspaceReconciler

type WorkspaceReconciler struct {
	client.Client
	Scheme *runtime.Scheme

	// APIReader is the manager's DIRECT (non-cached) reader for
	// cluster-scoped lookups on the reconcile path (#1587): RuntimeEnvironment
	// resolution (pod_builder.go) and StorageClass WFFC detection (pvc.go).
	// A cached Get of these unwatched cluster-scoped types lazily starts an
	// informer that blocks forever when RBAC withholds the list/watch (#1551)
	// — wedging workers silently; direct reads fail fast and requeue.
	// Wired to mgr.GetAPIReader() in SetupControllers; SetupWithManager
	// refuses nil. DirectReader() serves lookups; never use r.Client for them.
	APIReader client.Reader

	// HostResolver is used by the per-workspace NetworkPolicy generator
	// (network_policy.go) to resolve declared FQDNs to /32 ipBlocks at
	// reconcile time. Tests inject a stub; production uses
	// defaultHostResolver (net.DefaultResolver) when nil.
	HostResolver HostResolver

	// InferenceRelayURL is the self-hosted relay URL for free-tier inference
	// (Epic 42 InferenceRelay fleet). When set, workspace pods route opencode
	// free-tier provider requests through this URL for IP distribution. When
	// empty (the chart default), opencode uses its default gateway
	// (opencode.ai/zen/v1) directly with the built-in `public` key.
	InferenceRelayURL string

	// OrgStatusClient, when non-nil AND the workspace belongs to an org
	// (Spec.Owner.OrgID != ""), is consulted on every Active reconcile to drive
	// D20 org-level suspension: if the org is suspended, the workspace
	// transitions Active → Suspending (pod killed, PVC retained). The client is
	// nil when --api-service-url is unset, disabling the feature. Lookups are
	// cached (30s TTL); a lookup failure fails open (workspace keeps running).
	OrgStatusClient OrgStatusClient

	// WorkspaceTerminationGraceSeconds (#1507) bounds every workspace
	// pod's graceful termination: on pod deletion kubelet SIGTERMs
	// agentd, whose serial shutdown budget (HTTP drain 25s → bg wait 5s
	// → opencode child SIGTERM→SIGKILL 5s) fits inside the default 40s.
	// The SUSPEND path deliberately does NOT wait for busy sessions
	// (#1507): this grace IS the bounded graceful opencode termination;
	// the hard cut at expiry is the deadline. 0 = controller default
	// (40s, the serial budget + margin). Sourced from
	// --workspace-termination-grace-seconds / Helm
	// controller.workspaceTerminationGraceSeconds.
	WorkspaceTerminationGraceSeconds int64

	// DefaultRuntimeClass is the container runtime class applied to all
	// workspace pods (Epic 51 S51.1). Typically "gvisor" for production
	// multi-tenant deployments to provide kernel-level isolation against
	// container escape. Empty means use the default (runc). Set via
	// --default-runtime-class controller flag, sourced from Helm
	// .Values.gvisor.defaultRuntimeClass (only set when .Values.gvisor.enabled
	// is true). Individual workspaces can override via spec.runtimeClass for
	// compatibility opt-out (admin-gated).
	DefaultRuntimeClass string

	// APIServiceURL is the in-cluster URL of the API service, used by the
	// workspace init container's bootstrap subcommand (Epic 35 US-35.4) to
	// fetch decrypted credentials via POST /internal/v1/pod-bootstrap. Same
	// value as --api-service-url (also used for OrgStatusClient). When empty,
	// the bootstrap subcommand degrades gracefully (empty secrets, pod boots
	// without credentials; the notify-pull path handles delivery).
	APIServiceURL string

	// PreviewOriginBaseDomain (Epic 66 Phase 1): when non-empty, workspace
	// pods receive PREVIEW_ORIGIN_BASE_DOMAIN so agentd's dev_preview_url
	// MCP tool emits the per-workspace-origin bootstrap URL instead of the
	// path-based URL. Sourced from the same previewOrigin.baseDomain chart
	// value the API consumes — one flag enables the deployment-wide mode.
	// Empty = path-based dev preview (unchanged behavior).
	PreviewOriginBaseDomain string

	// APIPublicURL (#1332) is the externally reachable API origin wired
	// into every container that runs agentd tooling as
	// LLMSAFESPACE_API_PUBLIC_URL — the origin the dev_preview_url MCP
	// tool bakes into user-facing URLs. Distinct from APIServiceURL
	// (in-cluster, load-bearing for bootstrap): in sidecar mode the tool
	// runs in the sidecar, whose LLMSAFESPACE_API_URL is deliberately the
	// svc coordinate. When unset and PreviewOriginBaseDomain is set, the
	// controller derives https://api.<baseDomain>; when both are unset
	// the env is omitted and the tool validates LLMSAFESPACE_API_URL,
	// refusing cluster-internal values. Same value as --api-public-url
	// (Helm value controller.apiPublicURL).
	APIPublicURL string
	// #863 agentd overlay delivery. When AgentdImage is set, buildPod pins a
	// digest-addressed image volume into every workspace pod and the
	// supervisor self-verifies the binary's sha256 against the per-arch
	// pins before exec (design 0053 — the deleted entrypoint's check
	// moved into the binary). All three fields must be set together (validated at
	// startup by validateAgentdDeliveryConfig). Empty AgentdImage = legacy
	// mode (binary baked into runtimes/base; no volume, mount, or env).
	AgentdImage             string
	AgentdBinarySHA256AMD64 string
	AgentdBinarySHA256ARM64 string

	// Design 0053 §4.2 opencode overlay delivery. When OpencodeImage is
	// set, buildPod pins a digest-addressed image volume into every
	// workspace pod (RO mount at /opencode on the workspace container
	// only) and the supervisor verifies the binary's sha256 against the
	// per-arch pins before spawn. All three fields must be set together
	// (validated at startup by validateOpencodeDeliveryConfig). Empty
	// OpencodeImage = baked-in mode (opencode stays in runtimes/base; S1
	// default — the feature is opt-in and inert until the base strip).
	OpencodeImage             string
	OpencodeBinarySHA256AMD64 string
	OpencodeBinarySHA256ARM64 string

	// AgentdSidecarEnabled (design 0051 US-2, chart-gated, default
	// false): when true, buildPod appends the native agentd sidecar and
	// switches the main container to supervise-opencode mode. Requires
	// AgentdImage (validated at startup) — the sidecar runs the same
	// digest-pinned delivery artifact.
	AgentdSidecarEnabled bool
	// UploadStaging (design 0060 PR 2.5): the env knobs landed on BOTH
	// agentd-bearing containers. Zero value = no env = agentd defaults.
	UploadStaging UploadStagingConfig

	// Recorder emits Kubernetes events on the Workspace (agentd verify
	// failures). Injected from mgr.GetEventRecorderFor() in production;
	// a FakeRecorder in tests.
	Recorder record.EventRecorder

	// MaxConcurrentReconciles: how many DIFFERENT workspaces reconcile in
	// parallel (controller-runtime never runs the same object concurrently,
	// so per-workspace state stays single-writer). 0/negative = framework
	// default (1 = fully serial). The default serial behavior made fleet
	// resume linear (~8s per workspace — 40 workspaces ≈ 5+ minutes, run
	// 33811397260); main.go clamps this to 1..64 to bound apiserver load.
	MaxConcurrentReconciles int

	// RelayStaging (Epic 72 / design 0058, US-72.3): when non-nil, every
	// Creating/Active reconcile runs the staging pass — seal bound BYO
	// llm-provider credentials into llm-relay envelope Secrets, mint scoped
	// tokens, write the workspace-namespace handoff Secret, maintain the
	// CredentialsStaged/CredentialStale/CredentialRejected conditions, and
	// run the §4.2 DR/residual-window terminator. NIL (the default —
	// --relay-only-key-delivery=false) means zero behavior change: the
	// legacy raw-key path and byte-identical batches.
	RelayStaging *RelayStagingConfig
	// contains filtered or unexported fields
}

func (*WorkspaceReconciler) Reconcile

func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)

func (*WorkspaceReconciler) SetupWithManager

func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error

Jump to

Keyboard shortcuts

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