fix: generate agents with platform LLM grants
All checks were successful
build / build (push) Successful in 21s

This commit is contained in:
robert
2026-05-26 21:39:02 -03:00
parent 54b0788b82
commit 53ecc9a0cb
11 changed files with 74 additions and 40 deletions

View File

@@ -14,7 +14,7 @@ The single `build(name, prompt)` skill:
- `a2apack-agent-authoring` — how to shape `agent.py`, public `@skill` - `a2apack-agent-authoring` — how to shape `agent.py`, public `@skill`
schemas, runtime metadata, pricing, resources, and auth schemas, runtime metadata, pricing, resources, and auth
- `deepagents-implementation-patterns` — concrete `create_deep_agent` - `deepagents-implementation-patterns` — concrete `create_deep_agent`
wiring for caller LLMs, skills, tools, and subagents wiring for `ctx.llm` credentials, skills, tools, and subagents
- `workspace-artifact-safety` — workspace grants, artifacts, bounded - `workspace-artifact-safety` — workspace grants, artifacts, bounded
subprocesses, and scope expansion subprocesses, and scope expansion
- `agent-quality-review` — pre-sandbox and pre-deploy checks for generated - `agent-quality-review` — pre-sandbox and pre-deploy checks for generated
@@ -57,8 +57,9 @@ marketplace — the dashboard already surfaces these on the agent card.
`$0.10 / call` during the current pricing model. The builder uses your `$0.10 / call` during the current pricing model. The builder uses your
selected LLM creds when present and falls back to a scoped platform grant. selected LLM creds when present and falls back to a scoped platform grant.
The deployed agent is yours forever; subsequent invocations of your agent run on The deployed agent is yours forever; subsequent invocations of a generated LLM
their own pricing (whatever you set when generating it). agent default to scoped platform LLM grants unless you explicitly generate a
BYOK/caller-paid agent.
## How to call it ## How to call it

View File

@@ -48,7 +48,7 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]):
"Writes the project into the user's workspace, validates it in a " "Writes the project into the user's workspace, validates it in a "
"sandbox, then ships it via the control plane." "sandbox, then ships it via the control plane."
) )
version = "0.1.1" version = "0.1.2"
config_model = BuilderConfig config_model = BuilderConfig
auth_model = NoAuth auth_model = NoAuth
@@ -278,4 +278,4 @@ def _find_url(value: Any) -> str | None:
m = _URL_RE.search(value) m = _URL_RE.search(value)
return m.group(0) if m else None return m.group(0) if m else None
return None return None
# rebuild against a2a-pack 0.1.29 for negotiated write prefixes # rebuild against a2a-pack 0.1.30 for generated platform LLM defaults

View File

@@ -29,8 +29,8 @@ class BuilderContext:
grant_token: str | None = None grant_token: str | None = None
# LLM creds forwarded by the orchestrator according to this agent's Card. # LLM creds forwarded by the orchestrator according to this agent's Card.
# The builder accepts caller-selected creds and falls back to platform # The builder accepts caller-selected creds and falls back to platform
# grants; generated agents usually use caller-provided creds. Falls back # grants. Hosted generated agents should usually use platform grants too.
# to settings for local development. # Falls back to settings for local development.
llm_base_url: str | None = None llm_base_url: str | None = None
llm_api_key: str | None = None llm_api_key: str | None = None
llm_model: str | None = None llm_model: str | None = None
@@ -47,17 +47,20 @@ user's workspace at ``agents/<name>/`` and then deploy it through the
control plane. control plane.
The default starter is the current a2a-pack DeepAgents scaffold. It declares The default starter is the current a2a-pack DeepAgents scaffold. It declares
``LLMProvisioning.CALLER_PROVIDED``, reads caller LLM credentials from ``LLMProvisioning.PLATFORM``, reads the scoped LLM grant from ``ctx.llm``,
``ctx.llm``, builds a ``ChatOpenAI`` model from those credentials, and wires a builds a ``ChatOpenAI`` model from those credentials, and wires a small
small tool-calling DeepAgent with ``create_deep_agent`` plus tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call``
``wrap_model_call`` middleware. Do not recreate this from memory: call middleware. Do not recreate this from memory: call ``init_agent_template`` first
``init_agent_template`` first and modify the generated files. and modify the generated files.
Default generated/user agents to ``LLMProvisioning.CALLER_PROVIDED`` and Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and
``caller_pays_llm=True`` unless the user explicitly wants platform-paid LLM ``caller_pays_llm=False`` so main-agent handoffs mint a scoped A2A LiteLLM grant
usage. ``LLMProvisioning.PLATFORM`` is allowed only through the A2A LiteLLM for the callee. The generated agent must still read ``ctx.llm`` and must never
grant path: the generated agent must still read ``ctx.llm`` and must never read read ``A2A_LITELLM_KEY``, ``OPENAI_API_KEY``, LiteLLM master keys, or provider
``A2A_LITELLM_KEY`` or any provider key directly. keys directly. If the user explicitly asks for BYOK or caller-paid inference,
use ``LLMProvisioning.CALLER_PROVIDED`` with ``caller_pays_llm=True`` and make
missing ``ctx.llm.api_key`` a clear setup/config result before constructing
``ChatOpenAI``.
``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted ``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted
platform/meta agents that should prefer caller-selected credentials and fall platform/meta agents that should prefer caller-selected credentials and fall
back to a scoped platform grant; do not use it for ordinary generated agents back to a scoped platform grant; do not use it for ordinary generated agents

View File

@@ -1,6 +1,6 @@
--- ---
name: a2apack-agent-authoring name: a2apack-agent-authoring
description: Author production a2a-pack agents with clear public @skill schemas, runtime declarations, caller-provided LLMs, workspace grants, pricing, resources, and secure platform capabilities. Use when writing or reviewing agent.py for an A2A agent. description: Author production a2a-pack agents with clear public @skill schemas, runtime declarations, platform LLM grants, optional caller-provided LLMs, workspace grants, pricing, resources, and secure platform capabilities. Use when writing or reviewing agent.py for an A2A agent.
--- ---
# A2A Pack Agent Authoring # A2A Pack Agent Authoring
@@ -21,13 +21,16 @@ Use `A2AAgent` class attributes for runtime and marketplace behavior:
- `wants_cp_jwt=True` is only for trusted platform agents. It forwards the - `wants_cp_jwt=True` is only for trusted platform agents. It forwards the
caller's control-plane token. caller's control-plane token.
For generated/user-owned agents that call an LLM, default to For hosted generated/user-owned agents that call an LLM, default to
`LLMProvisioning.CALLER_PROVIDED` with `Pricing(..., caller_pays_llm=True, ...)`. `LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=False, ...)` so
Use `LLMProvisioning.PLATFORM` only when the user explicitly wants main-agent handoffs mint a scoped A2A LiteLLM grant for the callee. Use
platform-paid LLM usage. Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` `LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or
caller-paid inference. Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`
for trusted platform/meta agents that must work with either the caller's for trusted platform/meta agents that must work with either the caller's
selected creds or a platform fallback grant. In every mode, read `ctx.llm`; selected creds or a platform fallback grant. In every mode, read `ctx.llm`;
never read `A2A_LITELLM_KEY`, provider keys, or platform secrets directly. never read `A2A_LITELLM_KEY`, `OPENAI_API_KEY`, provider keys, or platform
secrets directly. If `ctx.llm.api_key` is empty, return a clear setup/config
result before constructing `ChatOpenAI`.
Keep each `@skill` method `async`, put `RunContext[...]` immediately after Keep each `@skill` method `async`, put `RunContext[...]` immediately after
`self`, and annotate every public argument. Do not use `*args` or `**kwargs`; `self`, and annotate every public argument. Do not use `*args` or `**kwargs`;
@@ -68,11 +71,11 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
config_model = ResearchConfig config_model = ResearchConfig
auth_model = NoAuth auth_model = NoAuth
llm_provisioning = LLMProvisioning.CALLER_PROVIDED llm_provisioning = LLMProvisioning.PLATFORM
pricing = Pricing( pricing = Pricing(
price_per_call_usd=0.05, price_per_call_usd=0.05,
caller_pays_llm=True, caller_pays_llm=False,
notes="Caller supplies LLM credentials through the platform.", notes="Uses a scoped platform LLM grant through ctx.llm.",
) )
resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900) resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900)
egress = EgressPolicy(allow_hosts=("api.openai.com",)) egress = EgressPolicy(allow_hosts=("api.openai.com",))
@@ -97,6 +100,12 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
) -> dict[str, Any]: ) -> dict[str, Any]:
creds = ctx.llm creds = ctx.llm
await ctx.emit_progress(f"researching with {creds.model}") await ctx.emit_progress(f"researching with {creds.model}")
if not creds.api_key:
return {
"summary": "LLM credentials were not available for this run.",
"artifact": None,
"path": save_path,
}
graph = self._build_graph(ctx) graph = self._build_graph(ctx)
state = await graph.ainvoke( state = await graph.ainvoke(
{ {

View File

@@ -33,11 +33,14 @@ Check these items:
- The implementation reads `ctx.llm` for both `CALLER_PROVIDED` and - The implementation reads `ctx.llm` for both `CALLER_PROVIDED` and
`PLATFORM` LLM provisioning. `PLATFORM_OR_CALLER_PROVIDED` is acceptable `PLATFORM` LLM provisioning. `PLATFORM_OR_CALLER_PROVIDED` is acceptable
only for trusted platform/meta agents and must also read `ctx.llm`. only for trusted platform/meta agents and must also read `ctx.llm`.
- Generated/user agents should default to - Hosted generated/user agents should default to
`llm_provisioning = LLMProvisioning.CALLER_PROVIDED` and `llm_provisioning = LLMProvisioning.PLATFORM` and
`Pricing(..., caller_pays_llm=True, ...)`. If the user explicitly wants `Pricing(..., caller_pays_llm=False, ...)`. Use
platform-paid LLM usage, `LLMProvisioning.PLATFORM` is acceptable only if `LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or
the code still uses `ctx.llm` and never reads LiteLLM/provider keys directly. caller-paid inference. In both modes, the code must use `ctx.llm` and never
read LiteLLM/provider keys directly.
- Any code path that builds `ChatOpenAI` checks `ctx.llm.api_key` first or
returns a clear setup/config result before the model constructor runs.
- Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`. - Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`.
- Any project skills are seeded into the backend and passed with - Any project skills are seeded into the backend and passed with
`skills=skill_sources or None`. `skills=skill_sources or None`.

View File

@@ -1,6 +1,6 @@
--- ---
name: deepagent-agent-design name: deepagent-agent-design
description: Design generated A2A agents around DeepAgents skills, subagents, durable workspace backends, and caller-provided LLM reasoning. Use whenever building or modifying an agent, deciding whether to add tools, creating skill bundles, wiring create_deep_agent, or avoiding shallow fake tools. description: Design generated A2A agents around DeepAgents skills, subagents, durable workspace backends, and ctx.llm-backed reasoning. Use whenever building or modifying an agent, deciding whether to add tools, creating skill bundles, wiring create_deep_agent, or avoiding shallow fake tools.
--- ---
# DeepAgent Agent Design # DeepAgent Agent Design
@@ -13,8 +13,8 @@ planning, file work, skill selection, and subagent delegation.
Use this shape for non-trivial generated agents: Use this shape for non-trivial generated agents:
1. Keep one or two public A2A `@skill` methods with clear typed parameters. 1. Keep one or two public A2A `@skill` methods with clear typed parameters.
2. Inside each method, read `ctx.llm`, construct `ChatOpenAI`, and build a 2. Inside each method, read `ctx.llm`, verify credentials are available before
DeepAgent with `create_deep_agent`. constructing `ChatOpenAI`, and build a DeepAgent with `create_deep_agent`.
3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the 3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the
caller's durable workspace instead of LangGraph state. caller's durable workspace instead of LangGraph state.
4. Seed project skills into the current grant's write prefix and pass 4. Seed project skills into the current grant's write prefix and pass

View File

@@ -1,12 +1,14 @@
--- ---
name: deepagents-implementation-patterns name: deepagents-implementation-patterns
description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and caller-provided ChatOpenAI models. Use when editing create_deep_agent calls or deciding what belongs in tools, skills, or subagents. description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and ctx.llm-backed ChatOpenAI models. Use when editing create_deep_agent calls or deciding what belongs in tools, skills, or subagents.
--- ---
# DeepAgents Implementation Patterns # DeepAgents Implementation Patterns
DeepAgents supplies the inner agent loop. In this platform, the reliable shape DeepAgents supplies the inner agent loop. In this platform, the reliable shape
is: caller-provided `ChatOpenAI`, A2A workspace backend, project DeepAgents is: `ctx.llm`-backed `ChatOpenAI`, A2A workspace backend, project DeepAgents
skills, and a small number of exact tools. skills, and a small number of exact tools. Hosted generated agents should
declare `LLMProvisioning.PLATFORM`; explicit BYOK agents can declare
`CALLER_PROVIDED`, but both modes still read the same `ctx.llm` object.
## Implementation Shape ## Implementation Shape
@@ -32,6 +34,11 @@ stable output paths only when that is one of the grant write prefixes.
def build_graph(ctx: Any) -> Any: def build_graph(ctx: Any) -> Any:
creds = ctx.llm creds = ctx.llm
if not creds.api_key:
raise RuntimeError(
"LLM credentials were not available; declare PLATFORM for hosted "
"generated agents or configure caller-provided credentials."
)
model = ChatOpenAI( model = ChatOpenAI(
model=creds.model, model=creds.model,
base_url=creds.base_url, base_url=creds.base_url,

View File

@@ -28,7 +28,7 @@ if TYPE_CHECKING:
from .config import Settings from .config import Settings
A2A_PACK_MIN_VERSION = "0.1.29" A2A_PACK_MIN_VERSION = "0.1.30"
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -1,4 +1,4 @@
a2a-pack>=0.1.29 a2a-pack>=0.1.30
httpx>=0.27 httpx>=0.27
boto3>=1.34 boto3>=1.34
deepagents>=0.5.0 deepagents>=0.5.0

View File

@@ -53,6 +53,17 @@ class BuilderPromptTests(unittest.TestCase):
self.assertIn(f"a2a-pack>={A2A_PACK_MIN_VERSION}", requirements) self.assertIn(f"a2a-pack>={A2A_PACK_MIN_VERSION}", requirements)
self.assertIn("a2a-pack>={A2A_PACK_MIN_VERSION}", tools_source) self.assertIn("a2a-pack>={A2A_PACK_MIN_VERSION}", tools_source)
def test_prompt_defaults_generated_agents_to_platform_llm_grants(self) -> None:
self.assertIn("LLMProvisioning.PLATFORM", SYSTEM_PROMPT)
self.assertIn("caller_pays_llm=False", SYSTEM_PROMPT)
self.assertIn("main-agent handoffs mint a scoped A2A LiteLLM grant", SYSTEM_PROMPT)
self.assertIn("ctx.llm.api_key", SYSTEM_PROMPT)
files = _builder_skill_files()
self.assertIn("LLMProvisioning.PLATFORM", files["a2apack-agent-authoring/SKILL.md"])
self.assertIn("caller_pays_llm=False", files["a2apack-agent-authoring/SKILL.md"])
self.assertIn("ctx.llm.api_key", files["agent-quality-review/SKILL.md"])
def test_prompt_requires_resource_mirror_for_heavy_agents(self) -> None: def test_prompt_requires_resource_mirror_for_heavy_agents(self) -> None:
self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT) self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT)
self.assertIn("resources = Resources", SYSTEM_PROMPT) self.assertIn("resources = Resources", SYSTEM_PROMPT)

View File

@@ -31,7 +31,7 @@ class TemplateInitTests(unittest.TestCase):
self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"}) self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"})
self.assertIn('name = "research-agent"', files["agent.py"]) self.assertIn('name = "research-agent"', files["agent.py"])
self.assertIn('description = "Research helper"', files["agent.py"]) self.assertIn('description = "Research helper"', files["agent.py"])
self.assertIn("LLMProvisioning.CALLER_PROVIDED", files["agent.py"]) self.assertIn("LLMProvisioning.PLATFORM", files["agent.py"])
self.assertIn("WorkspaceAccess.dynamic", files["agent.py"]) self.assertIn("WorkspaceAccess.dynamic", files["agent.py"])
self.assertIn("RUNTIME_SKILLS_DIR", files["agent.py"]) self.assertIn("RUNTIME_SKILLS_DIR", files["agent.py"])
self.assertIn("_runtime_skills_root", files["agent.py"]) self.assertIn("_runtime_skills_root", files["agent.py"])