feat(settings): DB-backed settings store + full Settings page wiring

DB:
  - Add system_settings table (key, namespace, value JSONB, description)
  - Seed 27 default settings across runtime/database/security/notifications/developer namespaces

Backend:
  - New settings router: GET /settings, GET /settings/namespace/:ns,
    GET /settings/:key, PUT /settings/:key, PUT /settings (bulk update)
  - Register settings router in main.py with /api/v1 prefix

Frontend (client.ts):
  - Add put() HTTP helper
  - Add ApiSetting interface
  - Add settingsApi (list, namespace, get, update, bulkUpdate)

Settings.tsx:
  - useQuery to load all settings on mount
  - Local draft state — edits accumulate before save
  - bulkUpdate mutation on Save Changes (spinner + success banner)
  - Reset clears draft back to DB values
  - Unsaved changes indicator in header
  - All TextInput/SelectInput/Toggle now controlled via settingKey + onChange
This commit is contained in:
2026-03-06 09:52:39 +05:30
parent a7624ac6b5
commit da2eb3247e
5 changed files with 402 additions and 93 deletions

View File

@@ -265,6 +265,24 @@ CREATE TABLE IF NOT EXISTS secrets (
CREATE INDEX IF NOT EXISTS idx_secrets_name ON secrets(name);
CREATE INDEX IF NOT EXISTS idx_secrets_created_by ON secrets(created_by);
-- ============================================================================
-- SYSTEM SETTINGS TABLE
-- ============================================================================
-- Key-value store for runtime configuration. Each setting has a namespace
-- (e.g. 'runtime', 'database', 'security') for grouping, a typed value,
-- and a human-readable description for the UI.
CREATE TABLE IF NOT EXISTS system_settings (
key VARCHAR(255) PRIMARY KEY,
namespace VARCHAR(100) NOT NULL,
value JSONB NOT NULL,
description TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_system_settings_namespace
ON system_settings(namespace);
-- ============================================================================
-- SECRET ACCESS LOG
-- ============================================================================

View File

@@ -22,7 +22,7 @@ from fastapi.middleware.cors import CORSMiddleware
from libs.db.connection import init_db, close_db, get_db
from libs.logging import get_logger
from src.api.dependencies import set_controller
from src.api.routers import agents, tasks, plugins, policies, security, logs
from src.api.routers import agents, tasks, plugins, policies, security, logs, settings
from src.kernel_runtime.scheduler.execution_controller import (
ExecutionController,
)
@@ -102,7 +102,8 @@ app.include_router(tasks.router, prefix=API_PREFIX)
app.include_router(plugins.router, prefix=API_PREFIX)
app.include_router(policies.router, prefix=API_PREFIX)
app.include_router(security.router, prefix=API_PREFIX)
app.include_router(logs.router, prefix=API_PREFIX)
app.include_router(logs.router, prefix=API_PREFIX)
app.include_router(settings.router, prefix=API_PREFIX)
# ── Health ────────────────────────────────────────────────────────────────────

165
src/api/routers/settings.py Normal file
View File

@@ -0,0 +1,165 @@
"""Settings router — read/write system_settings key-value store."""
import json
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from src.common.utils import generate_correlation_id
from src.api.dependencies import get_db_conn
from libs.db.connection import DatabaseConnection
from libs.logging import get_logger
log = get_logger("api.settings")
router = APIRouter(prefix="/settings", tags=["settings"])
# ── Models ────────────────────────────────────────────────────────────────────
class SettingResponse(BaseModel):
key: str
namespace: str
value: Any
description: Optional[str]
updated_at: str
class SettingUpdateRequest(BaseModel):
value: Any
class BulkUpdateRequest(BaseModel):
settings: Dict[str, Any]
# ── Helper ────────────────────────────────────────────────────────────────────
def _row_to_setting(row) -> SettingResponse:
value = row["value"]
if isinstance(value, str):
value = json.loads(value)
return SettingResponse(
key=row["key"],
namespace=row["namespace"],
value=value,
description=row.get("description"),
updated_at=row["updated_at"].isoformat(),
)
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("", response_model=List[SettingResponse])
async def list_settings(
db: DatabaseConnection = Depends(get_db_conn),
):
"""Return all settings, ordered by namespace then key."""
correlation_id = generate_correlation_id()
rows = await db.fetch(
"SELECT * FROM system_settings ORDER BY namespace, key"
)
log.info("api_list_settings", {
"count": len(rows),
"correlation_id": correlation_id,
})
return [_row_to_setting(r) for r in rows]
@router.get("/namespace/{namespace}",
response_model=List[SettingResponse])
async def list_settings_by_namespace(
namespace: str,
db: DatabaseConnection = Depends(get_db_conn),
):
"""Return all settings for a given namespace."""
correlation_id = generate_correlation_id()
rows = await db.fetch(
"SELECT * FROM system_settings WHERE namespace = $1"
" ORDER BY key",
namespace,
)
log.info("api_list_settings_namespace", {
"namespace": namespace,
"count": len(rows),
"correlation_id": correlation_id,
})
return [_row_to_setting(r) for r in rows]
@router.get("/{key}", response_model=SettingResponse)
async def get_setting(
key: str,
db: DatabaseConnection = Depends(get_db_conn),
):
"""Get a single setting by key."""
row = await db.fetchrow(
"SELECT * FROM system_settings WHERE key = $1", key
)
if row is None:
raise HTTPException(status_code=404, detail="setting not found")
return _row_to_setting(row)
@router.put("/{key}", response_model=SettingResponse)
async def update_setting(
key: str,
body: SettingUpdateRequest,
db: DatabaseConnection = Depends(get_db_conn),
):
"""Update the value of a single setting."""
correlation_id = generate_correlation_id()
result = await db.execute(
"""
UPDATE system_settings
SET value = $1, updated_at = now()
WHERE key = $2
""",
json.dumps(body.value),
key,
)
if result == "UPDATE 0":
raise HTTPException(status_code=404, detail="setting not found")
row = await db.fetchrow(
"SELECT * FROM system_settings WHERE key = $1", key
)
log.info("api_update_setting", {
"key": key,
"correlation_id": correlation_id,
})
return _row_to_setting(row)
@router.put("", response_model=List[SettingResponse])
async def bulk_update_settings(
body: BulkUpdateRequest,
db: DatabaseConnection = Depends(get_db_conn),
):
"""Update multiple settings in a single call.
Body: { "settings": { "runtime.log_level": "DEBUG", ... } }
Unknown keys are silently ignored (no upsert).
"""
correlation_id = generate_correlation_id()
updated = []
for key, value in body.settings.items():
result = await db.execute(
"""
UPDATE system_settings
SET value = $1, updated_at = now()
WHERE key = $2
""",
json.dumps(value),
key,
)
if result != "UPDATE 0":
updated.append(key)
rows = await db.fetch(
"SELECT * FROM system_settings ORDER BY namespace, key"
)
log.info("api_bulk_update_settings", {
"updated_count": len(updated),
"keys": updated,
"correlation_id": correlation_id,
})
return [_row_to_setting(r) for r in rows]

View File

@@ -36,10 +36,11 @@ async function request<T>(
return json as T
}
const get = <T>(path: string) => request<T>('GET', path)
const post = <T>(path: string, body?: unknown) => request<T>('POST', path, body)
const patch = <T>(path: string, body?: unknown) => request<T>('PATCH', path, body)
const del = <T>(path: string) => request<T>('DELETE', path)
const get = <T>(path: string) => request<T>('GET', path)
const post = <T>(path: string, body?: unknown) => request<T>('POST', path, body)
const patch = <T>(path: string, body?: unknown) => request<T>('PATCH', path, body)
const put = <T>(path: string, body?: unknown) => request<T>('PUT', path, body)
const del = <T>(path: string) => request<T>('DELETE', path)
// ── Types matching FastAPI response models ─────────────────────────────────
@@ -194,6 +195,14 @@ export interface ApiLogEntry {
created_at: string
}
export interface ApiSetting {
key: string
namespace: string
value: unknown
description: string | null
updated_at: string
}
export interface ApiLogList {
items: ApiLogEntry[]
total: number
@@ -326,3 +335,11 @@ export const logsApi = {
},
stats: () => get<ApiLogStats>('/logs/stats'),
}
export const settingsApi = {
list: () => get<ApiSetting[]>('/settings'),
namespace: (ns: string) => get<ApiSetting[]>(`/settings/namespace/${ns}`),
get: (key: string) => get<ApiSetting>(`/settings/${key}`),
update: (key: string, value: unknown) => put<ApiSetting>(`/settings/${key}`, { value }),
bulkUpdate: (settings: Record<string, unknown>) => put<ApiSetting[]>('/settings', { settings }),
}

View File

@@ -1,10 +1,12 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Settings as SettingsIcon, Database, Server, Shield,
Bell, Code2, Save, RefreshCw, AlertTriangle,
CheckCircle2, ExternalLink,
CheckCircle2, ExternalLink, Loader2,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { settingsApi, type ApiSetting } from '@/api/client'
// ─── Sub-components ───────────────────────────────────────────────────────────
@@ -42,17 +44,20 @@ function Field({ label, hint, children }: {
)
}
function TextInput({ defaultValue, placeholder, mono = true, disabled }: {
defaultValue?: string
function TextInput({ settingKey, value, onChange, placeholder, mono = true, disabled }: {
settingKey?: string
value?: string
onChange?: (key: string, val: string) => void
placeholder?: string
mono?: boolean
disabled?: boolean
}) {
return (
<input
defaultValue={defaultValue}
value={value ?? ''}
placeholder={placeholder}
disabled={disabled}
onChange={e => settingKey && onChange?.(settingKey, e.target.value)}
className={cn(
'w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm text-slate-200',
'focus:outline-none focus:border-[#6470f1]/50 placeholder:text-slate-600 transition-colors',
@@ -63,17 +68,30 @@ function TextInput({ defaultValue, placeholder, mono = true, disabled }: {
)
}
function SelectInput({ options, defaultValue }: { options: string[]; defaultValue?: string }) {
function SelectInput({ settingKey, value, options, onChange }: {
settingKey?: string
value?: string
options: string[]
onChange?: (key: string, val: string) => void
}) {
return (
<select defaultValue={defaultValue}
<select
value={value ?? ''}
onChange={e => settingKey && onChange?.(settingKey, e.target.value)}
className="w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm font-mono text-slate-200 focus:outline-none focus:border-[#6470f1]/50">
{options.map(o => <option key={o} value={o}>{o}</option>)}
</select>
)
}
function Toggle({ label, defaultChecked, desc }: { label: string; defaultChecked?: boolean; desc?: string }) {
const [checked, setChecked] = useState(defaultChecked ?? false)
function Toggle({ settingKey, label, checked, onChange, desc }: {
settingKey?: string
label: string
checked?: boolean
onChange?: (key: string, val: boolean) => void
desc?: string
}) {
const isOn = checked ?? false
return (
<div className="flex items-center justify-between py-2.5 border-b border-white/[0.04] last:border-0">
<div>
@@ -81,25 +99,29 @@ function Toggle({ label, defaultChecked, desc }: { label: string; defaultChecked
{desc && <div className="text-[10px] font-mono text-slate-600 mt-0.5">{desc}</div>}
</div>
<button
onClick={() => setChecked(!checked)}
className={cn('w-10 h-5 rounded-full transition-all relative', checked ? 'bg-[#6470f1]' : 'bg-white/[0.08]')}
onClick={() => settingKey && onChange?.(settingKey, !isOn)}
className={cn('w-10 h-5 rounded-full transition-all relative', isOn ? 'bg-[#6470f1]' : 'bg-white/[0.08]')}
>
<span className={cn('absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all',
checked ? 'left-5' : 'left-0.5')} />
isOn ? 'left-5' : 'left-0.5')} />
</button>
</div>
)
}
function SaveBar({ onSave }: { onSave: () => void }) {
function SaveBar({ onSave, onReset, saving }: {
onSave: () => void
onReset: () => void
saving?: boolean
}) {
return (
<div className="flex items-center justify-end gap-3 mt-5 pt-5 border-t border-white/[0.06]">
<button className="text-sm font-mono text-slate-500 hover:text-slate-300 px-4 py-2 transition-colors">
<button onClick={onReset} className="text-sm font-mono text-slate-500 hover:text-slate-300 px-4 py-2 transition-colors">
Reset
</button>
<button onClick={onSave}
className="flex items-center gap-2 bg-[#6470f1] hover:bg-[#4f52e5] text-white font-mono text-sm rounded-xl px-5 py-2.5 transition-all shadow-[0_0_20px_rgba(100,112,241,0.25)]">
<Save size={13} />
<button onClick={onSave} disabled={saving}
className="flex items-center gap-2 bg-[#6470f1] hover:bg-[#4f52e5] text-white font-mono text-sm rounded-xl px-5 py-2.5 transition-all shadow-[0_0_20px_rgba(100,112,241,0.25)] disabled:opacity-50">
{saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
Save Changes
</button>
</div>
@@ -113,20 +135,62 @@ type SettingsTab = 'runtime' | 'database' | 'security' | 'notifications' | 'deve
export function Settings() {
const [tab, setTab] = useState<SettingsTab>('runtime')
const [saved, setSaved] = useState(false)
const qc = useQueryClient()
const handleSave = () => {
setSaved(true)
setTimeout(() => setSaved(false), 2500)
}
// Local draft — keyed by setting key, value is whatever type the setting is
const [draft, setDraft] = useState<Record<string, unknown>>({})
const { data: allSettings, isLoading } = useQuery({
queryKey: ['settings'],
queryFn: () => settingsApi.list(),
})
// Build a lookup map for easy access in JSX
const settingMap: Record<string, ApiSetting> = {}
for (const s of allSettings ?? []) settingMap[s.key] = s
// Helpers to read current value (draft takes priority over DB)
const str = (key: string, fallback = '') =>
String(draft[key] ?? settingMap[key]?.value ?? fallback)
const bool = (key: string, fallback = false) =>
Boolean(draft[key] ?? settingMap[key]?.value ?? fallback)
// Update draft on field change
const onChange = (key: string, val: unknown) =>
setDraft(d => ({ ...d, [key]: val }))
// Reset draft to current DB values
const handleReset = () => setDraft({})
const saveMut = useMutation({
mutationFn: () => settingsApi.bulkUpdate(draft),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['settings'] })
setDraft({})
setSaved(true)
setTimeout(() => setSaved(false), 2500)
},
})
const TABS: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'runtime', label: 'Runtime', icon: Server },
{ id: 'database', label: 'Database', icon: Database },
{ id: 'security', label: 'Security', icon: Shield },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'developer', label: 'Developer', icon: Code2 },
{ id: 'runtime', label: 'Runtime', icon: Server },
{ id: 'database', label: 'Database', icon: Database },
{ id: 'security', label: 'Security', icon: Shield },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'developer', label: 'Developer', icon: Code2 },
]
const hasDraft = Object.keys(draft).length > 0
if (isLoading) {
return (
<div className="min-h-screen bg-[#0a0a0f] flex items-center justify-center gap-2">
<Loader2 size={16} className="animate-spin text-slate-600" />
<span className="text-sm font-mono text-slate-600">Loading settings...</span>
</div>
)
}
return (
<div className="min-h-screen bg-[#0a0a0f] text-slate-200">
<div className="max-w-screen-xl mx-auto px-6 py-8">
@@ -139,12 +203,19 @@ export function Settings() {
</div>
<p className="text-xs font-mono text-slate-600">Runtime configuration · environment · integrations</p>
</div>
{saved && (
<div className="flex items-center gap-2 text-[11px] font-mono text-[#00ff88] border border-[#00ff88]/20 rounded-xl px-3 py-2 bg-[#00ff88]/5">
<CheckCircle2 size={12} />
Changes saved
</div>
)}
<div className="flex items-center gap-3">
{hasDraft && (
<span className="text-[10px] font-mono text-[#ffb800] border border-[#ffb800]/20 rounded-xl px-3 py-2 bg-[#ffb800]/5">
Unsaved changes
</span>
)}
{saved && (
<div className="flex items-center gap-2 text-[11px] font-mono text-[#00ff88] border border-[#00ff88]/20 rounded-xl px-3 py-2 bg-[#00ff88]/5">
<CheckCircle2 size={12} />
Changes saved
</div>
)}
</div>
</div>
<div className="flex gap-6">
@@ -169,44 +240,51 @@ export function Settings() {
<div className="flex-1 min-w-0">
{tab === 'runtime' && (
<>
<Section title="Runtime Configuration" icon={Server} accent="#6470f1">
<Field label="Environment">
<SelectInput options={['development', 'staging', 'production']} defaultValue="development" />
</Field>
<Field label="Runtime Version" hint="Read-only — set at deploy time">
<TextInput defaultValue="0.1.0" disabled />
</Field>
<Field label="Log Level">
<SelectInput options={['DEBUG', 'INFO', 'WARN', 'ERROR']} defaultValue="INFO" />
</Field>
<Field label="Max Concurrent Agents" hint="Agents beyond this limit are queued">
<TextInput defaultValue="20" />
</Field>
<Field label="Default Task Timeout (seconds)">
<TextInput defaultValue="300" />
</Field>
<div className="mt-4 space-y-0">
<Toggle label="Enable execution event log" defaultChecked={true} desc="Append-only event log for all task graph transitions" />
<Toggle label="Enable step checkpointing" defaultChecked={true} desc="Write checkpoint after each completed node for crash recovery" />
<Toggle label="Auto-retry failed tasks" defaultChecked={true} desc="Apply retry policy automatically on node failure" />
</div>
<SaveBar onSave={handleSave} />
</Section>
</>
<Section title="Runtime Configuration" icon={Server} accent="#6470f1">
<Field label="Environment">
<SelectInput settingKey="runtime.environment" value={str('runtime.environment', 'development')}
options={['development', 'staging', 'production']} onChange={onChange} />
</Field>
<Field label="Runtime Version" hint="Read-only — set at deploy time">
<TextInput value="0.1.0" disabled />
</Field>
<Field label="Log Level">
<SelectInput settingKey="runtime.log_level" value={str('runtime.log_level', 'INFO')}
options={['DEBUG', 'INFO', 'WARN', 'ERROR']} onChange={onChange} />
</Field>
<Field label="Max Concurrent Agents" hint="Agents beyond this limit are queued">
<TextInput settingKey="runtime.max_concurrent_agents"
value={str('runtime.max_concurrent_agents', '20')} onChange={onChange} />
</Field>
<Field label="Default Task Timeout (seconds)">
<TextInput settingKey="runtime.default_task_timeout"
value={str('runtime.default_task_timeout', '300')} onChange={onChange} />
</Field>
<div className="mt-4 space-y-0">
<Toggle settingKey="runtime.execution_event_log" label="Enable execution event log"
checked={bool('runtime.execution_event_log', true)} onChange={onChange}
desc="Append-only event log for all task graph transitions" />
<Toggle settingKey="runtime.step_checkpointing" label="Enable step checkpointing"
checked={bool('runtime.step_checkpointing', true)} onChange={onChange}
desc="Write checkpoint after each completed node for crash recovery" />
<Toggle settingKey="runtime.auto_retry_failed" label="Auto-retry failed tasks"
checked={bool('runtime.auto_retry_failed', true)} onChange={onChange}
desc="Apply retry policy automatically on node failure" />
</div>
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
)}
{tab === 'database' && (
<>
<Section title="PostgreSQL" icon={Database} accent="#00d4ff">
<Field label="Connection String" hint="Used for agent state, task graph, event log, audit trail">
<TextInput defaultValue="postgresql://nebula:***@localhost:5432/nebula_db" />
</Field>
<Field label="Pool Size">
<TextInput defaultValue="20" />
<TextInput settingKey="database.pool_size"
value={str('database.pool_size', '20')} onChange={onChange} />
</Field>
<Field label="Max Overflow">
<TextInput defaultValue="40" />
<TextInput settingKey="database.max_overflow"
value={str('database.max_overflow', '40')} onChange={onChange} />
</Field>
<div className="flex items-center gap-3 mt-4">
<button className="flex items-center gap-2 text-sm font-mono text-[#00d4ff] border border-[#00d4ff]/25 rounded-xl px-4 py-2 hover:bg-[#00d4ff]/8 transition-all">
@@ -214,28 +292,35 @@ export function Settings() {
Test Connection
</button>
</div>
<SaveBar onSave={handleSave} />
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
<Section title="Redis (Working Memory)" icon={Database} accent="#a855f7">
<Field label="URL">
<TextInput defaultValue="redis://localhost:6379/0" />
<TextInput settingKey="database.redis_url"
value={str('database.redis_url', 'redis://localhost:6379/0')} onChange={onChange} />
</Field>
<Field label="Default TTL (seconds)" hint="Default TTL for working memory entries">
<TextInput defaultValue="3600" />
<TextInput settingKey="database.redis_ttl"
value={str('database.redis_ttl', '3600')} onChange={onChange} />
</Field>
<SaveBar onSave={handleSave} />
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
<Section title="Vector Database" icon={Database} accent="#00ff88">
<Field label="Provider">
<SelectInput options={['pgvector (PostgreSQL)', 'Qdrant', 'Weaviate', 'Pinecone']} defaultValue="pgvector (PostgreSQL)" />
<SelectInput settingKey="database.vector_provider"
value={str('database.vector_provider', 'pgvector (PostgreSQL)')}
options={['pgvector (PostgreSQL)', 'Qdrant', 'Weaviate', 'Pinecone']} onChange={onChange} />
</Field>
<Field label="Embedding Model">
<SelectInput options={['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002']} defaultValue="text-embedding-3-small" />
<SelectInput settingKey="database.embedding_model"
value={str('database.embedding_model', 'text-embedding-3-small')}
options={['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002']} onChange={onChange} />
</Field>
<Field label="Embedding Dimensions" hint="Must match the selected model">
<TextInput defaultValue="1536" />
<TextInput settingKey="database.embedding_dimensions"
value={str('database.embedding_dimensions', '1536')} onChange={onChange} />
</Field>
<SaveBar onSave={handleSave} />
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
</>
)}
@@ -245,7 +330,7 @@ export function Settings() {
<Section title="API Keys" icon={Shield} accent="#ffb800">
<Field label="Runtime API Key" hint="Used by webapp and CLI to authenticate with the runtime API">
<div className="flex items-center gap-2">
<TextInput defaultValue="nebula_sk_••••••••••••••••••••" />
<TextInput value="nebula_sk_••••••••••••••••••••" disabled />
<button className="shrink-0 text-[11px] font-mono text-slate-500 hover:text-slate-300 border border-white/[0.08] rounded-lg px-3 py-2.5 hover:bg-white/5 transition-all whitespace-nowrap">
Rotate
</button>
@@ -254,12 +339,20 @@ export function Settings() {
</Section>
<Section title="Policy Engine" icon={Shield} accent="#ff4757">
<div className="space-y-0">
<Toggle label="Enforce zero-trust by default" defaultChecked={true} desc="All actions require explicit policy allow; deny is default" />
<Toggle label="Require approval for high-risk actions" defaultChecked={true} desc="filesystem.write, db.write to sensitive paths" />
<Toggle label="Auto-expire capability tokens" defaultChecked={true} desc="Tokens expire after 24h unless explicitly renewed" />
<Toggle label="Log all policy evaluations" defaultChecked={true} desc="Write every decision to the immutable audit trail" />
<Toggle settingKey="security.zero_trust_default" label="Enforce zero-trust by default"
checked={bool('security.zero_trust_default', true)} onChange={onChange}
desc="All actions require explicit policy allow; deny is default" />
<Toggle settingKey="security.require_approval_high_risk" label="Require approval for high-risk actions"
checked={bool('security.require_approval_high_risk', true)} onChange={onChange}
desc="filesystem.write, db.write to sensitive paths" />
<Toggle settingKey="security.auto_expire_tokens" label="Auto-expire capability tokens"
checked={bool('security.auto_expire_tokens', true)} onChange={onChange}
desc="Tokens expire after 24h unless explicitly renewed" />
<Toggle settingKey="security.log_policy_evaluations" label="Log all policy evaluations"
checked={bool('security.log_policy_evaluations', true)} onChange={onChange}
desc="Write every decision to the immutable audit trail" />
</div>
<SaveBar onSave={handleSave} />
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
<Section title="Compliance" icon={Shield} accent="#a855f7">
<div className="flex items-center gap-2 p-3 rounded-xl bg-[#a855f7]/5 border border-[#a855f7]/15 mb-4">
@@ -278,18 +371,27 @@ export function Settings() {
{tab === 'notifications' && (
<Section title="Notification Channels" icon={Bell} accent="#ffb800">
<Field label="Slack Webhook URL" hint="Receives alerts for failed tasks, approval requests, and security events">
<TextInput placeholder="https://hooks.slack.com/services/..." />
<TextInput settingKey="notifications.slack_webhook"
value={str('notifications.slack_webhook', '')} onChange={onChange}
placeholder="https://hooks.slack.com/services/..." />
</Field>
<Field label="Alert Email">
<TextInput placeholder="alerts@your-org.com" mono={false} />
<TextInput settingKey="notifications.alert_email"
value={str('notifications.alert_email', '')} onChange={onChange}
placeholder="alerts@your-org.com" mono={false} />
</Field>
<div className="mt-4 space-y-0">
<Toggle label="Notify on task failure" defaultChecked={true} />
<Toggle label="Notify on approval requests" defaultChecked={true} />
<Toggle label="Notify on policy violations" defaultChecked={false} />
<Toggle label="Daily digest" defaultChecked={false} desc="Summary of completed tasks, agent activity, policy events" />
<Toggle settingKey="notifications.notify_task_failure" label="Notify on task failure"
checked={bool('notifications.notify_task_failure', true)} onChange={onChange} />
<Toggle settingKey="notifications.notify_approvals" label="Notify on approval requests"
checked={bool('notifications.notify_approvals', true)} onChange={onChange} />
<Toggle settingKey="notifications.notify_policy_violations" label="Notify on policy violations"
checked={bool('notifications.notify_policy_violations', false)} onChange={onChange} />
<Toggle settingKey="notifications.daily_digest" label="Daily digest"
checked={bool('notifications.daily_digest', false)} onChange={onChange}
desc="Summary of completed tasks, agent activity, policy events" />
</div>
<SaveBar onSave={handleSave} />
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
)}
@@ -322,9 +424,15 @@ export function Settings() {
</Section>
<Section title="Debug Options" icon={Code2} accent="#a855f7">
<div className="space-y-0">
<Toggle label="Verbose execution logging" defaultChecked={false} desc="Log DEBUG level for all execution engine operations" />
<Toggle label="Log tool invocation payloads" defaultChecked={false} desc="Warning: may log sensitive data. Disable in production." />
<Toggle label="Disable policy enforcement" defaultChecked={false} desc="DANGER: bypasses all security checks. Development only." />
<Toggle settingKey="developer.verbose_execution_log" label="Verbose execution logging"
checked={bool('developer.verbose_execution_log', false)} onChange={onChange}
desc="Log DEBUG level for all execution engine operations" />
<Toggle settingKey="developer.log_tool_payloads" label="Log tool invocation payloads"
checked={bool('developer.log_tool_payloads', false)} onChange={onChange}
desc="Warning: may log sensitive data. Disable in production." />
<Toggle settingKey="developer.disable_policy_enforcement" label="Disable policy enforcement"
checked={bool('developer.disable_policy_enforcement', false)} onChange={onChange}
desc="DANGER: bypasses all security checks. Development only." />
</div>
<div className="mt-4 flex items-center gap-2 rounded-xl bg-[#ff4757]/5 border border-[#ff4757]/15 px-4 py-3">
<AlertTriangle size={13} className="text-[#ff4757] shrink-0" />
@@ -332,7 +440,7 @@ export function Settings() {
Debug options should never be enabled in production environments.
</p>
</div>
<SaveBar onSave={handleSave} />
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
</Section>
</>
)}