Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
670 lines
25 KiB
JavaScript
670 lines
25 KiB
JavaScript
// OpenFang Settings Page — Provider Hub, Model Catalog, Config, Tools + Security, Network, Migration tabs
|
|
'use strict';
|
|
|
|
function settingsPage() {
|
|
return {
|
|
tab: 'providers',
|
|
sysInfo: {},
|
|
usageData: [],
|
|
tools: [],
|
|
config: {},
|
|
providers: [],
|
|
models: [],
|
|
toolSearch: '',
|
|
modelSearch: '',
|
|
modelProviderFilter: '',
|
|
modelTierFilter: '',
|
|
showCustomModelForm: false,
|
|
customModelId: '',
|
|
customModelProvider: 'openrouter',
|
|
customModelContext: 128000,
|
|
customModelMaxOutput: 8192,
|
|
customModelStatus: '',
|
|
providerKeyInputs: {},
|
|
providerUrlInputs: {},
|
|
providerUrlSaving: {},
|
|
providerTesting: {},
|
|
providerTestResults: {},
|
|
copilotOAuth: { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 },
|
|
loading: true,
|
|
loadError: '',
|
|
|
|
// -- Dynamic config state --
|
|
configSchema: null,
|
|
configValues: {},
|
|
configDirty: {},
|
|
configSaving: {},
|
|
|
|
// -- Security state --
|
|
securityData: null,
|
|
secLoading: false,
|
|
verifyingChain: false,
|
|
chainResult: null,
|
|
|
|
coreFeatures: [
|
|
{
|
|
name: 'Path Traversal Prevention', key: 'path_traversal',
|
|
description: 'Blocks directory escape attacks (../) in all file operations. Two-phase validation: syntactic rejection of path components, then canonicalization to normalize symlinks.',
|
|
threat: 'Directory escape, privilege escalation via symlinks',
|
|
impl: 'host_functions.rs — safe_resolve_path() + safe_resolve_parent()'
|
|
},
|
|
{
|
|
name: 'SSRF Protection', key: 'ssrf_protection',
|
|
description: 'Blocks outbound requests to private IPs, localhost, and cloud metadata endpoints (AWS/GCP/Azure). Validates DNS resolution results to defeat rebinding attacks.',
|
|
threat: 'Internal network reconnaissance, cloud credential theft',
|
|
impl: 'host_functions.rs — is_ssrf_target() + is_private_ip()'
|
|
},
|
|
{
|
|
name: 'Capability-Based Access Control', key: 'capability_system',
|
|
description: 'Deny-by-default permission system. Every agent operation (file I/O, network, shell, memory, spawn) requires an explicit capability grant in the manifest.',
|
|
threat: 'Unauthorized resource access, sandbox escape',
|
|
impl: 'host_functions.rs — check_capability() on every host function'
|
|
},
|
|
{
|
|
name: 'Privilege Escalation Prevention', key: 'privilege_escalation_prevention',
|
|
description: 'When a parent agent spawns a child, the kernel enforces child capabilities are a subset of parent capabilities. No agent can grant rights it does not have.',
|
|
threat: 'Capability escalation through agent spawning chains',
|
|
impl: 'kernel_handle.rs — spawn_agent_checked()'
|
|
},
|
|
{
|
|
name: 'Subprocess Environment Isolation', key: 'subprocess_isolation',
|
|
description: 'Child processes (shell tools) inherit only a safe allow-list of environment variables. API keys, database passwords, and secrets are never leaked to subprocesses.',
|
|
threat: 'Secret exfiltration via child process environment',
|
|
impl: 'subprocess_sandbox.rs — env_clear() + SAFE_ENV_VARS'
|
|
},
|
|
{
|
|
name: 'Security Headers', key: 'security_headers',
|
|
description: 'Every HTTP response includes CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, and X-XSS-Protection headers.',
|
|
threat: 'XSS, clickjacking, MIME sniffing, content injection',
|
|
impl: 'middleware.rs — security_headers()'
|
|
},
|
|
{
|
|
name: 'Wire Protocol Authentication', key: 'wire_hmac_auth',
|
|
description: 'Agent-to-agent OFP connections use HMAC-SHA256 mutual authentication with nonce-based handshake and constant-time signature comparison (subtle crate).',
|
|
threat: 'Man-in-the-middle attacks on mesh network',
|
|
impl: 'peer.rs — hmac_sign() + hmac_verify()'
|
|
},
|
|
{
|
|
name: 'Request ID Tracking', key: 'request_id_tracking',
|
|
description: 'Every API request receives a unique UUID (x-request-id header) and is logged with method, path, status code, and latency for full traceability.',
|
|
threat: 'Untraceable actions, forensic blind spots',
|
|
impl: 'middleware.rs — request_logging()'
|
|
}
|
|
],
|
|
|
|
configurableFeatures: [
|
|
{
|
|
name: 'API Rate Limiting', key: 'rate_limiter',
|
|
description: 'GCRA (Generic Cell Rate Algorithm) with cost-aware tokens. Different endpoints cost different amounts — spawning an agent costs 50 tokens, health check costs 1.',
|
|
configHint: 'Hard-coded: 500 tokens/minute per IP. Edit rate_limiter.rs to tune.',
|
|
valueKey: 'rate_limiter'
|
|
},
|
|
{
|
|
name: 'WebSocket Connection Limits', key: 'websocket_limits',
|
|
description: 'Per-IP connection cap prevents connection exhaustion. Idle timeout closes abandoned connections. Message rate limiting prevents flooding.',
|
|
configHint: 'Hard-coded: 5 connections/IP, 30min idle timeout, 64KB max message. Edit ws.rs to tune.',
|
|
valueKey: 'websocket_limits'
|
|
},
|
|
{
|
|
name: 'WASM Dual Metering', key: 'wasm_sandbox',
|
|
description: 'WASM modules run with two independent resource limits: fuel metering (CPU instruction count) and epoch interruption (wall-clock timeout with watchdog thread).',
|
|
configHint: 'Default: 1M fuel units, 30s timeout. Configurable per-agent via SandboxConfig.',
|
|
valueKey: 'wasm_sandbox'
|
|
},
|
|
{
|
|
name: 'Bearer Token Authentication', key: 'auth',
|
|
description: 'All non-health endpoints require Authorization: Bearer header. When no API key is configured, all requests are restricted to localhost only.',
|
|
configHint: 'Set api_key in ~/.openfang/config.toml for remote access. Empty = localhost only.',
|
|
valueKey: 'auth'
|
|
}
|
|
],
|
|
|
|
monitoringFeatures: [
|
|
{
|
|
name: 'Merkle Audit Trail', key: 'audit_trail',
|
|
description: 'Every security-critical action is appended to an immutable, tamper-evident log. Each entry is cryptographically linked to the previous via SHA-256 hash chain.',
|
|
configHint: 'Always active. Verify chain integrity from the Audit Log page.',
|
|
valueKey: 'audit_trail'
|
|
},
|
|
{
|
|
name: 'Information Flow Taint Tracking', key: 'taint_tracking',
|
|
description: 'Labels data by provenance (ExternalNetwork, UserInput, PII, Secret, UntrustedAgent) and blocks unsafe flows: external data cannot reach shell_exec, secrets cannot reach network.',
|
|
configHint: 'Always active. Prevents data flow attacks automatically.',
|
|
valueKey: 'taint_tracking'
|
|
},
|
|
{
|
|
name: 'Ed25519 Manifest Signing', key: 'manifest_signing',
|
|
description: 'Agent manifests can be cryptographically signed with Ed25519. Verify manifest integrity before loading to prevent supply chain tampering.',
|
|
configHint: 'Available for use. Sign manifests with ed25519-dalek for verification.',
|
|
valueKey: 'manifest_signing'
|
|
}
|
|
],
|
|
|
|
// -- Peers state --
|
|
peers: [],
|
|
peersLoading: false,
|
|
peersLoadError: '',
|
|
_peerPollTimer: null,
|
|
|
|
// -- Migration state --
|
|
migStep: 'intro',
|
|
detecting: false,
|
|
scanning: false,
|
|
migrating: false,
|
|
sourcePath: '',
|
|
targetPath: '',
|
|
scanResult: null,
|
|
migResult: null,
|
|
|
|
// -- Settings load --
|
|
async loadSettings() {
|
|
this.loading = true;
|
|
this.loadError = '';
|
|
try {
|
|
await Promise.all([
|
|
this.loadSysInfo(),
|
|
this.loadUsage(),
|
|
this.loadTools(),
|
|
this.loadConfig(),
|
|
this.loadProviders(),
|
|
this.loadModels()
|
|
]);
|
|
} catch(e) {
|
|
this.loadError = e.message || 'Could not load settings.';
|
|
}
|
|
this.loading = false;
|
|
},
|
|
|
|
async loadData() { return this.loadSettings(); },
|
|
|
|
async loadSysInfo() {
|
|
try {
|
|
var ver = await OpenFangAPI.get('/api/version');
|
|
var status = await OpenFangAPI.get('/api/status');
|
|
this.sysInfo = {
|
|
version: ver.version || '-',
|
|
platform: ver.platform || '-',
|
|
arch: ver.arch || '-',
|
|
uptime_seconds: status.uptime_seconds || 0,
|
|
agent_count: status.agent_count || 0,
|
|
default_provider: status.default_provider || '-',
|
|
default_model: status.default_model || '-'
|
|
};
|
|
} catch(e) { throw e; }
|
|
},
|
|
|
|
async loadUsage() {
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/usage');
|
|
this.usageData = data.agents || [];
|
|
} catch(e) { this.usageData = []; }
|
|
},
|
|
|
|
async loadTools() {
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/tools');
|
|
this.tools = data.tools || [];
|
|
} catch(e) { this.tools = []; }
|
|
},
|
|
|
|
async loadConfig() {
|
|
try {
|
|
this.config = await OpenFangAPI.get('/api/config');
|
|
} catch(e) { this.config = {}; }
|
|
},
|
|
|
|
async loadProviders() {
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/providers');
|
|
this.providers = data.providers || [];
|
|
for (var i = 0; i < this.providers.length; i++) {
|
|
var p = this.providers[i];
|
|
if (p.is_local && p.base_url && !this.providerUrlInputs[p.id]) {
|
|
this.providerUrlInputs[p.id] = p.base_url;
|
|
}
|
|
}
|
|
} catch(e) { this.providers = []; }
|
|
},
|
|
|
|
async loadModels() {
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/models');
|
|
this.models = data.models || [];
|
|
} catch(e) { this.models = []; }
|
|
},
|
|
|
|
async addCustomModel() {
|
|
var id = this.customModelId.trim();
|
|
if (!id) return;
|
|
this.customModelStatus = 'Adding...';
|
|
try {
|
|
await OpenFangAPI.post('/api/models/custom', {
|
|
id: id,
|
|
provider: this.customModelProvider || 'openrouter',
|
|
context_window: this.customModelContext || 128000,
|
|
max_output_tokens: this.customModelMaxOutput || 8192,
|
|
});
|
|
this.customModelStatus = 'Added!';
|
|
this.customModelId = '';
|
|
this.showCustomModelForm = false;
|
|
await this.loadModels();
|
|
} catch(e) {
|
|
this.customModelStatus = 'Error: ' + (e.message || 'Failed');
|
|
}
|
|
},
|
|
|
|
async loadConfigSchema() {
|
|
try {
|
|
var results = await Promise.all([
|
|
OpenFangAPI.get('/api/config/schema').catch(function() { return {}; }),
|
|
OpenFangAPI.get('/api/config')
|
|
]);
|
|
this.configSchema = results[0].sections || null;
|
|
this.configValues = results[1] || {};
|
|
} catch(e) { /* silent */ }
|
|
},
|
|
|
|
isConfigDirty(section, field) {
|
|
return this.configDirty[section + '.' + field] === true;
|
|
},
|
|
|
|
markConfigDirty(section, field) {
|
|
this.configDirty[section + '.' + field] = true;
|
|
},
|
|
|
|
async saveConfigField(section, field, value) {
|
|
var key = section + '.' + field;
|
|
this.configSaving[key] = true;
|
|
try {
|
|
await OpenFangAPI.post('/api/config/set', { path: key, value: value });
|
|
this.configDirty[key] = false;
|
|
OpenFangToast.success('Saved ' + key);
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save: ' + e.message);
|
|
}
|
|
this.configSaving[key] = false;
|
|
},
|
|
|
|
get filteredTools() {
|
|
var q = this.toolSearch.toLowerCase().trim();
|
|
if (!q) return this.tools;
|
|
return this.tools.filter(function(t) {
|
|
return t.name.toLowerCase().indexOf(q) !== -1 ||
|
|
(t.description || '').toLowerCase().indexOf(q) !== -1;
|
|
});
|
|
},
|
|
|
|
get filteredModels() {
|
|
var self = this;
|
|
return this.models.filter(function(m) {
|
|
if (self.modelProviderFilter && m.provider !== self.modelProviderFilter) return false;
|
|
if (self.modelTierFilter && m.tier !== self.modelTierFilter) return false;
|
|
if (self.modelSearch) {
|
|
var q = self.modelSearch.toLowerCase();
|
|
if (m.id.toLowerCase().indexOf(q) === -1 &&
|
|
(m.display_name || '').toLowerCase().indexOf(q) === -1) return false;
|
|
}
|
|
return true;
|
|
});
|
|
},
|
|
|
|
get uniqueProviderNames() {
|
|
var seen = {};
|
|
this.models.forEach(function(m) { seen[m.provider] = true; });
|
|
return Object.keys(seen).sort();
|
|
},
|
|
|
|
get uniqueTiers() {
|
|
var seen = {};
|
|
this.models.forEach(function(m) { if (m.tier) seen[m.tier] = true; });
|
|
return Object.keys(seen).sort();
|
|
},
|
|
|
|
providerAuthClass(p) {
|
|
if (p.auth_status === 'configured') return 'auth-configured';
|
|
if (p.auth_status === 'not_set' || p.auth_status === 'missing') return 'auth-not-set';
|
|
return 'auth-no-key';
|
|
},
|
|
|
|
providerAuthText(p) {
|
|
if (p.auth_status === 'configured') return 'Configured';
|
|
if (p.auth_status === 'not_set' || p.auth_status === 'missing') return 'Not Set';
|
|
return 'No Key Needed';
|
|
},
|
|
|
|
providerCardClass(p) {
|
|
if (p.auth_status === 'configured') return 'configured';
|
|
if (p.auth_status === 'not_set' || p.auth_status === 'missing') return 'not-configured';
|
|
return 'no-key';
|
|
},
|
|
|
|
tierBadgeClass(tier) {
|
|
if (!tier) return '';
|
|
var t = tier.toLowerCase();
|
|
if (t === 'frontier') return 'tier-frontier';
|
|
if (t === 'smart') return 'tier-smart';
|
|
if (t === 'balanced') return 'tier-balanced';
|
|
if (t === 'fast') return 'tier-fast';
|
|
return '';
|
|
},
|
|
|
|
formatCost(cost) {
|
|
if (!cost && cost !== 0) return '-';
|
|
return '$' + cost.toFixed(4);
|
|
},
|
|
|
|
formatContext(ctx) {
|
|
if (!ctx) return '-';
|
|
if (ctx >= 1000000) return (ctx / 1000000).toFixed(1) + 'M';
|
|
if (ctx >= 1000) return Math.round(ctx / 1000) + 'K';
|
|
return String(ctx);
|
|
},
|
|
|
|
formatUptime(secs) {
|
|
if (!secs) return '-';
|
|
var h = Math.floor(secs / 3600);
|
|
var m = Math.floor((secs % 3600) / 60);
|
|
var s = secs % 60;
|
|
if (h > 0) return h + 'h ' + m + 'm';
|
|
if (m > 0) return m + 'm ' + s + 's';
|
|
return s + 's';
|
|
},
|
|
|
|
async saveProviderKey(provider) {
|
|
var key = this.providerKeyInputs[provider.id];
|
|
if (!key || !key.trim()) { OpenFangToast.error('Please enter an API key'); return; }
|
|
try {
|
|
await OpenFangAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/key', { key: key.trim() });
|
|
OpenFangToast.success('API key saved for ' + provider.display_name);
|
|
this.providerKeyInputs[provider.id] = '';
|
|
await this.loadProviders();
|
|
await this.loadModels();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save key: ' + e.message);
|
|
}
|
|
},
|
|
|
|
async removeProviderKey(provider) {
|
|
try {
|
|
await OpenFangAPI.del('/api/providers/' + encodeURIComponent(provider.id) + '/key');
|
|
OpenFangToast.success('API key removed for ' + provider.display_name);
|
|
await this.loadProviders();
|
|
await this.loadModels();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to remove key: ' + e.message);
|
|
}
|
|
},
|
|
|
|
async startCopilotOAuth() {
|
|
this.copilotOAuth.polling = true;
|
|
this.copilotOAuth.userCode = '';
|
|
try {
|
|
var resp = await OpenFangAPI.post('/api/providers/github-copilot/oauth/start', {});
|
|
this.copilotOAuth.userCode = resp.user_code;
|
|
this.copilotOAuth.verificationUri = resp.verification_uri;
|
|
this.copilotOAuth.pollId = resp.poll_id;
|
|
this.copilotOAuth.interval = resp.interval || 5;
|
|
window.open(resp.verification_uri, '_blank');
|
|
this.pollCopilotOAuth();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to start Copilot login: ' + e.message);
|
|
this.copilotOAuth.polling = false;
|
|
}
|
|
},
|
|
|
|
pollCopilotOAuth() {
|
|
var self = this;
|
|
setTimeout(async function() {
|
|
if (!self.copilotOAuth.pollId) return;
|
|
try {
|
|
var resp = await OpenFangAPI.get('/api/providers/github-copilot/oauth/poll/' + self.copilotOAuth.pollId);
|
|
if (resp.status === 'complete') {
|
|
OpenFangToast.success('GitHub Copilot authenticated successfully!');
|
|
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
|
|
await self.loadProviders();
|
|
await self.loadModels();
|
|
} else if (resp.status === 'pending') {
|
|
if (resp.interval) self.copilotOAuth.interval = resp.interval;
|
|
self.pollCopilotOAuth();
|
|
} else if (resp.status === 'expired') {
|
|
OpenFangToast.error('Device code expired. Please try again.');
|
|
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
|
|
} else if (resp.status === 'denied') {
|
|
OpenFangToast.error('Access denied by user.');
|
|
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
|
|
} else {
|
|
OpenFangToast.error('OAuth error: ' + (resp.error || resp.status));
|
|
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Poll error: ' + e.message);
|
|
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
|
|
}
|
|
}, self.copilotOAuth.interval * 1000);
|
|
},
|
|
|
|
async testProvider(provider) {
|
|
this.providerTesting[provider.id] = true;
|
|
this.providerTestResults[provider.id] = null;
|
|
try {
|
|
var result = await OpenFangAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/test', {});
|
|
this.providerTestResults[provider.id] = result;
|
|
if (result.status === 'ok') {
|
|
OpenFangToast.success(provider.display_name + ' connected (' + (result.latency_ms || '?') + 'ms)');
|
|
} else {
|
|
OpenFangToast.error(provider.display_name + ': ' + (result.error || 'Connection failed'));
|
|
}
|
|
} catch(e) {
|
|
this.providerTestResults[provider.id] = { status: 'error', error: e.message };
|
|
OpenFangToast.error('Test failed: ' + e.message);
|
|
}
|
|
this.providerTesting[provider.id] = false;
|
|
},
|
|
|
|
async saveProviderUrl(provider) {
|
|
var url = this.providerUrlInputs[provider.id];
|
|
if (!url || !url.trim()) { OpenFangToast.error('Please enter a base URL'); return; }
|
|
url = url.trim();
|
|
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
|
|
OpenFangToast.error('URL must start with http:// or https://'); return;
|
|
}
|
|
this.providerUrlSaving[provider.id] = true;
|
|
try {
|
|
var result = await OpenFangAPI.put('/api/providers/' + encodeURIComponent(provider.id) + '/url', { base_url: url });
|
|
if (result.reachable) {
|
|
OpenFangToast.success(provider.display_name + ' URL saved — reachable (' + (result.latency_ms || '?') + 'ms)');
|
|
} else {
|
|
OpenFangToast.warning(provider.display_name + ' URL saved but not reachable');
|
|
}
|
|
await this.loadProviders();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save URL: ' + e.message);
|
|
}
|
|
this.providerUrlSaving[provider.id] = false;
|
|
},
|
|
|
|
// -- Security methods --
|
|
async loadSecurity() {
|
|
this.secLoading = true;
|
|
try {
|
|
this.securityData = await OpenFangAPI.get('/api/security');
|
|
} catch(e) {
|
|
this.securityData = null;
|
|
}
|
|
this.secLoading = false;
|
|
},
|
|
|
|
isActive(key) {
|
|
if (!this.securityData) return true;
|
|
var core = this.securityData.core_protections || {};
|
|
if (core[key] !== undefined) return core[key];
|
|
return true;
|
|
},
|
|
|
|
getConfigValue(key) {
|
|
if (!this.securityData) return null;
|
|
var cfg = this.securityData.configurable || {};
|
|
return cfg[key] || null;
|
|
},
|
|
|
|
getMonitoringValue(key) {
|
|
if (!this.securityData) return null;
|
|
var mon = this.securityData.monitoring || {};
|
|
return mon[key] || null;
|
|
},
|
|
|
|
formatConfigValue(feature) {
|
|
var val = this.getConfigValue(feature.valueKey);
|
|
if (!val) return feature.configHint;
|
|
switch (feature.valueKey) {
|
|
case 'rate_limiter':
|
|
return 'Algorithm: ' + (val.algorithm || 'GCRA') + ' | ' + (val.tokens_per_minute || 500) + ' tokens/min per IP';
|
|
case 'websocket_limits':
|
|
return 'Max ' + (val.max_per_ip || 5) + ' conn/IP | ' + Math.round((val.idle_timeout_secs || 1800) / 60) + 'min idle timeout | ' + Math.round((val.max_message_size || 65536) / 1024) + 'KB max msg';
|
|
case 'wasm_sandbox':
|
|
return 'Fuel: ' + (val.fuel_metering ? 'ON' : 'OFF') + ' | Epoch: ' + (val.epoch_interruption ? 'ON' : 'OFF') + ' | Timeout: ' + (val.default_timeout_secs || 30) + 's';
|
|
case 'auth':
|
|
return 'Mode: ' + (val.mode || 'unknown') + (val.api_key_set ? ' (key configured)' : ' (no key set)');
|
|
default:
|
|
return feature.configHint;
|
|
}
|
|
},
|
|
|
|
formatMonitoringValue(feature) {
|
|
var val = this.getMonitoringValue(feature.valueKey);
|
|
if (!val) return feature.configHint;
|
|
switch (feature.valueKey) {
|
|
case 'audit_trail':
|
|
return (val.enabled ? 'Active' : 'Disabled') + ' | ' + (val.algorithm || 'SHA-256') + ' | ' + (val.entry_count || 0) + ' entries logged';
|
|
case 'taint_tracking':
|
|
var labels = val.tracked_labels || [];
|
|
return (val.enabled ? 'Active' : 'Disabled') + ' | Tracking: ' + labels.join(', ');
|
|
case 'manifest_signing':
|
|
return 'Algorithm: ' + (val.algorithm || 'Ed25519') + ' | ' + (val.available ? 'Available' : 'Not available');
|
|
default:
|
|
return feature.configHint;
|
|
}
|
|
},
|
|
|
|
async verifyAuditChain() {
|
|
this.verifyingChain = true;
|
|
this.chainResult = null;
|
|
try {
|
|
var res = await OpenFangAPI.get('/api/audit/verify');
|
|
this.chainResult = res;
|
|
} catch(e) {
|
|
this.chainResult = { valid: false, error: e.message };
|
|
}
|
|
this.verifyingChain = false;
|
|
},
|
|
|
|
// -- Peers methods --
|
|
async loadPeers() {
|
|
this.peersLoading = true;
|
|
this.peersLoadError = '';
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/peers');
|
|
this.peers = (data.peers || []).map(function(p) {
|
|
return {
|
|
node_id: p.node_id,
|
|
node_name: p.node_name,
|
|
address: p.address,
|
|
state: p.state,
|
|
agent_count: (p.agents || []).length,
|
|
protocol_version: p.protocol_version || 1
|
|
};
|
|
});
|
|
} catch(e) {
|
|
this.peers = [];
|
|
this.peersLoadError = e.message || 'Could not load peers.';
|
|
}
|
|
this.peersLoading = false;
|
|
},
|
|
|
|
startPeerPolling() {
|
|
var self = this;
|
|
this.stopPeerPolling();
|
|
this._peerPollTimer = setInterval(async function() {
|
|
if (self.tab !== 'network') { self.stopPeerPolling(); return; }
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/peers');
|
|
self.peers = (data.peers || []).map(function(p) {
|
|
return {
|
|
node_id: p.node_id,
|
|
node_name: p.node_name,
|
|
address: p.address,
|
|
state: p.state,
|
|
agent_count: (p.agents || []).length,
|
|
protocol_version: p.protocol_version || 1
|
|
};
|
|
});
|
|
} catch(e) { /* silent */ }
|
|
}, 15000);
|
|
},
|
|
|
|
stopPeerPolling() {
|
|
if (this._peerPollTimer) { clearInterval(this._peerPollTimer); this._peerPollTimer = null; }
|
|
},
|
|
|
|
// -- Migration methods --
|
|
async autoDetect() {
|
|
this.detecting = true;
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/migrate/detect');
|
|
if (data.detected && data.scan) {
|
|
this.sourcePath = data.path;
|
|
this.scanResult = data.scan;
|
|
this.migStep = 'preview';
|
|
} else {
|
|
this.migStep = 'not_found';
|
|
}
|
|
} catch(e) {
|
|
this.migStep = 'not_found';
|
|
}
|
|
this.detecting = false;
|
|
},
|
|
|
|
async scanPath() {
|
|
if (!this.sourcePath) return;
|
|
this.scanning = true;
|
|
try {
|
|
var data = await OpenFangAPI.post('/api/migrate/scan', { path: this.sourcePath });
|
|
if (data.error) {
|
|
OpenFangToast.error('Scan error: ' + data.error);
|
|
this.scanning = false;
|
|
return;
|
|
}
|
|
this.scanResult = data;
|
|
this.migStep = 'preview';
|
|
} catch(e) {
|
|
OpenFangToast.error('Scan failed: ' + e.message);
|
|
}
|
|
this.scanning = false;
|
|
},
|
|
|
|
async runMigration(dryRun) {
|
|
this.migrating = true;
|
|
try {
|
|
var target = this.targetPath;
|
|
if (!target) target = '';
|
|
var data = await OpenFangAPI.post('/api/migrate', {
|
|
source: 'openclaw',
|
|
source_dir: this.sourcePath || (this.scanResult ? this.scanResult.path : ''),
|
|
target_dir: target,
|
|
dry_run: dryRun
|
|
});
|
|
this.migResult = data;
|
|
this.migStep = 'result';
|
|
} catch(e) {
|
|
this.migResult = { status: 'failed', error: e.message };
|
|
this.migStep = 'result';
|
|
}
|
|
this.migrating = false;
|
|
},
|
|
|
|
destroy() {
|
|
this.stopPeerPolling();
|
|
}
|
|
};
|
|
}
|