Skip to content

fix(research): fix AttributeError in suggest_objectives_from_chains#176

Merged
PurpleCHOIms merged 1 commit into
PurpleAILAB:mainfrom
Lempkey:fix/suggest-objectives-chain-attribute-access
May 9, 2026
Merged

fix(research): fix AttributeError in suggest_objectives_from_chains#176
PurpleCHOIms merged 1 commit into
PurpleAILAB:mainfrom
Lempkey:fix/suggest-objectives-chain-attribute-access

Conversation

@Lempkey
Copy link
Copy Markdown
Contributor

@Lempkey Lempkey commented May 8, 2026

Summary

Closes #165

suggest_objectives_from_chains crashes with AttributeError any time plan_chains() returns chains with steps, because the function accessed members that don't exist on the ChainStep and Chain dataclasses:

Broken access Actual field
step.node.props.get("mitre") not on ChainStep — no .node attribute
step.node.props.get("severity") not on ChainStep
step.node.kind step.node_kind (flat str)
chain.crown_jewel.label chain.crown_jewel_label (flat str)
chain.entrypoint.label chain.entrypoint_label (flat str)

ChainStep stores flat string fields (node_id, node_label, node_kind, edge_kind, hop_cost). Chain stores crown_jewel_label and entrypoint_label as flat strings — not objects with a .label attribute.

Changes

  • Replace all five broken attribute accesses with the correct flat-string fields
  • Remove the mitre/severity enrichment loop — neither field is carried by ChainStep; mitre defaults to [] and opsec to "standard" (these can be enriched in a follow-up if Chain is extended to carry full Node.props)

Test plan

  • uv run ruff check decepticon/tools/research/tools.py — passes
  • uv run basedpyright decepticon/tools/research/tools.py — 0 errors
  • uv run pytest tests/unit/research/test_tools.py — 10/10 passed

ChainStep stores flat string fields (node_kind, node_label, etc.) with
no .node object and no .props dict. Chain similarly stores flat strings
crown_jewel_label and entrypoint_label rather than objects with a .label
attribute.

The function was crashing with AttributeError any time plan_chains()
returned at least one chain with steps.

- Replace step.node.kind → step.node_kind
- Replace step.node.props.get(...) calls — mitre/severity are not
  carried by ChainStep; remove that enrichment and default mitre to []
  and opsec to "standard"
- Replace chain.crown_jewel.label → chain.crown_jewel_label
- Replace chain.entrypoint.label → chain.entrypoint_label

Fixes PurpleAILAB#165
@Lempkey Lempkey requested a review from PurpleCHOIms as a code owner May 8, 2026 19:03
@PurpleCHOIms PurpleCHOIms merged commit fc4842d into PurpleAILAB:main May 9, 2026
PurpleCHOIms added a commit that referenced this pull request May 12, 2026
Merge upstream main (48 commits behind) into the long-running benchmark
branch so the OCI loop runs against the current Decepticon code, not a
stale fork. Resolves a single conflict in
``decepticon/middleware/opplan.py`` introduced by upstream PR #184
(``Refactor middleware tools and harden OPPLAN persistence``), which
moved the OPPLAN ``@tool`` definitions out of the middleware module and
into the new ``decepticon/tools/opplan.py``.

Resolution
- Drop the duplicate ``@tool`` definitions on the benchmark side
  (~970 lines) — they now live behind ``build_opplan_tools(backend)``
  in ``decepticon/tools/opplan.py``.
- Preserve the benchmark-only recon-first guard in
  ``OPPLANMiddleware.after_model`` (added by 13ff3b3 / be918cf /
  08a98eb / d211c4e) — intercepts ``task('exploit'|'postexploit', ...)``
  dispatches when neither an OPPLAN recon objective nor on-disk
  evidence (``recon/SUMMARY.md`` or ``findings/FIND-*.md``) is present.
- Re-add the ``from pathlib import Path`` import that the auto-merge
  dropped (now needed only by the surviving guard, since the file-
  writing tools moved out).

Verification
- ``uv run ruff check decepticon/middleware/opplan.py`` — clean
- ``uv run ruff format --check decepticon/middleware/opplan.py`` — clean
- ``uv run pytest tests/unit/middleware/test_opplan_hierarchy.py
  tests/unit/middleware/test_opplan_persistence.py`` — 34 passed
- File shrinks from 1451 → 483 lines, diff vs origin/main is exactly
  the import line and the recon-first guard block.

Other upstream changes (LiteLLM OAuth refactor #187, workspace_path
reducer #183, launcher slug #182, research fix #176, AD index fix #177,
LLM kwargs typing #179) merged automatically without conflict.
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.

Bug: suggest_objectives_from_chains crashes with AttributeError — ChainStep has no .node attribute

2 participants