settings

package
v1.3.0 Latest Latest
Warning

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

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

Documentation

Overview

Package settings holds Ken's operator-editable runtime configuration. Values come from env/compiled defaults, are overridden by rows in app_setting (written by the web UI), and are exposed as an atomically-swapped Snapshot that live consumers (rate limiter, login guard, client-IP resolver, ACME host policy) read without a restart.

Index

Constants

This section is empty.

Variables

View Source
var Fields = []Field{
	{Key: "rl_enabled", Group: "Rate limiting", Label: "Enabled", Type: "bool", Live: true,
		Get: func(v Values) string { return boolStr(v.RLEnabled) },
		Set: func(v *Values, s string) error { v.RLEnabled = truthy(s); return nil }},
	intField("rl_ip_rpm", "Rate limiting", "Per-IP requests / minute", "Sustained per-IP request rate.",
		func(v Values) int { return v.IPPerMin }, func(v *Values, n int) { v.IPPerMin = n }, 1, 1_000_000),
	intField("rl_ip_burst", "Rate limiting", "Per-IP burst", "Bucket size — absorbs short bursts (page loads).",
		func(v Values) int { return v.IPBurst }, func(v *Values, n int) { v.IPBurst = n }, 1, 1_000_000),
	intField("rl_token_rpm", "Rate limiting", "Per-token requests / minute", "Per agent token (MCP).",
		func(v Values) int { return v.TokenPerMin }, func(v *Values, n int) { v.TokenPerMin = n }, 1, 1_000_000),
	intField("rl_token_burst", "Rate limiting", "Per-token burst", "",
		func(v Values) int { return v.TokenBurst }, func(v *Values, n int) { v.TokenBurst = n }, 1, 1_000_000),
	intField("rl_block_after", "Rate limiting", "Auto-block after", "Consecutive over-limit rejections before an IP is blocked (0 = never block).",
		func(v Values) int { return v.BlockAfter }, func(v *Values, n int) { v.BlockAfter = n }, 0, 1_000_000),
	intField("rl_lockout_sec", "Rate limiting", "Auto-block lockout (seconds)", "",
		func(v Values) int { return v.LockoutSec }, func(v *Values, n int) { v.LockoutSec = n }, 1, 7*24*3600),
	{Key: "rl_allow_cidrs", Group: "Rate limiting", Label: "Always-allowed CIDRs", Type: "cidrs", Live: true,
		Help: "Comma-separated CIDRs exempt from rate limiting (loopback is always exempt).",
		Get:  func(v Values) string { return v.AllowCIDRs },
		Set:  setCIDRs(func(v *Values, s string) { v.AllowCIDRs = s })},

	intField("login_max_fails", "Login", "Max failed logins", "Failures from one IP before a lockout.",
		func(v Values) int { return v.LoginMaxFails }, func(v *Values, n int) { v.LoginMaxFails = n }, 1, 10000),
	intField("login_lockout_sec", "Login", "Login lockout (seconds)", "",
		func(v Values) int { return v.LoginLockoutSec }, func(v *Values, n int) { v.LoginLockoutSec = n }, 1, 7*24*3600),
	intField("session_ttl_hours", "Session", "Session lifetime (hours)", "New sessions only.",
		func(v Values) int { return v.SessionTTLHours }, func(v *Values, n int) { v.SessionTTLHours = n }, 1, 24*30),

	{Key: "trusted_proxies", Group: "Network", Label: "Trusted proxy CIDRs", Type: "cidrs", Live: true,
		Help: "X-Forwarded-For is honored only from these peers. Blank = none. Sensitive: over-broad values let a client forge its IP.",
		Get:  func(v Values) string { return v.TrustedProxies },
		Set:  setCIDRs(func(v *Values, s string) { v.TrustedProxies = s })},

	{Key: "tls_mode", Group: "TLS", Label: "TLS mode", Type: "enum", ReadOnly: true,
		Help: "Set via KEN_TLS in the unit; a mode switch (off/acme/file) needs a service restart.",
		Get:  func(v Values) string { return v.TLSMode }},
	{Key: "tls_domains", Group: "TLS", Label: "ACME domains", Type: "domains", Live: true,
		Help: "Comma-separated hostnames the Let's Encrypt cert is issued for. Live (acme mode); a new host is issued on demand.",
		Get:  func(v Values) string { return v.TLSDomains },
		Set:  setDomains(func(v *Values, s string) { v.TLSDomains = s })},
	{Key: "tls_email", Group: "TLS", Label: "ACME account email", Type: "email", ReadOnly: true,
		Help: "Set via KEN_TLS_EMAIL; used when registering the Let's Encrypt account (not editable here).",
		Get:  func(v Values) string { return v.TLSEmail }},

	{Key: "curation_langs", Group: "Curation", Label: "Curation language(s)", Type: "langs", Live: true,
		Help: "Comma-separated language codes you can read (e.g. fr,zh). Agents are told to author entries in these so you can review and promote them; proposals outside them are flagged on the review queue. Blank = off.",
		Get:  func(v Values) string { return v.CurationLangs },
		Set:  setLangs(func(v *Values, s string) { v.CurationLangs = s })},

	intField("comm_max_body_bytes", "Inter-session comms", "Max message size (bytes)",
		"Message bodies are atomic; there is no multi-part send. Keep this small — tool arguments are generated token by token by a model, so even 64 KiB is a five-figure token count.",
		func(v Values) int { return v.CommMaxBodyBytes }, func(v *Values, n int) { v.CommMaxBodyBytes = n }, 256, 1<<20),
	intField("comm_max_unacked", "Inter-session comms", "Max unacknowledged per channel",
		"Backpressure. Past this a send is refused so two auto-processing sessions cannot loop unboundedly.",
		func(v Values) int { return v.CommMaxUnacked }, func(v *Values, n int) { v.CommMaxUnacked = n }, 1, 100_000),
	intField("comm_message_ttl_sec", "Inter-session comms", "Message lifetime (seconds)",
		"How long an unacknowledged message stays deliverable before it expires. A TTL is not a quota.",
		func(v Values) int { return v.CommMessageTTLSec }, func(v *Values, n int) { v.CommMessageTTLSec = n }, 60, 30*24*3600),
	intField("comm_metadata_ttl_sec", "Inter-session comms", "Metadata retention (seconds)",
		"How long a settled message's audit row survives. Bodies are deleted at acknowledgement regardless; this governs only the shell an operator can investigate.",
		func(v Values) int { return v.CommMetadataTTLSec }, func(v *Values, n int) { v.CommMetadataTTLSec = n }, 60, 90*24*3600),
	intField("comm_reply_deadline_sec", "Inter-session comms", "Reply deadline (seconds)",
		"Default deadline for a message that requires a response; past it the sender is told the reply is overdue instead of waiting forever.",
		func(v Values) int { return v.CommReplyDeadlineS }, func(v *Values, n int) { v.CommReplyDeadlineS = n }, 30, 7*24*3600),
	intField("comm_pairing_code_ttl_sec", "Inter-session comms", "Pairing code lifetime (seconds)",
		"How long a code you mint stays usable. Short is good: it only has to survive being pasted into two sessions.",
		func(v Values) int { return v.CommPairingCodeTTLS }, func(v *Values, n int) { v.CommPairingCodeTTLS = n }, 30, 24*3600),
	intField("comm_poll_wait_max_sec", "Inter-session comms", "Max long-poll wait (seconds)",
		"Ceiling on how long a receive call may block. Clamped to 30 in code regardless: a wait that ties the client's tool timeout turns a successful empty poll into an error.",
		func(v Values) int { return v.CommPollWaitMaxSec }, func(v *Values, n int) { v.CommPollWaitMaxSec = n }, 1, 30),
	intField("comm_provenance_window_sec", "Inter-session comms", "Hearsay window (seconds)",
		"If a token received an inter-session message this recently, entries it authors are flagged on the review queue as possibly second-hand. 0 disables the flag.",
		func(v Values) int { return v.CommProvenanceWindowSec }, func(v *Values, n int) { v.CommProvenanceWindowSec = n }, 0, 7*24*3600),

	{Key: "comm_files_enabled", Group: "Inter-session comms", Label: "File exchange enabled", Type: "bool", Live: true,
		Help: "Lets paired sessions exchange files (same-host handoff, or relayed through Ken). Off by default — the relay stores bytes on this server's disk.",
		Get:  func(v Values) string { return boolStr(v.CommFilesEnabled) },
		Set:  func(v *Values, s string) error { v.CommFilesEnabled = truthy(s); return nil }},
	intField("comm_file_max_mb", "Inter-session comms", "Max file size (MB)",
		"Cap on one relayed or offered file.",
		func(v Values) int { return v.CommFileMaxMB }, func(v *Values, n int) { v.CommFileMaxMB = n }, 1, 1024),
	intField("comm_file_budget_mb", "Inter-session comms", "Relay storage budget (MB)",
		"Global cap on bytes held in the relay at once. The relay shares this server's disk with the knowledge base; filling it would fail durable writes over chat traffic.",
		func(v Values) int { return v.CommFileBudgetMB }, func(v *Values, n int) { v.CommFileBudgetMB = n }, 1, 100_000),
	intField("comm_file_min_free_mb", "Inter-session comms", "Free-space floor (MB)",
		"Uploads are refused when the disk has less than this free, even under budget, so the knowledge base always has headroom. 0 disables the floor.",
		func(v Values) int { return v.CommFileMinFreeMB }, func(v *Values, n int) { v.CommFileMinFreeMB = n }, 0, 1_000_000),
	intField("comm_file_ttl_sec", "Inter-session comms", "File lifetime (seconds)",
		"How long an offered or undelivered file survives before it expires and its bytes are deleted.",
		func(v Values) int { return v.CommFileTTLSec }, func(v *Values, n int) { v.CommFileTTLSec = n }, 60, 30*24*3600),
	intField("comm_grant_ttl_sec", "Inter-session comms", "Transfer grant lifetime (seconds)",
		"How long a one-time upload/download URL stays valid. Short is right: it only has to survive being handed to curl.",
		func(v Values) int { return v.CommGrantTTLSec }, func(v *Values, n int) { v.CommGrantTTLSec = n }, 60, 3600),
	intField("comm_endpoint_idle_sec", "Inter-session comms", "Idle session cleanup (seconds)",
		"How long a registered session with no message traffic survives before it is removed. An actively polling session refreshes its last-seen and is never removed; this only reaps sessions that registered and went away. Keep it comfortably longer than a working session's idle gaps.",
		func(v Values) int { return v.CommEndpointIdleTTLSec }, func(v *Values, n int) { v.CommEndpointIdleTTLSec = n }, 300, 90*24*3600),
}

Fields is the ordered registry that drives both the form and validation.

Functions

This section is empty.

Types

type Field

type Field struct {
	Key, Group, Label, Help, Type string // type: int | bool | cidrs | domains | langs | email | text | enum
	Live                          bool   // applies live vs needs a restart
	ReadOnly                      bool   // display only
	Get                           func(Values) string
	Set                           func(*Values, string) error
}

Field describes one editable setting: how to render it and how to parse/validate it into Values.

type Live

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

Live holds the current snapshot atomically and applies edits.

func New

func New(st *store.Store, defaults Values) *Live

New builds a Live seeded with defaults (env-derived). Call Load to fold in the persisted overrides.

func (*Live) Apply

func (l *Live) Apply(ctx context.Context, form map[string]string, updater string) (*Snapshot, []string)

Apply validates the submitted form, persists the diffs from default, and swaps the new snapshot in live. Returns the resulting snapshot and any field errors (on error nothing is persisted or applied).

func (*Live) Current

func (l *Live) Current() *Snapshot

Current returns the effective snapshot.

func (*Live) Defaults

func (l *Live) Defaults() Values

Defaults returns the compiled/env defaults (before persisted overrides).

func (*Live) Load

func (l *Live) Load(ctx context.Context) error

Load reads persisted overrides on top of the defaults and swaps them in.

func (*Live) OnChange

func (l *Live) OnChange(f func(*Snapshot))

OnChange registers a listener fired (synchronously) whenever the snapshot swaps.

type Snapshot

type Snapshot struct {
	Values
	Resolver  *clientip.Resolver
	AllowNets []*net.IPNet
	Domains   []string
	// CurationLangSet is the normalized curation languages (lowercased BCP-47
	// PRIMARY subtags, de-duplicated, in order) — read by the MCP instructions
	// and (later) the review-queue guardrail. Empty ⇒ feature off.
	CurationLangSet []string
}

Snapshot is Values plus the derived objects consumers read.

type Values

type Values struct {
	RLEnabled   bool
	IPPerMin    int
	IPBurst     int
	TokenPerMin int
	TokenBurst  int
	BlockAfter  int
	LockoutSec  int
	AllowCIDRs  string

	LoginMaxFails   int
	LoginLockoutSec int
	SessionTTLHours int

	TrustedProxies string

	TLSMode    string // read-only display (a mode switch needs a listener restart)
	TLSDomains string // acme hostnames — live
	TLSEmail   string // acme account email — live (affects new registrations)

	// CurationLangs is the operator's comma-separated list of language codes the
	// human curator can read (e.g. "fr,zh"). Blank ⇒ the feature is off: agents get
	// no language guidance and nothing is flagged. Stored verbatim; the normalized
	// set consumers read is derived into Snapshot.CurationLangSet.
	CurationLangs string

	// Inter-session communication (COMM) limits. These are inert unless COMM is
	// enabled (KEN_COMM_ENABLED), which is a restart-level choice because it opens
	// a second database; everything here applies live on top of it.
	//
	// They are bounds on an EPHEMERAL subsystem that shares a disk with the durable
	// knowledge base, so the defaults are deliberately conservative: the failure
	// they guard against is message traffic filling the volume and failing KB writes.
	CommMaxBodyBytes    int
	CommMaxUnacked      int
	CommMessageTTLSec   int
	CommMetadataTTLSec  int
	CommReplyDeadlineS  int
	CommPairingCodeTTLS int
	CommPollWaitMaxSec  int
	// CommProvenanceWindowSec is how recently a token must have RECEIVED an
	// inter-session message for a version it authors to be marked as possible
	// hearsay (docs/COMM.md §7). 0 disables the marking.
	CommProvenanceWindowSec int

	// File exchange (docs/COMM.md §11) — gated separately from COMM itself because
	// the byte relay is the bulk of the subsystem's risk. Sizes are in MB in the
	// form (an operator thinks in MB); converted to bytes where enforced.
	CommFilesEnabled  bool
	CommFileMaxMB     int
	CommFileBudgetMB  int
	CommFileMinFreeMB int
	CommFileTTLSec    int
	CommGrantTTLSec   int
	// CommEndpointIdleTTLSec is how long a session endpoint with no traffic and no
	// live attachment survives before the sweeper removes it (its channels cascade).
	// Sessions register once and never unregister, so without a positive value the
	// row set grows forever — but a non-positive value must DISABLE the sweep, never
	// mean "sweep everything now" (see the sweep guard in internal/comm).
	CommEndpointIdleTTLSec int
}

Values are the raw, editable settings.

func DefaultsFromEnv

func DefaultsFromEnv() Values

DefaultsFromEnv builds the baseline from the same env vars the components read, so the settings UI starts from what the operator configured at launch.

Jump to

Keyboard shortcuts

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