Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions secator/ai/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,19 +647,21 @@ def _run_runner(action: Dict, ctx: ActionContext, runner_type: str) -> Generator


def _get_result_context(action, ctx):
"""Get result context from action.

Always stamps the ai task's ``session_id`` (the conversation id) onto the
derived context. The ai task's ``self.session_id`` may be derived (from
``session_name`` / the runner id) and is therefore not guaranteed to already
live in ``ctx.context``. Stamping it here means every sub-runner (task /
workflow / scan) dispatched by the ai task persists a runner doc whose
``context.session_id`` matches the conversation — so the runners spawned by a
conversation are queryable by that conversation's session_id.
"""Build the CHILD runner's context.

Stamps the conversation ``session_id`` (parenting link — see the runner-parenting
design) and marks the child ``has_parent``. Critically, it STRIPS the parent's
runner-identity keys (`task_id`/`workflow_id`/`scan_id`): a child that inherited
them would make `update_runner`/`runner_id` target the PARENT's doc instead of
minting its own. The child keeps drivers/workspace so it persists into the same
workspace, linked to the conversation by ``session_id``.
"""
new_ctx = ctx.context.copy()
for identity_key in ("task_id", "workflow_id", "scan_id", "task_chunk_id"):
new_ctx.pop(identity_key, None)
if ctx.session_id and not new_ctx.get("session_id"):
new_ctx["session_id"] = ctx.session_id
new_ctx["has_parent"] = True
action_context = {}
tool_call_id = action.get("tool_call_id")
tool_call_name = action.get("tool_call_name")
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_ai_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,5 +1280,36 @@ def test_normal_depth1_spawn_succeeds(self, mock_build_hooks, mock_task_cls, _mo
self.assertEqual(kwargs.get('context', {}).get('ai_subagent_depth'), 1)


@unittest.skipUnless(ADDONS_ENABLED['ai'], 'ai addon not installed')
class TestChildContextParenting(unittest.TestCase):
"""A spawned sub-runner must get a CLEAN identity: it inherits the conversation
session_id + drivers but NOT the parent's runner-doc id, so it mints its own doc
(linked to the conversation by session_id), instead of clobbering the parent."""

def test_child_context_strips_parent_identity_keeps_session(self):
from secator.ai.actions import _get_result_context, ActionContext
ctx = ActionContext(
targets=['t.com'], model='m',
context={
'workspace_id': 'ws1', 'workspace_name': 'w', 'drivers': ['mongodb'],
'task_id': 'PARENT_AI_ID', # the parent ai task's own doc id
'session_id': 'conv-1',
},
session_id='conv-1',
)
action = {'action': 'task', 'name': 'nmap', 'tool_call_id': 'tc1', 'tool_call_name': 'run_task'}
child = _get_result_context(action, ctx)
# keeps the conversation link + drivers/workspace
self.assertEqual(child['session_id'], 'conv-1')
self.assertEqual(child['drivers'], ['mongodb'])
self.assertEqual(child['workspace_id'], 'ws1')
# marks it a child
self.assertTrue(child.get('has_parent'))
# does NOT inherit the parent's runner-doc identity (would clobber / suppress its own doc)
self.assertNotIn('task_id', child)
self.assertNotIn('workflow_id', child)
self.assertNotIn('scan_id', child)


if __name__ == '__main__':
unittest.main()