Skip to content

feat(agents): wire DetectAgent.process to FusionEngine via cross-service HTTP (#190)#198

Merged
beenuar merged 5 commits into
mainfrom
feat/issue-190-detect-agent-fusion-wiring
May 20, 2026
Merged

feat(agents): wire DetectAgent.process to FusionEngine via cross-service HTTP (#190)#198
beenuar merged 5 commits into
mainfrom
feat/issue-190-detect-agent-fusion-wiring

Conversation

@beenuar

@beenuar beenuar commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #190.

DetectAgent already self-described as the public detection surface but had no synchronous entry point into the fusion pipeline. Anyone wanting to fuse an ad-hoc alert (e.g. mid-investigation) had to push to Kafka and wait for the consumer path to run dedup → correlation → ML scoring → confidence labelling → RBA. That's fine for streaming but unusable for interactive flows.

This PR wires the missing edge. Three additive pieces, no feature flag:

  • services/fusion/app/api/router.py exposes POST /process. Validates a RawAlert, runs it through the live FusionEngine instance owned by FusionWorker, returns the FusedAlert. Returns 503 if the worker hasn't bootstrapped its engine yet — fail loud rather than invent a verdict. Schema errors surface as the usual FastAPI 422.
  • services/agents/app/tools/fusion.py is a thin async httpx client posting to {FUSION_SERVICE_URL}/process (default http://fusion:8003/process in the docker-compose network — fusion mounts its router at root, not /api/fusion). Forwards optional bearer token. Raises on non-2xx and on transport failure: fusion is the primary detection path, so silent failure would lose alerts. The module docstring contrasts this with app.tools.graph, which intentionally degrades gracefully (graph data is best-effort context).
  • DetectAgent.process(raw_alert, api_token=None) now delegates to the client with no transformation. Class docstring updated. Same FusionEngine instance services both Kafka and HTTP paths, so dedup window / correlator buffer / entity-risk ledger stay consistent regardless of how alerts arrive.

Why no /api/fusion/process?

The fusion service mounts its APIRouter at the root path. Initial wiring used /api/fusion/process, which 404'd. The client tests pin the URL to /process as a regression guard.

Why raise on failure?

DetectAgent.process is the detection plane. Returning {\"error\": ...} would let downstream consumers treat a synthetic envelope as a real verdict and quietly drop the alert. The graph tool intentionally degrades gracefully because partial graph context is still useful in an in-flight investigation; fusion does not have that property.

Test plan

All 16 new tests pass offline (no network, no docker):

  • `services/fusion/tests/test_process_endpoint.py` (7 tests)
    • happy path: new-incident envelope
    • duplicate path: `FusionDecision.DUPLICATE`
    • `503` when `_worker_ref is None`
    • `503` when `_worker_ref.engine is None`
    • `422` on malformed payload
    • `422` on invalid severity
    • endpoint reuses `_worker_ref.engine` (no fresh `FusionEngine()` per request)
  • `services/agents/tests/test_fusion_client.py` (9 tests)
    • posts to `/process` at root, not `/api/fusion/process`
    • forwards `Authorization: Bearer ` when token provided
    • omits Authorization header when no token
    • `httpx.HTTPStatusError` propagates on 503
    • `httpx.HTTPStatusError` propagates on 422
    • `httpx.HTTPError` propagates on transport failure
    • `DetectAgent.process` delegates faithfully (args pass through)
    • `DetectAgent.process` forwards `api_token`
    • `DetectAgent.process` propagates fusion errors (no swallowing)
  • Existing fusion + agents test suites still green (no behaviour change in shared code).
  • Linter clean on all modified files.

Risk / blast radius

Purely additive:

  • new endpoint only fires when something explicitly posts to it
  • new client module only imports when something calls `DetectAgent.process`
  • no existing caller of either service changes shape

No env gate / feature flag intentionally: the wiring closes a documented TODO and is required for the four-agent façade contract.

Made with Cursor

Closes #190.

Adds the missing synchronous entrypoint to the fusion pipeline so the
four-agent facade (Detect/Investigate/Decide/Respond) actually delivers
on Detect's promise. Three additive pieces:

* fusion service exposes POST /process: validates a RawAlert, runs it
  through the live FusionEngine owned by FusionWorker, returns the
  FusedAlert envelope. Returns 503 if the worker is not yet bootstrapped
  (the engine attribute is None) — fail loud rather than invent a verdict.
  Schema errors surface as the usual FastAPI 422.
* services/agents/app/tools/fusion.py: thin async httpx client that
  POSTs to {FUSION_SERVICE_URL}/process (default
  http://fusion:8003/process in the docker-compose network — fusion
  mounts its router at the root, not /api/fusion). Forwards optional
  bearer token. RAISES on non-2xx and on transport failure: per the
  module docstring, fusion is the primary detection path, so silent
  failure would lose alerts. Contrast with app.tools.graph which
  intentionally degrades gracefully (graph data is best-effort context).
* DetectAgent.process(raw_alert, api_token=None) now delegates to the
  client with no transformation. Class docstring updated. Same
  FusionEngine instance services both Kafka and HTTP paths so the
  fused-state machine (dedup window, correlator buffer, entity risk
  ledger) stays consistent regardless of how alerts arrive.

Tests (16 total, all passing offline via respx for client, ASGITransport
+ in-memory fakes for endpoint):
* services/fusion/tests/test_process_endpoint.py covers new-incident and
  duplicate paths, both 503 modes (no worker, no engine), 422 on
  malformed and on invalid severity, and pins that the endpoint reuses
  the worker's engine instance instead of constructing a fresh one per
  request.
* services/agents/tests/test_fusion_client.py pins the URL to /process
  (regression guard against the /api/fusion/process mismatch caught
  during wiring), asserts bearer-token pass-through and absence of a
  bare Bearer header when no token is provided, confirms
  HTTPStatusError propagates on 503/422 and HTTPError on transport
  failure, and locks DetectAgent.process as a faithful delegate
  (args pass through, errors propagate, no swallowed exceptions).

No feature flag, no env gate. Purely additive: no existing caller of
either service changes shape, and the new endpoint/method only fire when
something explicitly invokes them.

Co-authored-by: Cursor <cursoragent@cursor.com>
# patched env.
import importlib

import app.tools.fusion as fusion_module
module attribute directly is intentional — it's the same mutation
the lifespan handler would do in reverse on shutdown.
"""
import app.api.router as router_mod
beenuar and others added 4 commits May 19, 2026 20:12
Squashes two over-eager line-breaks into the canonical one-line form
ruff format wants. No behaviour change; this just unblocks the
Python — Lint & Type-check CI gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ent into agents step (#190)

The Issue #190 PR added two new test files that the existing CI jobs
weren't shaped to run:

* ``services/fusion/tests/test_process_endpoint.py`` drives the new
  ``POST /process`` endpoint with FastAPI's ``ASGITransport`` plus
  ``httpx.AsyncClient``. The ``python-services-test`` job runs the
  fusion suite but installs only ``pydantic / pydantic-settings /
  pytest / pytest-asyncio / structlog / redis / aioredis`` — there was
  no ``fastapi`` on the PATH, so pytest blew up at collection time
  with ``ModuleNotFoundError: No module named 'fastapi'``. Fixed by
  adding ``fastapi`` and ``httpx`` to that job's minimal dep list and
  updating the comment to explicitly call out thin router-contract
  tests as in-scope (alongside the pure helpers it always covered).
  Postgres / Kafka / Redis stay out of scope — the new tests use
  ``set_worker(...)`` to inject a fake worker so no service boot is
  required.

* ``services/agents/tests/test_fusion_client.py`` exercises the thin
  httpx client that wires ``DetectAgent.process`` to the fusion
  service's ``POST /process`` endpoint, with respx mocking the network.
  The ``python-test`` job enumerates which ``services/agents/tests/*.py``
  files run, and this new file was missing from the list — so it was
  silently un-gated. Added it, and added ``respx`` to the pip install
  for that step. ``httpx`` is already pulled in via ``API_DEPS``.

Both jobs stay fast (no service boot, no infra), and both new tests
are now real CI signal instead of dead code.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two CI jobs were failing on PR #198 with ModuleNotFoundError after the
fastapi+httpx fix landed:

1. ``python-services-test`` — ``test_process_endpoint.py`` imports
   ``app.api.router``, which type-references ``FusionWorker`` from
   ``app.workers.consumer``. That module imports ``aiokafka`` at
   module scope, so the router can't be imported without it. Added
   ``aiokafka`` to the minimal pip install for this job.

2. ``python-test`` (agents) — ``test_fusion_client.py`` imports
   ``DetectAgent`` from ``app.agents``. The package's __init__.py
   eagerly loads sibling sub-agents (auto_triage, phishing,
   identity, insider_threat, cloud), all of which import
   ``langchain_core`` and ``langchain_openai`` at module scope.
   Added both packages to the agents-test pip install.

Both jobs keep their "minimal deps only" posture — no Postgres,
Kafka, Redis, or full service boot. We're just adding the
transitive import-time deps that the new Issue #190 tests pull in.

Verified locally:
  services/fusion: 7 passed
  services/agents: 9 passed (test_fusion_client.py)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ue-190-detect-agent-fusion-wiring

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	CHANGELOG.md
@beenuar beenuar merged commit fddbf2b into main May 20, 2026
22 of 23 checks passed
@beenuar beenuar deleted the feat/issue-190-detect-agent-fusion-wiring branch May 20, 2026 05:04
@beenuar beenuar mentioned this pull request May 30, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(agents): wire DetectAgent.process to FusionEngine via cross-service HTTP client

2 participants