feat(agents): runner subscription status on the credential card - #5994
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (21)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe change adds authenticated runner subscription checks and self-managed credential-card status display. It also extends provider connections with stable slugs, harness policies, preserved model metadata, and harness-specific default models. ChangesSubscription status flow
Provider connection metadata
Estimated code review effort: 5 (Critical) | ~120 minutes Mergeability Score: 🟡 Moderate · up to The change expands provider-secret creation and resolution behavior, but concurrent creations can still produce duplicate automatic names and description-only records can lack a usable identity. These bounded correctness issues should be fixed before merge; documentation, comment cleanup, and SDK verification remain follow-up items. Sequence Diagram(s)sequenceDiagram
participant ProviderCredentialsSection
participant WorkflowStatusQuery
participant AgentService
participant Runner
ProviderCredentialsSection->>WorkflowStatusQuery: request selected harness status
WorkflowStatusQuery->>AgentService: POST /runtime/subscription-status
AgentService->>Runner: GET /subscription-status with shared token
Runner-->>AgentService: redacted harness states
AgentService-->>WorkflowStatusQuery: normalized response
WorkflowStatusQuery-->>ProviderCredentialsSection: display state and refresh status
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
mmabrouk
left a comment
There was a problem hiding this comment.
Inline annotations for review. Each comment marks a decision point and where to revert it.
| /** This runner version cannot check this harness. */ | ||
| | "unsupported"; | ||
|
|
||
| export const SUBSCRIPTION_HARNESSES = [ |
There was a problem hiding this comment.
The two Pi harnesses share one probe (same mount, same auth.json) so they cannot drift apart. The review pass added pi_agenta here: without it, a current runner was reported as needing an update when the user picked Pi (Agenta).
| * The reads are async so a hung or slow mount (a stalled network filesystem) blocks only this | ||
| * request, never the runner's event loop and the runs sharing it. | ||
| */ | ||
| async function probeState( |
There was a problem hiding this comment.
State ladder: unset/blank env -> not_configured; ENOENT/ENOTDIR -> login_missing; any other stat/read failure, empty file, or non-object JSON -> login_unusable; else ready. The shape check is deliberately shallow so the runner is not coupled to each harness's private credential format. Reads are async so a hung network mount cannot block the event loop that serves live runs.
| harnesses: Dict[str, Any] = Field(default_factory=dict) | ||
|
|
||
|
|
||
| def _normalized_harnesses(raw: Dict[str, Any]) -> Dict[str, HarnessStatus]: |
There was a problem hiding this comment.
Boundary hardening from review: the state vocabulary is enforced here (unknown word -> unsupported for that harness only), and one malformed harness entry cannot condemn the whole body to incompatible. Any unexpected exception on the runner hop maps to the operational 'unavailable' result instead of a 500, so the card can always render a setup state.
| queryFn: () => fetchSubscriptionStatus({harness, projectId: projectId as string}), | ||
| enabled: !!harness && !!projectId, | ||
| staleTime: 10_000, | ||
| refetchInterval: 15_000, |
There was a problem hiding this comment.
Query rules from the plan: 10s stale, 15s poll while the card is visible, refetch on window focus, no browser persistence (deliberately unlike harnessCatalogQueryAtom, which persists). Fetches only in self-managed mode. Live QA measured the poll at 15.03s and confirmed zero requests in API-key mode.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
services/runner/src/subscription-status.ts (2)
145-158: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueGuard the probe lookup against inherited object keys.
PROBES[harness as SubscriptionHarness]resolves inheritedObject.prototypemembers. A call such asharnessSubscriptionStatus("toString")returns a truthy function instead ofundefined, so the code skips theunsupportedbranch and reportsnot_configured(becauseprobe.dirEnvisundefined). The current server route only passesSUBSCRIPTION_HARNESSES, so this is not reachable today. The exported function is a public seam, so an own-key check keeps it correct for future callers.♻️ Proposed own-key guard
- const probe = PROBES[harness as SubscriptionHarness]; - if (!probe) return { state: "unsupported" }; + if (!Object.hasOwn(PROBES, harness)) return { state: "unsupported" }; + const probe = PROBES[harness as SubscriptionHarness];
161-172: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider probing harnesses in parallel.
The loop awaits each probe in sequence. Four probes on a slow or hung mount add their latencies together.
Promise.allbounds the response time to the slowest probe. The agent caller uses a 3-second timeout, so the sequential sum can turn a partly slow mount intounavailable.♻️ Proposed parallel probe
- for (const harness of SUBSCRIPTION_HARNESSES) { - harnesses[harness] = await harnessSubscriptionStatus(harness, env); - } + const results = await Promise.all( + SUBSCRIPTION_HARNESSES.map((harness) => + harnessSubscriptionStatus(harness, env), + ), + ); + SUBSCRIPTION_HARNESSES.forEach((harness, index) => { + harnesses[harness] = results[index]; + });services/oss/src/agent/runtime_status.py (1)
101-117: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueConstrain harness names, not only harness states.
The function validates each entry value and closes the state vocabulary. It copies every dict key from the runner through unchanged. A newer or misbehaving runner can therefore place arbitrary key strings in a browser-facing response. The values are already constrained, so the residual risk is low, and the UI reads only known harness keys. An explicit key allow-list would make the boundary complete and match the module docstring.
♻️ Proposed key allow-list
+KNOWN_HARNESSES = frozenset({"codex", "claude", "pi_core", "pi_agenta"}) + def _normalized_harnesses(raw: Dict[str, Any]) -> Dict[str, HarnessStatus]: harnesses: Dict[str, HarnessStatus] = {} for name, entry in raw.items(): + if name not in KNOWN_HARNESSES: + continue try:Note: a strict allow-list means a harness added in a newer runner is dropped until this service learns it. If you prefer forward compatibility, bound the key instead (length plus a
[a-z0-9_]+pattern).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 2022015f-5ee9-4e69-85de-bfb42f83bd55
📒 Files selected for processing (19)
docs/docs/self-host/agents/01-use-your-own-subscription.mdxservices/oss/src/agent/app.pyservices/oss/src/agent/config.pyservices/oss/src/agent/runtime_status.pyservices/oss/tests/pytest/unit/agent/test_subscription_status.pyservices/runner/src/server.tsservices/runner/src/subscription-status.tsservices/runner/tests/unit/server.test.tsservices/runner/tests/unit/subscription-status.test.tsweb/packages/agenta-entities/src/workflow/api/index.tsweb/packages/agenta-entities/src/workflow/api/subscriptionStatus.tsweb/packages/agenta-entities/src/workflow/index.tsweb/packages/agenta-entities/src/workflow/state/index.tsweb/packages/agenta-entities/src/workflow/state/subscriptionStatus.tsweb/packages/agenta-entities/tests/unit/subscriptionStatusApi.test.tsweb/packages/agenta-entities/tests/unit/subscriptionStatusDisplay.test.tsweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ProviderCredentialsSection.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ProviderCredentialsSectionView.tsxweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx
Railway Preview Environment
|
6ac05c4 to
59ab9a3
Compare
|
@coderabbitai review |
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx (1)
37-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep the source comment to one short line.
Lines 37-38 add a two-line source comment. Replace it with one short line or remove it.
As per coding guidelines: “Keep in-code comments to at most one short line.”
Source: Coding guidelines
web/packages/agenta-entities/src/secret/core/types.ts (1)
38-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReduce the new in-code comments.
Move detailed policy rationale to design documentation. Keep each in-code comment to one short line.
web/packages/agenta-entities/src/secret/core/types.ts#L38-L46: reduce the DTO migration comment.web/packages/agenta-entities/src/secret/core/transforms.ts#L88-L89: reduce the absent-versus-empty policy comment.web/packages/agenta-entities/src/secret/core/transforms.ts#L140-L147: reduce the payload-transform JSDoc.web/packages/agenta-entities/src/secret/state/atoms.ts#L146-L147: reduce the round-trip policy comment.As per coding guidelines: “Keep in-code comments to at most one short line; use longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 24d67cbe-371a-4ba4-b8bc-8ddc9482c058
📒 Files selected for processing (21)
api/oss/src/core/secrets/dtos.pyapi/oss/src/core/secrets/enums.pyapi/oss/src/core/secrets/services.pyapi/oss/tests/legacy/vault_router/test_vault_secrets_apis.pyapi/oss/tests/pytest/unit/secrets/test_dtos.pyapi/oss/tests/pytest/unit/secrets/test_services.pydocs/design/agent-workflows/interfaces/cross-service/service-to-vault-and-tool-providers.mddocs/design/agent-workflows/interfaces/in-service/model-connection-resolution.mddocs/design/agent-workflows/interfaces/public-edge/workflow-inspect.mdsdks/python/agenta/sdk/agents/capabilities.pysdks/python/agenta/sdk/agents/platform/connections.pysdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.pysdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.pyweb/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsxweb/packages/agenta-entities/src/secret/core/index.tsweb/packages/agenta-entities/src/secret/core/transforms.tsweb/packages/agenta-entities/src/secret/core/types.tsweb/packages/agenta-entities/src/secret/state/atoms.tsweb/packages/agenta-entities/src/workflow/state/inspectMeta.tsweb/packages/agenta-entities/tests/unit/secret-transforms.test.tsweb/packages/agenta-shared/src/types/llmProvider.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 3
🧹 Nitpick comments (2)
web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx (1)
37-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep the source comment to one short line.
Lines 37-38 add a two-line source comment. Replace it with one short line or remove it.
As per coding guidelines: “Keep in-code comments to at most one short line.”
Source: Coding guidelines
web/packages/agenta-entities/src/secret/core/types.ts (1)
38-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReduce the new in-code comments.
Move detailed policy rationale to design documentation. Keep each in-code comment to one short line.
web/packages/agenta-entities/src/secret/core/types.ts#L38-L46: reduce the DTO migration comment.web/packages/agenta-entities/src/secret/core/transforms.ts#L88-L89: reduce the absent-versus-empty policy comment.web/packages/agenta-entities/src/secret/core/transforms.ts#L140-L147: reduce the payload-transform JSDoc.web/packages/agenta-entities/src/secret/state/atoms.ts#L146-L147: reduce the round-trip policy comment.As per coding guidelines: “Keep in-code comments to at most one short line; use longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 24d67cbe-371a-4ba4-b8bc-8ddc9482c058
📒 Files selected for processing (21)
api/oss/src/core/secrets/dtos.pyapi/oss/src/core/secrets/enums.pyapi/oss/src/core/secrets/services.pyapi/oss/tests/legacy/vault_router/test_vault_secrets_apis.pyapi/oss/tests/pytest/unit/secrets/test_dtos.pyapi/oss/tests/pytest/unit/secrets/test_services.pydocs/design/agent-workflows/interfaces/cross-service/service-to-vault-and-tool-providers.mddocs/design/agent-workflows/interfaces/in-service/model-connection-resolution.mddocs/design/agent-workflows/interfaces/public-edge/workflow-inspect.mdsdks/python/agenta/sdk/agents/capabilities.pysdks/python/agenta/sdk/agents/platform/connections.pysdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.pysdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.pyweb/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsxweb/packages/agenta-entities/src/secret/core/index.tsweb/packages/agenta-entities/src/secret/core/transforms.tsweb/packages/agenta-entities/src/secret/core/types.tsweb/packages/agenta-entities/src/secret/state/atoms.tsweb/packages/agenta-entities/src/workflow/state/inspectMeta.tsweb/packages/agenta-entities/tests/unit/secret-transforms.test.tsweb/packages/agenta-shared/src/types/llmProvider.ts
🛑 Comments failed to post (3)
api/oss/src/core/secrets/dtos.py (1)
232-236: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Require a non-empty
header.namefor non-provider secrets.A description-only header passes
any(header.values()).VaultService.create_secretderives the stable slug forcustom_secretandcustom_provideronly fromheader.name. The accepted record can therefore have neither a slug nor a usable name fallback.Validate a non-blank
header["name"]. Add a regression case with a description-only header.Proposed fix
if isinstance(header, BaseModel): header = header.model_dump() - if not isinstance(header, dict) or not any(header.values()): + name = header.get("name") if isinstance(header, dict) else None + if not isinstance(name, str) or not name.strip(): raise ValueError("Header cannot be empty.")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.header = values.get("header") if isinstance(header, BaseModel): header = header.model_dump() name = header.get("name") if isinstance(header, dict) else None if not isinstance(name, str) or not name.strip(): raise ValueError("Header cannot be empty.")api/oss/src/core/secrets/services.py (1)
118-137: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Make automatic provider naming atomic.
Two concurrent creates can both list the same records and select
"OpenAI". The distinct random slugs do not prevent the duplicate display name. If storage enforces name uniqueness, one request fails instead.Allocate the name and persist the secret in one database transaction with a suitable uniqueness constraint or retry path.
docs/design/agent-workflows/interfaces/in-service/model-connection-resolution.md (1)
38-42: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the legacy
header.nameidentity statement.A legacy
custom_providerwithout a stored slug resolves byheader.name. In that case, the header name is the identity fallback. State thatheader.nameis display-only only when a stored slug exists.
|
Re the CodeRabbit finding on the raw axios call in subscriptionStatusApi (fingerprint 914cc418beb279cf1d740142): the guideline applies to main-API calls, and this endpoint lives on the Python agent service, which the Fern client (generated from the main API's spec) cannot reach today; the call deliberately follows the same direct-service pattern as /services/agent/v0/invoke. The underlying debt is real though, and it now has a tracked home covering the whole batch: #6012 regenerates the Fern clients (picking up POST /providers/probe and the new secrets fields) and settles the pattern for service-hosted endpoints, either typed accessors from the services' own spec or a documented sanctioned exception, then migrates this call accordingly. |
mmabrouk
left a comment
There was a problem hiding this comment.
I found one correctness issue in the sanitizing boundary.
| continue | ||
| if status.state not in HARNESS_STATES: | ||
| status = status.model_copy(update={"state": UNSUPPORTED}) | ||
| harnesses[name] = status |
There was a problem hiding this comment.
The proxy still forwards runner-controlled strings through both the harness-map key and provider. A future or faulty runner could put an account name, path, or credential in either field, despite this endpoint's contract that only fixed state words and constant provider names reach the browser. Please restrict harness keys to the known catalog and derive or allow-list provider values at this boundary.
There was a problem hiding this comment.
You're right on both, and they were the two runner-controlled strings still passing through untouched. Fixed in c242840.
The harness map key. _normalized_harnesses iterated raw.items() and used name directly as the response key, so whatever the runner put there became an object key in the browser's JSON. Now allow-listed against a closed set built from HarnessKind (codex, claude, pi_core, pi_agenta); an unknown key is dropped like an unreadable entry.
Closing the key set also caps the map, so the separate size cap you suggested isn't needed — a runner cannot produce more entries than there are known harnesses no matter what it sends. There's a test that pushes 5000 entries and asserts the result is empty and bounded by len(KNOWN_HARNESSES).
The singular provider. It was a bare Optional[str] while providers had _known_families. It now gets the same validator against PROVIDER_FAMILIES, and drops to None on anything else rather than failing the entry — same reasoning as the list: an unrenderable family is the runner saying more than the card can read, and the state word is still good.
Blast radius: none for real runners. The runner's own SUBSCRIPTION_HARNESSES in services/runner/src/subscription-status.ts is exactly ["codex", "claude", "pi_core", "pi_agenta"], and its provider values are the harness constants "openai"/"anthropic", so both sets already match what it sends. This narrows the contract without changing any response a current runner produces.
Tests (services/oss/tests/pytest/unit/agent/test_subscription_status.py): the provider case is parametrized over a path, a sk-proj-… credential, an account-shaped string, and non-strings; the key case covers a newer runner's unknown harness plus the same three leakage shapes; plus the map-size cap. 57 pass.
POST /providers/probe validates a credential and discovers models in one action while keeping the two results separate: a public catalog can refresh models but can never produce a key-valid claim, and a missing list endpoint is unsupported, not invalid. Adapters cover the standard API-key providers, OpenRouter's authenticated key check, Azure and Bedrock bearer paths, and OpenAI-compatible endpoints behind the shared egress guard with DNS pinning. Credentials are never persisted or logged; the vault's SecretSafeRoute keeps them out of 422 bodies.
… card The Settings tab becomes AI providers: a table of connections (provider, masked credential, active models, connected date) whose rows open the connection card directly, plus an Add provider button that opens the catalog drawer. The catalog lists every standard provider except the defunct Aleph Alpha, adds AWS Bedrock, Azure OpenAI, and Google Vertex AI as first-class rows with their own credential field sets, and ends with the OpenAI-compatible endpoint row. The card runs one Test action against the probe API and keeps its two results honest, pre-checks the default models and tags them, always accepts manual model IDs, saves a policy only when the user actually chose one, and never shows a modal or toast. The old Secrets settings surface is deleted; its custom provider form lives on for other consumers.
…onnection SecretsManager resolves a stored connection slug first, of either vault kind, and raises a clean 400 for an unknown slug instead of silently running on another key. Without a slug the old family mapping still applies, extended so a record whose saved model list contains the requested model wins over the bare family match. The vault middleware stops collapsing same-family records, which had made a second OpenAI connection unreachable on every prompt path. The prompt and judge model pickers show one group per named connection, suppress the ambiguous static family group when two or more connections claim it, and persist the slug beside the model in one dispatch. The prompt-side model catalog gains the current identifiers it was missing.
The picker's first level lists the project's connections and the two consumer subscriptions instead of provider families; the flyout lists each connection's models crossed with the harnesses that may drive them, with the harness as a tag and a cost hint when a model is reachable twice. A pick writes model, provider, connection slug, and harness in one dispatch. The drawer's playground context adds the Connected and Subscriptions sections around the shared catalog, and the closed pill goes dashed with Set up AI providers when nothing is connected. Custom connections are matched by their stored slug with a name fallback for legacy records, and the chat composer's /model picker shares the same rows.
One family-to-prefix table, mirrored in TypeScript and Python with cross-referencing drift tests, turns provider-native model ids into the spelling litellm routes. Pick-time translation keeps stored configs in litellm format, so pre-existing prompts are untouched by construction; the resolver keeps an idempotent safety net on both context paths and custom-provider records are structurally exempt, pinned by mutation- checked tests. The saved-models tiebreak compares normalized spellings so an explicit claim beats first-record. Unknown connection slugs fail as a clean 400 naming the candidates, never by running on another key.
From accepted review findings: ProviderCredentials.key becomes SecretStr and extras drops out of repr, so a stray log line or traceback cannot print a key; adapters unwrap at the single point of use and canary tests pin repr, str, and both dump forms. The llm_v0 fallback path stops pre-binding one key per family and resolves each entry through the shared slug-first resolver, so an entry naming a specific connection runs on that connection's key across the whole retry chain.
Three entry contexts with their exact titles and section sets, the catalog as the only scrolling region, folded connected rows with one composed subtitle, subscription rows as one pair per plan and harness derived from the runner's live status, the setup row and empty state verbatim from the spec, the pair card, and the connection card per the handoff: one secret line, green-dot result with fetch count, recommended tags, manual IDs always, drawer-body scrolling with the close control hard right and a truncation guard on long names. Saved policy fields are sent only when the user actually chose them.
One cascade component: search on top, connections left with counts and olive Subscription tags on rows named by plan, flyouts right with curated names, quiet default and cheapest hints, harness sections with logos and a via-header for single-harness connections, the ink harness tooltip, and one Manage model providers footer. The completion context strips every trace of harness. Uncataloged saved ids appear in the harness's own spelling, labelled exactly as the user typed them, and the closed pill resolves the same beautified label as the menu. Subscription rows consume the runner's live pairs with a static fallback, sharing one status query with the drawer. The tooltip link color becomes a real palette token.
The Fallback and Retry titles inherited the 16px body over 12px labels; they join the panel's own scale, and the header aligns with its tabs. One shared function now computes a no-list connection's effective models, so the settings table, the drawer count, and both pickers cannot disagree; the playground model row renders the resolved label instead of the raw stored id, with manual ids shown exactly as typed.
A hand-added model id absent from Pi's registry is registered into the per-run models.json with its base model's real metadata, so it runs as a first-class model; the operator's mounted Pi folder is never written. The Pi probe also names which provider logins the mount holds (OAuth entries only, family names only), the service passes them through a closed allow-list, and subscription pairs derive from the live answer, which is how a ChatGPT login in Pi becomes a visible pair.
The connect gate opens the providers drawer directly, and a connection created from onboarding auto-switches the agent to its first model and commits, leaving the user typing; a second connection added mid-session never touches the current pick. The dead duplicate gate hook is deleted rather than synced. The settings table matches its siblings (Provider and Name columns, Created, an actions menu), Preferences reads Feature flags with a Developer mode toggle describing exactly what it gates, the eval stepper's all-rows hover highlight is fixed at its CSS root, and the dev api gains the graceful-shutdown bound that ends the reload deadlock.
The onboarding auto-switch pulses the config section through the shared draft signal; the new origin keeps it out of the approval dock's Undo.
The runner gains an authenticated GET /subscription-status that inspects the mounted login location per harness (codex, claude, pi_core, pi_agenta) and returns only state words. The Python agent service proxies it at POST /runtime/subscription-status behind the same auth as invoke, mapping network failure to unavailable and an old runner to incompatible. The self-managed credential card shows the status for the selected harness, polls while visible, and offers Check again. The status never claims provider access; a run remains the proof.
… and provider The public status proxy allow-listed state words and the `providers` list, but two runner-controlled strings still passed through untouched: the harness map's KEY, which becomes an object key in the browser's JSON, and the singular `provider`. A future or faulty runner could put an account name, a path, or a credential in either. Both are now closed sets. Keys must name a known harness (`HarnessKind`) and unknown ones are dropped, which also caps the map — a runner cannot push more entries than there are harnesses. `provider` gets the same treatment `providers` already had: a family the card cannot render is dropped rather than failing the entry, since the state word is still good. Both sets already match what the runner sends (its own SUBSCRIPTION_HARNESSES and its constant provider values), so this narrows the contract without changing any real response.
…el spelling Five review findings on the provider-connections surface: - The vault shadow filter compared provider kinds raw, so MISTRALAI_API_KEY survived next to a stored `mistral` connection and — locals coming first in the combined list — then won the resolver's tiebreak, beating the key the user saved. Both sides now normalize through a shared `normalize_provider_kind`, moved to `sdk/utils/providers.py` because the manager already imports the vault middleware and could not be imported back. - The family tiebreak fell through to the first record, which could be one the user narrowed away from the requested model while a later list-less record still offered it. A record with no saved list follows Agenta's defaults, so it now wins over a narrowed one; an explicit `[]` still means "offer nothing" and wins nothing. - The probe applied its 10s budget per httpx request, and OpenRouter issues two, so the worst case behind the button was double. The budget now covers the whole probe; running out of it maps to `unknown` + `failed`, which is what it honestly proved. - The playground picker offered a credential-set connection its bare saved slugs when it carried both spellings, but the SDK matches those connections on `model_keys` only, so the pick resolved to no provider settings. - `together_ai/moonshotai/Kimi-K2-Instruct` is gone from Together's serverless API and from the agent catalog; replaced with `Kimi-K2.6`, which the agent catalog carries. Plus a doc comment that stated the opposite of the save rule, and a catalog call moved inside its own try so an exception degrades to an empty set instead of failing module collection.
59ab9a3 to
8f546a0
Compare
feat(web): the AI providers experience - settings, picker, and run-path wiring
4da9581
into
feat/provider-connections-api
Implements the plan in #5985 (docs: plan runner subscription status path).
Context
Users can select a model subscription in the playground before Agenta knows whether the runner is
active or can read the mounted login. Setup mistakes surface only after a run fails. This change
shows the login status before the run, without weakening the security boundary: the browser never
contacts the runner, and no token, path, account detail, or file content ever crosses to the
frontend.
What changed
services/runner): new authenticatedGET /subscription-status. A probe perharness (
codex,claude,pi_core,pi_agenta; the two Pi harnesses share one probe) checksthe mount variable and login file and returns one of
ready | not_configured | login_missing | login_unusable. File reads are async so a hung mount cannot block the event loop. The responsecontains only state words and constant provider names.
/healthstays the only unauthenticatedroute.
services/oss/src/agent): newPOST /runtime/subscription-statusbehind thesame auth middleware as
/invoke, project-scoped. Maps runner reachability toconnected | unavailable | incompatible(404/405 or a bad body means an old runner), normalizesunknown states to
unsupportedper harness, returns HTTP 200 for all three operational states,and rejects any extra request field so a caller can never name a runner URL.
@agenta/entitiesworkflow domain +@agenta/entity-uicredential card): a queryatom (10s stale, 15s poll while visible, refetch on focus, no persistence) fetches status only in
self-managed mode. The card renders the exact message table from the plan, keeps the setup-guide
link in every state, and adds Check again. Unknown states and missing harness keys degrade to
"Update the runner to check subscription status."
that the check proves file presence, not provider access.
Scope / risk
read-only and additive.
AGENTA_RUNNER_INTERNAL_URL(local source checkout) reports "Runner is notconnected" although CLI-spawned runs still work; this is the spec'd v1 behavior and is noted for
a follow-up.
badge renders on every EE build (
isDemo()isisEE()), so a self-hosted EE deployment can showa green "Subscription login found" beside it. Fixing that needs a real cloud-vs-self-hosted
signal; left for a product decision.
How to QA
Prerequisites: a dev stack from this tree (
run.sh --oss|--ee --dev), a mounted Pi or Claudelogin folder on the runner container (the dev harness compose mounts Pi), a logged-in user.
runnerandservicescontainers so the watchers pick up the new modules.GET http://runner:8765/subscription-statuswithout a tokenexpects 401; with
Authorization: Bearer $AGENTA_RUNNER_TOKENexpects 200 with only statewords (no paths or tokens anywhere in the body).
Expected: the card shows the real state per harness (for example Pi "Subscription login
found", Codex "Runner found. Subscription folder is not configured." when
CODEX_HOMEis notmounted), and Check again fires a request to
/services/agent/v0/runtime/subscription-status?project_id=….poll (15s), and heals after
docker start.broken harness not affecting the others, and redaction (a fake credential written to the login
file never appears on the wire).
Test commands:
cd services/runner && pnpm test:unit(2152 tests)cd services && uv run pytest oss/tests/pytest/unit/agent/ -q(141 tests)cd web && pnpm --filter @agenta/entities run test:unit(1001 tests)Live QA on the dev stack (port 8180) covered all five steps above, both themes, WCAG AA contrast
on the status colors, and a full agent run regression (run completed; picker, API-key card, and
draft cancel unchanged).