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 ¶
- Variables
- func DefaultConfigPath() (string, error)
- func EnsureAdminBlock(path string) (changed bool, err error)
- func EnsureChatBlocks(path string) (changed bool, err error)
- func EnsureDefaultConfig(path string) (created bool, err error)
- func SecretEnvNames(cfg *config.Config) []string
- type Controller
- func (c *Controller) DeleteSecret(name string) error
- func (c *Controller) LoadConfig(path string) error
- func (c *Controller) ProxyHandler() http.Handler
- func (c *Controller) SecretInKeychain(name string) (bool, error)
- func (c *Controller) SetSecret(name, value string) error
- func (c *Controller) Start(ctx context.Context) error
- func (c *Controller) Status() Status
- func (c *Controller) Stop(ctx context.Context) error
- type Desktop
- func (d *Desktop) CheckOllama(baseURL string) OllamaCheck
- func (d *Desktop) CheckSecretPresence(name string) (SecretPresence, error)
- func (d *Desktop) DefaultConfigPath() (string, error)
- func (d *Desktop) DeleteSecret(name string) error
- func (d *Desktop) EnsureDefaultConfig() (bool, error)
- func (d *Desktop) LoadConfig(path string) error
- func (d *Desktop) SetSecret(name, value string) error
- func (d *Desktop) Start() error
- func (d *Desktop) Status() (Status, error)
- func (d *Desktop) Stop() error
- func (d *Desktop) Version() string
- type DesktopOption
- type OllamaCheck
- type Option
- type SecretPresence
- type SecretStore
- type Status
Constants ¶
This section is empty.
Variables ¶
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.
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.
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).
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 ¶
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
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
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 ¶
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 ¶
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 (*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 ¶
DefaultConfigPath resolves the desktop's default config location (pure, no Controller involved).
func (*Desktop) DeleteSecret ¶
DeleteSecret removes a secret from the OS keychain (keychain class).
func (*Desktop) EnsureDefaultConfig ¶
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 ¶
LoadConfig loads and validates the config file at path (bounded read; stopped-state operation, exactly the Controller's contract).
func (*Desktop) SetSecret ¶
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 ¶
Start boots the core (bounded lifecycle call; the ctx handed to the Controller carries the same deadline, bounding the boot wait itself).
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 ¶
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 ¶
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 ¶
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 ¶
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).
Source Files
¶
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). |