初始化提交
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
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
This commit is contained in:
669
crates/openfang-api/static/js/pages/settings.js
Normal file
669
crates/openfang-api/static/js/pages/settings.js
Normal file
@@ -0,0 +1,669 @@
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user