Backend phase 1#1
Merged
Merged
Conversation
Add build config, container, and package skeleton for the AgentHound backend following the spec module layout: pyproject.toml (fastapi, pydantic v2, networkx, pyyaml), Dockerfile (python:3.11-slim), and ignore rules for the venv and JSON data directory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the static catalogs the pipeline reads: capabilities.yaml (neutral verbs, spec section 3), node_properties.yaml (per-type safe defaults, section 4.2), and rules.yaml (declarative FH-001/002/003/006 descriptors with severity, recommended controls, and OWASP/MITRE mappings). core/catalogs.py loads and caches them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pydantic v2 internal representation (spec section 4): a single nodes list with seven node types plus neutral-capability edges, the Finding model (section 7.1), and the error envelope (section 13). normalizer.py fills safe defaults and maps concrete tool names to capabilities (sections 4.2 and 6); validators.py enforces unique ids, resolvable edges, catalog capabilities, and mandatory finding fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yaml_loader.py loads YAML safely, rejects empty documents, and detects the source framework (spec section 5.2). The generic adapter builds the IR from a thin declarative format; CrewAI, Dify, and LangGraph adapters are Fase 3 stubs that raise a clear not-implemented error via the dispatcher. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
builder.py turns the IR into a networkx.MultiDiGraph. traversal.py provides the reachability and path queries from spec section 9 (untrusted inputs, sensitive data, external outputs), bounded by max_depth=8. serializer.py emits the frontend graph shape (section 12.2) with node risk scores/badges and per-edge risk levels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scoring.py implements the additive risk formula, [0,10] clamp, and severity bands (spec section 10). path_rules.py and node_rules.py implement the Fase 1 rules FH-001/002/003/006; registry.py assembles findings from catalog metadata; engine.py runs the rule set, sorts by score descending, and assigns stable finding ids. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
repository.py keeps analyses in memory and mirrors each to JSON on disk (no DB in Fase 1, spec section 2). logging_util.py emits structured events without prompts or sensitive data (section 14). pipeline.py wires the stages end-to-end: YAML to IR to graph to rules to findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FastAPI routes for POST /analyses and the GET graph/findings endpoints (spec section 12), with errors rendered as the section 13 envelope. main.py assembles the app, registers routers and exception handlers, and exposes /health and the auto-generated OpenAPI docs. controls/ is a placeholder for Fase 2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixtures for the HireSmart exfiltration case (FH-001) and the fetchWeb indirect injection case (FH-006), plus pytest suites covering scoring, the generic adapter, IR validators, graph traversal, the rules engine, and the API end-to-end. Verifies the Fase 1 acceptance criteria (spec section 16): 26 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add app/docs/ (README, api, ir_and_rules, glossary, decisions) covering how to run/build, the endpoint contract, the IR/rules/scoring model, plain-language terms, and recorded decisions plus deferred Fase 2/3 scope. Update the top-level README to reflect that Fase 1 is implemented. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements the AgentHound Backend “Fase 1 / MVP” as a new FastAPI service that ingests YAML architectures, normalizes into an IR + NetworkX graph, runs initial FH rules, and exposes read endpoints with a spec-style error envelope.
Changes:
- Add core pipeline: YAML loading + adapter dispatch (generic), IR normalization/validation, graph build + traversal, rule engine (FH-001/002/003/006) with scoring and finding registry.
- Add FastAPI app with
/health,POST /analyses, and graph/findings read endpoints; persist analysis results in-memory and mirrored to disk as JSON. - Add developer enablement: docs (API/IR/rules), Dockerfile, pyproject, and a pytest suite with fixtures.
Reviewed changes
Copilot reviewed 50 out of 59 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates repo status/layout to reflect Backend Fase 1 being implemented and points to backend docs. |
| Backend/pyproject.toml | Defines backend package metadata, dependencies, and pytest config. |
| Backend/Dockerfile | Adds a container build for running the FastAPI service. |
| Backend/.gitignore | Ignores venv/data/cache artifacts for the backend. |
| Backend/.dockerignore | Excludes prototype/tests and most markdown from Docker build context (keeps app/docs/README). |
| Backend/app/init.py | Introduces backend package version. |
| Backend/app/main.py | Creates FastAPI app, routers, exception handlers, and CORS middleware. |
| Backend/app/logging_util.py | Adds structured JSON event logging utilities. |
| Backend/app/docs/README.md | Adds run/test instructions and module map for the backend MVP. |
| Backend/app/docs/api.md | Documents the Fase 1 HTTP API contract and error envelope. |
| Backend/app/docs/ir_and_rules.md | Documents IR schema, catalogs, rules, and scoring model. |
| Backend/app/docs/glossary.md | Adds glossary for key backend concepts. |
| Backend/app/docs/decisions.md | Records architectural decisions and deferred scope for later phases. |
| Backend/app/api/errors.py | Implements spec-style error envelope and exception handlers. |
| Backend/app/api/routes/analyze.py | Implements POST /analyses endpoint. |
| Backend/app/api/routes/graphs.py | Implements GET /analyses/{id}/graph endpoint. |
| Backend/app/api/routes/findings.py | Implements GET /analyses/{id}/findings and GET /analyses/{id}/findings/{finding_id}. |
| Backend/app/core/catalogs.py | Adds cached YAML catalog loaders (capabilities, defaults, rules). |
| Backend/app/core/pipeline.py | Implements end-to-end analysis orchestration and persistence. |
| Backend/app/core/parser/yaml_loader.py | Adds YAML parsing + framework detection heuristic. |
| Backend/app/core/parser/adapters/init.py | Adds adapter dispatch and wraps unimplemented adapters into IRValidationError. |
| Backend/app/core/parser/adapters/generic_adapter.py | Implements generic YAML → IR adapter. |
| Backend/app/core/parser/adapters/crewai_adapter.py | Adds Fase 3 stub adapter. |
| Backend/app/core/parser/adapters/dify_adapter.py | Adds Fase 3 stub adapter. |
| Backend/app/core/parser/adapters/langgraph_adapter.py | Adds Fase 3 stub adapter. |
| Backend/app/core/ir/models.py | Adds Pydantic v2 models for IR, findings, summaries, and error envelope. |
| Backend/app/core/ir/normalizer.py | Applies safe defaults and defines tool→capability mapping helpers. |
| Backend/app/core/ir/validators.py | Adds IR + finding validation routines raising IRValidationError. |
| Backend/app/core/graph/builder.py | Builds a NetworkX MultiDiGraph from the IR. |
| Backend/app/core/graph/traversal.py | Adds graph traversal/selectors and path utilities for rules. |
| Backend/app/core/graph/serializer.py | Serializes IR+findings into the frontend graph shape. |
| Backend/app/core/rules/scoring.py | Implements additive risk scoring and severity banding. |
| Backend/app/core/rules/registry.py | Loads rule metadata and assembles Finding objects. |
| Backend/app/core/rules/path_rules.py | Implements FH path-based rules (FH-001/003/006). |
| Backend/app/core/rules/node_rules.py | Implements FH node/reachability rule (FH-002). |
| Backend/app/core/rules/engine.py | Orchestrates rule execution, sorts findings, assigns stable ids. |
| Backend/app/core/storage/repository.py | Adds in-memory + JSON-on-disk persistence repository. |
| Backend/app/core/controls/.gitkeep | Placeholder for future Fase 2 controls modules. |
| Backend/app/catalogs/capabilities.yaml | Adds capability verb catalog used for validation and rules. |
| Backend/app/catalogs/node_properties.yaml | Adds safe defaults and enumerations for node properties. |
| Backend/app/catalogs/rules.yaml | Adds Fase 1 rule descriptors (metadata/control mappings). |
| Backend/tests/conftest.py | Sets test data dir and provides fixture-loading helpers. |
| Backend/tests/test_api.py | Adds API-level tests for health/analyze/graph/findings and error envelopes. |
| Backend/tests/test_generic_adapter.py | Adds tests for generic adapter + normalizer defaults and error handling. |
| Backend/tests/test_graph_traversal.py | Adds tests for traversal selectors/paths/capabilities/reachability. |
| Backend/tests/test_ir_validators.py | Adds tests for IR validation errors (empty, dup ids, dangling edges, unknown cap). |
| Backend/tests/test_rules_engine.py | Adds tests for rule triggering, sorting, and “safe” architecture behavior. |
| Backend/tests/test_scoring.py | Adds scoring and severity-band tests. |
| Backend/tests/fixtures/hiresmart_generic.yaml | Adds representative exfiltration fixture. |
| Backend/tests/fixtures/hiresmart_fetchweb.yaml | Adds indirect-injection fixture for FH-006. |
| Backend/tests/init.py | Marks tests as a package. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add "info": 0.0 to the BASE table so a base_severity of "info" scores as 0.0 instead of silently falling back to the medium default (4.0). Addresses Copilot review feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
get_reachable_nodes now uses nx.descendants (one traversal) instead of an nx.has_path call per node, avoiding O(V) path checks on larger graphs. edge_ids_on_path collects every parallel edge for each hop rather than only the first key, so findings on a MultiDiGraph reference all traversed capability edges. Addresses Copilot review feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_max_sensitivity now skips unknown/malformed sensitivity values instead of raising ValueError from list.index on unexpected input. The FH-006 fetchWeb check drops the dead sensitivity == "untrusted" comparison (sensitivity is enumerated public..critical) and relies on the external boundary signal. Addresses Copilot review feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
analysis_id arrives from URL path params and was interpolated directly into a filesystem path, so a value with separators or ".." could escape the data directory and probe arbitrary .json files. Restrict ids to a safe character set ([A-Za-z0-9_-], which still admits UUIDs); get/exists return not-found for anything else and _path refuses to build an unsafe path. Add regression tests. Addresses Copilot review feedback (High). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TOOL_CAPABILITY_MAP emitted createResource, search and extractInformation (per spec section 6), but these were absent from capabilities.yaml, so any IR synthesized from the map would fail validation as UNKNOWN_CAPABILITY. Add the three verbs to the catalog so the mapping stays valid for Fase 3 adapters, rather than dropping information from the map. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wide-open CORS (allow_origins=["*"]) is convenient locally but risky if the service is exposed. Default to any localhost/127.0.0.1 origin (any port) via a regex so a local frontend still works, and allow pinning exact origins for deployment through AGENTHOUND_CORS_ORIGINS. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sort claimed score desc / severity / rule id "for stability" but reverse=True also sorted rule_id descending. Negate the numeric keys instead so rule_id stays ascending as the tie-breaker (FH-001 before FH-002 on a tie), matching the stated intent. Addresses Copilot review feedback (Low). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
If a caller passed a mapping for 'nodes'/'edges', enumerate iterated its keys and produced a confusing "must be a mapping" error per key. Validate that both fields are lists up front and fail with a clear message. Add a regression test. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous checks were near no-ops: the Finding model already guarantees severity/score by type, and path is never None. Require a non-empty rule_id and a non-empty path instead. Per spec section 13 only presence is mandated, so edges/evidence/recommended_controls are deliberately not required non-empty (future node-only findings may omit them). Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RULES_EXECUTED telemetry hardcoded rule_count=4, which would drift if the registered rule set changed. Export RULE_COUNT from core.rules.engine and log that instead. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The normalizer docstring and the docs implied edges get tool->capability mapping during analysis, but Fase 1 requires explicit edge capabilities and normalize() only fills node defaults. Correct both to describe actual behavior and note the mapping helper is for Fase 3 adapters. Addresses Copilot review feedback (Low/Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.dockerignore kept app/docs/README.md while excluding the docs it links to, so the image would carry a README with broken links. The docs are developer documentation not needed at runtime (the Dockerfile installs dependencies explicitly rather than building the package), so exclude all of them. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
edge_ids_on_path returns every parallel edge id per hop, so describe edges as the ids for the path hops including parallel MultiDiGraph edges, rather than a single edge per hop. Addresses Copilot review feedback (Low). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The validation section claimed "mandatory finding fields"; state what is actually enforced -- a non-empty rule_id and path, with severity/score guaranteed by the model. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bash block used the Windows activation path (.venv/Scripts/activate), which fails on Linux/macOS. Use the POSIX path and note the Windows PowerShell alternative. Addresses Copilot review feedback (Low). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The get() comment still said the corrupt case was "not a 500", but a later change mapped CORRUPT_ANALYSIS to HTTP 500 in the API layer. Update the comment to match the actual behavior. Addresses Copilot review feedback (Low). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lock in the spec section 8 semantics with false-positive guards: FH-001 must not fire without a read edge into a sensitive asset or without a send into the output; FH-003 must not fire when the final hop into the output is not a send. Document that each rule arrow matches a capability on a specific hop, not anywhere on the path. This closes the negative-coverage gap that let the earlier "capability anywhere" bugs pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
get_finding deep-copied the whole IR only to filter nodes/edges for serialization. It reassigns the nodes/edges lists and never mutates the shared Node/Edge objects, so a shallow copy suffices and avoids copying the entire graph. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DEFAULT_MAX_DEPTH was defined in both pipeline.py and graph/traversal.py. Import it from traversal as the single source of truth to avoid drift. Addresses Copilot review feedback (Low). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
get() caught FileNotFoundError under the broad OSError clause, so a file deleted between the exists() check and the read was reported as CORRUPT_ANALYSIS (500) instead of missing (404); catch it first and return None. save() now catches OSError (disk full, permissions), cleans up the temp file, and raises a structured STORAGE_ERROR so the failure keeps the error-envelope contract. Add regression tests. Addresses Copilot review feedback (Medium x2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add STORAGE_ERROR to the server-side error codes so a persistence failure is rendered as a 500 with the error envelope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
log_event serialized metadata with json.dumps, which raises TypeError on a non-serializable value (Path, enum, datetime) and, since logging runs inline in the request pipeline, would turn into a 500. Pass default=str so logging cannot fail the request. Add a test. Addresses Copilot review feedback (Medium). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
edges/controls used "data.get(...) or []", which coerced falsy non-list values
(e.g. {}) to [], bypassing the list-type check and silently treating malformed
input as empty. Default only a missing key to []; a present non-list value is
rejected. Add a test.
Addresses Copilot review feedback (Medium x2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines
+91
to
+96
| raw_nodes = data.get("nodes") | ||
| if not raw_nodes: | ||
| raise IRValidationError("EMPTY_IR", "Generic input declares no nodes.") | ||
| if not isinstance(raw_nodes, list): | ||
| raise IRValidationError("INVALID_INPUT", "'nodes' must be a list.", {"got": type(raw_nodes).__name__}) | ||
|
|
Comment on lines
+46
to
+55
| score = compute_score( | ||
| ScoreInputs( | ||
| base_severity="critical", | ||
| data_sensitivity=_max_sensitivity(cg, path), | ||
| external_reach="external", | ||
| controls_absent=["approval_missing"], | ||
| reversible=True, | ||
| agent_count=len(agents), | ||
| ) | ||
| ) |
Comment on lines
+97
to
+105
| score = compute_score( | ||
| ScoreInputs( | ||
| base_severity="high", | ||
| data_sensitivity=_max_sensitivity(cg, path), | ||
| external_reach="external", | ||
| controls_absent=["approval_missing"], | ||
| agent_count=len(agents), | ||
| ) | ||
| ) |
Comment on lines
+157
to
+165
| score = compute_score( | ||
| ScoreInputs( | ||
| base_severity="high", | ||
| data_sensitivity=_max_sensitivity(cg, path), | ||
| external_reach="external", | ||
| controls_absent=["validation_missing", "approval_missing"], | ||
| agent_count=len(agents), | ||
| ) | ||
| ) |
Comment on lines
+26
to
+35
| agents = agents_on_path(cg, path) | ||
| score = compute_score( | ||
| ScoreInputs( | ||
| base_severity="high", | ||
| data_sensitivity=max_sensitivity(cg, path), | ||
| external_reach="internal", | ||
| controls_absent=["allowlist_missing"], | ||
| agent_count=len(agents), | ||
| ) | ||
| ) |
selenaschz
approved these changes
Jul 8, 2026
jcobosmeta
added a commit
that referenced
this pull request
Jul 13, 2026
- Unify naming: rename backend ReanalyzeRequest/Node/Edge -> Analyze* to match the /analyze route and the frontend Analyze* types, and fix the docstring that still referenced the old /reanalyze path (Copilot + review #1). - analyze route: expand the empty-body comment to state that an all-empty edits payload is intentionally treated as "analyse the stored graph" and cannot arise from the UI (agents are not removable), so it only guards an accidental empty body (review #2). - adapters.toAnalyzeRequest: correct the comment on the deliberate node/edge properties asymmetry (edges always send {} since the UI GraphEdge carries no properties; existing edge properties are preserved server-side by id) (#4). - AgentEditModal: only propagate onSave (which marks the graph dirty and triggers a re-analysis) when the draft actually changed, so opening and saving an agent without edits is a no-op (review #5). Not changed: toAnalyzeRequest still falls back to capability "" when both capability and label are nullish (#3) -- in practice every edge carries a capability, and an empty one is rejected by validate_ir (UNKNOWN_CAPABILITY, 400), so no malformed edge is ever stored. 124 backend tests pass; frontend tsc + eslint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jcobosmeta
added a commit
that referenced
this pull request
Jul 13, 2026
…ssing copy Copilot review follow-ups: #3 analyze_graph now keeps an existing edge's source/target from the stored IR (only capability is editable for a known edge id), matching the documented invariant. Resubmitting a known edge id with different endpoints can no longer silently rewire the graph. New test covers it. #2 Processing step copy reflects the parse stage (rules run later): en/es title, subtitle and doneTitle no longer claim rule-engine execution, and the reasoning animation lines drop the risk-path/rule/scoring steps in favour of parsing + capability extraction. Not changed: #1 (edges send properties: {}) — existing edges keep stored properties by id server-side and the UI GraphEdge carries none, so nothing is lost; documented as a deliberate asymmetry. 125 backend tests pass; frontend tsc clean; i18n JSON valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.