mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
442dcbdc6a
console/admin/settings gains a Decision provider section with a provider select, an enable toggle, a write-only API key stored encrypted, a model pin, and the privacy line. It is backed by GET/PUT /api/v1/admin/decision-provider. - The admin setting is a third source to decision.ResolveNamed: config file < admin setting < environment, per field. Resolve is now implemented on ResolveNamed, so the page's "set by environment" comes from the same pass that builds the provider. - Changing the setting rebuilds the provider and swaps the runner in place. The tick and the write doors' enqueue resolver both follow the swap, so no restart is needed. A swap cancels any pass in flight, so a disabled provider is not called again; the resolver is installed before the runner is published; and no tick can start after Stop(). - The key is encrypted through the PAD_ENCRYPTION_KEY path (store.SetSecretPlatformSetting). It is never returned by any read, masked or otherwise, and it is not writable through /admin/settings. The store refuses a secret write without an encryption key (`pad server` never reaches that state), and it refuses to READ a secret row that is not ciphertext. - A PUT to a field the environment sets is refused with 409 set_by_environment. On Pad Cloud the environment is the only source and PUT answers 403 managed_by_operator. - `pad server info` still resolves only the file and the environment on the CLI host. It now says so, in an additive `sources` JSON member and in its human line. Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// GetPlatformSetting returns a single platform setting value, or empty string if not set.
|
|
func (s *Store) GetPlatformSetting(key string) (string, error) {
|
|
return s.GetPlatformSettingQ(s.db, key)
|
|
}
|
|
|
|
// GetPlatformSettingQ is GetPlatformSetting parameterized over its executor
|
|
// (see Queryer).
|
|
func (s *Store) GetPlatformSettingQ(q Queryer, key string) (string, error) {
|
|
var value string
|
|
err := q.QueryRow(s.q("SELECT value FROM platform_settings WHERE key = ?"), key).Scan(&value)
|
|
if err == sql.ErrNoRows {
|
|
return "", nil
|
|
}
|
|
return value, err
|
|
}
|
|
|
|
// SetPlatformSetting upserts a platform setting.
|
|
func (s *Store) SetPlatformSetting(key, value string) error {
|
|
_, err := s.db.Exec(s.q(`
|
|
INSERT INTO platform_settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
`), key, value, now())
|
|
return err
|
|
}
|
|
|
|
// GetPlatformSettings returns all platform settings as a map.
|
|
func (s *Store) GetPlatformSettings() (map[string]string, error) {
|
|
rows, err := s.db.Query(s.q("SELECT key, value FROM platform_settings ORDER BY key"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
settings := make(map[string]string)
|
|
for rows.Next() {
|
|
var k, v string
|
|
if err := rows.Scan(&k, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
settings[k] = v
|
|
}
|
|
return settings, rows.Err()
|
|
}
|
|
|
|
// DeletePlatformSetting removes a platform setting.
|
|
func (s *Store) DeletePlatformSetting(key string) error {
|
|
_, err := s.db.Exec(s.q("DELETE FROM platform_settings WHERE key = ?"), key)
|
|
return err
|
|
}
|
|
|
|
// ErrEncryptionUnavailable reports a secret write refused because the store
|
|
// has no encryption key. See SetSecretPlatformSetting.
|
|
var ErrEncryptionUnavailable = errors.New("store: no encryption key configured; refusing to store a secret in plaintext")
|
|
|
|
// SetSecretPlatformSetting stores a secret platform setting encrypted with
|
|
// the store's key (the PAD_ENCRYPTION_KEY path). An empty value deletes the
|
|
// row, so "no secret" has exactly one stored form.
|
|
//
|
|
// Unlike encrypt()'s other callers, which fall back to plaintext without a
|
|
// key, this REFUSES with ErrEncryptionUnavailable: a secret setting is
|
|
// documented as stored encrypted, and a plaintext fallback would make that
|
|
// false. `pad server` never reaches the refusal — it will not boot without a
|
|
// 32-byte key (cmd/pad/cmd_server.go, TASK-668) — so this is a defence for a
|
|
// store built without one, not a user-facing state (TASK-3121).
|
|
func (s *Store) SetSecretPlatformSetting(key, value string) error {
|
|
if value == "" {
|
|
return s.DeletePlatformSetting(key)
|
|
}
|
|
if !s.HasEncryptionKey() {
|
|
return ErrEncryptionUnavailable
|
|
}
|
|
enc, err := s.encrypt(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.SetPlatformSetting(key, enc)
|
|
}
|
|
|
|
// ErrSecretNotEncrypted reports a secret setting row that is not ciphertext.
|
|
var ErrSecretNotEncrypted = errors.New("store: secret platform setting is not encrypted")
|
|
|
|
// GetSecretPlatformSetting returns a secret platform setting decrypted, or ""
|
|
// when unset.
|
|
//
|
|
// A row WITHOUT the encrypted prefix is refused rather than returned: the
|
|
// generic decrypt() passes plaintext through for callers that predate
|
|
// encryption, but a secret setting has only ever been written encrypted, so a
|
|
// plaintext row came from somewhere other than SetSecretPlatformSetting and
|
|
// must not become a live credential (codex round 2).
|
|
func (s *Store) GetSecretPlatformSetting(key string) (string, error) {
|
|
v, err := s.GetPlatformSetting(key)
|
|
if err != nil || v == "" {
|
|
return "", err
|
|
}
|
|
if !strings.HasPrefix(v, encryptedPrefix) {
|
|
return "", ErrSecretNotEncrypted
|
|
}
|
|
return s.decrypt(v)
|
|
}
|