shell

package
v0.9.2 Latest Latest
Warning

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

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

Documentation

Overview

Package shell is the desktop shell's lifecycle logic (ADR-0035 §3a): plain Go functions — load a config, start/stop the in-process core under the reload supervisor, the ephemeral-port policy, per-cycle admin-bearer provisioning, secret provisioning from the OS keychain (the SecretStore seam; real backend in the keyring subpackage), status, and the same-origin admin proxy the WebView rides (ProxyHandler, ADR-0035 §3b) — with NO Wails import, so everything tests as ordinary Go with -race. The thin Wails adapter in cmd/korvun-desktop wraps this package.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrBindingTimeout names a call that outlived its deadline. The
	// underlying operation CONTINUES in the background (a mutex acquisition
	// is uncancellable); its terminal result is logged and the status
	// polling reconciles the truth.
	ErrBindingTimeout = errors.New("shell: binding call timed out")
	// ErrLifecycleBusy is returned while a Start/Stop is already in flight
	// (including an abandoned, timed-out one that has not yet landed) — the
	// serialization that keeps timed-out goroutines from piling on the mutex.
	ErrLifecycleBusy = errors.New("shell: a lifecycle operation is already in flight")
)

THE LAW's sentinels (SP6 design spec FR-WIN-2): every binding call is bounded — never an unbounded wait from the UI into the Controller's mutex.

View Source
var (
	// ErrNoConfig is returned by Start when no config has been loaded yet.
	ErrNoConfig = errors.New("shell: no config loaded")
	// ErrAlreadyRunning is returned by Start while the core is running.
	ErrAlreadyRunning = errors.New("shell: core already running")
	// ErrNotRunning is returned by Stop while the core is stopped.
	ErrNotRunning = errors.New("shell: core not running")
	// ErrRunning is returned by LoadConfig while the core is running:
	// switching config files is a stopped-state operation (live mutation is
	// the builder/reload path's job, not the shell's).
	ErrRunning = errors.New("shell: cannot load a config while the core is running")
)

Sentinel errors of the Controller's state machine (design spec AS-7). They wrap nothing: each names a caller mistake, not a system failure.

View Source
var ErrNoSecretStore = errors.New("shell: no secret store configured")

ErrNoSecretStore is returned by SetSecret/DeleteSecret when the Controller was built without a store (WithSecretStore not supplied).

View Source
var ErrSecretNotFound = errors.New("shell: secret not found in the keychain")

ErrSecretNotFound is the SecretStore miss sentinel: the named entry does not exist in the keychain. A miss is NOT a provisioning failure — the boot then fails loudly with the core's ErrMissingSecret naming the variable.

Functions

func DefaultConfigPath

func DefaultConfigPath() (string, error)

DefaultConfigPath is the desktop's default config location: <os.UserConfigDir>/korvun/korvun.json — the exact pattern the core uses for the default korvun.db, so config and DB share one per-user directory (SP5 design spec FR-FIRST-1).

func EnsureAdminBlock added in v0.9.1

func EnsureAdminBlock(path string) (changed bool, err error)

EnsureAdminBlock upgrades an existing config at path for the builder's mutation surface (v0.9.1, app-audit finding A4): without an admin block the core mounts neither /api/config nor /builder, yet the desktop embeds the Builder unconditionally — the tab boots a 404 trap. A missing block gains token_env KORVUN_ADMIN_TOKEN (the shell-managed per-cycle bearer, controller.newAdminToken: generated in-process, never persisted); a present block — any token_env — is not touched, not even an identical rewrite. WHERE the admin server binds is untouched here: the shell's ephemeral loopback override (withEphemeralAdmin) stays the only bind authority. Same raw-object discipline as EnsureChatBlocks: unknown fields survive verbatim, the write is atomic.

func EnsureChatBlocks added in v0.7.0

func EnsureChatBlocks(path string) (changed bool, err error)

EnsureChatBlocks upgrades an existing config at path for the chat piece (operator-console close, 2026-08-08): the Chat tab requires the storage and session blocks, and a config written before the piece lacks them, so the desktop runs this right after EnsureDefaultConfig on mount — fresh installs get the blocks from the template, existing installs get exactly the missing ones added here (as empty blocks: the DB path and the reset triggers resolve to their defaults at boot). The file is read as a raw JSON object so fields this build does not know survive verbatim; the rewrite normalizes layout (canonical MarshalIndent, keys sorted) but never drops or edits a value. When both blocks are already present the file is not touched at all — not even an identical rewrite — keeping FR-FIRST-2's steady-state guarantee. The write is atomic (temp file + rename in the same directory), the WriteConfigAtomic discipline.

func EnsureDefaultConfig

func EnsureDefaultConfig(path string) (created bool, err error)

EnsureDefaultConfig provisions the first-run config at path (FR-FIRST-2): if the file exists it is not touched (not even an identical rewrite) and the return is (false, nil); if absent, the parent directory is created 0o700 when it does not yet exist (an existing directory keeps its mode) and the embedded template is written ATOMICALLY via supervisor.WriteConfigAtomic (temp file + rename — a failure leaves no partial file), returning (true, nil). The never-overwrite guarantee is check-then-write, not a single atomic operation: a file created by another process inside the stat→rename window would be replaced (by this same template — two racing first-runs of the shell converge on identical bytes, so the window is benign for the only realistic racer). The two outcomes let the caller drive different first-run UI.

func SecretEnvNames

func SecretEnvNames(cfg *config.Config) []string

SecretEnvNames enumerates the secret env-var NAMES the config references: every channel token_env and every model api_key_env (non-empty), deduplicated and sorted for determinism. admin.token_env is deliberately EXCLUDED — that is the SP2 per-cycle bearer, generated in-process, never stored in a keychain.

Types

type Controller

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

Controller is the desktop shell's lifecycle logic (ADR-0035 §§1, 3a, 4, 6) as plain framework-free Go: it loads a config, runs the in-process core under the reload supervisor (the builder's mount precondition), enforces the ephemeral-port policy, provisions the per-cycle admin bearer, and reports status. It never imports Wails (doc.go contract); the thin Wails adapter in cmd/korvun-desktop wraps it.

func New

func New(opts ...Option) *Controller

New builds a stopped Controller.

func (*Controller) DeleteSecret

func (c *Controller) DeleteSecret(name string) error

DeleteSecret removes a secret from the keychain (no orphans: the entry is deleted, never emptied). Effective on the NEXT cycle, like SetSecret.

func (*Controller) LoadConfig

func (c *Controller) LoadConfig(path string) error

LoadConfig loads and validates the config file at path (config.Load: every failure is fatal and names what is wrong). It is a stopped-state operation: while the core runs it returns ErrRunning.

func (*Controller) ProxyHandler

func (c *Controller) ProxyHandler() http.Handler

ProxyHandler returns the desktop asset seam (ADR-0035 §3b, SP4 design spec): an http.Handler cmd/korvun-desktop mounts as the Wails assetserver.Options.Handler. It reverse-proxies the core's admin surface (/api/*, /builder/*, /ui/*, /healthz, /metrics) to the CURRENT cycle's effective admin address — resolved per request, never cached across cycles — injecting the per-cycle admin bearer server-side so the token never enters the DOM (ADR-0035 §4). With the core stopped the proxied routes answer a stable honest 503 (`{"error":"core stopped"}`) the shell chrome can render, and any other path answers a small 404: the shell's own assets are the AssetServer's Assets side, never this handler's job (ADR-0035 §3c).

func (*Controller) SecretInKeychain

func (c *Controller) SecretInKeychain(name string) (bool, error)

SecretInKeychain reports whether the keychain holds an entry named name — PRESENCE only (SP6c wizard's "Comprobar entorno"). The seam's Get reads the value internally; it is dropped on this line and never crosses the boundary, is never logged, and never reaches a caller. No store configured reads as not-present (the honest answer, not an error); a miss is false; any other store failure surfaces as an error.

func (*Controller) SetSecret

func (c *Controller) SetSecret(name, value string) error

SetSecret writes a secret to the keychain (the future UI's path, FR-SEC-5). It takes effect on the NEXT cycle — provisioning happens at Start.

func (*Controller) Start

func (c *Controller) Start(ctx context.Context) error

Start boots the core under the reload supervisor and returns once the core CONFIRMED its Start (admin bound, channels started) or failed boot (the supervisor error is returned and the controller stays stopped). ctx bounds only the wait; the running core is stopped by Stop, never by ctx. Start holds the controller lock for the whole boot wait, so concurrent Status/ LoadConfig calls block until the boot resolves (boots are sub-second; a stuck boot is bounded by ctx).

func (*Controller) Status

func (c *Controller) Status() Status

Status reports the shell's view of the core.

func (*Controller) Stop

func (c *Controller) Stop(ctx context.Context) error

Stop cancels the supervisor and waits for a clean teardown, bounded by ctx. On a clean stop it returns nil, unsets the per-cycle bearer, and the controller is startable again. If ctx expires first the controller keeps its running state (the teardown is still in flight) and returns ctx's error — the caller retries with a longer deadline. Stop holds the controller lock through the teardown wait (see Start's locking note).

type Desktop

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

Desktop is the window's binding surface (ADR-0035 §3a, SP6 spec FR-WIN-2): plain framework-free Go over the Controller — cmd/korvun-desktop passes it to Wails' Bind and the generated JS calls arrive here. Every method obeys THE LAW via goroutine+select; the Wails glue adds nothing.

func NewDesktop

func NewDesktop(ctrl *Controller, opts ...DesktopOption) *Desktop

NewDesktop builds the binding surface over ctrl.

func (*Desktop) CheckOllama

func (d *Desktop) CheckOllama(baseURL string) OllamaCheck

CheckOllama probes {baseURL}/api/tags with the check deadline (onboarding step 1). Failures come back as an honest unreachable outcome with detail.

func (*Desktop) CheckSecretPresence

func (d *Desktop) CheckSecretPresence(name string) (SecretPresence, error)

CheckSecretPresence resolves whether the named variable is present in the ENVIRONMENT and/or the OS keychain — the wizard's "Comprobar entorno · sin leer su valor" (SP6c). Keychain deadline class: a Secret Service unlock prompt is a legitimate slow path. The name is shape-gated so this cannot enumerate arbitrary environment variables.

func (*Desktop) DefaultConfigPath

func (d *Desktop) DefaultConfigPath() (string, error)

DefaultConfigPath resolves the desktop's default config location (pure, no Controller involved).

func (*Desktop) DeleteSecret

func (d *Desktop) DeleteSecret(name string) error

DeleteSecret removes a secret from the OS keychain (keychain class).

func (*Desktop) EnsureDefaultConfig

func (d *Desktop) EnsureDefaultConfig() (bool, error)

EnsureDefaultConfig provisions the first-run template at the default path (the arg-less composition the spec fixes: resolve, then ensure). created = true is the chrome's onboarding trigger (ADR-0035 §5). It then heals EXISTING configs: the chat piece (EnsureChatBlocks — storage and session blocks, so the Chat tab never boots dead) and the builder's mutation surface (EnsureAdminBlock — an admin-less config left the Builder tab a 404 trap; v0.9.1, app-audit A4).

func (*Desktop) LoadConfig

func (d *Desktop) LoadConfig(path string) error

LoadConfig loads and validates the config file at path (bounded read; stopped-state operation, exactly the Controller's contract).

func (*Desktop) SetSecret

func (d *Desktop) SetSecret(name, value string) error

SetSecret writes a secret to the OS keychain (wizard step 3; keychain deadline class — an OS unlock prompt is a legitimate slow path).

func (*Desktop) Start

func (d *Desktop) Start() error

Start boots the core (bounded lifecycle call; the ctx handed to the Controller carries the same deadline, bounding the boot wait itself).

func (*Desktop) Status

func (d *Desktop) Status() (Status, error)

Status reports the shell's view of the core (bounded read).

func (*Desktop) Stop

func (d *Desktop) Stop() error

Stop tears the core down (bounded lifecycle call).

func (*Desktop) Version

func (d *Desktop) Version() string

Version reports the build-stamped version (the logo/version row).

type DesktopOption

type DesktopOption func(*Desktop)

DesktopOption configures NewDesktop.

func WithDesktopLogger

func WithDesktopLogger(l *slog.Logger) DesktopOption

WithDesktopLogger sets the structured logger. A nil logger is ignored.

func WithDesktopVersion

func WithDesktopVersion(v string) DesktopOption

WithDesktopVersion stamps the version string Version reports (the chrome's logo/version row); unset it reads "dev".

type OllamaCheck

type OllamaCheck struct {
	Reachable bool   `json:"reachable"`
	Detail    string `json:"detail"`
}

OllamaCheck is CheckOllama's outcome: reachability is a RESULT the onboarding paints, never a Go error.

type Option

type Option func(*Controller)

Option configures New.

func WithBuildOptions

func WithBuildOptions(opts ...app.Option) Option

WithBuildOptions appends extra app.Build/app.Preflight options to the shell's build seam — the embedding/test seam that lets the lifecycle suite boot a full real App with fake channels (no network, ADR-0034 discipline).

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger. A nil logger is ignored.

func WithSecretStore

func WithSecretStore(s SecretStore) Option

WithSecretStore injects the keychain seam. Without it (nil, the default) secret provisioning is skipped entirely — the SP2 behavior, byte for byte.

type SecretPresence

type SecretPresence struct {
	InEnv      bool `json:"inEnv"`
	InKeychain bool `json:"inKeychain"`
}

SecretPresence is CheckSecretPresence's outcome: PRESENCE booleans only — no field can carry a value, by construction (the ADR-0024 frame's discipline applied to secrets).

type SecretStore

type SecretStore interface {
	Get(name string) (string, error)
	Set(name, value string) error
	Delete(name string) error
}

SecretStore is the OS-keychain seam (ADR-0035 §4, ADR-0037): service `korvun`, account = env-var NAME, value = the secret. Get returns ErrSecretNotFound on a miss; Delete REMOVES the entry (no orphaned or emptied entries). The real backend lives in internal/shell/keyring; tests use an in-memory double.

type Status

type Status struct {
	// Running reports whether the core is up (a successful Start not yet
	// followed by Stop).
	Running bool
	// ConfigPath is the loaded config file's path ("" before LoadConfig).
	ConfigPath string
	// AdminAddr is the admin server's EFFECTIVE bound address while running
	// ("" when stopped) — the real ephemeral port, state only the running
	// core knows.
	AdminAddr string
	// TokenEnv is the NAME of the admin-bearer env var the shell rotates
	// each cycle ("" when no config is loaded or the config has no admin
	// block). A name only, never a value (ADR-0035 §4) — the chrome's
	// Settings security row paints it.
	TokenEnv string
}

Status is the shell's view of the core (design spec FR-7).

Directories

Path Synopsis
Package keyring is the real OS-keychain backend behind shell.SecretStore (ADR-0035 §4, ADR-0037): Keychain Services on macOS, Credential Manager on Windows, Secret Service D-Bus on Linux — via zalando/go-keyring, the only package in the repo that imports it.
Package keyring is the real OS-keychain backend behind shell.SecretStore (ADR-0035 §4, ADR-0037): Keychain Services on macOS, Credential Manager on Windows, Secret Service D-Bus on Linux — via zalando/go-keyring, the only package in the repo that imports it.
Package logsink is the desktop's file log sink (v0.9.1, app-audit finding B3).
Package logsink is the desktop's file log sink (v0.9.1, app-audit finding B3).

Jump to

Keyboard shortcuts

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