Skip to content

Commit 7d5a6b9

Browse files
committed
feat: planner oracle + discovery-driven malware hub (RFC-13 Phase 4, #68)
The core loop. Oracle (platform/services/oracle.py) is a thin request router over the shared ledger: route_pending resolves each open request to its decider by target_capability (the objective owner, or a quorum), record_decision enforces a distinct-approver rule so a branch cannot ratify its own request, and apply_decision applies ONLY the declared mechanical effect once ratified -- confirm the discovery (activate_phase), open or write an objective, or nothing (replan). It decides nothing and runs no LLM planner. The dispatch hub gains: overall-cap handling (emits budget_truncated on _budget_exhausted), a stall path that raises one replan request per distinct visited-set when unvisited phases are blocked by unmet conditions, and a ratified-replan relaxation -- a make_discovery_condition reads _dispatch_replan_relax and drops confirmed trust to advisory for one pass, so a quorum deadlock cannot freeze the graph. The hub's stall writer uses lazy service imports (phase_graph loads during db_models init), with a documented PLC0415 per-file-ignore. malware.investigate.hub runs the malware phases as a discovery-driven dispatch: triage (readiness), unpack (capability=re), config_extract (capability=crypto) both confirmed-trust, and full_analysis the unconditional advisory fallback. It ships bound nowhere live -- enabled by an operator seed rebind after smoke, exactly like the V2 graphs; the live V1 path is untouched. 14 tests: oracle routing/apply/reject/self-approval/quorum-k; the hub run through DurableStateMachine (setup->hub->A->hub->B->emit, budget cut, stall raises replan, resume preserves the visited-set); malware hub structure + capability/trust wiring + confirmed-discovery activation.
1 parent 7ac61ea commit 7d5a6b9

10 files changed

Lines changed: 749 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ operator action.
6666
closed branch's objectives to the investigation, so a terminal branch
6767
never keeps a live objective. No new table (objectives stay tagged
6868
ledger entries).
69+
- Planner oracle + discovery-driven malware hub (RFC-13 #68): a thin
70+
request router (`platform/services/oracle.py`) resolves ledger requests
71+
(activate_phase / open_objective / write_objective / replan) to their
72+
decider by `target_capability`, enforces a distinct-approver rule (a
73+
branch cannot ratify its own request), and applies only the declared
74+
mechanical effect once a quorum ratifies -- it decides nothing itself.
75+
The dispatch hub gained overall-cap handling (emits `budget_truncated`),
76+
a stall path that raises one `replan` request per visited-set, and a
77+
ratified-replan relaxation that drops confirmed trust to advisory for
78+
one pass so a quorum deadlock cannot freeze the graph.
79+
`malware.investigate.hub` ships the malware phases as a discovery-driven
80+
dispatch (unpack capability=re, config_extract capability=crypto, both
81+
confirmed-trust; full_analysis the advisory fallback), bound nowhere
82+
live -- enabled by an operator seed rebind after smoke, like the V2
83+
graphs.
6984
- Platform agent runtime (RFC-03): `AgentTurnRunnerBase`,
7085
`ToolExecutorHelpersBase`, the shared turn helpers, and platform bases
7186
for the pattern extractor, claim verifier, synthesis runner, persona

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ ignore = [
408408
"src/aila/platform/workflows/engine.py" = ["BLE001", "PLC0415"]
409409
"src/aila/platform/workflows/errors.py" = ["N818"]
410410
"src/aila/platform/workflows/log.py" = ["PLC0415", "BLE001"]
411+
# phase_graph.py: PLC0415 for the deferred LedgerService / UnitOfWork imports
412+
# inside the dispatch hub's stall handler. phase_graph is imported while
413+
# db_models is still loading (tasks -> workflows -> phase_graph), so a
414+
# module-scope services import would re-enter a half-built db_models.
415+
"src/aila/platform/workflows/phase_graph.py" = ["PLC0415"]
411416
"src/aila/storage/database.py" = ["PLC0415"]
412417
"src/aila/storage/db_models.py" = ["E402", "F401"]
413418
"src/aila/storage/operations.py" = ["PLC0415"]
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Malware investigation dispatch-hub graph (opt-in) over the shared substrate.
2+
3+
RFC-13 (#68). Where ``malware.investigate.v2`` wires the phases by static
4+
edges and a kind router, this graph runs them as a discovery-driven
5+
dispatch: setup, then the hub, then a phase, then back to the hub, until
6+
emit. The hub activates a phase only when its ``condition`` holds, so the
7+
deep phases fire on real discoveries posted to the shared ledger rather
8+
than on a fixed edge.
9+
10+
Phase activation:
11+
12+
* ``triage`` -- gated on target readiness (the same static-analysis gate
13+
V2 uses). It runs first and posts its structural findings to the ledger.
14+
* ``unpack`` -- ``capability="re"``, ``trust="confirmed"``: activates once
15+
a discovery is confirmed by a quorum decision (the reverse-engineering
16+
specialist's phase).
17+
* ``config_extract`` -- ``capability="crypto"``, ``trust="confirmed"``:
18+
activates on a confirmed discovery (the config/crypto specialist).
19+
* ``full_analysis`` -- unconditional, ``trust="advisory"``, declared last:
20+
the fallback deep phase that always remains available so the hub never
21+
dead-ends when no discovery routed to a specialist phase.
22+
23+
Malware runs a single agent server (ida_headless), so ``_branch_capability``
24+
is not threaded and the capability fields document intent rather than
25+
enforce per-branch routing (that matters for the multi-persona vr hub).
26+
The definition ships alongside V1 and V2; a seed binds it per investigation
27+
only after an operator smoke.
28+
"""
29+
from __future__ import annotations
30+
31+
from typing import cast
32+
33+
from aila.modules.malware.workflow.definitions import _build_services
34+
from aila.modules.malware.workflow.definitions_v2 import (
35+
_CONFIG_EXTRACT_DIRECTIVE,
36+
_FULL_ANALYSIS_DIRECTIVE,
37+
_TRIAGE_DIRECTIVE,
38+
_loop_builder,
39+
_setup_builder,
40+
_target_ready,
41+
)
42+
from aila.modules.malware.workflow.states.investigation_emit import (
43+
state_investigation_emit,
44+
)
45+
from aila.platform.services.ledger import make_discovery_condition
46+
from aila.platform.workflows.phase_graph import (
47+
PhaseSpec,
48+
build_dispatch_workflow,
49+
)
50+
from aila.platform.workflows.types import HandlerFn
51+
52+
__all__ = ["MALWARE_HUB_PHASES", "MALWARE_INVESTIGATE_HUB"]
53+
54+
_UNPACK_DIRECTIVE = (
55+
"UNPACKING PHASE. Objective: defeat the packer or obfuscation blocking "
56+
"static analysis -- identify the packer, dump the unpacked image, and "
57+
"reconstruct imports so the deeper phases read real code. Submit once "
58+
"the sample is statically analyzable."
59+
)
60+
61+
MALWARE_HUB_PHASES: tuple[PhaseSpec, ...] = (
62+
PhaseSpec(
63+
name="triage",
64+
directive=_TRIAGE_DIRECTIVE,
65+
max_turns=20,
66+
condition=_target_ready,
67+
trust="confirmed",
68+
),
69+
PhaseSpec(
70+
name="unpack",
71+
directive=_UNPACK_DIRECTIVE,
72+
condition=make_discovery_condition("discovery", confirmed_only=True),
73+
capability="re",
74+
trust="confirmed",
75+
),
76+
PhaseSpec(
77+
name="config_extract",
78+
directive=_CONFIG_EXTRACT_DIRECTIVE,
79+
condition=make_discovery_condition("discovery", confirmed_only=True),
80+
capability="crypto",
81+
trust="confirmed",
82+
),
83+
PhaseSpec(
84+
name="full_analysis",
85+
directive=_FULL_ANALYSIS_DIRECTIVE,
86+
trust="advisory",
87+
),
88+
)
89+
90+
91+
MALWARE_INVESTIGATE_HUB = build_dispatch_workflow(
92+
"malware.investigate.hub",
93+
MALWARE_HUB_PHASES,
94+
services_factory=_build_services,
95+
setup_builder=_setup_builder,
96+
loop_builder=_loop_builder,
97+
emit_handler=cast("HandlerFn", state_investigation_emit),
98+
)

src/aila/platform/services/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .http import build_http_client
99
from .knowledge import KnowledgeService
1010
from .ledger import LedgerService
11+
from .oracle import Oracle
1112
from .reasoning import CyberReasoningEngine
1213
from .reasoning_graphs import ReasoningGraphService
1314
from .report import ReportService
@@ -22,6 +23,7 @@
2223
"KnowledgeService",
2324
"LedgerService",
2425
"MiniLMProvider",
26+
"Oracle",
2527
"ReasoningGraphService",
2628
"ReportService",
2729
"SSHService",

src/aila/platform/services/ledger.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,13 +398,19 @@ async def _condition(state_input: dict[str, Any]) -> tuple[bool, str]:
398398
investigation_id = state_input.get(input_key)
399399
if not investigation_id:
400400
return False, f"no {input_key} on dispatch input"
401+
# A ratified replan relaxes confirmed trust for one hub pass, so a
402+
# confirmed-trust phase can activate on an unconfirmed discovery
403+
# rather than deadlocking when quorum never confirms (RFC-13 #68).
404+
effective_confirmed = confirmed_only and not state_input.get(
405+
"_dispatch_replan_relax"
406+
)
401407
entries = await LedgerService().read_general(
402408
str(investigation_id),
403409
kinds=[kind],
404-
confirmed_only=confirmed_only,
410+
confirmed_only=effective_confirmed,
405411
)
406412
if entries:
407-
scope = "confirmed " if confirmed_only else ""
413+
scope = "confirmed " if effective_confirmed else ""
408414
return True, f"{len(entries)} {scope}{kind} entries on ledger"
409415
return False, f"no {kind} entries on ledger yet"
410416

0 commit comments

Comments
 (0)