Spaces:
Sleeping
Sleeping
| """Advisory function-call tools β PATH 1 of the agent harness. | |
| This module holds the five advisory ``function_call`` actions (CVE lookup, risk | |
| scoring, and drafting), exposed to the model as native Pydantic-AI tools | |
| (``@agent.tool``). It is split out of :mod:`agents.soc_agent` so that file stays | |
| focused on the loop, the brain, and the builders. | |
| Each tool is a thin, typed wrapper around :func:`_run_action`, which fixes the | |
| backend + action name and routes through the harness's shared governance helper | |
| (:func:`agents.soc_agent.govern_action`) β the *same* checkpoint the structured- | |
| draft loop uses, so an advisory tool call is governed exactly like any other | |
| action. Typed tool parameters give the model a precise argument schema, which is | |
| what makes native tool-calling reliable here. | |
| Note the import direction: this module depends on :mod:`agents.soc_agent` for the | |
| shared governance primitives, and ``soc_agent.build_agent`` imports | |
| :func:`register_function_tools` lazily (inside the function) to register them. That | |
| keeps the dependency one-way at import time, so there is no circular import. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| from pydantic_ai import Agent, RunContext | |
| from agents.soc_agent import GovernanceContext, _action_signature, govern_action | |
| from control_plane.schema import Backend, ProposedAction | |
| #: The advisory action names exposed as native tools. Kept as data so a test (and | |
| #: a reader) can see the exact tool surface at a glance. | |
| FUNCTION_CALL_TOOLS: tuple[str, ...] = ( | |
| "cve_lookup", | |
| "risk_score", | |
| "draft_remediation", | |
| "draft_incident", | |
| "recommend_containment", | |
| ) | |
| def _run_action( | |
| deps: GovernanceContext, action_name: str, arguments: dict[str, Any] | |
| ) -> dict[str, Any]: | |
| """Govern one advisory action and return the outcome to the model as a dict. | |
| The five tools below are thin typed fronts for this function. Each fixes | |
| ``backend = FUNCTION_CALL`` and its own ``action_name`` (the model can't forge | |
| them); the model supplies only the business arguments. Returns a small JSON | |
| object the model reads as feedback β never an exception. | |
| """ | |
| arguments = {k: v for k, v in arguments.items() if v is not None} | |
| action = ProposedAction( | |
| incident_id=deps.incident_id, | |
| agent_id=deps.agent_id, | |
| # The model no longer chooses a tier; we record the operator ceiling the | |
| # gate evaluates against. The verdict stands on that ceiling alone. | |
| autonomy_tier=deps.operator_tier, | |
| backend=Backend.FUNCTION_CALL, | |
| action_name=action_name, | |
| arguments=arguments, | |
| reason=f"Agent requested {action_name} for incident {deps.incident_id}", | |
| ) | |
| # Repeat nudge: a live model can loop on the same advisory call β re-running a | |
| # successful one, OR (just as wasteful) re-trying one that was already denied β | |
| # burning its step budget without making progress. If this exact action+args was | |
| # already attempted in this run, short-circuit with a firm "don't repeat" note | |
| # instead of governing a duplicate, steering the model to move on or conclude. | |
| signature = _action_signature(action) | |
| for prior in deps.turns: | |
| if _action_signature(prior.action) != signature: | |
| continue | |
| if prior.executed: | |
| # Already succeeded β hand back the same result, tell it not to repeat. | |
| return { | |
| "governance": "ALLOWED", | |
| "note": ( | |
| f"You already ran {action_name} with these arguments earlier in " | |
| "this run; the result is unchanged. Do NOT call it again β use " | |
| "the earlier result, take a different step, or give your FINAL " | |
| "ANSWER." | |
| ), | |
| "result": prior.result, | |
| } | |
| # Already attempted and NOT allowed β repeating it will be denied again. | |
| return { | |
| "governance": prior.decision.value, | |
| "note": ( | |
| f"You already tried {action_name} with these arguments and it was " | |
| f"not allowed ({prior.decision.value}). Do NOT repeat it β take a " | |
| "different, allowed step or give your FINAL ANSWER." | |
| ), | |
| "reason": prior.reason, | |
| } | |
| turn = govern_action(deps, action) | |
| if turn.executed: | |
| return {"governance": "ALLOWED", "result": turn.result} | |
| if turn.execution_error is not None: | |
| return {"governance": "ALLOWED", "executed": False, "error": turn.execution_error} | |
| return {"governance": turn.decision.value, "reason": turn.reason} | |
| def register_function_tools(agent: Agent[GovernanceContext, Any]) -> None: | |
| """Register the five advisory `function_call` tools on *agent* (once, at build). | |
| Each tool is a thin, typed wrapper: the model sees a clear signature (e.g. | |
| ``cve_lookup(cve_id: str)``) and fills the arguments, while the body fixes the | |
| backend + action name and hands off to the shared governance helper. The typed | |
| parameters are deliberate β they give the model a precise argument schema, | |
| which is what makes native tool-calling reliable here. | |
| """ | |
| def cve_lookup(ctx: RunContext[GovernanceContext], cve_id: str) -> dict[str, Any]: | |
| """Look up a CVE record (advisory, read-only).""" | |
| return _run_action(ctx.deps, "cve_lookup", {"cve_id": cve_id}) | |
| def risk_score( | |
| ctx: RunContext[GovernanceContext], cve_id: str, asset_id: str | None = None | |
| ) -> dict[str, Any]: | |
| """Score a CVE's risk, optionally weighted by an asset's criticality (advisory).""" | |
| return _run_action(ctx.deps, "risk_score", {"cve_id": cve_id, "asset_id": asset_id}) | |
| def draft_remediation(ctx: RunContext[GovernanceContext], cve_id: str) -> dict[str, Any]: | |
| """Draft remediation guidance for a CVE β text only, nothing is applied.""" | |
| return _run_action(ctx.deps, "draft_remediation", {"cve_id": cve_id}) | |
| def draft_incident(ctx: RunContext[GovernanceContext]) -> dict[str, Any]: | |
| """Draft an incident summary from this incident's alerts β text only.""" | |
| return _run_action(ctx.deps, "draft_incident", {"incident_id": ctx.deps.incident_id}) | |
| def recommend_containment( | |
| ctx: RunContext[GovernanceContext], asset_id: str, container_id: str | None = None | |
| ) -> dict[str, Any]: | |
| """Recommend containment steps for an asset β advisory only, nothing is done.""" | |
| return _run_action( | |
| ctx.deps, "recommend_containment", {"asset_id": asset_id, "container_id": container_id} | |
| ) | |