Architecture
Package layout
Section titled “Package layout”keyledger/├── main.go CLI entry point (Cobra commands)├── provider/│ ├── provider.go Core interfaces and optional interfaces│ ├── types.go Key, KeyInventory, Actor, KeyWorkspace, KeyPattern│ ├── workspace.go WorkSpaceDisplay / WorkspaceName helpers│ └── registry.go Global provider registry (Register / All)├── providers/│ ├── openai/ OpenAI implementation│ ├── anthropic/ Anthropic implementation│ ├── aws/ AWS implementation│ ├── google/ Google Cloud implementation│ ├── mistral/ Mistral implementation (session-based auth)│ ├── groq/ Stub│ ├── cohere/ Stub│ └── ... Other stubs├── internal/│ ├── ci/ Non-interactive runner functions (watch…)│ ├── credentials/│ │ ├── providers.go Credential field names and env var mappings per provider│ │ └── search.go Auto-discovery of local credentials (SSO profiles, etc.)│ ├── inventory/│ │ ├── collector.go Parallel provider collection│ │ ├── health.go Risk scoring (ScoreKey, Summarize)│ │ └── workspaces.go WorkspaceTree — per-provider scope summaries│ ├── kratos/│ │ └── login.go Ory Kratos browser-flow login (2-step, cookie jar)│ ├── snapshot/ Snapshot serialisation / diff│ ├── store/│ │ ├── store.go SQLite store (provider config, settings, encrypted creds, snapshots)│ │ ├── snapshots.go Snapshot list / delete operations│ │ ├── config.go Config struct and loader│ │ └── defaults.go Default thresholds and paths│ ├── tui/ Bubble Tea TUI (app, views, messages)│ └── unsealapi/ Headless HTTPS unseal API for Docker / CI└── docs/ Astro Starlight documentation siteData flow
Section titled “Data flow”main.go └── loadConfigAndStore() ├── store.Load() read settings from SQLite └── store.Open() open SQLite, register as encrypted credential store
TUI / CI runner └── inventory.Collect() ├── for each enabled provider (parallel, up to cfg.Parallel goroutines) │ ├── keyring.LoadCredentials(name) read from encrypted SQLite store │ ├── provider.ListKeys(ctx, cfg) call provider admin API │ └── inventory.ScoreKey(key, thresholds) compute risk score └── return Result{ Inventories, Errors }Provider registration
Section titled “Provider registration”Providers register themselves via provider.Register(p) called from init(). Fully-implemented providers are activated by a blank import in main.go:
import ( _ "github.com/riptideslabs/keyledger/providers/openai" _ "github.com/riptideslabs/keyledger/providers/anthropic" // …)The registry is a simple slice protected by a mutex. provider.All() returns all registered providers in registration order.
Credential handling
Section titled “Credential handling”Credentials are never stored in the config file. The flow is:
- User enters a credential in the TUI or via the unseal API
store.EncSet(provider, field, value)is called- The value is encrypted with AES-256-GCM and written to the
enc_credentialsSQLite table
At collection time, store.EncGet(provider, field) reads all fields for a provider from the encrypted store and returns them as map[string]string. The store must be unlocked (password entered) before credentials can be read. This map is passed to the provider as ProviderConfig.Credentials.
Risk scoring
Section titled “Risk scoring”inventory.ScoreKey(k *provider.Key, t store.Thresholds) runs after every ListKeys call and sets Key.RiskScore and Key.RiskReasons in-place. No network calls — purely time-based arithmetic on the key’s AgeDays and IdleDays fields.
TUI architecture
Section titled “TUI architecture”The TUI uses Bubble Tea with a single root model in internal/tui/app.go. Sub-models handle individual screens and communicate via typed messages defined in messages.go:
| Sub-model | File | Purpose |
|---|---|---|
keysViewModel | keys_view.go | Keys table with filter, sort, WorkSpaces tab |
scopesViewModel | workspaces_view.go | Collapsible workspace/project tree |
healthViewModel | health_view.go | Risk-grouped key list |
diffViewModel | diff_view.go | Snapshot vs live diff |
snapshotsModel | snapshots_view.go | Snapshot list, delete, export |
providersModel | providers_screen.go | Provider config and credential management |
statusBarModel | status_bar.go | Context-sensitive footer with key bindings |
Async operations (provider refresh, Kratos login, snapshot save/delete) return tea.Cmd functions that run in goroutines and send result messages back to the update loop.
switchView(v ViewID) is the single place that changes the active view; it also calls updateStatusBar() immediately so the footer always reflects the current context.