An installable native Requirement for BeeAI Framework 0.1.83. It adds an explicit caller policy before native MCP tools/call and native HandoffTool → A2AAgent delegation. A bounded, unsigned observation from Agent Guild is evidence for that policy; it is never authorization or a verified identity.
This source artifact builds as beeai-agentguild==0.1.0. It has not been published to PyPI. Install from a reviewed source checkout or its built wheel:
python -m venv .venv
. .venv/bin/activate
python -m pip install .
# Or: python -m pip install dist/beeai_agentguild-0.1.0-py3-none-any.whlPython 3.11–3.13 is supported by the pinned framework. Native validation for this artifact uses Python 3.12.14. Dependencies are the released beeai-framework[mcp,a2a]==0.1.83 and httpx>=0.28.1,<0.29; no Agent Guild SDK or unpublished package is required. The framework's MCP/A2A extras bring their standard dependencies, including LiteLLM and the A2A gRPC dependencies. The integration itself does not call a model provider.
Use an existing caller-configured BeeAI ChatModel, a public server endpoint you selected, and an explicit tool-name set. This helper constructs actual BeeAI MCPTool instances from an initialized MCP session; it never guesses a private SDK URL property or exposes every discovered tool automatically.
from beeai_framework.agents.requirement import RequirementAgent
from beeai_agentguild import AgentGuildPreflightRequirement, PreflightPolicy, mcp_bindings
async def delegate(model, endpoint: str, approved_tool_names: set[str], task: str):
# The caller chooses policy before any request. Default blocks caution and all unknowns.
policy = PreflightPolicy()
async with mcp_bindings(endpoint, include=approved_tool_names) as bindings:
guard = AgentGuildPreflightRequirement(bindings, policy=policy, ttl_seconds=10)
agent = RequirementAgent(
llm=model,
tools=[binding.tool for binding in bindings],
requirements=[guard],
)
result = await agent.run(task)
return result, guard.evidenceMCP initialization and tools/list happen when entering the context, before the requirement runs. The requirement protects subsequent tools/call, not discovery. Keep the context open for the entire agent run. The helper adds no credentials, disables redirect following and environment proxy use, and does not expand the caller's selected names. Existing authenticated sessions can be explicitly paired with their operator-known endpoint using RemoteToolBinding; that manual pairing is trusted caller configuration and cannot be verified from BeeAI's opaque ClientSession.
A2AAgent is an agent, not a tool. a2a_binding constructs BeeAI's native HandoffTool around it. The caller supplies a previously selected a2a.types.AgentCard; this helper does not retrieve or verify a card. It copies the card and requires one JSON-RPC endpoint, with no alternate interfaces or authenticated extended-card replacement that could switch the destination.
from beeai_framework.agents.requirement import RequirementAgent
from beeai_agentguild import AgentGuildPreflightRequirement, PreflightPolicy, a2a_binding
binding = a2a_binding(caller_selected_card, name="remote_agent")
guard = AgentGuildPreflightRequirement([binding], policy=PreflightPolicy())
agent = RequirementAgent(llm=caller_model, tools=[binding.tool], requirements=[guard])
# await agent.run(caller_task)The complete importable composition is in examples/guarded_delegation.py. Its native allowed and denied executions are tested with a deterministic model and a loopback A2A endpoint. Running the example file directly only prints instructions. Native A2A transport behavior remains BeeAI's behavior: HTTP redirects are disabled by its defaults; its default 30-second HTTP configuration permits an unbounded read, and environment proxy use remains enabled. This requirement bounds the preflight, not the duration or all network behavior of a subsequently permitted task. In 0.1.83 HandoffTool clones its target and A2AAgent.clone() does not preserve custom parameters; this package therefore makes no unsupported claim that custom A2A transport parameters survive the handoff.
PreflightPolicy() requires endpoint_reachable and protocol_handshake to have status proven, denies do_not_delegate and delegate_with_caution, and tolerates no unknowns. This is deliberately restrictive: real responses commonly contain unknowns, so callers should review the check meanings before choosing a less restrictive policy.
For example, a caller can tolerate a named lack of independent history while retaining both default required checks:
policy = PreflightPolicy(tolerated_unknowns={"independent_evidence"})A caller dealing with an unauthenticated probe that cannot complete an authenticated protocol exchange may instead explicitly choose:
policy = PreflightPolicy(
required_proven={"endpoint_reachable"},
tolerated_unknowns={"protocol_handshake", "independent_evidence", "payment_claim_holds"},
)This permits only those named unknowns, subject to all other restrictions. protocol_handshake remains unknown in evidence. Set allow_caution=True only when your policy intentionally permits nonblocking failed checks; failed blocking checks still deny. Missing required checks, malformed or inconsistent bodies, mismatched target URLs, duplicate checks/JSON keys, unavailable observations and unknowns not explicitly tolerated all block. No result is called verified.
The integration uses only free GET /preflight?url=... at the fixed service origin https://agent-guild-5d5r.onrender.com. Calling it discloses the selected public endpoint to that service and asks the service to perform its bounded observation. URL credentials, queries, fragments, private IP literals and local-only hostnames are rejected first. This local screening is not DNS ownership verification; the service applies its own SSRF protections. Operator policy must authorize the service disclosure and any later task. No task payload, MCP credentials, card body or model prompt is sent to preflight. It never calls paid /check, registers an identity, provisions credit, verifies a passport or executes a payment.
guard.evidence retains each immutable Observation; guard.latest maps endpoint to the latest one. Each observation exposes:
source_origin,synthetic, request-startobserved_at, response-finishretrieved_at,cache_ttl_secondsandcache_valid_until;status(observed,invalidorunavailable),verdict,failedandunknowns;- the bounded original
raw_body, itssha256, and a fresh parseddocumentcopy. The digest detects changes to stored bytes; it is not a signature.
A successful bounded response is preserved in full. Oversized or interrupted responses are unavailable and may retain only a prefix; do not treat that prefix as complete evidence. Body size defaults to 65,536 bytes; the total preflight request/body timeout defaults to 5 seconds (maximum 30). Redirects are refused. There are no automatic retries or credential/environment-proxy inheritance in the preflight client.
Each reasoning step refreshes once per distinct bound endpoint. The blocking tool-start hook reuses that observation only while its monotonic TTL remains fresh (10 seconds by default, caller-adjustable from 0 to 60); otherwise it refreshes before the tool body. ttl_seconds=0 requires another observation at tool start. Response retrieval time is not proof of the endpoint's state at the later invocation. Evidence history resets on each agent run. Use one requirement instance per concurrent run.
Requirement.run() emits deny-only native Rule objects. A positive observation emits no allow rule and cannot override another requirement. In init(), the integration also registers the blocking tool-start event mechanism used by BeeAI's AskPermissionRequirement. Setting the start event's output prevents RunContext from executing the tool body. This second boundary matters because the released runner clears its rule requirements during one invalid-final-answer fallback. Tests exercise that exact fallback and confirm zero protected calls even when the tool returns to the model's offered list.
The guard covers the bound native tool instances when invoked directly by this RequirementAgent. It is not a process sandbox or a global network interceptor: direct calls outside that agent, tools omitted from its bindings, separately cloned/replaced tool instances, nested remote calls inside other tools, and explicit caller disabling of the requirement are outside that boundary. The pinned RequirementAgent.clone() shallow-copies its tool and requirement lists, so those identities survive its supported clone path; a native cloned-agent regression verifies denial still holds. Manually constructed bindings, mutable SDK configuration and competing middleware are trusted caller configuration.
An A2A card or signature's presence does not establish identity or signature validity. Preflight does not establish complete A2A/MCP conformance, task success, recipient/payment binding, paid execution, content safety or adoption. Unknowns are not automatically failures in the service's verdict, and a clean verdict can still contain unknowns. Preserve those distinctions downstream.
python -m pip install '.[test]' 'setuptools>=77'
LITELLM_LOCAL_MODEL_COST_MAP=True python -m pytest -q
python -m ruff check src tests examples
python -m ruff format --check src tests examples
python -m build --no-isolation
python examples/guarded_delegation.pyTests use actual installed framework classes, a deterministic native ChatModel, and controlled loopback HTTP servers that implement the minimum synthetic MCP/A2A exchanges. No external agent, production Guild endpoint, paid operation or model provider is contacted. The test socket guard rejects non-loopback connections. Loopback bindings require allow_loopback_fixture=True, and the preflight client accepts only a literal-loopback HTTP fixture override; a client using the public Guild origin still rejects loopback targets.
Source and contribution pins, test/build results and publication limits are in REVIEW.md. Apache-2.0; author AgentTanuki.