From 64fd7c26f236815da2c248d56948b13235267f52 Mon Sep 17 00:00:00 2001 From: robert Date: Sat, 6 Jun 2026 12:21:31 -0300 Subject: [PATCH] Require user LLM credentials --- README.md | 10 +++---- agent.py | 11 ++++---- agent_builder/builder.py | 26 +++++++++---------- .../skills/a2apack-agent-authoring/SKILL.md | 17 ++++++------ .../skills/agent-quality-review/SKILL.md | 7 +++-- tests/test_builder_prompt.py | 17 ++++++------ 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index b25d79b..005d3b4 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Three platform-managed bits land on the inner skill via `RunContext`: | Field | Source | Required? | |---|---|---| | `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` | 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 -`$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. -The deployed agent is yours forever; subsequent invocations of a generated LLM -agent default to scoped platform LLM grants unless you explicitly generate a -BYOK/caller-paid agent. +`$0.10 / call` during the current pricing model. The builder uses your saved +LLM credential. The deployed agent is yours forever; subsequent invocations of +a generated LLM agent also use the caller's saved LLM credential via `ctx.llm`. ## How to call it diff --git a/agent.py b/agent.py index a669ce7..8165bbf 100644 --- a/agent.py +++ b/agent.py @@ -10,8 +10,8 @@ This agent only works when invoked through the platform orchestrator, because it needs three things forwarded from the caller: 1. ``ctx.workspace.bucket`` — the user's MinIO bucket - 2. ``ctx.llm`` — caller-selected LLM credentials when available, otherwise - a platform-scoped LiteLLM grant token for the builder's own DeepAgents loop + 2. ``ctx.llm`` — the caller's saved LLM credential for the builder's own + DeepAgents loop 3. ``ctx.cp_jwt`` — the user's CP JWT, so cp_deploy_tarball can POST to /v1/agents/from-tarball as the user. """ @@ -53,17 +53,16 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]): config_model = BuilderConfig auth_model = NoAuth tools_used = ("deepagents", "langgraph", "a2a-pack", "litellm", "microsandbox") - llm_provisioning = LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED + llm_provisioning = LLMProvisioning.PLATFORM wants_cp_jwt = True workspace_access = WorkspaceAccess.dynamic( allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), ) pricing = Pricing( price_per_call_usd=0.10, - caller_pays_llm=False, + caller_pays_llm=True, notes=( - "Uses caller-selected LLM credentials when available; otherwise " - "uses a platform-scoped LiteLLM grant for the builder's own " + "Uses the caller's saved LLM credential for the builder's own " "reasoning loop. Deployed agents remain yours." ), ) diff --git a/agent_builder/builder.py b/agent_builder/builder.py index 4e182c9..7f65565 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -30,9 +30,9 @@ class BuilderContext: sandbox: Any | None = None grant_token: str | None = None # LLM creds forwarded by the orchestrator according to this agent's Card. - # The builder accepts caller-selected creds and falls back to platform - # grants. Hosted generated agents should usually use platform grants too. - # Falls back to settings for local development. + # The builder uses the caller's saved LLM credential, usually proxied + # through LiteLLM by the control plane. Falls back to settings only for + # local development. llm_base_url: str | None = None llm_api_key: str | None = None llm_model: str | None = None @@ -49,28 +49,26 @@ user's workspace at ``agents//`` and then deploy it through the control plane. The default starter is the current a2a-pack DeepAgents scaffold. It declares -``LLMProvisioning.PLATFORM``, reads the scoped LLM grant from ``ctx.llm``, -builds a ``ChatOpenAI`` model from those credentials, and wires a small +``LLMProvisioning.PLATFORM``, reads the caller's saved LLM credential from +``ctx.llm``, builds a ``ChatOpenAI`` model from those credentials, and wires a small tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call`` middleware. Do not recreate this from memory: call ``init_agent_template`` first and modify the generated files. Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and -``caller_pays_llm=False`` so main-agent handoffs mint a scoped A2A LiteLLM grant -for the callee. The generated agent must still read ``ctx.llm`` and must never +``caller_pays_llm=True`` so main-agent handoffs forward the caller's saved LLM +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 -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 +keys directly. ``LLMProvisioning.CALLER_PROVIDED`` is still acceptable for +explicit BYOK wording. Always make missing ``ctx.llm.api_key`` a clear setup/config result before constructing ``ChatOpenAI``. ``Pricing`` accepts only ``price_per_call_usd``, ``caller_pays_llm``, and ``notes``. Never set ``runtime.pricing.compute`` or ``runtime.pricing.total_usd`` in generated source; those are derived later by the control plane/dashboard from declared ``Resources`` and billing policy. -``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted -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 -unless the user explicitly asks for that mixed billing mode. +``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is retained only for backwards +compatibility; do not use it for ordinary generated agents unless the user +explicitly asks for that legacy mixed mode. You have packaged DeepAgents skills loaded from ``/.agent-builder/skills/``. Use ``deepagent-agent-design`` before designing generated agent internals, diff --git a/agent_builder/skills/a2apack-agent-authoring/SKILL.md b/agent_builder/skills/a2apack-agent-authoring/SKILL.md index 631d75f..8cfa589 100644 --- a/agent_builder/skills/a2apack-agent-authoring/SKILL.md +++ b/agent_builder/skills/a2apack-agent-authoring/SKILL.md @@ -1,6 +1,6 @@ --- 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 @@ -22,12 +22,11 @@ Use `A2AAgent` class attributes for runtime and marketplace behavior: caller's control-plane token. For hosted generated/user-owned agents that call an LLM, default to -`LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=False, ...)` so -main-agent handoffs mint a scoped A2A LiteLLM grant for the callee. Use -`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 -selected creds or a platform fallback grant. In every mode, read `ctx.llm`; +`LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=True, ...)` so +main-agent handoffs forward the caller's saved LLM credential for the callee. +`LLMProvisioning.CALLER_PROVIDED` is still acceptable for explicit BYOK wording. +Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` for legacy compatibility. +In every mode, read `ctx.llm`; 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`. @@ -79,8 +78,8 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]): llm_provisioning = LLMProvisioning.PLATFORM pricing = Pricing( price_per_call_usd=0.05, - caller_pays_llm=False, - notes="Uses a scoped platform LLM grant through ctx.llm.", + caller_pays_llm=True, + notes="Uses the caller's saved LLM credential through ctx.llm.", ) resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900) egress = EgressPolicy(allow_hosts=("api.openai.com",)) diff --git a/agent_builder/skills/agent-quality-review/SKILL.md b/agent_builder/skills/agent-quality-review/SKILL.md index e9ea43d..dc0442e 100644 --- a/agent_builder/skills/agent-quality-review/SKILL.md +++ b/agent_builder/skills/agent-quality-review/SKILL.md @@ -35,10 +35,9 @@ Check these items: only for trusted platform/meta agents and must also read `ctx.llm`. - Hosted generated/user agents should default to `llm_provisioning = LLMProvisioning.PLATFORM` and - `Pricing(..., caller_pays_llm=False, ...)`. Use - `LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or - caller-paid inference. In both modes, the code must use `ctx.llm` and never - read LiteLLM/provider keys directly. + `Pricing(..., caller_pays_llm=True, ...)`. `LLMProvisioning.CALLER_PROVIDED` + is acceptable for explicit BYOK wording. In both modes, the code must use + `ctx.llm` and never read LiteLLM/provider keys directly. - `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`. Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or `total_usd=` in `agent.py`; those are derived platform fields. diff --git a/tests/test_builder_prompt.py b/tests/test_builder_prompt.py index 2082ef6..abab23e 100644 --- a/tests/test_builder_prompt.py +++ b/tests/test_builder_prompt.py @@ -32,16 +32,15 @@ class BuilderPromptTests(unittest.TestCase): self.assertIn("stream=True", 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() self.assertIn( - "llm_provisioning = LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED", + "llm_provisioning = LLMProvisioning.PLATFORM", agent_source, ) - self.assertIn("caller_pays_llm=False", agent_source) - self.assertIn("platform-scoped LiteLLM grant", agent_source) - self.assertIn("caller-selected LLM credentials", agent_source) + self.assertIn("caller_pays_llm=True", agent_source) + self.assertIn("caller's saved LLM credential", agent_source) def test_builder_defaults_raise_timeout_budget(self) -> None: 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("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("caller_pays_llm=False", SYSTEM_PROMPT) - self.assertIn("main-agent handoffs mint a scoped A2A LiteLLM grant", SYSTEM_PROMPT) + self.assertIn("caller_pays_llm=True", SYSTEM_PROMPT) + self.assertIn("main-agent handoffs forward the caller's saved LLM", 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("caller_pays_llm=True", files["a2apack-agent-authoring/SKILL.md"]) self.assertIn("ctx.llm.api_key", files["agent-quality-review/SKILL.md"]) def test_prompt_rejects_platform_derived_pricing_fields(self) -> None: