Spaces:
Sleeping
Sleeping
File size: 6,707 Bytes
ebd50f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """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.
"""
@agent.tool
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})
@agent.tool
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})
@agent.tool
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})
@agent.tool
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})
@agent.tool
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}
)
|