Add fleet phenotype cloning for sibling machines.
Publish winning tier paths per host fingerprint on hashrate success, inherit on auth before adaptive strategy, and surface clone badges in LOTL Timeline and Access Depth.
This commit is contained in:
@@ -27,10 +27,17 @@ export interface AdaptiveStrategyView {
|
||||
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;
|
||||
@@ -67,7 +74,8 @@ export interface OnionTierRow {
|
||||
index: number;
|
||||
tier: string;
|
||||
label: string;
|
||||
status: 'active' | 'skipped' | 'pending' | 'done' | 'failed' | 'neutral';
|
||||
status: 'active' | 'skipped' | 'skipped_by_atlas' | 'pending' | 'done' | 'failed' | 'neutral';
|
||||
atlasCondition?: string;
|
||||
}
|
||||
|
||||
export interface AccessDepthModel {
|
||||
@@ -92,6 +100,9 @@ export interface AccessDepthModel {
|
||||
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. */
|
||||
@@ -143,11 +154,13 @@ export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): Acces
|
||||
|
||||
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,
|
||||
@@ -216,6 +229,43 @@ function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeC
|
||||
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[] = [];
|
||||
@@ -303,8 +353,13 @@ function buildOnionRows(
|
||||
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();
|
||||
@@ -313,12 +368,15 @@ function buildOnionRows(
|
||||
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';
|
||||
@@ -329,6 +387,7 @@ function buildOnionRows(
|
||||
tier,
|
||||
label: formatLotlTierLabel(tier),
|
||||
status,
|
||||
atlasCondition: atlas?.condition,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -367,6 +426,7 @@ export function buildAccessDepthModel(
|
||||
): 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);
|
||||
@@ -402,7 +462,7 @@ export function buildAccessDepthModel(
|
||||
inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined,
|
||||
pendingTiers: pending,
|
||||
pendingLabels: pending.map(formatLotlTierLabel),
|
||||
miningOnion: buildOnionRows(order, skipped, attempts, activeTier),
|
||||
miningOnion: buildOnionRows(order, skipped, attempts, activeTier, atlasSkips),
|
||||
spreadOnion: spreadOrder.map((tier, i) => ({
|
||||
index: i + 1,
|
||||
tier,
|
||||
@@ -414,6 +474,9 @@ export function buildAccessDepthModel(
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user