Documentation
¶
Overview ¶
Package agenttools is the agent's tool catalog: the bounded set of capabilities the LLM is allowed to invoke. It embodies two non-negotiable constraints in its very shape:
Anti-self-escalation. The catalog exposes ONLY read tools – scoped to the agent's own engagement – plus a single propose-only execute tool. There is NO tool to mutate scope, the authorization window, RoE, live-recon, credentials, or the approval mode; those are operator-only. The catalog literally holds no writer for any of them, so a hostile or confused model cannot widen its own authority by calling a tool. Data minimization. Scope data, tokens, and AUP records are NEVER returned to the LLM. The agent does not need the scope inventory to be useful – the safety gate enforces scope server-side regardless of what the model proposes – so it is never disclosed to the (third-party, untrusted) LLM provider. Read tools are locked to the session's engagement id; an engagement id is not even accepted as a tool argument.
Read tools return DATA (fed back to the model as a tool message). The execute tool (start_recon) returns a *ProposedAction ENVELOPE and runs nothing – the orchestrator MUST pass it through safety.Gate before any execution. Every dispatch is recorded in the append-only audit log as the agent (actor = "agent:<sessionID>").
Index ¶
- Constants
- type AgentToolset
- type Catalog
- func (c *Catalog) Dispatch(ctx context.Context, sess agent.Session, call agent.ToolCall) (Result, error)
- func (c *Catalog) EnableAgentToolset(t AgentToolset) error
- func (c *Catalog) EnableFindingProposals(p findingProposer)
- func (c *Catalog) EnableHypotheses(p hypothesisProposer)
- func (c *Catalog) EnableJudgments(p judgmentProposer)
- func (c *Catalog) EnablePlanning()
- func (c *Catalog) EnableReachability(r scanResultReader)
- func (c *Catalog) EnableWriteupDrafts(p writeupdraftProposer)
- func (c *Catalog) ProposeForNode(sess agent.Session, node agent.PlanNode) (agent.ProposedAction, error)
- func (c *Catalog) Tools() []agent.ToolSchema
- type Result
Constants ¶
const ( ToolListFindings = "list_findings" ToolGetFindingDetail = "get_finding_detail" ToolListSASTValidation = "list_sast_validation" ToolPlanRuntimeVerification = "plan_runtime_verification" ToolListEvidence = "list_evidence" ToolVerifyCustody = "verify_custody" ToolListReconTools = "list_recon_tools" ToolStartRecon = "start_recon" ToolProposePlan = "propose_plan" // propose a multi-step recon plan (DAG); runs nothing ToolProposeFinding = "propose_finding" // record an UNPROVEN exploitation claim at score 0 ToolReachabilityContext = "reachability_context" // read dep-graph reachability facts (T0/T1) ToolProposeReachability = "propose_reachability" // propose a reachability Judgment (score 0; verify is human-only) ToolProposeSASTValidation = "propose_sast_validation" // propose a gated CapSAST judgment for verifier review; score 0, no execution ToolProposeCritique = "propose_critique" // propose an adversarial critique Judgment against a finding (score 0) ToolEvidenceSufficiency = "evidence_sufficiency" // read-only advisory – what's missing for a finding to reach the bar ToolProposeRiskNarrative = "propose_risk_narrative" // propose a risk-narrative Judgment (ungated; a human accepts) ToolProposeThreat = "propose_threat" // propose a STRIDE threat Judgment over the architecture model (score 0; a human ratifies) ToolProposeWriteupDraft = "propose_writeup_draft" // propose a finding write-up DRAFT (prose) awaiting human sign-off (NOT a judgment) ToolProposeAttackChain = "propose_attack_chain" // propose an attack-chain HYPOTHESIS finding (score 0; gated until a human verifies) ToolProposeVexJustification = "propose_vex_justification" // propose an OpenVEX not_affected justification Judgment (score 0; a human ratifies) )
Catalog tool names. These are the EXACT set advertised to the LLM; the anti-self-escalation test pins that no scope/credential/mode-mutating tool is ever added here.
const MaxPlanNodes = agent.MaxPlanNodes
MaxPlanNodes bounds how many nodes a single propose_plan may carry (mirrors the domain cap); a larger proposal is rejected, never truncated.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AgentToolset ¶
type AgentToolset struct {
Findings findingProposer // required: propose_finding
Hypotheses hypothesisProposer // required: propose_attack_chain
Reachability scanResultReader // required: reachability_context (read-only)
Judgments judgmentProposer // optional (nil ⇒ off): propose_reachability/sast_validation/critique/risk_narrative/threat/vex_justification
WriteupDrafts writeupdraftProposer // optional (nil ⇒ off): propose_writeup_draft
}
AgentToolset bundles the dependencies that switch on the agent's engagement-scoped tools beyond the always-available read tools. Both composition roots — the inline agent in synapse-api and the durable agent in synapse-worker — enable their orchestrator catalog through THIS one struct, so a given agent run advertises an IDENTICAL tool set no matter WHERE it executes. Before this, the worker enabled only planning + finding proposals while the API enabled the full set, so the same session saw a smaller toolset when driven durably — a correctness bug (issue #161).
Findings, Hypotheses and Reachability are REQUIRED: a durable run must never silently advertise fewer PROPOSAL tools than the inline run. Judgments and WriteupDrafts are OPTIONAL and mirror their feature flags — a nil field leaves that tool off in BOTH binaries (they are gated by SYNAPSE_JUDGMENTS_ENABLED / SYNAPSE_WRITEUP_DRAFTS_ENABLED). To keep a tool off, the composition root leaves the field nil; it must NOT assign a typed-nil service pointer (a non-nil interface wrapping a nil pointer), because that would wire a tool backed by nothing. Each field is a propose/read-only slice — the agent can never confirm.
type Catalog ¶
type Catalog struct {
// contains filtered or unexported fields
}
Catalog dispatches the agent's tool calls: read tools against the engagement's own data, and start_recon into a gated proposal. It never executes a tool or mutates anything.
func New ¶
func New(findings findingReader, evidences evidenceReader, reconTools []ports.ReconTool, audit ports.AuditLogger, clock ports.Clock, ids ports.IDGenerator) (*Catalog, error)
New validates its dependencies. reconTools is the SAME set wired into the recon use-case (passed by the composition root); duplicates are rejected.
func (*Catalog) Dispatch ¶
func (c *Catalog) Dispatch(ctx context.Context, sess agent.Session, call agent.ToolCall) (Result, error)
Dispatch routes a single LLM tool call. The engagement is ALWAYS the session's; an engagement id is never read from the call arguments (anti cross-engagement self-escalation).
func (*Catalog) EnableAgentToolset ¶
func (c *Catalog) EnableAgentToolset(t AgentToolset) error
EnableAgentToolset turns on planning + the proposal/read tools from a single dependency set, so the inline (API) and durable (worker) catalogs are wired identically — the durable/inline parity guarantee (issue #161). It FAILS CLOSED: a missing REQUIRED dependency returns an error rather than advertising a partial toolset (a durable agent with fewer tools than the inline one is a correctness bug, not a valid degraded mode). Planning is always on — both composition roots pair it with an orchestrator PlanStore.
It calls the same narrow Enable* setters a composition root would, so the propose-only, never-confirm invariant is unchanged: the agent still only proposes (score 0); a distinct human/verifier confirms out of band. Optional deps are wired only when non-nil, matching each feature flag.
func (*Catalog) EnableFindingProposals ¶
func (c *Catalog) EnableFindingProposals(p findingProposer)
EnableFindingProposals turns on the propose_finding tool. The agent can then RECORD an unproven exploitation claim (score 0); it still cannot raise its own score or confirm – that needs a distinct verifier out of band. The composition root supplies the exploitation service.
func (*Catalog) EnableHypotheses ¶
func (c *Catalog) EnableHypotheses(p hypothesisProposer)
EnableHypotheses turns on the propose_attack_chain tool. The agent can then record an attack-chain HYPOTHESIS (a Kind=hypothesis finding at score 0, linking constituent findings); it still cannot raise the score or confirm – a distinct human verifies it out of band (the same gate as an exploitation finding).
func (*Catalog) EnableJudgments ¶
func (c *Catalog) EnableJudgments(p judgmentProposer)
EnableJudgments turns on the propose_reachability + propose_sast_validation + propose_critique + propose_risk_narrative + propose_threat tools: the agent can RECORD a judgment (score 0); it still cannot raise the score or confirm – a distinct human reviewer verifies it via PermReview, out of band. The composition root supplies the analysis service.
func (*Catalog) EnablePlanning ¶
func (c *Catalog) EnablePlanning()
EnablePlanning turns on the propose_plan tool. The composition root calls this for the orchestrator's catalog when (and only when) it also wires a PlanStore + SetPlanStore, so the catalog never advertises a capability the orchestrator cannot drive. Left off for the MCP/read-only catalog, which has no planner.
func (*Catalog) EnableReachability ¶
func (c *Catalog) EnableReachability(r scanResultReader)
EnableReachability turns on the reachability_context read tool: the agent can read the engagement's dependency-graph FACTS for a vulnerable package (dep path, direct flag, scope, manifest location) to reason about T0/T1 reachability. Read-only – it forms NO verdict (the agent proposes a ReachabilityClaim out of band). The composition root supplies the scan-result store.
func (*Catalog) EnableWriteupDrafts ¶
func (c *Catalog) EnableWriteupDrafts(p writeupdraftProposer)
EnableWriteupDrafts turns on the propose_writeup_draft tool: the agent can DRAFT a finding's description + remediation PROSE as a proposal (awaiting human sign-off). It still cannot edit, accept, or reject a draft – those are human actions behind PermReview + SoD (the proposer cannot sign off its own draft). The composition root supplies the writeupdraft service (which satisfies the narrow proposer).
func (*Catalog) ProposeForNode ¶
func (c *Catalog) ProposeForNode(sess agent.Session, node agent.PlanNode) (agent.ProposedAction, error)
ProposeForNode rebuilds the approval-required ProposedAction for a plan node at execution time, reusing the node's STABLE ActionID so the evidence-chain idempotency holds across redeliveries. It runs + audits nothing (the plan was audited at proposal); the orchestrator admits the result through safety.Gate. Same argv/target guards as start_recon (one source).
func (*Catalog) Tools ¶
func (c *Catalog) Tools() []agent.ToolSchema
Tools returns the JSON-schema tool definitions advertised to the LLM. The read tools take no arguments – they operate on the session's engagement, which is never an LLM-supplied id.
type Result ¶
type Result struct {
Data json.RawMessage
Proposal *agent.ProposedAction
// Plan is set by propose_plan: a validated, NOT-yet-persisted execution DAG (Go minted the
// node ids, classified risk, and validated acyclicity). It has executed nothing – the
// orchestrator persists it and drives each node through safety.Gate.Admit.
Plan *agent.Plan
}
Result is a tool dispatch outcome. Exactly one field is set: Data for a read tool (JSON to feed back to the model as a tool message), or Proposal for an execute tool (an approval-required envelope the orchestrator must run through safety.Gate). A non-nil Proposal has executed NOTHING.