Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ server-side reject rather than calling its own `/inspect`.

```
pi: providers = [openai, anthropic, google/gemini, mistral, groq, minimax, together_ai,
openrouter] (+ the ~24 no-vault-kind providers Pi also reaches)
openrouter, openai-codex] (+ the ~24 no-vault-kind providers Pi also reaches)
// openai-codex = OpenAI's ChatGPT/Codex subscription; reached via an OAuth login
// (no vault provider_key), so it is usable under self_managed and the agenta
// default's runtime_provided fallback. Models (gpt-5.5/gpt-5.4/gpt-5.4-mini) are
// carried explicitly in capabilities.py since the litellm catalog omits them.
deployments = [direct] (azure, bedrock, vertex declared; Pi consumption staged with
model-config -> fail loud in v1)
connection_modes = [agenta, self_managed]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ the playground's composite sidecar URI (`{mode, url}` → the agent config `uri`

> **Status:** verified working on 2026-06-25. Two `/run` turns on `harness=claude`, `model=haiku`,
> with no API key, returned correct output using only the mounted subscription OAuth.
>
> **Update 2026-06-30:** the same sidecar also serves the **OpenAI ChatGPT/Codex subscription**
> for Pi. Mount the host's Pi login (`~/.pi/agent`, provider key `openai-codex`, from `pi` →
> `/login`) alongside `~/.claude`, and run `harness=pi_core` with `model=openai-codex/<id>`
> (`gpt-5.5`/`gpt-5.4`/`gpt-5.4-mini`/`gpt-5.3-codex-spark`) and `connection.mode=self_managed`.
> Verified end to end through the live agent service. The full dual-login recipe, wiring, and
> verification now live in the **`subscription-sidecar` skill** (`.agents/skills/subscription-sidecar/`);
> this document keeps the Claude-specific rationale below.
>
> Note the app layer needs `openai-codex` in the per-harness capability table
> (`sdks/python/agenta/sdk/agents/capabilities.py`); it was missing, so a codex `self_managed` run
> was rejected before reaching the runner. Fixed 2026-06-30. See
> [`../provider-model-auth/harness-provider-matrix.md`](../provider-model-auth/harness-provider-matrix.md).

## Why this works without a code change

Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/ee/docker-compose.gh.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ services:
DAYTONA_TARGET: ${DAYTONA_TARGET:-}
DAYTONA_SNAPSHOT: ${DAYTONA_SNAPSHOT:-}
DAYTONA_IMAGE: ${DAYTONA_IMAGE:-}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-false}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-true}
# === NETWORK ============================================== #
networks:
- agenta-ee-gh-network
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/ee/docker-compose.gh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ services:
DAYTONA_TARGET: ${DAYTONA_TARGET:-}
DAYTONA_SNAPSHOT: ${DAYTONA_SNAPSHOT:-}
DAYTONA_IMAGE: ${DAYTONA_IMAGE:-}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-false}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-true}
# === NETWORK ============================================== #
networks:
- agenta-ee-gh-network
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/ee/env.ee.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ AGENTA_CRYPT_KEY=replace-me
# AGENTA_API_KEY=
# AGENTA_AGENT_MCPS_ENABLED=false
# AGENTA_AGENT_SANDBOX_PI_DIR=/home/sandbox/.pi/agent
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.79.4
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.80.6
# AGENTA_AGENT_SANDBOX_PI_INSTALLED=true

# ================================================================== #
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/ee/env.ee.gh.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ AGENTA_CRYPT_KEY=replace-me
# AGENTA_API_KEY=
# AGENTA_AGENT_MCPS_ENABLED=false
# AGENTA_AGENT_SANDBOX_PI_DIR=/home/sandbox/.pi/agent
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.79.4
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.80.6
# AGENTA_AGENT_SANDBOX_PI_INSTALLED=true

# ================================================================== #
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/oss/docker-compose.gh.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ services:
DAYTONA_TARGET: ${DAYTONA_TARGET:-}
DAYTONA_SNAPSHOT: ${DAYTONA_SNAPSHOT:-}
DAYTONA_IMAGE: ${DAYTONA_IMAGE:-}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-false}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-true}
# === NETWORK ============================================== #
networks:
- agenta-oss-gh-network
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/oss/docker-compose.gh.ssl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ services:
DAYTONA_TARGET: ${DAYTONA_TARGET:-}
DAYTONA_SNAPSHOT: ${DAYTONA_SNAPSHOT:-}
DAYTONA_IMAGE: ${DAYTONA_IMAGE:-}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-false}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-true}
# === NETWORK ============================================== #
networks:
- agenta-gh-ssl-network
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/oss/docker-compose.gh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ services:
DAYTONA_TARGET: ${DAYTONA_TARGET:-}
DAYTONA_SNAPSHOT: ${DAYTONA_SNAPSHOT:-}
DAYTONA_IMAGE: ${DAYTONA_IMAGE:-}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-false}
AGENTA_AGENT_SANDBOX_PI_INSTALLED: ${AGENTA_AGENT_SANDBOX_PI_INSTALLED:-true}
# === NETWORK ============================================== #
networks:
- agenta-oss-gh-network
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/oss/env.oss.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ AGENTA_CRYPT_KEY=replace-me
# AGENTA_API_KEY=
# AGENTA_AGENT_MCPS_ENABLED=false
# AGENTA_AGENT_SANDBOX_PI_DIR=/home/sandbox/.pi/agent
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.79.4
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.80.6
# AGENTA_AGENT_SANDBOX_PI_INSTALLED=true

# ================================================================== #
Expand Down
2 changes: 1 addition & 1 deletion hosting/docker-compose/oss/env.oss.gh.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ AGENTA_CRYPT_KEY=replace-me
# AGENTA_API_KEY=
# AGENTA_AGENT_MCPS_ENABLED=false
# AGENTA_AGENT_SANDBOX_PI_DIR=/home/sandbox/.pi/agent
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.79.4
# AGENTA_AGENT_SANDBOX_PI_VERSION=0.80.6
# AGENTA_AGENT_SANDBOX_PI_INSTALLED=true

# ================================================================== #
Expand Down
60 changes: 50 additions & 10 deletions sdks/python/agenta/sdk/agents/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
``docs/design/agent-workflows/projects/provider-model-auth/harness-provider-matrix.md``:

- **Pi** reaches eight Agenta-vault-mapped providers directly (the ones whose ``provider_key``
secret drives a Pi provider via its env-key map). Pi also reaches ~24 more providers that have
no Agenta vault kind; those are out of scope unless a ``custom_provider`` secret is made for
them, so they are not enumerated here. Pi's cloud deployments (azure/bedrock/vertex) are
*declared* but Pi *consumption* of them stages with the model-config sibling, so v1 fails loud:
``deployments`` is ``["direct"]`` for the live reach.
secret drives a Pi provider via its env-key map), plus ``openai-codex`` (OpenAI's ChatGPT/Codex
subscription), which Pi reaches through its own OAuth login rather than a vault key — usable
under ``self_managed`` (and the ``agenta`` default's ``runtime_provided`` fallback). Pi also
reaches ~24 more providers that have no Agenta vault kind; those are out of scope unless a
``custom_provider`` secret is made for them, so they are not enumerated here. Pi's cloud
deployments (azure/bedrock/vertex) are *declared* but Pi *consumption* of them stages with the
model-config sibling, so v1 fails loud: ``deployments`` is ``["direct"]`` for the live reach.
- **Claude** reaches anthropic only, direct, via a custom gateway, or through Anthropic on
Bedrock/Vertex. The runner passes the selected model id through to Claude Code and lets the
configured backend fail loudly if it rejects it.
Expand Down Expand Up @@ -51,6 +53,36 @@
"openrouter",
]

# Subscription/OAuth-only providers Pi also reaches. ``openai-codex`` is OpenAI's ChatGPT/Codex
# subscription: Pi authenticates it with an OAuth login (``~/.pi/agent/auth.json``, ``pi`` then
# ``/login``), NOT an Agenta vault ``provider_key`` (no vault secret kind maps to it). ``self_managed``
# is broader than this one provider: it covers any way a harness signs itself in without an
# Agenta-stored key, including machine credentials such as environment variables. This provider's
# on-ramp under ``self_managed`` happens to be the subscription OAuth. It is also reachable under
# the ``agenta`` default's ``runtime_provided`` fallback, so it belongs in Pi's reachable providers
# even though it carries no vault key. Its model ids are carried explicitly below because they are
# not in the litellm-derived ``supported_llm_models`` catalog. See
# ``docs/design/agent-workflows/projects/provider-model-auth/harness-provider-matrix.md`` and the
# subscription-sidecar recipe.
PI_SUBSCRIPTION_MODELS: Dict[str, List[str]] = {
# Bare ids (like the ``openai`` catalog); the ``openai-codex`` provider disambiguates from the
# plain ``openai`` models that share these names. The runner normalizes any of these onto the
# harness's ``openai-codex/<id>`` model. This is the full ``openai-codex`` model set Pi's
# vendored catalog exposes (``@earendil-works/pi-ai`` ``models.generated`` ->
# ``openai-codex``, served via ``chatgpt.com/backend-api``); keep it in sync when the pinned
# Pi version changes its codex model list.
"openai-codex": [
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.5",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex-spark",
],
}
PI_SUBSCRIPTION_PROVIDERS: List[str] = list(PI_SUBSCRIPTION_MODELS)

# Claude Code selects a model by alias, not a ``provider/id`` string. These are the aliases the
# Claude harness accepts (``default``/``sonnet``/``opus``/``haiku``) plus their ``[1m]``
# long-context variants. They live under the ``anthropic`` provider in the ``models`` map (Claude
Expand Down Expand Up @@ -88,17 +120,25 @@


def _pi_models() -> Dict[str, List[str]]:
"""The per-provider model ids Pi reaches: the catalog entry for each vault provider.
"""The per-provider model ids Pi reaches: the catalog entry for each vault provider, plus the
explicit ids for the subscription/OAuth providers (``openai-codex``) that the shared catalog
does not list.

Defensive against a provider missing from ``supported_llm_models`` (skip it) so a catalog
edit never breaks the capability document. The ids are provider-prefixed (``openai/gpt-...``),
edit never breaks the capability document. The ids match the shared catalog's shape (mostly
provider-prefixed like ``anthropic/...``; some, e.g. ``openai``, are bare like ``gpt-5.5``),
the same shape the playground model picker already renders.
"""
return {
models = {
provider: list(supported_llm_models[provider])
for provider in PI_VAULT_PROVIDERS
if provider in supported_llm_models
}
# The subscription/OAuth providers are not in the litellm-derived catalog, so carry their ids
# explicitly (like the Claude alias set).
for provider, ids in PI_SUBSCRIPTION_MODELS.items():
models[provider] = list(ids)
return models


class HarnessConnectionCapabilities(BaseModel):
Expand Down Expand Up @@ -127,14 +167,14 @@ class HarnessConnectionCapabilities(BaseModel):

HARNESS_CONNECTION_CAPABILITIES: Dict[str, HarnessConnectionCapabilities] = {
"pi_core": HarnessConnectionCapabilities(
providers=list(PI_VAULT_PROVIDERS),
providers=list(PI_VAULT_PROVIDERS) + list(PI_SUBSCRIPTION_PROVIDERS),
deployments=["direct"],
connection_modes=list(_ALL_MODES),
model_selection="provider/id",
models=_pi_models(),
),
"pi_agenta": HarnessConnectionCapabilities(
providers=list(PI_VAULT_PROVIDERS),
providers=list(PI_VAULT_PROVIDERS) + list(PI_SUBSCRIPTION_PROVIDERS),
deployments=["direct"],
connection_modes=list(_ALL_MODES),
model_selection="provider/id",
Expand Down
3 changes: 3 additions & 0 deletions sdks/python/agenta/sdk/utils/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
"mistral/open-mistral-nemo",
],
"openai": [
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.5-pro",
"gpt-5.5",
"gpt-5.4-pro",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from agenta.sdk.agents.capabilities import (
CLAUDE_MODEL_ALIASES,
HARNESS_CONNECTION_CAPABILITIES,
PI_SUBSCRIPTION_MODELS,
PI_SUBSCRIPTION_PROVIDERS,
PI_VAULT_PROVIDERS,
harness_allows_deployment,
harness_allows_mode,
Expand All @@ -35,6 +37,23 @@ def test_pi_and_agenta_reach_the_vault_providers_not_arbitrary_ones():
assert harness_allows_provider(harness, "anything-custom") is False


def test_pi_and_agenta_reach_the_openai_codex_subscription_provider():
"""The ChatGPT/Codex subscription provider is reachable (OAuth login, no vault key).

Without this, an ``openai-codex`` model fails the agent-layer pre-resolve provider check
even though the runner drives the subscription fine. ``self_managed`` is the subscription
path; the provider must be allowed for that mode to ever reach the runner.
"""
for harness in ("pi_core", "pi_agenta"):
for provider in PI_SUBSCRIPTION_PROVIDERS:
assert harness_allows_provider(harness, provider) is True
assert harness_allows_provider(harness, "openai-codex") is True
# case-insensitive, like the vault providers
assert harness_allows_provider(harness, "OpenAI-Codex") is True
# the subscription is a Pi reach only; Claude stays anthropic-only
assert harness_allows_provider("claude", "openai-codex") is False


def test_unknown_harness_is_permissive():
assert harness_allows_provider("some-future-harness", "openai") is True
assert harness_allows_mode("some-future-harness", "agenta") is True
Expand Down Expand Up @@ -68,7 +87,9 @@ def test_capabilities_document_shape():
assert set(doc) == {"pi_core", "pi_agenta", "claude"}
assert doc["claude"]["providers"] == ["anthropic"]
assert doc["claude"]["model_selection"] == "alias"
assert doc["pi_core"]["providers"] == list(PI_VAULT_PROVIDERS)
assert doc["pi_core"]["providers"] == list(PI_VAULT_PROVIDERS) + list(
PI_SUBSCRIPTION_PROVIDERS
)
assert doc["pi_core"]["connection_modes"] == ["agenta", "self_managed"]
assert doc["pi_core"]["deployments"] == ["direct"]
assert doc["claude"]["deployments"] == [
Expand All @@ -88,17 +109,32 @@ def test_every_harness_publishes_a_models_map():


def test_pi_models_are_a_subset_of_the_shared_catalog():
# Each Pi harness publishes, per vault provider, exactly that provider's catalog ids.
# Each Pi harness publishes, per vault provider, exactly that provider's catalog ids, plus the
# subscription/OAuth providers' explicit ids (which the shared catalog does not list).
for harness in ("pi_core", "pi_agenta"):
models = HARNESS_CONNECTION_CAPABILITIES[harness].models
# Only the vault-mapped providers are published (no arbitrary catalog providers).
assert set(models) <= set(PI_VAULT_PROVIDERS)
assert set(models) == set(PI_VAULT_PROVIDERS)
for provider, ids in models.items():
# The published providers are the vault-mapped ones plus the subscription providers.
assert set(models) == set(PI_VAULT_PROVIDERS) | set(PI_SUBSCRIPTION_PROVIDERS)
for provider in PI_VAULT_PROVIDERS:
# The published ids are exactly the shared catalog's ids for that provider
# (verbatim — most are provider-prefixed like ``anthropic/...``, but some
# providers, e.g. openai, list bare ids like ``gpt-5.5``).
assert ids == list(supported_llm_models[provider])
assert models[provider] == list(supported_llm_models[provider])
for provider, ids in PI_SUBSCRIPTION_MODELS.items():
# The subscription providers carry their explicit ids and are NOT in the shared
# catalog (they authenticate via OAuth, not a vault provider_key).
assert models[provider] == list(ids)
assert provider not in supported_llm_models


def test_pi_publishes_concrete_gpt_5_6_models_for_both_openai_providers():
expected = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]

for harness in ("pi_core", "pi_agenta"):
models = HARNESS_CONNECTION_CAPABILITIES[harness].models
for provider in ("openai", "openai-codex"):
assert models[provider][:3] == expected
assert "gpt-5.6" not in models[provider]


def test_claude_models_are_the_alias_set_under_anthropic():
Expand Down
43 changes: 43 additions & 0 deletions services/oss/tests/pytest/unit/agent/test_invoke_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,46 @@ async def test_agent_flag_cube(multi_message_patched, stream, trim, force):
assert result["messages"] == _TRIMMED_MESSAGES
else:
assert result["messages"] == _FULL_FOLDED_MESSAGES


async def test_pi_openai_codex_self_managed_reaches_session(monkeypatch, fake_backend):
"""Pi + the ``openai-codex`` subscription provider + ``self_managed`` is NOT rejected.

Before ``openai-codex`` joined Pi's capability providers, the agent-layer pre-resolve check
raised ``UnsupportedProviderError`` here even though the runner drives the ChatGPT/Codex
subscription fine — so the playground's self-managed (no-API-key) path was dead at the service
boundary. The ``self_managed`` connection resolves to a no-credential (``runtime_provided``)
plan: the harness authenticates with its own OAuth login and no key is injected.
"""
backend = fake_backend(result=AgentResult(output="echo", usage={"total": 1}))
captured = {}

async def _resolve(*, model, context):
captured["model"] = model
# Mirror the VaultConnectionResolver's self_managed short-circuit (no vault, empty env).
return ResolvedConnection(
provider="openai-codex",
model=model.model,
credential_mode="runtime_provided",
env={},
)

built = _patch_resolution(monkeypatch, backend, resolve=_resolve)

body = await _invoke(
"pi_core",
model={
"provider": "openai-codex",
"model": "gpt-5.4-mini",
"connection": {"mode": "self_managed"},
},
)

assert body == {"messages": [{"role": "assistant", "content": "echo"}]}
# The pre-resolve provider check let openai-codex through, so the resolver ran.
assert captured["model"].provider == "openai-codex"
assert captured["model"].connection.mode == "self_managed"
# No key injected: the harness uses its own subscription login.
assert backend.created_secrets == [{}]
assert built[0].secrets == {}
assert built[0].resolved_connection.credential_mode == "runtime_provided"
2 changes: 1 addition & 1 deletion services/runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"dependencies": {
"@daytonaio/sdk": "^0.187.0",
"@earendil-works/pi-coding-agent": "0.79.4",
"@earendil-works/pi-coding-agent": "0.80.6",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/core": "1.28.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.54.0",
Expand Down
Loading
Loading