Require user LLM credentials
All checks were successful
build / build (push) Successful in 17s

This commit is contained in:
robert
2026-06-06 12:21:31 -03:00
parent 00ad0b8790
commit 64fd7c26f2
6 changed files with 40 additions and 48 deletions

View File

@@ -47,7 +47,7 @@ Three platform-managed bits land on the inner skill via `RunContext`:
| Field | Source | Required? | | Field | Source | Required? |
|---|---|---| |---|---|---|
| `ctx.workspace.bucket` | grant minted by the orchestrator | yes | | `ctx.workspace.bucket` | grant minted by the orchestrator | yes |
| `ctx.llm` | caller-selected LLM creds when available, otherwise a platform-scoped LiteLLM grant (Card declares `llm_provisioning=platform_or_caller_provided`) | yes | | `ctx.llm` | caller's saved LLM credential, routed by the platform (Card declares `llm_provisioning=platform`) | yes |
| `ctx.cp_jwt` | caller's CP JWT (Card declares `wants_cp_jwt=True`) | yes — used by `cp_deploy_tarball` | | `ctx.cp_jwt` | caller's CP JWT (Card declares `wants_cp_jwt=True`) | yes — used by `cp_deploy_tarball` |
The user opts into all three when they pick `agent-builder` from the The user opts into all three when they pick `agent-builder` from the
@@ -55,11 +55,9 @@ marketplace — the dashboard already surfaces these on the agent card.
## Pricing ## Pricing
`$0.10 / call` during the current pricing model. The builder uses your `$0.10 / call` during the current pricing model. The builder uses your saved
selected LLM creds when present and falls back to a scoped platform grant. LLM credential. The deployed agent is yours forever; subsequent invocations of
The deployed agent is yours forever; subsequent invocations of a generated LLM a generated LLM agent also use the caller's saved LLM credential via `ctx.llm`.
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

@@ -10,8 +10,8 @@ This agent only works when invoked through the platform orchestrator,
because it needs three things forwarded from the caller: because it needs three things forwarded from the caller:
1. ``ctx.workspace.bucket`` — the user's MinIO bucket 1. ``ctx.workspace.bucket`` — the user's MinIO bucket
2. ``ctx.llm`` — caller-selected LLM credentials when available, otherwise 2. ``ctx.llm`` — the caller's saved LLM credential for the builder's own
a platform-scoped LiteLLM grant token for the builder's own DeepAgents loop DeepAgents loop
3. ``ctx.cp_jwt`` — the user's CP JWT, so cp_deploy_tarball can 3. ``ctx.cp_jwt`` — the user's CP JWT, so cp_deploy_tarball can
POST to /v1/agents/from-tarball as the user. POST to /v1/agents/from-tarball as the user.
""" """
@@ -53,17 +53,16 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]):
config_model = BuilderConfig config_model = BuilderConfig
auth_model = NoAuth auth_model = NoAuth
tools_used = ("deepagents", "langgraph", "a2a-pack", "litellm", "microsandbox") tools_used = ("deepagents", "langgraph", "a2a-pack", "litellm", "microsandbox")
llm_provisioning = LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED llm_provisioning = LLMProvisioning.PLATFORM
wants_cp_jwt = True wants_cp_jwt = True
workspace_access = WorkspaceAccess.dynamic( workspace_access = WorkspaceAccess.dynamic(
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
) )
pricing = Pricing( pricing = Pricing(
price_per_call_usd=0.10, price_per_call_usd=0.10,
caller_pays_llm=False, caller_pays_llm=True,
notes=( notes=(
"Uses caller-selected LLM credentials when available; otherwise " "Uses the caller's saved LLM credential for the builder's own "
"uses a platform-scoped LiteLLM grant for the builder's own "
"reasoning loop. Deployed agents remain yours." "reasoning loop. Deployed agents remain yours."
), ),
) )

View File

@@ -30,9 +30,9 @@ class BuilderContext:
sandbox: Any | None = None sandbox: Any | None = None
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 uses the caller's saved LLM credential, usually proxied
# grants. Hosted generated agents should usually use platform grants too. # through LiteLLM by the control plane. Falls back to settings only for
# Falls back to settings for local development. # 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
@@ -49,28 +49,26 @@ 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.PLATFORM``, reads the scoped LLM grant from ``ctx.llm``, ``LLMProvisioning.PLATFORM``, reads the caller's saved LLM credential from
builds a ``ChatOpenAI`` model from those credentials, and wires a small ``ctx.llm``, builds a ``ChatOpenAI`` model from those credentials, and wires a small
tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call`` tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call``
middleware. Do not recreate this from memory: call ``init_agent_template`` first middleware. Do not recreate this from memory: call ``init_agent_template`` first
and modify the generated files. and modify the generated files.
Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and
``caller_pays_llm=False`` so main-agent handoffs mint a scoped A2A LiteLLM grant ``caller_pays_llm=True`` so main-agent handoffs forward the caller's saved LLM
for the callee. The generated agent must still read ``ctx.llm`` and must never credential for the callee. The generated agent must still read ``ctx.llm`` and must never
read ``A2A_LITELLM_KEY``, ``OPENAI_API_KEY``, LiteLLM master keys, or provider read ``A2A_LITELLM_KEY``, ``OPENAI_API_KEY``, LiteLLM master keys, or provider
keys directly. If the user explicitly asks for BYOK or caller-paid inference, keys directly. ``LLMProvisioning.CALLER_PROVIDED`` is still acceptable for
use ``LLMProvisioning.CALLER_PROVIDED`` with ``caller_pays_llm=True`` and make explicit BYOK wording. Always make missing ``ctx.llm.api_key`` a clear setup/config result before constructing
missing ``ctx.llm.api_key`` a clear setup/config result before constructing
``ChatOpenAI``. ``ChatOpenAI``.
``Pricing`` accepts only ``price_per_call_usd``, ``caller_pays_llm``, and ``Pricing`` accepts only ``price_per_call_usd``, ``caller_pays_llm``, and
``notes``. Never set ``runtime.pricing.compute`` or ``notes``. Never set ``runtime.pricing.compute`` or
``runtime.pricing.total_usd`` in generated source; those are derived later by ``runtime.pricing.total_usd`` in generated source; those are derived later by
the control plane/dashboard from declared ``Resources`` and billing policy. the control plane/dashboard from declared ``Resources`` and billing policy.
``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted ``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is retained only for backwards
platform/meta agents that should prefer caller-selected credentials and fall compatibility; do not use it for ordinary generated agents unless the user
back to a scoped platform grant; do not use it for ordinary generated agents explicitly asks for that legacy mixed mode.
unless the user explicitly asks for that mixed billing mode.
You have packaged DeepAgents skills loaded from ``/.agent-builder/skills/``. You have packaged DeepAgents skills loaded from ``/.agent-builder/skills/``.
Use ``deepagent-agent-design`` before designing generated agent internals, Use ``deepagent-agent-design`` before designing generated agent internals,

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, 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. description: Author production a2a-pack agents with clear public @skill schemas, runtime declarations, caller-funded LLM credentials, optional agent BYOK, 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
@@ -22,12 +22,11 @@ Use `A2AAgent` class attributes for runtime and marketplace behavior:
caller's control-plane token. caller's control-plane token.
For hosted generated/user-owned agents that call an LLM, default to For hosted generated/user-owned agents that call an LLM, default to
`LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=False, ...)` so `LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=True, ...)` so
main-agent handoffs mint a scoped A2A LiteLLM grant for the callee. Use main-agent handoffs forward the caller's saved LLM credential for the callee.
`LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or `LLMProvisioning.CALLER_PROVIDED` is still acceptable for explicit BYOK wording.
caller-paid inference. Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` for legacy compatibility.
for trusted platform/meta agents that must work with either the caller's In every mode, read `ctx.llm`;
selected creds or a platform fallback grant. In every mode, read `ctx.llm`;
never read `A2A_LITELLM_KEY`, `OPENAI_API_KEY`, provider keys, or platform 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 secrets directly. If `ctx.llm.api_key` is empty, return a clear setup/config
result before constructing `ChatOpenAI`. result before constructing `ChatOpenAI`.
@@ -79,8 +78,8 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
llm_provisioning = LLMProvisioning.PLATFORM llm_provisioning = LLMProvisioning.PLATFORM
pricing = Pricing( pricing = Pricing(
price_per_call_usd=0.05, price_per_call_usd=0.05,
caller_pays_llm=False, caller_pays_llm=True,
notes="Uses a scoped platform LLM grant through ctx.llm.", notes="Uses the caller's saved LLM credential 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",))

View File

@@ -35,10 +35,9 @@ Check these items:
only for trusted platform/meta agents and must also read `ctx.llm`. only for trusted platform/meta agents and must also read `ctx.llm`.
- Hosted generated/user agents should default to - Hosted generated/user agents should default to
`llm_provisioning = LLMProvisioning.PLATFORM` and `llm_provisioning = LLMProvisioning.PLATFORM` and
`Pricing(..., caller_pays_llm=False, ...)`. Use `Pricing(..., caller_pays_llm=True, ...)`. `LLMProvisioning.CALLER_PROVIDED`
`LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or is acceptable for explicit BYOK wording. In both modes, the code must use
caller-paid inference. In both modes, the code must use `ctx.llm` and never `ctx.llm` and never read LiteLLM/provider keys directly.
read LiteLLM/provider keys directly.
- `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`. - `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`.
Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or
`total_usd=` in `agent.py`; those are derived platform fields. `total_usd=` in `agent.py`; those are derived platform fields.

View File

@@ -32,16 +32,15 @@ class BuilderPromptTests(unittest.TestCase):
self.assertIn("stream=True", agent_source) self.assertIn("stream=True", agent_source)
self.assertIn('grant_write_prefixes=("agents/{name}/",)', agent_source) self.assertIn('grant_write_prefixes=("agents/{name}/",)', agent_source)
def test_outer_builder_accepts_user_creds_or_platform_llm_grants(self) -> None: def test_outer_builder_uses_user_llm_credentials(self) -> None:
agent_source = Path(__file__).resolve().parents[1].joinpath("agent.py").read_text() agent_source = Path(__file__).resolve().parents[1].joinpath("agent.py").read_text()
self.assertIn( self.assertIn(
"llm_provisioning = LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED", "llm_provisioning = LLMProvisioning.PLATFORM",
agent_source, agent_source,
) )
self.assertIn("caller_pays_llm=False", agent_source) self.assertIn("caller_pays_llm=True", agent_source)
self.assertIn("platform-scoped LiteLLM grant", agent_source) self.assertIn("caller's saved LLM credential", agent_source)
self.assertIn("caller-selected LLM credentials", agent_source)
def test_builder_defaults_raise_timeout_budget(self) -> None: def test_builder_defaults_raise_timeout_budget(self) -> None:
config_source = Path(__file__).resolve().parents[1].joinpath( config_source = Path(__file__).resolve().parents[1].joinpath(
@@ -60,15 +59,15 @@ 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: def test_prompt_defaults_generated_agents_to_user_llm_credentials(self) -> None:
self.assertIn("LLMProvisioning.PLATFORM", SYSTEM_PROMPT) self.assertIn("LLMProvisioning.PLATFORM", SYSTEM_PROMPT)
self.assertIn("caller_pays_llm=False", SYSTEM_PROMPT) self.assertIn("caller_pays_llm=True", SYSTEM_PROMPT)
self.assertIn("main-agent handoffs mint a scoped A2A LiteLLM grant", SYSTEM_PROMPT) self.assertIn("main-agent handoffs forward the caller's saved LLM", SYSTEM_PROMPT)
self.assertIn("ctx.llm.api_key", SYSTEM_PROMPT) self.assertIn("ctx.llm.api_key", SYSTEM_PROMPT)
files = _builder_skill_files() files = _builder_skill_files()
self.assertIn("LLMProvisioning.PLATFORM", files["a2apack-agent-authoring/SKILL.md"]) self.assertIn("LLMProvisioning.PLATFORM", files["a2apack-agent-authoring/SKILL.md"])
self.assertIn("caller_pays_llm=False", files["a2apack-agent-authoring/SKILL.md"]) self.assertIn("caller_pays_llm=True", files["a2apack-agent-authoring/SKILL.md"])
self.assertIn("ctx.llm.api_key", files["agent-quality-review/SKILL.md"]) self.assertIn("ctx.llm.api_key", files["agent-quality-review/SKILL.md"])
def test_prompt_rejects_platform_derived_pricing_fields(self) -> None: def test_prompt_rejects_platform_derived_pricing_fields(self) -> None: