Skip to content

refactor(runner): canonical runner configuration names and typed config#5294

Merged
mmabrouk merged 6 commits into
big-agentsfrom
refactor/runner-config-names
Jul 14, 2026
Merged

refactor(runner): canonical runner configuration names and typed config#5294
mmabrouk merged 6 commits into
big-agentsfrom
refactor/runner-config-names

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

What an operator sees

One AGENTA_RUNNER_* namespace for the agent runner, parsed and validated once before the HTTP server accepts a request. A bad configuration fails startup with a clear message instead of silently misbehaving mid-run, and the runner logs one redacted summary of what it resolved:

runner providers enabled=[local,daytona] default=local
runner daytona target=eu artifact=snapshot:agenta-agent-runner

This is the interface-freeze slice: the public names cannot change after release, so it lands first.

Rename map (old -> new)

Old New
SANDBOX_AGENT_PROVIDER AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER (+ new AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS)
SANDBOX_AGENT_LOG_LEVEL AGENTA_RUNNER_LOG_LEVEL
DAYTONA_API_KEY (runner) AGENTA_RUNNER_DAYTONA_API_KEY
DAYTONA_API_URL (runner) AGENTA_RUNNER_DAYTONA_API_URL
DAYTONA_TARGET (runner) AGENTA_RUNNER_DAYTONA_TARGET
DAYTONA_SNAPSHOT (runner) AGENTA_RUNNER_DAYTONA_SNAPSHOT
DAYTONA_IMAGE (runner) AGENTA_RUNNER_DAYTONA_IMAGE
DAYTONA_AUTOSTOP / DAYTONA_AUTODELETE AGENTA_RUNNER_DAYTONA_AUTOSTOP_MINUTES / _AUTODELETE_MINUTES

The code evaluator's bare DAYTONA_* namespace (api/services and its Helm/Railway wiring) is deliberately untouched -- it is a separate consumer and gets its own namespace in a later slice.

Removed (no aliases, no fallback)

  • DAYTONA_SNAPSHOT_AGENT -- the runner uses its pinned default snapshot.
  • AGENTA_AGENT_SANDBOX_PI_INSTALLED -- replaced by a probe: the runner checks for the pinned Pi executable in the sandbox and installs it if a custom image lacks it, failing the run loudly if the install fails.
  • AGENTA_SESSION_HARNESS_MOUNTS -- transcript mounts derive from the session contract.
  • AGENTA_SANDBOX_LOCAL_ALLOWED -- replaced by the shared enabled/default provider pair below.

One shared value for API and web

The API, OSS agent service, SDK, and web read the same AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS / AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER pair the runner reads, with the same parsing rules (re-implemented per language, unit-tested to agree on every input). The producer-side gate refuses a sandbox the deployment has not enabled before any run; the runner remains the final authority, so even a drifted API produces an honest "provider not enabled on this deployment" failure instead of a wrong run.

Validation (fails startup)

Unset enabled = local; an explicitly empty list is invalid; unknown and duplicate ids are invalid; the default must be enabled; Daytona enabled without a provisioning credential fails; snapshot and image are mutually exclusive; invalid lifecycle minutes (zero, negative, non-numeric, fractional) fail; empty strings become absent at the parse boundary (so a Compose ${VAR:-} never becomes an invalid empty value). The runner constructs its Daytona client explicitly from the typed config and bridges the credential into the ambient name the vendored SDK reads; the daemon force-blanks both names from every harness environment so user code never sees them.

Test coverage

  • New TypeScript parser unit tests (every rule above) and updated provider/daemon/daytona/session-pool/orchestration/lifecycle suites: pnpm test green except two pre-existing transcript-replay failures unrelated to this change. pnpm run typecheck clean.
  • New Python parser unit tests for the API reader mirroring the TypeScript cases, plus updated services and SDK gate tests (api 15, services 12, sdk 11 passing).

Hosting (Compose, Helm, Railway) is updated in this same lane.

https://claude.ai/code/session_01XhENr63WL9npkKrJGnzDc1

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jul 13, 2026
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 14, 2026 1:19am

Request Review

@dosubot dosubot Bot added the refactoring A code change that neither fixes a bug nor adds a feature label Jul 13, 2026
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces local-only sandbox permission flags with validated local/daytona provider registries, typed runner configuration, provider-aware backend gating, runner-scoped Daytona settings, and deterministic Daytona Pi provisioning.

Changes

Sandbox provider migration

Layer / File(s) Summary
Configuration contracts and parsing
api/oss/src/utils/env.py, sdks/python/agenta/sdk/agents/*, services/runner/src/config/*, services/runner/tests/unit/runner-config.test.ts
Provider lists, defaults, Daytona settings, server settings, and validation errors are parsed through shared configuration boundaries.
Deployment configuration wiring
hosting/docker-compose/**, hosting/kubernetes/helm/templates/*, hosting/railway/oss/scripts/configure.sh, web/entrypoint.sh
Deployment environments now emit AGENTA_RUNNER_* provider and Daytona variables instead of legacy names.
API and backend provider gating
services/oss/src/agent/*, sdks/python/oss/tests/pytest/unit/agents/*, services/oss/tests/pytest/unit/agent/*
Backend selection checks whether the requested sandbox provider is enabled and reports denied provider IDs.
Typed runner runtime integration
services/runner/src/server.ts, services/runner/src/config/*, services/runner/src/engines/sandbox_agent/*
Runner startup and sandbox creation consume typed configuration for provider selection, Daytona clients, lifecycle settings, callbacks, logging, and credential handling.
Daytona Pi asset repair
services/runner/src/engines/sandbox_agent/daytona.ts, services/runner/sandbox-images/daytona/*, services/runner/tests/unit/sandbox-agent-daytona.test.ts
Pi setup probes the pinned executable, links a PATH executable, installs the pinned package when required, and fails if it remains unavailable.
Runtime test and hermetic-environment updates
services/runner/tests/setup/*, services/runner/tests/unit/*
Tests reset cached configuration and cover provider gating, Daytona configuration, credential scrubbing, orchestration, lifecycle, and session resolution.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 63.64% which is sufficient. The required threshold is 60.00%.
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 summarizes the main change: canonical runner config names and typed configuration.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the configuration refactor.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/runner-config-names

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.

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jul 13, 2026
@mmabrouk
mmabrouk marked this pull request as draft July 13, 2026 22:57
@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 Author note: Ready for review. This is the interface-freeze slice: one AGENTA_RUNNER_* namespace, typed RunnerConfig with startup validation, the API/SDK/services/web sandbox gate reading the same shared value, and the compose/Helm/Railway rename (including a Railway configure.sh bug where the runner's Daytona vars were unset right after being set).

Tests: runner 1062 pass / 2 pre-existing transcript-replay failures (proven on HEAD, unrelated); api 15, services 12, sdk 11.

One known-pending item, does not affect this review: the Helm values.yaml + values.schema.json rename is written and helm lint/template-verified but not yet in the diff. It file-locks to the merged MCP contract lane (#5290) while that lane is still applied in the local workspace; it lands as a follow-up commit on this branch before merge. Everything else in the rename is here.

@mmabrouk
mmabrouk marked this pull request as ready for review July 13, 2026 23:35

@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 added 4 commits July 14, 2026 01:39
One parse-and-validate boundary for the runner's operator-facing configuration, plus the final public AGENTA_RUNNER_* names everywhere the runner and API read them.

New services/runner/src/config/runner-config.ts: typed RunnerConfig (server, providers, daytona, callback) parsed and validated once before the HTTP server listens, with one redacted startup summary. Every provider-selection and Daytona read now flows through the typed object instead of scattered process.env reads.

Rename map (old -> new):
  SANDBOX_AGENT_PROVIDER    -> AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER (+ AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS)
  SANDBOX_AGENT_LOG_LEVEL   -> AGENTA_RUNNER_LOG_LEVEL
  DAYTONA_API_KEY/API_URL/TARGET/SNAPSHOT/IMAGE (runner) -> AGENTA_RUNNER_DAYTONA_*
  DAYTONA_AUTOSTOP/AUTODELETE -> AGENTA_RUNNER_DAYTONA_AUTOSTOP_MINUTES/_AUTODELETE_MINUTES

Removed (no aliases): DAYTONA_SNAPSHOT_AGENT, AGENTA_AGENT_SANDBOX_PI_INSTALLED (replaced by probe + pinned-install repair), AGENTA_SESSION_HARNESS_MOUNTS (transcript mounts derive from the session contract), AGENTA_SANDBOX_LOCAL_ALLOWED (replaced by the shared enabled/default provider pair the API/services/SDK read with the same rules).

Validation: unset enabled = local; empty list invalid; unknown/duplicate ids invalid; default must be enabled; daytona enabled without a credential fails; snapshot+image mutually exclusive; invalid lifecycle values fail; empty strings become absent at the parse boundary. The code evaluator's bare DAYTONA_* namespace is deliberately untouched.

Claude-Session: https://claude.ai/code/session_01XhENr63WL9npkKrJGnzDc1
The API, OSS agent service, SDK, and web now read AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS / AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER with the same parsing rules as the runner, replacing the removed AGENTA_SANDBOX_LOCAL_ALLOWED gate.

- api/oss/src/utils/env.py: RunnerConfig exposes enabled_sandbox_providers + default_sandbox_provider, validated with the runner's rules (shared semantics, reimplemented in Python).
- Producer-side gates (services/oss select_backend, SDK handler) refuse any sandbox not in the enabled set; the runner stays the final authority.
- web/entrypoint.sh derives NEXT_PUBLIC_AGENTA_SANDBOX_LOCAL_ENABLED from the enabled-provider list.
- Tests: API parser unit cases mirroring the runner's; services + SDK gate cases updated.

Claude-Session: https://claude.ai/code/session_01XhENr63WL9npkKrJGnzDc1
All hosting surfaces now set the runner from the canonical AGENTA_RUNNER_* names, with one operator-facing entry feeding both the api and runner services.

- Compose (7 files): runner blocks carry the enabled/default provider pair (default local) and AGENTA_RUNNER_DAYTONA_* (AUTOSTOP/AUTODELETE in minutes); DAYTONA_SNAPSHOT_AGENT and AGENTA_AGENT_SANDBOX_PI_INSTALLED removed.
- Env examples (4 files): AGENTA_SANDBOX_LOCAL_ALLOWED replaced by the provider pair; the shared daytona block split into a code-evaluator group (bare DAYTONA_*) and an agent-runner group (AGENTA_RUNNER_DAYTONA_*).
- Helm: agentRunner.providers.{enabled,default,daytona.*} per the design; the runner deployment renders the enabled list comma-joined and reads the Daytona key from providers.daytona.apiKeySecretRef; commonEnv exposes the provider pair to api/web/services and drops sandboxLocalAllowed. The code-evaluator daytona block stays bare and untouched.
- Railway: runner set_vars/set_optional_vars use the new names, and the pre-existing bug where unset_vars deleted the runner's DAYTONA_* credentials right after setting them is fixed by removing those names from the unset list.

Claude-Session: https://claude.ai/code/session_01XhENr63WL9npkKrJGnzDc1
The snapshot recipe installs pi globally, but the probe checked only the pinned .agenta-pi path, so the recipe's own snapshot would have paid a session-time reinstall on every run. The repair ladder now links a PATH-baked pi to the pinned path before falling back to the pinned install. Snapshot recipe docs updated to the canonical names.

Claude-Session: https://claude.ai/code/session_01XhENr63WL9npkKrJGnzDc1

@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: 4

🧹 Nitpick comments (4)
services/runner/src/engines/sandbox_agent.ts (1)

1-1: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Centralize "effective sandbox provider" resolution; verify ?? vs || don't disagree on empty-string input.

Five sites independently resolve "which provider does this request use, given request.sandbox and the configured default," with two different fallback operators and an unverified single-argument call path:

  • services/runner/src/engines/sandbox_agent.ts#L713-719: uses (request.sandbox ?? loadRunnerConfig().providers.default) === "daytona" for the durable-cwd path decision. Align this operator with run-plan.ts's resolution (or extract a shared helper both call), so an empty-string request.sandbox can't make this branch disagree with plan.isDaytona.
  • services/runner/src/engines/sandbox_agent.ts#L662-668: calls single-arg resolvesToLocalProvider(request.sandbox) to gate the local multi-runner ownership check; confirm this still falls back to loadRunnerConfig().providers.default (not a hardcoded "local") when request.sandbox is omitted.
  • services/runner/src/server.ts#L301-306: same single-arg resolvesToLocalProvider(request.sandbox) call, plus its own ??-based fallback for the daytona branch — same two concerns apply here.
  • services/runner/src/engines/sandbox_agent/run-plan.ts#L258-286: uses request.sandbox || defaultProvider || "local" (falsy-based ||), the one authoritative resolution plan.isDaytona is derived from; the other sites should match this exactly rather than reimplement it.
  • services/runner/tests/unit/session-pool.test.ts#L30-44: add a test case for the single-argument call pattern (resolvesToLocalProvider(request.sandbox), no second arg) to lock in the behavior the unchanged production call sites actually rely on.

Since resolvesToLocalProvider's implementation (likely in session-pool.ts) isn't in the provided files, please verify its default-argument behavior directly.

♻️ Suggested direction: single shared resolver
// services/runner/src/config/runner-config.ts (or a small shared helper)
export function resolveEffectiveProvider(
  requestSandbox: string | undefined,
  config: RunnerConfig = loadRunnerConfig(),
): SandboxProviderId {
  return (requestSandbox?.trim() || config.providers.default) as SandboxProviderId;
}

Then replace the ??/||/resolvesToLocalProvider(x) call sites above with this single function so all decisions agree.

services/runner/tests/unit/sandbox-agent-orchestration.test.ts (1)

26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated "enable Daytona for tests" beforeEach into a shared helper.

The same 4-line block (AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS="local,daytona" + AGENTA_RUNNER_DAYTONA_API_KEY="test-key" + resetRunnerConfigCache()) is duplicated verbatim across three suites. A shared helper (e.g. exported from hermetic-env.ts) would keep this in one place if the env names or reset semantics change again.

  • services/runner/tests/unit/sandbox-agent-orchestration.test.ts#L26-L34: replace the inline beforeEach body with a call to the shared helper.
  • services/runner/tests/unit/sandbox-agent-run-plan.test.ts#L16-L27: replace the inline beforeEach body with a call to the shared helper.
  • services/runner/tests/unit/sandbox-lifecycle.test.ts#L16-L24: replace the inline beforeEach body with a call to the shared helper.
♻️ Proposed shared helper
// services/runner/tests/setup/hermetic-env.ts (or a new tests/setup/daytona-env.ts)
export function enableDaytonaForTests(): void {
  process.env.AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS = "local,daytona";
  process.env.AGENTA_RUNNER_DAYTONA_API_KEY = "test-key";
  resetRunnerConfigCache();
}
-beforeEach(() => {
-  process.env.AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS = "local,daytona";
-  process.env.AGENTA_RUNNER_DAYTONA_API_KEY = "test-key";
-  resetRunnerConfigCache();
-});
+beforeEach(enableDaytonaForTests);
hosting/kubernetes/helm/templates/runner-deployment.yaml (1)

53-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Provider-registry env vars computed twice from the same Values path. Both sites independently derive AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS/AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER from .Values.agentRunner.providers with duplicated defaulting logic (default (list "local") / default "local"), instead of the runner deployment reusing the helper that _helpers.tpl explicitly documents as the shared, "one operator entry" definition. Values match today, but any future change to one copy's defaulting/validation rule risks silent drift between what api/web/services observe as "enabled providers" and what the runner itself enforces.

  • hosting/kubernetes/helm/templates/runner-deployment.yaml#L53-L56: replace the local $enabledProviders/$providers.default computation with a call to a shared named template (e.g. extract _helpers.tpl lines 1100-1105 into agenta.agentRunner.providers.enabledEnv/.defaultEnv helpers) so both files stay in sync.
  • hosting/kubernetes/helm/templates/_helpers.tpl#L1100-L1105: extract this block into a reusable named template that both this shared-services block and runner-deployment.yaml call, instead of each computing the values inline.
hosting/railway/oss/scripts/configure.sh (1)

305-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale deprecated SANDBOX_AGENT_PROVIDER (and any prior legacy runner vars) not cleaned up.

The old SANDBOX_AGENT_PROVIDER default was removed from set_vars runner here, but it's absent from unset_vars runner (Line 326), so pre-existing Railway environments keep this now-unread variable indefinitely. Consider adding it (and any other renamed/removed runner variable) to the unset_vars runner call for cleanup, now that it's excluded from the Daytona-credential caveat.

♻️ Proposed cleanup
-    unset_vars runner AGENTA_LICENSE SCRIPT_NAME REDIS_URI REDIS_URI_VOLATILE REDIS_URI_DURABLE SUPERTOKENS_CONNECTION_URI POSTGRES_URI_CORE POSTGRES_URI_TRACING POSTGRES_URI_SUPERTOKENS
+    unset_vars runner AGENTA_LICENSE SCRIPT_NAME REDIS_URI REDIS_URI_VOLATILE REDIS_URI_DURABLE SUPERTOKENS_CONNECTION_URI POSTGRES_URI_CORE POSTGRES_URI_TRACING POSTGRES_URI_SUPERTOKENS SANDBOX_AGENT_PROVIDER

Also applies to: 324-326


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fa106185-9bd2-4630-8407-e284ee25caf2

📥 Commits

Reviewing files that changed from the base of the PR and between 65fefb8 and 8e15fab.

📒 Files selected for processing (44)
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/unit/utils/test_env_runner_config.py
  • hosting/docker-compose/ee/docker-compose.dev.yml
  • hosting/docker-compose/ee/docker-compose.gh.local.yml
  • hosting/docker-compose/ee/docker-compose.gh.yml
  • hosting/docker-compose/ee/env.ee.dev.example
  • hosting/docker-compose/ee/env.ee.gh.example
  • hosting/docker-compose/oss/docker-compose.dev.yml
  • hosting/docker-compose/oss/docker-compose.gh.local.yml
  • hosting/docker-compose/oss/docker-compose.gh.ssl.yml
  • hosting/docker-compose/oss/docker-compose.gh.yml
  • hosting/docker-compose/oss/env.oss.dev.example
  • hosting/docker-compose/oss/env.oss.gh.example
  • hosting/kubernetes/helm/templates/_helpers.tpl
  • hosting/kubernetes/helm/templates/runner-deployment.yaml
  • hosting/railway/oss/scripts/configure.sh
  • sdks/python/agenta/sdk/agents/errors.py
  • sdks/python/agenta/sdk/agents/handler.py
  • sdks/python/agenta/sdk/agents/sandbox_providers.py
  • sdks/python/oss/tests/pytest/unit/agents/test_agent_composition_seam.py
  • services/oss/src/agent/app.py
  • services/oss/src/agent/config.py
  • services/oss/tests/pytest/unit/agent/test_select_backend.py
  • services/runner/sandbox-images/daytona/README.md
  • services/runner/sandbox-images/daytona/build_snapshot.py
  • services/runner/src/config/runner-config.ts
  • services/runner/src/engines/sandbox_agent.ts
  • services/runner/src/engines/sandbox_agent/daemon.ts
  • services/runner/src/engines/sandbox_agent/daytona-provider.ts
  • services/runner/src/engines/sandbox_agent/daytona.ts
  • services/runner/src/engines/sandbox_agent/provider.ts
  • services/runner/src/engines/sandbox_agent/run-plan.ts
  • services/runner/src/engines/sandbox_agent/session-pool.ts
  • services/runner/src/server.ts
  • services/runner/tests/setup/hermetic-env.ts
  • services/runner/tests/unit/runner-config.test.ts
  • services/runner/tests/unit/sandbox-agent-daemon.test.ts
  • services/runner/tests/unit/sandbox-agent-daytona.test.ts
  • services/runner/tests/unit/sandbox-agent-orchestration.test.ts
  • services/runner/tests/unit/sandbox-agent-provider.test.ts
  • services/runner/tests/unit/sandbox-agent-run-plan.test.ts
  • services/runner/tests/unit/sandbox-lifecycle.test.ts
  • services/runner/tests/unit/session-pool.test.ts
  • web/entrypoint.sh

Comment thread api/oss/src/utils/env.py
Comment on lines +1001 to +1028
if provider_id not in _KNOWN_SANDBOX_PROVIDERS:
raise ValueError(
f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists unknown provider "
f"'{provider_id}'; known: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
)
if provider_id in seen:
raise ValueError(
f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists provider '{provider_id}' "
f"more than once."
)
seen.add(provider_id)
return ids


def _parse_default_sandbox_provider(raw: Optional[str], enabled: List[str]) -> str:
"""Parse ``AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER``; unset -> ``local``; must be enabled."""
value = (raw or "").strip().lower() or "local"
if value not in _KNOWN_SANDBOX_PROVIDERS:
raise ValueError(
f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER is unknown provider '{value}'; "
f"known: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
)
if value not in enabled:
raise ValueError(
f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER '{value}' is not in the enabled set "
f"[{', '.join(enabled)}]."
)
return value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Error text has drifted from the SDK/runner's "in-lockstep" implementations.

The docstring above (lines 981-984) states this reimplements the runner's parser "so all three readers agree," but two messages have drifted from the SDK's sandbox_providers.py and the runner's runner-config.ts (both in this cohort/context): the unknown-provider message says "known:" here vs. "known providers:" there, and the default-not-in-enabled-set message here drops the "Add it to AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS or change the default." remediation sentence present in both other implementations. Operators debugging via the API get a less actionable error than via the runner.

🐛 Proposed fix to re-sync message text
         if provider_id not in _KNOWN_SANDBOX_PROVIDERS:
             raise ValueError(
                 f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists unknown provider "
-                f"'{provider_id}'; known: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
+                f"'{provider_id}'; known providers: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
             )
     if value not in enabled:
         raise ValueError(
             f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER '{value}' is not in the enabled set "
-            f"[{', '.join(enabled)}]."
+            f"[{', '.join(enabled)}]. Add it to AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS "
+            f"or change the default."
         )
📝 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.

Suggested change
if provider_id not in _KNOWN_SANDBOX_PROVIDERS:
raise ValueError(
f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists unknown provider "
f"'{provider_id}'; known: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
)
if provider_id in seen:
raise ValueError(
f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists provider '{provider_id}' "
f"more than once."
)
seen.add(provider_id)
return ids
def _parse_default_sandbox_provider(raw: Optional[str], enabled: List[str]) -> str:
"""Parse ``AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER``; unset -> ``local``; must be enabled."""
value = (raw or "").strip().lower() or "local"
if value not in _KNOWN_SANDBOX_PROVIDERS:
raise ValueError(
f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER is unknown provider '{value}'; "
f"known: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
)
if value not in enabled:
raise ValueError(
f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER '{value}' is not in the enabled set "
f"[{', '.join(enabled)}]."
)
return value
if provider_id not in _KNOWN_SANDBOX_PROVIDERS:
raise ValueError(
f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists unknown provider "
f"'{provider_id}'; known providers: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
)
if provider_id in seen:
raise ValueError(
f"AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS lists provider '{provider_id}' "
f"more than once."
)
seen.add(provider_id)
return ids
def _parse_default_sandbox_provider(raw: Optional[str], enabled: List[str]) -> str:
"""Parse ``AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER``; unset -> ``local``; must be enabled."""
value = (raw or "").strip().lower() or "local"
if value not in _KNOWN_SANDBOX_PROVIDERS:
raise ValueError(
f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER is unknown provider '{value}'; "
f"known providers: {', '.join(_KNOWN_SANDBOX_PROVIDERS)}."
)
if value not in enabled:
raise ValueError(
f"AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER '{value}' is not in the enabled set "
f"[{', '.join(enabled)}]. Add it to AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS "
f"or change the default."
)
return value

Comment thread api/oss/src/utils/env.py
Comment on lines +1052 to 1072
enabled_sandbox_providers: List[str] = Field(
default_factory=_enabled_sandbox_providers_default
)
default_sandbox_provider: str = Field(
default_factory=_default_sandbox_provider_default
)

model_config = ConfigDict(extra="ignore")

@model_validator(mode="after")
def _warn_local_sandbox(self) -> "RunnerConfig":
global _SANDBOX_LOCAL_WARNED
if self.sandbox_local_allowed and not _SANDBOX_LOCAL_WARNED:
if "local" in self.enabled_sandbox_providers and not _SANDBOX_LOCAL_WARNED:
_SANDBOX_LOCAL_WARNED = True
warnings.warn(
"AGENTA_SANDBOX_LOCAL_ALLOWED is on (default): local sandbox is not a "
"tenant boundary. Set it to false to harden a shared/multi-tenant deployment.",
"local sandbox is enabled (default): it is unconfined host bash, not a "
"tenant boundary. Set AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS=daytona to "
"harden a shared/multi-tenant deployment.",
stacklevel=2,
)
return self

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section with line numbers and nearby context.
sed -n '980,1125p' api/oss/src/utils/env.py

# Find parseRunnerConfig and related helpers in the same file.
rg -n "parseRunnerConfig|_enabled_sandbox_providers_default|_default_sandbox_provider_default|RunnerConfig|field_validator|model_validator" api/oss/src/utils/env.py

Repository: Agenta-AI/agenta

Length of output: 7137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== env.py relevant excerpt =="
sed -n '980,1125p' api/oss/src/utils/env.py

echo
echo "== search for RunnerConfig usages =="
rg -n "RunnerConfig\(" -S .

echo
echo "== search for parseRunnerConfig definition/usages =="
rg -n "parseRunnerConfig" -S api/oss/src/utils/env.py .

Repository: Agenta-AI/agenta

Length of output: 9305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from typing import List, Optional
from pydantic import BaseModel, Field, model_validator

KNOWN = {"local", "daytona"}

def parse_enabled(raw: Optional[str]) -> List[str]:
    if raw is None:
        return ["local"]
    return [x.strip().lower() for x in raw.split(",")]

def parse_default(raw: Optional[str], enabled: List[str]) -> str:
    value = (raw or "").strip().lower() or "local"
    if value not in enabled:
        raise ValueError(f"{value} not enabled")
    return value

def enabled_default() -> List[str]:
    return parse_enabled(None)

def default_default() -> str:
    return parse_default(None, enabled_default())

class M(BaseModel):
    enabled_sandbox_providers: List[str] = Field(default_factory=enabled_default)
    default_sandbox_provider: str = Field(default_factory=default_default)

    `@model_validator`(mode="after")
    def warn(self):
        return self

print("M() ->", M())
print("M(enabled_sandbox_providers=['daytona'], default_sandbox_provider='daytona') ->",
      M(enabled_sandbox_providers=['daytona'], default_sandbox_provider='daytona'))
try:
    print(M(enabled_sandbox_providers=['not-a-real-provider'], default_sandbox_provider='local'))
except Exception as e:
    print(type(e).__name__, e)
try:
    print(M(enabled_sandbox_providers=['daytona'], default_sandbox_provider='local'))
except Exception as e:
    print(type(e).__name__, e)
PY

Repository: Agenta-AI/agenta

Length of output: 537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from typing import List, Optional
from pydantic import BaseModel, Field, model_validator

def parse_enabled(raw: Optional[str]) -> List[str]:
    if raw is None:
        return ["local"]
    return [x.strip().lower() for x in raw.split(",")]

def parse_default(raw: Optional[str], enabled: List[str]) -> str:
    value = (raw or "").strip().lower() or "local"
    if value not in enabled:
        raise ValueError(f"{value} not enabled")
    return value

def enabled_default() -> List[str]:
    return parse_enabled(None)

def default_default() -> str:
    return parse_default(None, enabled_default())

class M(BaseModel):
    enabled_sandbox_providers: List[str] = Field(default_factory=enabled_default)
    default_sandbox_provider: str = Field(default_factory=default_default)

    `@model_validator`(mode="after")
    def warn(self):
        return self

print("M() ->", M())
print("M(enabled_sandbox_providers=['daytona'], default_sandbox_provider='daytona') ->",
      M(enabled_sandbox_providers=['daytona'], default_sandbox_provider='daytona'))
for kwargs in [
    dict(enabled_sandbox_providers=['not-a-real-provider'], default_sandbox_provider='local'),
    dict(enabled_sandbox_providers=['daytona'], default_sandbox_provider='local'),
]:
    try:
        print("TRY", kwargs, "=>", M(**kwargs))
    except Exception as e:
        print("ERR", kwargs, "=>", type(e).__name__, e)
PY

Repository: Agenta-AI/agenta

Length of output: 723


Move the sandbox-provider checks onto the model itself RunnerConfig(enabled_sandbox_providers=...) currently accepts invalid values because the parsing/consistency logic only runs through default_factory. Put the provider parsing and the enabled/default subset check in a field_validator or model_validator(mode="after") so the invariant holds for every construction path.

Comment on lines +2 to +4
{{- $providers := default dict $runner.providers -}}
{{- $enabledProviders := default (list "local") $providers.enabled -}}
{{- $daytona := default dict $providers.daytona -}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify values.yaml uses the new agentRunner.providers.daytona path (and not the old agentRunner.daytona)
fd -a values.yaml hosting/kubernetes/helm --exec cat -n {}
rg -n 'agentRunner' hosting/kubernetes/helm/values.yaml -A3 -B1

Repository: Agenta-AI/agenta

Length of output: 8074


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the chart files around agentRunner and schema/docs references.
fd -a 'values.schema.json|README.md|*.md|*.yaml' hosting/kubernetes/helm | sed -n '1,200p'

echo '--- values.schema.json ---'
fd -a values.schema.json hosting/kubernetes/helm --exec sh -c 'echo "### {}"; rg -n "agentRunner|daytona|providers" "{}" -A4 -B2' \;

echo '--- docs / upgrade notes mentioning daytona or agentRunner ---'
rg -n 'agentRunner\.daytona|agentRunner\.providers\.daytona|daytona|upgrade|migration' hosting/kubernetes/helm -g '*.md' -g '*.yaml' -g '*.yml' -A2 -B2

Repository: Agenta-AI/agenta

Length of output: 503


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- chart file list ---'
fd -a --type f --hidden --glob 'values.schema.json' --glob '*.md' --glob '*.yaml' --glob '*.yml' hosting/kubernetes/helm | sed -n '1,200p'

echo '--- values.schema.json ---'
fd -a --type f --hidden --glob 'values.schema.json' hosting/kubernetes/helm -x sh -c '
  echo "### {}"
  rg -n "agentRunner|daytona|providers" "{}" -A6 -B3
'

echo '--- markdown/yaml references ---'
rg -n "agentRunner\.daytona|agentRunner\.providers\.daytona|daytona|provider[s]?" hosting/kubernetes/helm -g '*.md' -g '*.yaml' -g '*.yml' -A2 -B2

Repository: Agenta-AI/agenta

Length of output: 13711


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any migration/upgrade note that mentions the agentRunner daytona path change.
rg -n 'agentRunner\.providers\.daytona|agentRunner\.daytona|providers\.daytona|daytona' \
  . \
  -g '*.md' -g '*.yaml' -g '*.yml' -g 'Chart.yaml' \
  -A2 -B2

Repository: Agenta-AI/agenta

Length of output: 50375


Keep the Daytona values path consistent
runner-deployment.yaml now reads agentRunner.providers.daytona, but values.yaml and values.schema.json still expose agentRunner.daytona. Existing overrides at the old path will be ignored on upgrade; update the chart defaults/schema (and upgrade notes) together, or add a compatibility shim.

Comment on lines 78 to 89
def _default_select_backend(agent_template: AgentTemplate) -> Backend:
"""Env-driven default: `AGENTA_RUNNER_INTERNAL_URL` picks HTTP transport; else local cwd.

`local` (unconfined host bash, not a tenant boundary) is refused unless
`AGENTA_SANDBOX_LOCAL_ALLOWED` is on — a bare `agent_v0` gets this protocol-level
safety behavior for free, same as capability gating and MCP gating above.
A sandbox the deployment has not enabled is refused before any run — a bare `agent_v0`
gets this protocol-level safety behavior for free, same as capability gating and MCP
gating above. The runner remains the final authority; this is a pre-filter that reads the
same `AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS` registry.
"""
if agent_template.sandbox == "local" and not _sandbox_local_allowed():
raise LocalSandboxNotAllowedError()
if not sandbox_provider_enabled(agent_template.sandbox):
raise LocalSandboxNotAllowedError(sandbox=agent_template.sandbox)
url = os.getenv("AGENTA_RUNNER_INTERNAL_URL", "").strip() or None
return SandboxAgentBackend(sandbox=agent_template.sandbox, url=url, cwd=os.getcwd())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether sandbox_provider_enabled is defined/imported anywhere relevant.
rg -nP '\bsandbox_provider_enabled\b' sdks/python/agenta -C3

Repository: Agenta-AI/agenta

Length of output: 1455


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' sdks/python/agenta/sdk/agents/handler.py

Repository: Agenta-AI/agenta

Length of output: 5082


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "sandbox_provider_enabled|from agenta\.sdk\.agents\.sandbox_providers import|import .*sandbox_providers" sdks/python/agenta/sdk/agents/handler.py sdks/python/agenta/sdk/agents -C2

Repository: Agenta-AI/agenta

Length of output: 1621


Missing import for sandbox_provider_enabled
_default_select_backend() calls sandbox_provider_enabled(...), but sdks/python/agenta/sdk/agents/handler.py doesn’t import it. That raises NameError whenever the default backend is selected.

🧰 Tools
🪛 Flake8 (7.3.0)

[error] 86-86: undefined name 'sandbox_provider_enabled'

(F821)

🪛 GitHub Actions: 11 - check code styling / 1_Python lint.txt

[error] 86-87: ruff (F821) Undefined name sandbox_provider_enabled

🪛 GitHub Actions: 11 - check code styling / Python lint

[error] 86-86: ruff (F821) Undefined name sandbox_provider_enabled.

Sources: Coding guidelines, Linters/SAST tools

@mmabrouk
mmabrouk force-pushed the fix/runner-snapshot-qa branch from 65fefb8 to 90f26c7 Compare July 14, 2026 00:04
@mmabrouk
mmabrouk force-pushed the refactor/runner-config-names branch from 8e15fab to d57bd38 Compare July 14, 2026 00:04
@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 Jul 14, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

Landed the Helm values.yaml + values.schema.json runner-provider rename on this lane (commit d57bd380f1).

Reconciled against the now-merged MCP contract base (#5290, base e0bbc2cd11): this commit makes no enableMcp change — the stale working-tree copies re-added it, so I rewrote from the lane baseline and re-applied only the runner-provider rename. The diff is runner-config-only:

  • agentRunner.providers.{enabled,default} + daytona.{apiKeySecretRef,apiUrl,target,snapshot,autostopMinutes,autodeleteMinutes,sessionIdleTtlMs,sessionMaxWarm}
  • removed agenta.sandboxLocalAllowed from the schema (per interface.md §9 removed-variables)

Validation: helm lint 0 failed; helm template renders AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS (7×); values.schema.json valid JSON; enableMcp count 0 on both files.

The runner gate rename referenced sandbox_provider_enabled without importing it (F821), crashing the sandbox pre-filter and failing SDK lint + unit tests.

Claude-Session: https://claude.ai/code/session_01XhENr63WL9npkKrJGnzDc1
@mmabrouk
mmabrouk changed the base branch from fix/runner-snapshot-qa to big-agents July 14, 2026 01:56
@mmabrouk
mmabrouk merged commit 1248efb into big-agents Jul 14, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactoring A code change that neither fixes a bug nor adds a feature size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant