diff --git a/README.md b/README.md index cce41e3..c23ab25 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` | platform-scoped LiteLLM grant (Card declares `llm_provisioning=platform`) | 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.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,8 +55,9 @@ marketplace — the dashboard already surfaces these on the agent card. ## Pricing -`$0.10 / call` during the current pricing model. The deployed -agent is yours forever; subsequent invocations of your agent run on +`$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 your agent run on their own pricing (whatever you set when generating it). ## How to call it diff --git a/agent.py b/agent.py index 27408ac..4f9f1b5 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`` — a platform-scoped LiteLLM grant token for the builder's - own DeepAgents loop + 2. ``ctx.llm`` — caller-selected LLM credentials when available, otherwise + a platform-scoped LiteLLM grant token 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,7 +53,7 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]): config_model = BuilderConfig auth_model = NoAuth tools_used = ("deepagents", "langgraph", "a2a-pack", "litellm", "microsandbox") - llm_provisioning = LLMProvisioning.PLATFORM + llm_provisioning = LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED wants_cp_jwt = True workspace_access = WorkspaceAccess.dynamic( allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), @@ -62,8 +62,9 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]): price_per_call_usd=0.10, caller_pays_llm=False, notes=( - "Uses a platform-scoped LiteLLM grant for the builder's own " - "reasoning loop; deployed agent is yours forever." + "Uses caller-selected LLM credentials when available; otherwise " + "uses a platform-scoped LiteLLM grant for the builder's own " + "reasoning loop. Deployed agents remain yours." ), ) diff --git a/agent_builder/builder.py b/agent_builder/builder.py index 260f617..30ec3bb 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -28,8 +28,9 @@ class BuilderContext: workspace: Any | None = None grant_token: str | None = None # LLM creds forwarded by the orchestrator according to this agent's Card. - # The builder itself uses platform grants; generated agents usually use - # caller-provided creds. Falls back to settings for local development. + # The builder accepts caller-selected creds and falls back to platform + # grants; generated agents usually use caller-provided creds. Falls back + # to settings for local development. llm_base_url: str | None = None llm_api_key: str | None = None llm_model: str | None = None @@ -57,6 +58,10 @@ Default generated/user agents to ``LLMProvisioning.CALLER_PROVIDED`` and usage. ``LLMProvisioning.PLATFORM`` is allowed only through the A2A LiteLLM grant path: the generated agent must still read ``ctx.llm`` and must never read ``A2A_LITELLM_KEY`` or any provider key directly. +``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. 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 1e6356e..8c186b8 100644 --- a/agent_builder/skills/a2apack-agent-authoring/SKILL.md +++ b/agent_builder/skills/a2apack-agent-authoring/SKILL.md @@ -24,8 +24,10 @@ Use `A2AAgent` class attributes for runtime and marketplace behavior: For generated/user-owned agents that call an LLM, default to `LLMProvisioning.CALLER_PROVIDED` with `Pricing(..., caller_pays_llm=True, ...)`. Use `LLMProvisioning.PLATFORM` only when the user explicitly wants -platform-paid LLM usage. In either mode, read `ctx.llm`; never read -`A2A_LITELLM_KEY`, provider keys, or platform secrets directly. +platform-paid LLM usage. 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`; +never read `A2A_LITELLM_KEY`, provider keys, or platform secrets directly. Keep each `@skill` method `async`, put `RunContext[...]` immediately after `self`, and annotate every public argument. Do not use `*args` or `**kwargs`; diff --git a/agent_builder/skills/agent-quality-review/SKILL.md b/agent_builder/skills/agent-quality-review/SKILL.md index c4ab0e2..daafab6 100644 --- a/agent_builder/skills/agent-quality-review/SKILL.md +++ b/agent_builder/skills/agent-quality-review/SKILL.md @@ -31,7 +31,8 @@ Check these items: - The public schema is small and stable. No unbounded command strings, hidden prompt fragments, or provider credentials as public inputs. - The implementation reads `ctx.llm` for both `CALLER_PROVIDED` and - `PLATFORM` LLM provisioning. + `PLATFORM` LLM provisioning. `PLATFORM_OR_CALLER_PROVIDED` is acceptable + only for trusted platform/meta agents and must also read `ctx.llm`. - Generated/user agents should default to `llm_provisioning = LLMProvisioning.CALLER_PROVIDED` and `Pricing(..., caller_pays_llm=True, ...)`. If the user explicitly wants diff --git a/agent_builder/tools.py b/agent_builder/tools.py index 9b367d4..8d90f3e 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: from .config import Settings -A2A_PACK_MIN_VERSION = "0.1.27" +A2A_PACK_MIN_VERSION = "0.1.28" @dataclass(frozen=True) diff --git a/requirements.txt b/requirements.txt index dc0185f..1ccee85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -a2a-pack>=0.1.27 +a2a-pack>=0.1.28 httpx>=0.27 boto3>=1.34 deepagents>=0.5.0 diff --git a/tests/test_builder_prompt.py b/tests/test_builder_prompt.py index 7a1d5f0..edd6f4a 100644 --- a/tests/test_builder_prompt.py +++ b/tests/test_builder_prompt.py @@ -24,12 +24,16 @@ class BuilderPromptTests(unittest.TestCase): ) self.assertIn("stream=True", agent_source) - def test_outer_builder_uses_platform_llm_grants(self) -> None: + def test_outer_builder_accepts_user_creds_or_platform_llm_grants(self) -> None: agent_source = Path(__file__).resolve().parents[1].joinpath("agent.py").read_text() - self.assertIn("llm_provisioning = LLMProvisioning.PLATFORM", agent_source) + self.assertIn( + "llm_provisioning = LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED", + 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) def test_builder_defaults_raise_timeout_budget(self) -> None: config_source = Path(__file__).resolve().parents[1].joinpath(