mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Revert the route-descriptor registry, the descriptor-driven effort pickers,
the Anthropic builder indirection and the effort_not_carried plumbing back
to the target's state, and restore DeepSeek's documented projection
(xhigh -> high per api-docs.deepseek.com Thinking Mode).
What the maintainers found on the combined tree: nine target-green tests
went red (five llm_golden compatible/cloudru/minimax cases, three
host-message-metadata cases, the provider_models/config leaf guard), the
07-configuration chapter left its byte budget, the picker hid saved and
working tiers (xhigh/max/none) and matched the catalog by bare model id,
the reviewer-slot wiring passed a fourth argument that the local wrapper
dropped, and the new usage fact never reached events.jsonl. The z.ai GLM
effort carriage itself is kept and lands as a first-class provider in the
next commit.
Kept from this change: Z.ai's HTTP 429 code 1113 ("Insufficient balance")
is a billing fact, so the provider Test maps it to "No credits" instead of
"Rate limited" (typed code only; the status copy stays provider-neutral).
1073 lines
71 KiB
JavaScript
1073 lines
71 KiB
JavaScript
import { renderPageHeader, renderSegmentedField, renderTabStrip, bindTabStrip } from './page_header.js';
|
|
import { PAGE_ICONS } from './page_icons.js';
|
|
import { renderAgentAccountsSection, renderAgentsServiceBanner } from './harness_accounts.js';
|
|
import { renderReviewerSlotsSection } from './reviewer_slots.js';
|
|
import { renderSubagentsSection } from './subagents_settings.js';
|
|
import { modelRolesHost } from './model_roles.js';
|
|
|
|
// Reads as a sequence: keys → secrets → which API models → who among the agents
|
|
// does what → behavior → technical. "Agents", not "Coding agents" (D-10): the
|
|
// same subscriptions build presentations and run arbitrary tasks, so the
|
|
// narrower word named only one of their uses.
|
|
const SETTINGS_TABS = [
|
|
{ value: 'providers', label: 'Accounts' },
|
|
{ value: 'secrets', label: 'Secrets' },
|
|
{ value: 'models', label: 'Models' },
|
|
{ value: 'agents', label: 'Agents' },
|
|
{ value: 'behavior', label: 'Behavior' },
|
|
{ value: 'appearance', label: 'Appearance' },
|
|
{ value: 'advanced', label: 'Advanced' },
|
|
{ value: 'about', label: 'About' },
|
|
];
|
|
// Guard markers: renderTabStrip emits behavior/advanced tabs at runtime.
|
|
|
|
// 6.3: Review and Scope Review efforts moved to per-slot dropdowns in
|
|
// Agents → Review lanes. Behavior keeps the surface-level lanes.
|
|
const EFFORT_FIELDS = [
|
|
['s-effort-task', 'Task / Chat', 'medium'],
|
|
['s-effort-evolution', 'Evolution', 'high'],
|
|
['s-effort-deep-self-review', 'Deep Self-Review', 'high'],
|
|
['s-effort-consciousness', 'Consciousness', ''], // '' = the Task / Chat effort (a wake-up is a Main turn)
|
|
];
|
|
|
|
// Runtime mode is one axis of the owner policy contract. Keep the Settings
|
|
// presentation in the same vocabulary as the onboarding setup contract; the
|
|
// saved value is still handled by settings.js and the owner endpoint.
|
|
const RUNTIME_MODE_OPTIONS = [
|
|
{ value: 'light', label: 'Light' },
|
|
{ value: 'advanced', label: 'Advanced' },
|
|
{ value: 'pro', label: 'Pro' },
|
|
{ value: 'cyber_pro', label: 'Cyber Pro' },
|
|
];
|
|
|
|
function providerCard({ id, title, icon, hint, body, open = false }) {
|
|
return `
|
|
<details class="settings-provider-card" data-provider-card="${id}" ${open ? 'open' : ''}>
|
|
<summary>
|
|
<div class="settings-provider-title">
|
|
${icon ? `<img src="${icon}" alt="" class="settings-provider-icon">` : ''}
|
|
<span>${title}</span>
|
|
</div>
|
|
<span class="settings-provider-hint">${hint || ''}</span>
|
|
</summary>
|
|
<div class="settings-provider-body">
|
|
${body}
|
|
</div>
|
|
</details>
|
|
`;
|
|
}
|
|
|
|
function secretField({ id, settingKey, label, placeholder }) {
|
|
return `
|
|
<div class="form-field ui-field">
|
|
<label for="${id}">${label}</label>
|
|
<div class="secret-input-row">
|
|
<input id="${id}" name="${settingKey}" data-secret-setting="${settingKey}" class="secret-input ui-control" type="password" placeholder="${placeholder}">
|
|
<button type="button" class="btn btn-default secret-toggle" data-target="${id}">Show</button>
|
|
<button type="button" class="btn btn-default secret-clear" data-target="${id}">Clear</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function plainField({ id, label, placeholder }) {
|
|
return `<div class="form-field ui-field"><label for="${id}">${label}</label><input id="${id}" name="${id}" type="text" class="ui-control" placeholder="${placeholder}"></div>`;
|
|
}
|
|
|
|
const PROVIDER_CARDS = [
|
|
{
|
|
id: 'openrouter', title: 'OpenRouter', icon: '/static/providers/openrouter.ico', hint: 'Default multi-model router', open: true,
|
|
fields: [{ id: 's-openrouter', settingKey: 'OPENROUTER_API_KEY', label: 'OpenRouter API Key', placeholder: 'sk-or-...' }],
|
|
// testInputs must cover BOTH the secret and the plain fields of the card:
|
|
// the probe runs against what is typed, before anything is saved.
|
|
testProvider: 'openrouter', testInputs: { 's-openrouter': 'OPENROUTER_API_KEY' },
|
|
},
|
|
{
|
|
id: 'openai', title: 'OpenAI', icon: '/static/providers/openai.svg', hint: 'Official OpenAI API',
|
|
fields: [{ id: 's-openai', settingKey: 'OPENAI_API_KEY', label: 'OpenAI API Key', placeholder: 'sk-...' }],
|
|
testProvider: 'openai', testInputs: { 's-openai': 'OPENAI_API_KEY' },
|
|
note: 'Pick OpenAI as the source in Models or Agents to route a role through this key. If OpenRouter is absent and the shipped defaults are still untouched, Ouroboros auto-remaps them to official OpenAI defaults.',
|
|
},
|
|
{
|
|
id: 'compatible', title: 'OpenAI Compatible', icon: '/static/providers/openai-compatible.svg', hint: 'Custom OpenAI-style endpoint',
|
|
fields: [
|
|
{ id: 's-openai-compatible-key', settingKey: 'OPENAI_COMPATIBLE_API_KEY', label: 'API Key', placeholder: 'Compatible provider key' },
|
|
{ id: 's-openai-compatible-base-url', label: 'Base URL', placeholder: 'https://provider.example/v1' },
|
|
],
|
|
testProvider: 'openai-compatible',
|
|
testInputs: {
|
|
's-openai-compatible-key': 'OPENAI_COMPATIBLE_API_KEY',
|
|
's-openai-compatible-base-url': 'OPENAI_COMPATIBLE_BASE_URL',
|
|
},
|
|
note: 'Use this card for custom base URLs. Built-in web search only works with the official OpenAI Responses API, so keep <code>OPENAI_BASE_URL</code> empty when you want <code>web_search</code>.',
|
|
},
|
|
{
|
|
// advanced: rendered inside the collapsed "More providers" section.
|
|
// Inputs stay mounted either way — settings.js applyInputValue has no
|
|
// null guard, so every input id must always exist in the DOM.
|
|
id: 'cloudru', title: 'Cloud.ru Foundation Models', icon: '/static/providers/cloudru.svg', hint: 'Cloud.ru OpenAI-compatible runtime', advanced: true,
|
|
fields: [
|
|
{ id: 's-cloudru-key', settingKey: 'CLOUDRU_FOUNDATION_MODELS_API_KEY', label: 'API Key', placeholder: 'Cloud.ru Foundation Models API key' },
|
|
{ id: 's-cloudru-base-url', label: 'Base URL', placeholder: 'https://foundation-models.api.cloud.ru/v1' },
|
|
],
|
|
testProvider: 'cloudru',
|
|
testInputs: {
|
|
's-cloudru-key': 'CLOUDRU_FOUNDATION_MODELS_API_KEY',
|
|
's-cloudru-base-url': 'CLOUDRU_FOUNDATION_MODELS_BASE_URL',
|
|
},
|
|
},
|
|
{
|
|
id: 'minimax', title: 'MiniMax', icon: '', hint: 'Direct regional OpenAI-compatible runtime', advanced: true,
|
|
fields: [
|
|
{ id: 's-minimax-key', settingKey: 'MINIMAX_API_KEY', label: 'API Key', placeholder: 'MiniMax API key' },
|
|
{ id: 's-minimax-region', label: 'Region', placeholder: 'global_en or cn_zh' },
|
|
],
|
|
testProvider: 'minimax',
|
|
testInputs: { 's-minimax-key': 'MINIMAX_API_KEY', 's-minimax-region': 'MINIMAX_REGION' },
|
|
note: 'Pick MiniMax as the source in Models or Agents, then choose MiniMax-M3 or MiniMax-M2.7. Leave Region empty for <code>global_en</code>; use <code>cn_zh</code> for the China endpoint.',
|
|
},
|
|
{
|
|
id: 'deepseek', title: 'DeepSeek', icon: '', hint: 'Direct OpenAI-compatible runtime (v4 family)', advanced: true,
|
|
fields: [
|
|
{ id: 's-deepseek-key', settingKey: 'DEEPSEEK_API_KEY', label: 'API Key', placeholder: 'sk-...' },
|
|
],
|
|
testProvider: 'deepseek',
|
|
testInputs: { 's-deepseek-key': 'DEEPSEEK_API_KEY' },
|
|
note: 'Pick DeepSeek as the source in Models or Agents, then choose deepseek-v4-pro or deepseek-v4-flash.',
|
|
},
|
|
{
|
|
id: 'gigachat', title: 'GigaChat', icon: '/static/providers/gigachat.svg', hint: 'Sber GigaChat via the gigachat library', advanced: true,
|
|
fields: [
|
|
{ id: 's-gigachat-credentials', settingKey: 'GIGACHAT_CREDENTIALS', label: 'Authorization Key', placeholder: 'Base64 client_id:secret (OAuth)' },
|
|
{ id: 's-gigachat-scope', label: 'Scope', placeholder: 'GIGACHAT_API_PERS' },
|
|
{ id: 's-gigachat-user', label: 'User (basic auth, optional)', placeholder: 'username' },
|
|
{ id: 's-gigachat-password', settingKey: 'GIGACHAT_PASSWORD', label: 'Password (basic auth, optional)', placeholder: 'password' },
|
|
{ id: 's-gigachat-base-url', label: 'Base URL', placeholder: 'https://api.giga.chat/v1' },
|
|
{ id: 's-gigachat-verify-ssl', label: 'Verify SSL Certs', placeholder: 'true / false' },
|
|
],
|
|
testProvider: 'gigachat',
|
|
testInputs: {
|
|
's-gigachat-credentials': 'GIGACHAT_CREDENTIALS',
|
|
's-gigachat-scope': 'GIGACHAT_SCOPE',
|
|
's-gigachat-user': 'GIGACHAT_USER',
|
|
's-gigachat-password': 'GIGACHAT_PASSWORD',
|
|
's-gigachat-base-url': 'GIGACHAT_BASE_URL',
|
|
's-gigachat-verify-ssl': 'GIGACHAT_VERIFY_SSL_CERTS',
|
|
},
|
|
note: 'Pick GigaChat as the source in Models or Agents to route a role through this account. Authenticate with either an Authorization Key (OAuth, scope <code>GIGACHAT_API_PERS</code>, <code>GIGACHAT_API_B2B</code>, or <code>GIGACHAT_API_CORP</code>) or User + Password.',
|
|
},
|
|
{
|
|
id: 'anthropic', title: 'Anthropic', icon: '/static/providers/anthropic.png', hint: 'Direct Anthropic API access',
|
|
fields: [{ id: 's-anthropic', settingKey: 'ANTHROPIC_API_KEY', label: 'Anthropic API Key', placeholder: 'sk-ant-...' }],
|
|
testProvider: 'anthropic', testInputs: { 's-anthropic': 'ANTHROPIC_API_KEY' },
|
|
note: 'Pick Anthropic as the source in Models or Agents to route a role directly through this key.',
|
|
},
|
|
];
|
|
|
|
// {backend provider id: {DOM input id: settings key}} — settings.js reads the
|
|
// live input values through this map to build the unsaved-credential overrides.
|
|
export const PROVIDER_TEST_INPUTS = Object.fromEntries(
|
|
PROVIDER_CARDS.filter((card) => card.testProvider).map((card) => [card.testProvider, card.testInputs]),
|
|
);
|
|
|
|
function providerSettingsCard(spec) {
|
|
const fields = (spec.fields || [])
|
|
.map((field) => {
|
|
const named = { ...field, label: field.label === "API Key" || field.label === "Base URL"
|
|
? `${spec.title} ${field.label}` : field.label };
|
|
return field.settingKey ? secretField(named) : plainField(named);
|
|
})
|
|
.join('');
|
|
const test = spec.testProvider ? `
|
|
<div class="settings-action-row">
|
|
<span class="settings-inline-status" data-provider-test-status="${spec.testProvider}" role="status" aria-live="polite" aria-atomic="true"></span>
|
|
<button type="button" class="btn btn-default" data-provider-test="${spec.testProvider}" title="Sends one short model request. Provider charges may apply.">Test</button>
|
|
</div>
|
|
` : '';
|
|
return providerCard({
|
|
id: spec.id,
|
|
title: spec.title,
|
|
icon: spec.icon,
|
|
hint: spec.hint,
|
|
open: spec.open,
|
|
body: `<div class="form-row">${fields}</div>${test}${spec.note ? `<div class="settings-inline-note">${spec.note}</div>` : ''}`,
|
|
});
|
|
}
|
|
|
|
// The owner-facing subset of ouroboros/config.py EFFORT_SCALE: `minimal` is a
|
|
// valid runtime tier (bench adapters / agent-side switch_model use it) but is
|
|
// deliberately NOT offered as an owner slot default — sub-`low` thinking is a
|
|
// per-call tactical choice, not a standing configuration. xhigh/max/ultra adapt
|
|
// down to each route's real ceiling (exact-route request-wire recovery on API
|
|
// routes, per-model resolution on delegated ones); the adaptation is disclosed
|
|
// in usage, and a cold route whose provider rejects without naming supported
|
|
// tiers remains the PR-disclosed limit of the two-send recovery rail.
|
|
const EFFORT_OPTIONS = [
|
|
{ value: 'none', label: 'None' }, { value: 'low', label: 'Low' },
|
|
{ value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' },
|
|
{ value: 'xhigh', label: 'X-High' }, { value: 'max', label: 'Max' }, { value: 'ultra', label: 'Ultra' },
|
|
];
|
|
|
|
function effortField({ id, label, defaultValue }) {
|
|
// Consciousness may inherit the Task / Chat effort ('' — a wake-up is a Main turn).
|
|
const options = id === 's-effort-consciousness' ? [{ value: '', label: 'Same as Task / Chat' }, ...EFFORT_OPTIONS] : EFFORT_OPTIONS;
|
|
return `
|
|
<div class="settings-effort-card">
|
|
<label for="${id}">${label}</label>
|
|
<input id="${id}" type="hidden" value="${defaultValue}">
|
|
${renderSegmentedField({ target: id, options })}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export const SECRET_KEYS = [
|
|
['OPENROUTER_API_KEY', 'OpenRouter API Key', 'sk-or-...'],
|
|
['OPENAI_API_KEY', 'OpenAI API Key', 'sk-...'],
|
|
['OPENAI_COMPATIBLE_API_KEY', 'OpenAI-compatible API Key', 'Compatible provider key'],
|
|
['CLOUDRU_FOUNDATION_MODELS_API_KEY', 'Cloud.ru Foundation Models API Key', 'Cloud.ru key'],
|
|
['GIGACHAT_CREDENTIALS', 'GigaChat Authorization Key', 'Base64 client_id:secret'],
|
|
['GIGACHAT_PASSWORD', 'GigaChat Password (basic auth)', 'password'],
|
|
['ANTHROPIC_API_KEY', 'Anthropic API Key', 'sk-ant-...'],
|
|
['MINIMAX_API_KEY', 'MiniMax API Key', 'MiniMax key'],
|
|
['DEEPSEEK_API_KEY', 'DeepSeek API Key', 'sk-...'],
|
|
['GITHUB_TOKEN', 'GitHub Token', 'ghp_...'],
|
|
['OUROBOROS_NETWORK_PASSWORD', 'Network Password', 'Required for LAN/Docker binds'],
|
|
];
|
|
|
|
function secretSettingsSection() {
|
|
return `
|
|
<section class="settings-card">
|
|
<h3>Stored Secrets</h3>
|
|
<div class="settings-section-copy">
|
|
Central place for API keys, bridge tokens, passwords, and future skill-requested secrets.
|
|
Skills only receive grant-only keys after explicit human approval.
|
|
</div>
|
|
<div class="form-grid two">
|
|
${SECRET_KEYS.map(([key, label, placeholder]) => secretField({
|
|
id: `s-secret-${key.toLowerCase().replace(/_/g, '-')}`,
|
|
settingKey: key,
|
|
label,
|
|
placeholder,
|
|
})).join('')}
|
|
</div>
|
|
</section>
|
|
<section class="settings-card">
|
|
<h3>Requested By Skills</h3>
|
|
<div class="settings-section-copy">
|
|
Secrets requested by installed skills appear here only when a skill asks for them.
|
|
</div>
|
|
<div id="skill-requested-secrets" class="settings-secret-list">
|
|
<div class="muted">No skill-requested secrets.</div>
|
|
</div>
|
|
</section>
|
|
<section class="settings-card">
|
|
<div class="settings-card-head">
|
|
<div>
|
|
<h3>Custom Keys</h3>
|
|
<div class="settings-section-copy">
|
|
Optional key/value storage for future skills. Use uppercase names such as <code>SLACK_WEBHOOK_URL</code>.
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-default btn-sm" id="btn-add-custom-secret">Add custom key</button>
|
|
</div>
|
|
<div id="custom-secrets-list" class="settings-secret-list settings-custom-secret-list"></div>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
export function renderSettingsPage() {
|
|
return `
|
|
${renderPageHeader({
|
|
title: 'Settings',
|
|
icon: PAGE_ICONS.settings,
|
|
description: 'Configure providers, secrets, models, behavior, source control, and runtime controls.',
|
|
tabsHtml: `
|
|
<div class="settings-tabs-bar">
|
|
<button type="button" class="settings-mobile-back" data-settings-back hidden>Settings</button>
|
|
${renderTabStrip({
|
|
items: SETTINGS_TABS.map((item) => ({ ...item,
|
|
tabId: `settings-tab-${item.value}`, panelId: `settings-panel-${item.value}` })),
|
|
active: 'providers',
|
|
dataAttr: 'data-settings-tab',
|
|
ariaLabel: 'Settings sections',
|
|
stripClass: 'settings-tabs',
|
|
tabClass: 'settings-tab',
|
|
})}
|
|
</div>
|
|
`,
|
|
})}
|
|
<div class="settings-shell">
|
|
<div class="settings-scroll scroll-fade-y">
|
|
<section class="settings-panel active" data-settings-panel="providers">
|
|
<div class="settings-section-copy">
|
|
Connect subscriptions and API keys here, then choose their roles in Models and Agents.
|
|
Adding an account keeps your existing assignments.
|
|
</div>
|
|
${renderAgentsServiceBanner()}
|
|
${renderAgentAccountsSection()}
|
|
<h3>API keys</h3>
|
|
${PROVIDER_CARDS.filter((card) => !card.advanced).map(providerSettingsCard).join('')}
|
|
<details class="settings-more-providers" id="settings-more-providers">
|
|
<summary>
|
|
<span class="settings-provider-title"><span>More providers</span></span>
|
|
<span class="settings-provider-hint">Cloud.ru Foundation Models, MiniMax, DeepSeek, and GigaChat</span>
|
|
</summary>
|
|
<div class="settings-more-providers-body">
|
|
${PROVIDER_CARDS.filter((card) => card.advanced).map(providerSettingsCard).join('')}
|
|
</div>
|
|
</details>
|
|
<div class="form-section compact">
|
|
<h3>Legacy Compatibility</h3>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<label for="s-openai-base-url">Legacy OpenAI Base URL</label>
|
|
<input id="s-openai-base-url" placeholder="https://api.openai.com/v1 or compatible endpoint" class="ui-control" name="s-openai-base-url" type="text">
|
|
</div>
|
|
</div>
|
|
<div class="settings-inline-note">Backward-compatibility escape hatch for older installs. For new custom providers, use the dedicated <code>OpenAI Compatible</code> card instead.</div>
|
|
</div>
|
|
<div class="form-section compact">
|
|
<h3>Network Gate</h3>
|
|
<div class="form-row">${secretField({
|
|
id: 's-network-password',
|
|
settingKey: 'OUROBOROS_NETWORK_PASSWORD',
|
|
label: 'Network Password (optional)',
|
|
placeholder: 'Leave blank to keep the network surface open',
|
|
})}</div>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<label for="s-server-host">Server Bind Host</label>
|
|
<input id="s-server-host" placeholder="127.0.0.1 or 0.0.0.0" class="ui-control" name="s-server-host" type="text" aria-describedby="s-server-host-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-server-host-help">Use <code>127.0.0.1</code> for this machine only. Use <code>0.0.0.0</code> for LAN/Docker access with a Network Password in the same save. Specific LAN IP binds are manual/env-only.</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-inline-note">Adds a password wall only for non-localhost app and API access. If you expose Ouroboros on LAN or Docker, set a password before sharing the URL.</div>
|
|
<div id="settings-lan-hint" class="settings-lan-hint" hidden></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="secrets">
|
|
${secretSettingsSection()}
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="models">
|
|
<div class="form-section">
|
|
<h3>Model Routing</h3>
|
|
<div class="settings-section-copy">
|
|
Choose a source, model, and subscription account for each role. Auto rotates
|
|
compatible accounts. Local uses the runtime configured in Advanced.
|
|
</div>
|
|
<div class="settings-action-row">
|
|
<span id="settings-model-catalog-status" class="settings-inline-status" role="status" aria-live="polite" aria-atomic="true">Model catalog is optional and failure-tolerant.</span>
|
|
<button type="button" class="btn btn-default" id="btn-refresh-model-catalog">Refresh Model Catalog</button>
|
|
</div>
|
|
${modelRolesHost('settings-model-roles')}
|
|
</div>
|
|
|
|
<!-- Review lanes and Delegation moved to the Agents tab
|
|
(D-10): they answer "who does the work", not "which API
|
|
model id". One capability, one section — no control here
|
|
duplicates one there. The deep self-review reviewer is a
|
|
Review lanes row too (R7); its former model field's key,
|
|
OUROBOROS_MODEL_DEEP_SELF_REVIEW, survives only as the
|
|
backend's invisible migration source for that row. -->
|
|
|
|
<div class="form-section">
|
|
<h3>Other Model Slots</h3>
|
|
<div class="form-grid two">
|
|
<div class="form-field ui-field">
|
|
<label for="s-websearch-model">Web Search Model</label>
|
|
<input id="s-websearch-model" placeholder="gpt-5.2" class="ui-control" name="s-websearch-model" type="text" aria-describedby="s-websearch-model-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-websearch-model-help">OpenAI model for <code>web_search</code>. Requires <code>OPENAI_API_KEY</code> and an empty Legacy Base URL.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="agents">
|
|
<div class="settings-section-copy">
|
|
Configure the subagents and reviewers Ouroboros works with. Subscriptions and
|
|
API keys are in Accounts; global model roles are in Models.
|
|
</div>
|
|
<!-- ONE service banner for the whole tab: the single place a
|
|
daemon or runtime problem is explained, instead of the
|
|
scattering of "(not in discovery)" the owner reported. -->
|
|
${renderSubagentsSection()}
|
|
${renderReviewerSlotsSection()}
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="behavior">
|
|
<div class="form-section">
|
|
<h3>Reasoning Effort</h3>
|
|
<div class="settings-section-copy">Controls how deeply the model thinks per task type. Higher effort = slower but more thorough.</div>
|
|
<div class="settings-effort-grid">
|
|
${EFFORT_FIELDS.map(([id, label, defaultValue]) => effortField({ id, label, defaultValue })).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Review Enforcement</h3>
|
|
<div class="settings-section-copy"><code>Advisory</code> keeps review visible but non-blocking. <code>Blocking</code> stops commits and reviewed-skill activation when critical findings remain unresolved.</div>
|
|
<div class="settings-effort-card">
|
|
<label>Enforcement Mode</label>
|
|
<input id="s-review-enforcement" type="hidden" value="advisory">
|
|
${renderSegmentedField({
|
|
target: 's-review-enforcement',
|
|
modifier: 'data-enforcement-group',
|
|
options: [
|
|
{ value: 'advisory', label: 'Advisory' },
|
|
{ value: 'blocking', label: 'Blocking' },
|
|
],
|
|
})}
|
|
<div class="settings-inline-note" data-policy-state="review" role="status" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Task Result Review</h3>
|
|
<div class="settings-section-copy">Auto and Required run the root-owned review panel for queued/headless work and substantive direct results. Pure conversation and routing controls are skipped. Once review applies, both follow the selected Advisory or Blocking policy.</div>
|
|
<div class="settings-effort-card">
|
|
<label>Task Result Review</label>
|
|
<input id="s-task-review-mode" type="hidden" value="auto">
|
|
${renderSegmentedField({
|
|
target: 's-task-review-mode',
|
|
modifier: 'data-task-review-group',
|
|
options: [
|
|
{ value: 'off', label: 'Off' },
|
|
{ value: 'auto', label: 'Auto' },
|
|
{ value: 'required', label: 'Required' },
|
|
],
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Max Review Cycles</h3>
|
|
<div class="settings-section-copy">Limits paid review waves, including dispatched technical failures: plan and task review per task, commit triad+scope per root task, and skill review per root task or manual snapshot. The last review still permits author corrections within ordinary task limits; explicit task-local author limits remain separate. Collection and exact replay are free. Advisory allows an explicit decision after receiving feedback or a disclosed unavailable result; Blocking still requires reviewer approval. <code>∞</code> removes the count cap, while deadlines, budgets and lifecycle limits still apply.</div>
|
|
<div class="settings-effort-card">
|
|
<label>Max Review Cycles</label>
|
|
<input id="s-review-max-cycles" type="hidden" value="2">
|
|
${renderSegmentedField({
|
|
target: 's-review-max-cycles',
|
|
modifier: 'data-review-cycles-group',
|
|
options: [
|
|
{ value: '1', label: '1' },
|
|
{ value: '2', label: '2' },
|
|
{ value: '3', label: '3' },
|
|
{ value: '5', label: '5' },
|
|
{ value: 'unlimited', label: '\u221E' },
|
|
],
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Image Input</h3>
|
|
<div class="settings-section-copy">Auto sends images inline to vision-capable models and captions them for blind models. Caption always uses text captions; Inline refuses caption fallback; Off emits placeholders.</div>
|
|
<div class="settings-effort-card">
|
|
<label>Image Input Mode</label>
|
|
<input id="s-image-input-mode" type="hidden" value="auto">
|
|
${renderSegmentedField({
|
|
target: 's-image-input-mode',
|
|
modifier: 'data-image-input-group',
|
|
options: [
|
|
{ value: 'auto', label: 'Auto' },
|
|
{ value: 'caption', label: 'Caption' },
|
|
{ value: 'inline', label: 'Inline' },
|
|
{ value: 'off', label: 'Off' },
|
|
],
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Skills</h3>
|
|
<div class="settings-section-copy">
|
|
Closed-loop skill development can auto-grant the keys and host permissions a skill declares after a fresh executable review for the current content hash.
|
|
Leave this off when every skill permission should require a separate human approval.
|
|
</div>
|
|
<label class="local-toggle ui-field ui-field-inline" title="Applies only after a fresh executable skill review and only to manifest-declared grants for that exact content hash.">
|
|
<input type="checkbox" id="s-auto-grant-reviewed-skills" class="ui-checkbox" name="s-auto-grant-reviewed-skills">
|
|
Auto-grant reviewed skills' keys and permissions
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Context Mode</h3>
|
|
<div class="settings-section-copy">
|
|
Working-context size profile (separate axis from Runtime Mode and Review Enforcement).
|
|
<code>Max</code> inlines ARCHITECTURE and DEVELOPMENT in full — for ~1M-context models (today's behavior).
|
|
<code>Nano</code> is the compact owner window. <code>Low</code> fits ~200K / local models: ARCHITECTURE becomes a navigation map (read full sections on demand), DEVELOPMENT stays full for normal runnable tasks unless a structured non-development caller opts out, and memory compacts sooner. It governs Ouroboros's own working window: it never changes the model or reasoning effort, and scope review runs in every mode.
|
|
<br><strong>Human controlled:</strong> saved via the owner endpoint; saves immediately (no restart), and lowering requires Ouroboros to be idle.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label>Context Mode</label>
|
|
<input id="s-context-mode" type="hidden" value="max">
|
|
${renderSegmentedField({
|
|
target: 's-context-mode',
|
|
title: 'Saves immediately; no restart required. Lowering requires Ouroboros to be idle.',
|
|
options: [
|
|
{ value: 'nano', label: 'Nano' },
|
|
{ value: 'low', label: 'Low' },
|
|
{ value: 'max', label: 'Max' },
|
|
],
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Prompt Cache TTL</h3>
|
|
<div class="settings-section-copy">
|
|
How long provider prompt caches keep this agent's stable context warm (Anthropic-family routes; other providers manage caching implicitly).
|
|
<code>1h</code> keeps the large stable prefix cached across long waits and review cycles — cache writes bill at 2× base input instead of 1.25×, but one wait longer than 5 minutes already pays that back on big contexts.
|
|
<code>5m</code> is the provider default tier as an explicit value; <code>Default</code> sends bare markers (provider default, callers may still declare their own TTL).
|
|
One honest global: it applies to every lane, including review and safety prompts.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label>Prompt Cache TTL</label>
|
|
<input id="s-prompt-cache-ttl" type="hidden" value="1h">
|
|
${renderSegmentedField({
|
|
target: 's-prompt-cache-ttl',
|
|
options: [
|
|
{ value: 'default', label: 'Default' },
|
|
{ value: '5m', label: '5m' },
|
|
{ value: '1h', label: '1h' },
|
|
],
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Safety Supervisor</h3>
|
|
<div class="settings-section-copy">
|
|
Coverage of the LLM safety-supervisor layer (a separate axis from Runtime Mode).
|
|
<code>Full</code> — every guarded tool call gets the LLM safety check.
|
|
<code>Light</code> keeps the LLM check only for integration-policy tools; conditional shell/verify fall to the deterministic guards. Light is the default for new DESKTOP setups (authored by the first-run wizard); existing installs, web and Docker keep Full.
|
|
<code>Off</code> makes no LLM safety calls. In every mode the deterministic registry sandbox, protected-path policy, and light-mode guards STAY ON — the LLM supervisor is a layer, not the floor. Lowering coverage emits a durable audit event per waved-through call.
|
|
<br><strong>Configuration authority:</strong> outside Cyber Pro, the agent cannot lower its own supervision. Cyber Pro also lets the agent configure Supervisor coverage. Changes apply on the next task.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label>Safety Supervisor</label>
|
|
<input id="s-safety-mode" type="hidden" value="full">
|
|
${renderSegmentedField({
|
|
target: 's-safety-mode',
|
|
title: 'Lowering coverage here prompts for confirmation.',
|
|
options: [
|
|
{ value: 'full', label: 'Full' },
|
|
{ value: 'light', label: 'Light' },
|
|
{ value: 'off', label: 'Off' },
|
|
],
|
|
})}
|
|
<div class="settings-inline-note" data-policy-state="supervisor" role="status" aria-live="polite"></div>
|
|
<div id="s-safety-skip-counter" class="settings-section-copy"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Update Channel</h3>
|
|
<div class="settings-section-copy">
|
|
Chooses which official branch Update checks. Your local work branch stays <code>ouroboros</code>.
|
|
<code>Stable</code> follows released code on <code>main</code> (default),
|
|
<code>QA</code> follows <code>ouroboros-stable</code>, and
|
|
<code>Development</code> follows <code>ouroboros</code>.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label>Official Update Source</label>
|
|
<input id="s-update-channel" type="hidden" value="stable">
|
|
${renderSegmentedField({
|
|
target: 's-update-channel',
|
|
modifier: 'data-update-channel-group',
|
|
title: 'Applies immediately; no restart required.',
|
|
options: [
|
|
{ value: 'stable', label: 'Stable' },
|
|
{ value: 'qa', label: 'QA' },
|
|
{ value: 'development', label: 'Development' },
|
|
],
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Access</h3>
|
|
<div class="settings-section-copy">
|
|
Separate axis from Review Enforcement. Controls how far Ouroboros is allowed to self-modify.
|
|
<code>Light</code> blocks repo self-modification but allows reviewed + enabled skills to run.
|
|
<code>Advanced</code> is the default — self-modify the evolutionary layer; protected core/contract/release files stay guarded by the shared runtime-mode policy.
|
|
<code>Pro</code> can edit protected core/contract/release surfaces, but commits still go through the normal triad + scope review gate; Advanced remains limited to the evolutionary layer.
|
|
<code>Cyber Pro</code> grants the full host and configuration authority, including credentials, models, Supervisor configuration and protected rewrites. Review scope and enforcement stay owner-controlled. Review Enforcement remains independent, so <code>Blocking</code> stays available in Cyber Pro.
|
|
<br><strong>Human controlled:</strong> desktop builds ask the launcher for native confirmation before saving a mode change.
|
|
Web/Docker sessions save mode changes through the owner endpoint; the new mode takes effect after restart.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label>Access level</label>
|
|
<input id="s-runtime-mode" type="hidden" value="advanced">
|
|
${renderSegmentedField({
|
|
target: 's-runtime-mode',
|
|
modifier: 'data-runtime-mode-group',
|
|
title: 'Access changes take effect after restart.',
|
|
options: RUNTIME_MODE_OPTIONS,
|
|
})}
|
|
<div class="settings-inline-note" data-policy-state="access" role="status" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Allow Mutative Subagents moved to Agents → Delegation: one
|
|
subagent story in one place, and two controls over one
|
|
setting would have carried two drafts. -->
|
|
|
|
<div class="form-section">
|
|
<h3>Post-Task Self-Evolution</h3>
|
|
<div class="settings-section-copy">
|
|
After an eligible task, Ouroboros can optionally run one reviewed self-improvement cycle: the worker asks a light model whether to promote a backlog item, writes a durable request, and the supervisor starts a one-shot campaign later on an idle tick if all gates pass.
|
|
<br><strong>Configuration authority:</strong> outside Cyber Pro, only the owner can enable this. Cyber Pro also lets the agent configure it; selecting Cyber Pro does not enable evolution automatically. Changes apply on the next task.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label>Self-Improvement Trigger</label>
|
|
<input id="s-post-task-evolution-mode" type="hidden" value="off">
|
|
${renderSegmentedField({
|
|
target: 's-post-task-evolution-mode',
|
|
options: [
|
|
{ value: 'off', label: 'Off' },
|
|
{ value: 'llm', label: 'After Each Task (LLM decides)' },
|
|
{ value: 'every_n', label: 'Every N Tasks' },
|
|
],
|
|
})}
|
|
<div class="settings-inline-note"><strong>Counts every eligible task, including trivial chats.</strong> <code>Every N=1</code> means Ouroboros considers self-improvement after every task, then runs the actual cycle later on an idle supervisor tick.</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<div data-evo-every-n-row>
|
|
<label for="s-evo-cadence-n">Every N Tasks</label>
|
|
<input id="s-evo-cadence-n" type="number" min="1" step="1" placeholder="3" class="ui-control" name="s-evo-cadence-n" aria-describedby="s-evo-cadence-n-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-evo-cadence-n-help">Visible only when Self-Improvement Trigger = Every N Tasks.</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-evo-budget">Per-Cycle Budget Reserve (USD)</label>
|
|
<input id="s-evo-budget" placeholder="0" class="ui-control" name="s-evo-budget" type="text" aria-describedby="s-evo-budget-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-evo-budget-help">Minimum remaining global budget required to start a post-task cycle. <code>0</code> = rely on the normal gates. Running cycles still inherit the global per-task hard cost cap and the supervisor's reserved-budget floor.</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-evo-objective">Standing Objective (optional)</label>
|
|
<input id="s-evo-objective" placeholder="(none) — e.g. prioritize test coverage and latency" class="ui-control" name="s-evo-objective" type="text" aria-describedby="s-evo-objective-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-evo-objective-help">Optional steer appended to every evolution cycle objective. It never overrides the LLM-first promotion; leave empty for pure LLM choice.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Background Cognition</h3>
|
|
<div class="settings-section-copy">When Ouroboros wakes up on its own, what a wake-up is allowed to do, and what it may spend doing it.</div>
|
|
<div class="settings-effort-card">
|
|
<label>Consciousness Autonomy</label>
|
|
<input id="s-consciousness-autonomy" type="hidden" value="act">
|
|
${renderSegmentedField({ target: 's-consciousness-autonomy', options: [{ value: 'observe', label: 'Observe' }, { value: 'act', label: 'Act' }, { value: 'full', label: 'Full' }] })}
|
|
<div class="settings-inline-note"><strong>Observe:</strong> research, internal memory and task/project notes, read-only research children it can also stop, schedule controls and replies to you; no shell, user-file, source, skill/settings or publication changes. <strong>Act (default):</strong> everything the runtime mode allows except editing Ouroboros's own code and prompts, evolution, restart and settings. <strong>Full:</strong> everything the runtime mode allows, evolution included.</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<label for="s-consciousness-daily-usd">Daily Allowance (USD)</label>
|
|
<input id="s-consciousness-daily-usd" placeholder="20" class="ui-control" name="s-consciousness-daily-usd" type="text" aria-describedby="s-consciousness-daily-usd-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-consciousness-daily-usd-help">Spending cap for consciousness over a rolling 24-hour window: the wake-ups plus the tasks they start. When it is exhausted, no new wake-up or task starts until spend leaves the window. <code>0</code> = consciousness may not spend.</div>
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-consciousness-max-tasks">Max Concurrent Tasks</label>
|
|
<input id="s-consciousness-max-tasks" type="number" min="0" step="1" placeholder="2" class="ui-control" name="s-consciousness-max-tasks" aria-describedby="s-consciousness-max-tasks-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-consciousness-max-tasks-help">How many tasks started by consciousness may run at once. <code>0</code> = it never starts tasks.</div>
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-bg-wakeup-min">Wake-Up Interval Min (sec)</label>
|
|
<input id="s-bg-wakeup-min" type="number" min="60" step="1" placeholder="900" class="ui-control" name="s-bg-wakeup-min">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-bg-wakeup-max">Wake-Up Interval Max (sec)</label>
|
|
<input id="s-bg-wakeup-max" type="number" min="60" step="1" placeholder="14400" class="ui-control" name="s-bg-wakeup-max">
|
|
</div>
|
|
</div>
|
|
<div class="settings-inline-note">Ouroboros chooses the interval between its own wake-ups; the two values above are the lower and upper bound it must stay within. All four settings apply without a restart: the alarm clock reads them at each decision.</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>External Skills Repo</h3>
|
|
<div class="settings-section-copy">
|
|
Optional EXTRA discovery path on top of the in-data-plane
|
|
<code>data/skills/{native,clawhub,external}/</code> tree.
|
|
Ouroboros scans this for additional skill packages without
|
|
cloning or pulling them. Leave empty to use only the data plane.
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<label for="s-skills-repo-path">Skills Repo Path</label>
|
|
<input id="s-skills-repo-path" placeholder="~/Ouroboros/skills or /absolute/path/to/skills" class="ui-control" name="s-skills-repo-path" type="text" aria-describedby="s-skills-repo-path-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-skills-repo-path-help">Absolute or <code>~</code>-prefixed path. Ouroboros never clones/pulls this directory — you manage it yourself.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>ClawHub Marketplace</h3>
|
|
<div class="settings-section-copy">
|
|
Always-on surface for installing community skills from
|
|
<a href="https://clawhub.ai" target="_blank" rel="noopener">clawhub.ai</a>.
|
|
The Skills page exposes a Marketplace tab; every install is
|
|
staged, OpenClaw frontmatter is translated into the
|
|
Ouroboros manifest shape, and the standard tri-model review runs
|
|
automatically before the skill becomes executable. Plugins (Node)
|
|
are filtered out — only skill packages are installable.
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<label for="s-clawhub-registry-url">Registry URL</label>
|
|
<input id="s-clawhub-registry-url" placeholder="https://clawhub.ai/api/v1" class="ui-control" name="s-clawhub-registry-url" type="text" aria-describedby="s-clawhub-registry-url-help">
|
|
<div class="settings-inline-note ui-field-help" id="s-clawhub-registry-url-help">Override only for self-hosted mirrors. Hostname must be <code>clawhub.ai</code> or localhost.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="appearance">
|
|
<div class="form-section">
|
|
<h3>Theme</h3>
|
|
<div class="settings-section-copy">
|
|
<code>System</code> follows this device's OS appearance and is the default for a new client.
|
|
<code>Light</code> and <code>Dark</code> pin the palette regardless of the OS.
|
|
<br><strong>Per device, not per account:</strong> the choice is stored by this client alone
|
|
(the desktop window and each browser keep their own), never sent to the server and never
|
|
shared with other devices. Clearing this client's site data returns it to System.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label class="theme-choice-label" id="s-appearance-theme-label">Theme</label>
|
|
<div data-theme-control aria-labelledby="s-appearance-theme-label"></div>
|
|
<div class="settings-inline-note theme-status" data-theme-status role="status" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section" data-notify-settings>
|
|
<h3>Notifications</h3>
|
|
<div class="settings-section-copy">
|
|
While this client is running, Ouroboros can pull you back to a question or a
|
|
finished task. Notifications arrive whether or not this window has focus, and
|
|
clicking one opens its source.
|
|
<br><strong>Per device, not per account:</strong> like the theme above, these choices
|
|
are stored by this client alone and never sent to the server.
|
|
Where this system exposes no notifications, or permission is denied, alerts appear
|
|
inside the app instead. Do Not Disturb and OS permissions still decide what you see.
|
|
</div>
|
|
<div class="settings-effort-card">
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="enabled">
|
|
Enable notifications
|
|
</label>
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="needs_answer">
|
|
A question or decision is waiting for you
|
|
</label>
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="task_done">
|
|
A task finished or stopped
|
|
</label>
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="important">
|
|
Messages Ouroboros sends you while it works
|
|
</label>
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="main_reply">
|
|
Ordinary replies in Main
|
|
</label>
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="sound">
|
|
Sound
|
|
</label>
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" class="ui-checkbox" data-notify-pref="show_text">
|
|
Show the message text (otherwise only the kind of event)
|
|
</label>
|
|
<div class="settings-toolbar">
|
|
<button type="button" class="btn btn-default btn-sm" data-notify-test>Send a test notification</button>
|
|
</div>
|
|
<div class="settings-inline-note" data-notify-status role="status" aria-live="polite"></div>
|
|
<div class="settings-inline-note" data-notify-attention-status role="status" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="advanced">
|
|
<div class="form-section">
|
|
<div class="settings-card-head">
|
|
<div>
|
|
<h3>MCP Servers</h3>
|
|
<div class="settings-section-copy">
|
|
External Model Context Protocol tool servers. MCP is a base-runtime client:
|
|
it borrows tools from trusted HTTP/SSE servers or local stdio processes and exposes them as non-core
|
|
<code>mcp_<server>__<tool></code> tools after refresh. Changes are hot-reloadable.
|
|
Treat server descriptions and results as untrusted third-party data.
|
|
</div>
|
|
</div>
|
|
<div class="settings-toolbar">
|
|
<button type="button" class="btn btn-default btn-sm" id="btn-mcp-add-server">Add server</button>
|
|
<button type="button" class="btn btn-default btn-sm" id="btn-mcp-refresh-all">Refresh all</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid two">
|
|
<label class="local-toggle ui-field ui-field-inline">
|
|
<input type="checkbox" id="s-mcp-enabled" class="ui-checkbox" name="s-mcp-enabled">
|
|
Enable MCP client
|
|
</label>
|
|
<div class="form-field ui-field">
|
|
<label for="s-mcp-tool-timeout">Per-tool timeout (s)</label>
|
|
<input id="s-mcp-tool-timeout" type="number" min="1" value="60" class="ui-control" name="s-mcp-tool-timeout">
|
|
</div>
|
|
</div>
|
|
<div id="mcp-global-status" class="settings-inline-status">Checking MCP status…</div>
|
|
<div id="mcp-servers-list" class="mcp-servers-list"></div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Source Control</h3>
|
|
<div class="settings-section-copy">Repository metadata for GitHub integration. Tokens live in Secrets; this is not secret.</div>
|
|
<div class="form-row">
|
|
<div class="form-field ui-field">
|
|
<label for="s-gh-repo">GitHub Repo</label>
|
|
<input id="s-gh-repo" placeholder="owner/repo-name" class="ui-control" name="s-gh-repo" type="text">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Local Model Runtime</h3>
|
|
<div class="settings-section-copy">Only fill this in when you want Ouroboros to start and route to a GGUF model on this machine.</div>
|
|
<div class="form-grid two">
|
|
<div class="form-field ui-field">
|
|
<label for="s-local-source">Model Source</label>
|
|
<input id="s-local-source" placeholder="bartowski/Llama-3.3-70B-Instruct-GGUF or /path/to/model.gguf" class="ui-control" name="s-local-source" type="text">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-local-filename">GGUF Filename (for HF repos)</label>
|
|
<input id="s-local-filename" placeholder="Llama-3.3-70B-Instruct-Q4_K_M.gguf" class="ui-control" name="s-local-filename" type="text">
|
|
</div>
|
|
</div>
|
|
<div class="form-grid four">
|
|
<div class="form-field ui-field">
|
|
<label for="s-local-port">Port</label>
|
|
<input id="s-local-port" type="number" value="8766" class="ui-control" name="s-local-port">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-local-gpu-layers">GPU Layers (-1 = all)</label>
|
|
<input id="s-local-gpu-layers" type="number" value="-1" class="ui-control" name="s-local-gpu-layers">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-local-ctx">Context Length</label>
|
|
<input id="s-local-ctx" type="number" value="16384" class="ui-control" name="s-local-ctx">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-local-chat-format">Chat Format</label>
|
|
<input id="s-local-chat-format" placeholder="auto-detect" class="ui-control" name="s-local-chat-format" type="text">
|
|
</div>
|
|
</div>
|
|
<div class="settings-toolbar">
|
|
<button class="btn btn-primary" id="btn-local-start">Start</button>
|
|
<button class="btn btn-primary" id="btn-local-stop">Stop</button>
|
|
<button class="btn btn-primary" id="btn-local-test">Test Tool Calling</button>
|
|
</div>
|
|
<div id="local-model-status" class="settings-inline-status" role="status" aria-live="polite">Status: Offline</div>
|
|
<div id="local-model-action-status" class="settings-inline-status" role="status" aria-live="polite" aria-atomic="true"></div>
|
|
<div id="local-model-progress-wrap" class="local-model-progress-wrap local-model-hidden" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
|
<div id="local-model-progress-bar" class="local-model-progress-bar"></div>
|
|
</div>
|
|
<button class="btn btn-secondary local-model-install-btn local-model-hidden" id="btn-local-install-runtime">Install Local Runtime</button>
|
|
<div id="local-model-test-result" class="settings-test-result"></div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Runtime Limits</h3>
|
|
<!-- Active Subagents / Root and Subagent Depth moved to
|
|
Agents → Delegation (D-10): they bound the agents,
|
|
not the process pool. Max Workers stays: it is
|
|
runtime worker processes, not an agent setting. -->
|
|
<div class="settings-section-copy">Workers control parallel task capacity. Task liveness is governed automatically by progress, deadlines, the idle rail and the reaper; the per-task round and lifetime limits are optional — a positive number, or <code>unlimited</code> for none (the fresh-install default). Budget limits control runtime cost thresholds. How many subagents a task may run, and how deep they may nest, live in <code>Agents</code>.</div>
|
|
<div class="form-grid two">
|
|
<div class="form-field ui-field">
|
|
<label for="s-workers">Max Workers</label>
|
|
<input id="s-workers" type="number" min="1" max="50" value="10" class="ui-control" name="s-workers">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-max-rounds">Max Rounds per Task</label>
|
|
<input id="s-max-rounds" type="text" inputmode="numeric" value="unlimited" placeholder="unlimited" class="ui-control" name="s-max-rounds">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-task-lifetime">Task Lifetime Limit (s)</label>
|
|
<input id="s-task-lifetime" type="text" inputmode="numeric" value="unlimited" placeholder="unlimited" class="ui-control" name="s-task-lifetime">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-presence-max-active">Concurrent Presence Conversations</label>
|
|
<input id="s-presence-max-active" type="number" min="1" max="20" value="2" class="ui-control" name="s-presence-max-active">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-tool-timeout">Tool Timeout (s)</label>
|
|
<input id="s-tool-timeout" type="number" value="600" class="ui-control" name="s-tool-timeout">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-total-budget">Total Budget (USD)</label>
|
|
<input id="s-total-budget" type="number" min="0.01" step="any" value="200.0" class="ui-control" name="s-total-budget">
|
|
</div>
|
|
<div class="form-field ui-field">
|
|
<label for="s-settings-per-task-cost">Per-Task Cost Cap (USD)</label>
|
|
<input id="s-settings-per-task-cost" type="number" min="0.01" step="any" value="50.0" class="ui-control" name="s-settings-per-task-cost">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Cleanup</h3>
|
|
<!-- The two subagent path roots moved to Agents →
|
|
Delegation → Advanced (D-10). GC Retention stays: it
|
|
governs every disposable runtime artifact, not just
|
|
subagent worktrees. -->
|
|
<div class="settings-section-copy">
|
|
<strong>GC Retention</strong> is the single age knob (days) for all disposable runtime artifacts the startup garbage collector removes: acting-subagent worktrees, terminal task drives, and leftover service logs (hard max 365). Genesis projects are durable and never auto-removed. Where subagents check out that work is set in <code>Agents</code>.
|
|
</div>
|
|
<div class="form-grid two">
|
|
<div class="form-field ui-field">
|
|
<label for="s-gc-retention-days">GC Retention (days)</label>
|
|
<input id="s-gc-retention-days" type="number" min="1" max="365" value="7" class="ui-control" name="s-gc-retention-days">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h3>Extension Settings</h3>
|
|
<div class="settings-section-copy">
|
|
Live extensions can register reviewed, host-rendered settings sections.
|
|
Sections appear here after the owning skill is reviewed, enabled, and loaded.
|
|
</div>
|
|
<div id="extension-settings-sections" class="settings-extension-sections">
|
|
<div class="muted">No extension settings registered.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section danger">
|
|
<h3>Danger Zone</h3>
|
|
<div class="settings-inline-note">Reset still uses the current restart-based flow. This clears runtime data but keeps the repo.</div>
|
|
<button class="btn btn-danger" id="btn-reset">Reset All Data</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="settings-panel" data-settings-panel="about">
|
|
<div class="about-body">
|
|
<img src="/static/logo.jpg" class="about-logo" alt="Ouroboros">
|
|
<div>
|
|
<h1 class="about-title">Ouroboros</h1>
|
|
<p id="about-version" class="about-version"></p>
|
|
</div>
|
|
<p class="about-desc">
|
|
A self-creating AI agent. Not a tool, but a becoming digital personality
|
|
with its own constitution, persistent identity, and background consciousness.
|
|
Born February 16, 2026.
|
|
</p>
|
|
<div class="about-credits">
|
|
<span>Created by <strong>Anton Razzhigaev</strong> & <strong>Andrew Kaznacheev</strong></span>
|
|
<div class="about-links">
|
|
<a href="https://t.me/abstractDL" target="_blank" rel="noopener noreferrer">@abstractDL</a>
|
|
<a href="https://github.com/razzant/ouroboros" target="_blank" rel="noopener noreferrer">GitHub</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="settings-footer">
|
|
<div class="settings-footer-actions">
|
|
<button type="button" class="btn btn-secondary" id="btn-reload-settings">Reload Settings</button>
|
|
<button class="btn btn-save" id="btn-save-settings">Save Settings</button>
|
|
<button type="button" class="btn btn-secondary" id="btn-restart-now" hidden
|
|
title="Restart the agent process">Restart now</button>
|
|
</div>
|
|
<div class="settings-footer-status">
|
|
<span id="settings-unsaved-indicator" class="settings-inline-status settings-unsaved-indicator" aria-hidden="true">Unsaved changes</span>
|
|
<div id="settings-restart-status" class="settings-inline-status" role="status" aria-live="polite" hidden></div>
|
|
<div id="settings-status" class="settings-inline-status" role="status" aria-live="polite" aria-atomic="true"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export function bindSettingsTabs(root, options = {}) {
|
|
const panels = Array.from(root.querySelectorAll('.settings-panel'));
|
|
const scrollRoot = root.querySelector('.settings-scroll');
|
|
const state = options.state || null;
|
|
const onActivate = typeof options.onActivate === 'function' ? options.onActivate : null;
|
|
|
|
const tabs = bindTabStrip(root.querySelector('.settings-tabs'), {
|
|
dataAttr: 'data-settings-tab', onChange: (value) => activate(value),
|
|
});
|
|
panels.forEach((panel) => {
|
|
panel.id = `settings-panel-${panel.dataset.settingsPanel}`;
|
|
panel.setAttribute('role', 'tabpanel');
|
|
panel.setAttribute('aria-labelledby', `settings-tab-${panel.dataset.settingsPanel}`);
|
|
});
|
|
|
|
function activate(tabName, notify = true) {
|
|
if (!tabs.select(tabName)) return;
|
|
const changed = root.dataset.activeSettingsTab !== tabName;
|
|
root.dataset.activeSettingsTab = tabName;
|
|
panels.forEach((panel) => {
|
|
panel.classList.toggle('active', panel.dataset.settingsPanel === tabName);
|
|
panel.hidden = panel.dataset.settingsPanel !== tabName;
|
|
});
|
|
if (scrollRoot && changed && notify) scrollRoot.scrollTop = 0;
|
|
if (state) state.settingsActiveSubtab = tabName;
|
|
if (notify && changed) {
|
|
if (onActivate) onActivate(tabName);
|
|
window.dispatchEvent(new CustomEvent('ouro:settings-subtab-shown', { detail: { tab: tabName } }));
|
|
}
|
|
}
|
|
|
|
root.activateSettingsTab = activate;
|
|
activate(state?.settingsActiveSubtab || 'providers', false);
|
|
return () => { tabs.destroy(); delete root.activateSettingsTab; };
|
|
}
|
|
|
|
export function bindSecretInputs(root) {
|
|
root.querySelectorAll('.secret-toggle, .secret-clear').forEach((button) => {
|
|
const input = root.querySelector(`#${button.dataset.target}`);
|
|
if (!input) return;
|
|
button.setAttribute('aria-controls', input.id);
|
|
const label = input.labels?.[0];
|
|
if (label) {
|
|
label.id ||= `${input.id}-label`;
|
|
button.setAttribute('aria-describedby', label.id);
|
|
}
|
|
});
|
|
root.querySelectorAll('.secret-input').forEach((input) => {
|
|
input.addEventListener('input', () => {
|
|
if (input.value.trim()) delete input.dataset.forceClear;
|
|
});
|
|
});
|
|
|
|
root.querySelectorAll('.secret-toggle').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const target = root.querySelector(`#${button.dataset.target}`);
|
|
if (!target) return;
|
|
const nextType = target.type === 'password' ? 'text' : 'password';
|
|
target.type = nextType;
|
|
button.textContent = nextType === 'password' ? 'Show' : 'Hide';
|
|
});
|
|
});
|
|
|
|
root.querySelectorAll('.secret-clear').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const target = root.querySelector(`#${button.dataset.target}`);
|
|
if (!target) return;
|
|
target.value = '';
|
|
target.type = 'password';
|
|
target.dataset.forceClear = '1';
|
|
const toggle = root.querySelector(`.secret-toggle[data-target="${button.dataset.target}"]`);
|
|
if (toggle) toggle.textContent = 'Show';
|
|
// Programmatic value changes fire no 'input' event, but a Clear is
|
|
// an edit like any other: the provider-test verdict-expiry listener
|
|
// must see it, or a stale OK keeps vouching for a cleared key.
|
|
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
});
|
|
}
|