Skip to content

feat(agents): runner subscription status on the credential card - #5994

Merged
mmabrouk merged 16 commits into
feat/provider-connections-apifrom
feat/runner-subscription-status
Aug 13, 2026
Merged

feat(agents): runner subscription status on the credential card#5994
mmabrouk merged 16 commits into
feat/provider-connections-apifrom
feat/runner-subscription-status

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

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.

Mounted login folder -> runner (local file check) -> agent service (sanitized proxy)
  -> authenticated frontend query -> credential card message

What changed

  • Runner (services/runner): new authenticated GET /subscription-status. A probe per
    harness (codex, claude, pi_core, pi_agenta; the two Pi harnesses share one probe) checks
    the 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 response
    contains only state words and constant provider names. /health stays the only unauthenticated
    route.
  • Agent service (services/oss/src/agent): new POST /runtime/subscription-status behind the
    same auth middleware as /invoke, project-scoped. Maps runner reachability to
    connected | unavailable | incompatible (404/405 or a bad body means an old runner), normalizes
    unknown states to unsupported per harness, returns HTTP 200 for all three operational states,
    and rejects any extra request field so a caller can never name a runner URL.
  • Frontend (@agenta/entities workflow domain + @agenta/entity-ui credential card): a query
    atom (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."
  • Docs: the self-host subscription guide now documents each card message and states plainly
    that the check proves file presence, not provider access.

Scope / risk

  • No change to run execution, model resolution, or the harness catalog. The status path is
    read-only and additive.
  • An old runner without the endpoint shows "Update the runner…" and nothing else breaks.
  • A deployment without AGENTA_RUNNER_INTERNAL_URL (local source checkout) reports "Runner is not
    connected" although CLI-spawned runs still work; this is the spec'd v1 behavior and is noted for
    a follow-up.
  • Pre-existing issue surfaced, not caused, by this change: the red "Unavailable in the cloud"
    badge renders on every EE build (isDemo() is isEE()), so a self-hosted EE deployment can show
    a 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 Claude
login folder on the runner container (the dev harness compose mounts Pi), a logged-in user.

  1. Restart the runner and services containers so the watchers pick up the new modules.
  2. Inside the compose network: GET http://runner:8765/subscription-status without a token
    expects 401; with Authorization: Bearer $AGENTA_RUNNER_TOKEN expects 200 with only state
    words (no paths or tokens anywhere in the body).
  3. In the playground, open an agent, choose a model, switch the credential mode to Subscription.
    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_HOME is not
    mounted), and Check again fires a request to
    /services/agent/v0/runtime/subscription-status?project_id=….
  4. Stop the runner container. Expected: the card flips to "Runner is not connected." within one
    poll (15s), and heals after docker start.
  5. Switch to the API-key mode. Expected: no status line and zero status requests.
  6. Edge cases covered by tests: wrong token, malformed runner body, unknown state words, one
    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).

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Error Error Aug 13, 2026 3:47pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • release/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b4154f76-a910-4885-b26b-7df4e5550325

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
ℹ️ Recent 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac05c4 and 59ab9a3.

📒 Files selected for processing (21)
  • api/oss/src/core/secrets/dtos.py
  • api/oss/src/core/secrets/enums.py
  • api/oss/src/core/secrets/services.py
  • api/oss/tests/legacy/vault_router/test_vault_secrets_apis.py
  • api/oss/tests/pytest/unit/secrets/test_dtos.py
  • api/oss/tests/pytest/unit/secrets/test_services.py
  • docs/design/agent-workflows/interfaces/cross-service/service-to-vault-and-tool-providers.md
  • docs/design/agent-workflows/interfaces/in-service/model-connection-resolution.md
  • docs/design/agent-workflows/interfaces/public-edge/workflow-inspect.md
  • sdks/python/agenta/sdk/agents/capabilities.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx
  • web/packages/agenta-entities/src/secret/core/index.ts
  • web/packages/agenta-entities/src/secret/core/transforms.ts
  • web/packages/agenta-entities/src/secret/core/types.ts
  • web/packages/agenta-entities/src/secret/state/atoms.ts
  • web/packages/agenta-entities/src/workflow/state/inspectMeta.ts
  • web/packages/agenta-entities/tests/unit/secret-transforms.test.ts
  • web/packages/agenta-shared/src/types/llmProvider.ts

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added live subscription credential status checks for self-managed deployments.
    • Status is shown per selected harness, including ready, missing, unreadable, unsupported, unavailable, and incompatible states.
    • Added automatic refresh and a “Check again” action with loading feedback.
    • Preserved provider connection names, models, harness settings, and stable connection identifiers.
    • Added curated default models when connections do not specify model lists.
    • Status checks avoid exposing credentials or local filesystem details.
  • Documentation
    • Documented status messages, refresh behavior, and clarified that checks detect local credentials without verifying provider subscription activity.

Walkthrough

The 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.

Changes

Subscription status flow

Layer / File(s) Summary
Runner credential detection
services/runner/src/subscription-status.ts, services/runner/src/server.ts, services/runner/tests/unit/*
The runner checks mounted Codex, Claude, and Pi login files. It returns versioned, redacted states through an authenticated endpoint.
Agent runner proxy
services/oss/src/agent/runtime_status.py, services/oss/src/agent/config.py, services/oss/src/agent/app.py, services/oss/tests/...
The agent authenticates with the runner, validates and normalizes responses, maps failures to public states, and exposes POST /runtime/subscription-status.
Workflow API and live state
web/packages/agenta-entities/src/workflow/api/..., web/packages/agenta-entities/src/workflow/state/..., web/packages/agenta-entities/tests/unit/*
The web API validates responses. Query atoms fetch live self-managed status and resolve runner and harness states into display data.
Provider credentials display
web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/*
The credentials card receives the selected harness, renders status messages, and provides a loading-aware “Check again” action.
Self-hosted documentation
docs/docs/self-host/agents/01-use-your-own-subscription.mdx
The documentation describes login-file checks, status messages, refresh behavior, and the limitation that local detection does not confirm provider subscription activity.

Provider connection metadata

Layer / File(s) Summary
Secret contracts and persistence
api/oss/src/core/secrets/*, web/packages/agenta-entities/src/secret/*, web/packages/agenta-shared/src/types/llmProvider.ts
Provider records and payload transforms preserve optional models and harnesses. Provider-key records receive display names and stable slugs.
Connection resolution
sdks/python/agenta/sdk/agents/platform/connections.py, sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py, docs/design/agent-workflows/interfaces/*
Connection candidates use stored slugs when available, retain legacy fallbacks, and carry saved model and harness policies.
Harness default models
sdks/python/agenta/sdk/agents/capabilities.py, sdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.py, web/packages/agenta-entities/src/workflow/state/inspectMeta.ts
Capabilities derive curated provider defaults from accepted model IDs and catalogs. Inspect metadata exposes the resulting default_models map.

Estimated code review effort: 5 (Critical) | ~120 minutes

Mergeability Score: 🟡 Moderate · up to 59ab9

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
Loading

Possibly related PRs

  • Agenta-AI/agenta#5995: Directly relates to the provider connection models, harnesses, stable slugs, DTOs, services, and resolution changes.
  • Agenta-AI/agenta#5298: Relates to runner authentication and subscription credential mounts.
  • Agenta-AI/agenta#5096: Relates to the self-managed provider-credentials and subscription-status flow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.24% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: adding runner subscription status to the credential card.
Description check ✅ Passed The description directly explains the runner, agent, frontend, documentation, security, scope, and testing changes.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/runner-subscription-status

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = [

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mmabrouk
mmabrouk marked this pull request as ready for review August 12, 2026 18:06
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. backend feature frontend labels Aug 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
services/runner/src/subscription-status.ts (2)

145-158: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Guard the probe lookup against inherited object keys.

PROBES[harness as SubscriptionHarness] resolves inherited Object.prototype members. A call such as harnessSubscriptionStatus("toString") returns a truthy function instead of undefined, so the code skips the unsupported branch and reports not_configured (because probe.dirEnv is undefined). The current server route only passes SUBSCRIPTION_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 value

Consider probing harnesses in parallel.

The loop awaits each probe in sequence. Four probes on a slow or hung mount add their latencies together. Promise.all bounds 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 into unavailable.

♻️ 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 value

Constrain 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

📥 Commits

Reviewing files that changed from the base of the PR and between 728f39f and 6ac05c4.

📒 Files selected for processing (19)
  • docs/docs/self-host/agents/01-use-your-own-subscription.mdx
  • services/oss/src/agent/app.py
  • services/oss/src/agent/config.py
  • services/oss/src/agent/runtime_status.py
  • services/oss/tests/pytest/unit/agent/test_subscription_status.py
  • services/runner/src/server.ts
  • services/runner/src/subscription-status.ts
  • services/runner/tests/unit/server.test.ts
  • services/runner/tests/unit/subscription-status.test.ts
  • web/packages/agenta-entities/src/workflow/api/index.ts
  • web/packages/agenta-entities/src/workflow/api/subscriptionStatus.ts
  • web/packages/agenta-entities/src/workflow/index.ts
  • web/packages/agenta-entities/src/workflow/state/index.ts
  • web/packages/agenta-entities/src/workflow/state/subscriptionStatus.ts
  • web/packages/agenta-entities/tests/unit/subscriptionStatusApi.test.ts
  • web/packages/agenta-entities/tests/unit/subscriptionStatusDisplay.test.ts
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ProviderCredentialsSection.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ProviderCredentialsSectionView.tsx
  • web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx

Comment thread web/packages/agenta-entities/src/workflow/api/subscriptionStatus.ts
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-5994.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-5994-67cb6d3
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-13T15:58:24.219Z

@mmabrouk
mmabrouk force-pushed the feat/runner-subscription-status branch from 6ac05c4 to 59ab9a3 Compare August 12, 2026 21:28
@dosubot dosubot Bot removed the size:XL This PR changes 500-999 lines, ignoring generated files. label Aug 12, 2026
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Aug 12, 2026
@mmabrouk
mmabrouk changed the base branch from main to feat/provider-connections-api August 12, 2026 21:28
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Aug 12, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx (1)

37-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep 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 win

Reduce 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac05c4 and 59ab9a3.

📒 Files selected for processing (21)
  • api/oss/src/core/secrets/dtos.py
  • api/oss/src/core/secrets/enums.py
  • api/oss/src/core/secrets/services.py
  • api/oss/tests/legacy/vault_router/test_vault_secrets_apis.py
  • api/oss/tests/pytest/unit/secrets/test_dtos.py
  • api/oss/tests/pytest/unit/secrets/test_services.py
  • docs/design/agent-workflows/interfaces/cross-service/service-to-vault-and-tool-providers.md
  • docs/design/agent-workflows/interfaces/in-service/model-connection-resolution.md
  • docs/design/agent-workflows/interfaces/public-edge/workflow-inspect.md
  • sdks/python/agenta/sdk/agents/capabilities.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx
  • web/packages/agenta-entities/src/secret/core/index.ts
  • web/packages/agenta-entities/src/secret/core/transforms.ts
  • web/packages/agenta-entities/src/secret/core/types.ts
  • web/packages/agenta-entities/src/secret/state/atoms.ts
  • web/packages/agenta-entities/src/workflow/state/inspectMeta.ts
  • web/packages/agenta-entities/tests/unit/secret-transforms.test.ts
  • web/packages/agenta-shared/src/types/llmProvider.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Keep 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 win

Reduce 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac05c4 and 59ab9a3.

📒 Files selected for processing (21)
  • api/oss/src/core/secrets/dtos.py
  • api/oss/src/core/secrets/enums.py
  • api/oss/src/core/secrets/services.py
  • api/oss/tests/legacy/vault_router/test_vault_secrets_apis.py
  • api/oss/tests/pytest/unit/secrets/test_dtos.py
  • api/oss/tests/pytest/unit/secrets/test_services.py
  • docs/design/agent-workflows/interfaces/cross-service/service-to-vault-and-tool-providers.md
  • docs/design/agent-workflows/interfaces/in-service/model-connection-resolution.md
  • docs/design/agent-workflows/interfaces/public-edge/workflow-inspect.md
  • sdks/python/agenta/sdk/agents/capabilities.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_model_catalog.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx
  • web/packages/agenta-entities/src/secret/core/index.ts
  • web/packages/agenta-entities/src/secret/core/transforms.ts
  • web/packages/agenta-entities/src/secret/core/types.ts
  • web/packages/agenta-entities/src/secret/state/atoms.ts
  • web/packages/agenta-entities/src/workflow/state/inspectMeta.ts
  • web/packages/agenta-entities/tests/unit/secret-transforms.test.ts
  • web/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.name for non-provider secrets.

A description-only header passes any(header.values()). VaultService.create_secret derives the stable slug for custom_secret and custom_provider only from header.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.name identity statement.

A legacy custom_provider without a stored slug resolves by header.name. In that case, the header name is the identity fallback. State that header.name is display-only only when a stored slug exists.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mmabrouk mmabrouk added the lgtm This PR has been approved by a maintainer label Aug 13, 2026

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mmabrouk

Copy link
Copy Markdown
Member Author

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 mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
feat(web): the AI providers experience - settings, picker, and run-path wiring
@dosubot dosubot Bot removed the size:XL This PR changes 500-999 lines, ignoring generated files. label Aug 13, 2026
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Aug 13, 2026
@mmabrouk
mmabrouk merged commit 4da9581 into feat/provider-connections-api Aug 13, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend feature frontend lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant